result = new ArrayList<>(declaredFields.length);
+ for (final Field field : declaredFields) {
+ final Asn1Field annotation = field.getAnnotation(Asn1Field.class);
+ if (annotation == null) {
+ continue;
+ }
+ if (Modifier.isStatic(field.getModifiers())) {
+ throw new Asn1EncodingException(
+ Asn1Field.class.getName() + " used on a static field: "
+ + containerClass.getName() + "." + field.getName());
+ }
+ final AnnotatedField annotatedField;
+ try {
+ annotatedField = new AnnotatedField(container, field, annotation);
+ } catch (final Asn1EncodingException e) {
+ throw new Asn1EncodingException(
+ "Invalid ASN.1 annotation on "
+ + containerClass.getName() + "." + field.getName(),
+ e);
+ }
+ result.add(annotatedField);
+ }
+ return result;
+ }
+
+ private static byte[] toInteger(final int value) {
+ return Asn1DerEncoder.toInteger((long) value);
+ }
+
+ private static byte[] toInteger(final long value) {
+ return Asn1DerEncoder.toInteger(BigInteger.valueOf(value));
+ }
+
+ private static byte[] toInteger(final BigInteger value) {
+ return Asn1DerEncoder.createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
+ value.toByteArray());
+ }
+
+ private static byte[] toOid(final String oid) throws Asn1EncodingException {
+ final ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
+ final String[] nodes = oid.split("\\.");
+ if (nodes.length < 2) {
+ throw new Asn1EncodingException(
+ "OBJECT IDENTIFIER must contain at least two nodes: " + oid);
+ }
+ final int firstNode;
+ try {
+ firstNode = Integer.parseInt(nodes[0]);
+ } catch (final NumberFormatException e) {
+ throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
+ }
+ if ((firstNode > 6) || (firstNode < 0)) {
+ throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
+ }
+ final int secondNode;
+ try {
+ secondNode = Integer.parseInt(nodes[1]);
+ } catch (final NumberFormatException e) {
+ throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
+ }
+ if ((secondNode >= 40) || (secondNode < 0)) {
+ throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
+ }
+ final int firstByte = firstNode * 40 + secondNode;
+ if (firstByte > 0xff) {
+ throw new Asn1EncodingException(
+ "First two nodes out of range: " + firstNode + "." + secondNode);
+ }
+ encodedValue.write(firstByte);
+ for (int i = 2; i < nodes.length; i++) {
+ final String nodeString = nodes[i];
+ final int node;
+ try {
+ node = Integer.parseInt(nodeString);
+ } catch (final NumberFormatException e) {
+ throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
+ }
+ if (node < 0) {
+ throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
+ }
+ if (node <= 0x7f) {
+ encodedValue.write(node);
+ continue;
+ }
+ if (node < 1 << 14) {
+ encodedValue.write(0x80 | (node >> 7));
+ encodedValue.write(node & 0x7f);
+ continue;
+ }
+ if (node < 1 << 21) {
+ encodedValue.write(0x80 | (node >> 14));
+ encodedValue.write(0x80 | ((node >> 7) & 0x7f));
+ encodedValue.write(node & 0x7f);
+ continue;
+ }
+ throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
+ }
+ return Asn1DerEncoder.createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
+ encodedValue.toByteArray());
+ }
+
+ private static Object getMemberFieldValue(final Object obj, final Field field)
+ throws Asn1EncodingException {
+ try {
+ return field.get(obj);
+ } catch (final ReflectiveOperationException e) {
+ throw new Asn1EncodingException(
+ "Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
+ }
+ }
+
+ private static final class AnnotatedField {
+ public final Field field;
+ private final Object mObject;
+ public final Asn1Field annotation;
+ private final Asn1Type mDataType;
+ private final Asn1Type mElementDataType;
+ private final int mDerTagClass;
+ private final int mDerTagNumber;
+ private final Asn1Tagging mTagging;
+ private final boolean mOptional;
+
+ public AnnotatedField(@NonNull final Object obj, @NonNull final Field field, @NonNull final Asn1Field annotation)
+ throws Asn1EncodingException {
+ this.mObject = obj;
+ this.field = field;
+ this.annotation = annotation;
+ this.mDataType = annotation.type();
+ this.mElementDataType = annotation.elementType();
+ Asn1TagClass tagClass = annotation.cls();
+ if (tagClass == Asn1TagClass.Automatic) {
+ if (annotation.tagNumber() != -1) {
+ tagClass = Asn1TagClass.ContextSpecific;
+ } else {
+ tagClass = Asn1TagClass.Universal;
+ }
+ }
+ this.mDerTagClass = BerEncoding.getTagClass(tagClass);
+ final int tagNumber;
+ if (annotation.tagNumber() != -1) {
+ tagNumber = annotation.tagNumber();
+ } else if ((this.mDataType == Asn1Type.Choice) || (this.mDataType == Asn1Type.Any)) {
+ tagNumber = -1;
+ } else {
+ tagNumber = BerEncoding.getTagNumber(this.mDataType);
+ }
+ this.mDerTagNumber = tagNumber;
+ this.mTagging = annotation.tagging();
+ if (((this.mTagging == Asn1Tagging.Explicit) || (this.mTagging == Asn1Tagging.Implicit))
+ && (annotation.tagNumber() == -1)) {
+ throw new Asn1EncodingException(
+ "Tag number must be specified when tagging mode is " + this.mTagging);
+ }
+ this.mOptional = annotation.optional();
+ }
+
+ @Nullable
+ public byte[] toDer() throws Asn1EncodingException {
+ final Object fieldValue = Asn1DerEncoder.getMemberFieldValue(this.mObject, this.field);
+ if (fieldValue == null) {
+ if (this.mOptional) {
+ return null;
+ }
+ throw new Asn1EncodingException("Required field not set");
+ }
+ final byte[] encoded = JavaToDerConverter.toDer(fieldValue, this.mDataType, this.mElementDataType);
+ switch (this.mTagging) {
+ case Normal:
+ return encoded;
+ case Explicit:
+ return Asn1DerEncoder.createTag(this.mDerTagClass, true, this.mDerTagNumber, encoded);
+ case Implicit:
+ final int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
+ if (originalTagNumber == 0x1f) {
+ throw new Asn1EncodingException("High-tag-number form not supported");
+ }
+ if (this.mDerTagNumber >= 0x1f) {
+ throw new Asn1EncodingException(
+ "Unsupported high tag number: " + this.mDerTagNumber);
+ }
+ encoded[0] = BerEncoding.setTagNumber(encoded[0], this.mDerTagNumber);
+ encoded[0] = BerEncoding.setTagClass(encoded[0], this.mDerTagClass);
+ return encoded;
+ default:
+ throw new RuntimeException("Unknown tagging mode: " + this.mTagging);
+ }
+ }
+ }
+
+ private static byte[] createTag(
+ final int tagClass, final boolean constructed, final int tagNumber, final byte[]... contents) {
+ if (tagNumber >= 0x1f) {
+ throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
+ }
+ // tag class & number fit into the first byte
+ final byte firstIdentifierByte =
+ (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
+ int contentsLength = 0;
+ for (final byte[] c : contents) {
+ contentsLength += c.length;
+ }
+ int contentsPosInResult;
+ final byte[] result;
+ if (contentsLength < 0x80) {
+ // Length fits into one byte
+ contentsPosInResult = 2;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[0] = firstIdentifierByte;
+ result[1] = (byte) contentsLength;
+ } else {
+ // Length is represented as multiple bytes
+ // The low 7 bits of the first byte represent the number of length bytes (following the
+ // first byte) in which the length is in big-endian base-256 form
+ if (contentsLength <= 0xff) {
+ contentsPosInResult = 3;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x81;
+ // 1 length byte
+ result[2] = (byte) contentsLength;
+ } else if (contentsLength <= 0xffff) {
+ contentsPosInResult = 4;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x82;
+ // 2 length bytes
+ result[2] = (byte) (contentsLength >> 8);
+ result[3] = (byte) (contentsLength & 0xff);
+ } else if (contentsLength <= 0xffffff) {
+ contentsPosInResult = 5;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x83;
+ // 3 length bytes
+ result[2] = (byte) (contentsLength >> 16);
+ result[3] = (byte) ((contentsLength >> 8) & 0xff);
+ result[4] = (byte) (contentsLength & 0xff);
+ } else {
+ contentsPosInResult = 6;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x84;
+ // 4 length bytes
+ result[2] = (byte) (contentsLength >> 24);
+ result[3] = (byte) ((contentsLength >> 16) & 0xff);
+ result[4] = (byte) ((contentsLength >> 8) & 0xff);
+ result[5] = (byte) (contentsLength & 0xff);
+ }
+ result[0] = firstIdentifierByte;
+ }
+ for (final byte[] c : contents) {
+ System.arraycopy(c, 0, result, contentsPosInResult, c.length);
+ contentsPosInResult += c.length;
+ }
+ return result;
+ }
+
+ private static final class JavaToDerConverter {
+ private JavaToDerConverter() {
+ }
+
+ public static byte[] toDer(@NonNull final Object source, final Asn1Type targetType, @Nullable final Asn1Type targetElementType)
+ throws Asn1EncodingException {
+ final Class> sourceType = source.getClass();
+ if (Asn1OpaqueObject.class.equals(sourceType)) {
+ final ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
+ final byte[] result = new byte[buf.remaining()];
+ buf.get(result);
+ return result;
+ }
+ if ((targetType == null) || (targetType == Asn1Type.Any)) {
+ return Asn1DerEncoder.encode(source);
+ }
+ switch (targetType) {
+ case OctetString:
+ byte[] value = null;
+ if (source instanceof ByteBuffer) {
+ final ByteBuffer buf = (ByteBuffer) source;
+ value = new byte[buf.remaining()];
+ buf.slice().get(value);
+ } else if (source instanceof byte[]) {
+ value = (byte[]) source;
+ }
+ if (value != null) {
+ return Asn1DerEncoder.createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL,
+ false,
+ BerEncoding.TAG_NUMBER_OCTET_STRING,
+ value);
+ }
+ break;
+ case Integer:
+ if (source instanceof Integer) {
+ return Asn1DerEncoder.toInteger((Integer) source);
+ } else if (source instanceof Long) {
+ return Asn1DerEncoder.toInteger((Long) source);
+ } else if (source instanceof BigInteger) {
+ return Asn1DerEncoder.toInteger((BigInteger) source);
+ }
+ break;
+ case ObjectIdentifier:
+ if (source instanceof String) {
+ return Asn1DerEncoder.toOid((String) source);
+ }
+ break;
+ case Sequence: {
+ final Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.Sequence)) {
+ return Asn1DerEncoder.toSequence(source);
+ }
+ break;
+ }
+ case Choice: {
+ final Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.Choice)) {
+ return Asn1DerEncoder.toChoice(source);
+ }
+ break;
+ }
+ case SetOf:
+ return Asn1DerEncoder.toSetOf((Collection>) source, targetElementType);
+ default:
+ break;
+ }
+ throw new Asn1EncodingException(
+ "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java
new file mode 100644
index 00000000..a04f7529
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1;
+
+/**
+ * Indicates that an ASN.1 structure could not be encoded.
+ */
+public class Asn1EncodingException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public Asn1EncodingException(final String message) {
+ super(message);
+ }
+
+ public Asn1EncodingException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java
new file mode 100644
index 00000000..b54adcb7
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Asn1Field {
+ /**
+ * Index used to order fields in a container. Required for fields of SEQUENCE containers.
+ */
+ int index() default 0;
+
+ Asn1TagClass cls() default Asn1TagClass.Automatic;
+
+ Asn1Type type();
+
+ /**
+ * Tagging mode. Default: NORMAL.
+ */
+ Asn1Tagging tagging() default Asn1Tagging.Normal;
+
+ /**
+ * Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.
+ */
+ int tagNumber() default -1;
+
+ /**
+ * {@code true} if this field is optional. Ignored for fields of CHOICE containers.
+ */
+ boolean optional() default false;
+
+ /**
+ * Type of elements. Used only for SET_OF or SEQUENCE_OF.
+ */
+ Asn1Type elementType() default Asn1Type.Any;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java
new file mode 100644
index 00000000..2b24bd4e
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Opaque holder of encoded ASN.1 stuff.
+ */
+public class Asn1OpaqueObject {
+ private final ByteBuffer encoded;
+
+ public Asn1OpaqueObject(final @NonNull ByteBuffer encoded) {
+ this.encoded = encoded.slice();
+ }
+
+ public Asn1OpaqueObject(final @NonNull byte[] encoded) {
+ this.encoded = ByteBuffer.wrap(encoded);
+ }
+
+ @NonNull
+ public ByteBuffer getEncoded() {
+ return this.encoded.slice();
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt
new file mode 100644
index 00000000..246fa87e
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1
+
+enum class Asn1TagClass {
+ Universal, Application, ContextSpecific, Private,
+
+ /**
+ * Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
+ * automatically.
+ */
+ Automatic
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt
new file mode 100644
index 00000000..b8f45764
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1
+
+enum class Asn1Tagging {
+ Normal, Explicit, Implicit
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt
new file mode 100644
index 00000000..28401aea
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1
+
+enum class Asn1Type {
+ Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence, SequenceOf, SetOf
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java
new file mode 100644
index 00000000..71d764ef
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1.ber;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
+ */
+public class BerDataValue {
+ private final ByteBuffer encoded;
+ private final ByteBuffer encodedContents;
+ /**
+ * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
+ * constants.
+ */
+ public final int tagClass;
+ /**
+ * Returns {@code true} if the content octets of this data value are the complete BER encoding
+ * of one or more data values, {@code false} if the content octets of this data value directly
+ * represent the value.
+ */
+ public final boolean isConstructed;
+ /**
+ * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
+ * constants.
+ */
+ public final int tagNumber;
+
+ BerDataValue(
+ @NonNull final ByteBuffer encoded,
+ @NonNull final ByteBuffer encodedContents,
+ final int tagClass,
+ final boolean constructed,
+ final int tagNumber) {
+ this.encoded = encoded;
+ this.encodedContents = encodedContents;
+ this.tagClass = tagClass;
+ this.isConstructed = constructed;
+ this.tagNumber = tagNumber;
+ }
+
+ /**
+ * Returns the encoded form of this data value.
+ */
+ public ByteBuffer getEncoded() {
+ return this.encoded.slice();
+ }
+
+ /**
+ * Returns the encoded contents of this data value.
+ */
+ @NonNull
+ public ByteBuffer getEncodedContents() {
+ return this.encodedContents.slice();
+ }
+
+ /**
+ * Returns a new reader of the contents of this data value.
+ */
+ @NonNull
+ public BerDataValueReader contentsReader() {
+ return new ByteBufferBerDataValueReader(this.getEncodedContents());
+ }
+
+ /**
+ * Returns a new reader which returns just this data value. This may be useful for re-reading
+ * this value in different contexts.
+ */
+ @NonNull
+ public BerDataValueReader dataValueReader() {
+ return new ParsedValueReader(this);
+ }
+
+ private static final class ParsedValueReader implements BerDataValueReader {
+ private final BerDataValue mValue;
+ private boolean mValueOutput;
+
+ public ParsedValueReader(final BerDataValue value) {
+ this.mValue = value;
+ }
+
+ @Nullable
+ @Override
+ public BerDataValue readDataValue() {
+ if (this.mValueOutput) {
+ return null;
+ }
+ this.mValueOutput = true;
+ return this.mValue;
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java
new file mode 100644
index 00000000..56ffcf18
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1.ber;
+
+/**
+ * Indicates that an ASN.1 data value being read could not be decoded using
+ * Basic Encoding Rules (BER).
+ */
+public class BerDataValueFormatException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public BerDataValueFormatException(final String message) {
+ super(message);
+ }
+
+ public BerDataValueFormatException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java
new file mode 100644
index 00000000..2407c804
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1.ber;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Reader of ASN.1 Basic Encoding Rules (BER) data values.
+ *
+ * BER data value reader returns data values, one by one, from a source. The interpretation of
+ * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
+ * the elements of a SEQUENCE value) is left to clients of the reader.
+ */
+public interface BerDataValueReader {
+
+ /**
+ * Returns the next data value or {@code null} if end of input has been reached.
+ *
+ * @throws BerDataValueFormatException if the value being read is malformed.
+ */
+ @Nullable
+ BerDataValue readDataValue() throws BerDataValueFormatException;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java
new file mode 100644
index 00000000..f13cec92
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1.ber;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1TagClass;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
+ */
+public abstract class BerEncoding {
+ private BerEncoding() {
+ }
+
+ /**
+ * Constructed vs primitive flag in the first identifier byte.
+ */
+ public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
+
+ /**
+ * Tag class: UNIVERSAL
+ */
+ public static final int TAG_CLASS_UNIVERSAL = 0;
+
+ /**
+ * Tag class: APPLICATION
+ */
+ public static final int TAG_CLASS_APPLICATION = 1;
+
+ /**
+ * Tag class: CONTEXT SPECIFIC
+ */
+ public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
+
+ /**
+ * Tag class: PRIVATE
+ */
+ public static final int TAG_CLASS_PRIVATE = 3;
+
+ /**
+ * Tag number: INTEGER
+ */
+ public static final int TAG_NUMBER_INTEGER = 0x2;
+
+ /**
+ * Tag number: OCTET STRING
+ */
+ public static final int TAG_NUMBER_OCTET_STRING = 0x4;
+
+ /**
+ * Tag number: NULL
+ */
+ public static final int TAG_NUMBER_NULL = 0x05;
+
+ /**
+ * Tag number: OBJECT IDENTIFIER
+ */
+ public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
+
+ /**
+ * Tag number: SEQUENCE
+ */
+ public static final int TAG_NUMBER_SEQUENCE = 0x10;
+
+ /**
+ * Tag number: SET
+ */
+ public static final int TAG_NUMBER_SET = 0x11;
+
+ public static int getTagNumber(final Asn1Type dataType) {
+ switch (dataType) {
+ case Integer:
+ return BerEncoding.TAG_NUMBER_INTEGER;
+ case ObjectIdentifier:
+ return BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER;
+ case OctetString:
+ return BerEncoding.TAG_NUMBER_OCTET_STRING;
+ case SetOf:
+ return BerEncoding.TAG_NUMBER_SET;
+ case Sequence:
+ case SequenceOf:
+ return BerEncoding.TAG_NUMBER_SEQUENCE;
+ default:
+ throw new IllegalArgumentException("Unsupported data type: " + dataType);
+ }
+ }
+
+ public static int getTagClass(final Asn1TagClass tagClass) {
+ switch (tagClass) {
+ case Application:
+ return BerEncoding.TAG_CLASS_APPLICATION;
+ case ContextSpecific:
+ return BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC;
+ case Private:
+ return BerEncoding.TAG_CLASS_PRIVATE;
+ case Universal:
+ return BerEncoding.TAG_CLASS_UNIVERSAL;
+ default:
+ throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
+ }
+ }
+
+ public static String tagClassToString(final int typeClass) {
+ switch (typeClass) {
+ case BerEncoding.TAG_CLASS_APPLICATION:
+ return "APPLICATION";
+ case BerEncoding.TAG_CLASS_CONTEXT_SPECIFIC:
+ return "";
+ case BerEncoding.TAG_CLASS_PRIVATE:
+ return "PRIVATE";
+ case BerEncoding.TAG_CLASS_UNIVERSAL:
+ return "UNIVERSAL";
+ default:
+ throw new IllegalArgumentException("Unsupported type class: " + typeClass);
+ }
+ }
+
+ public static String tagClassAndNumberToString(final int tagClass, final int tagNumber) {
+ final String classString = BerEncoding.tagClassToString(tagClass);
+ final String numberString = BerEncoding.tagNumberToString(tagNumber);
+ return classString.isEmpty() ? numberString : classString + " " + numberString;
+ }
+
+ public static String tagNumberToString(final int tagNumber) {
+ switch (tagNumber) {
+ case BerEncoding.TAG_NUMBER_INTEGER:
+ return "INTEGER";
+ case BerEncoding.TAG_NUMBER_OCTET_STRING:
+ return "OCTET STRING";
+ case BerEncoding.TAG_NUMBER_NULL:
+ return "NULL";
+ case BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER:
+ return "OBJECT IDENTIFIER";
+ case BerEncoding.TAG_NUMBER_SEQUENCE:
+ return "SEQUENCE";
+ case BerEncoding.TAG_NUMBER_SET:
+ return "SET";
+ default:
+ return "0x" + Integer.toHexString(tagNumber);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the provided first identifier byte indicates that the data value uses
+ * constructed encoding for its contents, or {@code false} if the data value uses primitive
+ * encoding for its contents.
+ */
+ public static boolean isConstructed(final byte firstIdentifierByte) {
+ return (firstIdentifierByte & BerEncoding.ID_FLAG_CONSTRUCTED_ENCODING) != 0;
+ }
+
+ /**
+ * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
+ * constants.
+ */
+ public static int getTagClass(final byte firstIdentifierByte) {
+ return (firstIdentifierByte & 0xff) >> 6;
+ }
+
+ public static byte setTagClass(final byte firstIdentifierByte, final int tagClass) {
+ return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6));
+ }
+
+ /**
+ * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
+ * constants.
+ */
+ public static int getTagNumber(final byte firstIdentifierByte) {
+ return firstIdentifierByte & 0x1f;
+ }
+
+ public static byte setTagNumber(final byte firstIdentifierByte, final int tagNumber) {
+ return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java
new file mode 100644
index 00000000..1c064734
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1.ber;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class ByteBufferBerDataValueReader implements BerDataValueReader {
+ private final ByteBuffer mBuf;
+
+ public ByteBufferBerDataValueReader(final @NonNull ByteBuffer buf) {
+ this.mBuf = buf;
+ }
+
+ @Nullable
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ final int startPosition = this.mBuf.position();
+ if (!this.mBuf.hasRemaining()) {
+ return null;
+ }
+ final byte firstIdentifierByte = this.mBuf.get();
+ final int tagNumber = this.readTagNumber(firstIdentifierByte);
+ final boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
+ if (!this.mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Missing length");
+ }
+ final int firstLengthByte = this.mBuf.get() & 0xff;
+ final int contentsLength;
+ final int contentsOffsetInTag;
+ if ((firstLengthByte & 0x80) == 0) {
+ // short form length
+ contentsLength = this.readShortFormLength(firstLengthByte);
+ contentsOffsetInTag = this.mBuf.position() - startPosition;
+ this.skipDefiniteLengthContents(contentsLength);
+ } else if (firstLengthByte != 0x80) {
+ // long form length
+ contentsLength = this.readLongFormLength(firstLengthByte);
+ contentsOffsetInTag = this.mBuf.position() - startPosition;
+ this.skipDefiniteLengthContents(contentsLength);
+ } else {
+ // indefinite length -- value ends with 0x00 0x00
+ contentsOffsetInTag = this.mBuf.position() - startPosition;
+ contentsLength =
+ constructed
+ ? this.skipConstructedIndefiniteLengthContents()
+ : this.skipPrimitiveIndefiniteLengthContents();
+ }
+ // Create the encoded data value ByteBuffer
+ final int endPosition = this.mBuf.position();
+ this.mBuf.position(startPosition);
+ final int bufOriginalLimit = this.mBuf.limit();
+ this.mBuf.limit(endPosition);
+ final ByteBuffer encoded = this.mBuf.slice();
+ this.mBuf.position(this.mBuf.limit());
+ this.mBuf.limit(bufOriginalLimit);
+ // Create the encoded contents ByteBuffer
+ encoded.position(contentsOffsetInTag);
+ encoded.limit(contentsOffsetInTag + contentsLength);
+ final ByteBuffer encodedContents = encoded.slice();
+ encoded.clear();
+ return new BerDataValue(
+ encoded,
+ encodedContents,
+ BerEncoding.getTagClass(firstIdentifierByte),
+ constructed,
+ tagNumber);
+ }
+
+ private int readTagNumber(final byte firstIdentifierByte) throws BerDataValueFormatException {
+ final int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
+ if (tagNumber == 0x1f) {
+ // high-tag-number form, where the tag number follows this byte in base-128
+ // big-endian form, where each byte has the highest bit set, except for the last
+ // byte
+ return this.readHighTagNumber();
+ } else {
+ // low-tag-number form
+ return tagNumber;
+ }
+ }
+
+ private int readHighTagNumber() throws BerDataValueFormatException {
+ // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+ // byte
+ int b;
+ int result = 0;
+ do {
+ if (!this.mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Truncated tag number");
+ }
+ b = this.mBuf.get();
+ if (result > Integer.MAX_VALUE >>> 7) {
+ throw new BerDataValueFormatException("Tag number too large");
+ }
+ result <<= 7;
+ result |= b & 0x7f;
+ } while ((b & 0x80) != 0);
+ return result;
+ }
+
+ private int readShortFormLength(final int firstLengthByte) {
+ return firstLengthByte & 0x7f;
+ }
+
+ private int readLongFormLength(final int firstLengthByte) throws BerDataValueFormatException {
+ // The low 7 bits of the first byte represent the number of bytes (following the first
+ // byte) in which the length is in big-endian base-256 form
+ final int byteCount = firstLengthByte & 0x7f;
+ if (byteCount > 4) {
+ throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+ }
+ int result = 0;
+ for (int i = 0; i < byteCount; i++) {
+ if (!this.mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Truncated length");
+ }
+ final int b = this.mBuf.get();
+ if (result > Integer.MAX_VALUE >>> 8) {
+ throw new BerDataValueFormatException("Length too large");
+ }
+ result <<= 8;
+ result |= b & 0xff;
+ }
+ return result;
+ }
+
+ private void skipDefiniteLengthContents(final int contentsLength) throws BerDataValueFormatException {
+ if (this.mBuf.remaining() < contentsLength) {
+ throw new BerDataValueFormatException(
+ "Truncated contents. Need: " + contentsLength + " bytes, available: "
+ + this.mBuf.remaining());
+ }
+ this.mBuf.position(this.mBuf.position() + contentsLength);
+ }
+
+ private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00
+ boolean prevZeroByte = false;
+ int bytesRead = 0;
+ while (true) {
+ if (!this.mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+
+ }
+ final int b = this.mBuf.get();
+ bytesRead++;
+ if (bytesRead < 0) {
+ throw new BerDataValueFormatException("Indefinite-length contents too long");
+ }
+ if (b == 0) {
+ if (prevZeroByte) {
+ // End of contents reached -- we've read the value and its terminator 0x00 0x00
+ return bytesRead - 2;
+ }
+ prevZeroByte = true;
+ } else {
+ prevZeroByte = false;
+ }
+ }
+ }
+
+ private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
+ // can contain data values which are themselves indefinite length encoded. As a result, we
+ // must parse the direct children of this data value to correctly skip over the contents of
+ // this data value.
+ final int startPos = this.mBuf.position();
+ while (this.mBuf.hasRemaining()) {
+ // Check whether the 0x00 0x00 terminator is at current position
+ if ((this.mBuf.remaining() > 1) && (this.mBuf.getShort(this.mBuf.position()) == 0)) {
+ final int contentsLength = this.mBuf.position() - startPos;
+ this.mBuf.position(this.mBuf.position() + 2);
+ return contentsLength;
+ }
+ // No luck. This must be a BER-encoded data value -- skip over it by parsing it
+ this.readDataValue();
+ }
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: "
+ + (this.mBuf.position() - startPos) + " bytes read");
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java
new file mode 100644
index 00000000..05f021c2
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.asn1.ber;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
+ * values. See {@code X.690} for the encoding.
+ */
+public class InputStreamBerDataValueReader implements BerDataValueReader {
+ private final InputStream mIn;
+
+ public InputStreamBerDataValueReader(final InputStream in) {
+ if (in == null) {
+ throw new NullPointerException("in == null");
+ }
+ this.mIn = in;
+ }
+
+ @Override
+ @Nullable
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ return InputStreamBerDataValueReader.readDataValue(this.mIn);
+ }
+
+ /**
+ * Returns the next data value or {@code null} if end of input has been reached.
+ *
+ * @throws BerDataValueFormatException if the value being read is malformed.
+ */
+ @Nullable
+ private static BerDataValue readDataValue(final InputStream input)
+ throws BerDataValueFormatException {
+ final RecordingInputStream in = new RecordingInputStream(input);
+
+ try {
+ final int firstIdentifierByte = in.read();
+ if (firstIdentifierByte == -1) {
+ // End of input
+ return null;
+ }
+ final int tagNumber = InputStreamBerDataValueReader.readTagNumber(in, firstIdentifierByte);
+
+ final int firstLengthByte = in.read();
+ if (firstLengthByte == -1) {
+ throw new BerDataValueFormatException("Missing length");
+ }
+
+ final boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
+ final int contentsLength;
+ final int contentsOffsetInDataValue;
+ if ((firstLengthByte & 0x80) == 0) {
+ // short form length
+ contentsLength = InputStreamBerDataValueReader.readShortFormLength(firstLengthByte);
+ contentsOffsetInDataValue = in.getReadByteCount();
+ InputStreamBerDataValueReader.skipDefiniteLengthContents(in, contentsLength);
+ } else if ((firstLengthByte & 0xff) != 0x80) {
+ // long form length
+ contentsLength = InputStreamBerDataValueReader.readLongFormLength(in, firstLengthByte);
+ contentsOffsetInDataValue = in.getReadByteCount();
+ InputStreamBerDataValueReader.skipDefiniteLengthContents(in, contentsLength);
+ } else {
+ // indefinite length
+ contentsOffsetInDataValue = in.getReadByteCount();
+ contentsLength =
+ constructed
+ ? InputStreamBerDataValueReader.skipConstructedIndefiniteLengthContents(in)
+ : InputStreamBerDataValueReader.skipPrimitiveIndefiniteLengthContents(in);
+ }
+
+ final byte[] encoded = in.getReadBytes();
+ final ByteBuffer encodedContents =
+ ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
+ return new BerDataValue(
+ ByteBuffer.wrap(encoded),
+ encodedContents,
+ BerEncoding.getTagClass((byte) firstIdentifierByte),
+ constructed,
+ tagNumber);
+ } catch (final IOException e) {
+ throw new BerDataValueFormatException("Failed to read data value", e);
+ }
+ }
+
+ private static int readTagNumber(final InputStream in, final int firstIdentifierByte)
+ throws IOException, BerDataValueFormatException {
+ final int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
+ if (tagNumber == 0x1f) {
+ // high-tag-number form
+ return InputStreamBerDataValueReader.readHighTagNumber(in);
+ } else {
+ // low-tag-number form
+ return tagNumber;
+ }
+ }
+
+ private static int readHighTagNumber(final InputStream in)
+ throws IOException, BerDataValueFormatException {
+ // Base-128 big-endian form, where each byte has the highest bit set, except for the last
+ // byte where the highest bit is not set
+ int b;
+ int result = 0;
+ do {
+ b = in.read();
+ if (b == -1) {
+ throw new BerDataValueFormatException("Truncated tag number");
+ }
+ if (result > Integer.MAX_VALUE >>> 7) {
+ throw new BerDataValueFormatException("Tag number too large");
+ }
+ result <<= 7;
+ result |= b & 0x7f;
+ } while ((b & 0x80) != 0);
+ return result;
+ }
+
+ private static int readShortFormLength(final int firstLengthByte) {
+ return firstLengthByte & 0x7f;
+ }
+
+ private static int readLongFormLength(final InputStream in, final int firstLengthByte)
+ throws IOException, BerDataValueFormatException {
+ // The low 7 bits of the first byte represent the number of bytes (following the first
+ // byte) in which the length is in big-endian base-256 form
+ final int byteCount = firstLengthByte & 0x7f;
+ if (byteCount > 4) {
+ throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
+ }
+ int result = 0;
+ for (int i = 0; i < byteCount; i++) {
+ final int b = in.read();
+ if (b == -1) {
+ throw new BerDataValueFormatException("Truncated length");
+ }
+ if (result > Integer.MAX_VALUE >>> 8) {
+ throw new BerDataValueFormatException("Length too large");
+ }
+ result <<= 8;
+ result |= b & 0xff;
+ }
+ return result;
+ }
+
+ private static void skipDefiniteLengthContents(final InputStream in, int len)
+ throws IOException, BerDataValueFormatException {
+ long bytesRead = 0;
+ while (len > 0) {
+ final int skipped = (int) in.skip(len);
+ if (skipped <= 0) {
+ throw new BerDataValueFormatException(
+ "Truncated definite-length contents: " + bytesRead + " bytes read"
+ + ", " + len + " missing");
+ }
+ len -= skipped;
+ bytesRead += skipped;
+ }
+ }
+
+ private static int skipPrimitiveIndefiniteLengthContents(final InputStream in)
+ throws IOException, BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00
+ boolean prevZeroByte = false;
+ int bytesRead = 0;
+ while (true) {
+ final int b = in.read();
+ if (b == -1) {
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+ }
+ bytesRead++;
+ if (bytesRead < 0) {
+ throw new BerDataValueFormatException("Indefinite-length contents too long");
+ }
+ if (b == 0) {
+ if (prevZeroByte) {
+ // End of contents reached -- we've read the value and its terminator 0x00 0x00
+ return bytesRead - 2;
+ }
+ prevZeroByte = true;
+ } else {
+ prevZeroByte = false;
+ }
+ }
+ }
+
+ private static int skipConstructedIndefiniteLengthContents(final RecordingInputStream in)
+ throws BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
+ // can contain data values which are indefinite length encoded as well. As a result, we
+ // must parse the direct children of this data value to correctly skip over the contents of
+ // this data value.
+ final int readByteCountBefore = in.getReadByteCount();
+ while (true) {
+ // We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
+ // Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
+ // then check below to see whether it's 0x00 0x00.
+ final BerDataValue dataValue = InputStreamBerDataValueReader.readDataValue(in);
+ if (dataValue == null) {
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: "
+ + (in.getReadByteCount() - readByteCountBefore) + " bytes read");
+ }
+ if (in.getReadByteCount() <= 0) {
+ throw new BerDataValueFormatException("Indefinite-length contents too long");
+ }
+ final ByteBuffer encoded = dataValue.getEncoded();
+ if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
+ // 0x00 0x00 encountered
+ return in.getReadByteCount() - readByteCountBefore - 2;
+ }
+ }
+ }
+
+ private static class RecordingInputStream extends InputStream {
+ private final InputStream mIn;
+ private final ByteArrayOutputStream mBuf;
+
+ private RecordingInputStream(final InputStream in) {
+ this.mIn = in;
+ this.mBuf = new ByteArrayOutputStream();
+ }
+
+ public byte[] getReadBytes() {
+ return this.mBuf.toByteArray();
+ }
+
+ public int getReadByteCount() {
+ return this.mBuf.size();
+ }
+
+ @Override
+ public int read() throws IOException {
+ final int b = this.mIn.read();
+ if (b != -1) {
+ this.mBuf.write(b);
+ }
+ return b;
+ }
+
+ @Override
+ public int read(@NonNull final byte[] b) throws IOException {
+ final int len = this.mIn.read(b);
+ if (len > 0) {
+ this.mBuf.write(b, 0, len);
+ }
+ return len;
+ }
+
+ @Override
+ public int read(@NonNull final byte[] b, final int off, int len) throws IOException {
+ len = this.mIn.read(b, off, len);
+ if (len > 0) {
+ this.mBuf.write(b, off, len);
+ }
+ return len;
+ }
+
+ @Override
+ public long skip(final long n) throws IOException {
+ if (n <= 0) {
+ return 0;
+ }
+
+ final byte[] buf = new byte[4096];
+ final int len = this.mIn.read(buf, 0, (int) Math.min(buf.length, n));
+ if (len > 0) {
+ this.mBuf.write(buf, 0, len);
+ }
+ return Math.max(len, 0);
+ }
+
+ @Override
+ public synchronized void mark(final int readlimit) {
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ throw new IOException("mark/reset not supported");
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java
new file mode 100644
index 00000000..495351f0
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+/**
+ * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class AlgorithmIdentifier {
+
+ @Asn1Field(index = 0, type = Asn1Type.ObjectIdentifier)
+ public String algorithm;
+
+ @Asn1Field(index = 1, type = Asn1Type.Any, optional = true)
+ public Asn1OpaqueObject parameters;
+
+ public AlgorithmIdentifier() {
+ }
+
+ public AlgorithmIdentifier(final String algorithmOid, final Asn1OpaqueObject parameters) {
+ this.algorithm = algorithmOid;
+ this.parameters = parameters;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java
new file mode 100644
index 00000000..794bef76
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+import java.util.List;
+
+/**
+ * PKCS #7 {@code Attribute} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class Attribute {
+
+ @Asn1Field(index = 0, type = Asn1Type.ObjectIdentifier)
+ public String attrType;
+
+ @Asn1Field(index = 1, type = Asn1Type.SetOf)
+ public List attrValues;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java
new file mode 100644
index 00000000..8fa3be92
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.asn1.Asn1Tagging;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+/**
+ * PKCS #7 {@code ContentInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class ContentInfo {
+
+ @Asn1Field(index = 1, type = Asn1Type.ObjectIdentifier)
+ public String contentType;
+
+ @Asn1Field(index = 2, type = Asn1Type.Any, tagging = Asn1Tagging.Explicit, tagNumber = 0)
+ public Asn1OpaqueObject content;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java
new file mode 100644
index 00000000..33d89cef
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1Tagging;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class EncapsulatedContentInfo {
+
+ @Asn1Field(index = 0, type = Asn1Type.ObjectIdentifier)
+ public String contentType;
+
+ @Asn1Field(
+ index = 1,
+ type = Asn1Type.OctetString,
+ tagging = Asn1Tagging.Explicit, tagNumber = 0,
+ optional = true)
+ public ByteBuffer content;
+
+ public EncapsulatedContentInfo() {
+ }
+
+ public EncapsulatedContentInfo(final String contentTypeOid) {
+ this.contentType = contentTypeOid;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java
new file mode 100644
index 00000000..f19071b7
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+import java.math.BigInteger;
+
+/**
+ * PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class IssuerAndSerialNumber {
+
+ @Asn1Field(index = 0, type = Asn1Type.Any)
+ public Asn1OpaqueObject issuer;
+
+ @Asn1Field(index = 1, type = Asn1Type.Integer)
+ public BigInteger certificateSerialNumber;
+
+ public IssuerAndSerialNumber() {
+ }
+
+ public IssuerAndSerialNumber(final Asn1OpaqueObject issuer, final BigInteger certificateSerialNumber) {
+ this.issuer = issuer;
+ this.certificateSerialNumber = certificateSerialNumber;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java
new file mode 100644
index 00000000..7062b51f
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+/**
+ * Assorted PKCS #7 constants from RFC 5652.
+ */
+public abstract class Pkcs7Constants {
+ private Pkcs7Constants() {
+ }
+
+ public static final String OID_DATA = "1.2.840.113549.1.7.1";
+ public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
+ public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
+ public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java
new file mode 100644
index 00000000..8f65e35b
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.asn1.Asn1Tagging;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * PKCS #7 {@code SignedData} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class SignedData {
+
+ @Asn1Field(index = 0, type = Asn1Type.Integer)
+ public int version;
+
+ @Asn1Field(index = 1, type = Asn1Type.SetOf)
+ public List digestAlgorithms;
+
+ @Asn1Field(index = 2, type = Asn1Type.Sequence)
+ public EncapsulatedContentInfo encapContentInfo;
+
+ @Asn1Field(
+ index = 3,
+ type = Asn1Type.SetOf,
+ tagging = Asn1Tagging.Implicit, tagNumber = 0,
+ optional = true)
+ public List certificates;
+
+ @Asn1Field(
+ index = 4,
+ type = Asn1Type.SetOf,
+ tagging = Asn1Tagging.Implicit, tagNumber = 1,
+ optional = true)
+ public List crls;
+
+ @Asn1Field(index = 5, type = Asn1Type.SetOf)
+ public List signerInfos;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java
new file mode 100644
index 00000000..ed2b4f12
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1Tagging;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Choice)
+public class SignerIdentifier {
+
+ @Asn1Field(type = Asn1Type.Sequence)
+ public IssuerAndSerialNumber issuerAndSerialNumber;
+
+ @Asn1Field(type = Asn1Type.OctetString, tagging = Asn1Tagging.Implicit, tagNumber = 0)
+ public ByteBuffer subjectKeyIdentifier;
+
+ public SignerIdentifier() {
+ }
+
+ public SignerIdentifier(final IssuerAndSerialNumber issuerAndSerialNumber) {
+ this.issuerAndSerialNumber = issuerAndSerialNumber;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java
new file mode 100644
index 00000000..26f63fdc
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 net.dongliu.apk.parser.cert.pkcs7;
+
+import net.dongliu.apk.parser.cert.asn1.Asn1Class;
+import net.dongliu.apk.parser.cert.asn1.Asn1Field;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.asn1.Asn1Tagging;
+import net.dongliu.apk.parser.cert.asn1.Asn1Type;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * PKCS #7 {@code SignerInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.Sequence)
+public class SignerInfo {
+
+ @Asn1Field(index = 0, type = Asn1Type.Integer)
+ public int version;
+
+ @Asn1Field(index = 1, type = Asn1Type.Choice)
+ public SignerIdentifier sid;
+
+ @Asn1Field(index = 2, type = Asn1Type.Sequence)
+ public AlgorithmIdentifier digestAlgorithm;
+
+ @Asn1Field(
+ index = 3,
+ type = Asn1Type.SetOf,
+ tagging = Asn1Tagging.Implicit, tagNumber = 0,
+ optional = true)
+ public Asn1OpaqueObject signedAttrs;
+
+ @Asn1Field(index = 4, type = Asn1Type.Sequence)
+ public AlgorithmIdentifier signatureAlgorithm;
+
+ @Asn1Field(index = 5, type = Asn1Type.OctetString)
+ public ByteBuffer signature;
+
+ @Asn1Field(
+ index = 6,
+ type = Asn1Type.SetOf,
+ tagging = Asn1Tagging.Implicit, tagNumber = 1,
+ optional = true)
+ public List unsignedAttrs;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java
new file mode 100644
index 00000000..91d0bba2
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/exception/ParserException.java
@@ -0,0 +1,30 @@
+package net.dongliu.apk.parser.exception;
+
+/**
+ * throwed when parse failed.
+ *
+ * @author dongliu
+ */
+public class ParserException extends RuntimeException {
+ private static final long serialVersionUID = -669279149141454276L;
+
+ public ParserException(final String msg) {
+ super(msg);
+ }
+
+ public ParserException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public ParserException(final Throwable cause) {
+ super(cause);
+ }
+
+ public ParserException(final String message, final Throwable cause, final boolean enableSuppression,
+ final boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+ public ParserException() {
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java
new file mode 100644
index 00000000..3b439c96
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java
@@ -0,0 +1,70 @@
+package net.dongliu.apk.parser.parser;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.xml.Attribute;
+import net.dongliu.apk.parser.struct.xml.Attributes;
+import net.dongliu.apk.parser.struct.xml.XmlCData;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag;
+
+/**
+ * Parse adaptive icon xml file.
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class AdaptiveIconParser implements XmlStreamer {
+ @Nullable
+ private String foreground;
+ @Nullable
+ private String background;
+
+ @Nullable
+ public String getForeground() {
+ return this.foreground;
+ }
+
+ @Nullable
+ public String getBackground() {
+ return this.background;
+ }
+
+ @Override
+ public void onStartTag(final @NonNull XmlNodeStartTag xmlNodeStartTag) {
+ if ("background".equals(xmlNodeStartTag.name)) {
+ this.background = this.getDrawable(xmlNodeStartTag);
+ } else if ("foreground".equals(xmlNodeStartTag.name)) {
+ this.foreground = this.getDrawable(xmlNodeStartTag);
+ }
+ }
+
+ @Nullable
+ private String getDrawable(final XmlNodeStartTag xmlNodeStartTag) {
+ final Attributes attributes = xmlNodeStartTag.attributes;
+ for (final Attribute attribute : attributes.attributes) {
+ if (attribute.name.equals("drawable")) {
+ return attribute.value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onEndTag(@NonNull final XmlNodeEndTag xmlNodeEndTag) {
+ }
+
+ @Override
+ public void onCData(@NonNull final XmlCData xmlCData) {
+ }
+
+ @Override
+ public void onNamespaceStart(@NonNull final XmlNamespaceStartTag tag) {
+ }
+
+ @Override
+ public void onNamespaceEnd(@NonNull final XmlNamespaceEndTag tag) {
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java
new file mode 100644
index 00000000..5f29ae1e
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java
@@ -0,0 +1,271 @@
+package net.dongliu.apk.parser.parser;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.bean.ApkMeta;
+import net.dongliu.apk.parser.bean.GlEsVersion;
+import net.dongliu.apk.parser.bean.IconPath;
+import net.dongliu.apk.parser.bean.Permission;
+import net.dongliu.apk.parser.bean.UseFeature;
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.resource.Densities;
+import net.dongliu.apk.parser.struct.resource.ResourceEntry;
+import net.dongliu.apk.parser.struct.resource.ResourceTable;
+import net.dongliu.apk.parser.struct.resource.Type;
+import net.dongliu.apk.parser.struct.xml.Attribute;
+import net.dongliu.apk.parser.struct.xml.Attributes;
+import net.dongliu.apk.parser.struct.xml.XmlCData;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * trans binary xml to apk meta info
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class ApkMetaTranslator implements XmlStreamer {
+ private final String[] tagStack = new String[100];
+ private int depth = 0;
+ @NonNull
+ private final ApkMeta.Builder apkMetaBuilder = ApkMeta.newBuilder();
+ private List iconPaths = Collections.emptyList();
+
+ private final ResourceTable resourceTable;
+ @Nullable
+ private final Locale locale;
+
+ public ApkMetaTranslator(final @NonNull ResourceTable resourceTable, @Nullable final Locale locale) {
+ this.resourceTable = resourceTable;
+ this.locale = locale;
+ }
+
+ @Override
+ public void onStartTag(final @NonNull XmlNodeStartTag xmlNodeStartTag) {
+ final Attributes attributes = xmlNodeStartTag.attributes;
+ final String xmlNodeStartTagName = xmlNodeStartTag.name;
+ switch (xmlNodeStartTagName) {
+ case "application": {
+ this.apkMetaBuilder.setDebuggable(attributes.getBoolean("debuggable", false));
+ //TODO fix this part in a better way. Workaround for this: https://github.com/hsiafan/apk-parser/issues/119
+ if (this.apkMetaBuilder.split == null)
+ this.apkMetaBuilder.setSplit(attributes.getString("split"));
+ if (this.apkMetaBuilder.configForSplit == null)
+ this.apkMetaBuilder.setConfigForSplit(attributes.getString("configForSplit"));
+ if (!this.apkMetaBuilder.isFeatureSplit)
+ this.apkMetaBuilder.setIsFeatureSplit(attributes.getBoolean("isFeatureSplit", false));
+ if (!this.apkMetaBuilder.isSplitRequired)
+ this.apkMetaBuilder.setIsSplitRequired(attributes.getBoolean("isSplitRequired", false));
+ if (!this.apkMetaBuilder.isolatedSplits)
+ this.apkMetaBuilder.setIsolatedSplits(attributes.getBoolean("isolatedSplits", false));
+ final String label = attributes.getString("label");
+ if (label != null) {
+ this.apkMetaBuilder.setLabel(label);
+ } else {
+ //workaround in case the real label can't be found, so we at least try to use the package name with the application class
+ final String packageName = this.apkMetaBuilder.getPackageName();
+ if (!TextUtils.isEmpty(packageName)) {
+ final String applicationClassRelativePath = attributes.getString("name");
+ if (TextUtils.isEmpty(applicationClassRelativePath)) {
+ this.apkMetaBuilder.setLabel(packageName);
+ } else {
+ this.apkMetaBuilder.applicationClassRelativePath = applicationClassRelativePath;
+ final String newLabel = packageName + applicationClassRelativePath;
+ this.apkMetaBuilder.setLabel(newLabel);
+ }
+ }
+ }
+ final Attribute iconAttr = attributes.get("icon");
+ if (iconAttr != null) {
+ final ResourceValue resourceValue = iconAttr.typedValue;
+ if (resourceValue instanceof ResourceValue.ReferenceResourceValue) {
+ final long resourceId = ((ResourceValue.ReferenceResourceValue) resourceValue).getReferenceResourceId();
+ final List resources = this.resourceTable.getResourcesById(resourceId);
+ if (!resources.isEmpty()) {
+ final List icons = new ArrayList<>();
+ boolean hasDefault = false;
+ for (final ResourceTable.Resource resource : resources) {
+ final Type type = resource.type;
+ final ResourceEntry resourceEntry = resource.resourceEntry;
+ final String path = resourceEntry.toStringValue(this.resourceTable, this.locale);
+ if (type.density == Densities.DEFAULT) {
+ hasDefault = true;
+ this.apkMetaBuilder.setIcon(path);
+ }
+ final IconPath iconPath = new IconPath(path, type.density);
+ icons.add(iconPath);
+ }
+ if (!hasDefault) {
+ this.apkMetaBuilder.setIcon(icons.get(0).path);
+ }
+ this.iconPaths = icons;
+ }
+ } else {
+ final String value = iconAttr.value;
+ if (value != null) {
+ this.apkMetaBuilder.setIcon(value);
+ final IconPath iconPath = new IconPath(value, Densities.DEFAULT);
+ this.iconPaths = Collections.singletonList(iconPath);
+ }
+ }
+ }
+ break;
+ }
+ case "manifest": {
+ final String packageName = attributes.getString("package");
+ this.apkMetaBuilder.setPackageName(packageName);
+ if (TextUtils.isEmpty(this.apkMetaBuilder.getLabel()) && !TextUtils.isEmpty(packageName)) {
+ //workaround in case the real label can't be found, so we at least try to use the package name with the application class
+ final String applicationClassRelativePath = this.apkMetaBuilder.applicationClassRelativePath;
+ if (TextUtils.isEmpty(applicationClassRelativePath)) {
+ this.apkMetaBuilder.setLabel(packageName);
+ } else {
+ final String newLabel = packageName + applicationClassRelativePath;
+ this.apkMetaBuilder.setLabel(newLabel);
+ }
+ }
+ this.apkMetaBuilder.setVersionName(attributes.getString("versionName"));
+ this.apkMetaBuilder.setRevisionCode(attributes.getLong("revisionCode"));
+ this.apkMetaBuilder.setSharedUserId(attributes.getString("sharedUserId"));
+ this.apkMetaBuilder.setSharedUserLabel(attributes.getString("sharedUserLabel"));
+ if (this.apkMetaBuilder.split == null)
+ this.apkMetaBuilder.setSplit(attributes.getString("split"));
+ if (this.apkMetaBuilder.configForSplit == null)
+ this.apkMetaBuilder.setConfigForSplit(attributes.getString("configForSplit"));
+ if (!this.apkMetaBuilder.isFeatureSplit)
+ this.apkMetaBuilder.setIsFeatureSplit(attributes.getBoolean("isFeatureSplit", false));
+ if (!this.apkMetaBuilder.isSplitRequired)
+ this.apkMetaBuilder.setIsSplitRequired(attributes.getBoolean("isSplitRequired", false));
+ if (!this.apkMetaBuilder.isolatedSplits)
+ this.apkMetaBuilder.setIsolatedSplits(attributes.getBoolean("isolatedSplits", false));
+ final Long majorVersionCode = attributes.getLong("versionCodeMajor");
+ Long versionCode = attributes.getLong("versionCode");
+ if (majorVersionCode != null) {
+ if (versionCode == null) {
+ versionCode = 0L;
+ }
+ versionCode = (majorVersionCode << 32) | (versionCode & 0xFFFFFFFFL);
+ }
+ if (versionCode != null)
+ this.apkMetaBuilder.setVersionCode(versionCode);
+ final String installLocation = attributes.getString("installLocation");
+ if (installLocation != null) {
+ this.apkMetaBuilder.setInstallLocation(installLocation);
+ }
+ this.apkMetaBuilder.setCompileSdkVersion(attributes.getString("compileSdkVersion"));
+ this.apkMetaBuilder.setCompileSdkVersionCodename(attributes.getString("compileSdkVersionCodename"));
+ this.apkMetaBuilder.setPlatformBuildVersionCode(attributes.getString("platformBuildVersionCode"));
+ this.apkMetaBuilder.setPlatformBuildVersionName(attributes.getString("platformBuildVersionName"));
+ break;
+ }
+ case "uses-sdk": {
+ final String minSdkVersion = attributes.getString("minSdkVersion");
+ if (minSdkVersion != null) {
+ this.apkMetaBuilder.setMinSdkVersion(minSdkVersion);
+ }
+ final String targetSdkVersion = attributes.getString("targetSdkVersion");
+ if (targetSdkVersion != null) {
+ this.apkMetaBuilder.setTargetSdkVersion(targetSdkVersion);
+ }
+ final String maxSdkVersion = attributes.getString("maxSdkVersion");
+ if (maxSdkVersion != null) {
+ this.apkMetaBuilder.setMaxSdkVersion(maxSdkVersion);
+ }
+ break;
+ }
+ case "supports-screens": {
+ this.apkMetaBuilder.setAnyDensity(attributes.getBoolean("anyDensity", false));
+ this.apkMetaBuilder.setSmallScreens(attributes.getBoolean("smallScreens", false));
+ this.apkMetaBuilder.setNormalScreens(attributes.getBoolean("normalScreens", false));
+ this.apkMetaBuilder.setLargeScreens(attributes.getBoolean("largeScreens", false));
+ break;
+ }
+ case "uses-feature": {
+ final String name = attributes.getString("name");
+ final boolean required = attributes.getBoolean("required", false);
+ if (name != null) {
+ final UseFeature useFeature = new UseFeature(name, required);
+ this.apkMetaBuilder.addUsesFeature(useFeature);
+ } else {
+ final Integer gl = attributes.getInt("glEsVersion");
+ if (gl != null) {
+ final int v = gl;
+ final GlEsVersion glEsVersion = new GlEsVersion(v >> 16, v & 0xffff, required);
+ this.apkMetaBuilder.setGlEsVersion(glEsVersion);
+ }
+ }
+ break;
+ }
+ case "uses-permission": {
+ this.apkMetaBuilder.addUsesPermission(attributes.getString("name"));
+ break;
+ }
+ case "permission": {
+ final Permission permission = new Permission(
+ attributes.getString("name"),
+ attributes.getString("label"),
+ attributes.getString("icon"),
+ attributes.getString("description"),
+ attributes.getString("group"),
+ attributes.getString("android:protectionLevel"));
+ this.apkMetaBuilder.addPermissions(permission);
+ break;
+ }
+ }
+ this.tagStack[this.depth++] = xmlNodeStartTagName;
+ }
+
+ @Override
+ public void onEndTag(@NonNull final XmlNodeEndTag xmlNodeEndTag) {
+ this.depth--;
+ }
+
+ @Override
+ public void onCData(@NonNull final XmlCData xmlCData) {
+ }
+
+ @Override
+ public void onNamespaceStart(@NonNull final XmlNamespaceStartTag tag) {
+ }
+
+ @Override
+ public void onNamespaceEnd(@NonNull final XmlNamespaceEndTag tag) {
+ }
+
+ @NonNull
+ public ApkMeta getApkMeta() {
+ return this.apkMetaBuilder.build();
+ }
+
+ @NonNull
+ public List getIconPaths() {
+ return this.iconPaths;
+ }
+
+ private boolean matchTagPath(final String... tags) {
+ // the root should always be "manifest"
+ if (this.depth != tags.length + 1) {
+ return false;
+ }
+ for (int i = 1; i < this.depth; i++) {
+ if (!this.tagStack[i].equals(tags[i - 1])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean matchLastTag(final String tag) {
+ // the root should always be "manifest"
+ return this.tagStack[this.depth - 1].endsWith(tag);
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt
new file mode 100644
index 00000000..e8bf5213
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.kt
@@ -0,0 +1,134 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.struct.signingv2.*
+import net.dongliu.apk.parser.utils.*
+import java.io.ByteArrayInputStream
+import java.nio.*
+import java.security.cert.*
+
+/**
+ * The Apk Sign Block V2 Parser.
+ * see https://source.android.com/security/apksigning/v2
+ */
+class ApkSignBlockParser(data: ByteBuffer) {
+ private val data: ByteBuffer
+
+ init {
+ this.data = data.order(ByteOrder.LITTLE_ENDIAN)
+ }
+
+ @Throws(CertificateException::class)
+ fun parse(): ApkSigningBlock {
+ // sign block found, read pairs
+ val signerBlocks: MutableList = ArrayList()
+ while (data.remaining() >= 8) {
+ val id = data.int
+ val size = Unsigned.ensureUInt(data.int.toLong())
+ if (id == ApkSigningBlock.SIGNING_V2_ID) {
+ val signingV2Buffer = Buffers.sliceAndSkip(data, size)
+ // now only care about apk signing v2 entry
+ while (signingV2Buffer.hasRemaining()) {
+ val signerBlock = readSigningV2(signingV2Buffer)
+ signerBlocks.add(signerBlock)
+ }
+ } else {
+ // just ignore now
+ Buffers.position(data, data.position() + size)
+ }
+ }
+ return ApkSigningBlock(signerBlocks)
+ }
+
+ @Throws(CertificateException::class)
+ private fun readSigningV2(inputBuffer: ByteBuffer): SignerBlock {
+ val buffer = readLenPrefixData(inputBuffer)
+ val signedData = readLenPrefixData(buffer)
+ val digestsData = readLenPrefixData(signedData)
+ val digests = readDigests(digestsData)
+ val certificateData = readLenPrefixData(signedData)
+ val certificates = readCertificates(certificateData)
+ val attributesData = readLenPrefixData(signedData)
+ readAttributes(attributesData)
+ val signaturesData = readLenPrefixData(buffer)
+ val signatures = readSignatures(signaturesData)
+ val publicKeyData = readLenPrefixData(buffer)
+ return SignerBlock(digests, certificates, signatures)
+ }
+
+ private fun readDigests(buffer: ByteBuffer): List {
+ val list: MutableList = ArrayList()
+ while (buffer.hasRemaining()) {
+ val digestData = readLenPrefixData(buffer)
+ val algorithmID = digestData.int
+ val digest = Buffers.readBytes(digestData)
+ list.add(Digest(algorithmID, digest))
+ }
+ return list
+ }
+
+ @Throws(CertificateException::class)
+ private fun readCertificates(buffer: ByteBuffer): List {
+ val certificateFactory = CertificateFactory.getInstance("X.509")
+ val certificates: MutableList = ArrayList()
+ while (buffer.hasRemaining()) {
+ val certificateData = readLenPrefixData(buffer)
+ val certificate = certificateFactory.generateCertificate(
+ ByteArrayInputStream(Buffers.readBytes(certificateData))
+ )
+ certificates.add(certificate as X509Certificate)
+ }
+ return certificates
+ }
+
+ private fun readAttributes(buffer: ByteBuffer) {
+ while (buffer.hasRemaining()) {
+ val attributeData = readLenPrefixData(buffer)
+ val id = attributeData.int
+ // byte[] value = Buffers.readBytes(attributeData);
+ }
+ }
+
+ private fun readSignatures(buffer: ByteBuffer): List {
+ val signatures: MutableList = ArrayList()
+ while (buffer.hasRemaining()) {
+ val signatureData = readLenPrefixData(buffer)
+ val algorithmID = signatureData.int
+ val signatureDataLen = Unsigned.ensureUInt(signatureData.int.toLong())
+ val signature = Buffers.readBytes(signatureData, signatureDataLen)
+ signatures.add(Signature(algorithmID, signature))
+ }
+ return signatures
+ }
+
+ private fun readLenPrefixData(buffer: ByteBuffer): ByteBuffer {
+ val len = Unsigned.ensureUInt(buffer.int.toLong())
+ return Buffers.sliceAndSkip(buffer, len)
+ } // /**
+ // * 0x0101—RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc
+ // */
+ // private static final int PSS_SHA_256 = 0x0101;
+ // /**
+ // * 0x0102—RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc
+ // */
+ // private static final int PSS_SHA_512 = 0x0102;
+ // /**
+ // * 0x0103—RSASSA-PKCS1-v1_5 with SHA2-256 digest. This is for build systems which require deterministic signatures.
+ // */
+ // private static final int PKCS1_SHA_256 = 0x0103;
+ // /**
+ // * 0x0104—RSASSA-PKCS1-v1_5 with SHA2-512 digest. This is for build systems which require deterministic signatures.
+ // */
+ // private static final int PKCS1_SHA_512 = 0x0104;
+ // /**
+ // * 0x0201—ECDSA with SHA2-256 digest
+ // */
+ // private static final int ECDSA_SHA_256 = 0x0201;
+ // /**
+ // * 0x0202—ECDSA with SHA2-512 digest
+ // */
+ // private static final int ECDSA_SHA_512 = 0x0202;
+ // /**
+ // * 0x0301—DSA with SHA2-256 digest
+ // */
+ // private static final int DSA_SHA_256 = 0x0301;
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt
new file mode 100644
index 00000000..2c4137c6
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.kt
@@ -0,0 +1,150 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.utils.Strings.join
+
+/**
+ * attribute value constant
+ *
+ * @author Liu Dong
+ */
+object AttributeValues {
+ /**
+ * Activity constants begin. see:
+ * http://developer.android.com/reference/android/content/pm/ActivityInfo.html
+ * http://developer.android.com/guide/topics/manifest/activity-element.html
+ */
+ @JvmStatic
+ fun getScreenOrientation(value: Int): String {
+ return when (value) {
+ 0x00000003 -> "behind"
+ 0x0000000a -> "fullSensor"
+ 0x0000000d -> "fullUser"
+ 0x00000000 -> "landscape"
+ 0x0000000e -> "locked"
+ 0x00000005 -> "nosensor"
+ 0x00000001 -> "portrait"
+ 0x00000008 -> "reverseLandscape"
+ 0x00000009 -> "reversePortrait"
+ 0x00000004 -> "sensor"
+ 0x00000006 -> "sensorLandscape"
+ 0x00000007 -> "sensorPortrait"
+ -0x1 -> "unspecified"
+ 0x00000002 -> "user"
+ 0x0000000b -> "userLandscape"
+ 0x0000000c -> "userPortrait"
+ else -> "ScreenOrientation:" + Integer.toHexString(value)
+ }
+ }
+
+ @JvmStatic
+ fun getLaunchMode(value: Int): String {
+ return when (value) {
+ 0x00000000 -> "standard"
+ 0x00000001 -> "singleTop"
+ 0x00000002 -> "singleTask"
+ 0x00000003 -> "singleInstance"
+ else -> "LaunchMode:" + Integer.toHexString(value)
+ }
+ }
+
+ @JvmStatic
+ fun getConfigChanges(value: Int): String? {
+ val list: MutableList = ArrayList()
+ if (value and 0x00001000 != 0) {
+ list.add("density")
+ } else if (value and 0x40000000 != 0) {
+ list.add("fontScale")
+ } else if (value and 0x00000010 != 0) {
+ list.add("keyboard")
+ } else if (value and 0x00000020 != 0) {
+ list.add("keyboardHidden")
+ } else if (value and 0x00002000 != 0) {
+ list.add("direction")
+ } else if (value and 0x00000004 != 0) {
+ list.add("locale")
+ } else if (value and 0x00000001 != 0) {
+ list.add("mcc")
+ } else if (value and 0x00000002 != 0) {
+ list.add("mnc")
+ } else if (value and 0x00000040 != 0) {
+ list.add("navigation")
+ } else if (value and 0x00000080 != 0) {
+ list.add("orientation")
+ } else if (value and 0x00000100 != 0) {
+ list.add("screenLayout")
+ } else if (value and 0x00000400 != 0) {
+ list.add("screenSize")
+ } else if (value and 0x00000800 != 0) {
+ list.add("smallestScreenSize")
+ } else if (value and 0x00000008 != 0) {
+ list.add("touchscreen")
+ } else if (value and 0x00000200 != 0) {
+ list.add("uiMode")
+ }
+ return join(list, "|")
+ }
+
+ @JvmStatic
+ fun getWindowSoftInputMode(value: Int): String? {
+ val adjust = value and 0x000000f0
+ val state = value and 0x0000000f
+ val list: MutableList = ArrayList(2)
+ when (adjust) {
+ 0x00000030 -> list.add("adjustNothing")
+ 0x00000020 -> list.add("adjustPan")
+ 0x00000010 -> list.add("adjustResize")
+ 0x00000000 -> {}
+ else -> list.add("WindowInputModeAdjust:" + Integer.toHexString(adjust))
+ }
+ when (state) {
+ 0x00000003 -> list.add("stateAlwaysHidden")
+ 0x00000005 -> list.add("stateAlwaysVisible")
+ 0x00000002 -> list.add("stateHidden")
+ 0x00000001 -> list.add("stateUnchanged")
+ 0x00000004 -> list.add("stateVisible")
+ 0x00000000 -> {}
+ else -> list.add("WindowInputModeState:" + Integer.toHexString(state))
+ }
+ return join(list, "|")
+ //isForwardNavigation(0x00000100),
+ //mode_changed(0x00000200),
+ }
+
+ /**
+ * http://developer.android.com/reference/android/content/pm/PermissionInfo.html
+ */
+ @JvmStatic
+ fun getProtectionLevel(inputValue: Int): String? {
+ var value = inputValue
+ val levels: MutableList = ArrayList(3)
+ if (value and 0x10 != 0) {
+ value = value xor 0x10
+ levels.add("system")
+ }
+ if (value and 0x20 != 0) {
+ value = value xor 0x20
+ levels.add("development")
+ }
+ when (value) {
+ 0 -> levels.add("normal")
+ 1 -> levels.add("dangerous")
+ 2 -> levels.add("signature")
+ 3 -> levels.add("signatureOrSystem")
+ else -> levels.add("ProtectionLevel:" + Integer.toHexString(value))
+ }
+ return join(levels, "|")
+ }
+ // Activity constants end
+ /**
+ * get Installation string values from int
+ */
+ @JvmStatic
+ fun getInstallLocation(value: Int): String {
+ return when (value) {
+ 0 -> "auto"
+ 1 -> "internalOnly"
+ 2 -> "preferExternal"
+ else -> "installLocation:" + Integer.toHexString(value)
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java
new file mode 100644
index 00000000..5f081548
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java
@@ -0,0 +1,65 @@
+package net.dongliu.apk.parser.parser;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.bean.CertificateMeta;
+
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerId;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.SignerInformationStore;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.Store;
+
+import java.security.Provider;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Parser certificate info using BouncyCastle.
+ *
+ * @author dongliu
+ */
+class BCCertificateParser extends CertificateParser {
+
+ private static final Provider provider = new BouncyCastleProvider();
+
+ public BCCertificateParser(@NonNull final byte[] data) {
+ super(data);
+ }
+
+ /**
+ * get certificate info
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ @NonNull
+ public List parse() throws CertificateException {
+ final CMSSignedData cmsSignedData;
+ try {
+ cmsSignedData = new CMSSignedData(this.data);
+ } catch (final CMSException e) {
+ throw new CertificateException(e);
+ }
+ final Store certStore = cmsSignedData.getCertificates();
+ final SignerInformationStore signerInfos = cmsSignedData.getSignerInfos();
+ final Collection signers = signerInfos.getSigners();
+ final List certificates = new ArrayList<>();
+ for (final SignerInformation signer : signers) {
+ final SignerId sid = signer.getSID();
+ final Collection matches = certStore.getMatches(sid);
+ for (final X509CertificateHolder holder : matches) {
+ certificates.add(new JcaX509CertificateConverter().setProvider(BCCertificateParser.provider)
+ .getCertificate(holder));
+ }
+ }
+ return CertificateMetas.from(certificates);
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java
new file mode 100644
index 00000000..2bf21b62
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java
@@ -0,0 +1,317 @@
+package net.dongliu.apk.parser.parser;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.exception.ParserException;
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.struct.StringPoolHeader;
+import net.dongliu.apk.parser.struct.resource.ResourceTable;
+import net.dongliu.apk.parser.struct.xml.Attribute;
+import net.dongliu.apk.parser.struct.xml.Attributes;
+import net.dongliu.apk.parser.struct.xml.NullHeader;
+import net.dongliu.apk.parser.struct.xml.XmlCData;
+import net.dongliu.apk.parser.struct.xml.XmlHeader;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeHeader;
+import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag;
+import net.dongliu.apk.parser.struct.xml.XmlResourceMapHeader;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.Locales;
+import net.dongliu.apk.parser.utils.ParseUtils;
+import net.dongliu.apk.parser.utils.Strings;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Android Binary XML format
+ * see http://justanapplication.wordpress.com/category/android/android-binary-xml/
+ *
+ * @author dongliu
+ */
+public class BinaryXmlParser {
+
+ /**
+ * By default the data buffer Chunks is buffer little-endian byte order both at runtime and when stored buffer
+ * files.
+ */
+ private StringPool stringPool;
+ /**
+ * some attribute name stored by resource id
+ */
+ private String[] resourceMap;
+ @NonNull
+ private final ByteBuffer buffer;
+ @NonNull
+ private final XmlStreamer xmlStreamer;
+ @NonNull
+ private final ResourceTable resourceTable;
+ /**
+ * default locale.
+ */
+ @NonNull
+ private final Locale locale;
+
+ public BinaryXmlParser(final @NonNull ByteBuffer buffer, final @NonNull ResourceTable resourceTable, final @NonNull XmlStreamer xmlStreamer
+ , final @Nullable Locale locale) {
+ this.buffer = buffer.duplicate();
+ this.buffer.order(ByteOrder.LITTLE_ENDIAN);
+ this.resourceTable = resourceTable;
+ this.xmlStreamer = xmlStreamer;
+ this.locale = locale == null ? Locales.any : locale;
+ }
+
+ /**
+ * Parse binary xml.
+ */
+ public void parse() {
+ final ChunkHeader firstChunkHeader = this.readChunkHeader();
+ if (firstChunkHeader == null) {
+ return;
+ }
+ switch ((int) firstChunkHeader.chunkType) {
+ case ChunkType.XML:
+ case ChunkType.NULL:
+ break;
+ case ChunkType.STRING_POOL:
+ default:
+ // strange chunk header type, just skip this chunk header?
+ }
+ // read string pool chunk
+ final ChunkHeader stringPoolChunkHeader = this.readChunkHeader();
+ if (stringPoolChunkHeader == null) {
+ return;
+ }
+ ParseUtils.checkChunkType(ChunkType.STRING_POOL, stringPoolChunkHeader.chunkType);
+ this.stringPool = ParseUtils.readStringPool(this.buffer, (StringPoolHeader) stringPoolChunkHeader);
+ // read on chunk, check if it was an optional XMLResourceMap chunk
+ ChunkHeader chunkHeader = this.readChunkHeader();
+ if (chunkHeader == null) {
+ return;
+ }
+ if ((int) chunkHeader.chunkType == ChunkType.XML_RESOURCE_MAP) {
+ final long[] resourceIds = this.readXmlResourceMap((XmlResourceMapHeader) chunkHeader);
+ this.resourceMap = new String[resourceIds.length];
+ for (int i = 0; i < resourceIds.length; i++) {
+ this.resourceMap[i] = Attribute.getString(resourceIds[i]);
+ }
+ chunkHeader = this.readChunkHeader();
+ }
+ while (chunkHeader != null) {
+ final long beginPos = this.buffer.position();
+ switch ((int) chunkHeader.chunkType) {
+ case ChunkType.XML_END_NAMESPACE:
+ final XmlNamespaceEndTag xmlNamespaceEndTag = this.readXmlNamespaceEndTag();
+ this.xmlStreamer.onNamespaceEnd(xmlNamespaceEndTag);
+ break;
+ case ChunkType.XML_START_NAMESPACE:
+ final XmlNamespaceStartTag namespaceStartTag = this.readXmlNamespaceStartTag();
+ this.xmlStreamer.onNamespaceStart(namespaceStartTag);
+ break;
+ case ChunkType.XML_START_ELEMENT:
+ final XmlNodeStartTag xmlNodeStartTag = this.readXmlNodeStartTag();
+ break;
+ case ChunkType.XML_END_ELEMENT:
+ final XmlNodeEndTag xmlNodeEndTag = this.readXmlNodeEndTag();
+ break;
+ case ChunkType.XML_CDATA:
+ final XmlCData xmlCData = this.readXmlCData();
+ break;
+ default:
+ if ((int) chunkHeader.chunkType >= ChunkType.XML_FIRST_CHUNK &&
+ (int) chunkHeader.chunkType <= ChunkType.XML_LAST_CHUNK) {
+ Buffers.skip(this.buffer, chunkHeader.getBodySize());
+ } else {
+ throw new ParserException("Unexpected chunk type:" + (int) chunkHeader.chunkType);
+ }
+ }
+ Buffers.position(this.buffer, beginPos + chunkHeader.getBodySize());
+ chunkHeader = this.readChunkHeader();
+ }
+ }
+
+ private XmlCData readXmlCData() {
+ final XmlCData xmlCData = new XmlCData();
+ final int dataRef = this.buffer.getInt();
+ if (dataRef > 0) {
+ xmlCData.setData(this.stringPool.get(dataRef));
+ }
+ xmlCData.setTypedData(ParseUtils.readResValue(this.buffer, this.stringPool));
+ //TODO: to know more about cdata. some cdata appears buffer xml tags
+// String value = xmlCData.toStringValue(resourceTable, locale);
+// xmlCData.setValue(value);
+// xmlStreamer.onCData(xmlCData);
+ return xmlCData;
+ }
+
+ private XmlNodeEndTag readXmlNodeEndTag() {
+ final XmlNodeEndTag xmlNodeEndTag = new XmlNodeEndTag();
+ final int nsRef = this.buffer.getInt();
+ final int nameRef = this.buffer.getInt();
+ if (nsRef > 0) {
+ xmlNodeEndTag.setNamespace(this.stringPool.get(nsRef));
+ }
+ xmlNodeEndTag.setName(this.stringPool.get(nameRef));
+ this.xmlStreamer.onEndTag(xmlNodeEndTag);
+ return xmlNodeEndTag;
+ }
+
+ private XmlNodeStartTag readXmlNodeStartTag() {
+ final int nsRef = this.buffer.getInt();
+ final int nameRef = this.buffer.getInt();
+ final String namespace = nsRef > 0 ? this.stringPool.get(nsRef) : null;
+ final String name = this.stringPool.get(nameRef);
+ // read attributes.
+ // attributeStart and attributeSize are always 20 (0x14)
+ final int attributeStart = Buffers.readUShort(this.buffer);
+ final int attributeSize = Buffers.readUShort(this.buffer);
+ final int attributeCount = Buffers.readUShort(this.buffer);
+ final int idIndex = Buffers.readUShort(this.buffer);
+ final int classIndex = Buffers.readUShort(this.buffer);
+ final int styleIndex = Buffers.readUShort(this.buffer);
+ // read attributes
+ final Attributes attributes = new Attributes(attributeCount);
+ for (int count = 0; count < attributeCount; count++) {
+ final Attribute attribute = this.readAttribute();
+ final String attributeName = attribute.name;
+ String value = attribute.toStringValue(this.resourceTable, this.locale);
+ if (value != null && BinaryXmlParser.intAttributes.contains(attributeName) && Strings.isNumeric(value)) {
+ try {
+ value = this.getFinalValueAsString(attributeName, value);
+ } catch (final Exception ignore) {
+ }
+ }
+ attribute.value = value;
+ attributes.set(count, attribute);
+ }
+ final XmlNodeStartTag xmlNodeStartTag = new XmlNodeStartTag(namespace, name, attributes);
+ this.xmlStreamer.onStartTag(xmlNodeStartTag);
+ return xmlNodeStartTag;
+ }
+
+ private static final Set intAttributes = new HashSet<>(
+ Arrays.asList("screenOrientation", "configChanges", "windowSoftInputMode",
+ "launchMode", "installLocation", "protectionLevel"));
+
+ /**
+ * trans int attr value to string
+ */
+ private String getFinalValueAsString(final String attributeName, @NonNull final String str) {
+ final int value = Integer.parseInt(str);
+ switch (attributeName) {
+ case "screenOrientation":
+ return AttributeValues.getScreenOrientation(value);
+ case "configChanges":
+ return AttributeValues.getConfigChanges(value);
+ case "windowSoftInputMode":
+ return AttributeValues.getWindowSoftInputMode(value);
+ case "launchMode":
+ return AttributeValues.getLaunchMode(value);
+ case "installLocation":
+ return AttributeValues.getInstallLocation(value);
+ case "protectionLevel":
+ return AttributeValues.getProtectionLevel(value);
+ default:
+ return str;
+ }
+ }
+
+ private Attribute readAttribute() {
+ final int namespaceRef = this.buffer.getInt();
+ final int nameRef = this.buffer.getInt();
+ String name = this.stringPool.get(nameRef);
+ if (name.isEmpty() && this.resourceMap != null && nameRef < this.resourceMap.length) {
+ // some processed apk file make the string pool value empty, if it is a xmlmap attr.
+ name = this.resourceMap[nameRef];
+ }
+ String namespace = namespaceRef > 0 ? this.stringPool.get(namespaceRef) : null;
+ if (namespace == null || namespace.isEmpty() || "http://schemas.android.com/apk/res/android".equals(namespace)) {
+ //TODO parse namespaces better
+ //workaround for a weird case that there is no namespace found: https://github.com/hsiafan/apk-parser/issues/122
+ // Log.d("AppLog", "Got a weird namespace, so setting as empty (namespace isn't supposed to be a URL): " + attribute.getName());
+ namespace = "android";
+ }
+ final int rawValueRef = this.buffer.getInt();
+ final String rawValue = rawValueRef > 0 ? this.stringPool.get(rawValueRef) : null;
+ final ResourceValue resValue = ParseUtils.readResValue(this.buffer, this.stringPool);
+ return new Attribute(namespace, name, rawValue, resValue);
+ }
+
+ @NonNull
+ private XmlNamespaceStartTag readXmlNamespaceStartTag() {
+ final int prefixRef = this.buffer.getInt();
+ final int uriRef = this.buffer.getInt();
+ final String prefix = prefixRef > 0 ? this.stringPool.get(prefixRef) : null;
+ final String uri = uriRef > 0 ? this.stringPool.get(uriRef) : null;
+ return new XmlNamespaceStartTag(prefix, uri);
+ }
+
+ @NonNull
+ private XmlNamespaceEndTag readXmlNamespaceEndTag() {
+ final int prefixRef = this.buffer.getInt();
+ final String prefix = prefixRef <= 0 ? null : this.stringPool.get(prefixRef);
+ final int uriRef = this.buffer.getInt();
+ final String uri = uriRef <= 0 ? null : this.stringPool.get(uriRef);
+ return new XmlNamespaceEndTag(prefix, uri);
+ }
+
+ private long[] readXmlResourceMap(final XmlResourceMapHeader chunkHeader) {
+ final int count = chunkHeader.getBodySize() / 4;
+ final long[] resourceIds = new long[count];
+ for (int i = 0; i < count; i++) {
+ resourceIds[i] = Buffers.readUInt(this.buffer);
+ }
+ return resourceIds;
+ }
+
+ @Nullable
+ private ChunkHeader readChunkHeader() {
+ // finished
+ if (!this.buffer.hasRemaining()) {
+ return null;
+ }
+ final long begin = this.buffer.position();
+ final int chunkType = Buffers.readUShort(this.buffer);
+ final int headerSize = Buffers.readUShort(this.buffer);
+ final long chunkSize = Buffers.readUInt(this.buffer);
+ switch (chunkType) {
+ case ChunkType.XML:
+ return new XmlHeader(chunkType, headerSize, chunkSize);
+ case ChunkType.STRING_POOL:
+ final StringPoolHeader stringPoolHeader = new StringPoolHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return stringPoolHeader;
+ case ChunkType.XML_RESOURCE_MAP:
+ Buffers.position(this.buffer, begin + headerSize);
+ return new XmlResourceMapHeader(chunkType, headerSize, chunkSize);
+ case ChunkType.XML_START_NAMESPACE:
+ case ChunkType.XML_END_NAMESPACE:
+ case ChunkType.XML_START_ELEMENT:
+ case ChunkType.XML_END_ELEMENT:
+ case ChunkType.XML_CDATA:
+ final XmlNodeHeader header = new XmlNodeHeader(chunkType, headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return header;
+ case ChunkType.NULL:
+ return new NullHeader(chunkType, headerSize, chunkSize);
+ default:
+ throw new ParserException("Unexpected chunk type:" + chunkType);
+ }
+ }
+
+ @NonNull
+ public Locale getLocale() {
+ return this.locale;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt
new file mode 100644
index 00000000..6a87046f
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.kt
@@ -0,0 +1,73 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.bean.CertificateMeta
+import java.math.BigInteger
+import java.nio.charset.StandardCharsets
+import java.security.*
+import java.security.cert.*
+import java.util.*
+
+object CertificateMetas {
+ @Throws(CertificateEncodingException::class)
+ @JvmStatic
+ fun from(certificates: List): List {
+ val certificateMetas: MutableList = ArrayList(certificates.size)
+ for (certificate in certificates) {
+ val certificateMeta = from(certificate)
+ certificateMetas.add(certificateMeta)
+ }
+ return certificateMetas
+ }
+
+ @Throws(CertificateEncodingException::class)
+ fun from(certificate: X509Certificate): CertificateMeta {
+ val bytes = certificate.encoded
+ val certMd5 = md5Digest(bytes)
+ val publicKeyString = byteToHexString(bytes)
+ val certBase64Md5 = md5Digest(publicKeyString)
+ return CertificateMeta(
+ certificate.sigAlgName.uppercase(Locale.getDefault()),
+ certificate.sigAlgOID,
+ certificate.notBefore,
+ certificate.notAfter,
+ bytes, certBase64Md5, certMd5
+ )
+ }
+
+ private fun md5Digest(input: ByteArray): String {
+ val digest = getDigest("md5")
+ digest.update(input)
+ return getHexString(digest.digest())
+ }
+
+ private fun md5Digest(input: String): String {
+ val digest = getDigest("md5")
+ digest.update(input.toByteArray(StandardCharsets.UTF_8))
+ return getHexString(digest.digest())
+ }
+
+ private fun byteToHexString(bArray: ByteArray): String {
+ val sb = StringBuilder(bArray.size)
+ for (aBArray in bArray) {
+ val sTemp = Integer.toHexString(0xFF and Char(aBArray.toUShort()).code)
+ if (sTemp.length < 2) {
+ sb.append(0)
+ }
+ sb.append(sTemp.uppercase(Locale.getDefault()))
+ }
+ return sb.toString()
+ }
+
+ private fun getHexString(digest: ByteArray): String {
+ val bi = BigInteger(1, digest)
+ return String.format("%032x", bi)
+ }
+
+ private fun getDigest(algorithm: String): MessageDigest {
+ return try {
+ MessageDigest.getInstance(algorithm)
+ } catch (e: NoSuchAlgorithmException) {
+ throw RuntimeException(e.message)
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt
new file mode 100644
index 00000000..4fdfd68c
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.kt
@@ -0,0 +1,28 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.ApkParsers
+import net.dongliu.apk.parser.bean.CertificateMeta
+import java.security.cert.CertificateException
+
+/**
+ * Parser certificate info.
+ * One apk may have multi certificates(certificate chain).
+ *
+ * @author dongliu
+ */
+abstract class CertificateParser(@JvmField protected val data: ByteArray) {
+ /**
+ * get certificate info
+ */
+ @Throws(CertificateException::class)
+ abstract fun parse(): List
+
+ companion object {
+ @JvmStatic
+ fun getInstance(data: ByteArray): CertificateParser {
+ return if (ApkParsers.useBouncyCastle()) {
+ BCCertificateParser(data)
+ } else JSSECertificateParser(data)
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt
new file mode 100644
index 00000000..c81c9628
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.kt
@@ -0,0 +1,41 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.struct.xml.*
+
+/**
+ * @author dongliu
+ */
+class CompositeXmlStreamer(vararg xmlStreamers: XmlStreamer) : XmlStreamer {
+ @Suppress("UNCHECKED_CAST")
+ private val xmlStreamers: Array = xmlStreamers as Array
+
+ override fun onStartTag(xmlNodeStartTag: XmlNodeStartTag) {
+ for (xmlStreamer in xmlStreamers) {
+ xmlStreamer.onStartTag(xmlNodeStartTag)
+ }
+ }
+
+ override fun onEndTag(xmlNodeEndTag: XmlNodeEndTag) {
+ for (xmlStreamer in xmlStreamers) {
+ xmlStreamer.onEndTag(xmlNodeEndTag)
+ }
+ }
+
+ override fun onCData(xmlCData: XmlCData) {
+ for (xmlStreamer in xmlStreamers) {
+ xmlStreamer.onCData(xmlCData)
+ }
+ }
+
+ override fun onNamespaceStart(tag: XmlNamespaceStartTag) {
+ for (xmlStreamer in xmlStreamers) {
+ xmlStreamer.onNamespaceStart(tag)
+ }
+ }
+
+ override fun onNamespaceEnd(tag: XmlNamespaceEndTag) {
+ for (xmlStreamer in xmlStreamers) {
+ xmlStreamer.onNamespaceEnd(tag)
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java
new file mode 100644
index 00000000..9960354a
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/DexParser.java
@@ -0,0 +1,248 @@
+package net.dongliu.apk.parser.parser;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.bean.DexClass;
+import net.dongliu.apk.parser.exception.ParserException;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.struct.dex.DexClassStruct;
+import net.dongliu.apk.parser.struct.dex.DexHeader;
+import net.dongliu.apk.parser.utils.Buffers;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * parse dex file.
+ * current we only get the class name.
+ * see:
+ * http://source.android.com/devices/tech/dalvik/dex-format.html
+ * http://dexandroid.googlecode.com/svn/trunk/dalvik/libdex/DexFile.h
+ *
+ * @author dongliu
+ */
+public class DexParser {
+
+ private final ByteBuffer buffer;
+
+ private static final int NO_INDEX = 0xffffffff;
+
+ public DexParser(final @NonNull ByteBuffer buffer) {
+ this.buffer = buffer.duplicate();
+ this.buffer.order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ @NonNull
+ public DexClass[] parse() {
+ // read magic
+ final String magic = new String(Buffers.readBytes(this.buffer, 8));
+ if (!magic.startsWith("dex\n")) {
+ return new DexClass[0];
+ }
+ final int version = Integer.parseInt(magic.substring(4, 7));
+ // now the version is 035
+ if (version < 35) {
+ // version 009 was used for the M3 releases of the Android platform (November–December 2007),
+ // and version 013 was used for the M5 releases of the Android platform (February–March 2008)
+ throw new ParserException("Dex file version: " + version + " is not supported");
+ }
+ // read header
+ final DexHeader header = this.readDexHeader();
+ header.setVersion(version);
+ // read string pool
+ final long[] stringOffsets = this.readStringPool(header.getStringIdsOff(), header.getStringIdsSize());
+ // read types
+ final int[] typeIds = this.readTypes(header.getTypeIdsOff(), header.getTypeIdsSize());
+ // read classes
+ final DexClassStruct[] dexClassStructs = this.readClass(header.getClassDefsOff(),
+ header.getClassDefsSize());
+ final StringPool stringpool = this.readStrings(stringOffsets);
+ final String[] types = new String[typeIds.length];
+ for (int i = 0; i < typeIds.length; i++) {
+ types[i] = stringpool.get(typeIds[i]);
+ }
+ final DexClass[] dexClasses = new DexClass[dexClassStructs.length];
+ for (int i = 0; i < dexClassStructs.length; i++) {
+ final DexClassStruct dexClassStruct = dexClassStructs[i];
+ String superClass = null;
+ if (dexClassStruct.getSuperclassIdx() != DexParser.NO_INDEX) {
+ superClass = types[dexClassStruct.getSuperclassIdx()];
+ }
+ dexClasses[i] = new DexClass(
+ types[dexClassStruct.getClassIdx()],
+ superClass,
+ dexClassStruct.getAccessFlags());
+ }
+ return dexClasses;
+ }
+
+ /**
+ * read class info.
+ */
+ private DexClassStruct[] readClass(final long classDefsOff, final int classDefsSize) {
+ Buffers.position(this.buffer, classDefsOff);
+ final DexClassStruct[] dexClassStructs = new DexClassStruct[classDefsSize];
+ for (int i = 0; i < classDefsSize; i++) {
+ final DexClassStruct dexClassStruct = new DexClassStruct();
+ dexClassStruct.setClassIdx(this.buffer.getInt());
+ dexClassStruct.setAccessFlags(this.buffer.getInt());
+ dexClassStruct.setSuperclassIdx(this.buffer.getInt());
+ dexClassStruct.setInterfacesOff(Buffers.readUInt(this.buffer));
+ dexClassStruct.setSourceFileIdx(this.buffer.getInt());
+ dexClassStruct.setAnnotationsOff(Buffers.readUInt(this.buffer));
+ dexClassStruct.setClassDataOff(Buffers.readUInt(this.buffer));
+ dexClassStruct.setStaticValuesOff(Buffers.readUInt(this.buffer));
+ dexClassStructs[i] = dexClassStruct;
+ }
+ return dexClassStructs;
+ }
+
+ /**
+ * read types.
+ */
+ private int[] readTypes(final long typeIdsOff, final int typeIdsSize) {
+ Buffers.position(this.buffer, typeIdsOff);
+ final int[] typeIds = new int[typeIdsSize];
+ for (int i = 0; i < typeIdsSize; i++) {
+ typeIds[i] = (int) Buffers.readUInt(this.buffer);
+ }
+ return typeIds;
+ }
+
+ /**
+ * read string pool for dex file.
+ * dex file string pool diff a bit with binary xml file or resource table.
+ */
+ private StringPool readStrings(final long[] offsets) {
+ // read strings.
+ // buffer some apk, the strings' offsets may not well ordered. we sort it first
+ final StringPoolEntry[] entries = new StringPoolEntry[offsets.length];
+ for (int i = 0; i < offsets.length; i++) {
+ entries[i] = new StringPoolEntry(i, offsets[i]);
+ }
+ String lastStr = null;
+ long lastOffset = -1;
+ final StringPool stringpool = new StringPool(offsets.length);
+ for (final StringPoolEntry entry : entries) {
+ if (entry.offset == lastOffset) {
+ stringpool.set(entry.idx, lastStr);
+ continue;
+ }
+ Buffers.position(this.buffer, entry.offset);
+ lastOffset = entry.offset;
+ final String str = this.readString();
+ lastStr = str;
+ stringpool.set(entry.idx, str);
+ }
+ return stringpool;
+ }
+
+ /*
+ * read string identifiers list.
+ */
+ private long[] readStringPool(final long stringIdsOff, final int stringIdsSize) {
+ Buffers.position(this.buffer, stringIdsOff);
+ final long[] offsets = new long[stringIdsSize];
+ for (int i = 0; i < stringIdsSize; i++) {
+ offsets[i] = Buffers.readUInt(this.buffer);
+ }
+ return offsets;
+ }
+
+ /**
+ * read dex encoding string.
+ */
+ @NonNull
+ private String readString() {
+ // the length is char len, not byte len
+ final int strLen = this.readVarInts();
+ return this.readString(strLen);
+ }
+
+ /**
+ * read Modified UTF-8 encoding str.
+ *
+ * @param strLen the java-utf16-char len, not strLen nor bytes len.
+ */
+ @NonNull
+ private String readString(final int strLen) {
+ final char[] chars = new char[strLen];
+ for (int i = 0; i < strLen; i++) {
+ final short a = Buffers.readUByte(this.buffer);
+ if ((a & 0x80) == 0) {
+ // ascii char
+ chars[i] = (char) a;
+ } else if ((a & 0xe0) == 0xc0) {
+ // read one more
+ final short b = Buffers.readUByte(this.buffer);
+ chars[i] = (char) (((a & 0x1F) << 6) | (b & 0x3F));
+ } else if ((a & 0xf0) == 0xe0) {
+ final short b = Buffers.readUByte(this.buffer);
+ final short c = Buffers.readUByte(this.buffer);
+ chars[i] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F));
+ } else //noinspection StatementWithEmptyBody
+ if ((a & 0xf0) == 0xf0) {
+ //throw new UTFDataFormatException();
+ } else {
+ //throw new UTFDataFormatException();
+ }
+ //noinspection StatementWithEmptyBody
+ if (chars[i] == 0) {
+ // the end of string.
+ }
+ }
+ return new String(chars);
+ }
+
+ /**
+ * read varints.
+ */
+ private int readVarInts() {
+ int i = 0;
+ int count = 0;
+ short s;
+ do {
+ if (count > 4) {
+ throw new ParserException("read varints error.");
+ }
+ s = Buffers.readUByte(this.buffer);
+ i |= (s & 0x7f) << (count * 7);
+ count++;
+ } while ((s & 0x80) != 0);
+ return i;
+ }
+
+ private DexHeader readDexHeader() {
+ // check sum. skip
+ this.buffer.getInt();
+ // signature skip
+ Buffers.readBytes(this.buffer, DexHeader.kSHA1DigestLen);
+ final DexHeader header = new DexHeader();
+ header.setFileSize(Buffers.readUInt(this.buffer));
+ header.setHeaderSize(Buffers.readUInt(this.buffer));
+ // skip?
+ Buffers.readUInt(this.buffer);
+ // static link data
+ header.setLinkSize(Buffers.readUInt(this.buffer));
+ header.setLinkOff(Buffers.readUInt(this.buffer));
+ // the map data is just the same as dex header.
+ header.setMapOff(Buffers.readUInt(this.buffer));
+ header.setStringIdsSize(this.buffer.getInt());
+ header.setStringIdsOff(Buffers.readUInt(this.buffer));
+ header.setTypeIdsSize(this.buffer.getInt());
+ header.setTypeIdsOff(Buffers.readUInt(this.buffer));
+ header.setProtoIdsSize(this.buffer.getInt());
+ header.setProtoIdsOff(Buffers.readUInt(this.buffer));
+ header.setFieldIdsSize(this.buffer.getInt());
+ header.setFieldIdsOff(Buffers.readUInt(this.buffer));
+ header.setMethodIdsSize(this.buffer.getInt());
+ header.setMethodIdsOff(Buffers.readUInt(this.buffer));
+ header.setClassDefsSize(this.buffer.getInt());
+ header.setClassDefsOff(Buffers.readUInt(this.buffer));
+ header.setDataSize(this.buffer.getInt());
+ header.setDataOff(Buffers.readUInt(this.buffer));
+ Buffers.position(this.buffer, header.getHeaderSize());
+ return header;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt
new file mode 100644
index 00000000..aa0790f5
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.kt
@@ -0,0 +1,44 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.bean.CertificateMeta
+import net.dongliu.apk.parser.cert.asn1.*
+import net.dongliu.apk.parser.cert.pkcs7.*
+import net.dongliu.apk.parser.parser.CertificateMetas.from
+import net.dongliu.apk.parser.utils.Buffers
+import java.io.ByteArrayInputStream
+import java.nio.ByteBuffer
+import java.security.cert.*
+
+/**
+ * Parser certificate info using jsse.
+ *
+ * @author dongliu
+ */
+internal class JSSECertificateParser(data: ByteArray) : CertificateParser(data) {
+ @Throws(CertificateException::class)
+ override fun parse(): List {
+ val contentInfo: ContentInfo = try {
+ Asn1BerParser.parse(ByteBuffer.wrap(data), ContentInfo::class.java)
+ } catch (e: Asn1DecodingException) {
+ throw CertificateException(e)
+ }
+ if (Pkcs7Constants.OID_SIGNED_DATA != contentInfo.contentType) {
+ throw CertificateException("Unsupported ContentInfo.contentType: " + contentInfo.contentType)
+ }
+ val signedData: SignedData = try {
+ Asn1BerParser.parse(contentInfo.content.encoded, SignedData::class.java)
+ } catch (e: Asn1DecodingException) {
+ throw CertificateException(e)
+ }
+ val encodedCertificates = signedData.certificates
+ val certFactory = CertificateFactory.getInstance("X.509")
+ val result: MutableList = ArrayList(encodedCertificates.size)
+ for (i in encodedCertificates.indices) {
+ val encodedCertificate = encodedCertificates[i]
+ val encodedForm = Buffers.readBytes(encodedCertificate.encoded)
+ val certificate = certFactory.generateCertificate(ByteArrayInputStream(encodedForm))
+ result.add(certificate as X509Certificate)
+ }
+ return from(result)
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java
new file mode 100644
index 00000000..31c22378
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java
@@ -0,0 +1,230 @@
+package net.dongliu.apk.parser.parser;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.exception.ParserException;
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.struct.StringPoolHeader;
+import net.dongliu.apk.parser.struct.resource.LibraryEntry;
+import net.dongliu.apk.parser.struct.resource.LibraryHeader;
+import net.dongliu.apk.parser.struct.resource.NullHeader;
+import net.dongliu.apk.parser.struct.resource.PackageHeader;
+import net.dongliu.apk.parser.struct.resource.ResourcePackage;
+import net.dongliu.apk.parser.struct.resource.ResourceTable;
+import net.dongliu.apk.parser.struct.resource.ResourceTableHeader;
+import net.dongliu.apk.parser.struct.resource.Type;
+import net.dongliu.apk.parser.struct.resource.TypeHeader;
+import net.dongliu.apk.parser.struct.resource.TypeSpec;
+import net.dongliu.apk.parser.struct.resource.TypeSpecHeader;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.Pair;
+import net.dongliu.apk.parser.utils.ParseUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Parse android resource table file.
+ *
+ * @author dongliu
+ * @see ResourceTypes.h
+ * @see ResourceTypes.cpp
+ */
+public class ResourceTableParser {
+
+ /**
+ * By default the data buffer Chunks is buffer little-endian byte order both at runtime and when stored buffer files.
+ */
+ private final ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
+ private StringPool stringPool;
+ @NonNull
+ private final ByteBuffer buffer;
+ /**
+ * the resource table file size
+ */
+ public ResourceTable resourceTable;
+ @NonNull
+ public final Set locales;
+
+ public ResourceTableParser(final @NonNull ByteBuffer buffer) {
+ this.buffer = buffer.duplicate();
+ this.buffer.order(this.byteOrder);
+ this.locales = new HashSet<>();
+ }
+
+ /**
+ * parse resource table file.
+ */
+ public void parse() {
+ // read resource file header.
+ final ResourceTableHeader resourceTableHeader = (ResourceTableHeader) this.readChunkHeader();
+ // read string pool chunk
+ final StringPool stringPool = ParseUtils.readStringPool(this.buffer, (StringPoolHeader) this.readChunkHeader());
+ this.stringPool = stringPool;
+ this.resourceTable = new ResourceTable(stringPool);
+ final long packageCount = resourceTableHeader.getPackageCount();
+ if (packageCount != 0) {
+ PackageHeader packageHeader = (PackageHeader) this.readChunkHeader();
+ for (int i = 0; i < packageCount; i++) {
+ final Pair pair = this.readPackage(packageHeader);
+ this.resourceTable.addPackage(pair.getLeft());
+ packageHeader = pair.getRight();
+ }
+ }
+ }
+
+ /**
+ * read one package
+ */
+ private Pair readPackage(@NonNull final PackageHeader packageHeader) {
+ final Pair pair = new Pair<>();
+ //read packageHeader
+ final ResourcePackage resourcePackage = new ResourcePackage(packageHeader);
+ pair.setLeft(resourcePackage);
+ final long beginPos = this.buffer.position();
+ // read type string pool
+ if (packageHeader.getTypeStrings() > 0) {
+ Buffers.position(this.buffer, beginPos + packageHeader.getTypeStrings() - (int) packageHeader.headerSize);
+ resourcePackage.setTypeStringPool(ParseUtils.readStringPool(this.buffer,
+ (StringPoolHeader) this.readChunkHeader()));
+ }
+ //read key string pool
+ if (packageHeader.getKeyStrings() > 0) {
+ Buffers.position(this.buffer, beginPos + packageHeader.getKeyStrings() - (int) packageHeader.headerSize);
+ resourcePackage.setKeyStringPool(ParseUtils.readStringPool(this.buffer,
+ (StringPoolHeader) this.readChunkHeader()));
+ }
+ outer:
+ while (this.buffer.hasRemaining()) {
+ final ChunkHeader chunkHeader = this.readChunkHeader();
+ final long chunkBegin = this.buffer.position();
+ switch ((int) chunkHeader.chunkType) {
+ case ChunkType.TABLE_TYPE_SPEC:
+ final TypeSpecHeader typeSpecHeader = (TypeSpecHeader) chunkHeader;
+ final long[] entryFlags = new long[typeSpecHeader.getEntryCount()];
+ for (int i = 0; i < typeSpecHeader.getEntryCount(); i++) {
+ entryFlags[i] = Buffers.readUInt(this.buffer);
+ }
+ //id start from 1
+ final String typeSpecName = resourcePackage.getTypeStringPool()
+ .get(typeSpecHeader.getId() - 1);
+ final TypeSpec typeSpec = new TypeSpec(typeSpecHeader, entryFlags, typeSpecName);
+ resourcePackage.addTypeSpec(typeSpec);
+ Buffers.position(this.buffer, chunkBegin + typeSpecHeader.getBodySize());
+ break;
+ case ChunkType.TABLE_TYPE:
+ final TypeHeader typeHeader = (TypeHeader) chunkHeader;
+ // read offsets table
+ final long[] offsets = new long[typeHeader.entryCount];
+ for (int i = 0; i < typeHeader.entryCount; i++) {
+ if( (typeHeader.getFlags() & 0x01 ) == 0x01 ) /* FLAG_SPARSE */ {
+ throw new RuntimeException("FLAG_SPARSE unsupported at the moment");
+ } else if( (typeHeader.getFlags() & 0x02 ) == 0x02 ) /* FLAG_OFFSET16 */ {
+ offsets[i] = Buffers.readUShort(buffer) * 4L;
+ } else {
+ offsets[i] = Buffers.readUInt(buffer);
+ }
+ }
+ final Type type = new Type(typeHeader);
+ type.setName(resourcePackage.getTypeStringPool().get(typeHeader.getId() - 1));
+ final long entryPos = chunkBegin + typeHeader.entriesStart - (int) typeHeader.headerSize;
+ Buffers.position(this.buffer, entryPos);
+ final ByteBuffer b = this.buffer.slice();
+ b.order(this.byteOrder);
+ type.setBuffer(b);
+ type.setKeyStringPool(resourcePackage.getKeyStringPool());
+ type.setOffsets(offsets);
+ type.setStringPool(this.stringPool);
+ resourcePackage.addType(type);
+ this.locales.add(type.locale);
+ Buffers.position(this.buffer, chunkBegin + typeHeader.getBodySize());
+ break;
+ case ChunkType.TABLE_PACKAGE:
+ // another package. we should read next package here
+ pair.setRight((PackageHeader) chunkHeader);
+ break outer;
+ case ChunkType.TABLE_LIBRARY:
+ // read entries
+ final LibraryHeader libraryHeader = (LibraryHeader) chunkHeader;
+ for (long i = 0; i < libraryHeader.getCount(); i++) {
+ final int packageId = this.buffer.getInt();
+ final String name = Buffers.readZeroTerminatedString(this.buffer, 128);
+ final LibraryEntry entry = new LibraryEntry(packageId, name);
+ //TODO: now just skip it..
+ }
+ Buffers.position(this.buffer, chunkBegin + chunkHeader.getBodySize());
+ break;
+ case ChunkType.NULL:
+// Buffers.position(buffer, chunkBegin + chunkHeader.getBodySize());
+ Buffers.position(this.buffer, this.buffer.position() + this.buffer.remaining());
+ break;
+ default:
+ throw new ParserException("unexpected chunk type: 0x" + (int) chunkHeader.chunkType);
+ }
+ }
+ return pair;
+
+ }
+
+ @NonNull
+ private ChunkHeader readChunkHeader() {
+ final long begin = this.buffer.position();
+ final int chunkType = Buffers.readUShort(this.buffer);
+ final int headerSize = Buffers.readUShort(this.buffer);
+ final int chunkSize = (int) Buffers.readUInt(this.buffer);
+ switch (chunkType) {
+ case ChunkType.TABLE: {
+ final ResourceTableHeader resourceTableHeader = new ResourceTableHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return resourceTableHeader;
+ }
+ case ChunkType.STRING_POOL: {
+ final StringPoolHeader stringPoolHeader = new StringPoolHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return stringPoolHeader;
+ }
+ case ChunkType.TABLE_PACKAGE: {
+ final PackageHeader packageHeader = new PackageHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return packageHeader;
+ }
+ case ChunkType.TABLE_TYPE_SPEC: {
+ final TypeSpecHeader typeSpecHeader = new TypeSpecHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return typeSpecHeader;
+ }
+ case ChunkType.TABLE_TYPE: {
+ final TypeHeader typeHeader = new TypeHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return typeHeader;
+ }
+ case ChunkType.TABLE_LIBRARY: {
+ //DynamicRefTable
+ final LibraryHeader libraryHeader = new LibraryHeader(headerSize, chunkSize, this.buffer);
+ Buffers.position(this.buffer, begin + headerSize);
+ return libraryHeader;
+ }
+ case ChunkType.TABLE_OVERLAYABLE:
+ case ChunkType.NULL: {
+ Buffers.position(this.buffer, begin + headerSize);
+ return new NullHeader(headerSize, chunkSize);
+ }
+ case ChunkType.TABLE_STAGED_ALIAS:
+ //unknown how to handle this, and it causes a crash when being treated as NullHeader : https://github.com/AndroidDeveloperLB/apk-parser/issues/1#issuecomment-1152937896
+ default:
+ throw new ParserException("Unexpected chunk Type: 0x" + Integer.toHexString(chunkType));
+ }
+ }
+
+ @Nullable
+ public ResourceTable getResourceTable() {
+ return this.resourceTable;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt
new file mode 100644
index 00000000..3bd4f972
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.kt
@@ -0,0 +1,6 @@
+package net.dongliu.apk.parser.parser
+
+/**
+ * class for sort string pool indexes
+ */
+class StringPoolEntry(@JvmField val idx: Int, @JvmField val offset: Long)
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt
new file mode 100644
index 00000000..6e70c189
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.kt
@@ -0,0 +1,64 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.struct.xml.*
+
+/**
+ * the xml file's namespaces.
+ *
+ * @author dongliu
+ */
+internal class XmlNamespaces {
+ private val namespaces: MutableList = ArrayList()
+ private val newNamespaces: MutableList = ArrayList()
+ fun addNamespace(tag: XmlNamespaceStartTag) {
+ val namespace = XmlNamespace(tag.prefix, tag.uri)
+ namespaces.add(namespace)
+ newNamespaces.add(namespace)
+ }
+
+ fun removeNamespace(tag: XmlNamespaceEndTag) {
+ val namespace = XmlNamespace(tag.prefix, tag.uri)
+ namespaces.remove(namespace)
+ newNamespaces.remove(namespace)
+ }
+
+ fun getPrefixViaUri(uri: String?): String? {
+ if (uri == null) {
+ return null
+ }
+ for (namespace in namespaces) {
+ if (uri == namespace.uri) {
+ return namespace.prefix
+ }
+ }
+ return null
+ }
+
+ fun consumeNameSpaces(): List {
+ return if (newNamespaces.isNotEmpty()) {
+ val xmlNamespaces: List =
+ ArrayList(newNamespaces)
+ newNamespaces.clear()
+ xmlNamespaces
+ } else {
+ emptyList()
+ }
+ }
+
+ /**
+ * one namespace
+ */
+ class XmlNamespace constructor(@JvmField val prefix: String?, @JvmField val uri: String?) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is XmlNamespace) return false
+ return prefix == other.prefix && uri == other.uri
+ }
+
+ override fun hashCode(): Int {
+ var result = prefix?.hashCode() ?: 0
+ result = 31 * result + (uri?.hashCode() ?: 0)
+ return result
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt
new file mode 100644
index 00000000..2f11e8c6
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.kt
@@ -0,0 +1,20 @@
+package net.dongliu.apk.parser.parser
+
+import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag
+import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag
+import net.dongliu.apk.parser.struct.xml.XmlCData
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag
+
+/**
+ * callback interface for parse binary xml file.
+ *
+ * @author dongliu
+ */
+interface XmlStreamer {
+ fun onStartTag(xmlNodeStartTag: XmlNodeStartTag)
+ fun onEndTag(xmlNodeEndTag: XmlNodeEndTag)
+ fun onCData(xmlCData: XmlCData)
+ fun onNamespaceStart(tag: XmlNamespaceStartTag)
+ fun onNamespaceEnd(tag: XmlNamespaceEndTag)
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java
new file mode 100644
index 00000000..866e0423
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java
@@ -0,0 +1,126 @@
+package net.dongliu.apk.parser.parser;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.struct.xml.Attribute;
+import net.dongliu.apk.parser.struct.xml.XmlCData;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNodeStartTag;
+import net.dongliu.apk.parser.utils.xml.XmlEscaper;
+
+import java.util.List;
+
+/**
+ * trans to xml text when parse binary xml file.
+ *
+ * @author dongliu
+ */
+public class XmlTranslator implements XmlStreamer {
+ @NonNull
+ private final StringBuilder sb;
+ private int shift = 0;
+ @NonNull
+ private final XmlNamespaces namespaces;
+ private boolean isLastStartTag;
+
+ public XmlTranslator() {
+ this.sb = new StringBuilder();
+ this.sb.append("\n");
+ this.namespaces = new XmlNamespaces();
+ }
+
+ @Override
+ public void onStartTag(final @NonNull XmlNodeStartTag xmlNodeStartTag) {
+ if (this.isLastStartTag) {
+ this.sb.append(">\n");
+ }
+ this.appendShift(this.shift++);
+ this.sb.append('<');
+ if (xmlNodeStartTag.namespace != null) {
+ final String prefix = this.namespaces.getPrefixViaUri(xmlNodeStartTag.namespace);
+ if (prefix != null) {
+ this.sb.append(prefix).append(":");
+ } else {
+ this.sb.append(xmlNodeStartTag.namespace).append(":");
+ }
+ }
+ this.sb.append(xmlNodeStartTag.name);
+ final List nps = this.namespaces.consumeNameSpaces();
+ if (!nps.isEmpty()) {
+ for (final XmlNamespaces.XmlNamespace np : nps) {
+ this.sb.append(" xmlns:").append(np.prefix).append("=\"")
+ .append(np.uri)
+ .append("\"");
+ }
+ }
+ this.isLastStartTag = true;
+ for (final Attribute attribute : xmlNodeStartTag.attributes.attributes) {
+ this.onAttribute(attribute);
+ }
+ }
+
+ private void onAttribute(final Attribute attribute) {
+ this.sb.append(" ");
+ String namespace = this.namespaces.getPrefixViaUri(attribute.namespace);
+ if (namespace == null) {
+ namespace = attribute.namespace;
+ }
+ if (!namespace.isEmpty()) {
+ this.sb.append(namespace).append(':');
+ }
+ final String escapedFinalValue = XmlEscaper.escapeXml10(attribute.value);
+ this.sb.append(attribute.name).append('=').append('"')
+ .append(escapedFinalValue).append('"');
+ }
+
+ @Override
+ public void onEndTag(@NonNull final XmlNodeEndTag xmlNodeEndTag) {
+ --this.shift;
+ if (this.isLastStartTag) {
+ this.sb.append(" />\n");
+ } else {
+ this.appendShift(this.shift);
+ this.sb.append("");
+ if (xmlNodeEndTag.getNamespace() != null) {
+ String namespace = this.namespaces.getPrefixViaUri(xmlNodeEndTag.getNamespace());
+ if (namespace == null) {
+ namespace = xmlNodeEndTag.getNamespace();
+ }
+ this.sb.append(namespace).append(":");
+ }
+ this.sb.append(xmlNodeEndTag.getName());
+ this.sb.append(">\n");
+ }
+ this.isLastStartTag = false;
+ }
+
+ @Override
+ public void onCData(@NonNull final XmlCData xmlCData) {
+ this.appendShift(this.shift);
+ this.sb.append(xmlCData.getValue()).append('\n');
+ this.isLastStartTag = false;
+ }
+
+ @Override
+ public void onNamespaceStart(@NonNull final XmlNamespaceStartTag tag) {
+ this.namespaces.addNamespace(tag);
+ }
+
+ @Override
+ public void onNamespaceEnd(@NonNull final XmlNamespaceEndTag tag) {
+ this.namespaces.removeNamespace(tag);
+ }
+
+ private void appendShift(final int shift) {
+ for (int i = 0; i < shift; i++) {
+ this.sb.append("\t");
+ }
+ }
+
+ @NonNull
+ public String getXml() {
+ return this.sb.toString();
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt
new file mode 100644
index 00000000..89ebe98a
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.kt
@@ -0,0 +1,33 @@
+package net.dongliu.apk.parser.struct
+
+/**
+ * android system file.
+ *
+ * @author dongiu
+ */
+object AndroidConstants {
+ const val RESOURCE_FILE = "resources.arsc"
+ const val MANIFEST_FILE = "AndroidManifest.xml"
+ const val DEX_FILE = "classes.dex"
+ const val DEX_ADDITIONAL = "classes%d.dex"
+// const val RES_PREFIX = "res/"
+// const val ASSETS_PREFIX = "assets/"
+// const val LIB_PREFIX = "lib/"
+// const val META_PREFIX = "META-INF/"
+// const val ARCH_ARMEABI = ""
+
+// /**
+// * the binary xml file used system attr id.
+// */
+// const val ATTR_ID_START = 0x01010000
+
+ /**
+ * start offset for system android.R.style
+ */
+ const val SYS_STYLE_ID_START = 0x01030000
+
+ /**
+ * end offset for system android.R.style
+ */
+ const val SYS_STYLE_ID_END = 0x01031000
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt
new file mode 100644
index 00000000..a4ecbb16
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.kt
@@ -0,0 +1,52 @@
+package net.dongliu.apk.parser.struct
+
+import net.dongliu.apk.parser.utils.Unsigned
+
+/**
+ * A Chunk is just a piece of memory split into two parts, a header and a body.
+ * The exact structure of the header and the body of a given Chunk is determined by its type.
+ *
+ * chunk header struct.
+ * struct ResChunk_header {
+ * uint16_t type;
+ * uint16_t headerSize;
+ * uint32_t size;
+ * }
+ *
+ *
+ * @author dongliu
+ */
+open class ChunkHeader(chunkType: Int, headerSize: Int, chunkSize: Long) {
+ /**
+ * Type identifier for this chunk. The meaning of this value depends
+ * on the containing chunk.
+ */
+ @JvmField
+ val chunkType: Short
+
+ /**
+ * Size of the chunk header (in bytes). Adding this value to
+ * the address of the chunk allows you to find its associated data
+ * (if any).
+ */
+ @JvmField
+ val headerSize: Short
+
+ /**
+ * Total size of this chunk (in bytes). This is the chunkSize plus
+ * the size of any data associated with the chunk. Adding this value
+ * to the chunk allows you to completely skip its contents (including
+ * any child chunks). If this value is the same as chunkSize, there is
+ * no data associated with the chunk.
+ */
+ val chunkSize: Int
+
+ init {
+ this.chunkType = Unsigned.toUShort(chunkType)
+ this.headerSize = Unsigned.toUShort(headerSize)
+ this.chunkSize = Unsigned.ensureUInt(chunkSize)
+ }
+
+ val bodySize: Int
+ get() = chunkSize - headerSize
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt
new file mode 100644
index 00000000..3c9e4d6b
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ChunkType.kt
@@ -0,0 +1,51 @@
+package net.dongliu.apk.parser.struct
+
+/**
+ * Resource type
+ * see https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h
+ *
+ * @author dongliu
+ */
+object ChunkType {
+ const val NULL = 0x0000
+ const val STRING_POOL = 0x0001
+ const val TABLE = 0x0002
+ const val XML = 0x0003
+
+ /**
+ * Chunk types in XML
+ */
+ const val XML_FIRST_CHUNK = 0x0100
+ const val XML_START_NAMESPACE = 0x0100
+ const val XML_END_NAMESPACE = 0x0101
+ const val XML_START_ELEMENT = 0x0102
+ const val XML_END_ELEMENT = 0x0103
+ const val XML_CDATA = 0x0104
+ const val XML_LAST_CHUNK = 0x017f
+
+ /**
+ * This contains a uint32_t array mapping strings in the string
+ * pool back to resource identifiers. It is optional.
+ */
+ const val XML_RESOURCE_MAP = 0x0180
+
+ /**
+ * Chunk types in RES_TABLE_TYPE
+ */
+ const val TABLE_PACKAGE = 0x0200
+ const val TABLE_TYPE = 0x0201
+ const val TABLE_TYPE_SPEC = 0x0202
+
+ /**
+ * android5.0+
+ * DynamicRefTable
+ */
+ const val TABLE_LIBRARY = 0x0203
+
+ /**
+ * TODO: handle the chunks types below
+ * https://github.com/hsiafan/apk-parser/issues/96#issuecomment-500275300 https://github.com/AndroidDeveloperLB/apk-parser/issues/1
+ */
+ const val TABLE_OVERLAYABLE = 0x0204
+ const val TABLE_STAGED_ALIAS = 0x0206
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt
new file mode 100644
index 00000000..903a0ffe
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResValue.kt
@@ -0,0 +1,184 @@
+package net.dongliu.apk.parser.struct
+
+/**
+ * Apk res value struct.
+ * Only for description now, The value is hold in ResourceValue
+ *
+ * @author dongliu
+ */
+object ResValue {
+ object ResType {
+ /**
+ * Contains no data.
+ */
+ const val NULL: Short = 0x00
+
+ /**
+ * The 'data' holds a ResTable_ref; a reference to another resource
+ * table entry.
+ */
+ const val REFERENCE: Short = 0x01
+ // /**
+ // * The 'data' holds an attribute resource identifier.
+ // */
+ // public static final short ATTRIBUTE = 0x02;
+ /**
+ * The 'data' holds an index into the containing resource table's
+ * global value string pool.
+ */
+ const val STRING: Short = 0x03
+ // /**
+ // * The 'data' holds a single-precision floating point number.
+ // */
+ // public static final short FLOAT = 0x04;
+ /**
+ * The 'data' holds a complex number encoding a dimension value;
+ * such as "100in".
+ */
+ const val DIMENSION: Short = 0x05
+
+ /**
+ * The 'data' holds a complex number encoding a fraction of a
+ * container.
+ */
+ const val FRACTION: Short = 0x06
+
+ /**
+ The 'data' holds a dynamic ResTable_ref, which needs to be
+ resolved before it can be used like a TYPE_REFERENCE.
+ */
+ const val TYPE_DYNAMIC_REFERENCE: Short = 0x07
+
+ // /**
+ // * Beginning of integer flavors...
+ // */
+ // public static final short FIRST_INT = 0x10;
+ /**
+ * The 'data' is a raw integer value of the form n..n.
+ */
+ const val INT_DEC: Short = 0x10
+
+ /**
+ * The 'data' is a raw integer value of the form 0xn..n.
+ */
+ const val INT_HEX: Short = 0x11
+
+ /**
+ * The 'data' is either 0 or 1; for input "false" or "true" respectively.
+ */
+ const val INT_BOOLEAN: Short = 0x12
+ // /**
+ // * Beginning of color integer flavors...
+ // */
+ // public static final short FIRST_COLOR_INT = 0x1c;
+ /**
+ * The 'data' is a raw integer value of the form #aarrggbb.
+ */
+ const val INT_COLOR_ARGB8: Short = 0x1c
+
+ /**
+ * The 'data' is a raw integer value of the form #rrggbb.
+ */
+ const val INT_COLOR_RGB8: Short = 0x1d
+
+ /**
+ * The 'data' is a raw integer value of the form #argb.
+ */
+ const val INT_COLOR_ARGB4: Short = 0x1e
+
+ /**
+ * The 'data' is a raw integer value of the form #rgb.
+ */
+ const val INT_COLOR_RGB4: Short = 0x1f // /**
+ // * ...end of integer flavors.
+ // */
+ // public static final short LAST_COLOR_INT = 0x1f;
+ //
+ // /**
+ // * ...end of integer flavors.
+ // */
+ // public static final short LAST_INT = 0x1f;
+ }
+
+ /**
+ * A number of constants used when the data is interpreted as a composite value are defined
+ * by the following anonymous C++ enum
+ */
+ object ResDataCOMPLEX {
+ // /**
+ // * Where the unit type information is. This gives us 16 possible
+ // * types; as defined below.
+ // */
+ // public static final short UNIT_SHIFT = 0;
+ // public static final short UNIT_MASK = 0xf;
+ /**
+ * TYPE_DIMENSION: Value is raw pixels.
+ */
+ const val UNIT_PX: Short = 0
+
+ /**
+ * TYPE_DIMENSION: Value is Device Independent Pixels.
+ */
+ const val UNIT_DIP: Short = 1
+
+ /**
+ * TYPE_DIMENSION: Value is a Scaled device independent Pixels.
+ */
+ const val UNIT_SP: Short = 2
+
+ /**
+ * TYPE_DIMENSION: Value is in points.
+ */
+ const val UNIT_PT: Short = 3
+
+ /**
+ * TYPE_DIMENSION: Value is in inches.
+ */
+ const val UNIT_IN: Short = 4
+
+ /**
+ * TYPE_DIMENSION: Value is in millimeters.
+ */
+ const val UNIT_MM: Short = 5
+
+ /**
+ * TYPE_FRACTION: A basic fraction of the overall size.
+ */
+ const val UNIT_FRACTION: Short = 0
+
+ /**
+ * TYPE_FRACTION: A fraction of the parent size.
+ */
+ const val UNIT_FRACTION_PARENT: Short = 1 // /**
+ // * Where the radix information is; telling where the decimal place
+ // * appears in the mantissa. This give us 4 possible fixed point
+ // * representations as defined below.
+ // */
+ // public static final short RADIX_SHIFT = 4;
+ // public static final short RADIX_MASK = 0x3;
+ //
+ // /**
+ // * The mantissa is an integral number -- i.e.; 0xnnnnnn.0
+ // */
+ // public static final short RADIX_23p0 = 0;
+ // /**
+ // * The mantissa magnitude is 16 bits -- i.e; 0xnnnn.nn
+ // */
+ // public static final short RADIX_16p7 = 1;
+ // /**
+ // * The mantissa magnitude is 8 bits -- i.e; 0xnn.nnnn
+ // */
+ // public static final short RADIX_8p15 = 2;
+ // /**
+ // * The mantissa magnitude is 0 bits -- i.e; 0x0.nnnnnn
+ // */
+ // public static final short RADIX_0p23 = 3;
+ //
+ // /**
+ // * Where the actual value is. This gives us 23 bits of
+ // * precision. The top bit is the sign.
+ // */
+ // public static final short MANTISSA_SHIFT = 8;
+ // public static final int MANTISSA_MASK = 0xffffff;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java
new file mode 100644
index 00000000..9aefaa81
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java
@@ -0,0 +1,316 @@
+package net.dongliu.apk.parser.struct;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.resource.Densities;
+import net.dongliu.apk.parser.struct.resource.ResourceEntry;
+import net.dongliu.apk.parser.struct.resource.ResourceTable;
+import net.dongliu.apk.parser.struct.resource.Type;
+import net.dongliu.apk.parser.struct.resource.TypeSpec;
+import net.dongliu.apk.parser.utils.Locales;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Resource entity, contains the resource id, should retrieve the value from resource table, or string pool if it is a string resource.
+ *
+ * @author dongliu
+ */
+public abstract class ResourceValue {
+ protected final int value;
+
+ protected ResourceValue(final int value) {
+ this.value = value;
+ }
+
+ /**
+ * get value as string.
+ */
+ @Nullable
+ public abstract String toStringValue(ResourceTable resourceTable, Locale locale);
+
+ @NonNull
+ public static ResourceValue decimal(final int value) {
+ return new DecimalResourceValue(value);
+ }
+
+ @NonNull
+ public static ResourceValue hexadecimal(final int value) {
+ return new HexadecimalResourceValue(value);
+ }
+
+ @NonNull
+ public static ResourceValue bool(final int value) {
+ return new BooleanResourceValue(value);
+ }
+
+ @NonNull
+ public static ResourceValue string(final int value, final StringPool stringPool) {
+ return new StringResourceValue(value, stringPool);
+ }
+
+ @NonNull
+ public static ResourceValue reference(final int value) {
+ return new ReferenceResourceValue(value);
+ }
+
+ @NonNull
+ public static ResourceValue nullValue() {
+ return NullResourceValue.instance;
+ }
+
+ @NonNull
+ public static ResourceValue rgb(final int value, final int len) {
+ return new RGBResourceValue(value, len);
+ }
+
+ @NonNull
+ public static ResourceValue dimension(final int value) {
+ return new DimensionValue(value);
+ }
+
+ @NonNull
+ public static ResourceValue fraction(final int value) {
+ return new FractionValue(value);
+ }
+
+ @NonNull
+ public static ResourceValue raw(final int value, final short type) {
+ return new RawValue(value, type);
+ }
+
+ private static class DecimalResourceValue extends ResourceValue {
+
+ private DecimalResourceValue(final int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ return String.valueOf(this.value);
+ }
+ }
+
+ private static class HexadecimalResourceValue extends ResourceValue {
+
+ private HexadecimalResourceValue(final int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ return "0x" + Integer.toHexString(this.value);
+ }
+ }
+
+ private static class BooleanResourceValue extends ResourceValue {
+
+ private BooleanResourceValue(final int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ return String.valueOf(this.value != 0);
+ }
+ }
+
+ private static class StringResourceValue extends ResourceValue {
+ private final StringPool stringPool;
+
+ private StringResourceValue(final int value, final StringPool stringPool) {
+ super(value);
+ this.stringPool = stringPool;
+ }
+
+ @Nullable
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ if (this.value >= 0) {
+ return this.stringPool.get(this.value);
+ } else {
+ return null;
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return this.value + ":" + this.stringPool.get(this.value);
+ }
+ }
+
+ /**
+ * ReferenceResource ref one another resources, and may has different value for different resource config(locale, density, etc)
+ */
+ public static class ReferenceResourceValue extends ResourceValue {
+
+ private ReferenceResourceValue(final int value) {
+ super(value);
+ }
+
+ @Override
+ @Nullable
+ public String toStringValue(final @Nullable ResourceTable resourceTable, final Locale locale) {
+ final long resourceId = this.getReferenceResourceId();
+ // android system styles.
+ if (resourceId > AndroidConstants.SYS_STYLE_ID_START && resourceId < AndroidConstants.SYS_STYLE_ID_END) {
+ return "@android:style/" + ResourceTable.sysStyle.get((int) resourceId);
+ }
+ final String raw = "resourceId:0x" + Long.toHexString(resourceId);
+ if (resourceTable == null) {
+ return raw;
+ }
+ final List resources = resourceTable.getResourcesById(resourceId);
+ // read from type resource
+ ResourceEntry selected = null;
+ TypeSpec typeSpec = null;
+ int currentLocalMatchLevel = -1;
+ int currentDensityLevel = -1;
+ for (final ResourceTable.Resource resource : resources) {
+ final Type type = resource.type;
+ typeSpec = resource.typeSpec;
+ final ResourceEntry resourceEntry = resource.resourceEntry;
+ final int localMatchLevel = Locales.match(locale, type.locale);
+ final int densityLevel = ReferenceResourceValue.densityLevel(type.density);
+ if (localMatchLevel > currentLocalMatchLevel) {
+ selected = resourceEntry;
+ currentLocalMatchLevel = localMatchLevel;
+ currentDensityLevel = densityLevel;
+ } else if (densityLevel > currentDensityLevel) {
+ selected = resourceEntry;
+ currentDensityLevel = densityLevel;
+ }
+ }
+ final String result;
+ if (selected == null) {
+ result = raw;
+ } else if (locale == null) {
+ result = "@" + typeSpec.name + "/" + selected.key;
+ } else {
+ result = selected.toStringValue(resourceTable, locale);
+ }
+ return result;
+ }
+
+ public long getReferenceResourceId() {
+ return this.value & 0xFFFFFFFFL;
+ }
+
+ private static int densityLevel(final int density) {
+ if (density == Densities.ANY || density == Densities.NONE) {
+ return -1;
+ }
+ return density;
+ }
+ }
+
+ private static class NullResourceValue extends ResourceValue {
+ private static final NullResourceValue instance = new NullResourceValue();
+
+ private NullResourceValue() {
+ super(-1);
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ return "";
+ }
+ }
+
+ private static class RGBResourceValue extends ResourceValue {
+ private final int len;
+
+ private RGBResourceValue(final int value, final int len) {
+ super(value);
+ this.len = len;
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = this.len / 2 - 1; i >= 0; i--) {
+ sb.append(Integer.toHexString((this.value >> i * 8) & 0xff));
+ }
+ return sb.toString();
+ }
+ }
+
+ private static class DimensionValue extends ResourceValue {
+
+ private DimensionValue(final int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ final short unit = (short) (this.value & 0xff);
+ final String unitStr;
+ switch (unit) {
+ case ResValue.ResDataCOMPLEX.UNIT_MM:
+ unitStr = "mm";
+ break;
+ case ResValue.ResDataCOMPLEX.UNIT_PX:
+ unitStr = "px";
+ break;
+ case ResValue.ResDataCOMPLEX.UNIT_DIP:
+ unitStr = "dp";
+ break;
+ case ResValue.ResDataCOMPLEX.UNIT_SP:
+ unitStr = "sp";
+ break;
+ case ResValue.ResDataCOMPLEX.UNIT_PT:
+ unitStr = "pt";
+ break;
+ case ResValue.ResDataCOMPLEX.UNIT_IN:
+ unitStr = "in";
+ break;
+ default:
+ unitStr = "unknown unit:0x" + Integer.toHexString(unit);
+ }
+ return (this.value >> 8) + unitStr;
+ }
+ }
+
+ private static class FractionValue extends ResourceValue {
+
+ private FractionValue(final int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ // The low-order 4 bits of the data value specify the type of the fraction
+ final short type = (short) (this.value & 0xf);
+ final String pstr;
+ switch (type) {
+ case ResValue.ResDataCOMPLEX.UNIT_FRACTION:
+ pstr = "%";
+ break;
+ case ResValue.ResDataCOMPLEX.UNIT_FRACTION_PARENT:
+ pstr = "%p";
+ break;
+ default:
+ pstr = "unknown type:0x" + Integer.toHexString(type);
+ }
+ final float f = Float.intBitsToFloat(this.value >> 4);
+ return f + pstr;
+ }
+ }
+
+ private static class RawValue extends ResourceValue {
+ private final short dataType;
+
+ private RawValue(final int value, final short dataType) {
+ super(value);
+ this.dataType = dataType;
+ }
+
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ return "{" + this.dataType + ":" + (this.value & 0xFFFFFFFFL) + "}";
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java
new file mode 100644
index 00000000..32f39507
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPool.java
@@ -0,0 +1,25 @@
+package net.dongliu.apk.parser.struct;
+
+import androidx.annotation.NonNull;
+
+/**
+ * String pool.
+ *
+ * @author dongliu
+ */
+public class StringPool {
+ @NonNull
+ private final String[] pool;
+
+ public StringPool(final int poolSize) {
+ this.pool = new String[poolSize];
+ }
+
+ public String get(final int idx) {
+ return this.pool[idx];
+ }
+
+ public void set(final int idx, final String value) {
+ this.pool[idx] = value;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt
new file mode 100644
index 00000000..71cc9108
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.kt
@@ -0,0 +1,56 @@
+package net.dongliu.apk.parser.struct
+
+import net.dongliu.apk.parser.utils.*
+import java.nio.ByteBuffer
+
+/**
+ * String pool chunk header.
+ *
+ * @author dongliu
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+class StringPoolHeader(headerSize: Int, chunkSize: Long, buffer: ByteBuffer) :
+ ChunkHeader(ChunkType.STRING_POOL, headerSize, chunkSize) {
+ /**
+ * Number of style span arrays in the pool (number of uint32_t indices
+ * follow the string indices).
+ */
+ val stringCount: Int
+
+ /**
+ * Number of style span arrays in the pool (number of uint32_t indices
+ * follow the string indices).
+ */
+ val styleCount: Int
+ val flags: Long
+
+ /**
+ * Index from header of the string data.
+ */
+ val stringsStart: Long
+
+ /**
+ * Index from header of the style data.
+ */
+ val stylesStart: Long
+
+ init {
+ stringCount = Unsigned.ensureUInt(Buffers.readUInt(buffer))
+ this.styleCount = Unsigned.ensureUInt(Buffers.readUInt(buffer))
+ flags = Buffers.readUInt(buffer)
+ stringsStart = Buffers.readUInt(buffer)
+ stylesStart = Buffers.readUInt(buffer)
+ }
+
+ companion object {
+ /**
+ * If set, the string index is sorted by the string values (based on strcmp16()).
+ */
+ const val SORTED_FLAG = 1
+
+ /**
+ * String pool is encoded in UTF-8
+ */
+ const val UTF8_FLAG = 1 shl 8
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java
new file mode 100644
index 00000000..d9610100
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java
@@ -0,0 +1,110 @@
+package net.dongliu.apk.parser.struct.dex;
+
+/**
+ * @author dongliu
+ */
+public class DexClassStruct {
+ /* index into typeIds for this class. u4 */
+ private int classIdx;
+
+ private int accessFlags;
+ /* index into typeIds for superclass. u4 */
+ private int superclassIdx;
+
+ /* file offset to DexTypeList. u4 */
+ private long interfacesOff;
+
+ /* index into stringIds for source file name. u4 */
+ private int sourceFileIdx;
+ /* file offset to annotations_directory_item. u4 */
+ private long annotationsOff;
+ /* file offset to class_data_item. u4 */
+ private long classDataOff;
+ /* file offset to DexEncodedArray. u4 */
+ private long staticValuesOff;
+
+ public static final int ACC_PUBLIC = 0x1;
+ public static int ACC_PRIVATE = 0x2;
+ public static final int ACC_PROTECTED = 0x4;
+ public static final int ACC_STATIC = 0x8;
+ public static int ACC_FINAL = 0x10;
+ public static int ACC_SYNCHRONIZED = 0x20;
+ public static int ACC_VOLATILE = 0x40;
+ public static int ACC_BRIDGE = 0x40;
+ public static int ACC_TRANSIENT = 0x80;
+ public static int ACC_VARARGS = 0x80;
+ public static int ACC_NATIVE = 0x100;
+ public static final int ACC_INTERFACE = 0x200;
+ public static int ACC_ABSTRACT = 0x400;
+ public static int ACC_STRICT = 0x800;
+ public static int ACC_SYNTHETIC = 0x1000;
+ public static final int ACC_ANNOTATION = 0x2000;
+ public static final int ACC_ENUM = 0x4000;
+ public static int ACC_CONSTRUCTOR = 0x10000;
+ public static int ACC_DECLARED_SYNCHRONIZED = 0x20000;
+
+
+ public int getClassIdx() {
+ return this.classIdx;
+ }
+
+ public void setClassIdx(final int classIdx) {
+ this.classIdx = classIdx;
+ }
+
+ public int getAccessFlags() {
+ return this.accessFlags;
+ }
+
+ public void setAccessFlags(final int accessFlags) {
+ this.accessFlags = accessFlags;
+ }
+
+ public int getSuperclassIdx() {
+ return this.superclassIdx;
+ }
+
+ public void setSuperclassIdx(final int superclassIdx) {
+ this.superclassIdx = superclassIdx;
+ }
+
+ public long getInterfacesOff() {
+ return this.interfacesOff;
+ }
+
+ public void setInterfacesOff(final long interfacesOff) {
+ this.interfacesOff = interfacesOff;
+ }
+
+ public int getSourceFileIdx() {
+ return this.sourceFileIdx;
+ }
+
+ public void setSourceFileIdx(final int sourceFileIdx) {
+ this.sourceFileIdx = sourceFileIdx;
+ }
+
+ public long getAnnotationsOff() {
+ return this.annotationsOff;
+ }
+
+ public void setAnnotationsOff(final long annotationsOff) {
+ this.annotationsOff = annotationsOff;
+ }
+
+ public long getClassDataOff() {
+ return this.classDataOff;
+ }
+
+ public void setClassDataOff(final long classDataOff) {
+ this.classDataOff = classDataOff;
+ }
+
+ public long getStaticValuesOff() {
+ return this.staticValuesOff;
+ }
+
+ public void setStaticValuesOff(final long staticValuesOff) {
+ this.staticValuesOff = staticValuesOff;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java
new file mode 100644
index 00000000..1b73bec3
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java
@@ -0,0 +1,271 @@
+package net.dongliu.apk.parser.struct.dex;
+
+/**
+ * dex file header.
+ * see http://dexandroid.googlecode.com/svn/trunk/dalvik/libdex/DexFile.h
+ *
+ * @author dongliu
+ */
+public class DexHeader {
+
+ public static final int kSHA1DigestLen = 20;
+ public static final int kSHA1DigestOutputLen = DexHeader.kSHA1DigestLen * 2 + 1;
+
+ /**
+ * includes version number. 8 bytes.
+ * public short magic;
+ */
+ private int version;
+ /**
+ * adler32 checksum. u4
+ * public long checksum;
+ * SHA-1 hash len = kSHA1DigestLen
+ */
+ private byte[] signature;
+ /**
+ * length of entire file. u4
+ */
+ private long fileSize;
+ /**
+ * len of header.offset to start of next section. u4
+ */
+ private long headerSize;
+ /**
+ * u4
+ * public long endianTag;
+ * u4
+ */
+ private long linkSize;
+ /**
+ * u4
+ */
+ private long linkOff;
+ /**
+ * u4
+ */
+ private long mapOff;
+ /**
+ * u4
+ */
+ private int stringIdsSize;
+ /**
+ * u4
+ */
+ private long stringIdsOff;
+ /**
+ * u4
+ */
+ private int typeIdsSize;
+ /**
+ * u4
+ */
+ private long typeIdsOff;
+ /**
+ * u4
+ */
+ private int protoIdsSize;
+ /**
+ * u4
+ */
+ private long protoIdsOff;
+ /**
+ * u4
+ */
+ private int fieldIdsSize;
+ /**
+ * u4
+ */
+ private long fieldIdsOff;
+ /**
+ * u4
+ */
+ private int methodIdsSize;
+ /**
+ * u4
+ */
+ private long methodIdsOff;
+ /**
+ * u4
+ */
+ private int classDefsSize;
+ /**
+ * u4
+ */
+ private long classDefsOff;
+ /**
+ * u4
+ */
+ private int dataSize;
+ /**
+ * u4
+ */
+ private long dataOff;
+
+ public int getVersion() {
+ return this.version;
+ }
+
+ public void setVersion(final int version) {
+ this.version = version;
+ }
+
+ public byte[] getSignature() {
+ return this.signature;
+ }
+
+ public void setSignature(final byte[] signature) {
+ this.signature = signature;
+ }
+
+ public long getFileSize() {
+ return this.fileSize;
+ }
+
+ public void setFileSize(final long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ public long getHeaderSize() {
+ return this.headerSize;
+ }
+
+ public void setHeaderSize(final long headerSize) {
+ this.headerSize = headerSize;
+ }
+
+ public long getLinkSize() {
+ return this.linkSize;
+ }
+
+ public void setLinkSize(final long linkSize) {
+ this.linkSize = linkSize;
+ }
+
+ public long getLinkOff() {
+ return this.linkOff;
+ }
+
+ public void setLinkOff(final long linkOff) {
+ this.linkOff = linkOff;
+ }
+
+ public long getMapOff() {
+ return this.mapOff;
+ }
+
+ public void setMapOff(final long mapOff) {
+ this.mapOff = mapOff;
+ }
+
+ public int getStringIdsSize() {
+ return this.stringIdsSize;
+ }
+
+ public void setStringIdsSize(final int stringIdsSize) {
+ this.stringIdsSize = stringIdsSize;
+ }
+
+ public long getStringIdsOff() {
+ return this.stringIdsOff;
+ }
+
+ public void setStringIdsOff(final long stringIdsOff) {
+ this.stringIdsOff = stringIdsOff;
+ }
+
+ public int getTypeIdsSize() {
+ return this.typeIdsSize;
+ }
+
+ public void setTypeIdsSize(final int typeIdsSize) {
+ this.typeIdsSize = typeIdsSize;
+ }
+
+ public long getTypeIdsOff() {
+ return this.typeIdsOff;
+ }
+
+ public void setTypeIdsOff(final long typeIdsOff) {
+ this.typeIdsOff = typeIdsOff;
+ }
+
+ public int getProtoIdsSize() {
+ return this.protoIdsSize;
+ }
+
+ public void setProtoIdsSize(final int protoIdsSize) {
+ this.protoIdsSize = protoIdsSize;
+ }
+
+ public long getProtoIdsOff() {
+ return this.protoIdsOff;
+ }
+
+ public void setProtoIdsOff(final long protoIdsOff) {
+ this.protoIdsOff = protoIdsOff;
+ }
+
+ public int getFieldIdsSize() {
+ return this.fieldIdsSize;
+ }
+
+ public void setFieldIdsSize(final int fieldIdsSize) {
+ this.fieldIdsSize = fieldIdsSize;
+ }
+
+ public long getFieldIdsOff() {
+ return this.fieldIdsOff;
+ }
+
+ public void setFieldIdsOff(final long fieldIdsOff) {
+ this.fieldIdsOff = fieldIdsOff;
+ }
+
+ public int getMethodIdsSize() {
+ return this.methodIdsSize;
+ }
+
+ public void setMethodIdsSize(final int methodIdsSize) {
+ this.methodIdsSize = methodIdsSize;
+ }
+
+ public long getMethodIdsOff() {
+ return this.methodIdsOff;
+ }
+
+ public void setMethodIdsOff(final long methodIdsOff) {
+ this.methodIdsOff = methodIdsOff;
+ }
+
+ public int getClassDefsSize() {
+ return this.classDefsSize;
+ }
+
+ public void setClassDefsSize(final int classDefsSize) {
+ this.classDefsSize = classDefsSize;
+ }
+
+ public long getClassDefsOff() {
+ return this.classDefsOff;
+ }
+
+ public void setClassDefsOff(final long classDefsOff) {
+ this.classDefsOff = classDefsOff;
+ }
+
+ public int getDataSize() {
+ return this.dataSize;
+ }
+
+ public void setDataSize(final int dataSize) {
+ this.dataSize = dataSize;
+ }
+
+ public long getDataOff() {
+ return this.dataOff;
+ }
+
+ public void setDataOff(final long dataOff) {
+ this.dataOff = dataOff;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt
new file mode 100644
index 00000000..befc2304
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.kt
@@ -0,0 +1,17 @@
+package net.dongliu.apk.parser.struct.resource
+
+/**
+ * Screen density values
+ */
+object Densities {
+ const val DEFAULT = 0
+ const val LOW = 120
+ const val MEDIUM = 160
+ const val TV = 213
+ const val HIGH = 240
+ const val XHIGH = 320
+ const val XXHIGH = 480
+ const val XXXHIGH = 640
+ const val ANY = 0xfffe
+ const val NONE = 0xffff
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java
new file mode 100644
index 00000000..eaafdb26
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java
@@ -0,0 +1,23 @@
+package net.dongliu.apk.parser.struct.resource;
+
+/**
+ * Library chunk entry
+ *
+ * @author Liu Dong
+ */
+public class LibraryEntry {
+ /**
+ * uint32. The package-id this shared library was assigned at build time.
+ */
+ public final int packageId;
+
+ /**
+ * The package name of the shared library. \0 terminated. max 128
+ */
+ public final String name;
+
+ public LibraryEntry(final int packageId, final String name) {
+ this.packageId = packageId;
+ this.name = name;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt
new file mode 100644
index 00000000..f9f123c9
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.kt
@@ -0,0 +1,23 @@
+package net.dongliu.apk.parser.struct.resource
+
+import net.dongliu.apk.parser.struct.*
+import net.dongliu.apk.parser.utils.Buffers
+import net.dongliu.apk.parser.utils.Unsigned.ensureUInt
+import java.nio.ByteBuffer
+
+/**
+ * Table library chunk header
+ *
+ * @author Liu Dong
+ */
+class LibraryHeader(headerSize: Int, chunkSize: Long, buffer: ByteBuffer) :
+ ChunkHeader(ChunkType.TABLE_LIBRARY, headerSize, chunkSize) {
+ /**
+ * uint32 value, The number of shared libraries linked in this resource table.
+ */
+ val count: Int
+
+ init {
+ count = ensureUInt(Buffers.readUInt(buffer))
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt
new file mode 100644
index 00000000..e93f6919
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.kt
@@ -0,0 +1,6 @@
+package net.dongliu.apk.parser.struct.resource
+
+import net.dongliu.apk.parser.struct.*
+
+class NullHeader(headerSize: Int, chunkSize: Int) :
+ ChunkHeader(ChunkType.NULL, headerSize, chunkSize.toLong())
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java
new file mode 100644
index 00000000..50ff3c2f
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java
@@ -0,0 +1,95 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.ParseUtils;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author dongliu
+ */
+public class PackageHeader extends ChunkHeader {
+
+ /**
+ * ResourcePackage IDs start at 1 (corresponding to the value of the package bits in a resource identifier).
+ * 0 means this is not a base package.
+ * uint32_t
+ * 0 framework-res.apk
+ * 2-9 other framework files
+ * 127 application package
+ * Anroid 5.0+: Shared libraries will be assigned a package ID of 0x00 at build-time.
+ * At runtime, all loaded shared libraries will be assigned a new package ID.
+ */
+ private int id;
+
+ /**
+ * Actual name of this package, -terminated.
+ * char16_t name[128]
+ */
+ @NonNull
+ private final String name;
+
+ /**
+ * Offset to a ResStringPool_header defining the resource type symbol table.
+ * If zero, this package is inheriting from another base package (overriding specific values in it).
+ * uinit 32
+ */
+ private final int typeStrings;
+
+ /**
+ * Last index into typeStrings that is for public use by others.
+ * uint32_t
+ */
+ public final int lastPublicType;
+
+ /**
+ * Offset to a ResStringPool_header defining the resource
+ * key symbol table. If zero, this package is inheriting from
+ * another base package (overriding specific values in it).
+ * uint32_t
+ */
+ private final int keyStrings;
+
+ /**
+ * Last index into keyStrings that is for public use by others.
+ * uint32_t
+ */
+ public final int lastPublicKey;
+
+ public PackageHeader(final int headerSize, final long chunkSize, final @NonNull ByteBuffer buffer) {
+ super(ChunkType.TABLE_PACKAGE, headerSize, chunkSize);
+ this.id = Unsigned.toUInt(Buffers.readUInt(buffer));
+ this.name = ParseUtils.readStringUTF16(buffer, 128);
+ this.typeStrings = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ this.lastPublicType = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ this.keyStrings = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ this.lastPublicKey = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ }
+
+ public long getId() {
+ return Unsigned.toLong(this.id);
+ }
+
+ public void setId(final long id) {
+ this.id = Unsigned.toUInt(id);
+ }
+
+ @NonNull
+ public String getName() {
+ return this.name;
+ }
+
+ public int getTypeStrings() {
+ return this.typeStrings;
+ }
+
+ public int getKeyStrings() {
+ return this.keyStrings;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java
new file mode 100644
index 00000000..ad42f111
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java
@@ -0,0 +1,262 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * used by resource Type.
+ *
+ * @author dongliu
+ */
+public class ResTableConfig {
+ /**
+ * Number of bytes in this structure. uint32_t
+ */
+ private int size;
+
+ /**
+ * Mobile country code (from SIM). 0 means "any". uint16_t
+ */
+ private short mcc;
+ /**
+ * Mobile network code (from SIM). 0 means "any". uint16_t
+ */
+ private short mnc;
+ /**
+ * uint32_t imsi;
+ * 0 means "any". Otherwise, en, fr, etc. char[2]
+ */
+ private String language;
+ /**
+ * 0 means "any". Otherwise, US, CA, etc. char[2]
+ */
+ private String country;
+ /**
+ * uint32_t locale;
+ * uint8_t
+ */
+ private byte orientation;
+ /**
+ * uint8_t
+ */
+ private byte touchscreen;
+ /**
+ * uint16_t
+ */
+ private short density;
+ /**
+ * uint32_t screenType;
+ * uint8_t
+ */
+ private short keyboard;
+ /**
+ * uint8_t
+ */
+ private short navigation;
+ /**
+ * uint8_t
+ */
+ private short inputFlags;
+ /**
+ * uint8_t
+ */
+ private short inputPad0;
+ /**
+ * uint32_t input;
+ * uint16_t
+ */
+ private int screenWidth;
+ /**
+ * uint16_t
+ */
+ private int screenHeight;
+ /**
+ * uint32_t screenSize;
+ * uint16_t
+ */
+ private int sdkVersion;
+ /**
+ * For now minorVersion must always be 0!!! Its meaning is currently undefined.
+ * uint16_t
+ */
+ private int minorVersion;
+ /**
+ * uint32_t version;
+ * uint8_t
+ */
+ private short screenLayout;
+ /**
+ * uint8_t
+ */
+ private short uiMode;
+ /**
+ * uint8_t
+ */
+ private short screenConfigPad1;
+ /**
+ * uint8_t
+ */
+ private short screenConfigPad2;
+
+ /**
+ * uint32_t screenConfig;
+ */
+ public int getSize() {
+ return this.size;
+ }
+
+ public void setSize(final long size) {
+ this.size = Unsigned.ensureUInt(size);
+ }
+
+ public short getMcc() {
+ return this.mcc;
+ }
+
+ public void setMcc(final short mcc) {
+ this.mcc = mcc;
+ }
+
+ public short getMnc() {
+ return this.mnc;
+ }
+
+ public void setMnc(final short mnc) {
+ this.mnc = mnc;
+ }
+
+ public String getLanguage() {
+ return this.language;
+ }
+
+ public void setLanguage(final String language) {
+ this.language = language;
+ }
+
+ public String getCountry() {
+ return this.country;
+ }
+
+ public void setCountry(final String country) {
+ this.country = country;
+ }
+
+ public short getOrientation() {
+ return (short) (this.orientation & 0xff);
+ }
+
+ public void setOrientation(final short orientation) {
+ this.orientation = (byte) orientation;
+ }
+
+ public short getTouchscreen() {
+ return (short) (this.touchscreen & 0xff);
+ }
+
+ public void setTouchscreen(final short touchscreen) {
+ this.touchscreen = (byte) touchscreen;
+ }
+
+ public int getDensity() {
+ return this.density & 0xffff;
+ }
+
+ public void setDensity(final int density) {
+ this.density = (short) density;
+ }
+
+ public short getKeyboard() {
+ return this.keyboard;
+ }
+
+ public void setKeyboard(final short keyboard) {
+ this.keyboard = keyboard;
+ }
+
+ public short getNavigation() {
+ return this.navigation;
+ }
+
+ public void setNavigation(final short navigation) {
+ this.navigation = navigation;
+ }
+
+ public short getInputFlags() {
+ return this.inputFlags;
+ }
+
+ public void setInputFlags(final short inputFlags) {
+ this.inputFlags = inputFlags;
+ }
+
+ public short getInputPad0() {
+ return this.inputPad0;
+ }
+
+ public void setInputPad0(final short inputPad0) {
+ this.inputPad0 = inputPad0;
+ }
+
+ public int getScreenWidth() {
+ return this.screenWidth;
+ }
+
+ public void setScreenWidth(final int screenWidth) {
+ this.screenWidth = screenWidth;
+ }
+
+ public int getScreenHeight() {
+ return this.screenHeight;
+ }
+
+ public void setScreenHeight(final int screenHeight) {
+ this.screenHeight = screenHeight;
+ }
+
+ public int getSdkVersion() {
+ return this.sdkVersion;
+ }
+
+ public void setSdkVersion(final int sdkVersion) {
+ this.sdkVersion = sdkVersion;
+ }
+
+ public int getMinorVersion() {
+ return this.minorVersion;
+ }
+
+ public void setMinorVersion(final int minorVersion) {
+ this.minorVersion = minorVersion;
+ }
+
+ public short getScreenLayout() {
+ return this.screenLayout;
+ }
+
+ public void setScreenLayout(final short screenLayout) {
+ this.screenLayout = screenLayout;
+ }
+
+ public short getUiMode() {
+ return this.uiMode;
+ }
+
+ public void setUiMode(final short uiMode) {
+ this.uiMode = uiMode;
+ }
+
+ public short getScreenConfigPad1() {
+ return this.screenConfigPad1;
+ }
+
+ public void setScreenConfigPad1(final short screenConfigPad1) {
+ this.screenConfigPad1 = screenConfigPad1;
+ }
+
+ public short getScreenConfigPad2() {
+ return this.screenConfigPad2;
+ }
+
+ public void setScreenConfigPad2(final short screenConfigPad2) {
+ this.screenConfigPad2 = screenConfigPad2;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java
new file mode 100644
index 00000000..5e172f79
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java
@@ -0,0 +1,91 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+
+import java.util.Locale;
+
+/**
+ * A Resource entry specifies the key (name) of the Resource.
+ * It is immediately followed by the value of that Resource.
+ *
+ * @author dongliu
+ */
+public class ResourceEntry {
+ /**
+ * Number of bytes in this structure. uint16_t
+ */
+ public final int size;
+
+ /**
+ * If set, this is a complex entry, holding a set of name/value
+ * mappings. It is followed by an array of ResTable_map structures.
+ */
+ public static final int FLAG_COMPLEX = 0x0001;
+// /**
+// * If set, this resource has been declared public, so libraries
+// * are allowed to reference it.
+// */
+// public static final int FLAG_PUBLIC = 0x0002;
+
+ /**
+ * If set, this is a weak resource and may be overriden by strong
+ * resources of the same name/type. This is only useful during
+ * linking with other resource tables.
+ */
+ public static final int FLAG_WEAK = 0x0004;
+ /**
+ * If set, this is a compact entry with data type and value directly
+ * encoded in this entry, see ResTable_entry::compact
+ */
+ public static final int FLAG_COMPACT = 0x0008;
+ /**
+ * uint16_t
+ */
+ public final int flags;
+
+ /**
+ * Reference into ResTable_package::keyStrings identifying this entry.
+ * public long keyRef;
+ */
+ public final String key;
+
+ /**
+ * the resvalue following this resource entry.
+ */
+ @Nullable
+ public final ResourceValue value;
+
+ public ResourceEntry(final int size, final int flags, final String key, @Nullable final ResourceValue value) {
+ this.size = size;
+ this.flags = flags;
+ this.key = key;
+ this.value = value;
+ }
+
+ /**
+ * get value as string
+ */
+ @Nullable
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ final ResourceValue value = this.value;
+ if (value != null) {
+ return value.toStringValue(resourceTable, locale);
+ } else {
+ return "null";
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ResourceEntry{" +
+ "size=" + this.size +
+ ", flags=" + this.flags +
+ ", key='" + this.key + '\'' +
+ ", value=" + this.value +
+ '}';
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java
new file mode 100644
index 00000000..00caf736
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java
@@ -0,0 +1,56 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * @author dongliu.
+ */
+public class ResourceMapEntry extends ResourceEntry {
+ /**
+ * Resource identifier of the parent mapping, or 0 if there is none.
+ * ResTable_ref specifies the parent Resource, if any, of this Resource.
+ * struct ResTable_ref { uint32_t ident; };
+ */
+ public final long parent;
+
+ /**
+ * Number of name/value pairs that follow for FLAG_COMPLEX. uint32_t
+ */
+ public final long count;
+ @NonNull
+ public final ResourceTableMap[] resourceTableMaps;
+
+ public ResourceMapEntry(final int size, final int flags, final String key, final long parent, final long count, final @NonNull ResourceTableMap[] resourceTableMaps) {
+ super(size, flags, key, null);
+ this.parent = parent;
+ this.count = count;
+ this.resourceTableMaps = resourceTableMaps;
+ }
+
+ /**
+ * get value as string
+ */
+ @Nullable
+ @Override
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ if (this.resourceTableMaps.length > 0) {
+ return this.resourceTableMaps[0].toString();
+ } else {
+ return null;
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ResourceMapEntry{" +
+ "parent=" + this.parent +
+ ", count=" + this.count +
+ ", resourceTableMaps=" + Arrays.toString(this.resourceTableMaps) +
+ '}';
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java
new file mode 100644
index 00000000..a9a70e6b
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java
@@ -0,0 +1,110 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.StringPool;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Resource packge.
+ *
+ * @author dongliu
+ */
+public class ResourcePackage {
+ // the packageName
+ private String name;
+ private short id;
+ /**
+ * contains the names of the types of the Resources defined in the ResourcePackage
+ */
+ private StringPool typeStringPool;
+ /**
+ * contains the names (keys) of the Resources defined in the ResourcePackage.
+ */
+ private StringPool keyStringPool;
+
+ public ResourcePackage(final @NonNull PackageHeader header) {
+ this.name = header.getName();
+ this.id = (short) header.getId();
+ }
+
+ private Map typeSpecMap = new HashMap<>();
+
+ private Map> typesMap = new HashMap<>();
+
+ public void addTypeSpec(final @NonNull TypeSpec typeSpec) {
+ this.typeSpecMap.put(typeSpec.id, typeSpec);
+ }
+
+ @Nullable
+ public TypeSpec getTypeSpec(final short id) {
+ return this.typeSpecMap.get(id);
+ }
+
+ public void addType(final Type type) {
+ List types = this.typesMap.get(type.id);
+ if (types == null) {
+ types = new ArrayList<>();
+ this.typesMap.put(type.id, types);
+ }
+ types.add(type);
+ }
+
+ @Nullable
+ public List getTypes(final short id) {
+ return this.typesMap.get(id);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public short getId() {
+ return this.id;
+ }
+
+ public void setId(final short id) {
+ this.id = id;
+ }
+
+ public StringPool getTypeStringPool() {
+ return this.typeStringPool;
+ }
+
+ public void setTypeStringPool(final @NonNull StringPool typeStringPool) {
+ this.typeStringPool = typeStringPool;
+ }
+
+ public StringPool getKeyStringPool() {
+ return this.keyStringPool;
+ }
+
+ public void setKeyStringPool(final @NonNull StringPool keyStringPool) {
+ this.keyStringPool = keyStringPool;
+ }
+
+ public Map getTypeSpecMap() {
+ return this.typeSpecMap;
+ }
+
+ public void setTypeSpecMap(final Map typeSpecMap) {
+ this.typeSpecMap = typeSpecMap;
+ }
+
+ public Map> getTypesMap() {
+ return this.typesMap;
+ }
+
+ public void setTypesMap(final Map> typesMap) {
+ this.typesMap = typesMap;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java
new file mode 100644
index 00000000..bcceacd7
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java
@@ -0,0 +1,106 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.utils.ResourceLoader;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The apk resource table
+ *
+ * @author dongliu
+ */
+public class ResourceTable {
+ private final Map packageMap = new HashMap<>();
+ @Nullable
+ public final StringPool stringPool;
+ @NonNull
+ public static final Map sysStyle = ResourceLoader.loadSystemStyles();
+
+ public ResourceTable(@Nullable final StringPool stringPool) {
+ this.stringPool = stringPool;
+ }
+
+ public void addPackage(final @NonNull ResourcePackage resourcePackage) {
+ this.packageMap.put(resourcePackage.getId(), resourcePackage);
+ }
+
+ @Nullable
+ public ResourcePackage getPackage(final short id) {
+ return this.packageMap.get(id);
+ }
+
+ /**
+ * Get resources match the given resource id.
+ */
+ @NonNull
+ public List getResourcesById(final long resourceId) {
+ // An Android Resource id is a 32-bit integer. It comprises
+ // an 8-bit Package id [bits 24-31]
+ // an 8-bit Type id [bits 16-23]
+ // a 16-bit Entry index [bits 0-15]
+ final short packageId = (short) (resourceId >> 24 & 0xff);
+ final short typeId = (short) ((resourceId >> 16) & 0xff);
+ final int entryIndex = (int) (resourceId & 0xffff);
+ final ResourcePackage resourcePackage = this.getPackage(packageId);
+ if (resourcePackage == null) {
+ return Collections.emptyList();
+ }
+ final TypeSpec typeSpec = resourcePackage.getTypeSpec(typeId);
+ final List types = resourcePackage.getTypes(typeId);
+ if (typeSpec == null || types == null) {
+ return Collections.emptyList();
+ }
+ if (!typeSpec.exists(entryIndex)) {
+ return Collections.emptyList();
+ }
+ // read from type resource
+ final List result = new ArrayList<>();
+ for (final Type type : types) {
+ final ResourceEntry resourceEntry = type.getResourceEntry(entryIndex);
+ if (resourceEntry == null) {
+ continue;
+ }
+ final ResourceValue currentResourceValue = resourceEntry.value;
+ if (currentResourceValue == null) {
+ continue;
+ }
+ // cyclic reference detect
+ if (currentResourceValue instanceof ResourceValue.ReferenceResourceValue) {
+ if (resourceId == ((ResourceValue.ReferenceResourceValue) currentResourceValue)
+ .getReferenceResourceId()) {
+ continue;
+ }
+ }
+ result.add(new Resource(typeSpec, type, resourceEntry));
+ }
+ return result;
+ }
+
+ /**
+ * contains all info for one resource
+ */
+ public static class Resource {
+ @Nullable
+ public final TypeSpec typeSpec;
+ @NonNull
+ public final Type type;
+ @NonNull
+ public final ResourceEntry resourceEntry;
+
+ public Resource(final @Nullable TypeSpec typeSpec, final @NonNull Type type, final @NonNull ResourceEntry resourceEntry) {
+ this.typeSpec = typeSpec;
+ this.type = type;
+ this.resourceEntry = resourceEntry;
+ }
+
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java
new file mode 100644
index 00000000..3519b513
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java
@@ -0,0 +1,32 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+import java.nio.ByteBuffer;
+
+/**
+ * resource file header
+ *
+ * @author dongliu
+ */
+public class ResourceTableHeader extends ChunkHeader {
+ /**
+ * The number of ResTable_package structures. uint32
+ */
+ private final int packageCount;
+
+ public ResourceTableHeader(final int headerSize, final int chunkSize, final @NonNull ByteBuffer buffer) {
+ super(ChunkType.TABLE, headerSize, chunkSize);
+ this.packageCount = Unsigned.toUInt(Buffers.readUInt(buffer));
+ }
+
+ public long getPackageCount() {
+ return Unsigned.toLong(this.packageCount);
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java
new file mode 100644
index 00000000..a6da3e53
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java
@@ -0,0 +1,149 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+
+/**
+ * @author dongliu
+ */
+public class ResourceTableMap {
+ /**
+ * ...elided
+ * ResTable_ref; unit32
+ */
+ private long nameRef;
+
+ private ResourceValue resValue;
+ private String data;
+
+ public long getNameRef() {
+ return this.nameRef;
+ }
+
+ public void setNameRef(final long nameRef) {
+ this.nameRef = nameRef;
+ }
+
+ public ResourceValue getResValue() {
+ return this.resValue;
+ }
+
+ public void setResValue(final @Nullable ResourceValue resValue) {
+ this.resValue = resValue;
+ }
+
+ public String getData() {
+ return this.data;
+ }
+
+ public void setData(final String data) {
+ this.data = data;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return this.data;
+ }
+
+ public static class MapAttr {
+ @SuppressWarnings("PointlessBitwiseExpression")
+ public static final int TYPE = 0x01000000 | (0 & 0xFFFF);
+
+ /**
+ * For integral attributes; this is the minimum value it can hold.
+ */
+ public static final int MIN = 0x01000000 | (1 & 0xFFFF);
+
+ /**
+ * For integral attributes; this is the maximum value it can hold.
+ */
+ public static final int MAX = 0x01000000 | (2 & 0xFFFF);
+
+ /**
+ * Localization of this resource is can be encouraged or required with
+ * an aapt flag if this is set
+ */
+ public static final int L10N = 0x01000000 | (3 & 0xFFFF);
+
+ /**
+ * for plural support; see android.content.res.PluralRules#attrForQuantity(int)
+ */
+ public static final int OTHER = 0x01000000 | (4 & 0xFFFF);
+ public static final int ZERO = 0x01000000 | (5 & 0xFFFF);
+ public static final int ONE = 0x01000000 | (6 & 0xFFFF);
+ public static final int TWO = 0x01000000 | (7 & 0xFFFF);
+ public static final int FEW = 0x01000000 | (8 & 0xFFFF);
+ public static final int MANY = 0x01000000 | (9 & 0xFFFF);
+
+ public static int makeArray(final int entry) {
+ return (0x02000000 | (entry & 0xFFFF));
+ }
+
+ }
+
+ public static class AttributeType {
+ /**
+ * No type has been defined for this attribute; use generic
+ * type handling. The low 16 bits are for types that can be
+ * handled generically; the upper 16 require additional information
+ * in the bag so can not be handled generically for ANY.
+ */
+ public static final int ANY = 0x0000FFFF;
+
+ /**
+ * Attribute holds a references to another resource.
+ */
+ public static final int REFERENCE = 1;
+
+ /**
+ * Attribute holds a generic string.
+ */
+ public static final int STRING = 1 << 1;
+
+ /**
+ * Attribute holds an integer value. ATTR_MIN and ATTR_MIN can
+ * optionally specify a constrained range of possible integer values.
+ */
+ public static final int INTEGER = 1 << 2;
+
+ /**
+ * Attribute holds a boolean integer.
+ */
+ public static final int BOOLEAN = 1 << 3;
+
+ /**
+ * Attribute holds a color value.
+ */
+ public static final int COLOR = 1 << 4;
+
+ /**
+ * Attribute holds a floating point value.
+ */
+ public static final int FLOAT = 1 << 5;
+
+ /**
+ * Attribute holds a dimension value; such as "20px".
+ */
+ public static final int DIMENSION = 1 << 6;
+
+ /**
+ * Attribute holds a fraction value; such as "20%".
+ */
+ public static final int FRACTION = 1 << 7;
+
+ /**
+ * Attribute holds an enumeration. The enumeration values are
+ * supplied as additional entries in the map.
+ */
+ public static final int ENUM = 1 << 16;
+
+ /**
+ * Attribute holds a bitmaks of flags. The flag bit values are
+ * supplied as additional entries in the map.
+ */
+ public static final int FLAGS = 1 << 17;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java
new file mode 100644
index 00000000..fe2cb151
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java
@@ -0,0 +1,150 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.ParseUtils;
+
+import java.nio.ByteBuffer;
+import java.util.Locale;
+
+/**
+ * @author dongliu
+ */
+public class Type {
+ private String name;
+ public final short id;
+
+ @NonNull
+ public final Locale locale;
+
+ private StringPool keyStringPool;
+ private ByteBuffer buffer;
+ private long[] offsets;
+ private StringPool stringPool;
+
+ /**
+ * see Densities.java for values
+ */
+ public final int density;
+
+ public Type(final @NonNull TypeHeader header) {
+ this.id = header.getId();
+ final ResTableConfig config = header.config;
+ this.locale = new Locale(config.getLanguage(), config.getCountry());
+ this.density = config.getDensity();
+ }
+
+ @Nullable
+ public ResourceEntry getResourceEntry(final int resId) {
+ if (resId >= this.offsets.length) {
+ return null;
+ }
+ if (this.offsets[resId] == TypeHeader.NO_ENTRY) {
+ return null;
+ }
+ if( offsets[resId] >= buffer.limit() ) {
+ //System.out.println( "invalid offset: " + offsets[resId] );
+ return null;
+ }
+ // read Resource Entries
+ Buffers.position(this.buffer, this.offsets[resId]);
+ return this.readResourceEntry();
+ }
+
+ private ResourceEntry readResourceEntry() {
+ long beginPos = buffer.position();
+// ResourceEntry resourceEntry = new ResourceEntry();
+ // size is always 8(simple), or 16(complex)
+ final int size = Buffers.readUShort(buffer);
+ final int flags = Buffers.readUShort(buffer);
+ long keyRef = buffer.getInt();
+
+ if ((flags & ResourceEntry.FLAG_COMPLEX) != 0) {
+ String key = keyStringPool.get((int) keyRef);
+
+ // Resource identifier of the parent mapping, or 0 if there is none.
+ final long parent = Buffers.readUInt(buffer);
+ final long count = Buffers.readUInt(buffer);
+
+ Buffers.position(buffer, beginPos + size);
+
+ //An individual complex Resource entry comprises an entry immediately followed by one or more fields.
+ ResourceTableMap[] resourceTableMaps = new ResourceTableMap[(int) count];
+ for (int i = 0; i < count; i++) {
+ resourceTableMaps[i] = readResourceTableMap();
+ }
+// ResourceEntry resourceEntry = new ResourceEntry(size,flags,key,resourceTableMaps);
+ ResourceMapEntry resourceMapEntry = new ResourceMapEntry(size,flags,key,parent,count,resourceTableMaps);
+ return resourceMapEntry;
+ } else if ((flags & ResourceEntry.FLAG_COMPACT) != 0) {
+ final ResourceValue value = ResourceValue.string((int) keyRef, stringPool);
+ return new ResourceEntry(size,flags,null,value);
+ } else {
+ String key = keyStringPool.get((int) keyRef);
+ Buffers.position(buffer, beginPos + size);
+ final ResourceValue value = ParseUtils.readResValue(buffer, stringPool);
+ return new ResourceEntry(size,flags,key,value);
+ }
+ }
+
+ private ResourceTableMap readResourceTableMap() {
+ final ResourceTableMap resourceTableMap = new ResourceTableMap();
+ resourceTableMap.setNameRef(Buffers.readUInt(this.buffer));
+ resourceTableMap.setResValue(ParseUtils.readResValue(this.buffer, this.stringPool));
+ //noinspection StatementWithEmptyBody
+ if ((resourceTableMap.getNameRef() & 0x02000000) != 0) {
+ //read arrays
+ } else //noinspection StatementWithEmptyBody
+ if ((resourceTableMap.getNameRef() & 0x01000000) != 0) {
+ // read attrs
+ } else {
+ }
+ return resourceTableMap;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public StringPool getKeyStringPool() {
+ return this.keyStringPool;
+ }
+
+ public void setKeyStringPool(final StringPool keyStringPool) {
+ this.keyStringPool = keyStringPool;
+ }
+
+ public ByteBuffer getBuffer() {
+ return this.buffer;
+ }
+
+ public void setBuffer(final ByteBuffer buffer) {
+ this.buffer = buffer;
+ }
+
+ public void setOffsets(final long[] offsets) {
+ this.offsets = offsets;
+ }
+
+ public void setStringPool(final StringPool stringPool) {
+ this.stringPool = stringPool;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "Type{" +
+ "name='" + this.name + '\'' +
+ ", id=" + this.id +
+ ", locale=" + this.locale +
+ '}';
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java
new file mode 100644
index 00000000..e444e57b
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java
@@ -0,0 +1,93 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author dongliu
+ */
+public class TypeHeader extends ChunkHeader {
+
+ public static final long NO_ENTRY = 0xFFFFFFFFL;
+
+ /**
+ * The type identifier this chunk is holding. Type IDs start at 1 (corresponding to the value
+ * of the type bits in a resource identifier). 0 is invalid.
+ * uint8_t
+ */
+ private final byte id;
+
+ /**
+ * Must be 0. uint8_t
+ */
+ private final byte flags;
+ /**
+ * Must be 0. uint16_t
+ */
+ private final short res;
+
+ /**
+ * Number of uint32_t entry indices that follow. uint32
+ */
+ public final int entryCount;
+
+ /**
+ * Offset from header where ResTable_entry data starts.uint32_t
+ */
+ public final int entriesStart;
+
+ /**
+ * Configuration this collection of entries is designed for.
+ */
+ @NonNull
+ public final ResTableConfig config;
+
+ public TypeHeader(final int headerSize, final long chunkSize, @NonNull final ByteBuffer buffer) {
+ super(ChunkType.TABLE_TYPE, headerSize, chunkSize);
+ this.id = Unsigned.toUByte(Buffers.readUByte(buffer));
+ this.flags = Unsigned.toUByte(Buffers.readUByte(buffer));
+ this.res = Unsigned.toUShort(Buffers.readUShort(buffer));
+ this.entryCount = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ this.entriesStart = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ this.config = this.readResTableConfig(buffer);
+ }
+
+ public short getId() {
+ return Unsigned.toShort(this.id);
+ }
+
+ public short getFlags() {
+ return Unsigned.toUShort(this.flags);
+ }
+
+ public int getRes() {
+ return Unsigned.toInt(this.res);
+ }
+
+ @NonNull
+ private ResTableConfig readResTableConfig(final ByteBuffer buffer) {
+ final long beginPos = buffer.position();
+ final ResTableConfig config = new ResTableConfig();
+ final long size = Buffers.readUInt(buffer);
+ // imsi
+ config.setMcc(buffer.getShort());
+ config.setMnc(buffer.getShort());
+ //read locale
+ config.setLanguage(new String(Buffers.readBytes(buffer, 2)).replace("\0", ""));
+ config.setCountry(new String(Buffers.readBytes(buffer, 2)).replace("\0", ""));
+ //screen type
+ config.setOrientation(Buffers.readUByte(buffer));
+ config.setTouchscreen(Buffers.readUByte(buffer));
+ config.setDensity(Buffers.readUShort(buffer));
+ // now just skip the others...
+ final long endPos = buffer.position();
+ Buffers.skip(buffer, (int) (size - (endPos - beginPos)));
+ return config;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java
new file mode 100644
index 00000000..5facc8b7
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java
@@ -0,0 +1,32 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+
+/**
+ * @author dongliu
+ */
+public class TypeSpec {
+
+ public final long[] entryFlags;
+ public final String name;
+ public final short id;
+
+ public TypeSpec(final @NonNull TypeSpecHeader header, @NonNull final long[] entryFlags, final String name) {
+ this.id = header.getId();
+ this.entryFlags = entryFlags;
+ this.name = name;
+ }
+
+ public boolean exists(final int id) {
+ return id < this.entryFlags.length;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "TypeSpec{" +
+ "name='" + this.name + '\'' +
+ ", id=" + this.id +
+ '}';
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java
new file mode 100644
index 00000000..c5e37ab4
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java
@@ -0,0 +1,65 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author dongliu
+ */
+public class TypeSpecHeader extends ChunkHeader {
+
+ /**
+ * The type identifier this chunk is holding. Type IDs start at 1 (corresponding to the value
+ * of the type bits in a resource identifier). 0 is invalid.
+ * The id also specifies the name of the Resource type. It is the string at index id - 1 in the
+ * typeStrings StringPool chunk in the containing Package chunk.
+ * uint8_t
+ */
+ private final byte id;
+
+ /**
+ * Must be 0. uint8_t
+ */
+ private final byte res0;
+
+ /**
+ * Must be 0.uint16_t
+ */
+ private final short res1;
+
+ /**
+ * Number of uint32_t entry configuration masks that follow.
+ */
+ private final int entryCount;
+
+ public TypeSpecHeader(final int headerSize, final long chunkSize, final @NonNull ByteBuffer buffer) {
+ super(ChunkType.TABLE_TYPE_SPEC, headerSize, chunkSize);
+ this.id = Unsigned.toUByte(Buffers.readUByte(buffer));
+ this.res0 = Unsigned.toUByte(Buffers.readUByte(buffer));
+ this.res1 = Unsigned.toUShort(Buffers.readUShort(buffer));
+ this.entryCount = Unsigned.ensureUInt(Buffers.readUInt(buffer));
+ }
+
+ public short getId() {
+ return Unsigned.toShort(this.id);
+ }
+
+ public short getRes0() {
+ return Unsigned.toShort(this.res0);
+ }
+
+ public int getRes1() {
+ return Unsigned.toInt(this.res1);
+ }
+
+ public int getEntryCount() {
+ return this.entryCount;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java
new file mode 100644
index 00000000..ae232c9c
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java
@@ -0,0 +1,23 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * For read apk signing block
+ *
+ * @see apksigning v2 scheme
+ */
+public class ApkSigningBlock {
+ public static final int SIGNING_V2_ID = 0x7109871a;
+
+ public static final String MAGIC = "APK Sig Block 42";
+ @NonNull
+ public final List signerBlocks;
+
+ public ApkSigningBlock(final @NonNull List signerBlocks) {
+ this.signerBlocks = signerBlocks;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java
new file mode 100644
index 00000000..b518d3b2
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java
@@ -0,0 +1,13 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+import androidx.annotation.NonNull;
+
+public class Digest {
+ public final int algorithmID;
+ public final byte[] value;
+
+ public Digest(final int algorithmID, final @NonNull byte[] value) {
+ this.algorithmID = algorithmID;
+ this.value = value;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java
new file mode 100644
index 00000000..b6a97421
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java
@@ -0,0 +1,14 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+import androidx.annotation.NonNull;
+
+public class Signature {
+ public final int algorithmID;
+ public final byte[] data;
+
+ public Signature(final int algorithmID, final @NonNull byte[] data) {
+ this.algorithmID = algorithmID;
+ this.data = data;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java
new file mode 100644
index 00000000..3457acad
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java
@@ -0,0 +1,19 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+import androidx.annotation.NonNull;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+public class SignerBlock {
+ public final List digests;
+ public final List certificates;
+ public final List signatures;
+
+ public SignerBlock(final @NonNull List digests, final @NonNull List certificates, final @NonNull List signatures) {
+ this.digests = digests;
+ this.certificates = certificates;
+ this.signatures = signatures;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt
new file mode 100644
index 00000000..ccf44543
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.kt
@@ -0,0 +1,65 @@
+package net.dongliu.apk.parser.struct.xml
+
+import net.dongliu.apk.parser.struct.ResourceValue
+import net.dongliu.apk.parser.struct.resource.ResourceTable
+import net.dongliu.apk.parser.utils.ResourceLoader
+import java.util.Locale
+
+/**
+ * xml node attribute
+ *
+ * @author dongliu
+ */
+class Attribute(
+ @JvmField val namespace: String, @JvmField val name: String,
+ /**
+ * The original raw string value of Attribute
+ */
+ @JvmField val rawValue: String?,
+ /**
+ * Processed typed value of Attribute
+ */
+ @JvmField val typedValue: ResourceValue?
+) {
+ /**
+ * the final value as string
+ */
+ @JvmField
+ var value: String? = null
+
+ fun toStringValue(resourceTable: ResourceTable, locale: Locale): String? {
+ val rawValue = rawValue
+ return if (rawValue != null) {
+ rawValue
+ } else {
+ val typedValue = typedValue
+ if (typedValue != null) {
+ typedValue.toStringValue(resourceTable, locale)
+ } else {
+ // something happen;
+ ""
+ }
+ }
+ }
+
+ override fun toString(): String {
+ return "Attribute{" +
+ "name='" + name + '\'' +
+ ", namespace='" + namespace + '\'' +
+ '}'
+ }
+
+ /**
+ * These are attribute resource constants for the platform; as found in android.R.attr
+ *
+ * @author dongliu
+ */
+ companion object {
+ private val ids = ResourceLoader.loadSystemAttrIds()
+
+ @JvmStatic
+ fun getString(id: Long): String {
+ return ids[id.toInt()] ?: "AttrId:0x${java.lang.Long.toHexString(id)}"
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt
new file mode 100644
index 00000000..ebf85bb5
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.kt
@@ -0,0 +1,70 @@
+package net.dongliu.apk.parser.struct.xml
+
+/**
+ * xml node attributes
+ *
+ * @author dongliu
+ */
+class Attributes(size: Int) {
+ /**
+ * return all attributes
+ */
+ @JvmField
+ val attributes: Array
+
+ init {
+ attributes = arrayOfNulls(size)
+ }
+
+ operator fun set(i: Int, attribute: Attribute) {
+ attributes[i] = attribute
+ }
+
+ operator fun get(name: String): Attribute? {
+ //TODO this is an inefficient search. Should probably be using HashMap
+ var result: Attribute? = null
+ for (attribute in attributes) {
+ if (attribute!!.name == name) {
+ val namespace = attribute.namespace
+ if (namespace.isEmpty() || namespace == "android" || namespace == "http://schemas.android.com/apk/res/android") {
+ //prefer default namespace of android.
+ result = attribute
+ break
+ }
+ if (result == null)
+ result = attribute
+ }
+ }
+ return result
+ }
+
+ /**
+ * Get attribute with name, return value as string
+ */
+ fun getString(name: String): String? {
+ return this[name]?.value
+ }
+
+ fun size(): Int {
+ return attributes.size
+ }
+
+ fun getBoolean(name: String, b: Boolean): Boolean {
+ val value = getString(name)
+ return if (value == null) b else java.lang.Boolean.parseBoolean(value)
+ }
+
+ fun getInt(name: String): Int? {
+ val value = getString(name) ?: return null
+ return if (value.startsWith("0x")) {
+ Integer.valueOf(value.substring(2), 16)
+ } else Integer.valueOf(value)
+ }
+
+ fun getLong(name: String): Long? {
+ val value = getString(name) ?: return null
+ return if (value.startsWith("0x")) {
+ java.lang.Long.valueOf(value.substring(2), 16)
+ } else java.lang.Long.valueOf(value)
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt
new file mode 100644
index 00000000..020c3440
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.kt
@@ -0,0 +1,11 @@
+package net.dongliu.apk.parser.struct.xml
+
+import net.dongliu.apk.parser.struct.ChunkHeader
+
+/**
+ * Null header.
+ *
+ * @author dongliu
+ */
+class NullHeader(chunkType: Int, headerSize: Int, chunkSize: Long) :
+ ChunkHeader(chunkType, headerSize, chunkSize)
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java
new file mode 100644
index 00000000..8f4cacab
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java
@@ -0,0 +1,78 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.resource.ResourceTable;
+
+import java.util.Locale;
+
+/**
+ * @author dongliu
+ */
+public class XmlCData {
+
+ public static final String CDATA_START = "";
+
+ /**
+ * The raw CDATA character data.
+ */
+ private String data;
+
+ /**
+ * The typed value of the character data if this is a CDATA node.
+ */
+ private ResourceValue typedData;
+
+ /**
+ * the final value as string
+ */
+ private String value;
+
+ /**
+ * get value as string
+ */
+ @NonNull
+ public String toStringValue(final ResourceTable resourceTable, final Locale locale) {
+ if (this.data != null) {
+ return XmlCData.CDATA_START + this.data + XmlCData.CDATA_END;
+ } else {
+ return XmlCData.CDATA_START + this.typedData.toStringValue(resourceTable, locale) + XmlCData.CDATA_END;
+ }
+ }
+
+ public String getData() {
+ return this.data;
+ }
+
+ public void setData(final String data) {
+ this.data = data;
+ }
+
+ public ResourceValue getTypedData() {
+ return this.typedData;
+ }
+
+ public void setTypedData(final @Nullable ResourceValue typedData) {
+ this.typedData = typedData;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "XmlCData{" +
+ "data='" + this.data + '\'' +
+ ", typedData=" + this.typedData +
+ '}';
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java
new file mode 100644
index 00000000..8d8b32e0
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java
@@ -0,0 +1,15 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+
+/**
+ * Binary XML header. It is simply a struct ResChunk_header.
+ * The header.type is always 0×0003 (XML).
+ *
+ * @author dongliu
+ */
+public class XmlHeader extends ChunkHeader {
+ public XmlHeader(final int chunkType, final int headerSize, final long chunkSize) {
+ super(chunkType, headerSize, chunkSize);
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java
new file mode 100644
index 00000000..eb57fc94
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java
@@ -0,0 +1,25 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * @author dongliu
+ */
+public class XmlNamespaceEndTag {
+ @Nullable
+ public final String prefix;
+ @Nullable
+ public final String uri;
+
+ public XmlNamespaceEndTag(final @Nullable String prefix, final @Nullable String uri) {
+ this.prefix = prefix;
+ this.uri = uri;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return this.prefix + "=" + this.uri;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java
new file mode 100644
index 00000000..43b21f4f
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java
@@ -0,0 +1,26 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * @author dongliu
+ */
+public class XmlNamespaceStartTag {
+ @Nullable
+ public final String prefix;
+ @Nullable
+ public final String uri;
+
+ public XmlNamespaceStartTag(final @Nullable String prefix, final @Nullable String uri) {
+ this.prefix = prefix;
+ this.uri = uri;
+ }
+
+
+ @NonNull
+ @Override
+ public String toString() {
+ return this.prefix + "=" + this.uri;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java
new file mode 100644
index 00000000..60beafec
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java
@@ -0,0 +1,39 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import androidx.annotation.NonNull;
+
+/**
+ * @author dongliu
+ */
+public class XmlNodeEndTag {
+ private String namespace;
+ private String name;
+
+ public String getNamespace() {
+ return this.namespace;
+ }
+
+ public void setNamespace(final String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("");
+ if (this.namespace != null) {
+ sb.append(this.namespace).append(":");
+ }
+ sb.append(this.name).append('>');
+ return sb.toString();
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java
new file mode 100644
index 00000000..f0b67c1f
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java
@@ -0,0 +1,29 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import androidx.annotation.NonNull;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.utils.Buffers;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author dongliu
+ */
+public class XmlNodeHeader extends ChunkHeader {
+ /**
+ * Line number in original source file at which this element appeared.
+ */
+ public final int lineNum;
+ /**
+ * Optional XML comment string pool ref, -1 if none
+ */
+ public final int commentRef;
+
+ public XmlNodeHeader(final int chunkType, final int headerSize, final long chunkSize, final @NonNull ByteBuffer buffer) {
+ super(chunkType, headerSize, chunkSize);
+ this.lineNum = (int) Buffers.readUInt(buffer);
+ this.commentRef = (int) Buffers.readUInt(buffer);
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java
new file mode 100644
index 00000000..3ee71c55
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java
@@ -0,0 +1,57 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * @author dongliu
+ */
+public class XmlNodeStartTag {
+ @Nullable
+ public final String namespace;
+ @Nullable
+ public final String name;
+
+ // /**
+// * Byte offset from the start of this structure where the attributes start. uint16
+// */
+// public int attributeStart;
+// /**
+// * Size of the ResXMLTree_attribute structures that follow. unit16
+// */
+// public int attributeSize;
+// /**
+// * Number of attributes associated with an ELEMENT. uint 16
+// * These are available as an array of ResXMLTree_attribute structures immediately following this node.
+// */
+// public int attributeCount;
+// /**
+// * Index (1-based) of the "id" attribute. 0 if none. uint16
+// */
+// public short idIndex;
+// /**
+// * Index (1-based) of the "style" attribute. 0 if none. uint16
+// */
+// public short styleIndex;
+ @NonNull
+ public final Attributes attributes;
+
+ public XmlNodeStartTag(@Nullable final String namespace, final @Nullable String name, final @NonNull Attributes attributes) {
+ this.namespace = namespace;
+ this.name = name;
+ this.attributes = attributes;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append('<');
+ if (this.namespace != null) {
+ sb.append(this.namespace).append(":");
+ }
+ sb.append(this.name);
+ sb.append('>');
+ return sb.toString();
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt
new file mode 100644
index 00000000..870b84d9
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.kt
@@ -0,0 +1,9 @@
+package net.dongliu.apk.parser.struct.xml
+
+import net.dongliu.apk.parser.struct.ChunkHeader
+
+/**
+ * @author dongliu
+ */
+class XmlResourceMapHeader(chunkType: Int, headerSize: Int, chunkSize: Long) :
+ ChunkHeader(chunkType, headerSize, chunkSize)
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java
new file mode 100644
index 00000000..8bb6c097
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java
@@ -0,0 +1,94 @@
+package net.dongliu.apk.parser.struct.zip;
+
+/**
+ * End of central directory record
+ */
+public class EOCD {
+ public static final int SIGNATURE = 0x06054b50;
+ /**
+ * private int signature;
+ * Number of this disk
+ */
+ private short diskNum;
+ /**
+ * Disk where central directory starts
+ */
+ private short cdStartDisk;
+ /**
+ * Number of central directory records on this disk
+ */
+ private short cdRecordNum;
+ /**
+ * Total number of central directory records
+ */
+ private short totalCDRecordNum;
+ /**
+ * Size of central directory (bytes)
+ */
+ private int cdSize;
+ /**
+ * Offset of start of central directory, relative to start of archive
+ */
+ private int cdStart;
+ /**
+ * Comment length (n)
+ */
+ private short commentLen;
+
+ public short getDiskNum() {
+ return this.diskNum;
+ }
+
+ public void setDiskNum(final int diskNum) {
+ this.diskNum = (short) diskNum;
+ }
+
+ public int getCdStartDisk() {
+ return this.cdStartDisk & 0xffff;
+ }
+
+ public void setCdStartDisk(final int cdStartDisk) {
+ this.cdStartDisk = (short) cdStartDisk;
+ }
+
+ public int getCdRecordNum() {
+ return this.cdRecordNum & 0xffff;
+ }
+
+ public void setCdRecordNum(final int cdRecordNum) {
+ this.cdRecordNum = (short) cdRecordNum;
+ }
+
+ public int getTotalCDRecordNum() {
+ return this.totalCDRecordNum & 0xffff;
+ }
+
+ public void setTotalCDRecordNum(final int totalCDRecordNum) {
+ this.totalCDRecordNum = (short) totalCDRecordNum;
+ }
+
+ public long getCdSize() {
+ return this.cdSize & 0xffffffffL;
+ }
+
+ public void setCdSize(final long cdSize) {
+ this.cdSize = (int) cdSize;
+ }
+
+ public long getCdStart() {
+ return this.cdStart & 0xffffffffL;
+ }
+
+ public void setCdStart(final long cdStart) {
+ this.cdStart = (int) cdStart;
+ }
+
+ public int getCommentLen() {
+ return this.commentLen & 0xffff;
+ }
+
+ public void setCommentLen(final int commentLen) {
+ this.commentLen = (short) commentLen;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java
new file mode 100644
index 00000000..56b66b74
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Buffers.java
@@ -0,0 +1,133 @@
+package net.dongliu.apk.parser.utils;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * utils method for byte buffer
+ *
+ * Cast java.nio.ByteBuffer instances where necessary to java.nio.Buffer to avoid NoSuchMethodError
+ * when running on Java 6 to Java 8.
+ * The Java 9 ByteBuffer classes introduces overloaded methods with covariant return types the following methods:
+ * position, limit, flip, clear, mark, reset, rewind, etc.
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class Buffers {
+
+ /**
+ * get one unsigned byte as short type
+ */
+ public static short readUByte(final @NonNull ByteBuffer buffer) {
+ final byte b = buffer.get();
+ return (short) (b & 0xff);
+ }
+
+ /**
+ * get one unsigned short as int type
+ */
+ public static int readUShort(final @NonNull ByteBuffer buffer) {
+ final short s = buffer.getShort();
+ return s & 0xffff;
+ }
+
+ /**
+ * get one unsigned int as long type
+ */
+ public static long readUInt(final @NonNull ByteBuffer buffer) {
+ final int i = buffer.getInt();
+ return i & 0xffffffffL;
+ }
+
+ /**
+ * get bytes
+ */
+ @NonNull
+ public static byte[] readBytes(final @NonNull ByteBuffer buffer, final int size) {
+ final byte[] bytes = new byte[size];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ /**
+ * get all bytes remains
+ */
+ @NonNull
+ public static byte[] readBytes(final @NonNull ByteBuffer buffer) {
+ return Buffers.readBytes(buffer, buffer.remaining());
+ }
+
+ /**
+ * Read ascii string ,by len
+ */
+ @NonNull
+ public static String readAsciiString(final @NonNull ByteBuffer buffer, final int strLen) {
+ final byte[] bytes = new byte[strLen];
+ buffer.get(bytes);
+ return new String(bytes);
+ }
+
+ /**
+ * read utf16 strings, use strLen, not ending 0 char.
+ */
+ @NonNull
+ public static String readString(final @NonNull ByteBuffer buffer, final int strLen) {
+ final StringBuilder sb = new StringBuilder(strLen);
+ for (int i = 0; i < strLen; i++) {
+ sb.append(buffer.getChar());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * read utf16 strings, ending with 0 char.
+ */
+ @NonNull
+ public static String readZeroTerminatedString(final @NonNull ByteBuffer buffer, final int strLen) {
+ final StringBuilder sb = new StringBuilder(strLen);
+ for (int i = 0; i < strLen; i++) {
+ final char c = buffer.getChar();
+ if (c == '\0') {
+ Buffers.skip(buffer, (strLen - i - 1) * 2);
+ break;
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * skip count bytes
+ */
+ public static void skip(final @NonNull ByteBuffer buffer, final int count) {
+ Buffers.position(buffer, buffer.position() + count);
+ }
+
+ /**
+ * set position
+ */
+ public static void position(final @NonNull ByteBuffer buffer, final int position) {
+ buffer.position(position);
+ }
+
+ /**
+ * set position
+ */
+ public static void position(final @NonNull ByteBuffer buffer, final long position) {
+ Buffers.position(buffer, Unsigned.ensureUInt(position));
+ }
+
+ /**
+ * Return one new ByteBuffer from current position, with size, the byte order of new buffer will be set to little endian;
+ * And advance the original buffer with size.
+ */
+ @NonNull
+ public static ByteBuffer sliceAndSkip(final @NonNull ByteBuffer buffer, final int size) {
+ final ByteBuffer buf = buffer.slice().order(ByteOrder.LITTLE_ENDIAN);
+ final ByteBuffer slice = (ByteBuffer) buf.limit(buf.position() + size);
+ Buffers.skip(buffer, size);
+ return slice;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt
new file mode 100644
index 00000000..5705791e
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Inputs.kt
@@ -0,0 +1,24 @@
+package net.dongliu.apk.parser.utils
+
+import java.io.*
+
+object Inputs {
+ @JvmStatic
+ @Throws(IOException::class)
+ fun readAll(inputStream: InputStream): ByteArray {
+ val buf = ByteArray(1024 * 8)
+ ByteArrayOutputStream().use { bos ->
+ var len: Int
+ while (inputStream.read(buf).also { len = it } != -1) {
+ bos.write(buf, 0, len)
+ }
+ return bos.toByteArray()
+ }
+ }
+
+ @JvmStatic
+ @Throws(IOException::class)
+ fun readAllAndClose(inputStream: InputStream): ByteArray {
+ inputStream.use { return readAll(inputStream) }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt
new file mode 100644
index 00000000..8f833d1c
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Locales.kt
@@ -0,0 +1,49 @@
+package net.dongliu.apk.parser.utils
+
+import java.util.Locale
+
+/**
+ * @author dongliu
+ */
+object Locales {
+ /**
+ * when do localize, any locale will match this
+ */
+ @JvmField
+ val any = Locale("", "")
+
+ /**
+ * How much the given locale match the expected locale.
+ */
+ @JvmStatic
+ fun match(locale: Locale?, targetLocale: Locale): Int {
+ if (locale == null) {
+ return -1
+ }
+ return when {
+ locale.language == targetLocale.language -> {
+ when {
+ locale.country == targetLocale.country -> {
+ 3
+ }
+
+ targetLocale.country.isEmpty() -> {
+ 2
+ }
+
+ else -> {
+ 0
+ }
+ }
+ }
+
+ targetLocale.country.isEmpty() || targetLocale.language.isEmpty() -> {
+ 1
+ }
+
+ else -> {
+ 0
+ }
+ }
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java
new file mode 100644
index 00000000..058f215d
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Pair.java
@@ -0,0 +1,33 @@
+package net.dongliu.apk.parser.utils;
+
+/**
+ * @author Liu Dong {@literal }
+ */
+public class Pair {
+ private K left;
+ private V right;
+
+ public Pair() {
+ }
+
+ public Pair(final K left, final V right) {
+ this.left = left;
+ this.right = right;
+ }
+
+ public K getLeft() {
+ return this.left;
+ }
+
+ public void setLeft(final K left) {
+ this.left = left;
+ }
+
+ public V getRight() {
+ return this.right;
+ }
+
+ public void setRight(final V right) {
+ this.right = right;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java
new file mode 100644
index 00000000..c354e344
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java
@@ -0,0 +1,195 @@
+package net.dongliu.apk.parser.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.dongliu.apk.parser.exception.ParserException;
+import net.dongliu.apk.parser.parser.StringPoolEntry;
+import net.dongliu.apk.parser.struct.ResValue;
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.struct.StringPoolHeader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author dongliu
+ */
+public class ParseUtils {
+
+ public static final Charset charsetUTF8 = StandardCharsets.UTF_8;
+
+ /**
+ * read string from input buffer. if get EOF before read enough data, throw IOException.
+ */
+ @NonNull
+ public static String readString(@NonNull final ByteBuffer buffer, final boolean utf8) {
+ if (utf8) {
+ // The lengths are encoded in the same way as for the 16-bit format
+ // but using 8-bit rather than 16-bit integers.
+ final int strLen = ParseUtils.readLen(buffer);
+ final int bytesLen = ParseUtils.readLen(buffer);
+ final byte[] bytes = Buffers.readBytes(buffer, bytesLen);
+ final String str = new String(bytes, ParseUtils.charsetUTF8);
+ // zero
+ final int trailling = Buffers.readUByte(buffer);
+ return str;
+ } else {
+ // The length is encoded as either one or two 16-bit integers as per the commentRef...
+ final int strLen = ParseUtils.readLen16(buffer);
+ final String str = Buffers.readString(buffer, strLen);
+ // zero
+ final int trailling = Buffers.readUShort(buffer);
+ return str;
+ }
+ }
+
+ /**
+ * read utf-16 encoding str, use zero char to end str.
+ */
+ @NonNull
+ public static String readStringUTF16(@NonNull final ByteBuffer buffer, final int strLen) {
+ final String str = Buffers.readString(buffer, strLen);
+ for (int i = 0; i < str.length(); i++) {
+ final char c = str.charAt(i);
+ if (c == 0) {
+ return str.substring(0, i);
+ }
+ }
+ return str;
+ }
+
+ /**
+ * read encoding len.
+ * see StringPool.cpp ENCODE_LENGTH
+ */
+ private static int readLen(@NonNull final ByteBuffer buffer) {
+ int len = 0;
+ final int i = Buffers.readUByte(buffer);
+ if ((i & 0x80) != 0) {
+ //read one more byte.
+ len |= (i & 0x7f) << 8;
+ len += Buffers.readUByte(buffer);
+ } else {
+ len = i;
+ }
+ return len;
+ }
+
+ /**
+ * read encoding len.
+ * see Stringpool.cpp ENCODE_LENGTH
+ */
+ private static int readLen16(@NonNull final ByteBuffer buffer) {
+ int len = 0;
+ final int i = Buffers.readUShort(buffer);
+ if ((i & 0x8000) != 0) {
+ len |= (i & 0x7fff) << 16;
+ len += Buffers.readUShort(buffer);
+ } else {
+ len = i;
+ }
+ return len;
+ }
+
+ /**
+ * read String pool, for apk binary xml file and resource table.
+ */
+ @NonNull
+ public static StringPool readStringPool(final @NonNull ByteBuffer buffer, final @NonNull StringPoolHeader stringPoolHeader) {
+ final long beginPos = buffer.position();
+ final int[] offsets = new int[stringPoolHeader.getStringCount()];
+ // read strings offset
+ if (stringPoolHeader.getStringCount() > 0) {
+ for (int idx = 0; idx < stringPoolHeader.getStringCount(); idx++) {
+ offsets[idx] = Unsigned.toUInt(Buffers.readUInt(buffer));
+ }
+ }
+ // read flag
+ // the string index is sorted by the string values if true
+ final boolean sorted = (stringPoolHeader.getFlags() & StringPoolHeader.SORTED_FLAG) != 0;
+ // string use utf-8 format if true, otherwise utf-16
+ final boolean utf8 = (stringPoolHeader.getFlags() & StringPoolHeader.UTF8_FLAG) != 0;
+ // read strings. the head and metas have 28 bytes
+ final long stringPos = beginPos + stringPoolHeader.getStringsStart() - (int) stringPoolHeader.headerSize;
+ Buffers.position(buffer, stringPos);
+ final StringPoolEntry[] entries = new StringPoolEntry[offsets.length];
+ for (int i = 0; i < offsets.length; i++) {
+ entries[i] = new StringPoolEntry(i, stringPos + Unsigned.toLong(offsets[i]));
+ }
+ String lastStr = null;
+ long lastOffset = -1;
+ final StringPool stringPool = new StringPool(stringPoolHeader.getStringCount());
+ for (final StringPoolEntry entry : entries) {
+ if (entry.offset == lastOffset) {
+ stringPool.set(entry.idx, lastStr);
+ continue;
+ }
+ Buffers.position(buffer, entry.offset);
+ lastOffset = entry.offset;
+ final String str = ParseUtils.readString(buffer, utf8);
+ lastStr = str;
+ stringPool.set(entry.idx, str);
+ }
+ // read styles
+ //noinspection StatementWithEmptyBody
+ if (stringPoolHeader.getStyleCount() > 0) {
+ // now we just skip it
+ }
+ Buffers.position(buffer, beginPos + stringPoolHeader.getBodySize());
+ return stringPool;
+ }
+
+ /**
+ * read res value, convert from different types to string.
+ */
+ @Nullable
+ public static ResourceValue readResValue(final @NonNull ByteBuffer buffer, final StringPool stringPool) {
+// ResValue resValue = new ResValue();
+ final int size = Buffers.readUShort(buffer);
+ final short res0 = Buffers.readUByte(buffer);
+ final short dataType = Buffers.readUByte(buffer);
+ switch (dataType) {
+ case ResValue.ResType.INT_DEC:
+ return ResourceValue.decimal(buffer.getInt());
+ case ResValue.ResType.INT_HEX:
+ return ResourceValue.hexadecimal(buffer.getInt());
+ case ResValue.ResType.STRING:
+ final int strRef = buffer.getInt();
+ if (strRef >= 0) {
+ return ResourceValue.string(strRef, stringPool);
+ } else {
+ return null;
+ }
+ case ResValue.ResType.REFERENCE:
+ case ResValue.ResType.TYPE_DYNAMIC_REFERENCE:
+ return ResourceValue.reference(buffer.getInt());
+ case ResValue.ResType.INT_BOOLEAN:
+ return ResourceValue.bool(buffer.getInt());
+ case ResValue.ResType.NULL:
+ return ResourceValue.nullValue();
+ case ResValue.ResType.INT_COLOR_RGB8:
+ case ResValue.ResType.INT_COLOR_RGB4:
+ return ResourceValue.rgb(buffer.getInt(), 6);
+ case ResValue.ResType.INT_COLOR_ARGB8:
+ case ResValue.ResType.INT_COLOR_ARGB4:
+ return ResourceValue.rgb(buffer.getInt(), 8);
+ case ResValue.ResType.DIMENSION:
+ return ResourceValue.dimension(buffer.getInt());
+ case ResValue.ResType.FRACTION:
+ return ResourceValue.fraction(buffer.getInt());
+ default:
+ return ResourceValue.raw(buffer.getInt(), dataType);
+ }
+ }
+
+ public static void checkChunkType(final int expected, final int real) {
+ if (expected != real) {
+ throw new ParserException("Expect chunk type:" + Integer.toHexString(expected)
+ + ", but got:" + Integer.toHexString(real));
+ }
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java
new file mode 100644
index 00000000..bdcd2088
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java
@@ -0,0 +1,142 @@
+package net.dongliu.apk.parser.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * fetch dependency resource file from android source
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class ResourceFetcher {
+
+ /**
+ * from https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml
+ */
+ private void fetchSystemAttrIds()
+ throws IOException, SAXException, ParserConfigurationException {
+ final String url = "https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml";
+ final String html = this.getUrl(url);
+ final String xml = this.retrieveCode(html);
+ if (xml != null) {
+ this.parseAttributeXml(xml);
+ }
+ }
+
+ private void parseAttributeXml(@NonNull final String xml)
+ throws IOException, ParserConfigurationException, SAXException {
+ final SAXParserFactory factory = SAXParserFactory.newInstance();
+ final SAXParser parser = factory.newSAXParser();
+ final List> attrIds = new ArrayList<>();
+ final DefaultHandler dh = new DefaultHandler() {
+ @Override
+ public void startElement(final String uri, final String localName, final String qName,
+ final Attributes attributes) {
+ if (!qName.equals("public")) {
+ return;
+ }
+ final String type = attributes.getValue("type");
+ if (type == null) {
+ return;
+ }
+ if (type.equals("attr")) {
+ //attr ids.
+ String idStr = attributes.getValue("id");
+ if (idStr == null) {
+ return;
+ }
+ final String name = attributes.getValue("name");
+ if (idStr.startsWith("0x")) {
+ idStr = idStr.substring(2);
+ }
+ final int id = Integer.parseInt(idStr, 16);
+ attrIds.add(new Pair<>(id, name));
+ }
+ }
+ };
+ parser.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), dh);
+ for (final Pair pair : attrIds) {
+ System.out.printf("%s=%d%n", pair.getRight(), pair.getLeft());
+ }
+ }
+
+ /**
+ * the android system r style.
+ * see http://developer.android.com/reference/android/R.style.html
+ * from https://android.googlesource.com/platform/frameworks/base/+/master/api/current.txt r.style section
+ */
+ private void fetchSystemStyle() throws IOException {
+ final String url = "https://android.googlesource.com/platform/frameworks/base/+/master/api/current.txt";
+ final String html = this.getUrl(url);
+ final String code = this.retrieveCode(html);
+ if (code == null) {
+ System.err.println("code area not found");
+ return;
+ }
+ final int begin = code.indexOf("R.style");
+ final int end = code.indexOf("}", begin);
+ final String styleCode = code.substring(begin, end);
+ final String[] lines = styleCode.split("\n");
+ for (String line : lines) {
+ line = line.trim();
+ if (line.startsWith("field public static final")) {
+ line = Strings.substringBefore(line, ";").replace("deprecated ", "")
+ .substring("field public static final int ".length()).replace("_", ".");
+ System.out.println(line);
+ }
+ }
+ }
+
+ @NonNull
+ private String getUrl(final String url) throws IOException {
+ final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ try {
+ conn.setRequestMethod("GET");
+ conn.setReadTimeout(10000);
+ conn.setConnectTimeout(10000);
+ final byte[] bytes = Inputs.readAllAndClose(conn.getInputStream());
+ return new String(bytes, StandardCharsets.UTF_8);
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ @Nullable
+ private String retrieveCode(@NonNull final String html) {
+ final Matcher matcher = Pattern.compile("(.*?)
").matcher(html);
+ if (matcher.find()) {
+ final String codeHtml = matcher.group(1);
+ if (codeHtml == null)
+ return null;
+ return codeHtml.replace("", "\n").replaceAll("<[^>]+>", "").replace("<", "<")
+ .replace(""", "\"").replace(">", ">");
+ } else {
+ return null;
+ }
+ }
+
+ public static void main(final String[] args)
+ throws ParserConfigurationException, SAXException, IOException {
+ final ResourceFetcher fetcher = new ResourceFetcher();
+ fetcher.fetchSystemAttrIds();
+ //fetcher.fetchSystemStyle();
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt
new file mode 100644
index 00000000..359b222a
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.kt
@@ -0,0 +1,64 @@
+package net.dongliu.apk.parser.utils
+
+import java.io.*
+
+/**
+ * methods for load resources.
+ *
+ * @author dongliu
+ */
+object ResourceLoader {
+ /**
+ * load system attr ids for parse binary xml.
+ */
+ fun loadSystemAttrIds(): Map {
+ try {
+ toReader("/r_values.ini").use { reader ->
+ val map: MutableMap = HashMap()
+ while (true) {
+ val line: String = reader.readLine() ?: break
+ val items = line.trim().split("=")
+ if (items.size != 2) {
+ continue
+ }
+ val name = items[0].trim()
+ val id = Integer.valueOf(items[1].trim())
+ map[id] = name
+ }
+ return map
+ }
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+ }
+
+ @JvmStatic
+ fun loadSystemStyles(): Map {
+ val map: MutableMap = HashMap()
+ try {
+ toReader("/r_styles.ini").use { reader ->
+ while (true) {
+ val line = reader.readLine() ?: break
+ val items = line.trim().split("=")
+ if (items.size != 2) {
+ continue
+ }
+ val id = Integer.valueOf(items[1].trim())
+ val name = items[0].trim()
+ map[id] = name
+ }
+ }
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+ return map
+ }
+
+ private fun toReader(path: String): BufferedReader {
+ return BufferedReader(
+ InputStreamReader(
+ ResourceLoader::class.java.getResourceAsStream(path)
+ )
+ )
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt
new file mode 100644
index 00000000..40a33bc8
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Strings.kt
@@ -0,0 +1,65 @@
+package net.dongliu.apk.parser.utils
+
+object Strings {
+ /**
+ * Copied fom commons StringUtils
+ *
+ * Joins the elements of the provided `Iterable` into
+ * a single String containing the provided elements.
+ */
+ @JvmStatic
+ fun join(iterable: Iterable<*>, separator: String): String? {
+ return join(iterable.iterator(), separator)
+ }
+
+ /**
+ * Copied fom commons StringUtils
+ */
+ fun join(iterator: Iterator<*>, separator: String?): String? {
+ if (!iterator.hasNext()) {
+ return ""
+ }
+ val first = iterator.next()
+ if (!iterator.hasNext()) {
+ return first?.toString()
+ }
+ // two or more elements
+ val buf = StringBuilder(256)
+ // Java default is 16, probably too small
+ if (first != null) {
+ buf.append(first)
+ }
+ while (iterator.hasNext()) {
+ if (separator != null) {
+ buf.append(separator)
+ }
+ val obj = iterator.next()
+ if (obj != null) {
+ buf.append(obj)
+ }
+ }
+ return buf.toString()
+ }
+
+ @JvmStatic
+ fun isNumeric(cs: CharSequence): Boolean {
+ if (cs.isEmpty()) {
+ return false
+ }
+ return cs.find { !it.isDigit() } == null
+ }
+
+ @JvmStatic
+ fun substringBefore(str: String, separator: String): String {
+ if (str.isEmpty()) {
+ return str
+ }
+ if (separator.isEmpty()) {
+ return ""
+ }
+ val pos = str.indexOf(separator)
+ return if (pos == -1) {
+ str
+ } else str.substring(0, pos)
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt
new file mode 100644
index 00000000..9ecd027d
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/Unsigned.kt
@@ -0,0 +1,52 @@
+package net.dongliu.apk.parser.utils
+
+/**
+ * Unsigned utils, for compatible with java6/java7.
+ */
+object Unsigned {
+ @JvmStatic
+ fun toLong(value: Int): Long {
+ return value.toLong() and 0xffffffffL
+ }
+
+ @JvmStatic
+ fun toUInt(value: Long): Int {
+ return value.toInt()
+ }
+
+ @JvmStatic
+ fun toInt(value: Short): Int {
+ return value.toInt() and 0xffff
+ }
+
+ @JvmStatic
+ fun toUShort(value: Int): Short {
+ return value.toShort()
+ }
+
+ @JvmStatic
+ fun ensureUInt(value: Long): Int {
+ if (value < 0 || value > Int.MAX_VALUE) {
+ throw ArithmeticException("unsigned integer overflow")
+ }
+ return value.toInt()
+ }
+
+ @JvmStatic
+ fun ensureULong(value: Long): Long {
+ if (value < 0) {
+ throw ArithmeticException("unsigned long overflow")
+ }
+ return value
+ }
+
+ @JvmStatic
+ fun toShort(value: Byte): Short {
+ return (value.toInt() and 0xff).toShort()
+ }
+
+ @JvmStatic
+ fun toUByte(value: Short): Byte {
+ return value.toByte()
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java
new file mode 100644
index 00000000..16aa135b
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Executes a sequence of translators one after the other. Execution ends whenever
+ * the first translator consumes codepoints from the input.
+ */
+class AggregateTranslator extends CharSequenceTranslator {
+
+ private final CharSequenceTranslator[] translators;
+
+ /**
+ * Specify the translators to be used at creation time.
+ *
+ * @param translators CharSequenceTranslator array to aggregate
+ */
+ public AggregateTranslator(final CharSequenceTranslator... translators) {
+ this.translators = translators;
+ }
+
+ /**
+ * The first translator to consume codepoints from the input is the 'winner'.
+ * Execution stops with the number of consumed codepoints being returned.
+ * {@inheritDoc}
+ */
+ @Override
+ public int translate(final CharSequence input, final int index, final Writer out) throws IOException {
+ for (final CharSequenceTranslator translator : this.translators) {
+ final int consumed = translator.translate(input, index, out);
+ if (consumed != 0) {
+ return consumed;
+ }
+ }
+ return 0;
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java
new file mode 100644
index 00000000..760ead8a
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Locale;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * An API for translating text.
+ * Its core use is to escape and unescape text. Because escaping and unescaping
+ * is completely contextual, the API does not present two separate signatures.
+ */
+abstract class CharSequenceTranslator {
+
+ /**
+ * Translate a set of codepoints, represented by an int index into a CharSequence,
+ * into another set of codepoints. The number of codepoints consumed must be returned,
+ * and the only IOExceptions thrown must be from interacting with the Writer so that
+ * the top level API may reliably ignore StringWriter IOExceptions.
+ *
+ * @param input CharSequence that is being translated
+ * @param index int representing the current point of translation
+ * @param out Writer to translate the text to
+ * @return int count of codepoints consumed
+ * @throws IOException if and only if the Writer produces an IOException
+ */
+ public abstract int translate(CharSequence input, int index, Writer out) throws IOException;
+
+ /**
+ * Helper for non-Writer usage.
+ *
+ * @param input CharSequence to be translated
+ * @return String output of translation
+ */
+ @Nullable
+ public final String translate(@Nullable final CharSequence input) {
+ if (input == null) {
+ return null;
+ }
+ try {
+ final StringWriter writer = new StringWriter(input.length() * 2);
+ this.translate(input, writer);
+ return writer.toString();
+ } catch (final IOException ioe) {
+ // this should never ever happen while writing to a StringWriter
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ /**
+ * Translate an input onto a Writer. This is intentionally final as its algorithm is
+ * tightly coupled with the abstract method of this class.
+ *
+ * @param input CharSequence that is being translated
+ * @param out Writer to translate the text to
+ * @throws IOException if and only if the Writer produces an IOException
+ */
+ public final void translate(final CharSequence input, final Writer out) throws IOException {
+ if (out == null) {
+ throw new IllegalArgumentException("The Writer must not be null");
+ }
+ if (input == null) {
+ return;
+ }
+ int pos = 0;
+ final int len = input.length();
+ while (pos < len) {
+ final int consumed = this.translate(input, pos, out);
+ if (consumed == 0) {
+ final char[] c = Character.toChars(Character.codePointAt(input, pos));
+ out.write(c);
+ pos += c.length;
+ continue;
+ }
+ // contract with translators is that they have to understand codepoints
+ // and they just took care of a surrogate pair
+ for (int pt = 0; pt < consumed; pt++) {
+ pos += Character.charCount(Character.codePointAt(input, pos));
+ }
+ }
+ }
+
+ /**
+ * Helper method to create a merger of this translator with another set of
+ * translators. Useful in customizing the standard functionality.
+ *
+ * @param translators CharSequenceTranslator array of translators to merge with this one
+ * @return CharSequenceTranslator merging this translator with the others
+ */
+ @NonNull
+ public final CharSequenceTranslator with(final CharSequenceTranslator... translators) {
+ final CharSequenceTranslator[] newArray = new CharSequenceTranslator[translators.length + 1];
+ newArray[0] = this;
+ System.arraycopy(translators, 0, newArray, 1, translators.length);
+ return new AggregateTranslator(newArray);
+ }
+
+ /**
+ * Returns an upper case hexadecimal String for the given
+ * character.
+ *
+ * @param codepoint The codepoint to convert.
+ * @return An upper case hexadecimal String
+ */
+ public static String hex(final int codepoint) {
+ return Integer.toHexString(codepoint).toUpperCase(Locale.ENGLISH);
+ }
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java
new file mode 100644
index 00000000..897db93d
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Helper subclass to CharSequenceTranslator to allow for translations that
+ * will replace up to one character at a time.
+ */
+abstract class CodePointTranslator extends CharSequenceTranslator {
+
+ /**
+ * Implementation of translate that maps onto the abstract translate(int, Writer) method.
+ * {@inheritDoc}
+ */
+ @Override
+ public final int translate(final CharSequence input, final int index, final Writer out) throws IOException {
+ final int codepoint = Character.codePointAt(input, index);
+ final boolean consumed = this.translate(codepoint, out);
+ return consumed ? 1 : 0;
+ }
+
+ /**
+ * Translate the specified codepoint into another.
+ *
+ * @param codepoint int character input to translate
+ * @param out Writer to optionally push the translated output to
+ * @return boolean as to whether translation occurred or not
+ * @throws IOException if and only if the Writer produces an IOException
+ */
+ public abstract boolean translate(int codepoint, Writer out) throws IOException;
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java
new file mode 100644
index 00000000..c7934914
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Class holding various entity data for HTML and XML - generally for use with
+ * the LookupTranslator.
+ * All arrays are of length [*][2].
+ */
+public class EntityArrays {
+ /**
+ * Mapping to escape the basic XML and HTML character entities.
+ *
+ * Namely: {@code " & < >}
+ *
+ * @return the mapping table
+ */
+ @NonNull
+ public static String[][] BASIC_ESCAPE() {
+ return EntityArrays.BASIC_ESCAPE.clone();
+ }
+
+ private static final String[][] BASIC_ESCAPE = {
+ {"\"", """}, // " - double-quote
+ {"&", "&"}, // & - ampersand
+ {"<", "<"}, // < - less-than
+ {">", ">"}, // > - greater-than
+ };
+
+ /**
+ * Mapping to escape the apostrophe character to its XML character entity.
+ *
+ * @return the mapping table
+ */
+ @NonNull
+ public static String[][] APOS_ESCAPE() {
+ return EntityArrays.APOS_ESCAPE.clone();
+ }
+
+ private static final String[][] APOS_ESCAPE = {
+ {"'", "'"}, // XML apostrophe
+ };
+
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java
new file mode 100644
index 00000000..fed78524
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashMap;
+
+/**
+ * Translates a value using a lookup table.
+ */
+class LookupTranslator extends CharSequenceTranslator {
+
+ private final HashMap lookupMap;
+ private final int shortest;
+ private final int longest;
+
+ /**
+ * Define the lookup table to be used in translation
+ *
+ * Note that, as of Lang 3.1, the key to the lookup table is converted to a
+ * java.lang.String, while the value remains as a java.lang.CharSequence.
+ * This is because we need the key to support hashCode and equals(Object),
+ * allowing it to be the key for a HashMap. See LANG-882.
+ *
+ * @param lookup CharSequence[][] table of size [*][2]
+ */
+ public LookupTranslator(final CharSequence[]... lookup) {
+ this.lookupMap = new HashMap<>();
+ int _shortest = Integer.MAX_VALUE;
+ int _longest = 0;
+ if (lookup != null) {
+ for (final CharSequence[] seq : lookup) {
+ this.lookupMap.put(seq[0].toString(), seq[1]);
+ final int sz = seq[0].length();
+ if (sz < _shortest) {
+ _shortest = sz;
+ }
+ if (sz > _longest) {
+ _longest = sz;
+ }
+ }
+ }
+ this.shortest = _shortest;
+ this.longest = _longest;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int translate(final CharSequence input, final int index, final Writer out) throws IOException {
+ int max = this.longest;
+ if (index + this.longest > input.length()) {
+ max = input.length() - index;
+ }
+ // descend so as to get a greedy algorithm
+ for (int i = max; i >= this.shortest; i--) {
+ final CharSequence subSeq = input.subSequence(index, index + i);
+ final CharSequence result = this.lookupMap.get(subSeq.toString());
+ if (result != null) {
+ out.write(result.toString());
+ return i;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java
new file mode 100644
index 00000000..9d6146cc
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Translates codepoints to their XML numeric entity escaped value.
+ */
+class NumericEntityEscaper extends CodePointTranslator {
+
+ private final int below;
+ private final int above;
+ private final boolean between;
+
+ /**
+ *
Constructs a NumericEntityEscaper for the specified range. This is
+ * the underlying method for the other constructors/builders. The below
+ * and above boundaries are inclusive when between is
+ * true and exclusive when it is false.
+ *
+ * @param below int value representing the lowest codepoint boundary
+ * @param above int value representing the highest codepoint boundary
+ * @param between whether to escape between the boundaries or outside them
+ */
+ private NumericEntityEscaper(final int below, final int above, final boolean between) {
+ this.below = below;
+ this.above = above;
+ this.between = between;
+ }
+
+ /**
+ * Constructs a NumericEntityEscaper for all characters.
+ */
+ public NumericEntityEscaper() {
+ this(0, Integer.MAX_VALUE, true);
+ }
+
+ /**
+ * Constructs a NumericEntityEscaper below the specified value (exclusive).
+ *
+ * @param codepoint below which to escape
+ * @return the newly created {@code NumericEntityEscaper} instance
+ */
+ @NonNull
+ public static NumericEntityEscaper below(final int codepoint) {
+ return NumericEntityEscaper.outsideOf(codepoint, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Constructs a NumericEntityEscaper above the specified value (exclusive).
+ *
+ * @param codepoint above which to escape
+ * @return the newly created {@code NumericEntityEscaper} instance
+ */
+ @NonNull
+ public static NumericEntityEscaper above(final int codepoint) {
+ return NumericEntityEscaper.outsideOf(0, codepoint);
+ }
+
+ /**
+ * Constructs a NumericEntityEscaper between the specified values (inclusive).
+ *
+ * @param codepointLow above which to escape
+ * @param codepointHigh below which to escape
+ * @return the newly created {@code NumericEntityEscaper} instance
+ */
+ @NonNull
+ public static NumericEntityEscaper between(final int codepointLow, final int codepointHigh) {
+ return new NumericEntityEscaper(codepointLow, codepointHigh, true);
+ }
+
+ /**
+ * Constructs a NumericEntityEscaper outside of the specified values (exclusive).
+ *
+ * @param codepointLow below which to escape
+ * @param codepointHigh above which to escape
+ * @return the newly created {@code NumericEntityEscaper} instance
+ */
+ @NonNull
+ public static NumericEntityEscaper outsideOf(final int codepointLow, final int codepointHigh) {
+ return new NumericEntityEscaper(codepointLow, codepointHigh, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean translate(final int codepoint, final Writer out) throws IOException {
+ if (this.between) {
+ if (codepoint < this.below || codepoint > this.above) {
+ return false;
+ }
+ } else {
+ if (codepoint >= this.below && codepoint <= this.above) {
+ return false;
+ }
+ }
+
+ out.write("");
+ out.write(Integer.toString(codepoint, 10));
+ out.write(';');
+ return true;
+ }
+}
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java
new file mode 100644
index 00000000..8bcd7d4a
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 net.dongliu.apk.parser.utils.xml;
+
+import java.io.Writer;
+
+/**
+ * Helper subclass to CharSequenceTranslator to remove unpaired surrogates.
+ */
+class UnicodeUnpairedSurrogateRemover extends CodePointTranslator {
+ /**
+ * Implementation of translate that throws out unpaired surrogates.
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean translate(final int codepoint, final Writer out) {
+ // It's a surrogate. Write nothing and say we've translated.
+ // It's not a surrogate. Don't translate it.
+ return codepoint >= Character.MIN_SURROGATE && codepoint <= Character.MAX_SURROGATE;
+ }
+}
+
diff --git a/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java
new file mode 100644
index 00000000..97c63619
--- /dev/null
+++ b/modules/apk-parser/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java
@@ -0,0 +1,62 @@
+package net.dongliu.apk.parser.utils.xml;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utils method to escape xml string, copied from apache commons lang3
+ *
+ * @author Liu Dong {@literal }
+ */
+public class XmlEscaper {
+
+ /**
+ * Escapes the characters in a {@code String} using XML entities.
+ */
+ @Nullable
+ public static String escapeXml10(@Nullable final String input) {
+ return XmlEscaper.ESCAPE_XML10.translate(input);
+ }
+
+ public static final CharSequenceTranslator ESCAPE_XML10 =
+ new AggregateTranslator(
+ new LookupTranslator(EntityArrays.BASIC_ESCAPE()),
+ new LookupTranslator(EntityArrays.APOS_ESCAPE()),
+ new LookupTranslator(
+ new String[][]{
+ {"\u0000", ""},
+ {"\u0001", ""},
+ {"\u0002", ""},
+ {"\u0003", ""},
+ {"\u0004", ""},
+ {"\u0005", ""},
+ {"\u0006", ""},
+ {"\u0007", ""},
+ {"\u0008", ""},
+ {"\u000b", ""},
+ {"\u000c", ""},
+ {"\u000e", ""},
+ {"\u000f", ""},
+ {"\u0010", ""},
+ {"\u0011", ""},
+ {"\u0012", ""},
+ {"\u0013", ""},
+ {"\u0014", ""},
+ {"\u0015", ""},
+ {"\u0016", ""},
+ {"\u0017", ""},
+ {"\u0018", ""},
+ {"\u0019", ""},
+ {"\u001a", ""},
+ {"\u001b", ""},
+ {"\u001c", ""},
+ {"\u001d", ""},
+ {"\u001e", ""},
+ {"\u001f", ""},
+ {"\ufffe", ""},
+ {"\uffff", ""}
+ }),
+ NumericEntityEscaper.between(0x7f, 0x84),
+ NumericEntityEscaper.between(0x86, 0x9f),
+ new UnicodeUnpairedSurrogateRemover()
+ );
+}
diff --git a/modules/apk-parser/src/main/resources/r_styles.ini b/modules/apk-parser/src/main/resources/r_styles.ini
new file mode 100644
index 00000000..38d71b6f
--- /dev/null
+++ b/modules/apk-parser/src/main/resources/r_styles.ini
@@ -0,0 +1,720 @@
+[style]
+Animation = 16973824
+Animation.Activity = 16973825
+Animation.Dialog = 16973826
+Animation.InputMethod = 16973910
+Animation.Toast = 16973828
+Animation.Translucent = 16973827
+DeviceDefault.ButtonBar = 16974287
+DeviceDefault.ButtonBar.AlertDialog = 16974288
+DeviceDefault.Light.ButtonBar = 16974290
+DeviceDefault.Light.ButtonBar.AlertDialog = 16974291
+DeviceDefault.Light.SegmentedButton = 16974292
+DeviceDefault.SegmentedButton = 16974289
+Holo.ButtonBar = 16974053
+Holo.ButtonBar.AlertDialog = 16974055
+Holo.Light.ButtonBar = 16974054
+Holo.Light.ButtonBar.AlertDialog = 16974056
+Holo.Light.SegmentedButton = 16974058
+Holo.SegmentedButton = 16974057
+MediaButton = 16973879
+MediaButton.Ffwd = 16973883
+MediaButton.Next = 16973881
+MediaButton.Pause = 16973885
+MediaButton.Play = 16973882
+MediaButton.Previous = 16973880
+MediaButton.Rew = 16973884
+TextAppearance = 16973886
+TextAppearance.DeviceDefault = 16974253
+TextAppearance.DeviceDefault.DialogWindowTitle = 16974264
+TextAppearance.DeviceDefault.Inverse = 16974254
+TextAppearance.DeviceDefault.Large = 16974255
+TextAppearance.DeviceDefault.Large.Inverse = 16974256
+TextAppearance.DeviceDefault.Medium = 16974257
+TextAppearance.DeviceDefault.Medium.Inverse = 16974258
+TextAppearance.DeviceDefault.SearchResult.Subtitle = 16974262
+TextAppearance.DeviceDefault.SearchResult.Title = 16974261
+TextAppearance.DeviceDefault.Small = 16974259
+TextAppearance.DeviceDefault.Small.Inverse = 16974260
+TextAppearance.DeviceDefault.Widget = 16974265
+TextAppearance.DeviceDefault.Widget.ActionBar.Menu = 16974286
+TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle = 16974279
+TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle.Inverse = 16974283
+TextAppearance.DeviceDefault.Widget.ActionBar.Title = 16974278
+TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse = 16974282
+TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle = 16974281
+TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle.Inverse = 16974285
+TextAppearance.DeviceDefault.Widget.ActionMode.Title = 16974280
+TextAppearance.DeviceDefault.Widget.ActionMode.Title.Inverse = 16974284
+TextAppearance.DeviceDefault.Widget.Button = 16974266
+TextAppearance.DeviceDefault.Widget.DropDownHint = 16974271
+TextAppearance.DeviceDefault.Widget.DropDownItem = 16974272
+TextAppearance.DeviceDefault.Widget.EditText = 16974274
+TextAppearance.DeviceDefault.Widget.IconMenu.Item = 16974267
+TextAppearance.DeviceDefault.Widget.PopupMenu = 16974275
+TextAppearance.DeviceDefault.Widget.PopupMenu.Large = 16974276
+TextAppearance.DeviceDefault.Widget.PopupMenu.Small = 16974277
+TextAppearance.DeviceDefault.Widget.TabWidget = 16974268
+TextAppearance.DeviceDefault.Widget.TextView = 16974269
+TextAppearance.DeviceDefault.Widget.TextView.PopupMenu = 16974270
+TextAppearance.DeviceDefault.Widget.TextView.SpinnerItem = 16974273
+TextAppearance.DeviceDefault.WindowTitle = 16974263
+TextAppearance.DialogWindowTitle = 16973889
+TextAppearance.Holo = 16974075
+TextAppearance.Holo.DialogWindowTitle = 16974103
+TextAppearance.Holo.Inverse = 16974076
+TextAppearance.Holo.Large = 16974077
+TextAppearance.Holo.Large.Inverse = 16974078
+TextAppearance.Holo.Medium = 16974079
+TextAppearance.Holo.Medium.Inverse = 16974080
+TextAppearance.Holo.SearchResult.Subtitle = 16974084
+TextAppearance.Holo.SearchResult.Title = 16974083
+TextAppearance.Holo.Small = 16974081
+TextAppearance.Holo.Small.Inverse = 16974082
+TextAppearance.Holo.Widget = 16974085
+TextAppearance.Holo.Widget.ActionBar.Menu = 16974112
+TextAppearance.Holo.Widget.ActionBar.Subtitle = 16974099
+TextAppearance.Holo.Widget.ActionBar.Subtitle.Inverse = 16974109
+TextAppearance.Holo.Widget.ActionBar.Title = 16974098
+TextAppearance.Holo.Widget.ActionBar.Title.Inverse = 16974108
+TextAppearance.Holo.Widget.ActionMode.Subtitle = 16974101
+TextAppearance.Holo.Widget.ActionMode.Subtitle.Inverse = 16974111
+TextAppearance.Holo.Widget.ActionMode.Title = 16974100
+TextAppearance.Holo.Widget.ActionMode.Title.Inverse = 16974110
+TextAppearance.Holo.Widget.Button = 16974086
+TextAppearance.Holo.Widget.DropDownHint = 16974091
+TextAppearance.Holo.Widget.DropDownItem = 16974092
+TextAppearance.Holo.Widget.EditText = 16974094
+TextAppearance.Holo.Widget.IconMenu.Item = 16974087
+TextAppearance.Holo.Widget.PopupMenu = 16974095
+TextAppearance.Holo.Widget.PopupMenu.Large = 16974096
+TextAppearance.Holo.Widget.PopupMenu.Small = 16974097
+TextAppearance.Holo.Widget.TabWidget = 16974088
+TextAppearance.Holo.Widget.TextView = 16974089
+TextAppearance.Holo.Widget.TextView.PopupMenu = 16974090
+TextAppearance.Holo.Widget.TextView.SpinnerItem = 16974093
+TextAppearance.Holo.WindowTitle = 16974102
+TextAppearance.Inverse = 16973887
+TextAppearance.Large = 16973890
+TextAppearance.Large.Inverse = 16973891
+TextAppearance.Material = 16974317
+TextAppearance.Material.Body1 = 16974320
+TextAppearance.Material.Body2 = 16974319
+TextAppearance.Material.Button = 16974318
+TextAppearance.Material.Caption = 16974321
+TextAppearance.Material.DialogWindowTitle = 16974322
+TextAppearance.Material.Display1 = 16974326
+TextAppearance.Material.Display2 = 16974325
+TextAppearance.Material.Display3 = 16974324
+TextAppearance.Material.Display4 = 16974323
+TextAppearance.Material.Headline = 16974327
+TextAppearance.Material.Inverse = 16974328
+TextAppearance.Material.Large = 16974329
+TextAppearance.Material.Large.Inverse = 16974330
+TextAppearance.Material.Medium = 16974331
+TextAppearance.Material.Medium.Inverse = 16974332
+TextAppearance.Material.Menu = 16974333
+TextAppearance.Material.Notification = 16974334
+TextAppearance.Material.Notification.Emphasis = 16974335
+TextAppearance.Material.Notification.Info = 16974336
+TextAppearance.Material.Notification.Line2 = 16974337
+TextAppearance.Material.Notification.Time = 16974338
+TextAppearance.Material.Notification.Title = 16974339
+TextAppearance.Material.SearchResult.Subtitle = 16974340
+TextAppearance.Material.SearchResult.Title = 16974341
+TextAppearance.Material.Small = 16974342
+TextAppearance.Material.Small.Inverse = 16974343
+TextAppearance.Material.Subhead = 16974344
+TextAppearance.Material.Title = 16974345
+TextAppearance.Material.Widget = 16974347
+TextAppearance.Material.Widget.ActionBar.Menu = 16974348
+TextAppearance.Material.Widget.ActionBar.Subtitle = 16974349
+TextAppearance.Material.Widget.ActionBar.Subtitle.Inverse = 16974350
+TextAppearance.Material.Widget.ActionBar.Title = 16974351
+TextAppearance.Material.Widget.ActionBar.Title.Inverse = 16974352
+TextAppearance.Material.Widget.ActionMode.Subtitle = 16974353
+TextAppearance.Material.Widget.ActionMode.Subtitle.Inverse = 16974354
+TextAppearance.Material.Widget.ActionMode.Title = 16974355
+TextAppearance.Material.Widget.ActionMode.Title.Inverse = 16974356
+TextAppearance.Material.Widget.Button = 16974357
+TextAppearance.Material.Widget.DropDownHint = 16974358
+TextAppearance.Material.Widget.DropDownItem = 16974359
+TextAppearance.Material.Widget.EditText = 16974360
+TextAppearance.Material.Widget.IconMenu.Item = 16974361
+TextAppearance.Material.Widget.PopupMenu = 16974362
+TextAppearance.Material.Widget.PopupMenu.Large = 16974363
+TextAppearance.Material.Widget.PopupMenu.Small = 16974364
+TextAppearance.Material.Widget.TabWidget = 16974365
+TextAppearance.Material.Widget.TextView = 16974366
+TextAppearance.Material.Widget.TextView.PopupMenu = 16974367
+TextAppearance.Material.Widget.TextView.SpinnerItem = 16974368
+TextAppearance.Material.Widget.Toolbar.Subtitle = 16974369
+TextAppearance.Material.Widget.Toolbar.Title = 16974370
+TextAppearance.Material.WindowTitle = 16974346
+TextAppearance.Medium = 16973892
+TextAppearance.Medium.Inverse = 16973893
+TextAppearance.Small = 16973894
+TextAppearance.Small.Inverse = 16973895
+TextAppearance.StatusBar.EventContent = 16973927
+TextAppearance.StatusBar.EventContent.Title = 16973928
+TextAppearance.StatusBar.Icon = 16973926
+TextAppearance.StatusBar.Title = 16973925
+TextAppearance.SuggestionHighlight = 16974104
+TextAppearance.Theme = 16973888
+TextAppearance.Theme.Dialog = 16973896
+TextAppearance.Widget = 16973897
+TextAppearance.Widget.Button = 16973898
+TextAppearance.Widget.DropDownHint = 16973904
+TextAppearance.Widget.DropDownItem = 16973905
+TextAppearance.Widget.EditText = 16973900
+TextAppearance.Widget.IconMenu.Item = 16973899
+TextAppearance.Widget.PopupMenu.Large = 16973952
+TextAppearance.Widget.PopupMenu.Small = 16973953
+TextAppearance.Widget.TabWidget = 16973901
+TextAppearance.Widget.TextView = 16973902
+TextAppearance.Widget.TextView.PopupMenu = 16973903
+TextAppearance.Widget.TextView.SpinnerItem = 16973906
+TextAppearance.WindowTitle = 16973907
+Theme = 16973829
+ThemeOverlay = 16974407
+ThemeOverlay.Material = 16974408
+ThemeOverlay.Material.ActionBar = 16974409
+ThemeOverlay.Material.Dark = 16974411
+ThemeOverlay.Material.Dark.ActionBar = 16974412
+ThemeOverlay.Material.Light = 16974410
+Theme.Black = 16973832
+Theme.Black.NoTitleBar = 16973833
+Theme.Black.NoTitleBar.Fullscreen = 16973834
+Theme.DeviceDefault = 16974120
+Theme.DeviceDefault.Dialog = 16974126
+Theme.DeviceDefault.DialogWhenLarge = 16974134
+Theme.DeviceDefault.DialogWhenLarge.NoActionBar = 16974135
+Theme.DeviceDefault.Dialog.Alert = 16974545
+Theme.DeviceDefault.Dialog.MinWidth = 16974127
+Theme.DeviceDefault.Dialog.NoActionBar = 16974128
+Theme.DeviceDefault.Dialog.NoActionBar.MinWidth = 16974129
+Theme.DeviceDefault.InputMethod = 16974142
+Theme.DeviceDefault.Light = 16974123
+Theme.DeviceDefault.Light.DarkActionBar = 16974143
+Theme.DeviceDefault.Light.Dialog = 16974130
+Theme.DeviceDefault.Light.DialogWhenLarge = 16974136
+Theme.DeviceDefault.Light.DialogWhenLarge.NoActionBar = 16974137
+Theme.DeviceDefault.Light.Dialog.Alert = 16974546
+Theme.DeviceDefault.Light.Dialog.MinWidth = 16974131
+Theme.DeviceDefault.Light.Dialog.NoActionBar = 16974132
+Theme.DeviceDefault.Light.Dialog.NoActionBar.MinWidth = 16974133
+Theme.DeviceDefault.Light.NoActionBar = 16974124
+Theme.DeviceDefault.Light.NoActionBar.Fullscreen = 16974125
+Theme.DeviceDefault.Light.NoActionBar.Overscan = 16974304
+Theme.DeviceDefault.Light.NoActionBar.TranslucentDecor = 16974308
+Theme.DeviceDefault.Light.Panel = 16974139
+Theme.DeviceDefault.NoActionBar = 16974121
+Theme.DeviceDefault.NoActionBar.Fullscreen = 16974122
+Theme.DeviceDefault.NoActionBar.Overscan = 16974303
+Theme.DeviceDefault.NoActionBar.TranslucentDecor = 16974307
+Theme.DeviceDefault.Panel = 16974138
+Theme.DeviceDefault.Settings = 16974371
+Theme.DeviceDefault.Wallpaper = 16974140
+Theme.DeviceDefault.Wallpaper.NoTitleBar = 16974141
+Theme.Dialog = 16973835
+Theme.Holo = 16973931
+Theme.Holo.Dialog = 16973935
+Theme.Holo.DialogWhenLarge = 16973943
+Theme.Holo.DialogWhenLarge.NoActionBar = 16973944
+Theme.Holo.Dialog.MinWidth = 16973936
+Theme.Holo.Dialog.NoActionBar = 16973937
+Theme.Holo.Dialog.NoActionBar.MinWidth = 16973938
+Theme.Holo.InputMethod = 16973951
+Theme.Holo.Light = 16973934
+Theme.Holo.Light.DarkActionBar = 16974105
+Theme.Holo.Light.Dialog = 16973939
+Theme.Holo.Light.DialogWhenLarge = 16973945
+Theme.Holo.Light.DialogWhenLarge.NoActionBar = 16973946
+Theme.Holo.Light.Dialog.MinWidth = 16973940
+Theme.Holo.Light.Dialog.NoActionBar = 16973941
+Theme.Holo.Light.Dialog.NoActionBar.MinWidth = 16973942
+Theme.Holo.Light.NoActionBar = 16974064
+Theme.Holo.Light.NoActionBar.Fullscreen = 16974065
+Theme.Holo.Light.NoActionBar.Overscan = 16974302
+Theme.Holo.Light.NoActionBar.TranslucentDecor = 16974306
+Theme.Holo.Light.Panel = 16973948
+Theme.Holo.NoActionBar = 16973932
+Theme.Holo.NoActionBar.Fullscreen = 16973933
+Theme.Holo.NoActionBar.Overscan = 16974301
+Theme.Holo.NoActionBar.TranslucentDecor = 16974305
+Theme.Holo.Panel = 16973947
+Theme.Holo.Wallpaper = 16973949
+Theme.Holo.Wallpaper.NoTitleBar = 16973950
+Theme.InputMethod = 16973908
+Theme.Light = 16973836
+Theme.Light.NoTitleBar = 16973837
+Theme.Light.NoTitleBar.Fullscreen = 16973838
+Theme.Light.Panel = 16973914
+Theme.Light.WallpaperSettings = 16973922
+Theme.Material = 16974372
+Theme.Material.Dialog = 16974373
+Theme.Material.DialogWhenLarge = 16974379
+Theme.Material.DialogWhenLarge.NoActionBar = 16974380
+Theme.Material.Dialog.Alert = 16974374
+Theme.Material.Dialog.MinWidth = 16974375
+Theme.Material.Dialog.NoActionBar = 16974376
+Theme.Material.Dialog.NoActionBar.MinWidth = 16974377
+Theme.Material.Dialog.Presentation = 16974378
+Theme.Material.InputMethod = 16974381
+Theme.Material.Light = 16974391
+Theme.Material.Light.DarkActionBar = 16974392
+Theme.Material.Light.Dialog = 16974393
+Theme.Material.Light.DialogWhenLarge = 16974399
+Theme.Material.Light.DialogWhenLarge.NoActionBar = 16974400
+Theme.Material.Light.Dialog.Alert = 16974394
+Theme.Material.Light.Dialog.MinWidth = 16974395
+Theme.Material.Light.Dialog.NoActionBar = 16974396
+Theme.Material.Light.Dialog.NoActionBar.MinWidth = 16974397
+Theme.Material.Light.Dialog.Presentation = 16974398
+Theme.Material.Light.NoActionBar = 16974401
+Theme.Material.Light.NoActionBar.Fullscreen = 16974402
+Theme.Material.Light.NoActionBar.Overscan = 16974403
+Theme.Material.Light.NoActionBar.TranslucentDecor = 16974404
+Theme.Material.Light.Panel = 16974405
+Theme.Material.Light.Voice = 16974406
+Theme.Material.NoActionBar = 16974382
+Theme.Material.NoActionBar.Fullscreen = 16974383
+Theme.Material.NoActionBar.Overscan = 16974384
+Theme.Material.NoActionBar.TranslucentDecor = 16974385
+Theme.Material.Panel = 16974386
+Theme.Material.Settings = 16974387
+Theme.Material.Voice = 16974388
+Theme.Material.Wallpaper = 16974389
+Theme.Material.Wallpaper.NoTitleBar = 16974390
+Theme.NoDisplay = 16973909
+Theme.NoTitleBar = 16973830
+Theme.NoTitleBar.Fullscreen = 16973831
+Theme.NoTitleBar.OverlayActionModes = 16973930
+Theme.Panel = 16973913
+Theme.Translucent = 16973839
+Theme.Translucent.NoTitleBar = 16973840
+Theme.Translucent.NoTitleBar.Fullscreen = 16973841
+Theme.Wallpaper = 16973918
+Theme.WallpaperSettings = 16973921
+Theme.Wallpaper.NoTitleBar = 16973919
+Theme.Wallpaper.NoTitleBar.Fullscreen = 16973920
+Theme.WithActionBar = 16973929
+Widget = 16973842
+Widget.AbsListView = 16973843
+Widget.ActionBar = 16973954
+Widget.ActionBar.TabBar = 16974068
+Widget.ActionBar.TabText = 16974067
+Widget.ActionBar.TabView = 16974066
+Widget.ActionButton = 16973956
+Widget.ActionButton.CloseMode = 16973960
+Widget.ActionButton.Overflow = 16973959
+Widget.AutoCompleteTextView = 16973863
+Widget.Button = 16973844
+Widget.Button.Inset = 16973845
+Widget.Button.Small = 16973846
+Widget.Button.Toggle = 16973847
+Widget.CalendarView = 16974059
+Widget.CompoundButton = 16973848
+Widget.CompoundButton.CheckBox = 16973849
+Widget.CompoundButton.RadioButton = 16973850
+Widget.CompoundButton.Star = 16973851
+Widget.DatePicker = 16974062
+Widget.DeviceDefault = 16974144
+Widget.DeviceDefault.ActionBar = 16974187
+Widget.DeviceDefault.ActionBar.Solid = 16974195
+Widget.DeviceDefault.ActionBar.TabBar = 16974194
+Widget.DeviceDefault.ActionBar.TabText = 16974193
+Widget.DeviceDefault.ActionBar.TabView = 16974192
+Widget.DeviceDefault.ActionButton = 16974182
+Widget.DeviceDefault.ActionButton.CloseMode = 16974186
+Widget.DeviceDefault.ActionButton.Overflow = 16974183
+Widget.DeviceDefault.ActionButton.TextButton = 16974184
+Widget.DeviceDefault.ActionMode = 16974185
+Widget.DeviceDefault.AutoCompleteTextView = 16974151
+Widget.DeviceDefault.Button = 16974145
+Widget.DeviceDefault.Button.Borderless = 16974188
+Widget.DeviceDefault.Button.Borderless.Small = 16974149
+Widget.DeviceDefault.Button.Inset = 16974147
+Widget.DeviceDefault.Button.Small = 16974146
+Widget.DeviceDefault.Button.Toggle = 16974148
+Widget.DeviceDefault.CalendarView = 16974190
+Widget.DeviceDefault.CheckedTextView = 16974299
+Widget.DeviceDefault.CompoundButton.CheckBox = 16974152
+Widget.DeviceDefault.CompoundButton.RadioButton = 16974169
+Widget.DeviceDefault.CompoundButton.Star = 16974173
+Widget.DeviceDefault.DatePicker = 16974191
+Widget.DeviceDefault.DropDownItem = 16974177
+Widget.DeviceDefault.DropDownItem.Spinner = 16974178
+Widget.DeviceDefault.EditText = 16974154
+Widget.DeviceDefault.ExpandableListView = 16974155
+Widget.DeviceDefault.FastScroll = 16974313
+Widget.DeviceDefault.GridView = 16974156
+Widget.DeviceDefault.HorizontalScrollView = 16974171
+Widget.DeviceDefault.ImageButton = 16974157
+Widget.DeviceDefault.Light = 16974196
+Widget.DeviceDefault.Light.ActionBar = 16974243
+Widget.DeviceDefault.Light.ActionBar.Solid = 16974247
+Widget.DeviceDefault.Light.ActionBar.Solid.Inverse = 16974248
+Widget.DeviceDefault.Light.ActionBar.TabBar = 16974246
+Widget.DeviceDefault.Light.ActionBar.TabBar.Inverse = 16974249
+Widget.DeviceDefault.Light.ActionBar.TabText = 16974245
+Widget.DeviceDefault.Light.ActionBar.TabText.Inverse = 16974251
+Widget.DeviceDefault.Light.ActionBar.TabView = 16974244
+Widget.DeviceDefault.Light.ActionBar.TabView.Inverse = 16974250
+Widget.DeviceDefault.Light.ActionButton = 16974239
+Widget.DeviceDefault.Light.ActionButton.CloseMode = 16974242
+Widget.DeviceDefault.Light.ActionButton.Overflow = 16974240
+Widget.DeviceDefault.Light.ActionMode = 16974241
+Widget.DeviceDefault.Light.ActionMode.Inverse = 16974252
+Widget.DeviceDefault.Light.AutoCompleteTextView = 16974203
+Widget.DeviceDefault.Light.Button = 16974197
+Widget.DeviceDefault.Light.Button.Borderless.Small = 16974201
+Widget.DeviceDefault.Light.Button.Inset = 16974199
+Widget.DeviceDefault.Light.Button.Small = 16974198
+Widget.DeviceDefault.Light.Button.Toggle = 16974200
+Widget.DeviceDefault.Light.CalendarView = 16974238
+Widget.DeviceDefault.Light.CheckedTextView = 16974300
+Widget.DeviceDefault.Light.CompoundButton.CheckBox = 16974204
+Widget.DeviceDefault.Light.CompoundButton.RadioButton = 16974224
+Widget.DeviceDefault.Light.CompoundButton.Star = 16974228
+Widget.DeviceDefault.Light.DropDownItem = 16974232
+Widget.DeviceDefault.Light.DropDownItem.Spinner = 16974233
+Widget.DeviceDefault.Light.EditText = 16974206
+Widget.DeviceDefault.Light.ExpandableListView = 16974207
+Widget.DeviceDefault.Light.FastScroll = 16974315
+Widget.DeviceDefault.Light.GridView = 16974208
+Widget.DeviceDefault.Light.HorizontalScrollView = 16974226
+Widget.DeviceDefault.Light.ImageButton = 16974209
+Widget.DeviceDefault.Light.ListPopupWindow = 16974235
+Widget.DeviceDefault.Light.ListView = 16974210
+Widget.DeviceDefault.Light.ListView.DropDown = 16974205
+Widget.DeviceDefault.Light.MediaRouteButton = 16974296
+Widget.DeviceDefault.Light.PopupMenu = 16974236
+Widget.DeviceDefault.Light.PopupWindow = 16974211
+Widget.DeviceDefault.Light.ProgressBar = 16974212
+Widget.DeviceDefault.Light.ProgressBar.Horizontal = 16974213
+Widget.DeviceDefault.Light.ProgressBar.Inverse = 16974217
+Widget.DeviceDefault.Light.ProgressBar.Large = 16974216
+Widget.DeviceDefault.Light.ProgressBar.Large.Inverse = 16974219
+Widget.DeviceDefault.Light.ProgressBar.Small = 16974214
+Widget.DeviceDefault.Light.ProgressBar.Small.Inverse = 16974218
+Widget.DeviceDefault.Light.ProgressBar.Small.Title = 16974215
+Widget.DeviceDefault.Light.RatingBar = 16974221
+Widget.DeviceDefault.Light.RatingBar.Indicator = 16974222
+Widget.DeviceDefault.Light.RatingBar.Small = 16974223
+Widget.DeviceDefault.Light.ScrollView = 16974225
+Widget.DeviceDefault.Light.SeekBar = 16974220
+Widget.DeviceDefault.Light.Spinner = 16974227
+Widget.DeviceDefault.Light.StackView = 16974316
+Widget.DeviceDefault.Light.Tab = 16974237
+Widget.DeviceDefault.Light.TabWidget = 16974229
+Widget.DeviceDefault.Light.TextView = 16974202
+Widget.DeviceDefault.Light.TextView.SpinnerItem = 16974234
+Widget.DeviceDefault.Light.WebTextView = 16974230
+Widget.DeviceDefault.Light.WebView = 16974231
+Widget.DeviceDefault.ListPopupWindow = 16974180
+Widget.DeviceDefault.ListView = 16974158
+Widget.DeviceDefault.ListView.DropDown = 16974153
+Widget.DeviceDefault.MediaRouteButton = 16974295
+Widget.DeviceDefault.PopupMenu = 16974181
+Widget.DeviceDefault.PopupWindow = 16974159
+Widget.DeviceDefault.ProgressBar = 16974160
+Widget.DeviceDefault.ProgressBar.Horizontal = 16974161
+Widget.DeviceDefault.ProgressBar.Large = 16974164
+Widget.DeviceDefault.ProgressBar.Small = 16974162
+Widget.DeviceDefault.ProgressBar.Small.Title = 16974163
+Widget.DeviceDefault.RatingBar = 16974166
+Widget.DeviceDefault.RatingBar.Indicator = 16974167
+Widget.DeviceDefault.RatingBar.Small = 16974168
+Widget.DeviceDefault.ScrollView = 16974170
+Widget.DeviceDefault.SeekBar = 16974165
+Widget.DeviceDefault.Spinner = 16974172
+Widget.DeviceDefault.StackView = 16974314
+Widget.DeviceDefault.Tab = 16974189
+Widget.DeviceDefault.TabWidget = 16974174
+Widget.DeviceDefault.TextView = 16974150
+Widget.DeviceDefault.TextView.SpinnerItem = 16974179
+Widget.DeviceDefault.WebTextView = 16974175
+Widget.DeviceDefault.WebView = 16974176
+Widget.DropDownItem = 16973867
+Widget.DropDownItem.Spinner = 16973868
+Widget.EditText = 16973859
+Widget.ExpandableListView = 16973860
+Widget.FastScroll = 16974309
+Widget.FragmentBreadCrumbs = 16973961
+Widget.Gallery = 16973877
+Widget.GridView = 16973874
+Widget.Holo = 16973962
+Widget.Holo.ActionBar = 16974004
+Widget.Holo.ActionBar.Solid = 16974113
+Widget.Holo.ActionBar.TabBar = 16974071
+Widget.Holo.ActionBar.TabText = 16974070
+Widget.Holo.ActionBar.TabView = 16974069
+Widget.Holo.ActionButton = 16973999
+Widget.Holo.ActionButton.CloseMode = 16974003
+Widget.Holo.ActionButton.Overflow = 16974000
+Widget.Holo.ActionButton.TextButton = 16974001
+Widget.Holo.ActionMode = 16974002
+Widget.Holo.AutoCompleteTextView = 16973968
+Widget.Holo.Button = 16973963
+Widget.Holo.Button.Borderless = 16974050
+Widget.Holo.Button.Borderless.Small = 16974106
+Widget.Holo.Button.Inset = 16973965
+Widget.Holo.Button.Small = 16973964
+Widget.Holo.Button.Toggle = 16973966
+Widget.Holo.CalendarView = 16974060
+Widget.Holo.CheckedTextView = 16974297
+Widget.Holo.CompoundButton.CheckBox = 16973969
+Widget.Holo.CompoundButton.RadioButton = 16973986
+Widget.Holo.CompoundButton.Star = 16973990
+Widget.Holo.DatePicker = 16974063
+Widget.Holo.DropDownItem = 16973994
+Widget.Holo.DropDownItem.Spinner = 16973995
+Widget.Holo.EditText = 16973971
+Widget.Holo.ExpandableListView = 16973972
+Widget.Holo.GridView = 16973973
+Widget.Holo.HorizontalScrollView = 16973988
+Widget.Holo.ImageButton = 16973974
+Widget.Holo.Light = 16974005
+Widget.Holo.Light.ActionBar = 16974049
+Widget.Holo.Light.ActionBar.Solid = 16974114
+Widget.Holo.Light.ActionBar.Solid.Inverse = 16974115
+Widget.Holo.Light.ActionBar.TabBar = 16974074
+Widget.Holo.Light.ActionBar.TabBar.Inverse = 16974116
+Widget.Holo.Light.ActionBar.TabText = 16974073
+Widget.Holo.Light.ActionBar.TabText.Inverse = 16974118
+Widget.Holo.Light.ActionBar.TabView = 16974072
+Widget.Holo.Light.ActionBar.TabView.Inverse = 16974117
+Widget.Holo.Light.ActionButton = 16974045
+Widget.Holo.Light.ActionButton.CloseMode = 16974048
+Widget.Holo.Light.ActionButton.Overflow = 16974046
+Widget.Holo.Light.ActionMode = 16974047
+Widget.Holo.Light.ActionMode.Inverse = 16974119
+Widget.Holo.Light.AutoCompleteTextView = 16974011
+Widget.Holo.Light.Button = 16974006
+Widget.Holo.Light.Button.Borderless.Small = 16974107
+Widget.Holo.Light.Button.Inset = 16974008
+Widget.Holo.Light.Button.Small = 16974007
+Widget.Holo.Light.Button.Toggle = 16974009
+Widget.Holo.Light.CalendarView = 16974061
+Widget.Holo.Light.CheckedTextView = 16974298
+Widget.Holo.Light.CompoundButton.CheckBox = 16974012
+Widget.Holo.Light.CompoundButton.RadioButton = 16974032
+Widget.Holo.Light.CompoundButton.Star = 16974036
+Widget.Holo.Light.DropDownItem = 16974040
+Widget.Holo.Light.DropDownItem.Spinner = 16974041
+Widget.Holo.Light.EditText = 16974014
+Widget.Holo.Light.ExpandableListView = 16974015
+Widget.Holo.Light.GridView = 16974016
+Widget.Holo.Light.HorizontalScrollView = 16974034
+Widget.Holo.Light.ImageButton = 16974017
+Widget.Holo.Light.ListPopupWindow = 16974043
+Widget.Holo.Light.ListView = 16974018
+Widget.Holo.Light.ListView.DropDown = 16974013
+Widget.Holo.Light.MediaRouteButton = 16974294
+Widget.Holo.Light.PopupMenu = 16974044
+Widget.Holo.Light.PopupWindow = 16974019
+Widget.Holo.Light.ProgressBar = 16974020
+Widget.Holo.Light.ProgressBar.Horizontal = 16974021
+Widget.Holo.Light.ProgressBar.Inverse = 16974025
+Widget.Holo.Light.ProgressBar.Large = 16974024
+Widget.Holo.Light.ProgressBar.Large.Inverse = 16974027
+Widget.Holo.Light.ProgressBar.Small = 16974022
+Widget.Holo.Light.ProgressBar.Small.Inverse = 16974026
+Widget.Holo.Light.ProgressBar.Small.Title = 16974023
+Widget.Holo.Light.RatingBar = 16974029
+Widget.Holo.Light.RatingBar.Indicator = 16974030
+Widget.Holo.Light.RatingBar.Small = 16974031
+Widget.Holo.Light.ScrollView = 16974033
+Widget.Holo.Light.SeekBar = 16974028
+Widget.Holo.Light.Spinner = 16974035
+Widget.Holo.Light.Tab = 16974052
+Widget.Holo.Light.TabWidget = 16974037
+Widget.Holo.Light.TextView = 16974010
+Widget.Holo.Light.TextView.SpinnerItem = 16974042
+Widget.Holo.Light.WebTextView = 16974038
+Widget.Holo.Light.WebView = 16974039
+Widget.Holo.ListPopupWindow = 16973997
+Widget.Holo.ListView = 16973975
+Widget.Holo.ListView.DropDown = 16973970
+Widget.Holo.MediaRouteButton = 16974293
+Widget.Holo.PopupMenu = 16973998
+Widget.Holo.PopupWindow = 16973976
+Widget.Holo.ProgressBar = 16973977
+Widget.Holo.ProgressBar.Horizontal = 16973978
+Widget.Holo.ProgressBar.Large = 16973981
+Widget.Holo.ProgressBar.Small = 16973979
+Widget.Holo.ProgressBar.Small.Title = 16973980
+Widget.Holo.RatingBar = 16973983
+Widget.Holo.RatingBar.Indicator = 16973984
+Widget.Holo.RatingBar.Small = 16973985
+Widget.Holo.ScrollView = 16973987
+Widget.Holo.SeekBar = 16973982
+Widget.Holo.Spinner = 16973989
+Widget.Holo.Tab = 16974051
+Widget.Holo.TabWidget = 16973991
+Widget.Holo.TextView = 16973967
+Widget.Holo.TextView.SpinnerItem = 16973996
+Widget.Holo.WebTextView = 16973992
+Widget.Holo.WebView = 16973993
+Widget.ImageButton = 16973862
+Widget.ImageWell = 16973861
+Widget.KeyboardView = 16973911
+Widget.ListPopupWindow = 16973957
+Widget.ListView = 16973870
+Widget.ListView.DropDown = 16973872
+Widget.ListView.Menu = 16973873
+Widget.ListView.White = 16973871
+Widget.Material = 16974413
+Widget.Material.ActionBar = 16974414
+Widget.Material.ActionBar.Solid = 16974415
+Widget.Material.ActionBar.TabBar = 16974416
+Widget.Material.ActionBar.TabText = 16974417
+Widget.Material.ActionBar.TabView = 16974418
+Widget.Material.ActionButton = 16974419
+Widget.Material.ActionButton.CloseMode = 16974420
+Widget.Material.ActionButton.Overflow = 16974421
+Widget.Material.ActionMode = 16974422
+Widget.Material.AutoCompleteTextView = 16974423
+Widget.Material.Button = 16974424
+Widget.Material.ButtonBar = 16974431
+Widget.Material.ButtonBar.AlertDialog = 16974432
+Widget.Material.Button.Borderless = 16974425
+Widget.Material.Button.Borderless.Colored = 16974426
+Widget.Material.Button.Borderless.Small = 16974427
+Widget.Material.Button.Inset = 16974428
+Widget.Material.Button.Small = 16974429
+Widget.Material.Button.Toggle = 16974430
+Widget.Material.CalendarView = 16974433
+Widget.Material.CheckedTextView = 16974434
+Widget.Material.CompoundButton.CheckBox = 16974435
+Widget.Material.CompoundButton.RadioButton = 16974436
+Widget.Material.CompoundButton.Star = 16974437
+Widget.Material.DatePicker = 16974438
+Widget.Material.DropDownItem = 16974439
+Widget.Material.DropDownItem.Spinner = 16974440
+Widget.Material.EditText = 16974441
+Widget.Material.ExpandableListView = 16974442
+Widget.Material.FastScroll = 16974443
+Widget.Material.GridView = 16974444
+Widget.Material.HorizontalScrollView = 16974445
+Widget.Material.ImageButton = 16974446
+Widget.Material.Light = 16974478
+Widget.Material.Light.ActionBar = 16974479
+Widget.Material.Light.ActionBar.Solid = 16974480
+Widget.Material.Light.ActionBar.TabBar = 16974481
+Widget.Material.Light.ActionBar.TabText = 16974482
+Widget.Material.Light.ActionBar.TabView = 16974483
+Widget.Material.Light.ActionButton = 16974484
+Widget.Material.Light.ActionButton.CloseMode = 16974485
+Widget.Material.Light.ActionButton.Overflow = 16974486
+Widget.Material.Light.ActionMode = 16974487
+Widget.Material.Light.AutoCompleteTextView = 16974488
+Widget.Material.Light.Button = 16974489
+Widget.Material.Light.ButtonBar = 16974496
+Widget.Material.Light.ButtonBar.AlertDialog = 16974497
+Widget.Material.Light.Button.Borderless = 16974490
+Widget.Material.Light.Button.Borderless.Colored = 16974491
+Widget.Material.Light.Button.Borderless.Small = 16974492
+Widget.Material.Light.Button.Inset = 16974493
+Widget.Material.Light.Button.Small = 16974494
+Widget.Material.Light.Button.Toggle = 16974495
+Widget.Material.Light.CalendarView = 16974498
+Widget.Material.Light.CheckedTextView = 16974499
+Widget.Material.Light.CompoundButton.CheckBox = 16974500
+Widget.Material.Light.CompoundButton.RadioButton = 16974501
+Widget.Material.Light.CompoundButton.Star = 16974502
+Widget.Material.Light.DatePicker = 16974503
+Widget.Material.Light.DropDownItem = 16974504
+Widget.Material.Light.DropDownItem.Spinner = 16974505
+Widget.Material.Light.EditText = 16974506
+Widget.Material.Light.ExpandableListView = 16974507
+Widget.Material.Light.FastScroll = 16974508
+Widget.Material.Light.GridView = 16974509
+Widget.Material.Light.HorizontalScrollView = 16974510
+Widget.Material.Light.ImageButton = 16974511
+Widget.Material.Light.ListPopupWindow = 16974512
+Widget.Material.Light.ListView = 16974513
+Widget.Material.Light.ListView.DropDown = 16974514
+Widget.Material.Light.MediaRouteButton = 16974515
+Widget.Material.Light.PopupMenu = 16974516
+Widget.Material.Light.PopupMenu.Overflow = 16974517
+Widget.Material.Light.PopupWindow = 16974518
+Widget.Material.Light.ProgressBar = 16974519
+Widget.Material.Light.ProgressBar.Horizontal = 16974520
+Widget.Material.Light.ProgressBar.Inverse = 16974521
+Widget.Material.Light.ProgressBar.Large = 16974522
+Widget.Material.Light.ProgressBar.Large.Inverse = 16974523
+Widget.Material.Light.ProgressBar.Small = 16974524
+Widget.Material.Light.ProgressBar.Small.Inverse = 16974525
+Widget.Material.Light.ProgressBar.Small.Title = 16974526
+Widget.Material.Light.RatingBar = 16974527
+Widget.Material.Light.RatingBar.Indicator = 16974528
+Widget.Material.Light.RatingBar.Small = 16974529
+Widget.Material.Light.ScrollView = 16974530
+Widget.Material.Light.SearchView = 16974531
+Widget.Material.Light.SeekBar = 16974532
+Widget.Material.Light.SegmentedButton = 16974533
+Widget.Material.Light.Spinner = 16974535
+Widget.Material.Light.Spinner.Underlined = 16974536
+Widget.Material.Light.StackView = 16974534
+Widget.Material.Light.Tab = 16974537
+Widget.Material.Light.TabWidget = 16974538
+Widget.Material.Light.TextView = 16974539
+Widget.Material.Light.TextView.SpinnerItem = 16974540
+Widget.Material.Light.TimePicker = 16974541
+Widget.Material.Light.WebTextView = 16974542
+Widget.Material.Light.WebView = 16974543
+Widget.Material.ListPopupWindow = 16974447
+Widget.Material.ListView = 16974448
+Widget.Material.ListView.DropDown = 16974449
+Widget.Material.MediaRouteButton = 16974450
+Widget.Material.PopupMenu = 16974451
+Widget.Material.PopupMenu.Overflow = 16974452
+Widget.Material.PopupWindow = 16974453
+Widget.Material.ProgressBar = 16974454
+Widget.Material.ProgressBar.Horizontal = 16974455
+Widget.Material.ProgressBar.Large = 16974456
+Widget.Material.ProgressBar.Small = 16974457
+Widget.Material.ProgressBar.Small.Title = 16974458
+Widget.Material.RatingBar = 16974459
+Widget.Material.RatingBar.Indicator = 16974460
+Widget.Material.RatingBar.Small = 16974461
+Widget.Material.ScrollView = 16974462
+Widget.Material.SearchView = 16974463
+Widget.Material.SeekBar = 16974464
+Widget.Material.SegmentedButton = 16974465
+Widget.Material.Spinner = 16974467
+Widget.Material.Spinner.Underlined = 16974468
+Widget.Material.StackView = 16974466
+Widget.Material.Tab = 16974469
+Widget.Material.TabWidget = 16974470
+Widget.Material.TextView = 16974471
+Widget.Material.TextView.SpinnerItem = 16974472
+Widget.Material.TimePicker = 16974473
+Widget.Material.Toolbar = 16974474
+Widget.Material.Toolbar.Button.Navigation = 16974475
+Widget.Material.WebTextView = 16974476
+Widget.Material.WebView = 16974477
+Widget.PopupMenu = 16973958
+Widget.PopupWindow = 16973878
+Widget.ProgressBar = 16973852
+Widget.ProgressBar.Horizontal = 16973855
+Widget.ProgressBar.Inverse = 16973915
+Widget.ProgressBar.Large = 16973853
+Widget.ProgressBar.Large.Inverse = 16973916
+Widget.ProgressBar.Small = 16973854
+Widget.ProgressBar.Small.Inverse = 16973917
+Widget.RatingBar = 16973857
+Widget.ScrollView = 16973869
+Widget.SeekBar = 16973856
+Widget.Spinner = 16973864
+Widget.Spinner.DropDown = 16973955
+Widget.StackView = 16974310
+Widget.TabWidget = 16973876
+Widget.TextView = 16973858
+Widget.TextView.PopupMenu = 16973865
+Widget.TextView.SpinnerItem = 16973866
+Widget.Toolbar = 16974311
+Widget.Toolbar.Button.Navigation = 16974312
+Widget.WebView = 16973875
\ No newline at end of file
diff --git a/modules/apk-parser/src/main/resources/r_values.ini b/modules/apk-parser/src/main/resources/r_values.ini
new file mode 100644
index 00000000..b0a50936
--- /dev/null
+++ b/modules/apk-parser/src/main/resources/r_values.ini
@@ -0,0 +1,1207 @@
+theme=16842752
+label=16842753
+icon=16842754
+name=16842755
+manageSpaceActivity=16842756
+allowClearUserData=16842757
+permission=16842758
+readPermission=16842759
+writePermission=16842760
+protectionLevel=16842761
+permissionGroup=16842762
+sharedUserId=16842763
+hasCode=16842764
+persistent=16842765
+enabled=16842766
+debuggable=16842767
+exported=16842768
+process=16842769
+taskAffinity=16842770
+multiprocess=16842771
+finishOnTaskLaunch=16842772
+clearTaskOnLaunch=16842773
+stateNotNeeded=16842774
+excludeFromRecents=16842775
+authorities=16842776
+syncable=16842777
+initOrder=16842778
+grantUriPermissions=16842779
+priority=16842780
+launchMode=16842781
+screenOrientation=16842782
+configChanges=16842783
+description=16842784
+targetPackage=16842785
+handleProfiling=16842786
+functionalTest=16842787
+value=16842788
+resource=16842789
+mimeType=16842790
+scheme=16842791
+host=16842792
+port=16842793
+path=16842794
+pathPrefix=16842795
+pathPattern=16842796
+action=16842797
+data=16842798
+targetClass=16842799
+colorForeground=16842800
+colorBackground=16842801
+backgroundDimAmount=16842802
+disabledAlpha=16842803
+textAppearance=16842804
+textAppearanceInverse=16842805
+textColorPrimary=16842806
+textColorPrimaryDisableOnly=16842807
+textColorSecondary=16842808
+textColorPrimaryInverse=16842809
+textColorSecondaryInverse=16842810
+textColorPrimaryNoDisable=16842811
+textColorSecondaryNoDisable=16842812
+textColorPrimaryInverseNoDisable=16842813
+textColorSecondaryInverseNoDisable=16842814
+textColorHintInverse=16842815
+textAppearanceLarge=16842816
+textAppearanceMedium=16842817
+textAppearanceSmall=16842818
+textAppearanceLargeInverse=16842819
+textAppearanceMediumInverse=16842820
+textAppearanceSmallInverse=16842821
+textCheckMark=16842822
+textCheckMarkInverse=16842823
+buttonStyle=16842824
+buttonStyleSmall=16842825
+buttonStyleInset=16842826
+buttonStyleToggle=16842827
+galleryItemBackground=16842828
+listPreferredItemHeight=16842829
+expandableListPreferredItemPaddingLeft=16842830
+expandableListPreferredChildPaddingLeft=16842831
+expandableListPreferredItemIndicatorLeft=16842832
+expandableListPreferredItemIndicatorRight=16842833
+expandableListPreferredChildIndicatorLeft=16842834
+expandableListPreferredChildIndicatorRight=16842835
+windowBackground=16842836
+windowFrame=16842837
+windowNoTitle=16842838
+windowIsFloating=16842839
+windowIsTranslucent=16842840
+windowContentOverlay=16842841
+windowTitleSize=16842842
+windowTitleStyle=16842843
+windowTitleBackgroundStyle=16842844
+alertDialogStyle=16842845
+panelBackground=16842846
+panelFullBackground=16842847
+panelColorForeground=16842848
+panelColorBackground=16842849
+panelTextAppearance=16842850
+scrollbarSize=16842851
+scrollbarThumbHorizontal=16842852
+scrollbarThumbVertical=16842853
+scrollbarTrackHorizontal=16842854
+scrollbarTrackVertical=16842855
+scrollbarAlwaysDrawHorizontalTrack=16842856
+scrollbarAlwaysDrawVerticalTrack=16842857
+absListViewStyle=16842858
+autoCompleteTextViewStyle=16842859
+checkboxStyle=16842860
+dropDownListViewStyle=16842861
+editTextStyle=16842862
+expandableListViewStyle=16842863
+galleryStyle=16842864
+gridViewStyle=16842865
+imageButtonStyle=16842866
+imageWellStyle=16842867
+listViewStyle=16842868
+listViewWhiteStyle=16842869
+popupWindowStyle=16842870
+progressBarStyle=16842871
+progressBarStyleHorizontal=16842872
+progressBarStyleSmall=16842873
+progressBarStyleLarge=16842874
+seekBarStyle=16842875
+ratingBarStyle=16842876
+ratingBarStyleSmall=16842877
+radioButtonStyle=16842878
+scrollbarStyle=16842879
+scrollViewStyle=16842880
+spinnerStyle=16842881
+starStyle=16842882
+tabWidgetStyle=16842883
+textViewStyle=16842884
+webViewStyle=16842885
+dropDownItemStyle=16842886
+spinnerDropDownItemStyle=16842887
+dropDownHintAppearance=16842888
+spinnerItemStyle=16842889
+mapViewStyle=16842890
+preferenceScreenStyle=16842891
+preferenceCategoryStyle=16842892
+preferenceInformationStyle=16842893
+preferenceStyle=16842894
+checkBoxPreferenceStyle=16842895
+yesNoPreferenceStyle=16842896
+dialogPreferenceStyle=16842897
+editTextPreferenceStyle=16842898
+ringtonePreferenceStyle=16842899
+preferenceLayoutChild=16842900
+textSize=16842901
+typeface=16842902
+textStyle=16842903
+textColor=16842904
+textColorHighlight=16842905
+textColorHint=16842906
+textColorLink=16842907
+state_focused=16842908
+state_window_focused=16842909
+state_enabled=16842910
+state_checkable=16842911
+state_checked=16842912
+state_selected=16842913
+state_active=16842914
+state_single=16842915
+state_first=16842916
+state_middle=16842917
+state_last=16842918
+state_pressed=16842919
+state_expanded=16842920
+state_empty=16842921
+state_above_anchor=16842922
+ellipsize=16842923
+x=16842924
+y=16842925
+windowAnimationStyle=16842926
+gravity=16842927
+autoLink=16842928
+linksClickable=16842929
+entries=16842930
+layout_gravity=16842931
+windowEnterAnimation=16842932
+windowExitAnimation=16842933
+windowShowAnimation=16842934
+windowHideAnimation=16842935
+activityOpenEnterAnimation=16842936
+activityOpenExitAnimation=16842937
+activityCloseEnterAnimation=16842938
+activityCloseExitAnimation=16842939
+taskOpenEnterAnimation=16842940
+taskOpenExitAnimation=16842941
+taskCloseEnterAnimation=16842942
+taskCloseExitAnimation=16842943
+taskToFrontEnterAnimation=16842944
+taskToFrontExitAnimation=16842945
+taskToBackEnterAnimation=16842946
+taskToBackExitAnimation=16842947
+orientation=16842948
+keycode=16842949
+fullDark=16842950
+topDark=16842951
+centerDark=16842952
+bottomDark=16842953
+fullBright=16842954
+topBright=16842955
+centerBright=16842956
+bottomBright=16842957
+bottomMedium=16842958
+centerMedium=16842959
+id=16842960
+tag=16842961
+scrollX=16842962
+scrollY=16842963
+background=16842964
+padding=16842965
+paddingLeft=16842966
+paddingTop=16842967
+paddingRight=16842968
+paddingBottom=16842969
+focusable=16842970
+focusableInTouchMode=16842971
+visibility=16842972
+fitsSystemWindows=16842973
+scrollbars=16842974
+fadingEdge=16842975
+fadingEdgeLength=16842976
+nextFocusLeft=16842977
+nextFocusRight=16842978
+nextFocusUp=16842979
+nextFocusDown=16842980
+clickable=16842981
+longClickable=16842982
+saveEnabled=16842983
+drawingCacheQuality=16842984
+duplicateParentState=16842985
+clipChildren=16842986
+clipToPadding=16842987
+layoutAnimation=16842988
+animationCache=16842989
+persistentDrawingCache=16842990
+alwaysDrawnWithCache=16842991
+addStatesFromChildren=16842992
+descendantFocusability=16842993
+layout=16842994
+inflatedId=16842995
+layout_width=16842996
+layout_height=16842997
+layout_margin=16842998
+layout_marginLeft=16842999
+layout_marginTop=16843000
+layout_marginRight=16843001
+layout_marginBottom=16843002
+listSelector=16843003
+drawSelectorOnTop=16843004
+stackFromBottom=16843005
+scrollingCache=16843006
+textFilterEnabled=16843007
+transcriptMode=16843008
+cacheColorHint=16843009
+dial=16843010
+hand_hour=16843011
+hand_minute=16843012
+format=16843013
+checked=16843014
+button=16843015
+checkMark=16843016
+foreground=16843017
+measureAllChildren=16843018
+groupIndicator=16843019
+childIndicator=16843020
+indicatorLeft=16843021
+indicatorRight=16843022
+childIndicatorLeft=16843023
+childIndicatorRight=16843024
+childDivider=16843025
+animationDuration=16843026
+spacing=16843027
+horizontalSpacing=16843028
+verticalSpacing=16843029
+stretchMode=16843030
+columnWidth=16843031
+numColumns=16843032
+src=16843033
+antialias=16843034
+filter=16843035
+dither=16843036
+scaleType=16843037
+adjustViewBounds=16843038
+maxWidth=16843039
+maxHeight=16843040
+tint=16843041
+baselineAlignBottom=16843042
+cropToPadding=16843043
+textOn=16843044
+textOff=16843045
+baselineAligned=16843046
+baselineAlignedChildIndex=16843047
+weightSum=16843048
+divider=16843049
+dividerHeight=16843050
+choiceMode=16843051
+itemTextAppearance=16843052
+horizontalDivider=16843053
+verticalDivider=16843054
+headerBackground=16843055
+itemBackground=16843056
+itemIconDisabledAlpha=16843057
+rowHeight=16843058
+maxRows=16843059
+maxItemsPerRow=16843060
+moreIcon=16843061
+max=16843062
+progress=16843063
+secondaryProgress=16843064
+indeterminate=16843065
+indeterminateOnly=16843066
+indeterminateDrawable=16843067
+progressDrawable=16843068
+indeterminateDuration=16843069
+indeterminateBehavior=16843070
+minWidth=16843071
+minHeight=16843072
+interpolator=16843073
+thumb=16843074
+thumbOffset=16843075
+numStars=16843076
+rating=16843077
+stepSize=16843078
+isIndicator=16843079
+checkedButton=16843080
+stretchColumns=16843081
+shrinkColumns=16843082
+collapseColumns=16843083
+layout_column=16843084
+layout_span=16843085
+bufferType=16843086
+text=16843087
+hint=16843088
+textScaleX=16843089
+cursorVisible=16843090
+maxLines=16843091
+lines=16843092
+height=16843093
+minLines=16843094
+maxEms=16843095
+ems=16843096
+width=16843097
+minEms=16843098
+scrollHorizontally=16843099
+password=16843100
+singleLine=16843101
+selectAllOnFocus=16843102
+includeFontPadding=16843103
+maxLength=16843104
+shadowColor=16843105
+shadowDx=16843106
+shadowDy=16843107
+shadowRadius=16843108
+numeric=16843109
+digits=16843110
+phoneNumber=16843111
+inputMethod=16843112
+capitalize=16843113
+autoText=16843114
+editable=16843115
+freezesText=16843116
+drawableTop=16843117
+drawableBottom=16843118
+drawableLeft=16843119
+drawableRight=16843120
+drawablePadding=16843121
+completionHint=16843122
+completionHintView=16843123
+completionThreshold=16843124
+dropDownSelector=16843125
+popupBackground=16843126
+inAnimation=16843127
+outAnimation=16843128
+flipInterval=16843129
+fillViewport=16843130
+prompt=16843131
+startYear=16843132
+endYear=16843133
+mode=16843134
+layout_x=16843135
+layout_y=16843136
+layout_weight=16843137
+layout_toLeftOf=16843138
+layout_toRightOf=16843139
+layout_above=16843140
+layout_below=16843141
+layout_alignBaseline=16843142
+layout_alignLeft=16843143
+layout_alignTop=16843144
+layout_alignRight=16843145
+layout_alignBottom=16843146
+layout_alignParentLeft=16843147
+layout_alignParentTop=16843148
+layout_alignParentRight=16843149
+layout_alignParentBottom=16843150
+layout_centerInParent=16843151
+layout_centerHorizontal=16843152
+layout_centerVertical=16843153
+layout_alignWithParentIfMissing=16843154
+layout_scale=16843155
+visible=16843156
+variablePadding=16843157
+constantSize=16843158
+oneshot=16843159
+duration=16843160
+drawable=16843161
+shape=16843162
+innerRadiusRatio=16843163
+thicknessRatio=16843164
+startColor=16843165
+endColor=16843166
+useLevel=16843167
+angle=16843168
+type=16843169
+centerX=16843170
+centerY=16843171
+gradientRadius=16843172
+color=16843173
+dashWidth=16843174
+dashGap=16843175
+radius=16843176
+topLeftRadius=16843177
+topRightRadius=16843178
+bottomLeftRadius=16843179
+bottomRightRadius=16843180
+left=16843181
+top=16843182
+right=16843183
+bottom=16843184
+minLevel=16843185
+maxLevel=16843186
+fromDegrees=16843187
+toDegrees=16843188
+pivotX=16843189
+pivotY=16843190
+insetLeft=16843191
+insetRight=16843192
+insetTop=16843193
+insetBottom=16843194
+shareInterpolator=16843195
+fillBefore=16843196
+fillAfter=16843197
+startOffset=16843198
+repeatCount=16843199
+repeatMode=16843200
+zAdjustment=16843201
+fromXScale=16843202
+toXScale=16843203
+fromYScale=16843204
+toYScale=16843205
+fromXDelta=16843206
+toXDelta=16843207
+fromYDelta=16843208
+toYDelta=16843209
+fromAlpha=16843210
+toAlpha=16843211
+delay=16843212
+animation=16843213
+animationOrder=16843214
+columnDelay=16843215
+rowDelay=16843216
+direction=16843217
+directionPriority=16843218
+factor=16843219
+cycles=16843220
+searchMode=16843221
+searchSuggestAuthority=16843222
+searchSuggestPath=16843223
+searchSuggestSelection=16843224
+searchSuggestIntentAction=16843225
+searchSuggestIntentData=16843226
+queryActionMsg=16843227
+suggestActionMsg=16843228
+suggestActionMsgColumn=16843229
+menuCategory=16843230
+orderInCategory=16843231
+checkableBehavior=16843232
+title=16843233
+titleCondensed=16843234
+alphabeticShortcut=16843235
+numericShortcut=16843236
+checkable=16843237
+selectable=16843238
+orderingFromXml=16843239
+key=16843240
+summary=16843241
+order=16843242
+widgetLayout=16843243
+dependency=16843244
+defaultValue=16843245
+shouldDisableView=16843246
+summaryOn=16843247
+summaryOff=16843248
+disableDependentsState=16843249
+dialogTitle=16843250
+dialogMessage=16843251
+dialogIcon=16843252
+positiveButtonText=16843253
+negativeButtonText=16843254
+dialogLayout=16843255
+entryValues=16843256
+ringtoneType=16843257
+showDefault=16843258
+showSilent=16843259
+scaleWidth=16843260
+scaleHeight=16843261
+scaleGravity=16843262
+ignoreGravity=16843263
+foregroundGravity=16843264
+tileMode=16843265
+targetActivity=16843266
+alwaysRetainTaskState=16843267
+allowTaskReparenting=16843268
+searchButtonText=16843269
+colorForegroundInverse=16843270
+textAppearanceButton=16843271
+listSeparatorTextViewStyle=16843272
+streamType=16843273
+clipOrientation=16843274
+centerColor=16843275
+minSdkVersion=16843276
+windowFullscreen=16843277
+unselectedAlpha=16843278
+progressBarStyleSmallTitle=16843279
+ratingBarStyleIndicator=16843280
+apiKey=16843281
+textColorTertiary=16843282
+textColorTertiaryInverse=16843283
+listDivider=16843284
+soundEffectsEnabled=16843285
+keepScreenOn=16843286
+lineSpacingExtra=16843287
+lineSpacingMultiplier=16843288
+listChoiceIndicatorSingle=16843289
+listChoiceIndicatorMultiple=16843290
+versionCode=16843291
+versionName=16843292
+marqueeRepeatLimit=16843293
+windowNoDisplay=16843294
+backgroundDimEnabled=16843295
+inputType=16843296
+isDefault=16843297
+windowDisablePreview=16843298
+privateImeOptions=16843299
+editorExtras=16843300
+settingsActivity=16843301
+fastScrollEnabled=16843302
+reqTouchScreen=16843303
+reqKeyboardType=16843304
+reqHardKeyboard=16843305
+reqNavigation=16843306
+windowSoftInputMode=16843307
+imeFullscreenBackground=16843308
+noHistory=16843309
+headerDividersEnabled=16843310
+footerDividersEnabled=16843311
+candidatesTextStyleSpans=16843312
+smoothScrollbar=16843313
+reqFiveWayNav=16843314
+keyBackground=16843315
+keyTextSize=16843316
+labelTextSize=16843317
+keyTextColor=16843318
+keyPreviewLayout=16843319
+keyPreviewOffset=16843320
+keyPreviewHeight=16843321
+verticalCorrection=16843322
+popupLayout=16843323
+state_long_pressable=16843324
+keyWidth=16843325
+keyHeight=16843326
+horizontalGap=16843327
+verticalGap=16843328
+rowEdgeFlags=16843329
+codes=16843330
+popupKeyboard=16843331
+popupCharacters=16843332
+keyEdgeFlags=16843333
+isModifier=16843334
+isSticky=16843335
+isRepeatable=16843336
+iconPreview=16843337
+keyOutputText=16843338
+keyLabel=16843339
+keyIcon=16843340
+keyboardMode=16843341
+isScrollContainer=16843342
+fillEnabled=16843343
+updatePeriodMillis=16843344
+initialLayout=16843345
+voiceSearchMode=16843346
+voiceLanguageModel=16843347
+voicePromptText=16843348
+voiceLanguage=16843349
+voiceMaxResults=16843350
+bottomOffset=16843351
+topOffset=16843352
+allowSingleTap=16843353
+handle=16843354
+content=16843355
+animateOnClick=16843356
+configure=16843357
+hapticFeedbackEnabled=16843358
+innerRadius=16843359
+thickness=16843360
+sharedUserLabel=16843361
+dropDownWidth=16843362
+dropDownAnchor=16843363
+imeOptions=16843364
+imeActionLabel=16843365
+imeActionId=16843366
+imeExtractEnterAnimation=16843368
+imeExtractExitAnimation=16843369
+tension=16843370
+extraTension=16843371
+anyDensity=16843372
+searchSuggestThreshold=16843373
+includeInGlobalSearch=16843374
+onClick=16843375
+targetSdkVersion=16843376
+maxSdkVersion=16843377
+testOnly=16843378
+contentDescription=16843379
+gestureStrokeWidth=16843380
+gestureColor=16843381
+uncertainGestureColor=16843382
+fadeOffset=16843383
+fadeDuration=16843384
+gestureStrokeType=16843385
+gestureStrokeLengthThreshold=16843386
+gestureStrokeSquarenessThreshold=16843387
+gestureStrokeAngleThreshold=16843388
+eventsInterceptionEnabled=16843389
+fadeEnabled=16843390
+backupAgent=16843391
+allowBackup=16843392
+glEsVersion=16843393
+queryAfterZeroResults=16843394
+dropDownHeight=16843395
+smallScreens=16843396
+normalScreens=16843397
+largeScreens=16843398
+progressBarStyleInverse=16843399
+progressBarStyleSmallInverse=16843400
+progressBarStyleLargeInverse=16843401
+searchSettingsDescription=16843402
+textColorPrimaryInverseDisableOnly=16843403
+autoUrlDetect=16843404
+resizeable=16843405
+required=16843406
+accountType=16843407
+contentAuthority=16843408
+userVisible=16843409
+windowShowWallpaper=16843410
+wallpaperOpenEnterAnimation=16843411
+wallpaperOpenExitAnimation=16843412
+wallpaperCloseEnterAnimation=16843413
+wallpaperCloseExitAnimation=16843414
+wallpaperIntraOpenEnterAnimation=16843415
+wallpaperIntraOpenExitAnimation=16843416
+wallpaperIntraCloseEnterAnimation=16843417
+wallpaperIntraCloseExitAnimation=16843418
+supportsUploading=16843419
+killAfterRestore=16843420
+restoreNeedsApplication=16843421
+smallIcon=16843422
+accountPreferences=16843423
+textAppearanceSearchResultSubtitle=16843424
+textAppearanceSearchResultTitle=16843425
+summaryColumn=16843426
+detailColumn=16843427
+detailSocialSummary=16843428
+thumbnail=16843429
+detachWallpaper=16843430
+finishOnCloseSystemDialogs=16843431
+scrollbarFadeDuration=16843432
+scrollbarDefaultDelayBeforeFade=16843433
+fadeScrollbars=16843434
+colorBackgroundCacheHint=16843435
+dropDownHorizontalOffset=16843436
+dropDownVerticalOffset=16843437
+quickContactBadgeStyleWindowSmall=16843438
+quickContactBadgeStyleWindowMedium=16843439
+quickContactBadgeStyleWindowLarge=16843440
+quickContactBadgeStyleSmallWindowSmall=16843441
+quickContactBadgeStyleSmallWindowMedium=16843442
+quickContactBadgeStyleSmallWindowLarge=16843443
+author=16843444
+autoStart=16843445
+expandableListViewWhiteStyle=16843446
+installLocation=16843447
+vmSafeMode=16843448
+webTextViewStyle=16843449
+restoreAnyVersion=16843450
+tabStripLeft=16843451
+tabStripRight=16843452
+tabStripEnabled=16843453
+logo=16843454
+xlargeScreens=16843455
+immersive=16843456
+overScrollMode=16843457
+overScrollHeader=16843458
+overScrollFooter=16843459
+filterTouchesWhenObscured=16843460
+textSelectHandleLeft=16843461
+textSelectHandleRight=16843462
+textSelectHandle=16843463
+textSelectHandleWindowStyle=16843464
+popupAnimationStyle=16843465
+screenSize=16843466
+screenDensity=16843467
+allContactsName=16843468
+windowActionBar=16843469
+actionBarStyle=16843470
+navigationMode=16843471
+displayOptions=16843472
+subtitle=16843473
+customNavigationLayout=16843474
+hardwareAccelerated=16843475
+measureWithLargestChild=16843476
+animateFirstView=16843477
+dropDownSpinnerStyle=16843478
+actionDropDownStyle=16843479
+actionButtonStyle=16843480
+showAsAction=16843481
+previewImage=16843482
+actionModeBackground=16843483
+actionModeCloseDrawable=16843484
+windowActionModeOverlay=16843485
+valueFrom=16843486
+valueTo=16843487
+valueType=16843488
+propertyName=16843489
+ordering=16843490
+fragment=16843491
+windowActionBarOverlay=16843492
+fragmentOpenEnterAnimation=16843493
+fragmentOpenExitAnimation=16843494
+fragmentCloseEnterAnimation=16843495
+fragmentCloseExitAnimation=16843496
+fragmentFadeEnterAnimation=16843497
+fragmentFadeExitAnimation=16843498
+actionBarSize=16843499
+imeSubtypeLocale=16843500
+imeSubtypeMode=16843501
+imeSubtypeExtraValue=16843502
+splitMotionEvents=16843503
+listChoiceBackgroundIndicator=16843504
+spinnerMode=16843505
+animateLayoutChanges=16843506
+actionBarTabStyle=16843507
+actionBarTabBarStyle=16843508
+actionBarTabTextStyle=16843509
+actionOverflowButtonStyle=16843510
+actionModeCloseButtonStyle=16843511
+titleTextStyle=16843512
+subtitleTextStyle=16843513
+iconifiedByDefault=16843514
+actionLayout=16843515
+actionViewClass=16843516
+activatedBackgroundIndicator=16843517
+state_activated=16843518
+listPopupWindowStyle=16843519
+popupMenuStyle=16843520
+textAppearanceLargePopupMenu=16843521
+textAppearanceSmallPopupMenu=16843522
+breadCrumbTitle=16843523
+breadCrumbShortTitle=16843524
+listDividerAlertDialog=16843525
+textColorAlertDialogListItem=16843526
+loopViews=16843527
+dialogTheme=16843528
+alertDialogTheme=16843529
+dividerVertical=16843530
+homeAsUpIndicator=16843531
+enterFadeDuration=16843532
+exitFadeDuration=16843533
+selectableItemBackground=16843534
+autoAdvanceViewId=16843535
+useIntrinsicSizeAsMinimum=16843536
+actionModeCutDrawable=16843537
+actionModeCopyDrawable=16843538
+actionModePasteDrawable=16843539
+textEditPasteWindowLayout=16843540
+textEditNoPasteWindowLayout=16843541
+textIsSelectable=16843542
+windowEnableSplitTouch=16843543
+indeterminateProgressStyle=16843544
+progressBarPadding=16843545
+animationResolution=16843546
+state_accelerated=16843547
+baseline=16843548
+homeLayout=16843549
+opacity=16843550
+alpha=16843551
+transformPivotX=16843552
+transformPivotY=16843553
+translationX=16843554
+translationY=16843555
+scaleX=16843556
+scaleY=16843557
+rotation=16843558
+rotationX=16843559
+rotationY=16843560
+showDividers=16843561
+dividerPadding=16843562
+borderlessButtonStyle=16843563
+dividerHorizontal=16843564
+itemPadding=16843565
+buttonBarStyle=16843566
+buttonBarButtonStyle=16843567
+segmentedButtonStyle=16843568
+staticWallpaperPreview=16843569
+allowParallelSyncs=16843570
+isAlwaysSyncable=16843571
+verticalScrollbarPosition=16843572
+fastScrollAlwaysVisible=16843573
+fastScrollThumbDrawable=16843574
+fastScrollPreviewBackgroundLeft=16843575
+fastScrollPreviewBackgroundRight=16843576
+fastScrollTrackDrawable=16843577
+fastScrollOverlayPosition=16843578
+customTokens=16843579
+nextFocusForward=16843580
+firstDayOfWeek=16843581
+showWeekNumber=16843582
+minDate=16843583
+maxDate=16843584
+shownWeekCount=16843585
+selectedWeekBackgroundColor=16843586
+focusedMonthDateColor=16843587
+unfocusedMonthDateColor=16843588
+weekNumberColor=16843589
+weekSeparatorLineColor=16843590
+selectedDateVerticalBar=16843591
+weekDayTextAppearance=16843592
+dateTextAppearance=16843593
+solidColor=16843594
+spinnersShown=16843595
+calendarViewShown=16843596
+state_multiline=16843597
+detailsElementBackground=16843598
+textColorHighlightInverse=16843599
+textColorLinkInverse=16843600
+editTextColor=16843601
+editTextBackground=16843602
+horizontalScrollViewStyle=16843603
+layerType=16843604
+alertDialogIcon=16843605
+windowMinWidthMajor=16843606
+windowMinWidthMinor=16843607
+queryHint=16843608
+fastScrollTextColor=16843609
+largeHeap=16843610
+windowCloseOnTouchOutside=16843611
+datePickerStyle=16843612
+calendarViewStyle=16843613
+textEditSidePasteWindowLayout=16843614
+textEditSideNoPasteWindowLayout=16843615
+actionMenuTextAppearance=16843616
+actionMenuTextColor=16843617
+textCursorDrawable=16843618
+resizeMode=16843619
+requiresSmallestWidthDp=16843620
+compatibleWidthLimitDp=16843621
+largestWidthLimitDp=16843622
+state_hovered=16843623
+state_drag_can_accept=16843624
+state_drag_hovered=16843625
+stopWithTask=16843626
+switchTextOn=16843627
+switchTextOff=16843628
+switchPreferenceStyle=16843629
+switchTextAppearance=16843630
+track=16843631
+switchMinWidth=16843632
+switchPadding=16843633
+thumbTextPadding=16843634
+textSuggestionsWindowStyle=16843635
+textEditSuggestionItemLayout=16843636
+rowCount=16843637
+rowOrderPreserved=16843638
+columnCount=16843639
+columnOrderPreserved=16843640
+useDefaultMargins=16843641
+alignmentMode=16843642
+layout_row=16843643
+layout_rowSpan=16843644
+layout_columnSpan=16843645
+actionModeSelectAllDrawable=16843646
+isAuxiliary=16843647
+accessibilityEventTypes=16843648
+packageNames=16843649
+accessibilityFeedbackType=16843650
+notificationTimeout=16843651
+accessibilityFlags=16843652
+canRetrieveWindowContent=16843653
+listPreferredItemHeightLarge=16843654
+listPreferredItemHeightSmall=16843655
+actionBarSplitStyle=16843656
+actionProviderClass=16843657
+backgroundStacked=16843658
+backgroundSplit=16843659
+textAllCaps=16843660
+colorPressedHighlight=16843661
+colorLongPressedHighlight=16843662
+colorFocusedHighlight=16843663
+colorActivatedHighlight=16843664
+colorMultiSelectHighlight=16843665
+drawableStart=16843666
+drawableEnd=16843667
+actionModeStyle=16843668
+minResizeWidth=16843669
+minResizeHeight=16843670
+actionBarWidgetTheme=16843671
+uiOptions=16843672
+subtypeLocale=16843673
+subtypeExtraValue=16843674
+actionBarDivider=16843675
+actionBarItemBackground=16843676
+actionModeSplitBackground=16843677
+textAppearanceListItem=16843678
+textAppearanceListItemSmall=16843679
+targetDescriptions=16843680
+directionDescriptions=16843681
+overridesImplicitlyEnabledSubtype=16843682
+listPreferredItemPaddingLeft=16843683
+listPreferredItemPaddingRight=16843684
+requiresFadingEdge=16843685
+publicKey=16843686
+parentActivityName=16843687
+isolatedProcess=16843689
+importantForAccessibility=16843690
+keyboardLayout=16843691
+fontFamily=16843692
+mediaRouteButtonStyle=16843693
+mediaRouteTypes=16843694
+supportsRtl=16843695
+textDirection=16843696
+textAlignment=16843697
+layoutDirection=16843698
+paddingStart=16843699
+paddingEnd=16843700
+layout_marginStart=16843701
+layout_marginEnd=16843702
+layout_toStartOf=16843703
+layout_toEndOf=16843704
+layout_alignStart=16843705
+layout_alignEnd=16843706
+layout_alignParentStart=16843707
+layout_alignParentEnd=16843708
+listPreferredItemPaddingStart=16843709
+listPreferredItemPaddingEnd=16843710
+singleUser=16843711
+presentationTheme=16843712
+subtypeId=16843713
+initialKeyguardLayout=16843714
+widgetCategory=16843716
+permissionGroupFlags=16843717
+labelFor=16843718
+permissionFlags=16843719
+checkedTextViewStyle=16843720
+showOnLockScreen=16843721
+format12Hour=16843722
+format24Hour=16843723
+timeZone=16843724
+mipMap=16843725
+mirrorForRtl=16843726
+windowOverscan=16843727
+requiredForAllUsers=16843728
+indicatorStart=16843729
+indicatorEnd=16843730
+childIndicatorStart=16843731
+childIndicatorEnd=16843732
+restrictedAccountType=16843733
+requiredAccountType=16843734
+canRequestTouchExplorationMode=16843735
+canRequestEnhancedWebAccessibility=16843736
+canRequestFilterKeyEvents=16843737
+layoutMode=16843738
+keySet=16843739
+targetId=16843740
+fromScene=16843741
+toScene=16843742
+transition=16843743
+transitionOrdering=16843744
+fadingMode=16843745
+startDelay=16843746
+ssp=16843747
+sspPrefix=16843748
+sspPattern=16843749
+addPrintersActivity=16843750
+vendor=16843751
+category=16843752
+isAsciiCapable=16843753
+autoMirrored=16843754
+supportsSwitchingToNextInputMethod=16843755
+requireDeviceUnlock=16843756
+apduServiceBanner=16843757
+accessibilityLiveRegion=16843758
+windowTranslucentStatus=16843759
+windowTranslucentNavigation=16843760
+advancedPrintOptionsActivity=16843761
+banner=16843762
+windowSwipeToDismiss=16843763
+isGame=16843764
+allowEmbedded=16843765
+setupActivity=16843766
+fastScrollStyle=16843767
+windowContentTransitions=16843768
+windowContentTransitionManager=16843769
+translationZ=16843770
+tintMode=16843771
+controlX1=16843772
+controlY1=16843773
+controlX2=16843774
+controlY2=16843775
+transitionName=16843776
+transitionGroup=16843777
+viewportWidth=16843778
+viewportHeight=16843779
+fillColor=16843780
+pathData=16843781
+strokeColor=16843782
+strokeWidth=16843783
+trimPathStart=16843784
+trimPathEnd=16843785
+trimPathOffset=16843786
+strokeLineCap=16843787
+strokeLineJoin=16843788
+strokeMiterLimit=16843789
+colorControlNormal=16843817
+colorControlActivated=16843818
+colorButtonNormal=16843819
+colorControlHighlight=16843820
+persistableMode=16843821
+titleTextAppearance=16843822
+subtitleTextAppearance=16843823
+slideEdge=16843824
+actionBarTheme=16843825
+textAppearanceListItemSecondary=16843826
+colorPrimary=16843827
+colorPrimaryDark=16843828
+colorAccent=16843829
+nestedScrollingEnabled=16843830
+windowEnterTransition=16843831
+windowExitTransition=16843832
+windowSharedElementEnterTransition=16843833
+windowSharedElementExitTransition=16843834
+windowAllowReturnTransitionOverlap=16843835
+windowAllowEnterTransitionOverlap=16843836
+sessionService=16843837
+stackViewStyle=16843838
+switchStyle=16843839
+elevation=16843840
+excludeId=16843841
+excludeClass=16843842
+hideOnContentScroll=16843843
+actionOverflowMenuStyle=16843844
+documentLaunchMode=16843845
+maxRecents=16843846
+autoRemoveFromRecents=16843847
+stateListAnimator=16843848
+toId=16843849
+fromId=16843850
+reversible=16843851
+splitTrack=16843852
+targetName=16843853
+excludeName=16843854
+matchOrder=16843855
+windowDrawsSystemBarBackgrounds=16843856
+statusBarColor=16843857
+navigationBarColor=16843858
+contentInsetStart=16843859
+contentInsetEnd=16843860
+contentInsetLeft=16843861
+contentInsetRight=16843862
+paddingMode=16843863
+layout_rowWeight=16843864
+layout_columnWeight=16843865
+translateX=16843866
+translateY=16843867
+selectableItemBackgroundBorderless=16843868
+elegantTextHeight=16843869
+searchKeyphraseId=16843870
+searchKeyphrase=16843871
+searchKeyphraseSupportedLocales=16843872
+windowTransitionBackgroundFadeDuration=16843873
+overlapAnchor=16843874
+progressTint=16843875
+progressTintMode=16843876
+progressBackgroundTint=16843877
+progressBackgroundTintMode=16843878
+secondaryProgressTint=16843879
+secondaryProgressTintMode=16843880
+indeterminateTint=16843881
+indeterminateTintMode=16843882
+backgroundTint=16843883
+backgroundTintMode=16843884
+foregroundTint=16843885
+foregroundTintMode=16843886
+buttonTint=16843887
+buttonTintMode=16843888
+thumbTint=16843889
+thumbTintMode=16843890
+fullBackupOnly=16843891
+propertyXName=16843892
+propertyYName=16843893
+relinquishTaskIdentity=16843894
+tileModeX=16843895
+tileModeY=16843896
+actionModeShareDrawable=16843897
+actionModeFindDrawable=16843898
+actionModeWebSearchDrawable=16843899
+transitionVisibilityMode=16843900
+minimumHorizontalAngle=16843901
+minimumVerticalAngle=16843902
+maximumAngle=16843903
+searchViewStyle=16843904
+closeIcon=16843905
+goIcon=16843906
+searchIcon=16843907
+voiceIcon=16843908
+commitIcon=16843909
+suggestionRowLayout=16843910
+queryBackground=16843911
+submitBackground=16843912
+buttonBarPositiveButtonStyle=16843913
+buttonBarNeutralButtonStyle=16843914
+buttonBarNegativeButtonStyle=16843915
+popupElevation=16843916
+actionBarPopupTheme=16843917
+multiArch=16843918
+touchscreenBlocksFocus=16843919
+windowElevation=16843920
+launchTaskBehindTargetAnimation=16843921
+launchTaskBehindSourceAnimation=16843922
+restrictionType=16843923
+dayOfWeekBackground=16843924
+dayOfWeekTextAppearance=16843925
+headerMonthTextAppearance=16843926
+headerDayOfMonthTextAppearance=16843927
+headerYearTextAppearance=16843928
+yearListItemTextAppearance=16843929
+yearListSelectorColor=16843930
+calendarTextColor=16843931
+recognitionService=16843932
+timePickerStyle=16843933
+timePickerDialogTheme=16843934
+headerTimeTextAppearance=16843935
+headerAmPmTextAppearance=16843936
+numbersTextColor=16843937
+numbersBackgroundColor=16843938
+numbersSelectorColor=16843939
+amPmTextColor=16843940
+amPmBackgroundColor=16843941
+searchKeyphraseRecognitionFlags=16843942
+checkMarkTint=16843943
+checkMarkTintMode=16843944
+popupTheme=16843945
+toolbarStyle=16843946
+windowClipToOutline=16843947
+datePickerDialogTheme=16843948
+showText=16843949
+windowReturnTransition=16843950
+windowReenterTransition=16843951
+windowSharedElementReturnTransition=16843952
+windowSharedElementReenterTransition=16843953
+resumeWhilePausing=16843954
+datePickerMode=16843955
+timePickerMode=16843956
+inset=16843957
+letterSpacing=16843958
+fontFeatureSettings=16843959
+outlineProvider=16843960
+contentAgeHint=16843961
+country=16843962
+windowSharedElementsUseOverlay=16843963
+reparent=16843964
+reparentWithOverlay=16843965
+ambientShadowAlpha=16843966
+spotShadowAlpha=16843967
+navigationIcon=16843968
+navigationContentDescription=16843969
+fragmentExitTransition=16843970
+fragmentEnterTransition=16843971
+fragmentSharedElementEnterTransition=16843972
+fragmentReturnTransition=16843973
+fragmentSharedElementReturnTransition=16843974
+fragmentReenterTransition=16843975
+fragmentAllowEnterTransitionOverlap=16843976
+fragmentAllowReturnTransitionOverlap=16843977
+patternPathData=16843978
+strokeAlpha=16843979
+fillAlpha=16843980
+windowActivityTransitions=16843981
+colorEdgeEffect=16843982
+resizeClip=16843983
+collapseContentDescription=16843984
+accessibilityTraversalBefore=16843985
+accessibilityTraversalAfter=16843986
+dialogPreferredPadding=16843987
+searchHintIcon=16843988
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 7ed9b4d7..7277762e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,6 +28,7 @@
private val modules = listOf(
"jieba-analysis",
"apk-signer",
+ "apk-parser",
"color-picker",
)
diff --git a/version.properties b/version.properties
index 4e0460ff..f9046bd9 100644
--- a/version.properties
+++ b/version.properties
@@ -1,5 +1,5 @@
-#Sat Apr 19 00:16:16 CST 2025
-BUILD_TIME=1744992976260
+#Sat Apr 19 01:36:17 CST 2025
+BUILD_TIME=1744997777676
COMPILE_SDK_VERSION=35
JAVA_VERSION=23
JAVA_VERSION_MIN_RADICAL=0
@@ -17,6 +17,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=35
TARGET_SDK_VERSION_INRT=29
-VERSION_BUILD=3149
+VERSION_BUILD=3150
VERSION_NAME=6.6.3 Alpha
VSCODE_EXT_REQUIRED_VERSION=1.0.8