Allow passing AAR archives to D8.

Bug: 132131664
Bug: 132422247
Change-Id: Ifa9cdc3f51e0924c3494a287b0e1936d7c5a600c
diff --git a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
new file mode 100644
index 0000000..7de79dc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import static com.android.tools.r8.utils.FileUtils.isArchive;
+
+import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+public class AarArchiveResourceProvider implements ProgramResourceProvider {
+
+  private final Origin origin;
+  private final Path archive;
+
+  public static AarArchiveResourceProvider fromArchive(Path archive) {
+    return new AarArchiveResourceProvider(archive);
+  }
+
+  AarArchiveResourceProvider(Path archive) {
+    assert isArchive(archive);
+    origin = new ArchiveEntryOrigin("classes.jar", new PathOrigin(archive));
+    this.archive = archive;
+  }
+
+  private List<ProgramResource> readClassesJar(ZipInputStream stream) throws IOException {
+    ZipEntry entry;
+    List<ProgramResource> resources = new ArrayList<>();
+    while (null != (entry = stream.getNextEntry())) {
+      String name = entry.getName();
+      if (ZipUtils.isClassFile(name)) {
+        Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
+        String descriptor = DescriptorUtils.guessTypeDescriptor(name);
+        ProgramResource resource =
+            OneShotByteResource.create(
+                Kind.CF,
+                entryOrigin,
+                ByteStreams.toByteArray(stream),
+                Collections.singleton(descriptor));
+        resources.add(resource);
+      }
+    }
+    return resources;
+  }
+
+  private List<ProgramResource> readArchive() throws IOException {
+    List<ProgramResource> classResources = null;
+    try (ZipFile zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8)) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        try (InputStream stream = zipFile.getInputStream(entry)) {
+          String name = entry.getName();
+          if (name.equals("classes.jar")) {
+            try (ZipInputStream classesStream = new ZipInputStream(stream)) {
+              classResources = readClassesJar(classesStream);
+            }
+            break;
+          }
+        }
+      }
+    } catch (ZipException e) {
+      throw new CompilationError("Zip error while reading '" + archive + "': " + e.getMessage(), e);
+    }
+    return classResources == null ? Collections.emptyList() : classResources;
+  }
+
+  @Override
+  public Collection<ProgramResource> getProgramResources() throws ResourceException {
+    try {
+      return readArchive();
+    } catch (IOException e) {
+      throw new ResourceException(origin, e);
+    }
+  }
+
+  @Override
+  public DataResourceProvider getDataResourceProvider() {
+    return null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 4c15524..f420261 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.FileUtils.isAarFile;
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
@@ -711,6 +712,8 @@
         addProgramResources(ProgramResource.fromFile(Kind.DEX, file));
       } else if (isClassFile(file)) {
         addProgramResources(ProgramResource.fromFile(Kind.CF, file));
+      } else if (isAarFile(file)) {
+        addProgramResourceProvider(AarArchiveResourceProvider.fromArchive(file));
       } else if (isArchive(file)) {
         addProgramResourceProvider(ArchiveResourceProvider.fromArchive(file, ignoreDexInArchive));
       } else {
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 3f1f26e..87b8506 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -18,6 +18,7 @@
 
 public class FileUtils {
 
+  public static final String AAR_EXTENSION = ".aar";
   public static final String APK_EXTENSION = ".apk";
   public static final String CLASS_EXTENSION = ".class";
   public static final String DEX_EXTENSION = ".dex";
@@ -61,11 +62,17 @@
     return name.endsWith(APK_EXTENSION);
   }
 
+  public static boolean isAarFile(Path path) {
+    String name = path.getFileName().toString().toLowerCase();
+    return name.endsWith(AAR_EXTENSION);
+  }
+
   public static boolean isArchive(Path path) {
     String name = path.getFileName().toString().toLowerCase();
     return name.endsWith(APK_EXTENSION)
         || name.endsWith(JAR_EXTENSION)
-        || name.endsWith(ZIP_EXTENSION);
+        || name.endsWith(ZIP_EXTENSION)
+        || name.endsWith(AAR_EXTENSION);
   }
 
   public static String readTextFile(Path file, Charset charset) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/files/AarInputTest.java b/src/test/java/com/android/tools/r8/files/AarInputTest.java
new file mode 100644
index 0000000..17c86eb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/files/AarInputTest.java
@@ -0,0 +1,71 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.files;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.ZipUtils;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class AarInputTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public AarInputTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private Path buildAar() throws Exception {
+    Path aar = temp.newFolder().toPath().resolve("out.aar");
+    Path classesJar = temp.newFolder().toPath().resolve("classes.jar");
+    writeClassesToJar(classesJar, Collections.singletonList(TestClass.class));
+    try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(aar))) {
+      ZipUtils.writeToZipStream(
+          stream, "classes.jar", Files.readAllBytes(classesJar), ZipEntry.DEFLATED);
+    }
+    return aar;
+  }
+
+  @Test
+  public void allowAarProgramInputD8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addProgramClasses(TestClass.class)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutputLines("Hello!");
+    } else {
+      testForD8()
+          .addProgramFiles(buildAar())
+          .setMinApi(parameters.getApiLevel())
+          .compile()
+          .inspect(inspector -> assertThat(inspector.clazz(TestClass.class), isPresent()))
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutputLines("Hello!");
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello!");
+    }
+  }
+}