API for supporting resources

Change-Id: I4cdb2bb64986b72d4bf1d49c939c95520e40b8b7
diff --git a/build.gradle b/build.gradle
index c38343b..94c002f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -866,6 +866,7 @@
 // We change the options passed to javac to ignore it.
 compileExamplesJava.options.compilerArgs = ["-Xlint:none"]
 
+
 task buildExampleJars {
     dependsOn downloadProguard
     def examplesDir = file("src/test/examples")
@@ -884,7 +885,16 @@
         into "$buildDir/runtime/examples/"
     }
 
-    task "compile_examples"(type: JavaCompile, dependsOn: "generateExamplesProto") {
+    task "copy_examples_resources"(type: org.gradle.api.tasks.Copy) {
+        from examplesDir
+        exclude "**/*.java"
+        exclude "**/keep-rules*.txt"
+        into file("build/test/examples/classes")
+    }
+
+    task "compile_examples"(type: JavaCompile) {
+        dependsOn "generateExamplesProto"
+        dependsOn "copy_examples_resources"
         source examplesDir, protoSourceDir
         include "**/*.java"
         destinationDir = file("build/test/examples/classes")
@@ -976,9 +986,9 @@
                 archiveName = "${name}.jar"
                 destinationDir = exampleOutputDir
                 from "build/test/examples/classes"
-                include name + "/**/*.class"
+                include name + "/**/*"
                 with runtimeDependencies
-                includeEmptyDirs false
+                includeEmptyDirs true
             }
             task "jar_example_${name}_debuginfo_all"(type: Jar, dependsOn: "compile_examples_debuginfo_all") {
                 archiveName = "${name}_debuginfo_all.jar"
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 6c43c7b..1bf77b1 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -232,27 +232,28 @@
       assert outputMode != null;
       this.outputPath = outputPath;
       this.outputMode = outputMode;
-      programConsumer = createProgramOutputConsumer(outputPath, outputMode);
+      programConsumer = createProgramOutputConsumer(outputPath, outputMode, false);
       return self();
     }
 
-    private InternalProgramOutputPathConsumer createProgramOutputConsumer(
+    protected InternalProgramOutputPathConsumer createProgramOutputConsumer(
         Path path,
-        OutputMode mode) {
+        OutputMode mode,
+        boolean consumeDataResources) {
       if (mode == OutputMode.DexIndexed) {
         return FileUtils.isArchive(path)
-            ? new DexIndexedConsumer.ArchiveConsumer(path)
-            : new DexIndexedConsumer.DirectoryConsumer(path);
+            ? new DexIndexedConsumer.ArchiveConsumer(path, consumeDataResources)
+            : new DexIndexedConsumer.DirectoryConsumer(path, consumeDataResources);
       }
       if (mode == OutputMode.DexFilePerClassFile) {
         return FileUtils.isArchive(path)
-            ? new DexFilePerClassFileConsumer.ArchiveConsumer(path)
-            : new DexFilePerClassFileConsumer.DirectoryConsumer(path);
+            ? new DexFilePerClassFileConsumer.ArchiveConsumer(path, consumeDataResources)
+            : new DexFilePerClassFileConsumer.DirectoryConsumer(path, consumeDataResources);
       }
       if (mode == OutputMode.ClassFile) {
         return FileUtils.isArchive(path)
-            ? new ClassFileConsumer.ArchiveConsumer(path)
-            : new ClassFileConsumer.DirectoryConsumer(path);
+            ? new ClassFileConsumer.ArchiveConsumer(path, consumeDataResources)
+            : new ClassFileConsumer.DirectoryConsumer(path, consumeDataResources);
       }
       throw new Unreachable("Unexpected output mode: " + mode);
     }
diff --git a/src/main/java/com/android/tools/r8/ClassFileConsumer.java b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
index eab3d66..0394441 100644
--- a/src/main/java/com/android/tools/r8/ClassFileConsumer.java
+++ b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
@@ -5,11 +5,11 @@
 
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ArchiveBuilder;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.DirectoryBuilder;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
+import com.android.tools.r8.utils.OutputBuilder;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
@@ -61,6 +61,11 @@
     }
 
     @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumer != null ? consumer.getDataResourceConsumer() : null;
+    }
+
+    @Override
     public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
       if (consumer != null) {
         consumer.accept(data, descriptor, handler);
@@ -73,75 +78,70 @@
         consumer.finished(handler);
       }
     }
-
   }
 
-  /** Archive consumer to write program resources to a zip archive. */
-  class ArchiveConsumer extends ForwardingConsumer implements InternalProgramOutputPathConsumer {
-
-    private final Path archive;
-    private final Origin origin;
-    private ZipOutputStream stream = null;
-    private boolean closed = false;
+  /** Consumer to write program resources to an output. */
+  class ArchiveConsumer extends ForwardingConsumer
+      implements DataResourceConsumer, InternalProgramOutputPathConsumer {
+    private final OutputBuilder outputBuilder;
+    protected final boolean consumeDataResources;
 
     public ArchiveConsumer(Path archive) {
-      this(archive, null);
+      this(archive, null, false);
+    }
+
+    public ArchiveConsumer(Path archive, boolean consumeDataResouces) {
+      this(archive, null, consumeDataResouces);
     }
 
     public ArchiveConsumer(Path archive, ClassFileConsumer consumer) {
+      this(archive, consumer, false);
+    }
+
+    public ArchiveConsumer(Path archive, ClassFileConsumer consumer, boolean consumeDataResouces) {
       super(consumer);
-      this.archive = archive;
-      origin = new PathOrigin(archive);
+      this.outputBuilder = new ArchiveBuilder(archive);
+      this.consumeDataResources = consumeDataResouces;
+      this.outputBuilder.open();
+      if (getDataResourceConsumer() != null) {
+        this.outputBuilder.open();
+      }
+    }
+
+    @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumeDataResources ? this : null;
     }
 
     @Override
     public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
       super.accept(data, descriptor, handler);
-      synchronizedWrite(getClassFileName(descriptor), data, handler);
+      outputBuilder.addFile(getClassFileName(descriptor), data, handler);
+    }
+
+    @Override
+    public void accept(DataDirectoryResource directory, DiagnosticsHandler handler) {
+      outputBuilder.addDirectory(directory.getName(), handler);
+    }
+
+    @Override
+    public void accept(DataEntryResource file, DiagnosticsHandler handler) {
+      outputBuilder.addFile(file.getName(), file, handler);
     }
 
     @Override
     public void finished(DiagnosticsHandler handler) {
       super.finished(handler);
-      assert !closed;
-      closed = true;
       try {
-        if (stream != null) {
-          stream.close();
-          stream = null;
-        }
+        outputBuilder.close();
       } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, origin));
+        handler.error(new IOExceptionDiagnostic(e, outputBuilder.getOrigin()));
       }
     }
 
     @Override
     public Path internalGetOutputPath() {
-      return archive;
-    }
-
-    private ZipOutputStream getStream(DiagnosticsHandler handler) {
-      assert !closed;
-      if (stream == null) {
-        try {
-          stream =
-              new ZipOutputStream(
-                  Files.newOutputStream(
-                      archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
-        } catch (IOException e) {
-          handler.error(new IOExceptionDiagnostic(e, origin));
-        }
-      }
-      return stream;
-    }
-
-    private synchronized void synchronizedWrite(
-        String entry, byte[] content, DiagnosticsHandler handler) {
-      try {
-        ZipUtils.writeToZipStream(getStream(handler), entry, content, ZipEntry.STORED);
-      } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, origin));
-      }
+      return outputBuilder.getPath();
     }
 
     private static String getClassFileName(String classDescriptor) {
@@ -169,27 +169,47 @@
 
   /** Directory consumer to write program resources to a directory. */
   class DirectoryConsumer extends ForwardingConsumer implements InternalProgramOutputPathConsumer {
-
-    private final Path directory;
+    private final OutputBuilder outputBuilder;
+    protected final boolean consumeDataResouces;
 
     public DirectoryConsumer(Path directory) {
-      this(directory, null);
+      this(directory, null, false);
+    }
+
+    public DirectoryConsumer(Path directory, boolean consumeDataResouces) {
+      this(directory, null, consumeDataResouces);
     }
 
     public DirectoryConsumer(Path directory, ClassFileConsumer consumer) {
+      this(directory, consumer, false);
+    }
+
+    public DirectoryConsumer(
+        Path directory, ClassFileConsumer consumer, boolean consumeDataResouces) {
       super(consumer);
-      this.directory = directory;
+      this.outputBuilder = new DirectoryBuilder(directory);
+      this.consumeDataResouces = consumeDataResouces;
+    }
+
+    @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumeDataResouces ? this : null;
     }
 
     @Override
     public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
       super.accept(data, descriptor, handler);
-      Path target = directory.resolve(ArchiveConsumer.getClassFileName(descriptor));
-      try {
-        writeFileFromDescriptor(data, target);
-      } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
-      }
+      outputBuilder.addFile(ArchiveConsumer.getClassFileName(descriptor), data, handler);
+    }
+
+    @Override
+    public void accept(DataDirectoryResource directory, DiagnosticsHandler handler) {
+      outputBuilder.addDirectory(directory.getName(), handler);
+    }
+
+    @Override
+    public void accept(DataEntryResource file, DiagnosticsHandler handler) {
+      outputBuilder.addFile(file.getName(), file, handler);
     }
 
     @Override
