Version 1.6.48

Cherry pick: Revert changes to JasminBuilder
CL: https://r8-review.googlesource.com/c/r8/+/45191

Cherry pick: Add support for dumping to a directory and continuing compilation.
CL: https://r8-review.googlesource.com/c/r8/+/45175

Cherry pick: Add tests and support for data resources to the dump file.
CL: https://r8-review.googlesource.com/c/r8/+/45167

Cherry pick: Add duplicate entries to the dump test.
CL: https://r8-review.googlesource.com/c/r8/+/45163

Cherry pick: Add support for reading a dump file.
CL: https://r8-review.googlesource.com/c/r8/+/45162

Cherry pick: Move dump utilities into AndroidApp.
CL: https://r8-review.googlesource.com/c/r8/+/45101

Change-Id: Ia22756fe323429b2275122ce7bc2f5055ab95550
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index e374f53..26b8f8e 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.6.47";
+  public static final String LABEL = "1.6.48";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 6e95cac..122e1c8 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -7,17 +7,15 @@
 import static com.android.tools.r8.graph.ClassKind.LIBRARY;
 import static com.android.tools.r8.graph.ClassKind.PROGRAM;
 import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
-import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
 
 import com.android.tools.r8.ClassFileResourceProvider;
-import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.Diagnostic;
 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.StringResource;
-import com.android.tools.r8.Version;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexApplication;
@@ -43,31 +41,21 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
-import com.android.tools.r8.utils.ZipUtils;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closer;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
-import java.nio.file.OpenOption;
+import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Queue;
-import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.stream.Collectors;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-import org.objectweb.asm.ClassVisitor;
 
 public class ApplicationReader {
 
@@ -122,9 +110,30 @@
       ProgramClassConflictResolver resolver)
       throws IOException, ExecutionException {
     assert verifyMainDexOptionsCompatible(inputApp, options);
+    Path dumpOutput = null;
+    boolean cleanDump = false;
     if (options.dumpInputToFile != null) {
-      dumpInputToFile();
-      throw options.reporter.fatalError("Dumped compilation inputs to: " + options.dumpInputToFile);
+      dumpOutput = Paths.get(options.dumpInputToFile);
+    } else if (options.dumpInputToDirectory != null) {
+      dumpOutput =
+          Paths.get(options.dumpInputToDirectory).resolve("dump" + System.nanoTime() + ".zip");
+    } else if (options.testing.dumpAll) {
+      cleanDump = true;
+      dumpOutput = Paths.get("/tmp").resolve("dump" + System.nanoTime() + ".zip");
+    }
+    if (dumpOutput != null) {
+      timing.begin("ApplicationReader.dump");
+      dumpInputToFile(inputApp, dumpOutput, options);
+      if (cleanDump) {
+        Files.delete(dumpOutput);
+      }
+      timing.end();
+      Diagnostic message = new StringDiagnostic("Dumped compilation inputs to: " + dumpOutput);
+      if (options.dumpInputToFile != null) {
+        throw options.reporter.fatalError(message);
+      } else if (!cleanDump) {
+        options.reporter.info(message);
+      }
     }
     timing.begin("DexApplication.read");
     final LazyLoadedDexApplication.Builder builder =
@@ -160,141 +169,8 @@
     return builder.build();
   }
 
