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;
+ }
+ }
+}