@@ -199,12 +219,7 @@
 
     @Override
     public Path internalGetOutputPath() {
-      return directory;
-    }
-
-    private static void writeFileFromDescriptor(byte[] contents, Path target) throws IOException {
-      Files.createDirectories(target.getParent());
-      FileUtils.writeToFile(target, null, contents);
+      return outputBuilder.getPath();
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 39bdd96..aa99407 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -181,7 +181,7 @@
       R8.unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
     } finally {
-      options.signalFinishedToProgramConsumer();
+      options.signalFinishedToConsumers();
       // Dump timings.
       if (options.printTimes) {
         timing.report();
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index d14e79b..c300bbf 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -140,24 +139,6 @@
           intermediate,
           isOptimizeMultidexForLinearAlloc());
     }
-
-    private static DexIndexedConsumer createIndexedConsumer(Path path) {
-      if (path == null) {
-        return DexIndexedConsumer.emptyConsumer();
-      }
-      return FileUtils.isArchive(path)
-          ? new DexIndexedConsumer.ArchiveConsumer(path)
-          : new DexIndexedConsumer.DirectoryConsumer(path);
-    }
-
-    private static DexFilePerClassFileConsumer createPerClassFileConsumer(Path path) {
-      if (path == null) {
-        return DexFilePerClassFileConsumer.emptyConsumer();
-      }
-      return FileUtils.isArchive(path)
-          ? new DexFilePerClassFileConsumer.ArchiveConsumer(path)
-          : new DexFilePerClassFileConsumer.DirectoryConsumer(path);
-    }
   }
 
   static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
diff --git a/src/main/java/com/android/tools/r8/DataDirectoryResource.java b/src/main/java/com/android/tools/r8/DataDirectoryResource.java
new file mode 100644
index 0000000..2f421f7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DataDirectoryResource.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public interface DataDirectoryResource extends DataResource {
+
+  static DataDirectoryResource fromFile(Path dir, Path file) {
+    return new LocalDataDirectoryResource(dir.resolve(file).toFile(),
+        file.toString().replace(File.separatorChar, SEPARATOR));
+  }
+
+  static DataDirectoryResource fromZip(ZipFile zip, ZipEntry entry) {
+    return new ZipDataDirectoryResource(zip, entry);
+  }
+
+  class ZipDataDirectoryResource implements DataDirectoryResource {
+    private final ZipFile zip;
+    private final ZipEntry entry;
+
+    private ZipDataDirectoryResource(ZipFile zip, ZipEntry entry) {
+      assert zip != null;
+      assert entry != null;
+      this.zip = zip;
+      this.entry = entry;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return new ArchiveEntryOrigin(entry.getName(), new PathOrigin(Paths.get(zip.getName())));
+    }
+
+    @Override
+    public String getName() {
+      return entry.getName();
+    }
+  }
+
+  class LocalDataDirectoryResource implements DataDirectoryResource {
+    private final File file;
+    private final String relativePath;
+
+    private LocalDataDirectoryResource(File file, String relativePath) {
+      assert file != null;
+      assert relativePath != null;
+      this.file = file;
+      this.relativePath = relativePath;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return new PathOrigin(file.toPath());
+    }
+
+    @Override
+    public String getName() {
+      return relativePath;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/DataEntryResource.java b/src/main/java/com/android/tools/r8/DataEntryResource.java
new file mode 100644
index 0000000..189d5eb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DataEntryResource.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public interface DataEntryResource extends DataResource {
+
+  /** Get the bytes of the data entry resource. */
+  InputStream getByteStream() throws ResourceException;
+
+  static DataEntryResource fromFile(Path dir, Path file) {
+    return new LocalDataEntryResource(dir.resolve(file).toFile(),
+        file.toString().replace(File.separatorChar, SEPARATOR));
+  }
+
+  static DataEntryResource fromZip(ZipFile zip, ZipEntry entry) {
+    return new ZipDataEntryResource(zip, entry);
+  }
+
+  class ZipDataEntryResource implements DataEntryResource {
+    private final ZipFile zip;
+    private final ZipEntry entry;
+
+    private ZipDataEntryResource(ZipFile zip, ZipEntry entry) {
+      assert zip != null;
+      assert entry != null;
+      this.zip = zip;
+      this.entry = entry;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return new ArchiveEntryOrigin(entry.getName(), new PathOrigin(Paths.get(zip.getName())));
+    }
+
+    @Override
+    public String getName() {
+      return entry.getName();
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      try {
+        return zip.getInputStream(entry);
+      } catch (IOException e) {
+        throw new ResourceException(getOrigin(), e);
+      }
+    }
+  }
+
+  class LocalDataEntryResource implements DataEntryResource {
+    private final File file;
+    private final String relativePath;
+
+    private LocalDataEntryResource(File file, String relativePath) {
+      assert file != null;
+      assert relativePath != null;
+      this.file = file;
+      this.relativePath = relativePath;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return new PathOrigin(file.toPath());
+    }
+
+    @Override
+    public String getName() {
+      return relativePath;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      try {
+        return new FileInputStream(file);
+      } catch (IOException e) {
+        throw new ResourceException(getOrigin(), e);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/DataResource.java b/src/main/java/com/android/tools/r8/DataResource.java
new file mode 100644
index 0000000..4609c97
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DataResource.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2018, 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;
+
+public interface DataResource extends Resource {
+  char SEPARATOR = '/';
+
+  String getName();
+}
diff --git a/src/main/java/com/android/tools/r8/DataResourceConsumer.java b/src/main/java/com/android/tools/r8/DataResourceConsumer.java
new file mode 100644
index 0000000..215ca59
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DataResourceConsumer.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, 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;
+
+public interface DataResourceConsumer {
+
+  void accept(DataDirectoryResource directory, DiagnosticsHandler diagnosticsHandler);
+  void accept(DataEntryResource file, DiagnosticsHandler diagnosticsHandler);
+  void finished(DiagnosticsHandler handler);
+
+}
diff --git a/src/main/java/com/android/tools/r8/DataResourceProvider.java b/src/main/java/com/android/tools/r8/DataResourceProvider.java
new file mode 100644
index 0000000..e851f0f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DataResourceProvider.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2018, 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;
+
+public interface DataResourceProvider {
+
+  interface Visitor {
+    void visit(DataDirectoryResource directory);
+    void visit(DataEntryResource file);
+  }
+
+  void accept(Visitor visitor) throws ResourceException;
+
+}
diff --git a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
index 06f6d03..9c76484 100644
--- a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
+++ b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
@@ -98,7 +98,7 @@
         R8.unwrapExecutionException(e);
         throw new AssertionError(e); // unwrapping method should have thrown
       } finally {
-        options.signalFinishedToProgramConsumer();
+        options.signalFinishedToConsumers();
       }
     } finally {
       executor.shutdown();
diff --git a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
index 7f0a927..f93c338 100644
--- a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
+++ b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
@@ -5,11 +5,12 @@
 
 import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
 
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ArchiveBuilder;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.DirectoryBuilder;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
+import com.android.tools.r8.utils.OutputBuilder;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
@@ -68,6 +69,11 @@
     }
 
     @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumer != null ? consumer.getDataResourceConsumer() : null;
+    }
+
+    @Override
     public void accept(
         String primaryClassDescriptor,
         byte[] data,
@@ -84,30 +90,44 @@
         consumer.finished(handler);
       }
     }
-
   }
 
-  /** Archive consumer to write program resources to a zip archive. */
-  class ArchiveConsumer extends ForwardingConsumer implements InternalProgramOutputPathConsumer {
+  /** Consumer to write program resources to an output. */
+  class ArchiveConsumer extends ForwardingConsumer
+      implements DataResourceConsumer, InternalProgramOutputPathConsumer {
+    private final OutputBuilder outputBuilder;
+    protected final boolean consumeDataResources;
 
     private static String getDexFileName(String classDescriptor) {
       assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor);
       return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + DEX_EXTENSION;
     }
 
-    private final Path archive;
-    private final Origin origin;
-    private ZipOutputStream stream = null;
-    private boolean closed = false;
-
     public ArchiveConsumer(Path archive) {
-      this(archive, null);
+      this(archive, null, false);
+    }
+
+    public ArchiveConsumer(Path archive, boolean consumeDataResouces) {
+      this(archive, null, consumeDataResouces);
     }
 
     public ArchiveConsumer(Path archive, DexFilePerClassFileConsumer consumer) {
+      this(archive, consumer, false);
+    }
+
+    public ArchiveConsumer(Path archive, DexFilePerClassFileConsumer consumer, boolean consumeDataResouces) {
       super(consumer);
-      this.archive = archive;
-      origin = new PathOrigin(archive);
+      this.outputBuilder = new ArchiveBuilder(archive);
+      this.consumeDataResources = consumeDataResouces;
+      this.outputBuilder.open();
+      if (getDataResourceConsumer() != null) {
+        this.outputBuilder.open();
+      }
+    }
+
+    @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumeDataResources ? this : null;
     }
 
     @Override
@@ -117,51 +137,32 @@
         Set<String> descriptors,
         DiagnosticsHandler handler) {
       super.accept(primaryClassDescriptor, data, descriptors, handler);
-      synchronizedWrite(getDexFileName(primaryClassDescriptor), data, handler);
+      outputBuilder.addFile(getDexFileName(primaryClassDescriptor), data, handler);
+    }
+
+    @Override
+    public void accept(DataDirectoryResource directory, DiagnosticsHandler handler) {
+      outputBuilder.addDirectory(directory.getName(), handler);
+    }
+
+    @Override
+    public void accept(DataEntryResource file, DiagnosticsHandler handler) {
+      outputBuilder.addFile(file.getName(), file, handler);
     }
 
     @Override
     public void finished(DiagnosticsHandler handler) {
       super.finished(handler);
-      assert !closed;
-      closed = true;
       try {
-        if (stream != null) {
-          stream.close();
-          stream = null;
-        }
+        outputBuilder.close();
       } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, origin));
