| // 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.build.shrinker.r8integration; |
| |
| import static java.nio.charset.StandardCharsets.UTF_16BE; |
| import static java.nio.charset.StandardCharsets.UTF_16LE; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.android.aapt.Resources.ResourceTable; |
| import com.android.aapt.Resources.XmlNode; |
| import com.android.build.shrinker.NoDebugReporter; |
| import com.android.build.shrinker.ResourceShrinkerImplKt; |
| import com.android.build.shrinker.ResourceTableUtilKt; |
| import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder; |
| import com.android.build.shrinker.graph.ResFolderFileTree; |
| import com.android.build.shrinker.obfuscation.ProguardMappingsRecorder; |
| import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel; |
| import com.android.build.shrinker.usages.DexFileAnalysisCallback; |
| import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt; |
| import com.android.build.shrinker.usages.R8ResourceShrinker; |
| import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt; |
| import com.android.ide.common.resources.ResourcesUtil; |
| import com.android.ide.common.resources.usage.ResourceStore; |
| import com.android.ide.common.resources.usage.ResourceUsageModel.Resource; |
| import com.android.resources.ResourceType; |
| import com.android.tools.r8.FeatureSplit; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.protobuf.InvalidProtocolBufferException; |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import javax.xml.parsers.ParserConfigurationException; |
| import org.xml.sax.SAXException; |
| |
| public class LegacyResourceShrinker { |
| private final Map<String, byte[]> dexInputs; |
| private final List<PathAndBytes> resFolderInputs; |
| private final List<PathAndBytes> xmlInputs; |
| private List<String> proguardMapStrings; |
| private final List<PathAndBytes> manifest; |
| private final Map<PathAndBytes, FeatureSplit> resourceTables; |
| |
| public static class Builder { |
| |
| private final Map<String, byte[]> dexInputs = new HashMap<>(); |
| private final List<PathAndBytes> resFolderInputs = new ArrayList<>(); |
| private final List<PathAndBytes> xmlInputs = new ArrayList<>(); |
| |
| private final List<PathAndBytes> manifests = new ArrayList<>(); |
| private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>(); |
| private List<String> proguardMapStrings; |
| |
| private Builder() {} |
| |
| public Builder addManifest(Path path, byte[] bytes) { |
| manifests.add(new PathAndBytes(bytes, path)); |
| return this; |
| } |
| |
| public Builder addResourceTable(Path path, byte[] bytes, FeatureSplit featureSplit) { |
| resourceTables.put(new PathAndBytes(bytes, path), featureSplit); |
| try { |
| ResourceTable resourceTable = ResourceTable.parseFrom(bytes); |
| System.currentTimeMillis(); |
| } catch (InvalidProtocolBufferException e) { |
| throw new RuntimeException(e); |
| } |
| return this; |
| } |
| |
| public Builder addDexInput(String classesLocation, byte[] bytes) { |
| dexInputs.put(classesLocation, bytes); |
| return this; |
| } |
| |
| public Builder addResFolderInput(Path path, byte[] bytes) { |
| resFolderInputs.add(new PathAndBytes(bytes, path)); |
| return this; |
| } |
| |
| public Builder addXmlInput(Path path, byte[] bytes) { |
| xmlInputs.add(new PathAndBytes(bytes, path)); |
| return this; |
| } |
| |
| public LegacyResourceShrinker build() { |
| assert manifests != null && resourceTables != null; |
| return new LegacyResourceShrinker( |
| dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs, proguardMapStrings); |
| } |
| |
| public void setProguardMapStrings(List<String> proguardMapStrings) { |
| this.proguardMapStrings = proguardMapStrings; |
| } |
| } |
| |
| private LegacyResourceShrinker( |
| Map<String, byte[]> dexInputs, |
| List<PathAndBytes> resFolderInputs, |
| List<PathAndBytes> manifests, |
| Map<PathAndBytes, FeatureSplit> resourceTables, |
| List<PathAndBytes> xmlInputs, |
| List<String> proguardMapStrings) { |
| this.dexInputs = dexInputs; |
| this.resFolderInputs = resFolderInputs; |
| this.manifest = manifests; |
| this.resourceTables = resourceTables; |
| this.xmlInputs = xmlInputs; |
| this.proguardMapStrings = proguardMapStrings; |
| } |
| |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException { |
| R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, true); |
| for (PathAndBytes pathAndBytes : resourceTables.keySet()) { |
| ResourceTable loadedResourceTable = ResourceTable.parseFrom(pathAndBytes.bytes); |
| model.instantiateFromResourceTable(loadedResourceTable, false); |
| } |
| return shrinkModel(model, false); |
| } |
| |
| public ShrinkerResult shrinkModel( |
| R8ResourceShrinkerModel model, boolean exactMatchingOfStyleablesAndAttr) throws IOException { |
| if (proguardMapStrings != null) { |
| new ProguardMappingsRecorder(proguardMapStrings).recordObfuscationMappings(model); |
| proguardMapStrings = null; |
| } |
| for (Entry<String, byte[]> entry : dexInputs.entrySet()) { |
| // The analysis needs an origin for the dex files, synthesize an easy recognizable one. |
| Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex"); |
| R8ResourceShrinker.runResourceShrinkerAnalysis( |
| entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model)); |
| } |
| for (PathAndBytes pathAndBytes : manifest) { |
| ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode( |
| XmlNode.parseFrom(pathAndBytes.bytes), model); |
| } |
| for (PathAndBytes xmlInput : xmlInputs) { |
| if (xmlInput.path.startsWith("res/raw")) { |
| ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(xmlInput.getBytes()), model); |
| } |
| } |
| |
| ImmutableMap<String, PathAndBytes> resFolderMappings = |
| new ImmutableMap.Builder<String, PathAndBytes>() |
| .putAll( |
| xmlInputs.stream() |
| .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a))) |
| .putAll( |
| resFolderInputs.stream() |
| .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a))) |
| .build(); |
| for (PathAndBytes pathAndBytes : resourceTables.keySet()) { |
| ResourceTable resourceTable = ResourceTable.parseFrom(pathAndBytes.bytes); |
| new ProtoResourcesGraphBuilder( |
| new ResFolderFileTree() { |
| @Override |
| public byte[] getEntryByName(String pathInRes) { |
| return resFolderMappings.get(pathInRes).getBytes(); |
| } |
| }, |
| unused -> resourceTable) |
| .buildGraph(model); |
| } |
| ResourceStore resourceStore = model.getResourceStore(); |
| resourceStore.processToolsAttributes(); |
| model.keepPossiblyReferencedResources(); |
| // Transitively mark the reachable resources in the model. |
| // Finds unused resources in provided resources collection. |
| // Marks all used resources as 'reachable' in original collection. |
| List<Resource> unusedResources = |
| ResourcesUtil.findUnusedResources(model.getResourceStore().getResources(), x -> {}); |
| ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>(); |
| for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) { |
| if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) { |
| resEntriesToKeep.add(xmlInput.path.toString()); |
| } |
| } |
| List<Integer> resourceIdsToRemove = |
| getResourceIdsToRemove(unusedResources, model, exactMatchingOfStyleablesAndAttr); |
| Map<FeatureSplit, ResourceTable> shrunkenTables = new HashMap<>(); |
| for (Entry<PathAndBytes, FeatureSplit> entry : resourceTables.entrySet()) { |
| ResourceTable shrunkenResourceTable = |
| ResourceTableUtilKt.nullOutEntriesWithIds( |
| ResourceTable.parseFrom(entry.getKey().bytes), resourceIdsToRemove); |
| shrunkenTables.put(entry.getValue(), shrunkenResourceTable); |
| } |
| return new ShrinkerResult(resEntriesToKeep.build(), shrunkenTables); |
| } |
| |
| private static List<Integer> getResourceIdsToRemove( |
| List<Resource> unusedResources, |
| R8ResourceShrinkerModel model, |
| boolean exactMatchingOfStyleablesAndAttr) { |
| if (!exactMatchingOfStyleablesAndAttr) { |
| return unusedResources.stream() |
| .filter(s -> s.type != ResourceType.ID) |
| .map(resource -> resource.value) |
| .collect(Collectors.toList()); |
| } |
| return model.getResourceStore().getResources().stream() |
| .filter(r -> !r.isReachable()) |
| .filter(r -> r.type != ResourceType.ID) |
| .map(r -> r.value) |
| .collect(Collectors.toList()); |
| } |
| |
| // Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency |
| // for. |
| /** |
| * Returns a character reader for the given bytes, which must be a UTF encoded file. |
| * |
| * <p>The reader does not need to be closed by the caller (because the file is read in full in one |
| * shot and the resulting array is then wrapped in a byte array input stream, which does not need |
| * to be closed.) |
| */ |
| public static Reader getUtfReader(byte[] bytes) throws IOException { |
| int length = bytes.length; |
| if (length == 0) { |
| return new StringReader(""); |
| } |
| |
| switch (bytes[0]) { |
| case (byte) 0xEF: |
| { |
| if (length >= 3 && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { |
| // UTF-8 BOM: EF BB BF: Skip it |
| return new InputStreamReader(new ByteArrayInputStream(bytes, 3, length - 3), UTF_8); |
| } |
| break; |
| } |
| case (byte) 0xFE: |
| { |
| if (length >= 2 && bytes[1] == (byte) 0xFF) { |
| // UTF-16 Big Endian BOM: FE FF |
| return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16BE); |
| } |
| break; |
| } |
| case (byte) 0xFF: |
| { |
| if (length >= 2 && bytes[1] == (byte) 0xFE) { |
| if (length >= 4 && bytes[2] == (byte) 0x00 && bytes[3] == (byte) 0x00) { |
| // UTF-32 Little Endian BOM: FF FE 00 00 |
| return new InputStreamReader( |
| new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32LE"); |
| } |
| |
| // UTF-16 Little Endian BOM: FF FE |
| return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16LE); |
| } |
| break; |
| } |
| case (byte) 0x00: |
| { |
| if (length >= 4 |
| && bytes[0] == (byte) 0x00 |
| && bytes[1] == (byte) 0x00 |
| && bytes[2] == (byte) 0xFE |
| && bytes[3] == (byte) 0xFF) { |
| // UTF-32 Big Endian BOM: 00 00 FE FF |
| return new InputStreamReader( |
| new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32BE"); |
| } |
| break; |
| } |
| } |
| |
| // No byte order mark: Assume UTF-8 (where the BOM is optional). |
| return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8); |
| } |
| |
| public static class ShrinkerResult { |
| private final Set<String> resFolderEntriesToKeep; |
| private final Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat; |
| |
| public ShrinkerResult( |
| Set<String> resFolderEntriesToKeep, |
| Map<FeatureSplit, ResourceTable> resourceTableInProtoFormat) { |
| this.resFolderEntriesToKeep = resFolderEntriesToKeep; |
| this.resourceTableInProtoFormat = resourceTableInProtoFormat; |
| } |
| |
| public byte[] getResourceTableInProtoFormat(FeatureSplit featureSplit) { |
| return resourceTableInProtoFormat.get(featureSplit).toByteArray(); |
| } |
| |
| public Set<String> getResFolderEntriesToKeep() { |
| return resFolderEntriesToKeep; |
| } |
| } |
| |
| private static class PathAndBytes { |
| private final byte[] bytes; |
| private final Path path; |
| |
| private PathAndBytes(byte[] bytes, Path path) { |
| this.bytes = bytes; |
| this.path = path; |
| } |
| |
| public Path getPath() { |
| return path; |
| } |
| |
| public String getPathWithoutRes() { |
| assert path.toString().startsWith("res/"); |
| return path.toString().substring(4); |
| } |
| |
| public byte[] getBytes() { |
| return bytes; |
| } |
| } |
| } |