aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools
diff options
context:
space:
mode:
authorGravatar corysmith <corysmith@google.com>2018-08-01 09:59:34 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-08-01 10:01:47 -0700
commit8beb8bb95d7e3be23288acff71afd634e8c836bf (patch)
treeee6b34ec502a256f755b88cd3508f10b6ac2e0ba /src/tools
parentbf68cda03d8efc697aedb13e75d0511fe5297f2b (diff)
Add support for extracting references and declarations from a proto formatted apk
RELNOTES: None PiperOrigin-RevId: 206945173
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java6
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/aapt2/ProtoApk.java375
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java2
3 files changed, 377 insertions, 6 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java b/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java
index ac89b36ee3..65c3438ba8 100644
--- a/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java
+++ b/src/tools/android/java/com/google/devtools/build/android/Aapt2ResourcePackagingAction.java
@@ -60,7 +60,7 @@ public class Aapt2ResourcePackagingAction {
private static Options options;
public static void main(String[] args) throws Exception {
- InMemoryProfiler profiler = InMemoryProfiler.createAndStart("setup");
+ Profiler profiler = InMemoryProfiler.createAndStart("setup");
OptionsParser optionsParser =
OptionsParser.newOptionsParser(Options.class, Aapt2ConfigOptions.class);
optionsParser.enableParamsFileSupport(
@@ -192,11 +192,9 @@ public class Aapt2ResourcePackagingAction {
.copyRTxtTo(options.rOutput);
profiler.recordEndOf("link");
if (options.resourcesOutput != null) {
- // The compiled resources and the merged resources should be the same.
- // TODO(corysmith): Decompile or otherwise provide the exact resources in the apk.
packagedResources
.packageWith(mergedAndroidData.getResourceDir())
- .writeTo(options.resourcesOutput, false);
+ .writeTo(options.resourcesOutput, /* compress= */ false);
}
}
}
diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/ProtoApk.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/ProtoApk.java
new file mode 100644
index 0000000000..5a13ff8e0a
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/ProtoApk.java
@@ -0,0 +1,375 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.aapt2;
+
+import com.android.aapt.Resources;
+import com.android.aapt.Resources.Array;
+import com.android.aapt.Resources.Attribute.Symbol;
+import com.android.aapt.Resources.CompoundValue;
+import com.android.aapt.Resources.ConfigValue;
+import com.android.aapt.Resources.Entry;
+import com.android.aapt.Resources.FileReference;
+import com.android.aapt.Resources.FileReference.Type;
+import com.android.aapt.Resources.Item;
+import com.android.aapt.Resources.Package;
+import com.android.aapt.Resources.Plural;
+import com.android.aapt.Resources.Reference;
+import com.android.aapt.Resources.ResourceTable;
+import com.android.aapt.Resources.Style;
+import com.android.aapt.Resources.Value;
+import com.android.aapt.Resources.XmlAttribute;
+import com.android.aapt.Resources.XmlElement;
+import com.android.aapt.Resources.XmlNode;
+import com.android.resources.ResourceType;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Provides an interface to an apk in proto format. */
+public class ProtoApk {
+
+ private static final String RESOURCE_TABLE = "resources.pb";
+ private static final String MANIFEST = "AndroidManifest.xml";
+ private final FileSystem apkFileSystem;
+
+ private ProtoApk(FileSystem apkFileSystem) {
+ this.apkFileSystem = apkFileSystem;
+ }
+
+ /** Reads a ProtoApk from a path and verifies that it is in the expected format. */
+ public static ProtoApk readFrom(Path apkPath) throws IOException {
+ final FileSystem apkFileSystem =
+ FileSystems.newFileSystem(URI.create("jar:" + apkPath.toUri()), ImmutableMap.of());
+
+ Preconditions.checkArgument(Files.exists(apkFileSystem.getPath(RESOURCE_TABLE)));
+ Preconditions.checkArgument(Files.exists(apkFileSystem.getPath(MANIFEST)));
+ return new ProtoApk(apkFileSystem);
+ }
+
+ /** Visits all resource declarations and references using the {@link ResourceVisitor}. */
+ public <T extends ResourceVisitor<T>> T visitResources(T sink) throws IOException {
+
+ // visit manifest
+ visitXmlResource(apkFileSystem.getPath(MANIFEST), sink.enteringManifest());
+
+ // visit resource table and associated files.
+ final ResourceTable resourceTable =
+ ResourceTable.parseFrom(Files.newInputStream(apkFileSystem.getPath(RESOURCE_TABLE)));
+
+ final List<String> sourcePool =
+ resourceTable.hasSourcePool()
+ ? decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray())
+ : ImmutableList.of();
+
+ for (Package pkg : resourceTable.getPackageList()) {
+ ResourcePackageVisitor pkgVisitor = sink.enteringPackage(pkg.getPackageId().getId());
+ for (Resources.Type type : pkg.getTypeList()) {
+ ResourceTypeVisitor typeVisitor =
+ pkgVisitor.enteringResourceType(
+ type.getTypeId().getId(), ResourceType.getEnum(type.getName()));
+ for (Entry entry : type.getEntryList()) {
+ ResourceValueVisitor entryVisitor =
+ typeVisitor.acceptDeclaration(entry.getName(), entry.getEntryId().getId());
+ for (ConfigValue configValue : entry.getConfigValueList()) {
+ if (configValue.hasValue()) {
+ visitValue(entryVisitor, configValue.getValue(), sourcePool);
+ }
+ }
+ }
+ }
+ }
+ return sink;
+ }
+
+ // TODO(corysmith): Centralize duplicated code with AndroidCompiledDataDeserializer.
+ private static List<String> decodeSourcePool(byte[] bytes) throws UnsupportedEncodingException {
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+
+ int stringCount = byteBuffer.getInt(8);
+ boolean isUtf8 = (byteBuffer.getInt(16) & (1 << 8)) != 0;
+ int stringsStart = byteBuffer.getInt(20);
+ // Position the ByteBuffer after the metadata
+ byteBuffer.position(28);
+
+ List<String> strings = new ArrayList<>();
+
+ for (int i = 0; i < stringCount; i++) {
+ int stringOffset = stringsStart + byteBuffer.getInt();
+
+ if (isUtf8) {
+ int characterCount = byteBuffer.get(stringOffset) & 0xFF;
+ if ((characterCount & 0x80) != 0) {
+ characterCount =
+ ((characterCount & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
+ }
+
+ stringOffset += (characterCount >= 0x80 ? 2 : 1);
+
+ int length = byteBuffer.get(stringOffset) & 0xFF;
+ if ((length & 0x80) != 0) {
+ length = ((length & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
+ }
+
+ stringOffset += (length >= 0x80 ? 2 : 1);
+
+ strings.add(new String(bytes, stringOffset, length, "UTF8"));
+ } else {
+ int characterCount = byteBuffer.get(stringOffset) & 0xFFFF;
+ if ((characterCount & 0x8000) != 0) {
+ characterCount =
+ ((characterCount & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
+ }
+
+ stringOffset += 2 * (characterCount >= 0x8000 ? 2 : 1);
+
+ int length = byteBuffer.get(stringOffset) & 0xFFFF;
+ if ((length & 0x8000) != 0) {
+ length = ((length & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
+ }
+
+ stringOffset += 2 * (length >= 0x8000 ? 2 : 1);
+
+ strings.add(new String(bytes, stringOffset, length, "UTF16"));
+ }
+ }
+
+ return strings;
+ }
+
+ private void visitValue(ResourceValueVisitor entryVisitor, Value value, List<String> sourcePool) {
+ if (value.hasSource()) {
+ entryVisitor.entering(apkFileSystem.getPath(sourcePool.get(value.getSource().getPathIdx())));
+ }
+ switch (value.getValueCase()) {
+ case ITEM:
+ visitItem(entryVisitor, value.getItem());
+ break;
+ case COMPOUND_VALUE:
+ visitCompoundValue(entryVisitor, value.getCompoundValue());
+ break;
+ default:
+ throw new IllegalStateException(
+ "Config value does not have a declared value case: " + value);
+ }
+ }
+
+ private void visitCompoundValue(ResourceValueVisitor entryVisitor, CompoundValue value) {
+ switch (value.getValueCase()) {
+ case STYLE:
+ visitStyle(entryVisitor, value);
+ break;
+ case STYLEABLE:
+ visitStyleable(entryVisitor, value);
+ break;
+ case ATTR:
+ visitAttr(entryVisitor, value);
+ break;
+ case ARRAY:
+ visitArray(entryVisitor, value);
+ break;
+ case PLURAL:
+ visitPlural(entryVisitor, value);
+ break;
+ default:
+ }
+ }
+
+ private void visitPlural(ResourceValueVisitor entryVisitor, CompoundValue value) {
+ value
+ .getPlural()
+ .getEntryList()
+ .stream()
+ .filter(Plural.Entry::hasItem)
+ .map(Plural.Entry::getItem)
+ .forEach(
+ i -> {
+ switch (i.getValueCase()) {
+ case FILE:
+ visitFile(entryVisitor, i.getFile());
+ break;
+ case REF:
+ visitReference(entryVisitor, i.getRef());
+ break;
+ default:
+ }
+ });
+ }
+
+ private void visitArray(ResourceValueVisitor entryVisitor, CompoundValue value) {
+ value
+ .getArray()
+ .getElementList()
+ .stream()
+ .filter(Array.Element::hasItem)
+ .map(Array.Element::getItem)
+ .forEach(
+ i -> {
+ switch (i.getValueCase()) {
+ case FILE:
+ visitFile(entryVisitor, i.getFile());
+ break;
+ case REF:
+ visitReference(entryVisitor, i.getRef());
+ break;
+ default:
+ }
+ });
+ }
+
+ private void visitAttr(ResourceValueVisitor entryVisitor, CompoundValue value) {
+ value
+ .getAttr()
+ .getSymbolList()
+ .stream()
+ .map(Symbol::getName)
+ .forEach(r -> visitReference(entryVisitor, r));
+ }
+
+ private void visitStyleable(ResourceValueVisitor entryVisitor, CompoundValue value) {
+ value.getStyleable().getEntryList().forEach(e -> visitReference(entryVisitor, e.getAttr()));
+ }
+
+ private void visitStyle(ResourceValueVisitor entryVisitor, CompoundValue value) {
+ final Style style = value.getStyle();
+ if (style.hasParent()) {
+ visitReference(entryVisitor, style.getParent());
+ }
+ for (Style.Entry entry : style.getEntryList()) {
+ if (entry.hasItem()) {
+ visitItem(entryVisitor, entry.getItem());
+ } else if (entry.hasKey()) {
+ visitReference(entryVisitor, entry.getKey());
+ }
+ }
+ }
+
+ private void visitItem(ResourceValueVisitor entryVisitor, Item item) {
+ switch (item.getValueCase()) {
+ case FILE:
+ visitFile(entryVisitor, item.getFile());
+ break;
+ case REF:
+ visitReference(entryVisitor, item.getRef());
+ break;
+ default:
+ }
+ }
+
+ private void visitFile(ResourceValueVisitor entryVisitor, FileReference file) {
+ final Path path = apkFileSystem.getPath(file.getPath());
+ if (file.getType() == Type.PROTO_XML) {
+ visitXmlResource(path, entryVisitor.entering(path));
+ } else if (file.getType() != Type.PNG) {
+ entryVisitor.acceptOpaqueFileType(path);
+ }
+ }
+
+ private void visitXmlResource(Path path, ReferenceVisitor sink) {
+ try (InputStream in = Files.newInputStream(path)) {
+ visit(XmlNode.parseFrom(in), sink);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void visit(XmlNode node, ReferenceVisitor sink) {
+ if (node.hasElement()) {
+ final XmlElement element = node.getElement();
+ element
+ .getAttributeList()
+ .stream()
+ .filter(XmlAttribute::hasCompiledItem)
+ .map(XmlAttribute::getCompiledItem)
+ .filter(Item::hasRef)
+ .map(Item::getRef)
+ .forEach(ref -> visitReference(sink, ref));
+ element.getChildList().forEach(child -> visit(child, sink));
+ }
+ }
+
+ private void visitReference(ReferenceVisitor visitor, Reference ref) {
+ if (ref.getId() != 0) {
+ visitor.accept(ref.getId());
+ } else if (!ref.getName().isEmpty()) {
+ visitor.accept(ref.getName());
+ } else {
+ throw new IllegalStateException("Reference without number or id in :" + ref);
+ }
+ }
+
+ /** Provides an entry point to recording declared and referenced resources in the apk. */
+ public interface ResourceVisitor<T extends ResourceVisitor<T>> {
+ /** Called when entering the manifest. */
+ ManifestVisitor enteringManifest();
+
+ /** Called when entering a resource package. */
+ ResourcePackageVisitor enteringPackage(int pkgId);
+ }
+
+ /** Provides a visitor for packages. */
+ public interface ResourcePackageVisitor {
+ /** Called when entering the resource types of the package. */
+ ResourceTypeVisitor enteringResourceType(int typeId, ResourceType type);
+ }
+
+ /** Visitor for resource types */
+ public interface ResourceTypeVisitor {
+ /**
+ * Called for resource declarations.
+ *
+ * @param name The name of the resource.
+ * @param resourceId The id of the resource, without the package and type.
+ * @return A visitor for accepting references to other resources from the declared resource.
+ */
+ ResourceValueVisitor acceptDeclaration(String name, int resourceId);
+ }
+
+ /** A manifest specific resource reference visitor. */
+ public interface ManifestVisitor extends ReferenceVisitor {}
+
+ /** General resource reference visitor. */
+ public interface ResourceValueVisitor extends ReferenceVisitor {
+ /** Called when entering the source of a value. Maybe called multiple times for each value. */
+ ReferenceVisitor entering(Path path);
+
+ /* Called when a raw resource contains a non-proto xml file type. */
+ void acceptOpaqueFileType(Path path);
+ }
+
+ /** Role interface for visiting resource references. */
+ public interface ReferenceVisitor {
+ /** Called when a reference is defined by name (resourceType/name). */
+ void accept(String name);
+
+ /** Called when a reference is defined by id (full id, with package and type.) */
+ void accept(int value);
+
+ /** Called when a reference has no id or name. */
+ default void acceptEmptyReference() {
+ // pass
+ }
+ }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java
index 9ea3703d42..2a47c74378 100644
--- a/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java
+++ b/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java
@@ -200,8 +200,6 @@ public class ResourceLinker {
/**
* Statically links the {@link CompiledResources} with the dependencies to produce a {@link
* StaticLibrary}.
- *
- * @throws IOException
*/
public StaticLibrary linkStatically(CompiledResources compiled) {
try {