+        handler.error(new IOExceptionDiagnostic(e, outputBuilder.getOrigin()));
       }
     }
 
     @Override
     public Path internalGetOutputPath() {
-      return archive;
-    }
-
-    private ZipOutputStream getStream(DiagnosticsHandler handler) {
-      assert !closed;
-      if (stream == null) {
-        try {
-          stream =
-              new ZipOutputStream(
-                  Files.newOutputStream(
-                      archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
-        } catch (IOException e) {
-          handler.error(new IOExceptionDiagnostic(e, origin));
-        }
-      }
-      return stream;
-    }
-
-    private synchronized void synchronizedWrite(
-        String entry, byte[] content, DiagnosticsHandler handler) {
-      try {
-        ZipUtils.writeToZipStream(getStream(handler), entry, content, ZipEntry.STORED);
-      } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, origin));
-      }
+      return outputBuilder.getPath();
     }
 
     public static void writeResources(
@@ -185,17 +186,33 @@
   }
 
   /** Directory consumer to write program resources to a directory. */
-  class DirectoryConsumer extends ForwardingConsumer implements InternalProgramOutputPathConsumer {
+  class DirectoryConsumer extends ForwardingConsumer
+      implements DataResourceConsumer, InternalProgramOutputPathConsumer {
+    private final OutputBuilder outputBuilder;
+    protected final boolean consumeDataResouces;
 
-    private final Path directory;
+    private static String getDexFileName(String classDescriptor) {
+      assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor);
+      return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + DEX_EXTENSION;
+    }
 
     public DirectoryConsumer(Path directory) {
-      this(directory, null);
+      this(directory, null, false);
+    }
+
+    public DirectoryConsumer(Path directory, boolean consumeDataResouces) {
+      this(directory, null, consumeDataResouces);
     }
 
     public DirectoryConsumer(Path directory, DexFilePerClassFileConsumer consumer) {
+      this(directory, consumer, false);
+    }
+
+    public DirectoryConsumer(
+        Path directory, DexFilePerClassFileConsumer consumer, boolean consumeDataResouces) {
       super(consumer);
-      this.directory = directory;
+      this.outputBuilder = new DirectoryBuilder(directory);
+      this.consumeDataResouces = consumeDataResouces;
     }
 
     @Override
@@ -205,14 +222,19 @@
         Set<String> descriptors,
         DiagnosticsHandler handler) {
       super.accept(primaryClassDescriptor, data, descriptors, handler);
-      Path target = getTargetDexFile(directory, primaryClassDescriptor);
-      try {
-        writeFile(data, target);
-      } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
-      }
+      outputBuilder.addFile(getDexFileName(primaryClassDescriptor), data, handler);
     }
 
+
+    @Override
+    public void accept(DataDirectoryResource directory, DiagnosticsHandler handler) {
+      outputBuilder.addDirectory(directory.getName(), handler);
+    }
+
+    @Override
+    public void accept(DataEntryResource file, DiagnosticsHandler handler) {
+      outputBuilder.addFile(file.getName(), file, handler);
+    }
     @Override
     public void finished(DiagnosticsHandler handler) {
       super.finished(handler);
@@ -220,7 +242,7 @@
 
     @Override
     public Path internalGetOutputPath() {
-      return directory;
+      return outputBuilder.getPath();
     }
 
     public static void writeResources(
diff --git a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
index 5c7722b..0f932d5 100644
--- a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
+++ b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
@@ -3,10 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ArchiveBuilder;
+import com.android.tools.r8.utils.DirectoryBuilder;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
+import com.android.tools.r8.utils.OutputBuilder;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
@@ -65,6 +67,21 @@
       this.consumer = consumer;
     }
 
+    protected static String getDefaultDexFileName(int fileIndex) {
+      return fileIndex == 0
+          ? "classes" + FileUtils.DEX_EXTENSION
+          : ("classes" + (fileIndex + 1) + FileUtils.DEX_EXTENSION);
+    }
+
+    protected String getDexFileName(int fileIndex) {
+      return getDefaultDexFileName(fileIndex);
+    }
+
+    @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumer != null ? consumer.getDataResourceConsumer() : null;
+    }
+
     @Override
     public void accept(
         int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
@@ -79,81 +96,65 @@
         consumer.finished(handler);
       }
     }
-
   }
 
-  /** Archive consumer to write program resources to a zip archive. */
-  class ArchiveConsumer extends ForwardingConsumer implements InternalProgramOutputPathConsumer {
-
-    private static String getDefaultDexFileName(int fileIndex) {
-      return fileIndex == 0
-          ? "classes" + FileUtils.DEX_EXTENSION
-          : ("classes" + (fileIndex + 1) + FileUtils.DEX_EXTENSION);
-    }
-
-    private final Path archive;
-    private final Origin origin;
-    private ZipOutputStream stream = null;
-    private boolean closed = false;
+  /** Consumer to write program resources to an output. */
+  class ArchiveConsumer extends ForwardingConsumer
+      implements DataResourceConsumer, InternalProgramOutputPathConsumer {
+    protected final OutputBuilder outputBuilder;
+    protected final boolean consumeDataResources;
 
     public ArchiveConsumer(Path archive) {
-      this(archive, null);
+      this(archive, null, false);
+    }
+
+    public ArchiveConsumer(Path archive, boolean consumeDataResouces) {
+      this(archive, null, consumeDataResouces);
     }
 
     public ArchiveConsumer(Path archive, DexIndexedConsumer consumer) {
-      super(consumer);
-      this.archive = archive;
-      origin = new PathOrigin(archive);
+      this(archive, consumer, false);
     }
 
-    protected String getDexFileName(int fileIndex) {
-      return getDefaultDexFileName(fileIndex);
+    public ArchiveConsumer(Path archive, DexIndexedConsumer consumer, boolean consumeDataResouces) {
+      super(consumer);
+      this.outputBuilder = new ArchiveBuilder(archive);
+      this.consumeDataResources = consumeDataResouces;
+      this.outputBuilder.open();
+      if (getDataResourceConsumer() != null) {
+        this.outputBuilder.open();
+      }
+    }
+
+    @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumeDataResources ? this : null;
     }
 
     @Override
     public void accept(
         int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
       super.accept(fileIndex, data, descriptors, handler);
-      synchronizedWrite(getDexFileName(fileIndex), data, handler);
+      outputBuilder.addFile(getDexFileName(fileIndex), data, handler);
+    }
+
+    @Override
+    public void accept(DataDirectoryResource directory, DiagnosticsHandler handler) {
+      outputBuilder.addDirectory(directory.getName(), handler);
+    }
+
+    @Override
+    public void accept(DataEntryResource file, DiagnosticsHandler handler) {
+      outputBuilder.addFile(file.getName(), file, handler);
     }
 
     @Override
     public void finished(DiagnosticsHandler handler) {
       super.finished(handler);
-      assert !closed;
-      closed = true;
       try {
-        if (stream != null) {
-          stream.close();
-          stream = null;
-        }
+        outputBuilder.close();
       } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, origin));
-      }
-    }
-
-    /** Get or open the zip output stream. */
-    protected synchronized ZipOutputStream getStream(DiagnosticsHandler handler) {
-      assert !closed;
-      if (stream == null) {
-        try {
-          stream =
-              new ZipOutputStream(
-                  Files.newOutputStream(
-                      archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
-        } catch (IOException e) {
-          handler.error(new IOExceptionDiagnostic(e, origin));
-        }
-      }
-      return stream;
-    }
-
-    private synchronized void synchronizedWrite(
-        String entry, byte[] content, DiagnosticsHandler handler) {
-      try {
-        ZipUtils.writeToZipStream(getStream(handler), entry, content, ZipEntry.STORED);
-      } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, origin));
+        handler.error(new IOExceptionDiagnostic(e, outputBuilder.getOrigin()));
       }
     }
 
