Use same compression in out as in input for resources

Apps can rely on having uncompressed access to files in res

Bug: b/309078004
Change-Id: I488d5ea585749a7313c254338581241475d6913f
diff --git a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
index 9bb7f82..eed5427 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProtoAndroidResourceConsumer.java
@@ -4,29 +4,67 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.ArchiveBuilder;
-import com.android.tools.r8.utils.OutputBuilder;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
 
 @KeepForApi
 public class ArchiveProtoAndroidResourceConsumer implements AndroidResourceConsumer {
-  private final OutputBuilder outputBuilder;
+  private final ArchiveBuilder archiveBuilder;
+  private final Path inputPath;
+  private Map<String, Boolean> compressionMap;
 
   public ArchiveProtoAndroidResourceConsumer(Path outputPath) {
-    this.outputBuilder = new ArchiveBuilder(outputPath);
-    this.outputBuilder.open();
+    this(outputPath, null);
+  }
+
+  public ArchiveProtoAndroidResourceConsumer(Path outputPath, Path inputPath) {
+    this.archiveBuilder = new ArchiveBuilder(outputPath);
+    this.archiveBuilder.open();
+    this.inputPath = inputPath;
+  }
+
+  private synchronized Map<String, Boolean> getCompressionMap(
+      DiagnosticsHandler diagnosticsHandler) {
+    if (compressionMap != null) {
+      return compressionMap;
+    }
+    if (inputPath != null) {
+      compressionMap = new HashMap<>();
+      try {
+        ZipUtils.iter(
+            inputPath,
+            entry -> {
+              compressionMap.put(entry.getName(), entry.getMethod() != ZipEntry.STORED);
+            });
+      } catch (IOException e) {
+        diagnosticsHandler.error(new ExceptionDiagnostic(e, new PathOrigin(inputPath)));
+      }
+    } else {
+      compressionMap = Collections.emptyMap();
+    }
+    return compressionMap;
   }
 
   @Override
   public void accept(AndroidResourceOutput androidResource, DiagnosticsHandler diagnosticsHandler) {
-    outputBuilder.addFile(
+    archiveBuilder.addFile(
         androidResource.getPath().location(),
         androidResource.getByteDataView(),
-        diagnosticsHandler);
+        diagnosticsHandler,
+        getCompressionMap(diagnosticsHandler)
+            .getOrDefault(androidResource.getPath().location(), true));
   }
 
   @Override
   public void finished(DiagnosticsHandler handler) {
-    outputBuilder.close(handler);
+    archiveBuilder.close(handler);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
index a50dbb9..1ff1d22 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
@@ -73,7 +73,7 @@
         writeDirectoryNow(data.name, handler);
       } else {
         assert data.content != null;
-        writeFileNow(data.name, data.content, handler);
+        writeFileNow(data.name, data.content, handler, data.storeCompressed);
       }
     }
   }