-  private void dumpInputToFile() throws IOException  {
-    try {
-      List<ProgramResourceProvider> programResourceProviders =
-          inputApp.getProgramResourceProviders();
-      Set<DataEntryResource> dataEntryResources = inputApp.getDataEntryResourcesForTesting();
-      List<ProgramResource> programResourcesWithDescriptors = new ArrayList<>();
-      for (ProgramResourceProvider programResourceProvider : programResourceProviders) {
-        addProgramResourcesWithDescriptor(
-            programResourcesWithDescriptors, programResourceProvider.getProgramResources());
-      }
-
-      List<ProgramResource> libraryProgramResourcesWithDescriptors =
-          getProgramResourcesWithDescriptors(inputApp.getLibraryResourceProviders());
-
-      List<ProgramResource> classpathProgramResourcesWithDescriptors =
-          getProgramResourcesWithDescriptors(inputApp.getClasspathResourceProviders());
-
-      OpenOption[] openOptions =
-          new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
-      try (Closer closer = Closer.create()) {
-        try (ZipOutputStream out =
-            new ZipOutputStream(
-                Files.newOutputStream(Paths.get(options.dumpInputToFile), openOptions))) {
-          writeToZip(
-              dataEntryResources, programResourcesWithDescriptors, closer, out, "program.jar");
-          writeToZip(
-              ImmutableSet.of(),
-              libraryProgramResourcesWithDescriptors,
-              closer,
-              out,
-              "library.jar");
-          writeToZip(
-              ImmutableSet.of(),
-              classpathProgramResourcesWithDescriptors,
-              closer,
-              out,
-              "classpath.jar");
-          if (options.hasProguardConfiguration()) {
-            String proguardConfig = options.getProguardConfiguration().getParsedConfiguration();
-            ZipUtils.writeToZipStream(
-                out, "proguard.config", proguardConfig.getBytes(), ZipEntry.DEFLATED);
-          }
-
-          ZipUtils.writeToZipStream(
-              out, "r8-version", Version.getVersionString().getBytes(), ZipEntry.DEFLATED);
-        }
-      }
-    } catch (ResourceException e) {
-      options.reporter.fatalError("Failed to write input:" + e.getMessage());
-    }
-  }
-
-  private static void writeToZip(
-      Set<DataEntryResource> dataEntryResources,
-      List<ProgramResource> programResourcesWithDescriptors,
-      Closer closer,
-      ZipOutputStream out,
-      String entry)
-      throws IOException, ResourceException {
-    try (ByteArrayOutputStream programByteStream = new ByteArrayOutputStream()) {
-      try (ZipOutputStream archiveOutputStream = new ZipOutputStream(programByteStream)) {
-        ZipUtils.writeResourcesToZip(
-            programResourcesWithDescriptors, dataEntryResources, closer, archiveOutputStream);
-      }
-      ZipUtils.writeToZipStream(out, entry, programByteStream.toByteArray(), ZipEntry.DEFLATED);
-    }
-  }
-
-  private static List<ProgramResource> getProgramResourcesWithDescriptors(
-      List<ClassFileResourceProvider> classFileResourceProviders)
-      throws IOException, ResourceException {
-    ArrayList<ProgramResource> programResourcesWithDescriptors = new ArrayList<>();
-    for (ClassFileResourceProvider libraryResourceProvider : classFileResourceProviders) {
-      List<ProgramResource> programResources = new ArrayList<>();
-      for (String classDescriptor : libraryResourceProvider.getClassDescriptors()) {
-        programResources.add(libraryResourceProvider.getProgramResource(classDescriptor));
-      }
-      addProgramResourcesWithDescriptor(programResourcesWithDescriptors, programResources);
-    }
-    return programResourcesWithDescriptors;
-  }
-
-  private static void addProgramResourcesWithDescriptor(
-      List<ProgramResource> programResourcesWithDescriptors,
-      Collection<ProgramResource> programResources)
-      throws IOException, ResourceException {
-    for (ProgramResource programResource : programResources) {
-      if (programResource.getKind() != Kind.CF) {
-        continue;
-      }
-      try (InputStream inputStream = programResource.getByteStream()) {
-        byte[] bytes = ByteStreams.toByteArray(inputStream);
-        String descriptor = extractClassInternalType(bytes);
-        programResourcesWithDescriptors.add(
-            ProgramResource.fromBytes(
-                programResource.getOrigin(),
-                programResource.getKind(),
-                bytes,
-                ImmutableSet.of(descriptor)));
-      }
-    }
-  }
-
-  private static String extractClassInternalType(byte[] bytes) throws IOException {
-    class ClassNameExtractor extends ClassVisitor {
-      private String className;
-
-      private ClassNameExtractor() {
-        super(ASM_VERSION);
-      }
-
-      @Override
-      public void visit(
-          int version,
-          int access,
-          String name,
-          String signature,
-          String superName,
-          String[] interfaces) {
-        className = name;
-      }
-
-      String getDescriptor() {
-        return "L" + className + ";";
-      }
-    }
-
-    org.objectweb.asm.ClassReader reader = new org.objectweb.asm.ClassReader(bytes);
-    ClassNameExtractor extractor = new ClassNameExtractor();
-    reader.accept(
-        extractor,
-        org.objectweb.asm.ClassReader.SKIP_CODE
-            | org.objectweb.asm.ClassReader.SKIP_DEBUG
-            | org.objectweb.asm.ClassReader.SKIP_FRAMES);
-    return extractor.getDescriptor();
+  private static void dumpInputToFile(AndroidApp app, Path output, InternalOptions options) {
+    app.dump(output, options.getProguardConfiguration(), options.reporter);
   }
 
   private static boolean verifyMainDexOptionsCompatible(
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 1c3af7a..c4a37c2 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -7,6 +7,8 @@
 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;
+import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
+import static com.android.tools.r8.utils.ZipUtils.writeToZipStream;
 
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.ClassFileResourceProvider;
@@ -25,28 +27,44 @@
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.StringResource;
+import com.android.tools.r8.Version;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.shaking.FilteredClassPath;
+import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.ByteStreams;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
 import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.function.Consumer;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassVisitor;
 
 /**
  * Collection of program files needed for processing.
@@ -55,6 +73,12 @@
  */
 public class AndroidApp {
 
+  private static final String dumpVersionFileName = "r8-version";
+  private static final String dumpProgramFileName = "program.jar";
+  private static final String dumpClasspathFileName = "classpath.jar";
+  private static final String dumpLibraryFileName = "library.jar";
+  private static final String dumpConfigFileName = "proguard.config";
+
   private final ImmutableList<ProgramResourceProvider> programResourceProviders;
   private final ImmutableMap<Resource, String> programResourcesMainDescriptor;
   private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
@@ -382,6 +406,173 @@
     return programResourcesMainDescriptor.get(resource);
   }
 
+  public void dump(Path output, ProguardConfiguration configuration, Reporter reporter) {
+    int nextDexIndex = 0;
+    OpenOption[] openOptions =
+        new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+    try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(output, openOptions))) {
+      writeToZipStream(
+          out, dumpVersionFileName, Version.getVersionString().getBytes(), ZipEntry.DEFLATED);
+      if (configuration != null) {
+        String proguardConfig = configuration.getParsedConfiguration();
+        writeToZipStream(out, dumpConfigFileName, proguardConfig.getBytes(), ZipEntry.DEFLATED);
+      }
+      nextDexIndex = dumpProgramResources(dumpProgramFileName, nextDexIndex, out);
+      nextDexIndex = dumpClasspathResources(nextDexIndex, out);
+      nextDexIndex = dumpLibraryResources(nextDexIndex, out);
+    } catch (IOException | ResourceException e) {
+      reporter.fatalError(new StringDiagnostic("Failed to dump inputs"), e);
+    }
+  }
+
+  private int dumpLibraryResources(int nextDexIndex, ZipOutputStream out)
+      throws IOException, ResourceException {
+    nextDexIndex =
+        dumpClassFileResources(dumpLibraryFileName, nextDexIndex, out, libraryResourceProviders);
+    return nextDexIndex;
+  }
+
+  private int dumpClasspathResources(int nextDexIndex, ZipOutputStream out)
+      throws IOException, ResourceException {
+    nextDexIndex =
+        dumpClassFileResources(
+            dumpClasspathFileName, nextDexIndex, out, classpathResourceProviders);
+    return nextDexIndex;
+  }
+
+  private static ClassFileResourceProvider createClassFileResourceProvider(
+      Map<String, ProgramResource> classPathResources) {
+    return new ClassFileResourceProvider() {
+      @Override
+      public Set<String> getClassDescriptors() {
+        return classPathResources.keySet();
+      }
+
+      @Override
+      public ProgramResource getProgramResource(String descriptor) {
+        return classPathResources.get(descriptor);
+      }
+    };
+  }
+
+  private Consumer<ProgramResource> createClassFileResourceConsumer(
+      Map<String, ProgramResource> classPathResources) {
+    return programResource -> {
+      assert programResource.getClassDescriptors().size() == 1;
+      String descriptor = programResource.getClassDescriptors().iterator().next();
+      classPathResources.put(descriptor, programResource);
+    };
+  }
+
+  private int dumpProgramResources(String archiveName, int nextDexIndex, ZipOutputStream out)
+      throws IOException, ResourceException {
+    try (ByteArrayOutputStream archiveByteStream = new ByteArrayOutputStream()) {
+      try (ZipOutputStream archiveOutputStream = new ZipOutputStream(archiveByteStream)) {
+        Object2IntMap<String> seen = new Object2IntOpenHashMap<>();
+        Set<DataEntryResource> dataEntries = getDataEntryResourcesForTesting();
+        for (DataEntryResource dataResource : dataEntries) {
+          String entryName = dataResource.getName();
+          try (InputStream dataStream = dataResource.getByteStream()) {
+            byte[] bytes = ByteStreams.toByteArray(dataStream);
+            writeToZipStream(archiveOutputStream, entryName, bytes, ZipEntry.DEFLATED);
+          }
+        }
+        for (ProgramResourceProvider provider : programResourceProviders) {
+          for (ProgramResource programResource : provider.getProgramResources()) {
+            nextDexIndex =
+                dumpProgramResource(seen, nextDexIndex, archiveOutputStream, programResource);
+          }
+        }
+      }
+      writeToZipStream(out, archiveName, archiveByteStream.toByteArray(), ZipEntry.DEFLATED);
+    }
+    return nextDexIndex;
+  }
+
+  private static int dumpClassFileResources(
+      String archiveName,
+      int nextDexIndex,
+      ZipOutputStream out,
+      ImmutableList<ClassFileResourceProvider> classpathResourceProviders)
+      throws IOException, ResourceException {
+    try (ByteArrayOutputStream archiveByteStream = new ByteArrayOutputStream()) {
+      try (ZipOutputStream archiveOutputStream = new ZipOutputStream(archiveByteStream)) {
+        Object2IntMap<String> seen = new Object2IntOpenHashMap<>();
+        for (ClassFileResourceProvider provider : classpathResourceProviders) {
+          for (String descriptor : provider.getClassDescriptors()) {
+            ProgramResource programResource = provider.getProgramResource(descriptor);
+            int oldDexIndex = nextDexIndex;
+            nextDexIndex =
+                dumpProgramResource(seen, nextDexIndex, archiveOutputStream, programResource);
+            assert nextDexIndex == oldDexIndex;
+          }
+        }
+      }
+      writeToZipStream(out, archiveName, archiveByteStream.toByteArray(), ZipEntry.DEFLATED);
+    }
+    return nextDexIndex;
+  }
+
+  private static int dumpProgramResource(
+      Object2IntMap<String> seen,
+      int nextDexIndex,
+      ZipOutputStream archiveOutputStream,
+      ProgramResource programResource)
+      throws ResourceException, IOException {
+    byte[] bytes = ByteStreams.toByteArray(programResource.getByteStream());
+    String entryName;
+    if (programResource.getKind() == Kind.CF) {
+      Set<String> classDescriptors = programResource.getClassDescriptors();
+      String classDescriptor =
+          (classDescriptors == null || classDescriptors.size() != 1)
+              ? extractClassDescriptor(bytes)
+              : classDescriptors.iterator().next();
+      String classFileName = DescriptorUtils.getClassFileName(classDescriptor);
+      int dupCount = seen.getOrDefault(classDescriptor, 0);
+      seen.put(classDescriptor, dupCount + 1);
+      entryName = dupCount == 0 ? classFileName : (classFileName + "." + dupCount + ".dup");
+    } else {
+      assert programResource.getKind() == Kind.DEX;
+      entryName = "classes" + nextDexIndex++ + ".dex";
+    }
+    writeToZipStream(archiveOutputStream, entryName, bytes, ZipEntry.DEFLATED);
+    return nextDexIndex;
+  }
+
+  private static String extractClassDescriptor(byte[] bytes) {
+    class ClassNameExtractor extends ClassVisitor {
+      private String className;
+
+      private ClassNameExtractor() {
+        super(ASM_VERSION);
+      }
+
+      @Override
+      public void visit(
+          int version,
+          int access,
+          String name,
+          String signature,
+          String superName,
+          String[] interfaces) {
+        className = name;
+      }
+
+      String getDescriptor() {
+        return "L" + className + ";";
+      }
+    }
+
+    org.objectweb.asm.ClassReader reader = new org.objectweb.asm.ClassReader(bytes);
+    ClassNameExtractor extractor = new ClassNameExtractor();
+    reader.accept(
+        extractor,
+        org.objectweb.asm.ClassReader.SKIP_CODE
+            | org.objectweb.asm.ClassReader.SKIP_DEBUG
+            | org.objectweb.asm.ClassReader.SKIP_FRAMES);
+    return extractor.getDescriptor();
+  }
+
   /**
    * Builder interface for constructing an AndroidApp.
    */