@@ -175,42 +176,72 @@
 
     @Override
     public Path internalGetOutputPath() {
-      return archive;
+      return outputBuilder.getPath();
     }
-
   }
 
-  /** Directory consumer to write program resources to a directory. */
-  class DirectoryConsumer extends ForwardingConsumer implements InternalProgramOutputPathConsumer {
-
+  class DirectoryConsumer extends ForwardingConsumer
+      implements DataResourceConsumer, InternalProgramOutputPathConsumer {
     private final Path directory;
     private boolean preparedDirectory = false;
+    private final OutputBuilder outputBuilder;
+    protected final boolean consumeDataResouces;
 
     public DirectoryConsumer(Path directory) {
-      this(directory, null);
+      this(directory, null, false);
+    }
+
+    public DirectoryConsumer(Path directory, boolean consumeDataResouces) {
+      this(directory, null, consumeDataResouces);
     }
 
     public DirectoryConsumer(Path directory, DexIndexedConsumer consumer) {
+      this(directory, consumer, false);
+    }
+
+    public DirectoryConsumer(
+        Path directory, DexIndexedConsumer consumer, boolean consumeDataResouces) {
       super(consumer);
       this.directory = directory;
+      this.outputBuilder = new DirectoryBuilder(directory);
+      this.consumeDataResouces = consumeDataResouces;
+    }
+
+    @Override
+    public DataResourceConsumer getDataResourceConsumer() {
+      return consumeDataResouces ? this : null;
     }
 
     @Override
     public void accept(
         int fileIndex, byte[] data, Set<String> descriptors, DiagnosticsHandler handler) {
       super.accept(fileIndex, data, descriptors, handler);
-      Path target = getTargetDexFile(directory, fileIndex);
       try {
         prepareDirectory();
-        writeFile(data, target);
       } catch (IOException e) {
-        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
+        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(directory)));
       }
+      outputBuilder.addFile(getDexFileName(fileIndex), data, handler);
+    }
+
+    @Override
+    public void accept(DataDirectoryResource directory, DiagnosticsHandler handler) {
+      outputBuilder.addDirectory(directory.getName(), handler);
+    }
+
+    @Override
+    public void accept(DataEntryResource file, DiagnosticsHandler handler) {
+      outputBuilder.addFile(file.getName(), file, handler);
     }
 
     @Override
     public void finished(DiagnosticsHandler handler) {
       super.finished(handler);
+      try {
+        outputBuilder.close();
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, outputBuilder.getOrigin()));
+      }
     }
 
     private synchronized void prepareDirectory() throws IOException {
@@ -221,7 +252,7 @@
       deleteClassesDexFiles(directory);
     }
 