@@ -135,9 +135,9 @@
       ByteDataView view = ByteDataView.of(ByteStreams.toByteArray(in));
       synchronized (this) {
         if (AndroidApiDataAccess.isApiDatabaseEntry(name)) {
-          writeFileNow(name, view, handler);
+          writeFileNow(name, view, handler, true);
         } else {
-          delayedWrites.add(DelayedData.createFile(name, view));
+          delayedWrites.add(DelayedData.createFile(name, view, true));
         }
       }
     } catch (IOException e) {
@@ -150,16 +150,25 @@
 
   @Override
   public synchronized void addFile(String name, ByteDataView content, DiagnosticsHandler handler) {
-    delayedWrites.add(DelayedData.createFile(name,  ByteDataView.of(content.copyByteData())));
+    addFile(name, content, handler, true);
   }
 
-  private void writeFileNow(String name, ByteDataView content, DiagnosticsHandler handler) {
+  public synchronized void addFile(
+      String name, ByteDataView content, DiagnosticsHandler handler, boolean storeCompressed) {
+    delayedWrites.add(
+        DelayedData.createFile(name, ByteDataView.of(content.copyByteData()), storeCompressed));
+  }
+
+  private void writeFileNow(
+      String name, ByteDataView content, DiagnosticsHandler handler, boolean compressed) {
     try {
       ZipUtils.writeToZipStream(
           getStream(),
           name,
           content,
-          AndroidApiDataAccess.isApiDatabaseEntry(name) ? ZipEntry.STORED : ZipEntry.DEFLATED);
+          AndroidApiDataAccess.isApiDatabaseEntry(name) || !compressed
+              ? ZipEntry.STORED
+              : ZipEntry.DEFLATED);
     } catch (IOException e) {
       handleIOException(e, handler);
     }
@@ -168,7 +177,7 @@
   private void writeNextIfAvailable(DiagnosticsHandler handler) {
     DelayedData data = delayedClassesDexFiles.remove(classesFileIndex);
     while (data != null) {
-      writeFileNow(data.name, data.content, handler);
+      writeFileNow(data.name, data.content, handler, data.storeCompressed);
       classesFileIndex++;
       data = delayedClassesDexFiles.remove(classesFileIndex);
     }
@@ -179,13 +188,13 @@
       int index, String name, ByteDataView content, DiagnosticsHandler handler) {
     if (index == classesFileIndex) {
       // Fast case, we got the file in order (or we only had one).
-      writeFileNow(name, content, handler);
+      writeFileNow(name, content, handler, true);
       classesFileIndex++;
       writeNextIfAvailable(handler);
     } else {
       // Data is released in the application writer, take a copy.
-      delayedClassesDexFiles.put(index,
-          new DelayedData(name, ByteDataView.of(content.copyByteData()), false));
+      delayedClassesDexFiles.put(
+          index, new DelayedData(name, ByteDataView.of(content.copyByteData()), false, true));
     }
   }
 
@@ -203,19 +212,23 @@
     public final String name;
     public final ByteDataView content;
     public final boolean isDirectory;
+    public final boolean storeCompressed;
 
-    public static DelayedData createFile(String name, ByteDataView content) {
-      return new DelayedData(name, content, false);
+    public static DelayedData createFile(
+        String name, ByteDataView content, boolean storeCompressed) {
+      return new DelayedData(name, content, false, storeCompressed);
     }
 
     public static DelayedData createDirectory(String name) {
-      return new DelayedData(name, null, true);
+      return new DelayedData(name, null, true, true);
     }
 
-    private DelayedData(String name, ByteDataView content, boolean isDirectory) {
+    private DelayedData(
+        String name, ByteDataView content, boolean isDirectory, boolean storeCompressed) {
       this.name = name;
       this.content = content;
       this.isDirectory = isDirectory;
+      this.storeCompressed = storeCompressed;
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index e02d3e0..2b11a9f 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -37,6 +37,7 @@
 import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.function.BiFunction;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -100,6 +101,15 @@
     }
   }
 
+  public static void iter(Path zipFilePath, Consumer<ZipEntry> entryConsumer) throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        entryConsumer.accept(entries.nextElement());
+      }
+    }
+  }
+
   @SuppressWarnings("UnnecessaryParentheses")
   public static boolean containsEntry(Path zipfile, String name) throws IOException {
     BooleanBox result = new BooleanBox();
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index bbab1ac..d1eb192 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -910,8 +910,8 @@
     getBuilder()
         .setAndroidResourceProvider(
             new ArchiveProtoAndroidResourceProvider(resources, new PathOrigin(resources)));
-    getBuilder().setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output));
-    self();
+    getBuilder()
+        .setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output, resources));
     return addProgramClassFileData(testResource.getRClass().getClassFileData());
   }
 
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 785a049..021902d 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -279,7 +279,7 @@
             addStringValue(name, name);
           }
           if (rClassType == RClassType.DRAWABLE) {
-            addDrawable(name, TINY_PNG);
+            addDrawable(name + ".png", TINY_PNG);
           }
           if (rClassType == RClassType.STYLEABLE) {
             // Add 4 different values, i.e., the array will be 4 integers.
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java b/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
new file mode 100644
index 0000000..e7d0e79
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestArchiveCompression.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2023, 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.androidresources;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestArchiveCompression extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class, R.drawable.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    AndroidTestResource testResources = getTestResources(temp);
+    Path resourceZip = testResources.getResourceZip();
+    assertTrue(ZipUtils.containsEntry(resourceZip, "res/drawable/foobar.png"));
+    assertTrue(ZipUtils.containsEntry(resourceZip, "res/drawable/unused_drawable.png"));
+    assertTrue(ZipUtils.containsEntry(resourceZip, "AndroidManifest.xml"));
+    assertTrue(ZipUtils.containsEntry(resourceZip, "resources.pb"));
+
+    validateCompression(resourceZip);
+    Path resourceOutput = temp.newFile("resources_out.zip").toPath();
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addAndroidResources(testResources, resourceOutput)
+        .addKeepMainRule(FooBar.class)
+        .compile()
+        .run(parameters.getRuntime(), FooBar.class)
+        .assertSuccess();
+    assertTrue(ZipUtils.containsEntry(resourceOutput, "res/drawable/foobar.png"));
+    assertFalse(ZipUtils.containsEntry(resourceOutput, "res/drawable/unused_drawable.png"));
+    assertTrue(ZipUtils.containsEntry(resourceOutput, "AndroidManifest.xml"));
+    assertTrue(ZipUtils.containsEntry(resourceOutput, "resources.pb"));
+    validateCompression(resourceOutput);
+  }
+
+  private static void validateCompression(Path resourceZip) throws IOException {
+    ZipUtils.iter(
+        resourceZip,
+        entry -> {
+          if (entry.getName().endsWith(".png")) {
+            assertEquals(ZipEntry.STORED, entry.getMethod());
+          } else {
+            assertEquals(ZipEntry.DEFLATED, entry.getMethod());
+          }
+        });
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(R.drawable.foobar);
+        System.out.println(R.string.bar);
+        System.out.println(R.string.foo);
+      }
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+
+      public static int bar;
+      public static int foo;
+      public static int unused_string;
+    }
+
+    public static class drawable {
+
+      public static int foobar;
+      public static int unused_drawable;
+    }
+  }
+}