@@ -424,6 +615,118 @@
       return reporter;
     }
 
+    public Builder addDump(Path dumpFile) throws IOException {
+      System.out.println("Reading dump from file: " + dumpFile);
+      Origin origin = new PathOrigin(dumpFile);
+      ZipUtils.iter(
+          dumpFile.toString(),
+          (entry, input) -> {
+            String name = entry.getName();
+            if (name.equals(dumpVersionFileName)) {
+              String content = new String(ByteStreams.toByteArray(input), StandardCharsets.UTF_8);
+              System.out.println("Dump produced by R8 version: " + content);
+            } else if (name.equals(dumpProgramFileName)) {
+              readProgramDump(origin, input);
+            } else if (name.equals(dumpClasspathFileName)) {
+              readClassFileDump(origin, input, this::addClasspathResourceProvider, "classpath");
+            } else if (name.equals(dumpLibraryFileName)) {
+              readClassFileDump(origin, input, this::addLibraryResourceProvider, "library");
+            } else {
+              System.out.println("WARNING: Unexpected dump file entry: " + entry.getName());
+            }
+          });
+      return this;
+    }
+
+    private void readClassFileDump(
+        Origin origin,
+        InputStream input,
+        Consumer<ClassFileResourceProvider> addProvider,
+        String inputType)
+        throws IOException {
+      Map<String, ProgramResource> resources = new HashMap<>();
+      try (ZipInputStream stream = new ZipInputStream(input)) {
+        ZipEntry entry;
+        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.put(descriptor, resource);
+          } else if (name.endsWith(".dup")) {
+            System.out.println("WARNING: Duplicate " + inputType + " resource: " + name);
+          } else {
+            System.out.println("WARNING: Unexpected " + inputType + " resource: " + name);
+          }
+        }
+      }
+      if (!resources.isEmpty()) {
+        addProvider.accept(createClassFileResourceProvider(resources));
+      }
+    }
+
+    private void readProgramDump(Origin origin, InputStream input) throws IOException {
+      List<ProgramResource> programResources = new ArrayList<>();
+      List<DataEntryResource> dataResources = new ArrayList<>();
+      try (ZipInputStream stream = new ZipInputStream(input)) {
+        ZipEntry entry;
+        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));
+            programResources.add(resource);
+          } else if (ZipUtils.isDexFile(name)) {
+            Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
+            ProgramResource resource =
+                OneShotByteResource.create(
+                    Kind.DEX, entryOrigin, ByteStreams.toByteArray(stream), null);
+            programResources.add(resource);
+          } else if (name.endsWith(".dup")) {
+            System.out.println("WARNING: Duplicate program resource: " + name);
+          } else {
+            dataResources.add(
+                DataEntryResource.fromBytes(ByteStreams.toByteArray(stream), name, origin));
+          }
+        }
+      }
+      if (!programResources.isEmpty() || !dataResources.isEmpty()) {
+        addProgramResourceProvider(
+            new ProgramResourceProvider() {
+              @Override
+              public Collection<ProgramResource> getProgramResources() throws ResourceException {
+                return programResources;
+              }
+
+              @Override
+              public DataResourceProvider getDataResourceProvider() {
+                return dataResources.isEmpty()
+                    ? null
+                    : new DataResourceProvider() {
+                      @Override
+                      public void accept(Visitor visitor) throws ResourceException {
+                        for (DataEntryResource dataResource : dataResources) {
+                          visitor.visit(dataResource);
+                        }
+                      }
+                    };
+              }
+            });
+      }
+    }
+
     /** Add program file resources. */
     public Builder addProgramFiles(Path... files) {
       return addProgramFiles(Arrays.asList(files));
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 8e93b77..218baef 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -174,6 +174,8 @@
   public boolean printMemory = System.getProperty("com.android.tools.r8.printmemory") != null;
 
   public String dumpInputToFile = System.getProperty("com.android.tools.r8.dumpinputtofile");
+  public String dumpInputToDirectory =
+      System.getProperty("com.android.tools.r8.dumpinputtodirectory");
 
   // Flag to toggle if DEX code objects should pass-through without IR processing.
   public boolean passthroughDexCode = false;
@@ -965,6 +967,10 @@
     // Flag to turn on/off JDK11+ nest-access control even when not required (Cf backend)
     public boolean enableForceNestBasedAccessDesugaringForTest = false;
 
+    // Force each call of application read to dump its inputs to a file, which is subsequently
+    // deleted. Useful to check that our dump functionality does not cause compilation failure.
+    public boolean dumpAll = false;
+
     public boolean desugarLambdasThroughLensCodeRewriter() {
       return enableStatefulLambdaCreateInstanceMethod;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java b/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
index da958c3..1dfbc64 100644
--- a/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
+++ b/src/main/java/com/android/tools/r8/utils/OneShotByteResource.java
@@ -17,7 +17,7 @@
   private byte[] bytes;
   private final Set<String> classDescriptors;
 
-  static ProgramResource create(
+  public static OneShotByteResource create(
       Kind kind, Origin origin, byte[] bytes, Set<String> classDescriptors) {
     return new OneShotByteResource(origin, kind, bytes, classDescriptors);
   }
diff --git a/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java b/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java
new file mode 100644
index 0000000..04c4bf2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java
@@ -0,0 +1,183 @@
+// 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.DataResourceProvider.Visitor;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.Reporter;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class AndroidAppDumpsTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public AndroidAppDumpsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    Reporter reporter = new Reporter();
+
+    String dataResourceName = "my-resource.bin";
+    byte[] dataResourceData = new byte[] {1, 2, 3};
+
+    Path dexForB =
+        testForD8().addProgramClasses(B.class).setMinApi(AndroidApiLevel.B).compile().writeToZip();
+
+    AndroidApp appIn =
+        AndroidApp.builder(reporter)
+            .addClassProgramData(ToolHelper.getClassAsBytes(A.class), origin("A"))
+            .addClassProgramData(ToolHelper.getClassAsBytes(A.class), origin("A"))
+            .addClassProgramData(ToolHelper.getClassAsBytes(A.class), origin("A"))
+            .addProgramFile(dexForB)
+            .addClasspathResourceProvider(provider(B.class))
+            .addClasspathResourceProvider(provider(B.class))
+            .addClasspathResourceProvider(provider(B.class))
+            .addLibraryResourceProvider(provider(C.class))
+            .addLibraryResourceProvider(provider(C.class))
+            .addLibraryResourceProvider(provider(C.class))
+            .addProgramResourceProvider(
+                createDataResourceProvider(
+                    dataResourceName, dataResourceData, origin(dataResourceName)))
+            .build();
+
+    Path dumpFile = temp.newFolder().toPath().resolve("dump.zip");
+    appIn.dump(dumpFile, null, reporter);
+
+    AndroidApp appOut = AndroidApp.builder(reporter).addDump(dumpFile).build();
+    assertEquals(1, appOut.getClassProgramResourcesForTesting().size());
+    assertEquals(
+        DescriptorUtils.javaTypeToDescriptor(A.class.getTypeName()),
+        appOut.getClassProgramResourcesForTesting().get(0).getClassDescriptors().iterator().next());
+
+    assertEquals(1, appOut.getDexProgramResourcesForTesting().size());
+
+    assertEquals(1, appOut.getClasspathResourceProviders().size());
+    assertEquals(
+        DescriptorUtils.javaTypeToDescriptor(B.class.getTypeName()),
+        appOut.getClasspathResourceProviders().get(0).getClassDescriptors().iterator().next());
+
+    assertEquals(1, appOut.getLibraryResourceProviders().size());
+    assertEquals(
+        DescriptorUtils.javaTypeToDescriptor(C.class.getTypeName()),
+        appOut.getLibraryResourceProviders().get(0).getClassDescriptors().iterator().next());
+
+    Box<Boolean> foundData = new Box<>(false);
+    for (ProgramResourceProvider provider : appOut.getProgramResourceProviders()) {
+      DataResourceProvider dataProvider = provider.getDataResourceProvider();
+      if (dataProvider != null) {
+        dataProvider.accept(
+            new Visitor() {
+              @Override
+              public void visit(DataDirectoryResource directory) {}
+
+              @Override
+              public void visit(DataEntryResource file) {
+                if (file.getName().equals(dataResourceName)) {
+                  foundData.set(true);
+                }
+              }
+            });
+      }
+    }
+    assertTrue(foundData.get());
+  }
+
+  private ProgramResourceProvider createDataResourceProvider(
+      String name, byte[] content, Origin origin) {
+    return new ProgramResourceProvider() {
+      @Override
+      public Collection<ProgramResource> getProgramResources() throws ResourceException {
+        return Collections.emptyList();
+      }
+
+      @Override
+      public DataResourceProvider getDataResourceProvider() {
+        return new DataResourceProvider() {
+          @Override
+          public void accept(Visitor visitor) throws ResourceException {
+            visitor.visit(
+                new DataEntryResource() {
+                  @Override
+                  public InputStream getByteStream() throws ResourceException {
+                    return new ByteArrayInputStream(content);
+                  }
+
+                  @Override
+                  public String getName() {
+                    return name;
+                  }
+
+                  @Override
+                  public Origin getOrigin() {
+                    return origin;
+                  }
+                });
+          }
+        };
+      }
+    };
+  }
+
+  private Origin origin(String name) {
+    return new Origin(Origin.root()) {
+      @Override
+      public String part() {
+        return name;
+      }
+    };
+  }
+
+  private ClassFileResourceProvider provider(Class<?> clazz) {
+    return new ClassFileResourceProvider() {
+      @Override
+      public Set<String> getClassDescriptors() {
+        return Collections.singleton(DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()));
+      }
+
+      @Override
+      public ProgramResource getProgramResource(String descriptor) {
+        try {
+          return ProgramResource.fromBytes(
+              origin(clazz.getTypeName()),
+              Kind.CF,
+              ToolHelper.getClassAsBytes(clazz),
+              getClassDescriptors());
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    };
+  }
+
+  public static class A {}
+
+  public static class B {}
+
+  public static class C {}
+}
diff --git a/src/test/java/com/android/tools/r8/DumpInputsTest.java b/src/test/java/com/android/tools/r8/DumpInputsTest.java
new file mode 100644
index 0000000..b8b3a04
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/DumpInputsTest.java
@@ -0,0 +1,109 @@
+// 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;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DumpInputsTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntime(CfVm.JDK9).build();
+  }
+
+  public DumpInputsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testDumpToFile() throws Exception {
+    Path dump = temp.newFolder().toPath().resolve("dump.zip");
+    try {
+      testForExternalR8(parameters.getBackend())
+          .useExternalJDK(parameters.getRuntime().asCf().getVm())
+          .addJvmFlag("-Dcom.android.tools.r8.dumpinputtofile=" + dump)
+          .addProgramClasses(TestClass.class)
+          .compile();
+    } catch (AssertionError e) {
+      verifyDump(dump, false, true);
+      return;
+    }
+    fail("Expected external compilation to exit");
+  }
+
+  @Test
+  public void testDumpToDirectory() throws Exception {
+    Path dumpDir = temp.newFolder().toPath();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        // Setting a directory will allow compilation to continue.
+        // Ensure the compilation and run can actually succeed.
+        .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
+        .addKeepMainRule(TestClass.class)
+        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
+    assertTrue(Files.isDirectory(dumpDir));
+    List<Path> paths = Files.walk(dumpDir, 1).collect(Collectors.toList());
+    boolean hasVerified = false;
+    for (Path path : paths) {
+      if (!path.equals(dumpDir)) {
+        // The non-external run here results in assert code calling application read.
+        verifyDump(path, false, false);
+        hasVerified = true;
+      }
+    }
+    assertTrue(hasVerified);
+  }
+
+  private void verifyDump(Path dumpFile, boolean hasClasspath, boolean hasProguardConfig)
+      throws IOException {
+    assertTrue(Files.exists(dumpFile));
+    Path unzipped = temp.newFolder().toPath();
+    ZipUtils.unzip(dumpFile.toString(), unzipped.toFile());
+    assertTrue(Files.exists(unzipped.resolve("r8-version")));
+    assertTrue(Files.exists(unzipped.resolve("program.jar")));
+    assertTrue(Files.exists(unzipped.resolve("library.jar")));
+    if (hasClasspath) {
+      assertTrue(Files.exists(unzipped.resolve("classpath.jar")));
+    }
+    if (hasProguardConfig) {
+      assertTrue(Files.exists(unzipped.resolve("proguard.config")));
+    }
+    Set<String> entries = new HashSet<>();
+    ZipUtils.iter(
+        unzipped.resolve("program.jar").toString(), (entry, input) -> entries.add(entry.getName()));
+    assertTrue(
+        entries.contains(
+            DescriptorUtils.getClassFileName(
+                DescriptorUtils.javaTypeToDescriptor(TestClass.class.getTypeName()))));
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
index 0328184..ae4a36c 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -57,6 +57,8 @@
 
   private boolean addR8ExternalDeps = false;
 
+  private List<String> jvmFlags = new ArrayList<>();
+
   private ExternalR8TestBuilder(TestState state, Builder builder, Backend backend) {
     super(state, builder, backend);
   }
@@ -72,7 +74,7 @@
 
   public ExternalR8TestBuilder useExternalJDK(CfVm externalJDK) {
     this.externalJDK = externalJDK;
-    return this;
+    return self();
   }
 
   private String getJDKToRun() {
@@ -82,6 +84,11 @@
     return getJavaExecutable(externalJDK);
   }
 
+  public ExternalR8TestBuilder addJvmFlag(String flag) {
+    jvmFlags.add(flag);
+    return self();
+  }
+
   @Override
   ExternalR8TestCompileResult internalCompile(
       Builder builder, Consumer<InternalOptions> optionsConsumer, Supplier<AndroidApp> app)
@@ -99,9 +106,12 @@
               : r8jar.toAbsolutePath().toString();
 
       List<String> command = new ArrayList<>();
+      Collections.addAll(command, getJDKToRun());
+
+      command.addAll(jvmFlags);
+
       Collections.addAll(
           command,
-          getJDKToRun(),
           "-ea",
           "-cp",
           classPath,
diff --git a/src/test/java/com/android/tools/r8/R8EntryPointTests.java b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
index 553ff0e..0bbc7bc 100644
--- a/src/test/java/com/android/tools/r8/R8EntryPointTests.java
+++ b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
@@ -9,20 +9,13 @@
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.ZipUtils;
-import com.android.tools.r8.utils.ZipUtils.OnEntryHandler;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
-import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.zip.ZipEntry;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -180,53 +173,6 @@
   }
 
   @Test
-  public void testDumpInputs() throws IOException {
-    Path out = temp.newFile("dex.zip").toPath();
-    Path dump = temp.newFile("dump.zip").toPath();
-    ProcessResult r8 =
-        ToolHelper.forkR8WithJavaOptions(
-            Paths.get("."),
-            ImmutableList.of("-Dcom.android.tools.r8.dumpinputtofile=" + dump.toString()),
-            "--lib",
-            ToolHelper.getDefaultAndroidJar().toString(),
-            "--dex",
-            "--output",
-            out.toString(),
-            "--pg-conf",
-            PROGUARD_FLAGS.toString(),
-            "--pg-conf",
-            testFlags.toString(),
-            INPUT_JAR.toString());
-
-    List<ZipEntry> entries = new ArrayList<>();
-    ZipUtils.iter(
-        dump.toString(),
-        new OnEntryHandler() {
-          @Override
-          public void onEntry(ZipEntry entry, InputStream input) throws IOException {
-            entries.add(entry);
-          }
-        });
-    Assert.assertTrue(hasEntry(entries, "program.jar"));
-    Assert.assertTrue(hasEntry(entries, "library.jar"));
-    Assert.assertTrue(hasEntry(entries, "classpath.jar"));
-    Assert.assertTrue(hasEntry(entries, "proguard.config"));
-    Assert.assertTrue(hasEntry(entries, "r8-version"));
-    // When dumping the inputs we throw an error in the program to exit early.
-    Assert.assertNotEquals(0, r8.exitCode);
-  }
-
-  private boolean hasEntry(Collection<ZipEntry> entries, String name) {
-    for (ZipEntry entry : entries) {
-      if (entry.getName().equals(name)) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  @Test
   public void testSpecifyDexAndClassfileNotAllowed() throws IOException, InterruptedException {
     Path out = temp.newFile("dex.zip").toPath();
     ProcessResult r8 =
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index 99b2271..26f2030 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -98,9 +98,7 @@
     }
 
     private ClassBuilder(String name, String superName) {
-      this.name = name;
-      this.superName = superName;
-      this.interfaces = ImmutableList.of();
+      this(name , superName, new String[0]);
     }
 
     private ClassBuilder(String name, String superName, String... interfaces) {