-    public static void deleteClassesDexFiles(Path directory) throws IOException {
+    static void deleteClassesDexFiles(Path directory) throws IOException {
       try (Stream<Path> filesInDir = Files.list(directory)) {
         for (Path path : filesInDir.collect(Collectors.toList())) {
           if (FileUtils.isClassesDexFile(path)) {
@@ -244,7 +275,7 @@
     }
 
     private static Path getTargetDexFile(Path directory, int fileIndex) {
-      return directory.resolve(ArchiveConsumer.getDefaultDexFileName(fileIndex));
+      return directory.resolve(ForwardingConsumer.getDefaultDexFileName(fileIndex));
     }
 
     private static void writeFile(byte[] contents, Path target) throws IOException {
@@ -254,8 +285,7 @@
 
     @Override
     public Path internalGetOutputPath() {
-      return directory;
+      return outputBuilder.getPath();
     }
-
   }
 }
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index e7496f4..ab86367 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -111,7 +111,7 @@
     } catch (FeatureMappingException e) {
       options.reporter.error(e.getMessage());
     } finally {
-      options.signalFinishedToProgramConsumer();
+      options.signalFinishedToConsumers();
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/InternalProgramOutputPathConsumer.java b/src/main/java/com/android/tools/r8/InternalProgramOutputPathConsumer.java
index ebdfb55..a092100 100644
--- a/src/main/java/com/android/tools/r8/InternalProgramOutputPathConsumer.java
+++ b/src/main/java/com/android/tools/r8/InternalProgramOutputPathConsumer.java
@@ -6,7 +6,7 @@
 import java.nio.file.Path;
 
 // Internal interface for testing the command setup.
-interface InternalProgramOutputPathConsumer extends ProgramConsumer {
+interface InternalProgramOutputPathConsumer extends ProgramConsumer, DataResourceConsumer {
 
   Path internalGetOutputPath();
 
diff --git a/src/main/java/com/android/tools/r8/ProgramConsumer.java b/src/main/java/com/android/tools/r8/ProgramConsumer.java
index 0e9b125..08d7d7c 100644
--- a/src/main/java/com/android/tools/r8/ProgramConsumer.java
+++ b/src/main/java/com/android/tools/r8/ProgramConsumer.java
@@ -9,6 +9,14 @@
 public interface ProgramConsumer {
 
   /**
+   * Returns a {@link DataResourceConsumer} that will receive data resources. If this
+   * returns <code>null</code> no data resources will be processed.
+   */
+  default DataResourceConsumer getDataResourceConsumer() {
+    return null;
+  }
+
+  /**
    * Callback signifying that compilation of program resources has finished.
    *
    * <p>Called only once after all program outputs have been generated and consumed.
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index df3d81b..6a1d7c8 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -489,7 +489,7 @@
       unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
     } finally {
-      options.signalFinishedToProgramConsumer();
+      options.signalFinishedToConsumers();
       // Dump timings.
       if (options.printTimes) {
         timing.report();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 8c69b7c..83f9dc2 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -238,6 +238,20 @@
           new EnsureNonDexProgramResourceProvider(programProvider));
     }
 
+    public Builder addDataResourceProvider(DataResourceProvider dataResourceProvider) {
+      assert dataResourceProvider != null;
+      getAppBuilder().addDataResourceProvider(dataResourceProvider);
+      return self();
+    }
+
+    @Override
+    protected InternalProgramOutputPathConsumer createProgramOutputConsumer(
+        Path path,
+        OutputMode mode,
+        boolean consumeDataResources) {
+      return super.createProgramOutputConsumer(path, mode, true);
+    }
+
     @Override
     void validate() {
       Reporter reporter = getReporter();
@@ -415,6 +429,7 @@
       "  --help                   # Print this message."));
 
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+  private DataResourceConsumer dataResourceConsumer;
   private final StringConsumer mainDexListConsumer;
   private final ProguardConfiguration proguardConfiguration;
   private final boolean enableTreeShaking;
@@ -719,6 +734,7 @@
     }
 
     internal.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
+    internal.dataResourceConsumer = internal.programConsumer.getDataResourceConsumer();
 
     // EXPERIMENTAL flags.
     assert !internal.forceProguardCompatibility;
diff --git a/src/main/java/com/android/tools/r8/Resource.java b/src/main/java/com/android/tools/r8/Resource.java
index f8f70d6..b64fe01 100644
--- a/src/main/java/com/android/tools/r8/Resource.java
+++ b/src/main/java/com/android/tools/r8/Resource.java
@@ -20,7 +20,6 @@
  * client is free to provide their own.
  */
 public interface Resource {
-
   /**
    * Get the origin of the resource.
    *
@@ -28,5 +27,4 @@
    * what that means for a particular resource.
    */
   Origin getOrigin();
-
 }
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index ad04e36..1de9606 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
 import java.io.File;
@@ -46,7 +45,6 @@
 import java.util.concurrent.ExecutorService;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
 import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
@@ -458,7 +456,8 @@
       CompatDxHelper.ignoreDexInArchive(builder);
       builder
           .addProgramFiles(inputs)
-          .setProgramConsumer(createConsumer(inputs, output, singleDexFile, dexArgs.keepClasses))
+          .setProgramConsumer(
+              createConsumer(inputs, output, singleDexFile, dexArgs.keepClasses))
           .setMode(mode)
           .setMinApiLevel(dexArgs.minApiLevel);
       if (mainDexList != null) {
@@ -486,7 +485,8 @@
   }
 
   private static DexIndexedConsumer createDexConsumer(
-      Path output, List<Path> inputs, boolean keepClasses) throws DxUsageMessage {
+      Path output, List<Path> inputs, boolean keepClasses)
+      throws DxUsageMessage {
     if (keepClasses) {
       if (!isArchive(output)) {
         throw new DxCompatOptions.DxUsageMessage(
@@ -564,14 +564,14 @@
     @Override
     public void finished(DiagnosticsHandler handler) {
       try {
-        writeZipWithClasses(getStream(handler));
+        writeZipWithClasses(handler);
       } catch (IOException e) {
         handler.error(new IOExceptionDiagnostic(e));
       }
       super.finished(handler);
     }
 
-    private void writeZipWithClasses(ZipOutputStream out) throws IOException {
+    private void writeZipWithClasses(DiagnosticsHandler handler) throws IOException {
       // For each input archive file, add all class files within.
       for (Path input : inputs) {
         if (isArchive(input)) {
@@ -581,8 +581,8 @@
               ZipEntry entry = entries.nextElement();
               if (isClassFile(Paths.get(entry.getName()))) {
                 try (InputStream entryStream = zipFile.getInputStream(entry)) {
-                  ZipUtils.writeToZipStream(
-                      out, entry.getName(), ByteStreams.toByteArray(entryStream), ZipEntry.STORED);
+                  outputBuilder.addFile(
+                      entry.getName(), ByteStreams.toByteArray(entryStream), handler);
                 }
               }
             }
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 6a63a8e..7dec61a 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -114,6 +114,7 @@
       classReader.readSources();
       ThreadUtils.awaitFutures(futures);
       classReader.initializeLazyClassCollection(builder);
+      builder.replaceDataResourceProviders(inputApp.getDataResourceProviders());
     } catch (ResourceException e) {
       throw options.reporter.fatalError(new StringDiagnostic(e.getMessage(), e.getOrigin()));
     } finally {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 664b8d1..b8d17ce 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -4,7 +4,14 @@
 package com.android.tools.r8.dex;
 
 import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.DataDirectoryResource;
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DataResourceConsumer;
+import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DataResourceProvider.Visitor;
 import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
@@ -288,6 +295,28 @@
       ExceptionUtils.withConsumeResourceHandler(
           options.reporter, options.mainDexListConsumer, writeMainDexList(application, namingLens));
     }
+    DataResourceConsumer dataResourceConsumer = options.dataResourceConsumer;
+    if (dataResourceConsumer != null) {
+      for (DataResourceProvider dataResourceProvider : application.dataResourceProviders) {
+        try {
+          dataResourceProvider.accept(new Visitor() {
+            @Override
+            public void visit(DataDirectoryResource directory) {
+              dataResourceConsumer.accept(directory, options.reporter);
+              options.reporter.failIfPendingErrors();
+            }
+
+            @Override
+            public void visit(DataEntryResource file) {
+              dataResourceConsumer.accept(file, options.reporter);
+              options.reporter.failIfPendingErrors();
+            }
+          });
+        } catch (ResourceException e) {
+          throw new CompilationError(e.getMessage(), e);
+        }
+      }
+    }
   }
 
   private void insertAttributeAnnotations() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index b5a3fc2..e908b64 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -6,12 +6,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -25,6 +27,8 @@
   // Maps type into class, may be used concurrently.
   final ProgramClassCollection programClasses;
 
+  public final ImmutableList<DataResourceProvider> dataResourceProviders;
+
   public final ImmutableSet<DexType> mainDexList;
   public final String deadCode;
 
@@ -43,6 +47,7 @@
   DexApplication(
       ClassNameMapper proguardMap,
       ProgramClassCollection programClasses,
+      ImmutableList<DataResourceProvider> dataResourceProviders,
       ImmutableSet<DexType> mainDexList,
       String deadCode,
       DexItemFactory dexItemFactory,
@@ -51,6 +56,7 @@
     assert programClasses != null;
     this.proguardMap = proguardMap;
     this.programClasses = programClasses;
+    this.dataResourceProviders = dataResourceProviders;
     this.mainDexList = mainDexList;
     this.deadCode = deadCode;
     this.dexItemFactory = dexItemFactory;
@@ -112,6 +118,8 @@
 
     final List<DexProgramClass> programClasses;
 
+    final List<DataResourceProvider> dataResourceProviders = new ArrayList<>();
+
     public final DexItemFactory dexItemFactory;
     ClassNameMapper proguardMap;
     final Timing timing;
@@ -133,6 +141,7 @@
 
     public Builder(DexApplication application) {
       programClasses = application.programClasses.getAllClasses();
+      replaceDataResourceProviders(application.dataResourceProviders);
       proguardMap = application.getProguardMap();
       timing = application.timing;
       highestSortingString = application.highestSortingString;
@@ -155,6 +164,15 @@
       return self();
     }
 
+    public synchronized T replaceDataResourceProviders(
+        List<DataResourceProvider> dataResourceProviders) {
+      this.dataResourceProviders.clear();
+      if (dataResourceProviders != null) {
+        this.dataResourceProviders.addAll(dataResourceProviders);
+      }
+      return self();
+    }
+
     public T appendDeadCode(String deadCodeAtAnotherRound) {
       if (deadCodeAtAnotherRound == null) {
         return self();
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index be55f18..a28d8ef 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -6,9 +6,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -23,11 +25,12 @@
 
   private DirectMappedDexApplication(ClassNameMapper proguardMap,
       ProgramClassCollection programClasses,
+      ImmutableList<DataResourceProvider> dataResourceProviders,
       ImmutableMap<DexType, DexLibraryClass> libraryClasses,
       ImmutableSet<DexType> mainDexList, String deadCode,
       DexItemFactory dexItemFactory, DexString highestSortingString,
       Timing timing) {
-    super(proguardMap, programClasses, mainDexList, deadCode,
+    super(proguardMap, programClasses, dataResourceProviders, mainDexList, deadCode,
         dexItemFactory, highestSortingString, timing);
     this.libraryClasses = libraryClasses;
   }
@@ -116,6 +119,7 @@
           proguardMap,
           ProgramClassCollection.create(
               programClasses, ProgramClassCollection::resolveClassConflictImpl),
+          ImmutableList.copyOf(dataResourceProviders),
           libraryClasses.stream().collect(ImmutableMap.toImmutableMap(c -> c.type, c -> c)),
           ImmutableSet.copyOf(mainDexList),
           deadCode,
diff --git a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
index 4021473..6eb9ce5 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -6,12 +6,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -26,12 +28,13 @@
    */
   private LazyLoadedDexApplication(ClassNameMapper proguardMap,
       ProgramClassCollection programClasses,
+      ImmutableList<DataResourceProvider> dataResourceProviders,
       ClasspathClassCollection classpathClasses,
       LibraryClassCollection libraryClasses,
       ImmutableSet<DexType> mainDexList, String deadCode,
       DexItemFactory dexItemFactory, DexString highestSortingString,
       Timing timing) {
-    super(proguardMap, programClasses, mainDexList, deadCode,
+    super(proguardMap, programClasses, dataResourceProviders, mainDexList, deadCode,
         dexItemFactory, highestSortingString, timing);
     this.classpathClasses = classpathClasses;
     this.libraryClasses = libraryClasses;
@@ -119,6 +122,7 @@
       return new LazyLoadedDexApplication(
           proguardMap,
           ProgramClassCollection.create(programClasses, resolver),
+          ImmutableList.copyOf(dataResourceProviders),
           classpathClasses,
           libraryClasses,
           ImmutableSet.copyOf(mainDexList),
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 ad1011c..d6ddf5d 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DirectoryClassFileProvider;
 import com.android.tools.r8.OutputMode;
+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;
@@ -48,6 +49,7 @@
 public class AndroidApp {
 
   private final ImmutableList<ProgramResourceProvider> programResourceProviders;
+  private final ImmutableList<DataResourceProvider> dataResourceProviders;
   private final ImmutableMap<Resource, String> programResourcesMainDescriptor;
   private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
   private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
@@ -59,6 +61,7 @@
   // See factory methods and AndroidApp.Builder below.
   private AndroidApp(
       ImmutableList<ProgramResourceProvider> programResourceProviders,
+      ImmutableList<DataResourceProvider> dataResourceProviders,
       ImmutableMap<Resource, String> programResourcesMainDescriptor,
       ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
       ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
@@ -66,6 +69,7 @@
       List<StringResource> mainDexListResources,
       List<String> mainDexClasses) {
     this.programResourceProviders = programResourceProviders;
+    this.dataResourceProviders = dataResourceProviders;
     this.programResourcesMainDescriptor = programResourcesMainDescriptor;
     this.classpathResourceProviders = classpathResourceProviders;
     this.libraryResourceProviders = libraryResourceProviders;
@@ -128,6 +132,11 @@
     return programResourceProviders;
   }
 
+  /** Get non program resource providers. */
+  public List<DataResourceProvider> getDataResourceProviders() {
+    return dataResourceProviders;
+  }
+
   /** Get classpath resource providers. */
   public List<ClassFileResourceProvider> getClasspathResourceProviders() {
     return classpathResourceProviders;
@@ -253,6 +262,7 @@
 
     private final List<ProgramResourceProvider> programResourceProviders = new ArrayList<>();
     private final List<ProgramResource> programResources = new ArrayList<>();
+    private final List<DataResourceProvider> dataResourceProviders = new ArrayList<>();
     private final Map<ProgramResource, String> programResourcesMainDescriptor = new HashMap<>();
     private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
     private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
@@ -272,6 +282,7 @@
       programResourceProviders.addAll(app.programResourceProviders);
       classpathResourceProviders.addAll(app.classpathResourceProviders);
       libraryResourceProviders.addAll(app.libraryResourceProviders);
+      dataResourceProviders.addAll(app.dataResourceProviders);
       mainDexListResources = app.mainDexListResources;
       mainDexListClasses = app.mainDexClasses;
     }
@@ -294,8 +305,11 @@
         throws NoSuchFileException {
       for (FilteredClassPath archive : filteredArchives) {
         assert isArchive(archive.getPath());
-        addProgramResourceProvider(
-            new FilteredArchiveProgramResourceProvider(archive, ignoreDexInArchive));
+        ArchiveResourceProvider archiveResourceProvider =
+            new ArchiveResourceProvider(archive, ignoreDexInArchive);
+        addProgramResourceProvider(archiveResourceProvider);
+        addDataResourceProvider(archiveResourceProvider);
+
       }
       return this;
     }
@@ -306,6 +320,12 @@
       return this;
     }
 
+    public Builder addDataResourceProvider(DataResourceProvider provider) {
+      assert provider != null;
+      dataResourceProviders.add(provider);
+      return this;
+    }
+
     /**
      * Add classpath file resources.
      */
@@ -518,6 +538,7 @@
       }
       return new AndroidApp(
           ImmutableList.copyOf(programResourceProviders),
+          ImmutableList.copyOf(dataResourceProviders),
           ImmutableMap.copyOf(programResourcesMainDescriptor),
           ImmutableList.copyOf(classpathResourceProviders),
           ImmutableList.copyOf(libraryResourceProviders),
@@ -535,9 +556,10 @@
       } else if (isClassFile(file)) {
         addProgramResources(ProgramResource.fromFile(Kind.CF, file));
       } else if (isArchive(file)) {
-        addProgramResourceProvider(
-            new FilteredArchiveProgramResourceProvider(
-                FilteredClassPath.unfiltered(file), ignoreDexInArchive));
+        ArchiveResourceProvider archiveResourceProvider = new ArchiveResourceProvider(
+            FilteredClassPath.unfiltered(file), ignoreDexInArchive);
+        addProgramResourceProvider(archiveResourceProvider);
+        addDataResourceProvider(archiveResourceProvider);
       } else {
         throw new CompilationError("Unsupported source file type", new PathOrigin(file));
       }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
index e923749..14bf6f5 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
@@ -92,9 +92,13 @@
           @Override
           public void finished(DiagnosticsHandler handler) {
             super.finished(handler);
-            closed = true;
-            files.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors));
-            files = null;
+            if (!closed) {
+              closed = true;
+              files.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors));
+              files = null;
+            } else {
+              assert getDataResourceConsumer() != null;
+            }
           }
 
           synchronized void addDexFile(int fileIndex, byte[] data, Set<String> descriptors) {
@@ -132,9 +136,13 @@
           @Override
           public void finished(DiagnosticsHandler handler) {
             super.finished(handler);
-            closed = true;
-            files.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v));
-            files = null;
+            if (!closed) {
+              closed = true;
+              files.forEach((v, d) -> builder.addDexProgramData(d.contents, d.descriptors, v));
+              files = null;
+            } else {
+              assert getDataResourceConsumer() != null;
+            }
           }
         };
     programConsumer = wrapped;
@@ -161,10 +169,14 @@
           @Override
           public void finished(DiagnosticsHandler handler) {
             super.finished(handler);
-            closed = true;
-            files.forEach(
-                d -> builder.addClassProgramData(d.contents, Origin.unknown(), d.descriptors));
-            files = null;
+            if (!closed) {
+              closed = true;
+              files.forEach(
+                  d -> builder.addClassProgramData(d.contents, Origin.unknown(), d.descriptors));
+              files = null;
+            } else {
+              assert getDataResourceConsumer() != null;
+            }
           }
         };
     programConsumer = wrapped;
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
new file mode 100644
index 0000000..4a7abc3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
@@ -0,0 +1,127 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DataResource;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ResourceException;
+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.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipOutputStream;
+
+public class ArchiveBuilder implements OutputBuilder {
+  private final Path archive;
+  private final Origin origin;
+  private ZipOutputStream stream = null;
+  private boolean closed = false;
+  private int openCount = 0;
+
+  public ArchiveBuilder(Path archive) {
+    this.archive = archive;
+    origin = new PathOrigin(archive);
+  }
+
+  @Override
+  public synchronized void open() {
+    assert !closed;
+    openCount ++;
+  }
+
+  @Override
+  public synchronized void close() throws IOException {
+    assert !closed;
+    openCount--;
+    if (openCount == 0) {
+      closed = true;
+      if (stream != null) {
+        stream.close();
+        stream = null;
+      }
+    }
+  }
+
+  /** Get or open the zip output stream. */
+  private synchronized ZipOutputStream getStream(DiagnosticsHandler handler) {
+    assert !closed;
+    if (stream == null) {
+      try {
+        stream =
+            new ZipOutputStream(
+                Files.newOutputStream(
+                    archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+    return stream;
+  }
+
+  private void handleIOException(IOException e, DiagnosticsHandler handler) {
+    if (e instanceof ZipException && e.getMessage().startsWith("duplicate entry")) {
+      // For now we stick to the Proguard behaviour, see section "Warning: can't write resource ...
+      // Duplicate zip entry" on https://www.guardsquare.com/en/proguard/manual/troubleshooting.
+      handler.warning(new IOExceptionDiagnostic(e, origin));
+    } else {
+      handler.error(new IOExceptionDiagnostic(e, origin));
+    }
+  }
+
+  @Override
+  public void addDirectory(String name, DiagnosticsHandler handler) {
+    if (name.charAt(name.length() - 1) != DataResource.SEPARATOR) {
+      name += DataResource.SEPARATOR;
+    }
+    ZipEntry entry = new ZipEntry(name);
+    ZipOutputStream zip = getStream(handler);
+    synchronized (this) {
+      try {
+        zip.putNextEntry(entry);
+        zip.closeEntry();
+      } catch (IOException e) {
+        handleIOException(e, handler);
+      }
+    }
+  }
+
+  @Override
+  public void addFile(String name, DataEntryResource content, DiagnosticsHandler handler) {
+    try (InputStream in = content.getByteStream()) {
+      addFile(name, ByteStreams.toByteArray(in), handler);
+    } catch (IOException e) {
+      handleIOException(e, handler);
+    } catch (ResourceException e) {
+      handler.error(new StringDiagnostic("Failed to open input: " + e.getMessage(),
+          content.getOrigin()));
+    }
+  }
+
+   @Override
+   public synchronized void addFile(String name, byte[] content, DiagnosticsHandler handler) {
+    try {
+      ZipUtils.writeToZipStream(getStream(handler), name, content, ZipEntry.STORED);
+    } catch (IOException e) {
+      handleIOException(e, handler);
+    }
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Path getPath() {
+    return archive;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/FilteredArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
similarity index 71%
rename from src/main/java/com/android/tools/r8/utils/FilteredArchiveProgramResourceProvider.java
rename to src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index 7cde903..9c33fc5 100644
--- a/src/main/java/com/android/tools/r8/utils/FilteredArchiveProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -7,6 +7,9 @@
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 
+import com.android.tools.r8.DataDirectoryResource;
+import com.android.tools.r8.DataEntryResource;
+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;
@@ -30,20 +33,20 @@
 import java.util.zip.ZipException;
 import java.util.zip.ZipFile;
 
-public class FilteredArchiveProgramResourceProvider implements ProgramResourceProvider {
+public class ArchiveResourceProvider implements ProgramResourceProvider, DataResourceProvider {
 
   private final Origin origin;
   private final FilteredClassPath archive;
   private final boolean ignoreDexInArchive;
 
-  FilteredArchiveProgramResourceProvider(FilteredClassPath archive, boolean ignoreDexInArchive) {
+  ArchiveResourceProvider(FilteredClassPath archive, boolean ignoreDexInArchive) {
+    assert isArchive(archive.getPath());
     origin = new PathOrigin(archive.getPath());
     this.archive = archive;
     this.ignoreDexInArchive = ignoreDexInArchive;
   }
 
   private List<ProgramResource> readArchive() throws IOException {
-    assert isArchive(archive.getPath());
     List<ProgramResource> dexResources = new ArrayList<>();
     List<ProgramResource> classResources = new ArrayList<>();
     try (ZipFile zipFile = new ZipFile(archive.getPath().toFile())) {
@@ -94,4 +97,32 @@
       throw new ResourceException(origin, e);
     }
   }
+
+  @Override
+  public void accept(Visitor resourceBrowser) throws ResourceException {
+    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile())) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        Path name = Paths.get(entry.getName());
+        if (archive.matchesFile(name) && !isProgramResourceName(name)) {
+          if (entry.isDirectory()) {
+            resourceBrowser.visit(DataDirectoryResource.fromZip(zipFile, entry));
+          } else {
+            resourceBrowser.visit(DataEntryResource.fromZip(zipFile, entry));
+          }
+        }
+      }
+    } catch (ZipException e) {
+      throw new ResourceException(origin, new CompilationError(
+          "Zip error while reading '" + archive + "': " + e.getMessage(), e));
+    } catch (IOException e) {
+      throw new ResourceException(origin, new CompilationError(
+          "I/O exception while reading '" + archive + "': " + e.getMessage(), e));
+    }
+  }
+
+  private boolean isProgramResourceName(Path name) {
+    return isClassFile(name) || (isDexFile(name) && !ignoreDexInArchive);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryBuilder.java b/src/main/java/com/android/tools/r8/utils/DirectoryBuilder.java
new file mode 100644
index 0000000..b793f19
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryBuilder.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class DirectoryBuilder implements OutputBuilder {
+  private final Path root;
+  private final Origin origin;
+
+  public DirectoryBuilder(Path root) {
+    this.root = root;
+    origin = new PathOrigin(root);
+  }
+
+  @Override
+  public void open() {
+  }
+
+  @Override
+  public void close() {
+  }
+
+  @Override
+  public void addDirectory(String name, DiagnosticsHandler handler) {
+    Path target = root.resolve(name.replace(NAME_SEPARATOR, File.separatorChar));
+    try {
+      Files.createDirectories(target.getParent());
+    } catch (IOException e) {
+      handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
+    }
+  }
+
+  @Override
+  public void addFile(String name, DataEntryResource content, DiagnosticsHandler handler) {
+    try (InputStream in = content.getByteStream()) {
+      addFile(name, ByteStreams.toByteArray(in), handler);
+    } catch (IOException e) {
+      handler.error(new IOExceptionDiagnostic(e, content.getOrigin()));
+    } catch (ResourceException e) {
+      handler.error(new StringDiagnostic("Failed to open input: " + e.getMessage(),
+          content.getOrigin()));
+    }
+  }
+
+  @Override
+  public synchronized void addFile(String name, byte[] content, DiagnosticsHandler handler) {
+    Path target = root.resolve(name.replace(NAME_SEPARATOR, File.separatorChar));
+    try {
+      Files.createDirectories(target.getParent());
+      FileUtils.writeToFile(target, null, content);
+    } catch (IOException e) {
+      handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
+    }
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Path getPath() {
+    return root;
+  }
+}
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 0a3b2a4..3086662 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -6,6 +6,8 @@
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DataResourceConsumer;
+import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.dex.Marker;
@@ -47,6 +49,9 @@
   // TODO(zerny): Make this private-final once we have full program-consumer support.
   public ProgramConsumer programConsumer = null;
 
+  public final List<DataResourceProvider> dataResourceProviders = new ArrayList<>();
+  public DataResourceConsumer dataResourceConsumer;
+
   // Constructor for testing and/or other utilities.
   public InternalOptions() {
     reporter = new Reporter(new DefaultDiagnosticsHandler());
@@ -154,9 +159,12 @@
     return (ClassFileConsumer) programConsumer;
   }
 
-  public void signalFinishedToProgramConsumer() {
+  public void signalFinishedToConsumers() {
     if (programConsumer != null) {
       programConsumer.finished(reporter);
+      if (dataResourceConsumer != null) {
+        dataResourceConsumer.finished(reporter);
+      }
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/OutputBuilder.java b/src/main/java/com/android/tools/r8/utils/OutputBuilder.java
new file mode 100644
index 0000000..27d07c4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/OutputBuilder.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2018, 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 com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.origin.Origin;
+import java.io.Closeable;
+import java.nio.file.Path;
+
+public interface OutputBuilder extends Closeable {
+  char NAME_SEPARATOR = '/';
+
+  void open();
+
+  void addDirectory(String name, DiagnosticsHandler handler);
+
+  void addFile(String name, DataEntryResource content, DiagnosticsHandler handler);
+
+  void addFile(String name, byte[] content, DiagnosticsHandler handler);
+
+  Path getPath();
+
+  Origin getOrigin();
+}
diff --git a/src/test/examples/dataresource/ResourceTest.java b/src/test/examples/dataresource/ResourceTest.java
new file mode 100644
index 0000000..bb63826
--- /dev/null
+++ b/src/test/examples/dataresource/ResourceTest.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2018, 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.
+
+// This code is not run directly. It needs to be compiled to dex code.
+// 'arithmetic.dex' is what is run.
+
+package dataresource;
+
+import dataresource.lib.LibClass;
+import java.io.IOException;
+
+public class ResourceTest {
+  public static void main(String[] args) throws IOException {
+    System.out.println("LibClass dir: " + (LibClass.getThisDir() != null));
+    System.out.println("LibClass properties: " + (LibClass.getLibClassProperties() != null));
+    System.out.println("LibClass property: " + LibClass.getLibClassProperty());
+    System.out.println("LibClass text: " + LibClass.getText());
+    System.out.println("LibClass const string: " + LibClass.getConstString());
+    System.out.println("LibClass concat string: " + LibClass.getConcatConstString());
+    System.out.println("LibClass field: " + LibClass.getConstField());
+  }
+}
diff --git a/src/test/examples/dataresource/dataresource.lib.LibClass.txt b/src/test/examples/dataresource/dataresource.lib.LibClass.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/test/examples/dataresource/dataresource.lib.LibClass.txt
diff --git a/src/test/examples/dataresource/lib/LibClass.java b/src/test/examples/dataresource/lib/LibClass.java
new file mode 100644
index 0000000..870adf5
--- /dev/null
+++ b/src/test/examples/dataresource/lib/LibClass.java
@@ -0,0 +1,62 @@
+// Copyright (c) 2018, 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.
+
+// This code is not run directly. It needs to be compiled to dex code.
+// 'arithmetic.dex' is what is run.
+
+package dataresource.lib;
+
+import dataresource.ResourceTest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Properties;
+import java.util.Random;
+
+public class LibClass {
+  static final String name;
+  static {
+    name = "dataresource.lib.LibClass";
+  }
+
+  public static String getConstString() {
+    return "dataresource.lib.LibClass";
+  }
+
+  public static String getConcatConstString() throws IOException {
+    return "dataresource.lib.LibClass" + getLibClassProperty();
+  }
+
+  public static String getConstField() {
+    return name;
+  }
+
+  public static URL getThisDir() {
+    return LibClass.class.getResource("");
+  }
+
+  public static URL getLibClassProperties() {
+    return LibClass.class.getResource(LibClass.class.getSimpleName() + ".properties");
+  }
+
+  public static String getLibClassProperty() throws IOException {
+    Properties properties = new Properties();
+    properties.load(LibClass.class.getResourceAsStream(
+        LibClass.class.getSimpleName() + ".properties"));
+    return "" + properties.get(LibClass.class.getName());
+  }
+
+  public static String getText() throws IOException {
+    byte[] buffer = new byte[1000];
+    StringBuilder sb = new StringBuilder();
+    try (InputStream stream = ResourceTest.class.getResourceAsStream("resource.txt")) {
+      int size = stream.read(buffer);
+      while (size != -1) {
+        sb.append(new String(buffer, 0, size));
+        size = stream.read(buffer);
+      }
+    }
+    return sb.toString();
+  }
+}
diff --git a/src/test/examples/dataresource/lib/LibClass.properties b/src/test/examples/dataresource/lib/LibClass.properties
new file mode 100644
index 0000000..3f634e7
--- /dev/null
+++ b/src/test/examples/dataresource/lib/LibClass.properties
@@ -0,0 +1 @@
+dataresource.lib.LibClass=com.test.lib.LibClass
\ No newline at end of file
diff --git a/src/test/examples/dataresource/resource.txt b/src/test/examples/dataresource/resource.txt
new file mode 100644
index 0000000..5eb5177
--- /dev/null
+++ b/src/test/examples/dataresource/resource.txt
@@ -0,0 +1,42 @@
+this is a text with some content
+- partly matching pattern 123dataresource.lib.LibClass123
+- totally matching pattern dataresource.lib.LibClass and something after
+- matching the package dataresource.lib
+- matching class simple name LibClass
+- or only single element of the package name: lib
+- matching class descriptor dataresource/lib/LibClass
+- matching class full descriptor Ldataresource/lib/LibClass;
+- matching windows path dataresource\lib\LibClass
+- matching pattern dataresource.lib.LibClass.
+- matching pattern .dataresource.lib.LibClass
+- matching pattern dataresource.lib.LibClass,
+- matching pattern ,dataresource.lib.LibClass
+- matching pattern =dataresource.lib.LibClass
+- matching pattern dataresource.lib.LibClass=
+- matching pattern dataresource.lib.LibClass/
+- matching pattern /dataresource.lib.LibClass
+- matching pattern ?dataresource.lib.LibClass
+- matching pattern dataresource.lib.LibClass?
+- matching pattern dataresource.lib.LibClass!
+- matching pattern !dataresource.lib.LibClass
+- matching pattern :dataresource.lib.LibClass
+- matching pattern dataresource.lib.LibClass:
+- matching pattern dataresource.lib.LibClass*
+- matching pattern *dataresource.lib.LibClass
+- matching pattern $dataresource.lib.LibClass
+- matching pattern +dataresource.lib.LibClass
+- matching pattern -dataresource.lib.LibClass
+- matching pattern ^dataresource.lib.LibClass
+- matching pattern @dataresource.lib.LibClass
+- matching pattern (dataresource.lib.LibClass
+- matching pattern )dataresource.lib.LibClass
+- matching pattern àdataresource.lib.LibClass
+- matching pattern |dataresource.lib.LibClass
+- matching pattern [dataresource.lib.LibClass
+- matching pattern 'dataresource.lib.LibClass
+- matching pattern "dataresource.lib.LibClass
+- matching pattern `dataresource.lib.LibClass
+- matching pattern ~dataresource.lib.LibClass
+- matching pattern &dataresource.lib.LibClass
+- matching pattern -dataresource.lib.LibClass
+- matching pattern dataresource.lib.LibClass-
diff --git a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
index 3aa8f4a..a24e95a 100644
--- a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
@@ -6,8 +6,8 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.ClassFileConsumer;
-import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
diff --git a/src/test/java/com/android/tools/r8/cf/NegativeZeroTestRunner.java b/src/test/java/com/android/tools/r8/cf/NegativeZeroTestRunner.java
index 7196120..7dc64fd 100644
--- a/src/test/java/com/android/tools/r8/cf/NegativeZeroTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/NegativeZeroTestRunner.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidAppConsumers;
 import java.nio.file.Path;
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
index 074c704..5afb068 100644
--- a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
@@ -122,7 +122,7 @@
         throws DexOverflowException {
       try {
         ToolHelper.writeApplication(application, options);
-        options.signalFinishedToProgramConsumer();
+        options.signalFinishedToConsumers();
         return consumers.build();
       } catch (ExecutionException e) {
         throw new RuntimeException(e);
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 50a3c06..d6b26f0 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -653,7 +653,7 @@
     } finally {
       executor.shutdown();
     }
-    options.signalFinishedToProgramConsumer();
+    options.signalFinishedToConsumers();
     return compatSink.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java b/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
index 6c054e5..3b2cf8c 100644
--- a/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.naming;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 
 import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.CompilationMode;
diff --git a/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java b/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java
index 6642352..3e75ac6 100644
--- a/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java
+++ b/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java
@@ -45,6 +45,7 @@
     assertEquals(0, runInput.exitCode);
     Path outDex = temp.getRoot().toPath().resolve("dex.zip");
     build(new DexIndexedConsumer.ArchiveConsumer(outDex));
+    // TODO(b/76191597): Change to runArtNoVerificationErrors + assertEquals when bug is fixed
     ProcessResult runDex = ToolHelper.runArtNoVerificationErrorsRaw(
         outDex.toString(), CLASS.getCanonicalName());
     assertEquals(runInput.stdout, runDex.stdout);
diff --git a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
new file mode 100644
index 0000000..167262f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2018, 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.resource;
+
+import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.FileUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class DataResourceTest {
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void dataResourceTest()
+      throws IOException, CompilationFailedException, CompilationException {
+    String packageName = "dataresource";
+    String mainClassName = packageName + ".ResourceTest";
+    Path inputJar = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR,
+        packageName + FileUtils.JAR_EXTENSION);
+
+    ProcessResult referenceResult = ToolHelper.runJava(inputJar, mainClassName);
+
+    Path r8Out = temp.getRoot().toPath().resolve("r8out.jar");
+    R8Command.Builder builder = R8Command.builder()
+        .addProgramFiles(inputJar)
+        .setOutput(r8Out, OutputMode.DexIndexed);
+    ToolHelper.runR8(builder.build());
+
+    ProcessResult r8Result = ToolHelper.runArtRaw(r8Out.toString(), mainClassName);
+    Assert.assertEquals(referenceResult.stdout, r8Result.stdout);
+
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 2dfe42e..286544b 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -17,7 +17,6 @@
 import static org.hamcrest.core.StringContains.containsString;
 
 import com.android.tools.r8.Diagnostic;
-import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -36,13 +35,13 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
+import com.android.tools.r8.utils.KeepingDiagnosticHandler;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.ImmutableList;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
@@ -136,27 +135,6 @@
   private static final String TARGET =
       VALID_PROGUARD_DIR + "target.flags";
 
-  private static class KeepingDiagnosticHandler implements DiagnosticsHandler {
-    private final List<Diagnostic> infos = new ArrayList<>();
-    private final List<Diagnostic> warnings = new ArrayList<>();
-    private final List<Diagnostic> errors = new ArrayList<>();
-
-    @Override
-    public void info(Diagnostic info) {
-      infos.add(info);
-    }
-
-    @Override
-    public void warning(Diagnostic warning) {
-      warnings.add(warning);
-    }
-
-    @Override
-    public void error(Diagnostic error) {
-      errors.add(error);
-    }
-  }
-
   private Reporter reporter;
   private KeepingDiagnosticHandler handler;
   private ProguardConfigurationParser parser;
diff --git a/src/test/java/com/android/tools/r8/utils/KeepingDiagnosticHandler.java b/src/test/java/com/android/tools/r8/utils/KeepingDiagnosticHandler.java
new file mode 100644
index 0000000..e7ad488
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/KeepingDiagnosticHandler.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, 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 com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.DiagnosticsHandler;
+import java.util.ArrayList;
+import java.util.List;
+
+public class KeepingDiagnosticHandler implements DiagnosticsHandler {
+  public final List<Diagnostic> infos = new ArrayList<>();
+  public final List<Diagnostic> warnings = new ArrayList<>();
+  public final List<Diagnostic> errors = new ArrayList<>();
+
+  @Override
+  public void info(Diagnostic info) {
+    infos.add(info);
+  }
+
+  @Override
+  public void warning(Diagnostic warning) {
+    warnings.add(warning);
+  }
+
+  @Override
+  public void error(Diagnostic error) {
+    errors.add(error);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/ThrowingDiagnosticHandler.java b/src/test/java/com/android/tools/r8/utils/ThrowingDiagnosticHandler.java
new file mode 100644
index 0000000..7b634b9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/ThrowingDiagnosticHandler.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2017, 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 com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.DiagnosticsHandler;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ThrowingDiagnosticHandler extends KeepingDiagnosticHandler {
+
+  @Override
+  public void error(Diagnostic error) {
+    super.error(error);
+    throw new AssertionError(error);
+  }
+}