result = new ArrayList<>(declaredFields.length);
+ for (Field field : declaredFields) {
+ 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());
+ }
+
+ AnnotatedField annotatedField;
+ try {
+ annotatedField = new AnnotatedField(container, field, annotation);
+ } catch (Asn1EncodingException e) {
+ throw new Asn1EncodingException(
+ "Invalid ASN.1 annotation on "
+ + containerClass.getName() + "." + field.getName(),
+ e);
+ }
+ result.add(annotatedField);
+ }
+ return result;
+ }
+
+ private static byte[] toInteger(int value) {
+ return toInteger((long) value);
+ }
+
+ private static byte[] toInteger(long value) {
+ return toInteger(BigInteger.valueOf(value));
+ }
+
+ private static byte[] toInteger(BigInteger value) {
+ return createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
+ value.toByteArray());
+ }
+
+ private static byte[] toOid(String oid) throws Asn1EncodingException {
+ ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
+ String[] nodes = oid.split("\\.");
+ if (nodes.length < 2) {
+ throw new Asn1EncodingException(
+ "OBJECT IDENTIFIER must contain at least two nodes: " + oid);
+ }
+ int firstNode;
+ try {
+ firstNode = Integer.parseInt(nodes[0]);
+ } catch (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);
+ }
+
+ int secondNode;
+ try {
+ secondNode = Integer.parseInt(nodes[1]);
+ } catch (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);
+ }
+ 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++) {
+ String nodeString = nodes[i];
+ int node;
+ try {
+ node = Integer.parseInt(nodeString);
+ } catch (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 createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
+ encodedValue.toByteArray());
+ }
+
+ private static Object getMemberFieldValue(Object obj, Field field)
+ throws Asn1EncodingException {
+ try {
+ return field.get(obj);
+ } catch (ReflectiveOperationException e) {
+ throw new Asn1EncodingException(
+ "Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
+ }
+ }
+
+ private static final class AnnotatedField {
+
+ private final Field mField;
+ private final Object mObject;
+ private final Asn1Field mAnnotation;
+ private final Asn1Type mDataType;
+ private final Asn1Type mElementDataType;
+ private final Asn1TagClass mTagClass;
+ private final int mDerTagClass;
+ private final int mDerTagNumber;
+ private final Asn1Tagging mTagging;
+ private final boolean mOptional;
+
+ public AnnotatedField(Object obj, Field field, Asn1Field annotation)
+ throws Asn1EncodingException {
+ mObject = obj;
+ mField = field;
+ mAnnotation = annotation;
+ mDataType = annotation.type();
+ mElementDataType = annotation.elementType();
+
+ Asn1TagClass tagClass = annotation.cls();
+ if (tagClass == Asn1TagClass.AUTOMATIC) {
+ if (annotation.tagNumber() != -1) {
+ tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
+ } else {
+ tagClass = Asn1TagClass.UNIVERSAL;
+ }
+ }
+ mTagClass = tagClass;
+ mDerTagClass = BerEncoding.getTagClass(mTagClass);
+
+ int tagNumber;
+ if (annotation.tagNumber() != -1) {
+ tagNumber = annotation.tagNumber();
+ } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
+ tagNumber = -1;
+ } else {
+ tagNumber = BerEncoding.getTagNumber(mDataType);
+ }
+ mDerTagNumber = tagNumber;
+
+ mTagging = annotation.tagging();
+ if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
+ && (annotation.tagNumber() == -1)) {
+ throw new Asn1EncodingException(
+ "Tag number must be specified when tagging mode is " + mTagging);
+ }
+
+ mOptional = annotation.optional();
+ }
+
+ public Field getField() {
+ return mField;
+ }
+
+ public Asn1Field getAnnotation() {
+ return mAnnotation;
+ }
+
+ public byte[] toDer() throws Asn1EncodingException {
+ Object fieldValue = getMemberFieldValue(mObject, mField);
+ if (fieldValue == null) {
+ if (mOptional) {
+ return null;
+ }
+ throw new Asn1EncodingException("Required field not set");
+ }
+
+ byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
+ switch (mTagging) {
+ case NORMAL:
+ return encoded;
+ case EXPLICIT:
+ return createTag(mDerTagClass, true, mDerTagNumber, encoded);
+ case IMPLICIT:
+ int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
+ if (originalTagNumber == 0x1f) {
+ throw new Asn1EncodingException("High-tag-number form not supported");
+ }
+ if (mDerTagNumber >= 0x1f) {
+ throw new Asn1EncodingException(
+ "Unsupported high tag number: " + mDerTagNumber);
+ }
+ encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
+ encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
+ return encoded;
+ default:
+ throw new RuntimeException("Unknown tagging mode: " + mTagging);
+ }
+ }
+ }
+
+ private static byte[] createTag(
+ int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
+ if (tagNumber >= 0x1f) {
+ throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
+ }
+ // tag class & number fit into the first byte
+ byte firstIdentifierByte
+ = (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
+
+ int contentsLength = 0;
+ for (byte[] c : contents) {
+ contentsLength += c.length;
+ }
+ int contentsPosInResult;
+ 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 (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(Object source, Asn1Type targetType, Asn1Type targetElementType)
+ throws Asn1EncodingException {
+ Class> sourceType = source.getClass();
+ if (Asn1OpaqueObject.class.equals(sourceType)) {
+ ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
+ byte[] result = new byte[buf.remaining()];
+ buf.get(result);
+ return result;
+ }
+
+ if ((targetType == null) || (targetType == Asn1Type.ANY)) {
+ return encode(source);
+ }
+
+ switch (targetType) {
+ case OCTET_STRING:
+ byte[] value = null;
+ if (source instanceof ByteBuffer) {
+ 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 createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL,
+ false,
+ BerEncoding.TAG_NUMBER_OCTET_STRING,
+ value);
+ }
+ break;
+ case INTEGER:
+ if (source instanceof Integer) {
+ return toInteger((Integer) source);
+ } else if (source instanceof Long) {
+ return toInteger((Long) source);
+ } else if (source instanceof BigInteger) {
+ return toInteger((BigInteger) source);
+ }
+ break;
+ case OBJECT_IDENTIFIER:
+ if (source instanceof String) {
+ return toOid((String) source);
+ }
+ break;
+ case SEQUENCE: {
+ Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
+ return toSequence(source);
+ }
+ break;
+ }
+ case CHOICE: {
+ Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.CHOICE)) {
+ return toChoice(source);
+ }
+ break;
+ }
+ case SET_OF:
+ return toSetOf((Collection>) source, targetElementType);
+ default:
+ break;
+ }
+
+ throw new Asn1EncodingException(
+ "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java
new file mode 100644
index 0000000..da8ca0d
--- /dev/null
+++ b/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(String message) {
+ super(message);
+ }
+
+ public Asn1EncodingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java
new file mode 100644
index 0000000..479abe0
--- /dev/null
+++ b/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.
+ */
+ public int index() default 0;
+
+ public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
+
+ public Asn1Type type();
+
+ /**
+ * Tagging mode. Default: NORMAL.
+ */
+ public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
+
+ /**
+ * Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.
+ */
+ public int tagNumber() default -1;
+
+ /**
+ * {@code true} if this field is optional. Ignored for fields of CHOICE containers.
+ */
+ public boolean optional() default false;
+
+ /**
+ * Type of elements. Used only for SET_OF or SEQUENCE_OF.
+ */
+ public Asn1Type elementType() default Asn1Type.ANY;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java
new file mode 100644
index 0000000..16a3208
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java
@@ -0,0 +1,38 @@
+/*
+ * 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.nio.ByteBuffer;
+
+/**
+ * Opaque holder of encoded ASN.1 stuff.
+ */
+public class Asn1OpaqueObject {
+
+ private final ByteBuffer mEncoded;
+
+ public Asn1OpaqueObject(ByteBuffer encoded) {
+ mEncoded = encoded.slice();
+ }
+
+ public Asn1OpaqueObject(byte[] encoded) {
+ mEncoded = ByteBuffer.wrap(encoded);
+ }
+
+ public ByteBuffer getEncoded() {
+ return mEncoded.slice();
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.java
new file mode 100644
index 0000000..b3751b6
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+public enum Asn1TagClass {
+ UNIVERSAL,
+ APPLICATION,
+ CONTEXT_SPECIFIC,
+ PRIVATE,
+ /**
+ * Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class automatically.
+ */
+ AUTOMATIC,
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.java
new file mode 100644
index 0000000..16c21a0
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+public enum Asn1Tagging {
+ NORMAL,
+ EXPLICIT,
+ IMPLICIT,
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.java
new file mode 100644
index 0000000..ad43aae
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+public enum Asn1Type {
+ ANY,
+ CHOICE,
+ INTEGER,
+ OBJECT_IDENTIFIER,
+ OCTET_STRING,
+ SEQUENCE,
+ SEQUENCE_OF,
+ SET_OF,
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java
new file mode 100644
index 0000000..71a3ca5
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java
@@ -0,0 +1,111 @@
+/*
+ * 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.nio.ByteBuffer;
+
+/**
+ * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
+ */
+public class BerDataValue {
+
+ private final ByteBuffer mEncoded;
+ private final ByteBuffer mEncodedContents;
+ private final int mTagClass;
+ private final boolean mConstructed;
+ private final int mTagNumber;
+
+ BerDataValue(
+ ByteBuffer encoded,
+ ByteBuffer encodedContents,
+ int tagClass,
+ boolean constructed,
+ int tagNumber) {
+ mEncoded = encoded;
+ mEncodedContents = encodedContents;
+ mTagClass = tagClass;
+ mConstructed = constructed;
+ mTagNumber = tagNumber;
+ }
+
+ /**
+ * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS} constants.
+ */
+ public int getTagClass() {
+ return mTagClass;
+ }
+
+ /**
+ * 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 boolean isConstructed() {
+ return mConstructed;
+ }
+
+ /**
+ * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER} constants.
+ */
+ public int getTagNumber() {
+ return mTagNumber;
+ }
+
+ /**
+ * Returns the encoded form of this data value.
+ */
+ public ByteBuffer getEncoded() {
+ return mEncoded.slice();
+ }
+
+ /**
+ * Returns the encoded contents of this data value.
+ */
+ public ByteBuffer getEncodedContents() {
+ return mEncodedContents.slice();
+ }
+
+ /**
+ * Returns a new reader of the contents of this data value.
+ */
+ public BerDataValueReader contentsReader() {
+ return new ByteBufferBerDataValueReader(getEncodedContents());
+ }
+
+ /**
+ * Returns a new reader which returns just this data value. This may be useful for re-reading this value in different contexts.
+ */
+ public BerDataValueReader dataValueReader() {
+ return new ParsedValueReader(this);
+ }
+
+ private static final class ParsedValueReader implements BerDataValueReader {
+
+ private final BerDataValue mValue;
+ private boolean mValueOutput;
+
+ public ParsedValueReader(BerDataValue value) {
+ mValue = value;
+ }
+
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ if (mValueOutput) {
+ return null;
+ }
+ mValueOutput = true;
+ return mValue;
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java
new file mode 100644
index 0000000..686fabf
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.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.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(String message) {
+ super(message);
+ }
+
+ public BerDataValueFormatException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java
new file mode 100644
index 0000000..77b6ccb
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.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.ber;
+
+/**
+ * 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.
+ */
+ BerDataValue readDataValue() throws BerDataValueFormatException;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java
new file mode 100644
index 0000000..13f1991
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java
@@ -0,0 +1,185 @@
+/*
+ * 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(Asn1Type dataType) {
+ switch (dataType) {
+ case INTEGER:
+ return TAG_NUMBER_INTEGER;
+ case OBJECT_IDENTIFIER:
+ return TAG_NUMBER_OBJECT_IDENTIFIER;
+ case OCTET_STRING:
+ return TAG_NUMBER_OCTET_STRING;
+ case SET_OF:
+ return TAG_NUMBER_SET;
+ case SEQUENCE:
+ case SEQUENCE_OF:
+ return TAG_NUMBER_SEQUENCE;
+ default:
+ throw new IllegalArgumentException("Unsupported data type: " + dataType);
+ }
+ }
+
+ public static int getTagClass(Asn1TagClass tagClass) {
+ switch (tagClass) {
+ case APPLICATION:
+ return TAG_CLASS_APPLICATION;
+ case CONTEXT_SPECIFIC:
+ return TAG_CLASS_CONTEXT_SPECIFIC;
+ case PRIVATE:
+ return TAG_CLASS_PRIVATE;
+ case UNIVERSAL:
+ return TAG_CLASS_UNIVERSAL;
+ default:
+ throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
+ }
+ }
+
+ public static String tagClassToString(int typeClass) {
+ switch (typeClass) {
+ case TAG_CLASS_APPLICATION:
+ return "APPLICATION";
+ case TAG_CLASS_CONTEXT_SPECIFIC:
+ return "";
+ case TAG_CLASS_PRIVATE:
+ return "PRIVATE";
+ case TAG_CLASS_UNIVERSAL:
+ return "UNIVERSAL";
+ default:
+ throw new IllegalArgumentException("Unsupported type class: " + typeClass);
+ }
+ }
+
+ public static String tagClassAndNumberToString(int tagClass, int tagNumber) {
+ String classString = tagClassToString(tagClass);
+ String numberString = tagNumberToString(tagNumber);
+ return classString.isEmpty() ? numberString : classString + " " + numberString;
+ }
+
+ public static String tagNumberToString(int tagNumber) {
+ switch (tagNumber) {
+ case TAG_NUMBER_INTEGER:
+ return "INTEGER";
+ case TAG_NUMBER_OCTET_STRING:
+ return "OCTET STRING";
+ case TAG_NUMBER_NULL:
+ return "NULL";
+ case TAG_NUMBER_OBJECT_IDENTIFIER:
+ return "OBJECT IDENTIFIER";
+ case TAG_NUMBER_SEQUENCE:
+ return "SEQUENCE";
+ case 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(byte firstIdentifierByte) {
+ return (firstIdentifierByte & 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(byte firstIdentifierByte) {
+ return (firstIdentifierByte & 0xff) >> 6;
+ }
+
+ public static byte setTagClass(byte firstIdentifierByte, 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(byte firstIdentifierByte) {
+ return firstIdentifierByte & 0x1f;
+ }
+
+ public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) {
+ return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java
new file mode 100644
index 0000000..5326f43
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java
@@ -0,0 +1,207 @@
+/*
+ * 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.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(ByteBuffer buf) {
+ if (buf == null) {
+ throw new NullPointerException("buf == null");
+ }
+ mBuf = buf;
+ }
+
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ int startPosition = mBuf.position();
+ if (!mBuf.hasRemaining()) {
+ return null;
+ }
+ byte firstIdentifierByte = mBuf.get();
+ int tagNumber = readTagNumber(firstIdentifierByte);
+ boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
+
+ if (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Missing length");
+ }
+ int firstLengthByte = mBuf.get() & 0xff;
+ int contentsLength;
+ int contentsOffsetInTag;
+ if ((firstLengthByte & 0x80) == 0) {
+ // short form length
+ contentsLength = readShortFormLength(firstLengthByte);
+ contentsOffsetInTag = mBuf.position() - startPosition;
+ skipDefiniteLengthContents(contentsLength);
+ } else if (firstLengthByte != 0x80) {
+ // long form length
+ contentsLength = readLongFormLength(firstLengthByte);
+ contentsOffsetInTag = mBuf.position() - startPosition;
+ skipDefiniteLengthContents(contentsLength);
+ } else {
+ // indefinite length -- value ends with 0x00 0x00
+ contentsOffsetInTag = mBuf.position() - startPosition;
+ contentsLength
+ = constructed
+ ? skipConstructedIndefiniteLengthContents()
+ : skipPrimitiveIndefiniteLengthContents();
+ }
+
+ // Create the encoded data value ByteBuffer
+ int endPosition = mBuf.position();
+ mBuf.position(startPosition);
+ int bufOriginalLimit = mBuf.limit();
+ mBuf.limit(endPosition);
+ ByteBuffer encoded = mBuf.slice();
+ mBuf.position(mBuf.limit());
+ mBuf.limit(bufOriginalLimit);
+
+ // Create the encoded contents ByteBuffer
+ encoded.position(contentsOffsetInTag);
+ encoded.limit(contentsOffsetInTag + contentsLength);
+ ByteBuffer encodedContents = encoded.slice();
+ encoded.clear();
+
+ return new BerDataValue(
+ encoded,
+ encodedContents,
+ BerEncoding.getTagClass(firstIdentifierByte),
+ constructed,
+ tagNumber);
+ }
+
+ private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
+ 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 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 (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Truncated tag number");
+ }
+ b = 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(int firstLengthByte) {
+ return firstLengthByte & 0x7f;
+ }
+
+ private int readLongFormLength(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
+ 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 (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException("Truncated length");
+ }
+ int b = mBuf.get();
+ if (result > Integer.MAX_VALUE >>> 8) {
+ throw new BerDataValueFormatException("Length too large");
+ }
+ result <<= 8;
+ result |= b & 0xff;
+ }
+ return result;
+ }
+
+ private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
+ if (mBuf.remaining() < contentsLength) {
+ throw new BerDataValueFormatException(
+ "Truncated contents. Need: " + contentsLength + " bytes, available: "
+ + mBuf.remaining());
+ }
+ mBuf.position(mBuf.position() + contentsLength);
+ }
+
+ private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00
+ boolean prevZeroByte = false;
+ int bytesRead = 0;
+ while (true) {
+ if (!mBuf.hasRemaining()) {
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: " + bytesRead + " bytes read");
+
+ }
+ int b = 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.
+ int startPos = mBuf.position();
+ while (mBuf.hasRemaining()) {
+ // Check whether the 0x00 0x00 terminator is at current position
+ if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) {
+ int contentsLength = mBuf.position() - startPos;
+ mBuf.position(mBuf.position() + 2);
+ return contentsLength;
+ }
+ // No luck. This must be a BER-encoded data value -- skip over it by parsing it
+ readDataValue();
+ }
+
+ throw new BerDataValueFormatException(
+ "Truncated indefinite-length contents: "
+ + (mBuf.position() - startPos) + " bytes read");
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java
new file mode 100644
index 0000000..64c111c
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java
@@ -0,0 +1,314 @@
+/*
+ * 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;
+
+/**
+ * {@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(InputStream in) {
+ if (in == null) {
+ throw new NullPointerException("in == null");
+ }
+ mIn = in;
+ }
+
+ @Override
+ public BerDataValue readDataValue() throws BerDataValueFormatException {
+ return readDataValue(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.
+ */
+ @SuppressWarnings("resource")
+ private static BerDataValue readDataValue(InputStream input)
+ throws BerDataValueFormatException {
+ RecordingInputStream in = new RecordingInputStream(input);
+
+ try {
+ int firstIdentifierByte = in.read();
+ if (firstIdentifierByte == -1) {
+ // End of input
+ return null;
+ }
+ int tagNumber = readTagNumber(in, firstIdentifierByte);
+
+ int firstLengthByte = in.read();
+ if (firstLengthByte == -1) {
+ throw new BerDataValueFormatException("Missing length");
+ }
+
+ boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
+ int contentsLength;
+ int contentsOffsetInDataValue;
+ if ((firstLengthByte & 0x80) == 0) {
+ // short form length
+ contentsLength = readShortFormLength(firstLengthByte);
+ contentsOffsetInDataValue = in.getReadByteCount();
+ skipDefiniteLengthContents(in, contentsLength);
+ } else if ((firstLengthByte & 0xff) != 0x80) {
+ // long form length
+ contentsLength = readLongFormLength(in, firstLengthByte);
+ contentsOffsetInDataValue = in.getReadByteCount();
+ skipDefiniteLengthContents(in, contentsLength);
+ } else {
+ // indefinite length
+ contentsOffsetInDataValue = in.getReadByteCount();
+ contentsLength
+ = constructed
+ ? skipConstructedIndefiniteLengthContents(in)
+ : skipPrimitiveIndefiniteLengthContents(in);
+ }
+
+ byte[] encoded = in.getReadBytes();
+ ByteBuffer encodedContents
+ = ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
+ return new BerDataValue(
+ ByteBuffer.wrap(encoded),
+ encodedContents,
+ BerEncoding.getTagClass((byte) firstIdentifierByte),
+ constructed,
+ tagNumber);
+ } catch (IOException e) {
+ throw new BerDataValueFormatException("Failed to read data value", e);
+ }
+ }
+
+ private static int readTagNumber(InputStream in, int firstIdentifierByte)
+ throws IOException, BerDataValueFormatException {
+ int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
+ if (tagNumber == 0x1f) {
+ // high-tag-number form
+ return readHighTagNumber(in);
+ } else {
+ // low-tag-number form
+ return tagNumber;
+ }
+ }
+
+ private static int readHighTagNumber(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(int firstLengthByte) {
+ return firstLengthByte & 0x7f;
+ }
+
+ private static int readLongFormLength(InputStream in, 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
+ 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++) {
+ 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(InputStream in, int len)
+ throws IOException, BerDataValueFormatException {
+ long bytesRead = 0;
+ while (len > 0) {
+ 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(InputStream in)
+ throws IOException, BerDataValueFormatException {
+ // Contents are terminated by 0x00 0x00
+ boolean prevZeroByte = false;
+ int bytesRead = 0;
+ while (true) {
+ 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;
+ continue;
+ } else {
+ prevZeroByte = false;
+ }
+ }
+ }
+
+ private static int skipConstructedIndefiniteLengthContents(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.
+ 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.
+ BerDataValue dataValue = 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");
+ }
+ 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(InputStream in) {
+ mIn = in;
+ mBuf = new ByteArrayOutputStream();
+ }
+
+ public byte[] getReadBytes() {
+ return mBuf.toByteArray();
+ }
+
+ public int getReadByteCount() {
+ return mBuf.size();
+ }
+
+ @Override
+ public int read() throws IOException {
+ int b = mIn.read();
+ if (b != -1) {
+ mBuf.write(b);
+ }
+ return b;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int len = mIn.read(b);
+ if (len > 0) {
+ mBuf.write(b, 0, len);
+ }
+ return len;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ len = mIn.read(b, off, len);
+ if (len > 0) {
+ mBuf.write(b, off, len);
+ }
+ return len;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (n <= 0) {
+ return mIn.skip(n);
+ }
+
+ byte[] buf = new byte[4096];
+ int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
+ if (len > 0) {
+ mBuf.write(buf, 0, len);
+ }
+ return (len < 0) ? 0 : len;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return super.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ }
+
+ @Override
+ public synchronized void mark(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/src/main/java/net/dongliu/apk/parser/cert/package-info.java b/src/main/java/net/dongliu/apk/parser/cert/package-info.java
new file mode 100644
index 0000000..9319ec4
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Code in ths package copied from Android apksig source.
+ * Only for Internal Use.
+ */
+package net.dongliu.apk.parser.cert;
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java
new file mode 100644
index 0000000..bc91874
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java
@@ -0,0 +1,42 @@
+/*
+ * 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.OBJECT_IDENTIFIER)
+ public String algorithm;
+
+ @Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
+ public Asn1OpaqueObject parameters;
+
+ public AlgorithmIdentifier() {
+ }
+
+ public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
+ this.algorithm = algorithmOid;
+ this.parameters = parameters;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java
new file mode 100644
index 0000000..af00d4e
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.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.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.OBJECT_IDENTIFIER)
+ public String attrType;
+
+ @Asn1Field(index = 1, type = Asn1Type.SET_OF)
+ public List attrValues;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java
new file mode 100644
index 0000000..5871a54
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java
@@ -0,0 +1,31 @@
+/*
+ * 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.*;
+
+/**
+ * PKCS #7 {@code ContentInfo} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class ContentInfo {
+
+ @Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
+ public String contentType;
+
+ @Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
+ public Asn1OpaqueObject content;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java
new file mode 100644
index 0000000..e909160
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.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.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.OBJECT_IDENTIFIER)
+ public String contentType;
+
+ @Asn1Field(index = 1, type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0, optional = true)
+ public ByteBuffer content;
+
+ public EncapsulatedContentInfo() {
+ }
+
+ public EncapsulatedContentInfo(String contentTypeOid) {
+ contentType = contentTypeOid;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java
new file mode 100644
index 0000000..b479302
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.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.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(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
+ this.issuer = issuer;
+ this.certificateSerialNumber = certificateSerialNumber;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java
new file mode 100644
index 0000000..9841a0d
--- /dev/null
+++ b/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/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java
new file mode 100644
index 0000000..9fda067
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java
@@ -0,0 +1,54 @@
+/*
+ * 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.*;
+
+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.SET_OF)
+ public List digestAlgorithms;
+
+ @Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
+ public EncapsulatedContentInfo encapContentInfo;
+
+ @Asn1Field(
+ index = 3,
+ type = Asn1Type.SET_OF,
+ tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
+ optional = true)
+ public List certificates;
+
+ @Asn1Field(
+ index = 4,
+ type = Asn1Type.SET_OF,
+ tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
+ optional = true)
+ public List crls;
+
+ @Asn1Field(index = 5, type = Asn1Type.SET_OF)
+ public List signerInfos;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java
new file mode 100644
index 0000000..8233bed
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.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.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.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
+ public ByteBuffer subjectKeyIdentifier;
+
+ public SignerIdentifier() {
+ }
+
+ public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
+ this.issuerAndSerialNumber = issuerAndSerialNumber;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java
new file mode 100644
index 0000000..3d06966
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java
@@ -0,0 +1,57 @@
+/*
+ * 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.*;
+
+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.SET_OF,
+ tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
+ optional = true)
+ public Asn1OpaqueObject signedAttrs;
+
+ @Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
+ public AlgorithmIdentifier signatureAlgorithm;
+
+ @Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
+ public ByteBuffer signature;
+
+ @Asn1Field(
+ index = 6,
+ type = Asn1Type.SET_OF,
+ tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
+ optional = true)
+ public List unsignedAttrs;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/exception/ParserException.java b/src/main/java/net/dongliu/apk/parser/exception/ParserException.java
new file mode 100644
index 0000000..a268dae
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/exception/ParserException.java
@@ -0,0 +1,29 @@
+package net.dongliu.apk.parser.exception;
+
+/**
+ * throwed when parse failed.
+ *
+ * @author dongliu
+ */
+public class ParserException extends RuntimeException {
+
+ public ParserException(String msg) {
+ super(msg);
+ }
+
+ public ParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ParserException(Throwable cause) {
+ super(cause);
+ }
+
+ public ParserException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+ public ParserException() {
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java b/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java
new file mode 100644
index 0000000..32ef7fa
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/AdaptiveIconParser.java
@@ -0,0 +1,61 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.struct.xml.*;
+
+/**
+ * Parse adaptive icon xml file.
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class AdaptiveIconParser implements XmlStreamer {
+
+ private String foreground;
+ private String background;
+
+ public String getForeground() {
+ return foreground;
+ }
+
+ public String getBackground() {
+ return background;
+ }
+
+ @Override
+ public void onStartTag(XmlNodeStartTag xmlNodeStartTag) {
+ if (xmlNodeStartTag.getName().equals("background")) {
+ background = getDrawable(xmlNodeStartTag);
+ } else if (xmlNodeStartTag.getName().equals("foreground")) {
+ foreground = getDrawable(xmlNodeStartTag);
+ }
+ }
+
+ private String getDrawable(XmlNodeStartTag xmlNodeStartTag) {
+ Attributes attributes = xmlNodeStartTag.getAttributes();
+ for (Attribute attribute : attributes.values()) {
+ if (attribute.getName().equals("drawable")) {
+ return attribute.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onEndTag(XmlNodeEndTag xmlNodeEndTag) {
+
+ }
+
+ @Override
+ public void onCData(XmlCData xmlCData) {
+
+ }
+
+ @Override
+ public void onNamespaceStart(XmlNamespaceStartTag tag) {
+
+ }
+
+ @Override
+ public void onNamespaceEnd(XmlNamespaceEndTag tag) {
+
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java b/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java
new file mode 100644
index 0000000..244ba51
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/ApkMetaTranslator.java
@@ -0,0 +1,212 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.bean.*;
+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.*;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.*;
+
+/**
+ * trans binary xml to apk meta info
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class ApkMetaTranslator implements XmlStreamer {
+
+ private String[] tagStack = new String[100];
+ private int depth = 0;
+ private ApkMeta.Builder apkMetaBuilder = ApkMeta.newBuilder();
+ private List iconPaths = Collections.emptyList();
+
+ private ResourceTable resourceTable;
+ @Nullable
+ private Locale locale;
+
+ public ApkMetaTranslator(ResourceTable resourceTable, @Nullable Locale locale) {
+ this.resourceTable = Objects.requireNonNull(resourceTable);
+ this.locale = locale;
+ }
+
+ @Override
+ public void onStartTag(XmlNodeStartTag xmlNodeStartTag) {
+ Attributes attributes = xmlNodeStartTag.getAttributes();
+ switch (xmlNodeStartTag.getName()) {
+ case "application":
+ boolean debuggable = attributes.getBoolean("debuggable", false);
+ apkMetaBuilder.setDebuggable(debuggable);
+ String label = attributes.getString("label");
+ if (label != null) {
+ apkMetaBuilder.setLabel(label);
+ }
+ Attribute iconAttr = attributes.get("icon");
+ if (iconAttr != null) {
+ ResourceValue resourceValue = iconAttr.getTypedValue();
+ if (resourceValue instanceof ResourceValue.ReferenceResourceValue) {
+ long resourceId = ((ResourceValue.ReferenceResourceValue) resourceValue).getReferenceResourceId();
+ List resources = this.resourceTable.getResourcesById(resourceId);
+ if (!resources.isEmpty()) {
+ List icons = new ArrayList<>();
+ boolean hasDefault = false;
+ for (ResourceTable.Resource resource : resources) {
+ Type type = resource.getType();
+ ResourceEntry resourceEntry = resource.getResourceEntry();
+ String path = resourceEntry.toStringValue(resourceTable, locale);
+ if (type.getDensity() == Densities.DEFAULT) {
+ hasDefault = true;
+ apkMetaBuilder.setIcon(path);
+ }
+ IconPath iconPath = new IconPath(path, type.getDensity());
+ icons.add(iconPath);
+ }
+ if (!hasDefault) {
+ apkMetaBuilder.setIcon(icons.get(0).getPath());
+ }
+ this.iconPaths = icons;
+ }
+ } else {
+ String value = iconAttr.getValue();
+ if (value != null) {
+ apkMetaBuilder.setIcon(value);
+ IconPath iconPath = new IconPath(value, Densities.DEFAULT);
+ this.iconPaths = Collections.singletonList(iconPath);
+ }
+ }
+ }
+ break;
+ case "manifest":
+ apkMetaBuilder.setPackageName(attributes.getString("package"));
+ apkMetaBuilder.setVersionName(attributes.getString("versionName"));
+ apkMetaBuilder.setRevisionCode(attributes.getLong("revisionCode"));
+ apkMetaBuilder.setSharedUserId(attributes.getString("sharedUserId"));
+ apkMetaBuilder.setSharedUserLabel(attributes.getString("sharedUserLabel"));
+ apkMetaBuilder.setSplit(attributes.getString("split"));
+ apkMetaBuilder.setConfigForSplit(attributes.getString("configForSplit"));
+ apkMetaBuilder.setIsFeatureSplit(attributes.getBoolean("isFeatureSplit", false));
+ apkMetaBuilder.setIsSplitRequired(attributes.getBoolean("isSplitRequired", false));
+ apkMetaBuilder.setIsolatedSplits(attributes.getBoolean("isolatedSplits", false));
+
+ Long majorVersionCode = attributes.getLong("versionCodeMajor");
+ Long versionCode = attributes.getLong("versionCode");
+ if (majorVersionCode != null) {
+ if (versionCode == null) {
+ versionCode = 0L;
+ }
+ versionCode = (majorVersionCode << 32) | (versionCode & 0xFFFFFFFFL);
+ }
+ apkMetaBuilder.setVersionCode(versionCode);
+
+ String installLocation = attributes.getString("installLocation");
+ if (installLocation != null) {
+ apkMetaBuilder.setInstallLocation(installLocation);
+ }
+ apkMetaBuilder.setCompileSdkVersion(attributes.getString("compileSdkVersion"));
+ apkMetaBuilder.setCompileSdkVersionCodename(attributes.getString("compileSdkVersionCodename"));
+ apkMetaBuilder.setPlatformBuildVersionCode(attributes.getString("platformBuildVersionCode"));
+ apkMetaBuilder.setPlatformBuildVersionName(attributes.getString("platformBuildVersionName"));
+ break;
+ case "uses-sdk":
+ String minSdkVersion = attributes.getString("minSdkVersion");
+ if (minSdkVersion != null) {
+ apkMetaBuilder.setMinSdkVersion(minSdkVersion);
+ }
+ String targetSdkVersion = attributes.getString("targetSdkVersion");
+ if (targetSdkVersion != null) {
+ apkMetaBuilder.setTargetSdkVersion(targetSdkVersion);
+ }
+ String maxSdkVersion = attributes.getString("maxSdkVersion");
+ if (maxSdkVersion != null) {
+ apkMetaBuilder.setMaxSdkVersion(maxSdkVersion);
+ }
+ break;
+ case "supports-screens":
+ apkMetaBuilder.setAnyDensity(attributes.getBoolean("anyDensity", false));
+ apkMetaBuilder.setSmallScreens(attributes.getBoolean("smallScreens", false));
+ apkMetaBuilder.setNormalScreens(attributes.getBoolean("normalScreens", false));
+ apkMetaBuilder.setLargeScreens(attributes.getBoolean("largeScreens", false));
+ break;
+ case "uses-feature":
+ String name = attributes.getString("name");
+ boolean required = attributes.getBoolean("required", false);
+ if (name != null) {
+ UseFeature useFeature = new UseFeature(name, required);
+ apkMetaBuilder.addUsesFeature(useFeature);
+ } else {
+ Integer gl = attributes.getInt("glEsVersion");
+ if (gl != null) {
+ int v = gl;
+ GlEsVersion glEsVersion = new GlEsVersion(v >> 16, v & 0xffff, required);
+ apkMetaBuilder.setGlEsVersion(glEsVersion);
+ }
+ }
+ break;
+ case "uses-permission":
+ apkMetaBuilder.addUsesPermission(attributes.getString("name"));
+ break;
+ case "permission":
+ Permission permission = new Permission(
+ attributes.getString("name"),
+ attributes.getString("label"),
+ attributes.getString("icon"),
+ attributes.getString("description"),
+ attributes.getString("group"),
+ attributes.getString("android:protectionLevel"));
+ apkMetaBuilder.addPermissions(permission);
+ break;
+ }
+ tagStack[depth++] = xmlNodeStartTag.getName();
+ }
+
+ @Override
+ public void onEndTag(XmlNodeEndTag xmlNodeEndTag) {
+ depth--;
+ }
+
+ @Override
+ public void onCData(XmlCData xmlCData) {
+
+ }
+
+ @Override
+ public void onNamespaceStart(XmlNamespaceStartTag tag) {
+
+ }
+
+ @Override
+ public void onNamespaceEnd(XmlNamespaceEndTag tag) {
+
+ }
+
+ @Nonnull
+ public ApkMeta getApkMeta() {
+ return apkMetaBuilder.build();
+ }
+
+ @Nonnull
+ public List getIconPaths() {
+ return iconPaths;
+ }
+
+ private boolean matchTagPath(String... tags) {
+ // the root should always be "manifest"
+ if (depth != tags.length + 1) {
+ return false;
+ }
+ for (int i = 1; i < depth; i++) {
+ if (!tagStack[i].equals(tags[i - 1])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean matchLastTag(String tag) {
+ // the root should always be "manifest"
+ return tagStack[depth - 1].endsWith(tag);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.java b/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.java
new file mode 100644
index 0000000..1e955dd
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/ApkSignBlockParser.java
@@ -0,0 +1,134 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.struct.signingv2.ApkSigningBlock;
+import net.dongliu.apk.parser.struct.signingv2.Digest;
+import net.dongliu.apk.parser.struct.signingv2.Signature;
+import net.dongliu.apk.parser.struct.signingv2.SignerBlock;
+import net.dongliu.apk.parser.utils.Buffers;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+// see https://source.android.com/security/apksigning/v2
+/**
+ * The Apk Sign Block V2 Parser.
+ */
+public class ApkSignBlockParser {
+
+ private ByteBuffer data;
+
+ public ApkSignBlockParser(ByteBuffer data) {
+ this.data = data.order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ public ApkSigningBlock parse() throws CertificateException {
+ // sign block found, read pairs
+ List signerBlocks = new ArrayList<>();
+ while (data.remaining() >= 8) {
+ int id = data.getInt();
+ int size = Unsigned.ensureUInt(data.getInt());
+ if (id == ApkSigningBlock.SIGNING_V2_ID) {
+ ByteBuffer signingV2Buffer = Buffers.sliceAndSkip(data, size);
+ // now only care about apk signing v2 entry
+ while (signingV2Buffer.hasRemaining()) {
+ SignerBlock signerBlock = readSigningV2(signingV2Buffer);
+ signerBlocks.add(signerBlock);
+ }
+ } else {
+ // just ignore now
+ Buffers.position(data, data.position() + size);
+ }
+ }
+ return new ApkSigningBlock(signerBlocks);
+ }
+
+ private SignerBlock readSigningV2(ByteBuffer buffer) throws CertificateException {
+ buffer = readLenPrefixData(buffer);
+
+ ByteBuffer signedData = readLenPrefixData(buffer);
+ ByteBuffer digestsData = readLenPrefixData(signedData);
+ List digests = readDigests(digestsData);
+ ByteBuffer certificateData = readLenPrefixData(signedData);
+ List certificates = readCertificates(certificateData);
+ ByteBuffer attributesData = readLenPrefixData(signedData);
+ readAttributes(attributesData);
+
+ ByteBuffer signaturesData = readLenPrefixData(buffer);
+ List signatures = readSignatures(signaturesData);
+
+ ByteBuffer publicKeyData = readLenPrefixData(buffer);
+ return new SignerBlock(digests, certificates, signatures);
+ }
+
+ private List readDigests(ByteBuffer buffer) {
+ List list = new ArrayList<>();
+ while (buffer.hasRemaining()) {
+ ByteBuffer digestData = readLenPrefixData(buffer);
+ int algorithmID = digestData.getInt();
+ byte[] digest = Buffers.readBytes(digestData);
+ list.add(new Digest(algorithmID, digest));
+ }
+ return list;
+ }
+
+ private List readCertificates(ByteBuffer buffer) throws CertificateException {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ List certificates = new ArrayList<>();
+ while (buffer.hasRemaining()) {
+ ByteBuffer certificateData = readLenPrefixData(buffer);
+ Certificate certificate = certificateFactory.generateCertificate(
+ new ByteArrayInputStream(Buffers.readBytes(certificateData)));
+ certificates.add((X509Certificate) certificate);
+ }
+ return certificates;
+ }
+
+ private void readAttributes(ByteBuffer buffer) {
+ while (buffer.hasRemaining()) {
+ ByteBuffer attributeData = readLenPrefixData(buffer);
+ int id = attributeData.getInt();
+// byte[] value = Buffers.readBytes(attributeData);
+ }
+ }
+
+ private List readSignatures(ByteBuffer buffer) {
+ List signatures = new ArrayList<>();
+ while (buffer.hasRemaining()) {
+ ByteBuffer signatureData = readLenPrefixData(buffer);
+ int algorithmID = signatureData.getInt();
+ int signatureDataLen = Unsigned.ensureUInt(signatureData.getInt());
+ byte[] signature = Buffers.readBytes(signatureData, signatureDataLen);
+ signatures.add(new Signature(algorithmID, signature));
+ }
+ return signatures;
+ }
+
+ private ByteBuffer readLenPrefixData(ByteBuffer buffer) {
+ int len = Unsigned.ensureUInt(buffer.getInt());
+ 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/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.java b/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.java
new file mode 100644
index 0000000..ef50d5b
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/AttributeValues.java
@@ -0,0 +1,203 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.utils.Strings;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * attribute value constant
+ *
+ * @author Liu Dong
+ */
+public class 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
+ public static String getScreenOrientation(int value) {
+ switch (value) {
+ case 0x00000003:
+ return "behind";
+ case 0x0000000a:
+ return "fullSensor";
+ case 0x0000000d:
+ return "fullUser";
+ case 0x00000000:
+ return "landscape";
+ case 0x0000000e:
+ return "locked";
+ case 0x00000005:
+ return "nosensor";
+ case 0x00000001:
+ return "portrait";
+ case 0x00000008:
+ return "reverseLandscape";
+ case 0x00000009:
+ return "reversePortrait";
+ case 0x00000004:
+ return "sensor";
+ case 0x00000006:
+ return "sensorLandscape";
+ case 0x00000007:
+ return "sensorPortrait";
+ case 0xffffffff:
+ return "unspecified";
+ case 0x00000002:
+ return "user";
+ case 0x0000000b:
+ return "userLandscape";
+ case 0x0000000c:
+ return "userPortrait";
+ default:
+ return "ScreenOrientation:" + Integer.toHexString(value);
+ }
+ }
+
+ public static String getLaunchMode(int value) {
+ switch (value) {
+ case 0x00000000:
+ return "standard";
+ case 0x00000001:
+ return "singleTop";
+ case 0x00000002:
+ return "singleTask";
+ case 0x00000003:
+ return "singleInstance";
+ default:
+ return "LaunchMode:" + Integer.toHexString(value);
+ }
+
+ }
+
+ public static String getConfigChanges(int value) {
+ List list = new ArrayList<>();
+ if ((value & 0x00001000) != 0) {
+ list.add("density");
+ } else if ((value & 0x40000000) != 0) {
+ list.add("fontScale");
+ } else if ((value & 0x00000010) != 0) {
+ list.add("keyboard");
+ } else if ((value & 0x00000020) != 0) {
+ list.add("keyboardHidden");
+ } else if ((value & 0x00002000) != 0) {
+ list.add("direction");
+ } else if ((value & 0x00000004) != 0) {
+ list.add("locale");
+ } else if ((value & 0x00000001) != 0) {
+ list.add("mcc");
+ } else if ((value & 0x00000002) != 0) {
+ list.add("mnc");
+ } else if ((value & 0x00000040) != 0) {
+ list.add("navigation");
+ } else if ((value & 0x00000080) != 0) {
+ list.add("orientation");
+ } else if ((value & 0x00000100) != 0) {
+ list.add("screenLayout");
+ } else if ((value & 0x00000400) != 0) {
+ list.add("screenSize");
+ } else if ((value & 0x00000800) != 0) {
+ list.add("smallestScreenSize");
+ } else if ((value & 0x00000008) != 0) {
+ list.add("touchscreen");
+ } else if ((value & 0x00000200) != 0) {
+ list.add("uiMode");
+ }
+ return Strings.join(list, "|");
+ }
+
+ public static String getWindowSoftInputMode(int value) {
+ int adjust = value & 0x000000f0;
+ int state = value & 0x0000000f;
+ List list = new ArrayList<>(2);
+ switch (adjust) {
+ case 0x00000030:
+ list.add("adjustNothing");
+ break;
+ case 0x00000020:
+ list.add("adjustPan");
+ break;
+ case 0x00000010:
+ list.add("adjustResize");
+ break;
+ case 0x00000000:
+ //levels.add("adjustUnspecified");
+ break;
+ default:
+ list.add("WindowInputModeAdjust:" + Integer.toHexString(adjust));
+ }
+ switch (state) {
+ case 0x00000003:
+ list.add("stateAlwaysHidden");
+ break;
+ case 0x00000005:
+ list.add("stateAlwaysVisible");
+ break;
+ case 0x00000002:
+ list.add("stateHidden");
+ break;
+ case 0x00000001:
+ list.add("stateUnchanged");
+ break;
+ case 0x00000004:
+ list.add("stateVisible");
+ break;
+ case 0x00000000:
+ //levels.add("stateUnspecified");
+ break;
+ default:
+ list.add("WindowInputModeState:" + Integer.toHexString(state));
+ }
+ return Strings.join(list, "|");
+ //isForwardNavigation(0x00000100),
+ //mode_changed(0x00000200),
+ }
+
+ //http://developer.android.com/reference/android/content/pm/PermissionInfo.html
+ public static String getProtectionLevel(int value) {
+ List levels = new ArrayList<>(3);
+ if ((value & 0x10) != 0) {
+ value = value ^ 0x10;
+ levels.add("system");
+ }
+ if ((value & 0x20) != 0) {
+ value = value ^ 0x20;
+ levels.add("development");
+ }
+ switch (value) {
+ case 0:
+ levels.add("normal");
+ break;
+ case 1:
+ levels.add("dangerous");
+ break;
+ case 2:
+ levels.add("signature");
+ break;
+ case 3:
+ levels.add("signatureOrSystem");
+ break;
+ default:
+ levels.add("ProtectionLevel:" + Integer.toHexString(value));
+ }
+ return Strings.join(levels, "|");
+ }
+
+ // Activity constants end
+ /**
+ * get Installation string values from int
+ */
+ public static String getInstallLocation(int value) {
+ switch (value) {
+ case 0:
+ return "auto";
+ case 1:
+ return "internalOnly";
+ case 2:
+ return "preferExternal";
+ default:
+ return "installLocation:" + Integer.toHexString(value);
+ }
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java b/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java
new file mode 100644
index 0000000..72507cc
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java
@@ -0,0 +1,58 @@
+package net.dongliu.apk.parser.parser;
+
+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.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(byte[] data) {
+ super(data);
+ }
+
+ /**
+ * get certificate info
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public List parse() throws CertificateException {
+ CMSSignedData cmsSignedData;
+ try {
+ cmsSignedData = new CMSSignedData(data);
+ } catch (CMSException e) {
+ throw new CertificateException(e);
+ }
+ Store certStore = cmsSignedData.getCertificates();
+ SignerInformationStore signerInfos = cmsSignedData.getSignerInfos();
+ Collection signers = signerInfos.getSigners();
+ List certificates = new ArrayList<>();
+ for (SignerInformation signer : signers) {
+ Collection matches = certStore.getMatches(signer.getSID());
+ for (X509CertificateHolder holder : matches) {
+ certificates.add(new JcaX509CertificateConverter().setProvider(provider).getCertificate(holder));
+ }
+ }
+ return CertificateMetas.from(certificates);
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java b/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java
new file mode 100644
index 0000000..22fbd6f
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/BinaryXmlParser.java
@@ -0,0 +1,343 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.exception.ParserException;
+import net.dongliu.apk.parser.struct.*;
+import net.dongliu.apk.parser.struct.resource.ResourceTable;
+import net.dongliu.apk.parser.struct.xml.*;
+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 ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
+ private StringPool stringPool;
+ // some attribute name stored by resource id
+ private String[] resourceMap;
+ private ByteBuffer buffer;
+ private XmlStreamer xmlStreamer;
+ private final ResourceTable resourceTable;
+ /**
+ * default locale.
+ */
+ private Locale locale = Locales.any;
+
+ public BinaryXmlParser(ByteBuffer buffer, ResourceTable resourceTable) {
+ this.buffer = buffer.duplicate();
+ this.buffer.order(byteOrder);
+ this.resourceTable = resourceTable;
+ }
+
+ /**
+ * Parse binary xml.
+ */
+ public void parse() {
+ ChunkHeader firstChunkHeader = readChunkHeader();
+ if (firstChunkHeader == null) {
+ return;
+ }
+
+ switch (firstChunkHeader.getChunkType()) {
+ 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
+ ChunkHeader stringPoolChunkHeader = readChunkHeader();
+ if (stringPoolChunkHeader == null) {
+ return;
+ }
+
+ ParseUtils.checkChunkType(ChunkType.STRING_POOL, stringPoolChunkHeader.getChunkType());
+ stringPool = ParseUtils.readStringPool(buffer, (StringPoolHeader) stringPoolChunkHeader);
+
+ // read on chunk, check if it was an optional XMLResourceMap chunk
+ ChunkHeader chunkHeader = readChunkHeader();
+ if (chunkHeader == null) {
+ return;
+ }
+
+ if (chunkHeader.getChunkType() == ChunkType.XML_RESOURCE_MAP) {
+ long[] resourceIds = readXmlResourceMap((XmlResourceMapHeader) chunkHeader);
+ resourceMap = new String[resourceIds.length];
+ for (int i = 0; i < resourceIds.length; i++) {
+ resourceMap[i] = Attribute.AttrIds.getString(resourceIds[i]);
+ }
+ chunkHeader = readChunkHeader();
+ }
+
+ while (chunkHeader != null) {
+ /*if (chunkHeader.chunkType == ChunkType.XML_END_NAMESPACE) {
+ break;
+ }*/
+ long beginPos = buffer.position();
+ switch (chunkHeader.getChunkType()) {
+ case ChunkType.XML_END_NAMESPACE:
+ XmlNamespaceEndTag xmlNamespaceEndTag = readXmlNamespaceEndTag();
+ xmlStreamer.onNamespaceEnd(xmlNamespaceEndTag);
+ break;
+ case ChunkType.XML_START_NAMESPACE:
+ XmlNamespaceStartTag namespaceStartTag = readXmlNamespaceStartTag();
+ xmlStreamer.onNamespaceStart(namespaceStartTag);
+ break;
+ case ChunkType.XML_START_ELEMENT:
+ XmlNodeStartTag xmlNodeStartTag = readXmlNodeStartTag();
+ break;
+ case ChunkType.XML_END_ELEMENT:
+ XmlNodeEndTag xmlNodeEndTag = readXmlNodeEndTag();
+ break;
+ case ChunkType.XML_CDATA:
+ XmlCData xmlCData = readXmlCData();
+ break;
+ default:
+ if (chunkHeader.getChunkType() >= ChunkType.XML_FIRST_CHUNK
+ && chunkHeader.getChunkType() <= ChunkType.XML_LAST_CHUNK) {
+ Buffers.skip(buffer, chunkHeader.getBodySize());
+ } else {
+ throw new ParserException("Unexpected chunk type:" + chunkHeader.getChunkType());
+ }
+ }
+ Buffers.position(buffer, beginPos + chunkHeader.getBodySize());
+ chunkHeader = readChunkHeader();
+ }
+ }
+
+ private XmlCData readXmlCData() {
+ XmlCData xmlCData = new XmlCData();
+ int dataRef = buffer.getInt();
+ if (dataRef > 0) {
+ xmlCData.setData(stringPool.get(dataRef));
+ }
+ xmlCData.setTypedData(ParseUtils.readResValue(buffer, stringPool));
+ if (xmlStreamer != null) {
+ //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() {
+ XmlNodeEndTag xmlNodeEndTag = new XmlNodeEndTag();
+ int nsRef = buffer.getInt();
+ int nameRef = buffer.getInt();
+ if (nsRef > 0) {
+ xmlNodeEndTag.setNamespace(stringPool.get(nsRef));
+ }
+ xmlNodeEndTag.setName(stringPool.get(nameRef));
+ if (xmlStreamer != null) {
+ xmlStreamer.onEndTag(xmlNodeEndTag);
+ }
+ return xmlNodeEndTag;
+ }
+
+ private XmlNodeStartTag readXmlNodeStartTag() {
+ int nsRef = buffer.getInt();
+ int nameRef = buffer.getInt();
+ XmlNodeStartTag xmlNodeStartTag = new XmlNodeStartTag();
+ if (nsRef > 0) {
+ xmlNodeStartTag.setNamespace(stringPool.get(nsRef));
+ }
+ xmlNodeStartTag.setName(stringPool.get(nameRef));
+
+ // read attributes.
+ // attributeStart and attributeSize are always 20 (0x14)
+ int attributeStart = Buffers.readUShort(buffer);
+ int attributeSize = Buffers.readUShort(buffer);
+ int attributeCount = Buffers.readUShort(buffer);
+ int idIndex = Buffers.readUShort(buffer);
+ int classIndex = Buffers.readUShort(buffer);
+ int styleIndex = Buffers.readUShort(buffer);
+
+ // read attributes
+ Attributes attributes = new Attributes(attributeCount);
+ for (int count = 0; count < attributeCount; count++) {
+ Attribute attribute = readAttribute();
+ if (xmlStreamer != null) {
+ String value = attribute.toStringValue(resourceTable, locale);
+ if (intAttributes.contains(attribute.getName()) && Strings.isNumeric(value)) {
+ try {
+ value = getFinalValueAsString(attribute.getName(), value);
+ } catch (Exception ignore) {
+ }
+ }
+ attribute.setValue(value);
+ attributes.set(count, attribute);
+ }
+ }
+ xmlNodeStartTag.setAttributes(attributes);
+
+ if (xmlStreamer != null) {
+ 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(String attributeName, String str) {
+ 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() {
+ int nsRef = buffer.getInt();
+ int nameRef = buffer.getInt();
+ Attribute attribute = new Attribute();
+ if (nsRef > 0) {
+ attribute.setNamespace(stringPool.get(nsRef));
+ }
+
+ attribute.setName(stringPool.get(nameRef));
+ if (attribute.getName().isEmpty() && resourceMap != null && nameRef < resourceMap.length) {
+ // some processed apk file make the string pool value empty, if it is a xmlmap attr.
+ attribute.setName(resourceMap[nameRef]);
+ //TODO: how to get the namespace of attribute
+ }
+
+ int rawValueRef = buffer.getInt();
+ if (rawValueRef > 0) {
+ attribute.setRawValue(stringPool.get(rawValueRef));
+ }
+ ResourceValue resValue = ParseUtils.readResValue(buffer, stringPool);
+ attribute.setTypedValue(resValue);
+
+ return attribute;
+ }
+
+ private XmlNamespaceStartTag readXmlNamespaceStartTag() {
+ int prefixRef = buffer.getInt();
+ int uriRef = buffer.getInt();
+ XmlNamespaceStartTag nameSpace = new XmlNamespaceStartTag();
+ if (prefixRef > 0) {
+ nameSpace.setPrefix(stringPool.get(prefixRef));
+ }
+ if (uriRef > 0) {
+ nameSpace.setUri(stringPool.get(uriRef));
+ }
+ return nameSpace;
+ }
+
+ private XmlNamespaceEndTag readXmlNamespaceEndTag() {
+ int prefixRef = buffer.getInt();
+ int uriRef = buffer.getInt();
+ XmlNamespaceEndTag nameSpace = new XmlNamespaceEndTag();
+ if (prefixRef > 0) {
+ nameSpace.setPrefix(stringPool.get(prefixRef));
+ }
+ if (uriRef > 0) {
+ nameSpace.setUri(stringPool.get(uriRef));
+ }
+ return nameSpace;
+ }
+
+ private long[] readXmlResourceMap(XmlResourceMapHeader chunkHeader) {
+ int count = chunkHeader.getBodySize() / 4;
+ long[] resourceIds = new long[count];
+ for (int i = 0; i < count; i++) {
+ resourceIds[i] = Buffers.readUInt(buffer);
+ }
+ return resourceIds;
+ }
+
+ private ChunkHeader readChunkHeader() {
+ // finished
+ if (!buffer.hasRemaining()) {
+ return null;
+ }
+
+ long begin = buffer.position();
+ int chunkType = Buffers.readUShort(buffer);
+ int headerSize = Buffers.readUShort(buffer);
+ long chunkSize = Buffers.readUInt(buffer);
+
+ switch (chunkType) {
+ case ChunkType.XML:
+ return new XmlHeader(chunkType, headerSize, chunkSize);
+ case ChunkType.STRING_POOL:
+ StringPoolHeader stringPoolHeader = new StringPoolHeader(headerSize, chunkSize);
+ stringPoolHeader.setStringCount(Buffers.readUInt(buffer));
+ stringPoolHeader.setStyleCount(Buffers.readUInt(buffer));
+ stringPoolHeader.setFlags(Buffers.readUInt(buffer));
+ stringPoolHeader.setStringsStart(Buffers.readUInt(buffer));
+ stringPoolHeader.setStylesStart(Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return stringPoolHeader;
+ case ChunkType.XML_RESOURCE_MAP:
+ Buffers.position(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:
+ XmlNodeHeader header = new XmlNodeHeader(chunkType, headerSize, chunkSize);
+ header.setLineNum((int) Buffers.readUInt(buffer));
+ header.setCommentRef((int) Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return header;
+ case ChunkType.NULL:
+ return new NullHeader(chunkType, headerSize, chunkSize);
+ default:
+ throw new ParserException("Unexpected chunk type:" + chunkType);
+ }
+ }
+
+ public void setLocale(Locale locale) {
+ if (locale != null) {
+ this.locale = locale;
+ }
+ }
+
+ public Locale getLocale() {
+ return locale;
+ }
+
+ public XmlStreamer getXmlStreamer() {
+ return xmlStreamer;
+ }
+
+ public void setXmlStreamer(XmlStreamer xmlStreamer) {
+ this.xmlStreamer = xmlStreamer;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java b/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java
new file mode 100644
index 0000000..5c0fbcb
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java
@@ -0,0 +1,75 @@
+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.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CertificateMetas {
+
+ public static List from(List certificates) throws CertificateEncodingException {
+ List certificateMetas = new ArrayList<>(certificates.size());
+ for (X509Certificate certificate : certificates) {
+ CertificateMeta certificateMeta = CertificateMetas.from(certificate);
+ certificateMetas.add(certificateMeta);
+ }
+ return certificateMetas;
+ }
+
+ public static CertificateMeta from(X509Certificate certificate) throws CertificateEncodingException {
+ byte[] bytes = certificate.getEncoded();
+ String certMd5 = md5Digest(bytes);
+ String publicKeyString = byteToHexString(bytes);
+ String certBase64Md5 = md5Digest(publicKeyString);
+ return new CertificateMeta(
+ certificate.getSigAlgName().toUpperCase(),
+ certificate.getSigAlgOID(),
+ certificate.getNotBefore(),
+ certificate.getNotAfter(),
+ bytes, certBase64Md5, certMd5);
+ }
+
+ private static String md5Digest(byte[] input) {
+ MessageDigest digest = getDigest("md5");
+ digest.update(input);
+ return getHexString(digest.digest());
+ }
+
+ private static String md5Digest(String input) {
+ MessageDigest digest = getDigest("md5");
+ digest.update(input.getBytes(StandardCharsets.UTF_8));
+ return getHexString(digest.digest());
+ }
+
+ private static String byteToHexString(byte[] bArray) {
+ StringBuilder sb = new StringBuilder(bArray.length);
+ String sTemp;
+ for (byte aBArray : bArray) {
+ sTemp = Integer.toHexString(0xFF & (char) aBArray);
+ if (sTemp.length() < 2) {
+ sb.append(0);
+ }
+ sb.append(sTemp.toUpperCase());
+ }
+ return sb.toString();
+ }
+
+ private static String getHexString(byte[] digest) {
+ BigInteger bi = new BigInteger(1, digest);
+ return String.format("%032x", bi);
+ }
+
+ private static MessageDigest getDigest(String algorithm) {
+ try {
+ return MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java b/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java
new file mode 100644
index 0000000..99c5f2a
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java
@@ -0,0 +1,34 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.ApkParsers;
+import net.dongliu.apk.parser.bean.CertificateMeta;
+
+import java.security.cert.CertificateException;
+import java.util.List;
+
+/**
+ * Parser certificate info. One apk may have multi certificates(certificate chain).
+ *
+ * @author dongliu
+ */
+public abstract class CertificateParser {
+
+ protected final byte[] data;
+
+ public CertificateParser(byte[] data) {
+ this.data = data;
+ }
+
+ public static CertificateParser getInstance(byte[] data) {
+ if (ApkParsers.useBouncyCastle()) {
+ return new BCCertificateParser(data);
+ }
+ return new JSSECertificateParser(data);
+ }
+
+ /**
+ * get certificate info
+ */
+ public abstract List parse() throws CertificateException;
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.java b/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.java
new file mode 100644
index 0000000..b792172
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/CompositeXmlStreamer.java
@@ -0,0 +1,50 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.struct.xml.*;
+
+/**
+ * @author dongliu
+ */
+public class CompositeXmlStreamer implements XmlStreamer {
+
+ public XmlStreamer[] xmlStreamers;
+
+ public CompositeXmlStreamer(XmlStreamer... xmlStreamers) {
+ this.xmlStreamers = xmlStreamers;
+ }
+
+ @Override
+ public void onStartTag(XmlNodeStartTag xmlNodeStartTag) {
+ for (XmlStreamer xmlStreamer : xmlStreamers) {
+ xmlStreamer.onStartTag(xmlNodeStartTag);
+ }
+ }
+
+ @Override
+ public void onEndTag(XmlNodeEndTag xmlNodeEndTag) {
+ for (XmlStreamer xmlStreamer : xmlStreamers) {
+ xmlStreamer.onEndTag(xmlNodeEndTag);
+ }
+ }
+
+ @Override
+ public void onCData(XmlCData xmlCData) {
+ for (XmlStreamer xmlStreamer : xmlStreamers) {
+ xmlStreamer.onCData(xmlCData);
+ }
+ }
+
+ @Override
+ public void onNamespaceStart(XmlNamespaceStartTag tag) {
+ for (XmlStreamer xmlStreamer : xmlStreamers) {
+ xmlStreamer.onNamespaceStart(tag);
+ }
+ }
+
+ @Override
+ public void onNamespaceEnd(XmlNamespaceEndTag tag) {
+ for (XmlStreamer xmlStreamer : xmlStreamers) {
+ xmlStreamer.onNamespaceEnd(tag);
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/DexParser.java b/src/main/java/net/dongliu/apk/parser/parser/DexParser.java
new file mode 100644
index 0000000..7322eb6
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/DexParser.java
@@ -0,0 +1,277 @@
+package net.dongliu.apk.parser.parser;
+
+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.io.IOException;
+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 ByteBuffer buffer;
+
+ private static final int NO_INDEX = 0xffffffff;
+
+ public DexParser(ByteBuffer buffer) {
+ this.buffer = buffer.duplicate();
+ this.buffer.order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ public DexClass[] parse() {
+ // read magic
+ String magic = new String(Buffers.readBytes(buffer, 8));
+ if (!magic.startsWith("dex\n")) {
+ return new DexClass[0];
+ }
+ 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
+ DexHeader header = readDexHeader();
+ header.setVersion(version);
+
+ // read string pool
+ long[] stringOffsets = readStringPool(header.getStringIdsOff(), header.getStringIdsSize());
+
+ // read types
+ int[] typeIds = readTypes(header.getTypeIdsOff(), header.getTypeIdsSize());
+
+ // read classes
+ DexClassStruct[] dexClassStructs = readClass(header.getClassDefsOff(),
+ header.getClassDefsSize());
+
+ StringPool stringpool = readStrings(stringOffsets);
+
+ String[] types = new String[typeIds.length];
+ for (int i = 0; i < typeIds.length; i++) {
+ types[i] = stringpool.get(typeIds[i]);
+ }
+
+ DexClass[] dexClasses = new DexClass[dexClassStructs.length];
+ for (int i = 0; i < dexClassStructs.length; i++) {
+ DexClassStruct dexClassStruct = dexClassStructs[i];
+ String superClass = null;
+ if (dexClassStruct.getSuperclassIdx() != NO_INDEX) {
+ superClass = types[dexClassStruct.getSuperclassIdx()];
+ }
+ dexClasses[i] = new DexClass(
+ types[dexClassStruct.getClassIdx()],
+ superClass,
+ dexClassStruct.getAccessFlags());
+ }
+ return dexClasses;
+ }
+
+ /**
+ * read class info.
+ */
+ private DexClassStruct[] readClass(long classDefsOff, int classDefsSize) {
+ Buffers.position(buffer, classDefsOff);
+
+ DexClassStruct[] dexClassStructs = new DexClassStruct[classDefsSize];
+ for (int i = 0; i < classDefsSize; i++) {
+ DexClassStruct dexClassStruct = new DexClassStruct();
+ dexClassStruct.setClassIdx(buffer.getInt());
+
+ dexClassStruct.setAccessFlags(buffer.getInt());
+ dexClassStruct.setSuperclassIdx(buffer.getInt());
+
+ dexClassStruct.setInterfacesOff(Buffers.readUInt(buffer));
+ dexClassStruct.setSourceFileIdx(buffer.getInt());
+ dexClassStruct.setAnnotationsOff(Buffers.readUInt(buffer));
+ dexClassStruct.setClassDataOff(Buffers.readUInt(buffer));
+ dexClassStruct.setStaticValuesOff(Buffers.readUInt(buffer));
+ dexClassStructs[i] = dexClassStruct;
+ }
+
+ return dexClassStructs;
+ }
+
+ /**
+ * read types.
+ */
+ private int[] readTypes(long typeIdsOff, int typeIdsSize) {
+ Buffers.position(buffer, typeIdsOff);
+ int[] typeIds = new int[typeIdsSize];
+ for (int i = 0; i < typeIdsSize; i++) {
+ typeIds[i] = (int) Buffers.readUInt(buffer);
+ }
+ return typeIds;
+ }
+
+ /**
+ * read string pool for dex file. dex file string pool diff a bit with binary xml file or resource table.
+ *
+ * @param offsets
+ * @return
+ * @throws IOException
+ */
+ private StringPool readStrings(long[] offsets) {
+ // read strings.
+ // buffer some apk, the strings' offsets may not well ordered. we sort it first
+
+ 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;
+ StringPool stringpool = new StringPool(offsets.length);
+ for (StringPoolEntry entry : entries) {
+ if (entry.getOffset() == lastOffset) {
+ stringpool.set(entry.getIdx(), lastStr);
+ continue;
+ }
+ Buffers.position(buffer, entry.getOffset());
+ lastOffset = entry.getOffset();
+ String str = readString();
+ lastStr = str;
+ stringpool.set(entry.getIdx(), str);
+ }
+ return stringpool;
+ }
+
+ /*
+ * read string identifiers list.
+ */
+ private long[] readStringPool(long stringIdsOff, int stringIdsSize) {
+ Buffers.position(buffer, stringIdsOff);
+ long offsets[] = new long[stringIdsSize];
+ for (int i = 0; i < stringIdsSize; i++) {
+ offsets[i] = Buffers.readUInt(buffer);
+ }
+
+ return offsets;
+ }
+
+ /**
+ * read dex encoding string.
+ */
+ private String readString() {
+ // the length is char len, not byte len
+ int strLen = readVarInts();
+ return readString(strLen);
+ }
+
+ /**
+ * read Modified UTF-8 encoding str.
+ *
+ * @param strLen the java-utf16-char len, not strLen nor bytes len.
+ */
+ private String readString(int strLen) {
+ char[] chars = new char[strLen];
+
+ for (int i = 0; i < strLen; i++) {
+ short a = Buffers.readUByte(buffer);
+ if ((a & 0x80) == 0) {
+ // ascii char
+ chars[i] = (char) a;
+ } else if ((a & 0xe0) == 0xc0) {
+ // read one more
+ short b = Buffers.readUByte(buffer);
+ chars[i] = (char) (((a & 0x1F) << 6) | (b & 0x3F));
+ } else if ((a & 0xf0) == 0xe0) {
+ short b = Buffers.readUByte(buffer);
+ short c = Buffers.readUByte(buffer);
+ chars[i] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F));
+ } else if ((a & 0xf0) == 0xf0) {
+ //throw new UTFDataFormatException();
+
+ } else {
+ //throw new UTFDataFormatException();
+ }
+ if (chars[i] == 0) {
+ // the end of string.
+ }
+ }
+
+ return new String(chars);
+ }
+
+ /**
+ * read varints.
+ *
+ * @return
+ * @throws IOException
+ */
+ private int readVarInts() {
+ int i = 0;
+ int count = 0;
+ short s;
+ do {
+ if (count > 4) {
+ throw new ParserException("read varints error.");
+ }
+ s = Buffers.readUByte(buffer);
+ i |= (s & 0x7f) << (count * 7);
+ count++;
+ } while ((s & 0x80) != 0);
+
+ return i;
+ }
+
+ private DexHeader readDexHeader() {
+
+ // check sum. skip
+ buffer.getInt();
+
+ // signature skip
+ Buffers.readBytes(buffer, DexHeader.kSHA1DigestLen);
+
+ DexHeader header = new DexHeader();
+ header.setFileSize(Buffers.readUInt(buffer));
+ header.setHeaderSize(Buffers.readUInt(buffer));
+
+ // skip?
+ Buffers.readUInt(buffer);
+
+ // static link data
+ header.setLinkSize(Buffers.readUInt(buffer));
+ header.setLinkOff(Buffers.readUInt(buffer));
+
+ // the map data is just the same as dex header.
+ header.setMapOff(Buffers.readUInt(buffer));
+
+ header.setStringIdsSize(buffer.getInt());
+ header.setStringIdsOff(Buffers.readUInt(buffer));
+
+ header.setTypeIdsSize(buffer.getInt());
+ header.setTypeIdsOff(Buffers.readUInt(buffer));
+
+ header.setProtoIdsSize(buffer.getInt());
+ header.setProtoIdsOff(Buffers.readUInt(buffer));
+
+ header.setFieldIdsSize(buffer.getInt());
+ header.setFieldIdsOff(Buffers.readUInt(buffer));
+
+ header.setMethodIdsSize(buffer.getInt());
+ header.setMethodIdsOff(Buffers.readUInt(buffer));
+
+ header.setClassDefsSize(buffer.getInt());
+ header.setClassDefsOff(Buffers.readUInt(buffer));
+
+ header.setDataSize(buffer.getInt());
+ header.setDataOff(Buffers.readUInt(buffer));
+
+ Buffers.position(buffer, header.getHeaderSize());
+
+ return header;
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.java b/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.java
new file mode 100644
index 0000000..2949f7a
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.java
@@ -0,0 +1,60 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.bean.CertificateMeta;
+import net.dongliu.apk.parser.cert.asn1.Asn1BerParser;
+import net.dongliu.apk.parser.cert.asn1.Asn1DecodingException;
+import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject;
+import net.dongliu.apk.parser.cert.pkcs7.ContentInfo;
+import net.dongliu.apk.parser.cert.pkcs7.Pkcs7Constants;
+import net.dongliu.apk.parser.cert.pkcs7.SignedData;
+import net.dongliu.apk.parser.utils.Buffers;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parser certificate info using jsse.
+ *
+ * @author dongliu
+ */
+class JSSECertificateParser extends CertificateParser {
+
+ public JSSECertificateParser(byte[] data) {
+ super(data);
+ }
+
+ public List parse() throws CertificateException {
+ ContentInfo contentInfo;
+ try {
+ contentInfo = Asn1BerParser.parse(ByteBuffer.wrap(data), ContentInfo.class);
+ } catch (Asn1DecodingException e) {
+ throw new CertificateException(e);
+ }
+ if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) {
+ throw new CertificateException("Unsupported ContentInfo.contentType: " + contentInfo.contentType);
+ }
+ SignedData signedData;
+ try {
+ signedData = Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class);
+ } catch (Asn1DecodingException e) {
+ throw new CertificateException(e);
+ }
+ List encodedCertificates = signedData.certificates;
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ List result = new ArrayList<>(encodedCertificates.size());
+ for (int i = 0; i < encodedCertificates.size(); i++) {
+ Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
+ byte[] encodedForm = Buffers.readBytes(encodedCertificate.getEncoded());
+ Certificate certificate = certFactory.generateCertificate(new ByteArrayInputStream(encodedForm));
+ result.add((X509Certificate) certificate);
+ }
+ return CertificateMetas.from(result);
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java b/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java
new file mode 100644
index 0000000..1691799
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/ResourceTableParser.java
@@ -0,0 +1,257 @@
+package net.dongliu.apk.parser.parser;
+
+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.*;
+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;
+
+import static net.dongliu.apk.parser.struct.ChunkType.UNKNOWN_YET;
+
+/**
+ * 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 ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
+ private StringPool stringPool;
+ private ByteBuffer buffer;
+ // the resource table file size
+ private ResourceTable resourceTable;
+
+ private Set locales;
+
+ public ResourceTableParser(ByteBuffer buffer) {
+ this.buffer = buffer.duplicate();
+ this.buffer.order(byteOrder);
+ this.locales = new HashSet<>();
+ }
+
+ /**
+ * parse resource table file.
+ */
+ public void parse() {
+ // read resource file header.
+ ResourceTableHeader resourceTableHeader = (ResourceTableHeader) readChunkHeader();
+
+ // read string pool chunk
+ stringPool = ParseUtils.readStringPool(buffer, (StringPoolHeader) readChunkHeader());
+
+ resourceTable = new ResourceTable();
+ resourceTable.setStringPool(stringPool);
+
+ if (resourceTableHeader.getPackageCount() != 0) {
+ PackageHeader packageHeader = (PackageHeader) readChunkHeader();
+ for (int i = 0; i < resourceTableHeader.getPackageCount(); i++) {
+ Pair pair = readPackage(packageHeader);
+ resourceTable.addPackage(pair.getLeft());
+ packageHeader = pair.getRight();
+ }
+ }
+ }
+
+ // read one package
+ private Pair readPackage(PackageHeader packageHeader) {
+ Pair pair = new Pair<>();
+ //read packageHeader
+ ResourcePackage resourcePackage = new ResourcePackage(packageHeader);
+ pair.setLeft(resourcePackage);
+
+ long beginPos = buffer.position();
+ // read type string pool
+ if (packageHeader.getTypeStrings() > 0) {
+ Buffers.position(buffer, beginPos + packageHeader.getTypeStrings() - packageHeader.getHeaderSize());
+ resourcePackage.setTypeStringPool(ParseUtils.readStringPool(buffer,
+ (StringPoolHeader) readChunkHeader()));
+ }
+
+ //read key string pool
+ if (packageHeader.getKeyStrings() > 0) {
+ Buffers.position(buffer, beginPos + packageHeader.getKeyStrings() - packageHeader.getHeaderSize());
+ resourcePackage.setKeyStringPool(ParseUtils.readStringPool(buffer,
+ (StringPoolHeader) readChunkHeader()));
+ }
+
+ outer:
+ while (buffer.hasRemaining()) {
+ ChunkHeader chunkHeader = readChunkHeader();
+ long chunkBegin = buffer.position();
+ switch (chunkHeader.getChunkType()) {
+ case ChunkType.TABLE_TYPE_SPEC:
+ TypeSpecHeader typeSpecHeader = (TypeSpecHeader) chunkHeader;
+ long[] entryFlags = new long[(int) typeSpecHeader.getEntryCount()];
+ for (int i = 0; i < typeSpecHeader.getEntryCount(); i++) {
+ entryFlags[i] = Buffers.readUInt(buffer);
+ }
+
+ TypeSpec typeSpec = new TypeSpec(typeSpecHeader);
+
+ typeSpec.setEntryFlags(entryFlags);
+ //id start from 1
+ typeSpec.setName(resourcePackage.getTypeStringPool()
+ .get(typeSpecHeader.getId() - 1));
+
+ resourcePackage.addTypeSpec(typeSpec);
+ Buffers.position(buffer, chunkBegin + typeSpecHeader.getBodySize());
+ break;
+ case ChunkType.TABLE_TYPE:
+ TypeHeader typeHeader = (TypeHeader) chunkHeader;
+ // read offsets table
+ long[] offsets = new long[(int) typeHeader.getEntryCount()];
+ for (int i = 0; i < typeHeader.getEntryCount(); i++) {
+ offsets[i] = Buffers.readUInt(buffer);
+ }
+
+ Type type = new Type(typeHeader);
+ type.setName(resourcePackage.getTypeStringPool().get(typeHeader.getId() - 1));
+ long entryPos = chunkBegin + typeHeader.getEntriesStart() - typeHeader.getHeaderSize();
+ Buffers.position(buffer, entryPos);
+ ByteBuffer b = buffer.slice();
+ b.order(byteOrder);
+ type.setBuffer(b);
+ type.setKeyStringPool(resourcePackage.getKeyStringPool());
+ type.setOffsets(offsets);
+ type.setStringPool(stringPool);
+ resourcePackage.addType(type);
+ locales.add(type.getLocale());
+ Buffers.position(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
+ LibraryHeader libraryHeader = (LibraryHeader) chunkHeader;
+ for (long i = 0; i < libraryHeader.getCount(); i++) {
+ int packageId = buffer.getInt();
+ String name = Buffers.readZeroTerminatedString(buffer, 128);
+ LibraryEntry entry = new LibraryEntry(packageId, name);
+ //TODO: now just skip it..
+ }
+ Buffers.position(buffer, chunkBegin + chunkHeader.getBodySize());
+ break;
+ case ChunkType.NULL:
+// Buffers.position(buffer, chunkBegin + chunkHeader.getBodySize());
+ Buffers.position(buffer, buffer.position() + buffer.remaining());
+ break;
+ default:
+ throw new ParserException("unexpected chunk type: 0x" + chunkHeader.getChunkType());
+ }
+ }
+
+ return pair;
+
+ }
+
+ private ChunkHeader readChunkHeader() {
+ long begin = buffer.position();
+
+ int chunkType = Buffers.readUShort(buffer);
+ int headerSize = Buffers.readUShort(buffer);
+ int chunkSize = (int) Buffers.readUInt(buffer);
+
+ switch (chunkType) {
+ case ChunkType.TABLE:
+ ResourceTableHeader resourceTableHeader = new ResourceTableHeader(headerSize, chunkSize);
+ resourceTableHeader.setPackageCount(Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return resourceTableHeader;
+ case ChunkType.STRING_POOL:
+ StringPoolHeader stringPoolHeader = new StringPoolHeader(headerSize, chunkSize);
+ stringPoolHeader.setStringCount(Buffers.readUInt(buffer));
+ stringPoolHeader.setStyleCount(Buffers.readUInt(buffer));
+ stringPoolHeader.setFlags(Buffers.readUInt(buffer));
+ stringPoolHeader.setStringsStart(Buffers.readUInt(buffer));
+ stringPoolHeader.setStylesStart(Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return stringPoolHeader;
+ case ChunkType.TABLE_PACKAGE:
+ PackageHeader packageHeader = new PackageHeader(headerSize, chunkSize);
+ packageHeader.setId(Buffers.readUInt(buffer));
+ packageHeader.setName(ParseUtils.readStringUTF16(buffer, 128));
+ packageHeader.setTypeStrings(Buffers.readUInt(buffer));
+ packageHeader.setLastPublicType(Buffers.readUInt(buffer));
+ packageHeader.setKeyStrings(Buffers.readUInt(buffer));
+ packageHeader.setLastPublicKey(Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return packageHeader;
+ case ChunkType.TABLE_TYPE_SPEC:
+ TypeSpecHeader typeSpecHeader = new TypeSpecHeader(headerSize, chunkSize);
+ typeSpecHeader.setId(Buffers.readUByte(buffer));
+ typeSpecHeader.setRes0(Buffers.readUByte(buffer));
+ typeSpecHeader.setRes1(Buffers.readUShort(buffer));
+ typeSpecHeader.setEntryCount(Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return typeSpecHeader;
+ case ChunkType.TABLE_TYPE:
+ TypeHeader typeHeader = new TypeHeader(headerSize, chunkSize);
+ typeHeader.setId(Buffers.readUByte(buffer));
+ typeHeader.setRes0(Buffers.readUByte(buffer));
+ typeHeader.setRes1(Buffers.readUShort(buffer));
+ typeHeader.setEntryCount(Buffers.readUInt(buffer));
+ typeHeader.setEntriesStart(Buffers.readUInt(buffer));
+ typeHeader.setConfig(readResTableConfig());
+ Buffers.position(buffer, begin + headerSize);
+ return typeHeader;
+ case ChunkType.TABLE_LIBRARY:
+ //DynamicRefTable
+ LibraryHeader libraryHeader = new LibraryHeader(headerSize, chunkSize);
+ libraryHeader.setCount(Buffers.readUInt(buffer));
+ Buffers.position(buffer, begin + headerSize);
+ return libraryHeader;
+ case UNKNOWN_YET:
+ case ChunkType.NULL:
+ Buffers.position(buffer, begin + headerSize);
+ return new NullHeader(headerSize, chunkSize);
+ default:
+ throw new ParserException("Unexpected chunk Type: 0x" + Integer.toHexString(chunkType));
+ }
+ }
+
+ private ResTableConfig readResTableConfig() {
+ long beginPos = buffer.position();
+ ResTableConfig config = new ResTableConfig();
+ 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...
+ long endPos = buffer.position();
+ Buffers.skip(buffer, (int) (size - (endPos - beginPos)));
+ return config;
+ }
+
+ public ResourceTable getResourceTable() {
+ return resourceTable;
+ }
+
+ public Set getLocales() {
+ return this.locales;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.java b/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.java
new file mode 100644
index 0000000..1e3c094
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/StringPoolEntry.java
@@ -0,0 +1,32 @@
+package net.dongliu.apk.parser.parser;
+
+/**
+ * class for sort string pool indexes
+ */
+public class StringPoolEntry {
+
+ private int idx;
+ private long offset;
+
+ public StringPoolEntry(int idx, long offset) {
+ this.idx = idx;
+ this.offset = offset;
+ }
+
+ public int getIdx() {
+ return idx;
+ }
+
+ public void setIdx(int idx) {
+ this.idx = idx;
+ }
+
+ public long getOffset() {
+ return offset;
+ }
+
+ public void setOffset(long offset) {
+ this.offset = offset;
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.java b/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.java
new file mode 100644
index 0000000..7b7895a
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/XmlNamespaces.java
@@ -0,0 +1,116 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceEndTag;
+import net.dongliu.apk.parser.struct.xml.XmlNamespaceStartTag;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * the xml file's namespaces.
+ *
+ * @author dongliu
+ */
+class XmlNamespaces {
+
+ private List namespaces;
+
+ private List newNamespaces;
+
+ public XmlNamespaces() {
+ this.namespaces = new ArrayList<>();
+ this.newNamespaces = new ArrayList<>();
+ }
+
+ public void addNamespace(XmlNamespaceStartTag tag) {
+ XmlNamespace namespace = new XmlNamespace(tag.getPrefix(), tag.getUri());
+ namespaces.add(namespace);
+ newNamespaces.add(namespace);
+ }
+
+ public void removeNamespace(XmlNamespaceEndTag tag) {
+ XmlNamespace namespace = new XmlNamespace(tag.getPrefix(), tag.getUri());
+ namespaces.remove(namespace);
+ newNamespaces.remove(namespace);
+ }
+
+ public String getPrefixViaUri(String uri) {
+ if (uri == null) {
+ return null;
+ }
+ for (XmlNamespace namespace : namespaces) {
+ if (namespace.uri.equals(uri)) {
+ return namespace.prefix;
+ }
+ }
+ return null;
+ }
+
+ public List consumeNameSpaces() {
+ if (!newNamespaces.isEmpty()) {
+ List xmlNamespaces = new ArrayList<>();
+ xmlNamespaces.addAll(newNamespaces);
+ newNamespaces.clear();
+ return xmlNamespaces;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * one namespace
+ */
+ public static class XmlNamespace {
+
+ private String prefix;
+ private String uri;
+
+ private XmlNamespace(String prefix, String uri) {
+ this.prefix = prefix;
+ this.uri = uri;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ XmlNamespace namespace = (XmlNamespace) o;
+
+ if (prefix == null && namespace.prefix != null) {
+ return false;
+ }
+ if (uri == null && namespace.uri != null) {
+ return false;
+ }
+ if (prefix != null && !prefix.equals(namespace.prefix)) {
+ return false;
+ }
+ if (uri != null && !uri.equals(namespace.uri)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = prefix.hashCode();
+ result = 31 * result + uri.hashCode();
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.java b/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.java
new file mode 100644
index 0000000..b061b7c
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/XmlStreamer.java
@@ -0,0 +1,21 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.struct.xml.*;
+
+/**
+ * callback interface for parse binary xml file.
+ *
+ * @author dongliu
+ */
+public interface XmlStreamer {
+
+ void onStartTag(XmlNodeStartTag xmlNodeStartTag);
+
+ void onEndTag(XmlNodeEndTag xmlNodeEndTag);
+
+ void onCData(XmlCData xmlCData);
+
+ void onNamespaceStart(XmlNamespaceStartTag tag);
+
+ void onNamespaceEnd(XmlNamespaceEndTag tag);
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java b/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java
new file mode 100644
index 0000000..18c9f87
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/XmlTranslator.java
@@ -0,0 +1,119 @@
+package net.dongliu.apk.parser.parser;
+
+import net.dongliu.apk.parser.struct.xml.*;
+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 {
+
+ private StringBuilder sb;
+ private int shift = 0;
+ private XmlNamespaces namespaces;
+ private boolean isLastStartTag;
+
+ public XmlTranslator() {
+ sb = new StringBuilder();
+ sb.append("\n");
+ this.namespaces = new XmlNamespaces();
+ }
+
+ @Override
+ public void onStartTag(XmlNodeStartTag xmlNodeStartTag) {
+ if (isLastStartTag) {
+ sb.append(">\n");
+ }
+ appendShift(shift++);
+ sb.append('<');
+ if (xmlNodeStartTag.getNamespace() != null) {
+ String prefix = namespaces.getPrefixViaUri(xmlNodeStartTag.getNamespace());
+ if (prefix != null) {
+ sb.append(prefix).append(":");
+ } else {
+ sb.append(xmlNodeStartTag.getNamespace()).append(":");
+ }
+ }
+ sb.append(xmlNodeStartTag.getName());
+
+ List nps = namespaces.consumeNameSpaces();
+ if (!nps.isEmpty()) {
+ for (XmlNamespaces.XmlNamespace np : nps) {
+ sb.append(" xmlns:").append(np.getPrefix()).append("=\"")
+ .append(np.getUri())
+ .append("\"");
+ }
+ }
+ isLastStartTag = true;
+
+ for (Attribute attribute : xmlNodeStartTag.getAttributes().values()) {
+ onAttribute(attribute);
+ }
+ }
+
+ private void onAttribute(Attribute attribute) {
+ sb.append(" ");
+ String namespace = this.namespaces.getPrefixViaUri(attribute.getNamespace());
+ if (namespace == null) {
+ namespace = attribute.getNamespace();
+ }
+ if (namespace != null && !namespace.isEmpty()) {
+ sb.append(namespace).append(':');
+ }
+ String escapedFinalValue = XmlEscaper.escapeXml10(attribute.getValue());
+ sb.append(attribute.getName()).append('=').append('"')
+ .append(escapedFinalValue).append('"');
+ }
+
+ @Override
+ public void onEndTag(XmlNodeEndTag xmlNodeEndTag) {
+ --shift;
+ if (isLastStartTag) {
+ sb.append(" />\n");
+ } else {
+ appendShift(shift);
+ sb.append("");
+ if (xmlNodeEndTag.getNamespace() != null) {
+ String namespace = this.namespaces.getPrefixViaUri(xmlNodeEndTag.getNamespace());
+ if (namespace == null) {
+ namespace = xmlNodeEndTag.getNamespace();
+ }
+ sb.append(namespace).append(":");
+ }
+ sb.append(xmlNodeEndTag.getName());
+ sb.append(">\n");
+ }
+ isLastStartTag = false;
+ }
+
+ @Override
+ public void onCData(XmlCData xmlCData) {
+ appendShift(shift);
+ sb.append(xmlCData.getValue()).append('\n');
+ isLastStartTag = false;
+ }
+
+ @Override
+ public void onNamespaceStart(XmlNamespaceStartTag tag) {
+ this.namespaces.addNamespace(tag);
+ }
+
+ @Override
+ public void onNamespaceEnd(XmlNamespaceEndTag tag) {
+ this.namespaces.removeNamespace(tag);
+ }
+
+ private void appendShift(int shift) {
+ for (int i = 0; i < shift; i++) {
+ sb.append("\t");
+ }
+ }
+
+ public String getXml() {
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/parser/package-info.java b/src/main/java/net/dongliu/apk/parser/parser/package-info.java
new file mode 100644
index 0000000..d6fef13
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/parser/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Parsers for Apk.
+ * Only for internal implementation, user should not depend on classes in this package directly.
+ */
+package net.dongliu.apk.parser.parser;
diff --git a/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.java b/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.java
new file mode 100644
index 0000000..57dce88
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/AndroidConstants.java
@@ -0,0 +1,42 @@
+package net.dongliu.apk.parser.struct;
+
+/**
+ * android system file.
+ *
+ * @author dongiu
+ */
+public class AndroidConstants {
+
+ public static final String RESOURCE_FILE = "resources.arsc";
+
+ public static final String MANIFEST_FILE = "AndroidManifest.xml";
+
+ public static final String DEX_FILE = "classes.dex";
+
+ public static final String DEX_ADDITIONAL = "classes%d.dex";
+
+ public static final String RES_PREFIX = "res/";
+
+ public static final String ASSETS_PREFIX = "assets/";
+
+ public static final String LIB_PREFIX = "lib/";
+
+ public static final String META_PREFIX = "META-INF/";
+
+ public static final String ARCH_ARMEABI = "";
+ /**
+ * the binary xml file used system attr id.
+ */
+ public static final int ATTR_ID_START = 0x01010000;
+
+ /**
+ * start offset for system android.R.style
+ */
+ public static final int SYS_STYLE_ID_START = 0x01030000;
+
+ /**
+ * end offset for system android.R.style
+ */
+ public static final int SYS_STYLE_ID_END = 0x01031000;
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.java b/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.java
new file mode 100644
index 0000000..5d88b8c
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/ChunkHeader.java
@@ -0,0 +1,69 @@
+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
+ */
+public class ChunkHeader {
+
+ // Type identifier for this chunk. The meaning of this value depends
+ // on the containing chunk.
+ private short chunkType;
+
+ // 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).
+ private short headerSize;
+
+ // 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.
+ private int chunkSize;
+
+ public ChunkHeader(int chunkType, int headerSize, long chunkSize) {
+ this.chunkType = Unsigned.toUShort(chunkType);
+ this.headerSize = Unsigned.toUShort(headerSize);
+ this.chunkSize = Unsigned.ensureUInt(chunkSize);
+ }
+
+ public int getBodySize() {
+ return this.chunkSize - this.headerSize;
+ }
+
+ public int getChunkType() {
+ return chunkType;
+ }
+
+ public void setChunkType(int chunkType) {
+ this.chunkType = Unsigned.toUShort(chunkType);
+ }
+
+ public int getHeaderSize() {
+ return headerSize;
+ }
+
+ public void setHeaderSize(int headerSize) {
+ this.headerSize = Unsigned.toUShort(headerSize);
+ }
+
+ public long getChunkSize() {
+ return chunkSize;
+ }
+
+ public void setChunkSize(long chunkSize) {
+ this.chunkSize = Unsigned.ensureUInt(chunkSize);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/ChunkType.java b/src/main/java/net/dongliu/apk/parser/struct/ChunkType.java
new file mode 100644
index 0000000..d39a148
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/ChunkType.java
@@ -0,0 +1,36 @@
+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
+ */
+public class ChunkType {
+
+ public static final int NULL = 0x0000;
+ public static final int STRING_POOL = 0x0001;
+ public static final int TABLE = 0x0002;
+ public static final int XML = 0x0003;
+
+ // Chunk types in XML
+ public static final int XML_FIRST_CHUNK = 0x0100;
+ public static final int XML_START_NAMESPACE = 0x0100;
+ public static final int XML_END_NAMESPACE = 0x0101;
+ public static final int XML_START_ELEMENT = 0x0102;
+ public static final int XML_END_ELEMENT = 0x0103;
+ public static final int XML_CDATA = 0x0104;
+ public static final int XML_LAST_CHUNK = 0x017f;
+ // This contains a uint32_t array mapping strings in the string
+ // pool back to resource identifiers. It is optional.
+ public static final int XML_RESOURCE_MAP = 0x0180;
+
+ // Chunk types in RES_TABLE_TYPE
+ public static final int TABLE_PACKAGE = 0x0200;
+ public static final int TABLE_TYPE = 0x0201;
+ public static final int TABLE_TYPE_SPEC = 0x0202;
+ // android5.0+
+ // DynamicRefTable
+ public static final int TABLE_LIBRARY = 0x0203;
+ //TODO: fix this later. Do not found definition for chunk type 0x0204 in android source yet...
+ public static final int UNKNOWN_YET = 0x0204;
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/ResValue.java b/src/main/java/net/dongliu/apk/parser/struct/ResValue.java
new file mode 100644
index 0000000..170a671
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/ResValue.java
@@ -0,0 +1,143 @@
+package net.dongliu.apk.parser.struct;
+
+import javax.annotation.Nullable;
+
+/**
+ * Apk res value struct. Only for description now, The value is hold in ResourceValue
+ *
+ * @author dongliu
+ */
+public class ResValue {
+
+ // Number of bytes in this structure. uint16; always 8
+ private int size;
+ // Always set to 0. uint8
+ private short res0;
+ // Type of the data value. uint8
+ private short dataType;
+ // The data for this item; as interpreted according to dataType. unit32
+
+ /*
+ * The data field is a fixed size 32-bit integer.
+ * How it is interpreted depends upon the value of the type field.
+ * Some of the possible interpretations are as
+ * a boolean value
+ * a float value
+ * an integer value
+ * an index into the Table chunk’s StringPool
+ * a composite value
+ */
+ /**
+ * the real data represented by string
+ */
+ @Nullable
+ private ResourceValue data;
+
+ @Override
+ public String toString() {
+ return "ResValue{"
+ + "size=" + size
+ + ", res0=" + res0
+ + ", dataType=" + dataType
+ + ", data=" + data
+ + '}';
+ }
+
+ public static class ResType {
+
+ // Contains no data.
+ public static final short NULL = 0x00;
+ // The 'data' holds a ResTable_ref; a reference to another resource
+ // table entry.
+ public static final short REFERENCE = 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.
+ public static final short STRING = 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".
+ public static final short DIMENSION = 0x05;
+ // The 'data' holds a complex number encoding a fraction of a
+ // container.
+ public static final short FRACTION = 0x06;
+
+ // Beginning of integer flavors...
+ public static final short FIRST_INT = 0x10;
+
+ // The 'data' is a raw integer value of the form n..n.
+ public static final short INT_DEC = 0x10;
+ // The 'data' is a raw integer value of the form 0xn..n.
+ public static final short INT_HEX = 0x11;
+ // The 'data' is either 0 or 1; for input "false" or "true" respectively.
+ public static final short INT_BOOLEAN = 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.
+ public static final short INT_COLOR_ARGB8 = 0x1c;
+ // The 'data' is a raw integer value of the form #rrggbb.
+ public static final short INT_COLOR_RGB8 = 0x1d;
+ // The 'data' is a raw integer value of the form #argb.
+ public static final short INT_COLOR_ARGB4 = 0x1e;
+ // The 'data' is a raw integer value of the form #rgb.
+ public static final short INT_COLOR_RGB4 = 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
+ public static class 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.
+ public static final short UNIT_PX = 0;
+ // TYPE_DIMENSION: Value is Device Independent Pixels.
+ public static final short UNIT_DIP = 1;
+ // TYPE_DIMENSION: Value is a Scaled device independent Pixels.
+ public static final short UNIT_SP = 2;
+ // TYPE_DIMENSION: Value is in points.
+ public static final short UNIT_PT = 3;
+ // TYPE_DIMENSION: Value is in inches.
+ public static final short UNIT_IN = 4;
+ // TYPE_DIMENSION: Value is in millimeters.
+ public static final short UNIT_MM = 5;
+
+ // TYPE_FRACTION: A basic fraction of the overall size.
+ public static final short UNIT_FRACTION = 0;
+ // TYPE_FRACTION: A fraction of the parent size.
+ public static final short UNIT_FRACTION_PARENT = 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/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java b/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java
new file mode 100644
index 0000000..0804dbf
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/ResourceValue.java
@@ -0,0 +1,305 @@
+package net.dongliu.apk.parser.struct;
+
+import net.dongliu.apk.parser.struct.resource.*;
+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(int value) {
+ this.value = value;
+ }
+
+ /**
+ * get value as string.
+ */
+ public abstract String toStringValue(ResourceTable resourceTable, Locale locale);
+
+ public static ResourceValue decimal(int value) {
+ return new DecimalResourceValue(value);
+ }
+
+ public static ResourceValue hexadecimal(int value) {
+ return new HexadecimalResourceValue(value);
+ }
+
+ public static ResourceValue bool(int value) {
+ return new BooleanResourceValue(value);
+ }
+
+ public static ResourceValue string(int value, StringPool stringPool) {
+ return new StringResourceValue(value, stringPool);
+ }
+
+ public static ResourceValue reference(int value) {
+ return new ReferenceResourceValue(value);
+ }
+
+ public static ResourceValue nullValue() {
+ return NullResourceValue.instance;
+ }
+
+ public static ResourceValue rgb(int value, int len) {
+ return new RGBResourceValue(value, len);
+ }
+
+ public static ResourceValue dimension(int value) {
+ return new DimensionValue(value);
+ }
+
+ public static ResourceValue fraction(int value) {
+ return new FractionValue(value);
+ }
+
+ public static ResourceValue raw(int value, short type) {
+ return new RawValue(value, type);
+ }
+
+ private static class DecimalResourceValue extends ResourceValue {
+
+ private DecimalResourceValue(int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ return String.valueOf(value);
+ }
+ }
+
+ private static class HexadecimalResourceValue extends ResourceValue {
+
+ private HexadecimalResourceValue(int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ return "0x" + Integer.toHexString(value);
+ }
+ }
+
+ private static class BooleanResourceValue extends ResourceValue {
+
+ private BooleanResourceValue(int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ return String.valueOf(value != 0);
+ }
+ }
+
+ private static class StringResourceValue extends ResourceValue {
+
+ private final StringPool stringPool;
+
+ private StringResourceValue(int value, StringPool stringPool) {
+ super(value);
+ this.stringPool = stringPool;
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ if (value >= 0) {
+ return stringPool.get(value);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return value + ":" + stringPool.get(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(int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ long resourceId = 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);
+ }
+
+ String raw = "resourceId:0x" + Long.toHexString(resourceId);
+ if (resourceTable == null) {
+ return raw;
+ }
+
+ List resources = resourceTable.getResourcesById(resourceId);
+ // read from type resource
+ ResourceEntry selected = null;
+ TypeSpec typeSpec = null;
+ int currentLocalMatchLevel = -1;
+ int currentDensityLevel = -1;
+ for (ResourceTable.Resource resource : resources) {
+ Type type = resource.getType();
+ typeSpec = resource.getTypeSpec();
+ ResourceEntry resourceEntry = resource.getResourceEntry();
+ int localMatchLevel = Locales.match(locale, type.getLocale());
+ int densityLevel = densityLevel(type.getDensity());
+ if (localMatchLevel > currentLocalMatchLevel) {
+ selected = resourceEntry;
+ currentLocalMatchLevel = localMatchLevel;
+ currentDensityLevel = densityLevel;
+ } else if (densityLevel > currentDensityLevel) {
+ selected = resourceEntry;
+ currentDensityLevel = densityLevel;
+ }
+ }
+ String result;
+ if (selected == null) {
+ result = raw;
+ } else if (locale == null) {
+ result = "@" + typeSpec.getName() + "/" + selected.getKey();
+ } else {
+ result = selected.toStringValue(resourceTable, locale);
+ }
+ return result;
+ }
+
+ public long getReferenceResourceId() {
+ return value & 0xFFFFFFFFL;
+ }
+
+ private static int densityLevel(int density) {
+ if (density == Densities.ANY || density == Densities.NONE) {
+ return -1;
+ }
+ if (density == Densities.DEFAULT) {
+ return Densities.DEFAULT;
+ }
+ return density;
+ }
+ }
+
+ private static class NullResourceValue extends ResourceValue {
+
+ private static final NullResourceValue instance = new NullResourceValue();
+
+ private NullResourceValue() {
+ super(-1);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ return "";
+ }
+ }
+
+ private static class RGBResourceValue extends ResourceValue {
+
+ private final int len;
+
+ private RGBResourceValue(int value, int len) {
+ super(value);
+ this.len = len;
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = len / 2 - 1; i >= 0; i--) {
+ sb.append(Integer.toHexString((value >> i * 8) & 0xff));
+ }
+ return sb.toString();
+ }
+ }
+
+ private static class DimensionValue extends ResourceValue {
+
+ private DimensionValue(int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ short unit = (short) (value & 0xff);
+ 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 (value >> 8) + unitStr;
+ }
+ }
+
+ private static class FractionValue extends ResourceValue {
+
+ private FractionValue(int value) {
+ super(value);
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ // The low-order 4 bits of the data value specify the type of the fraction
+ short type = (short) (value & 0xf);
+ 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);
+ }
+ float f = Float.intBitsToFloat(value >> 4);
+ return f + pstr;
+ }
+ }
+
+ private static class RawValue extends ResourceValue {
+
+ private final short dataType;
+
+ private RawValue(int value, short dataType) {
+ super(value);
+ this.dataType = dataType;
+ }
+
+ @Override
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ return "{" + dataType + ":" + (value & 0xFFFFFFFFL) + "}";
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/StringPool.java b/src/main/java/net/dongliu/apk/parser/struct/StringPool.java
new file mode 100644
index 0000000..163d8ed
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/StringPool.java
@@ -0,0 +1,23 @@
+package net.dongliu.apk.parser.struct;
+
+/**
+ * String pool.
+ *
+ * @author dongliu
+ */
+public class StringPool {
+
+ private String[] pool;
+
+ public StringPool(int poolSize) {
+ pool = new String[poolSize];
+ }
+
+ public String get(int idx) {
+ return pool[idx];
+ }
+
+ public void set(int idx, String value) {
+ pool[idx] = value;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.java b/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.java
new file mode 100644
index 0000000..c046616
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/StringPoolHeader.java
@@ -0,0 +1,73 @@
+package net.dongliu.apk.parser.struct;
+
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * String pool chunk header.
+ *
+ * @author dongliu
+ */
+public class StringPoolHeader extends ChunkHeader {
+
+ public StringPoolHeader(int headerSize, long chunkSize) {
+ super(ChunkType.STRING_POOL, headerSize, chunkSize);
+ }
+
+ // Number of style span arrays in the pool (number of uint32_t indices
+ // follow the string indices).
+ private int stringCount;
+ // Number of style span arrays in the pool (number of uint32_t indices
+ // follow the string indices).
+ private int styleCount;
+
+ // If set, the string index is sorted by the string values (based on strcmp16()).
+ public static final int SORTED_FLAG = 1;
+ // String pool is encoded in UTF-8
+ public static final int UTF8_FLAG = 1 << 8;
+ private long flags;
+
+ // Index from header of the string data.
+ private long stringsStart;
+ // Index from header of the style data.
+ private long stylesStart;
+
+ public int getStringCount() {
+ return stringCount;
+ }
+
+ public void setStringCount(long stringCount) {
+ this.stringCount = Unsigned.ensureUInt(stringCount);
+ }
+
+ public int getStyleCount() {
+ return styleCount;
+ }
+
+ public void setStyleCount(long styleCount) {
+ this.styleCount = Unsigned.ensureUInt(styleCount);
+ }
+
+ public long getFlags() {
+ return flags;
+ }
+
+ public void setFlags(long flags) {
+ this.flags = flags;
+ }
+
+ public long getStringsStart() {
+ return stringsStart;
+ }
+
+ public void setStringsStart(long stringsStart) {
+ this.stringsStart = stringsStart;
+ }
+
+ public long getStylesStart() {
+ return stylesStart;
+ }
+
+ public void setStylesStart(long stylesStart) {
+ this.stylesStart = stylesStart;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java b/src/main/java/net/dongliu/apk/parser/struct/dex/DexClassStruct.java
new file mode 100644
index 0000000..a666896
--- /dev/null
+++ b/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 int ACC_PUBLIC = 0x1;
+ public static int ACC_PRIVATE = 0x2;
+ public static int ACC_PROTECTED = 0x4;
+ public static 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 int ACC_INTERFACE = 0x200;
+ public static int ACC_ABSTRACT = 0x400;
+ public static int ACC_STRICT = 0x800;
+ public static int ACC_SYNTHETIC = 0x1000;
+ public static int ACC_ANNOTATION = 0x2000;
+ public static int ACC_ENUM = 0x4000;
+ public static int ACC_CONSTRUCTOR = 0x10000;
+ public static int ACC_DECLARED_SYNCHRONIZED = 0x20000;
+
+ public int getClassIdx() {
+ return classIdx;
+ }
+
+ public void setClassIdx(int classIdx) {
+ this.classIdx = classIdx;
+ }
+
+ public int getAccessFlags() {
+ return accessFlags;
+ }
+
+ public void setAccessFlags(int accessFlags) {
+ this.accessFlags = accessFlags;
+ }
+
+ public int getSuperclassIdx() {
+ return superclassIdx;
+ }
+
+ public void setSuperclassIdx(int superclassIdx) {
+ this.superclassIdx = superclassIdx;
+ }
+
+ public long getInterfacesOff() {
+ return interfacesOff;
+ }
+
+ public void setInterfacesOff(long interfacesOff) {
+ this.interfacesOff = interfacesOff;
+ }
+
+ public int getSourceFileIdx() {
+ return sourceFileIdx;
+ }
+
+ public void setSourceFileIdx(int sourceFileIdx) {
+ this.sourceFileIdx = sourceFileIdx;
+ }
+
+ public long getAnnotationsOff() {
+ return annotationsOff;
+ }
+
+ public void setAnnotationsOff(long annotationsOff) {
+ this.annotationsOff = annotationsOff;
+ }
+
+ public long getClassDataOff() {
+ return classDataOff;
+ }
+
+ public void setClassDataOff(long classDataOff) {
+ this.classDataOff = classDataOff;
+ }
+
+ public long getStaticValuesOff() {
+ return staticValuesOff;
+ }
+
+ public void setStaticValuesOff(long staticValuesOff) {
+ this.staticValuesOff = staticValuesOff;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java b/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java
new file mode 100644
index 0000000..d438fed
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/dex/DexHeader.java
@@ -0,0 +1,228 @@
+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 = 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 version;
+ }
+
+ public void setVersion(int version) {
+ this.version = version;
+ }
+
+ public byte[] getSignature() {
+ return signature;
+ }
+
+ public void setSignature(byte[] signature) {
+ this.signature = signature;
+ }
+
+ public long getFileSize() {
+ return fileSize;
+ }
+
+ public void setFileSize(long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ public long getHeaderSize() {
+ return headerSize;
+ }
+
+ public void setHeaderSize(long headerSize) {
+ this.headerSize = headerSize;
+ }
+
+ public long getLinkSize() {
+ return linkSize;
+ }
+
+ public void setLinkSize(long linkSize) {
+ this.linkSize = linkSize;
+ }
+
+ public long getLinkOff() {
+ return linkOff;
+ }
+
+ public void setLinkOff(long linkOff) {
+ this.linkOff = linkOff;
+ }
+
+ public long getMapOff() {
+ return mapOff;
+ }
+
+ public void setMapOff(long mapOff) {
+ this.mapOff = mapOff;
+ }
+
+ public int getStringIdsSize() {
+ return stringIdsSize;
+ }
+
+ public void setStringIdsSize(int stringIdsSize) {
+ this.stringIdsSize = stringIdsSize;
+ }
+
+ public long getStringIdsOff() {
+ return stringIdsOff;
+ }
+
+ public void setStringIdsOff(long stringIdsOff) {
+ this.stringIdsOff = stringIdsOff;
+ }
+
+ public int getTypeIdsSize() {
+ return typeIdsSize;
+ }
+
+ public void setTypeIdsSize(int typeIdsSize) {
+ this.typeIdsSize = typeIdsSize;
+ }
+
+ public long getTypeIdsOff() {
+ return typeIdsOff;
+ }
+
+ public void setTypeIdsOff(long typeIdsOff) {
+ this.typeIdsOff = typeIdsOff;
+ }
+
+ public int getProtoIdsSize() {
+ return protoIdsSize;
+ }
+
+ public void setProtoIdsSize(int protoIdsSize) {
+ this.protoIdsSize = protoIdsSize;
+ }
+
+ public long getProtoIdsOff() {
+ return protoIdsOff;
+ }
+
+ public void setProtoIdsOff(long protoIdsOff) {
+ this.protoIdsOff = protoIdsOff;
+ }
+
+ public int getFieldIdsSize() {
+ return fieldIdsSize;
+ }
+
+ public void setFieldIdsSize(int fieldIdsSize) {
+ this.fieldIdsSize = fieldIdsSize;
+ }
+
+ public long getFieldIdsOff() {
+ return fieldIdsOff;
+ }
+
+ public void setFieldIdsOff(long fieldIdsOff) {
+ this.fieldIdsOff = fieldIdsOff;
+ }
+
+ public int getMethodIdsSize() {
+ return methodIdsSize;
+ }
+
+ public void setMethodIdsSize(int methodIdsSize) {
+ this.methodIdsSize = methodIdsSize;
+ }
+
+ public long getMethodIdsOff() {
+ return methodIdsOff;
+ }
+
+ public void setMethodIdsOff(long methodIdsOff) {
+ this.methodIdsOff = methodIdsOff;
+ }
+
+ public int getClassDefsSize() {
+ return classDefsSize;
+ }
+
+ public void setClassDefsSize(int classDefsSize) {
+ this.classDefsSize = classDefsSize;
+ }
+
+ public long getClassDefsOff() {
+ return classDefsOff;
+ }
+
+ public void setClassDefsOff(long classDefsOff) {
+ this.classDefsOff = classDefsOff;
+ }
+
+ public int getDataSize() {
+ return dataSize;
+ }
+
+ public void setDataSize(int dataSize) {
+ this.dataSize = dataSize;
+ }
+
+ public long getDataOff() {
+ return dataOff;
+ }
+
+ public void setDataOff(long dataOff) {
+ this.dataOff = dataOff;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/package-info.java b/src/main/java/net/dongliu/apk/parser/struct/package-info.java
new file mode 100644
index 0000000..3bd1778
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Only for internal implementation!
+ */
+package net.dongliu.apk.parser.struct;
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.java b/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.java
new file mode 100644
index 0000000..618f655
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/Densities.java
@@ -0,0 +1,20 @@
+package net.dongliu.apk.parser.struct.resource;
+
+/**
+ * Screen density values
+ */
+public abstract class Densities {
+
+ public static final int DEFAULT = 0;
+ public static final int LOW = 120;
+ public static final int MEDIUM = 160;
+ public static final int TV = 213;
+ public static final int HIGH = 240;
+ public static final int XHIGH = 320;
+ public static final int XXHIGH = 480;
+ public static final int XXXHIGH = 640;
+
+ public static final int ANY = 0xfffe;
+ public static final int NONE = 0xffff;
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java b/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java
new file mode 100644
index 0000000..c31081f
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryEntry.java
@@ -0,0 +1,20 @@
+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.
+ private int packageId;
+
+ //The package name of the shared library. \0 terminated. max 128
+ private String name;
+
+ public LibraryEntry(int packageId, String name) {
+ this.packageId = packageId;
+ this.name = name;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.java b/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.java
new file mode 100644
index 0000000..59847ec
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/LibraryHeader.java
@@ -0,0 +1,33 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * Table library chunk header
+ *
+ * @author Liu Dong
+ */
+public class LibraryHeader extends ChunkHeader {
+
+ /**
+ * A package-id to package name mapping for any shared libraries used in this resource table. The package-id's encoded in this resource table may be different than the id's assigned at runtime. We must be able to translate the package-id's based on the package name.
+ */
+ /**
+ * uint32 value, The number of shared libraries linked in this resource table.
+ */
+ private int count;
+
+ public LibraryHeader(int headerSize, long chunkSize) {
+ super(ChunkType.TABLE_LIBRARY, headerSize, chunkSize);
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public void setCount(long count) {
+ this.count = Unsigned.ensureUInt(count);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.java b/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.java
new file mode 100644
index 0000000..fecec81
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/NullHeader.java
@@ -0,0 +1,11 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+
+public class NullHeader extends ChunkHeader {
+
+ public NullHeader(int headerSize, int chunkSize) {
+ super(ChunkType.NULL, headerSize, chunkSize);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java b/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java
new file mode 100644
index 0000000..678fe3e
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/PackageHeader.java
@@ -0,0 +1,96 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * @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]
+ private 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 int typeStrings;
+
+ // Last index into typeStrings that is for public use by others.
+ // uint32_t
+ private 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 int keyStrings;
+
+ // Last index into keyStrings that is for public use by others.
+ // uint32_t
+ private int lastPublicKey;
+
+ public PackageHeader(int headerSize, long chunkSize) {
+ super(ChunkType.TABLE_PACKAGE, headerSize, chunkSize);
+ }
+
+ public long getId() {
+ return Unsigned.toLong(id);
+ }
+
+ public void setId(long id) {
+ this.id = Unsigned.toUInt(id);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getTypeStrings() {
+ return typeStrings;
+ }
+
+ public void setTypeStrings(long typeStrings) {
+ this.typeStrings = Unsigned.ensureUInt(typeStrings);
+ }
+
+ public int getLastPublicType() {
+ return lastPublicType;
+ }
+
+ public void setLastPublicType(long lastPublicType) {
+ this.lastPublicType = Unsigned.ensureUInt(lastPublicType);
+ }
+
+ public int getKeyStrings() {
+ return keyStrings;
+ }
+
+ public void setKeyStrings(long keyStrings) {
+ this.keyStrings = Unsigned.ensureUInt(keyStrings);
+ }
+
+ public int getLastPublicKey() {
+ return lastPublicKey;
+ }
+
+ public void setLastPublicKey(long lastPublicKey) {
+ this.lastPublicKey = Unsigned.ensureUInt(lastPublicKey);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java
new file mode 100644
index 0000000..53171e6
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResTableConfig.java
@@ -0,0 +1,227 @@
+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 size;
+ }
+
+ public void setSize(long size) {
+ this.size = Unsigned.ensureUInt(size);
+ }
+
+ public short getMcc() {
+ return mcc;
+ }
+
+ public void setMcc(short mcc) {
+ this.mcc = mcc;
+ }
+
+ public short getMnc() {
+ return mnc;
+ }
+
+ public void setMnc(short mnc) {
+ this.mnc = mnc;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public short getOrientation() {
+ return (short) (orientation & 0xff);
+ }
+
+ public void setOrientation(short orientation) {
+ this.orientation = (byte) orientation;
+ }
+
+ public short getTouchscreen() {
+ return (short) (touchscreen & 0xff);
+ }
+
+ public void setTouchscreen(short touchscreen) {
+ this.touchscreen = (byte) touchscreen;
+ }
+
+ public int getDensity() {
+ return density & 0xffff;
+ }
+
+ public void setDensity(int density) {
+ this.density = (short) density;
+ }
+
+ public short getKeyboard() {
+ return keyboard;
+ }
+
+ public void setKeyboard(short keyboard) {
+ this.keyboard = keyboard;
+ }
+
+ public short getNavigation() {
+ return navigation;
+ }
+
+ public void setNavigation(short navigation) {
+ this.navigation = navigation;
+ }
+
+ public short getInputFlags() {
+ return inputFlags;
+ }
+
+ public void setInputFlags(short inputFlags) {
+ this.inputFlags = inputFlags;
+ }
+
+ public short getInputPad0() {
+ return inputPad0;
+ }
+
+ public void setInputPad0(short inputPad0) {
+ this.inputPad0 = inputPad0;
+ }
+
+ public int getScreenWidth() {
+ return screenWidth;
+ }
+
+ public void setScreenWidth(int screenWidth) {
+ this.screenWidth = screenWidth;
+ }
+
+ public int getScreenHeight() {
+ return screenHeight;
+ }
+
+ public void setScreenHeight(int screenHeight) {
+ this.screenHeight = screenHeight;
+ }
+
+ public int getSdkVersion() {
+ return sdkVersion;
+ }
+
+ public void setSdkVersion(int sdkVersion) {
+ this.sdkVersion = sdkVersion;
+ }
+
+ public int getMinorVersion() {
+ return minorVersion;
+ }
+
+ public void setMinorVersion(int minorVersion) {
+ this.minorVersion = minorVersion;
+ }
+
+ public short getScreenLayout() {
+ return screenLayout;
+ }
+
+ public void setScreenLayout(short screenLayout) {
+ this.screenLayout = screenLayout;
+ }
+
+ public short getUiMode() {
+ return uiMode;
+ }
+
+ public void setUiMode(short uiMode) {
+ this.uiMode = uiMode;
+ }
+
+ public short getScreenConfigPad1() {
+ return screenConfigPad1;
+ }
+
+ public void setScreenConfigPad1(short screenConfigPad1) {
+ this.screenConfigPad1 = screenConfigPad1;
+ }
+
+ public short getScreenConfigPad2() {
+ return screenConfigPad2;
+ }
+
+ public void setScreenConfigPad2(short screenConfigPad2) {
+ this.screenConfigPad2 = screenConfigPad2;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java
new file mode 100644
index 0000000..aa5cf6d
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceEntry.java
@@ -0,0 +1,90 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+
+import javax.annotation.Nullable;
+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
+ private 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;
+ // uint16_t
+ private int flags;
+
+ // Reference into ResTable_package::keyStrings identifying this entry.
+ //public long keyRef;
+ private String key;
+
+ // the resvalue following this resource entry.
+ private ResourceValue value;
+
+ /**
+ * get value as string
+ *
+ * @return
+ */
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ if (value != null) {
+ return value.toStringValue(resourceTable, locale);
+ } else {
+ return "null";
+ }
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public void setSize(int size) {
+ this.size = size;
+ }
+
+ public int getFlags() {
+ return flags;
+ }
+
+ public void setFlags(int flags) {
+ this.flags = flags;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ @Nullable
+ public ResourceValue getValue() {
+ return value;
+ }
+
+ @Nullable
+ public void setValue(@Nullable ResourceValue value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "ResourceEntry{"
+ + "size=" + size
+ + ", flags=" + flags
+ + ", key='" + key + '\''
+ + ", value=" + value
+ + '}';
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java
new file mode 100644
index 0000000..a31cabd
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceMapEntry.java
@@ -0,0 +1,72 @@
+package net.dongliu.apk.parser.struct.resource;
+
+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; };
+ private long parent;
+
+ // Number of name/value pairs that follow for FLAG_COMPLEX. uint32_t
+ private long count;
+
+ private ResourceTableMap[] resourceTableMaps;
+
+ public ResourceMapEntry(ResourceEntry resourceEntry) {
+ this.setSize(resourceEntry.getSize());
+ this.setFlags(resourceEntry.getFlags());
+ this.setKey(resourceEntry.getKey());
+ }
+
+ public long getParent() {
+ return parent;
+ }
+
+ public void setParent(long parent) {
+ this.parent = parent;
+ }
+
+ public long getCount() {
+ return count;
+ }
+
+ public void setCount(long count) {
+ this.count = count;
+ }
+
+ public ResourceTableMap[] getResourceTableMaps() {
+ return resourceTableMaps;
+ }
+
+ public void setResourceTableMaps(ResourceTableMap[] resourceTableMaps) {
+ this.resourceTableMaps = resourceTableMaps;
+ }
+
+ /**
+ * get value as string
+ *
+ * @return
+ */
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ if (resourceTableMaps.length > 0) {
+ return resourceTableMaps[0].toString();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ResourceMapEntry{"
+ + "parent=" + parent
+ + ", count=" + count
+ + ", resourceTableMaps=" + Arrays.toString(resourceTableMaps)
+ + '}';
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java
new file mode 100644
index 0000000..25add59
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourcePackage.java
@@ -0,0 +1,105 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.StringPool;
+
+import javax.annotation.Nullable;
+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(PackageHeader header) {
+ this.name = header.getName();
+ this.id = (short) header.getId();
+ }
+
+ private Map typeSpecMap = new HashMap<>();
+
+ private Map> typesMap = new HashMap<>();
+
+ public void addTypeSpec(TypeSpec typeSpec) {
+ this.typeSpecMap.put(typeSpec.getId(), typeSpec);
+ }
+
+ @Nullable
+ public TypeSpec getTypeSpec(short id) {
+ return this.typeSpecMap.get(id);
+ }
+
+ public void addType(Type type) {
+ List types = this.typesMap.get(type.getId());
+ if (types == null) {
+ types = new ArrayList<>();
+ this.typesMap.put(type.getId(), types);
+ }
+ types.add(type);
+ }
+
+ @Nullable
+ public List getTypes(short id) {
+ return this.typesMap.get(id);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public short getId() {
+ return id;
+ }
+
+ public void setId(short id) {
+ this.id = id;
+ }
+
+ public StringPool getTypeStringPool() {
+ return typeStringPool;
+ }
+
+ public void setTypeStringPool(StringPool typeStringPool) {
+ this.typeStringPool = typeStringPool;
+ }
+
+ public StringPool getKeyStringPool() {
+ return keyStringPool;
+ }
+
+ public void setKeyStringPool(StringPool keyStringPool) {
+ this.keyStringPool = keyStringPool;
+ }
+
+ public Map getTypeSpecMap() {
+ return typeSpecMap;
+ }
+
+ public void setTypeSpecMap(Map typeSpecMap) {
+ this.typeSpecMap = typeSpecMap;
+ }
+
+ public Map> getTypesMap() {
+ return typesMap;
+ }
+
+ public void setTypesMap(Map> typesMap) {
+ this.typesMap = typesMap;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java
new file mode 100644
index 0000000..0b42aaa
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTable.java
@@ -0,0 +1,116 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ResourceValue;
+import net.dongliu.apk.parser.struct.StringPool;
+import net.dongliu.apk.parser.utils.ResourceLoader;
+
+import javax.annotation.Nonnull;
+import java.util.*;
+
+/**
+ * The apk resource table
+ *
+ * @author dongliu
+ */
+public class ResourceTable {
+
+ private Map packageMap = new HashMap<>();
+ private StringPool stringPool;
+
+ public static Map sysStyle = ResourceLoader.loadSystemStyles();
+
+ public void addPackage(ResourcePackage resourcePackage) {
+ this.packageMap.put(resourcePackage.getId(), resourcePackage);
+ }
+
+ public ResourcePackage getPackage(short id) {
+ return this.packageMap.get(id);
+ }
+
+ public StringPool getStringPool() {
+ return stringPool;
+ }
+
+ public void setStringPool(StringPool stringPool) {
+ this.stringPool = stringPool;
+ }
+
+ /**
+ * Get resources match the given resource id.
+ */
+ @Nonnull
+ public List getResourcesById(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]
+
+ short packageId = (short) (resourceId >> 24 & 0xff);
+ short typeId = (short) ((resourceId >> 16) & 0xff);
+ int entryIndex = (int) (resourceId & 0xffff);
+ ResourcePackage resourcePackage = this.getPackage(packageId);
+ if (resourcePackage == null) {
+ return Collections.emptyList();
+ }
+ TypeSpec typeSpec = resourcePackage.getTypeSpec(typeId);
+ List types = resourcePackage.getTypes(typeId);
+ if (typeSpec == null || types == null) {
+ return Collections.emptyList();
+ }
+ if (!typeSpec.exists(entryIndex)) {
+ return Collections.emptyList();
+ }
+
+ // read from type resource
+ List result = new ArrayList<>();
+ for (Type type : types) {
+ ResourceEntry resourceEntry = type.getResourceEntry(entryIndex);
+ if (resourceEntry == null) {
+ continue;
+ }
+ ResourceValue currentResourceValue = resourceEntry.getValue();
+ 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 {
+
+ private TypeSpec typeSpec;
+ private Type type;
+ private ResourceEntry resourceEntry;
+
+ public Resource(TypeSpec typeSpec, Type type, ResourceEntry resourceEntry) {
+ this.typeSpec = typeSpec;
+ this.type = type;
+ this.resourceEntry = resourceEntry;
+ }
+
+ public TypeSpec getTypeSpec() {
+ return typeSpec;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public ResourceEntry getResourceEntry() {
+ return resourceEntry;
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java
new file mode 100644
index 0000000..6b87058
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableHeader.java
@@ -0,0 +1,28 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * resource file header
+ *
+ * @author dongliu
+ */
+public class ResourceTableHeader extends ChunkHeader {
+
+ // The number of ResTable_package structures. uint32
+ private int packageCount;
+
+ public ResourceTableHeader(int headerSize, int chunkSize) {
+ super(ChunkType.TABLE, headerSize, chunkSize);
+ }
+
+ public long getPackageCount() {
+ return Unsigned.toLong(packageCount);
+ }
+
+ public void setPackageCount(long packageCount) {
+ this.packageCount = Unsigned.toUInt(packageCount);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java
new file mode 100644
index 0000000..7452593
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/ResourceTableMap.java
@@ -0,0 +1,115 @@
+package net.dongliu.apk.parser.struct.resource;
+
+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 nameRef;
+ }
+
+ public void setNameRef(long nameRef) {
+ this.nameRef = nameRef;
+ }
+
+ public ResourceValue getResValue() {
+ return resValue;
+ }
+
+ public void setResValue(ResourceValue resValue) {
+ this.resValue = resValue;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+ @Override
+ public String toString() {
+ return data;
+ }
+
+ public static class MapAttr {
+
+ 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(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/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java b/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java
new file mode 100644
index 0000000..8a7ce89
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/Type.java
@@ -0,0 +1,166 @@
+package net.dongliu.apk.parser.struct.resource;
+
+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;
+ private short id;
+
+ private Locale locale;
+
+ private StringPool keyStringPool;
+ private ByteBuffer buffer;
+ private long[] offsets;
+ private StringPool stringPool;
+
+ // see Densities.java for values
+ private int density;
+
+ public Type(TypeHeader header) {
+ this.id = header.getId();
+ ResTableConfig config = header.getConfig();
+ this.locale = new Locale(config.getLanguage(), config.getCountry());
+ this.density = config.getDensity();
+ }
+
+ public ResourceEntry getResourceEntry(int id) {
+ if (id >= offsets.length) {
+ return null;
+ }
+
+ if (offsets[id] == TypeHeader.NO_ENTRY) {
+ return null;
+ }
+
+ // read Resource Entries
+ Buffers.position(buffer, offsets[id]);
+ return readResourceEntry();
+ }
+
+ private ResourceEntry readResourceEntry() {
+ long beginPos = buffer.position();
+ ResourceEntry resourceEntry = new ResourceEntry();
+ // size is always 8(simple), or 16(complex)
+ resourceEntry.setSize(Buffers.readUShort(buffer));
+ resourceEntry.setFlags(Buffers.readUShort(buffer));
+ long keyRef = buffer.getInt();
+ String key = keyStringPool.get((int) keyRef);
+ resourceEntry.setKey(key);
+
+ if ((resourceEntry.getFlags() & ResourceEntry.FLAG_COMPLEX) != 0) {
+ ResourceMapEntry resourceMapEntry = new ResourceMapEntry(resourceEntry);
+
+ // Resource identifier of the parent mapping, or 0 if there is none.
+ resourceMapEntry.setParent(Buffers.readUInt(buffer));
+ resourceMapEntry.setCount(Buffers.readUInt(buffer));
+
+ Buffers.position(buffer, beginPos + resourceEntry.getSize());
+
+ //An individual complex Resource entry comprises an entry immediately followed by one or more fields.
+ ResourceTableMap[] resourceTableMaps = new ResourceTableMap[(int) resourceMapEntry.getCount()];
+ for (int i = 0; i < resourceMapEntry.getCount(); i++) {
+ resourceTableMaps[i] = readResourceTableMap();
+ }
+
+ resourceMapEntry.setResourceTableMaps(resourceTableMaps);
+ return resourceMapEntry;
+ } else {
+ Buffers.position(buffer, beginPos + resourceEntry.getSize());
+ resourceEntry.setValue(ParseUtils.readResValue(buffer, stringPool));
+ return resourceEntry;
+ }
+ }
+
+ private ResourceTableMap readResourceTableMap() {
+ ResourceTableMap resourceTableMap = new ResourceTableMap();
+ resourceTableMap.setNameRef(Buffers.readUInt(buffer));
+ resourceTableMap.setResValue(ParseUtils.readResValue(buffer, stringPool));
+
+ if ((resourceTableMap.getNameRef() & 0x02000000) != 0) {
+ //read arrays
+ } else if ((resourceTableMap.getNameRef() & 0x01000000) != 0) {
+ // read attrs
+ } else {
+ }
+
+ return resourceTableMap;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public short getId() {
+ return id;
+ }
+
+ public void setId(short id) {
+ this.id = id;
+ }
+
+ public Locale getLocale() {
+ return locale;
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ public StringPool getKeyStringPool() {
+ return keyStringPool;
+ }
+
+ public void setKeyStringPool(StringPool keyStringPool) {
+ this.keyStringPool = keyStringPool;
+ }
+
+ public ByteBuffer getBuffer() {
+ return buffer;
+ }
+
+ public void setBuffer(ByteBuffer buffer) {
+ this.buffer = buffer;
+ }
+
+ public long[] getOffsets() {
+ return offsets;
+ }
+
+ public void setOffsets(long[] offsets) {
+ this.offsets = offsets;
+ }
+
+ public StringPool getStringPool() {
+ return stringPool;
+ }
+
+ public void setStringPool(StringPool stringPool) {
+ this.stringPool = stringPool;
+ }
+
+ public int getDensity() {
+ return density;
+ }
+
+ @Override
+ public String toString() {
+ return "Type{"
+ + "name='" + name + '\''
+ + ", id=" + id
+ + ", locale=" + locale
+ + '}';
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java b/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java
new file mode 100644
index 0000000..d2d9731
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/TypeHeader.java
@@ -0,0 +1,84 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * @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 byte id;
+
+ // Must be 0. uint8_t
+ private byte res0;
+ // Must be 0. uint16_t
+ private short res1;
+
+ // Number of uint32_t entry indices that follow. uint32
+ private int entryCount;
+
+ // Offset from header where ResTable_entry data starts.uint32_t
+ private int entriesStart;
+
+ // Configuration this collection of entries is designed for.
+ private ResTableConfig config;
+
+ public TypeHeader(int headerSize, long chunkSize) {
+ super(ChunkType.TABLE_TYPE, headerSize, chunkSize);
+ }
+
+ public short getId() {
+ return Unsigned.toShort(id);
+ }
+
+ public void setId(short id) {
+ this.id = Unsigned.toUByte(id);
+ }
+
+ public short getRes0() {
+ return Unsigned.toUShort(res0);
+ }
+
+ public void setRes0(short res0) {
+ this.res0 = Unsigned.toUByte(res0);
+ }
+
+ public int getRes1() {
+ return Unsigned.toInt(res1);
+ }
+
+ public void setRes1(int res1) {
+ this.res1 = Unsigned.toUShort(res1);
+ }
+
+ public int getEntryCount() {
+ return entryCount;
+ }
+
+ public void setEntryCount(long entryCount) {
+ this.entryCount = Unsigned.ensureUInt(entryCount);
+ }
+
+ public int getEntriesStart() {
+ return entriesStart;
+ }
+
+ public void setEntriesStart(long entriesStart) {
+ this.entriesStart = Unsigned.ensureUInt(entriesStart);
+ }
+
+ public ResTableConfig getConfig() {
+ return config;
+ }
+
+ public void setConfig(ResTableConfig config) {
+ this.config = config;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java b/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java
new file mode 100644
index 0000000..fd7b019
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpec.java
@@ -0,0 +1,51 @@
+package net.dongliu.apk.parser.struct.resource;
+
+/**
+ * @author dongliu
+ */
+public class TypeSpec {
+
+ private long[] entryFlags;
+ private String name;
+ private short id;
+
+ public TypeSpec(TypeSpecHeader header) {
+ this.id = header.getId();
+ }
+
+ public boolean exists(int id) {
+ return id < entryFlags.length;
+ }
+
+ public long[] getEntryFlags() {
+ return entryFlags;
+ }
+
+ public void setEntryFlags(long[] entryFlags) {
+ this.entryFlags = entryFlags;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public short getId() {
+ return id;
+ }
+
+ public void setId(short id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return "TypeSpec{"
+ + "name='" + name + '\''
+ + ", id=" + id
+ + '}';
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java b/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java
new file mode 100644
index 0000000..b0c447c
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/resource/TypeSpecHeader.java
@@ -0,0 +1,63 @@
+package net.dongliu.apk.parser.struct.resource;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+import net.dongliu.apk.parser.struct.ChunkType;
+import net.dongliu.apk.parser.utils.Unsigned;
+
+/**
+ * @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 byte id;
+
+ // Must be 0. uint8_t
+ private byte res0;
+
+ // Must be 0.uint16_t
+ private short res1;
+
+ // Number of uint32_t entry configuration masks that follow.
+ private int entryCount;
+
+ public TypeSpecHeader(int headerSize, long chunkSize) {
+ super(ChunkType.TABLE_TYPE_SPEC, headerSize, chunkSize);
+ }
+
+ public short getId() {
+ return Unsigned.toShort(id);
+ }
+
+ public void setId(short id) {
+ this.id = Unsigned.toUByte(id);
+ }
+
+ public short getRes0() {
+ return Unsigned.toShort(res0);
+ }
+
+ public void setRes0(short res0) {
+ this.res0 = Unsigned.toUByte(res0);
+ }
+
+ public int getRes1() {
+ return Unsigned.toInt(res1);
+ }
+
+ public void setRes1(int res1) {
+ this.res1 = Unsigned.toUShort(res1);
+ }
+
+ public int getEntryCount() {
+ return entryCount;
+ }
+
+ public void setEntryCount(long entryCount) {
+ this.entryCount = Unsigned.ensureUInt(entryCount);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java b/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java
new file mode 100644
index 0000000..5a60234
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/signingv2/ApkSigningBlock.java
@@ -0,0 +1,25 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+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";
+
+ private List signerBlocks;
+
+ public ApkSigningBlock(List signerBlocks) {
+ this.signerBlocks = signerBlocks;
+ }
+
+ public List getSignerBlocks() {
+ return signerBlocks;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java b/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java
new file mode 100644
index 0000000..95310fe
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/signingv2/Digest.java
@@ -0,0 +1,20 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+public class Digest {
+
+ private int algorithmID;
+ private byte[] value;
+
+ public Digest(int algorithmID, byte[] value) {
+ this.algorithmID = algorithmID;
+ this.value = value;
+ }
+
+ public int getAlgorithmID() {
+ return algorithmID;
+ }
+
+ public byte[] getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java b/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java
new file mode 100644
index 0000000..5e01048
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/signingv2/Signature.java
@@ -0,0 +1,20 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+public class Signature {
+
+ private int algorithmID;
+ private byte[] data;
+
+ public Signature(int algorithmID, byte[] data) {
+ this.algorithmID = algorithmID;
+ this.data = data;
+ }
+
+ public int getAlgorithmID() {
+ return algorithmID;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java b/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java
new file mode 100644
index 0000000..fcc2919
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/signingv2/SignerBlock.java
@@ -0,0 +1,29 @@
+package net.dongliu.apk.parser.struct.signingv2;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+public class SignerBlock {
+
+ private List digests;
+ private List certificates;
+ private List signatures;
+
+ public SignerBlock(List digests, List certificates, List signatures) {
+ this.digests = digests;
+ this.certificates = certificates;
+ this.signatures = signatures;
+ }
+
+ public List getDigests() {
+ return digests;
+ }
+
+ public List getCertificates() {
+ return certificates;
+ }
+
+ public List getSignatures() {
+ return signatures;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.java b/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.java
new file mode 100644
index 0000000..d72d56e
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/Attribute.java
@@ -0,0 +1,103 @@
+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;
+import java.util.Map;
+
+/**
+ * xml node attribute
+ *
+ * @author dongliu
+ */
+public class Attribute {
+
+ private String namespace;
+ private String name;
+ // The original raw string value of Attribute
+ private String rawValue;
+ // Processed typed value of Attribute
+ private ResourceValue typedValue;
+ // the final value as string
+ private String value;
+
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ if (rawValue != null) {
+ return rawValue;
+ } else if (typedValue != null) {
+ return typedValue.toStringValue(resourceTable, locale);
+ } else {
+ // something happen;
+ return "";
+ }
+ }
+
+ /**
+ * These are attribute resource constants for the platform; as found in android.R.attr
+ *
+ * @author dongliu
+ */
+ public static class AttrIds {
+
+ private static final Map ids = ResourceLoader.loadSystemAttrIds();
+
+ public static String getString(long id) {
+ String value = ids.get((int) id);
+ if (value == null) {
+ value = "AttrId:0x" + Long.toHexString(id);
+ }
+ return value;
+ }
+
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getRawValue() {
+ return rawValue;
+ }
+
+ public void setRawValue(String rawValue) {
+ this.rawValue = rawValue;
+ }
+
+ public ResourceValue getTypedValue() {
+ return typedValue;
+ }
+
+ public void setTypedValue(ResourceValue typedValue) {
+ this.typedValue = typedValue;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "Attribute{"
+ + "name='" + name + '\''
+ + ", namespace='" + namespace + '\''
+ + '}';
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.java b/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.java
new file mode 100644
index 0000000..27246f7
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/Attributes.java
@@ -0,0 +1,83 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import javax.annotation.Nullable;
+
+/**
+ * xml node attributes
+ *
+ * @author dongliu
+ */
+public class Attributes {
+
+ private final Attribute[] attributes;
+
+ public Attributes(int size) {
+ this.attributes = new Attribute[size];
+ }
+
+ public void set(int i, Attribute attribute) {
+ attributes[i] = attribute;
+ }
+
+ @Nullable
+ public Attribute get(String name) {
+ for (Attribute attribute : attributes) {
+ if (attribute.getName().equals(name)) {
+ return attribute;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get attribute with name, return value as string
+ */
+ @Nullable
+ public String getString(String name) {
+ Attribute attribute = get(name);
+ if (attribute == null) {
+ return null;
+ }
+ return attribute.getValue();
+ }
+
+ public int size() {
+ return attributes.length;
+ }
+
+ public boolean getBoolean(String name, boolean b) {
+ String value = getString(name);
+ return value == null ? b : Boolean.parseBoolean(value);
+ }
+
+ @Nullable
+ public Integer getInt(String name) {
+ String value = getString(name);
+ if (value == null) {
+ return null;
+ }
+ if (value.startsWith("0x")) {
+ return Integer.valueOf(value.substring(2), 16);
+ }
+ return Integer.valueOf(value);
+ }
+
+ @Nullable
+ public Long getLong(String name) {
+ String value = getString(name);
+ if (value == null) {
+ return null;
+ }
+ if (value.startsWith("0x")) {
+ return Long.valueOf(value.substring(2), 16);
+ }
+ return Long.valueOf(value);
+ }
+
+ /**
+ * return all attributes
+ */
+ public Attribute[] values() {
+ return this.attributes;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.java b/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.java
new file mode 100644
index 0000000..89a3359
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/NullHeader.java
@@ -0,0 +1,15 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+
+/**
+ * Null header.
+ *
+ * @author dongliu
+ */
+public class NullHeader extends ChunkHeader {
+
+ public NullHeader(int chunkType, int headerSize, long chunkSize) {
+ super(chunkType, headerSize, chunkSize);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java
new file mode 100644
index 0000000..21f7442
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlCData.java
@@ -0,0 +1,69 @@
+package net.dongliu.apk.parser.struct.xml;
+
+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
+ *
+ * @return
+ */
+ public String toStringValue(ResourceTable resourceTable, Locale locale) {
+ if (data != null) {
+ return CDATA_START + data + CDATA_END;
+ } else {
+ return CDATA_START + typedData.toStringValue(resourceTable, locale) + CDATA_END;
+ }
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+ public ResourceValue getTypedData() {
+ return typedData;
+ }
+
+ public void setTypedData(ResourceValue typedData) {
+ this.typedData = typedData;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "XmlCData{"
+ + "data='" + data + '\''
+ + ", typedData=" + typedData
+ + '}';
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlHeader.java
new file mode 100644
index 0000000..106a871
--- /dev/null
+++ b/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(int chunkType, int headerSize, long chunkSize) {
+ super(chunkType, headerSize, chunkSize);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java
new file mode 100644
index 0000000..6e560ac
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceEndTag.java
@@ -0,0 +1,31 @@
+package net.dongliu.apk.parser.struct.xml;
+
+/**
+ * @author dongliu
+ */
+public class XmlNamespaceEndTag {
+
+ private String prefix;
+ private String uri;
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ @Override
+ public String toString() {
+ return prefix + "=" + uri;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java
new file mode 100644
index 0000000..01f2e3d
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNamespaceStartTag.java
@@ -0,0 +1,31 @@
+package net.dongliu.apk.parser.struct.xml;
+
+/**
+ * @author dongliu
+ */
+public class XmlNamespaceStartTag {
+
+ private String prefix;
+ private String uri;
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ @Override
+ public String toString() {
+ return prefix + "=" + uri;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java
new file mode 100644
index 0000000..9893ca6
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeEndTag.java
@@ -0,0 +1,37 @@
+package net.dongliu.apk.parser.struct.xml;
+
+/**
+ * @author dongliu
+ */
+public class XmlNodeEndTag {
+
+ private String namespace;
+ private String name;
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("");
+ if (namespace != null) {
+ sb.append(namespace).append(":");
+ }
+ sb.append(name).append('>');
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java
new file mode 100644
index 0000000..8835b2f
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeHeader.java
@@ -0,0 +1,34 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+
+/**
+ * @author dongliu
+ */
+public class XmlNodeHeader extends ChunkHeader {
+
+ // Line number in original source file at which this element appeared.
+ private int lineNum;
+ // Optional XML comment string pool ref, -1 if none
+ private int commentRef;
+
+ public XmlNodeHeader(int chunkType, int headerSize, long chunkSize) {
+ super(chunkType, headerSize, chunkSize);
+ }
+
+ public int getLineNum() {
+ return lineNum;
+ }
+
+ public void setLineNum(int lineNum) {
+ this.lineNum = lineNum;
+ }
+
+ public int getCommentRef() {
+ return commentRef;
+ }
+
+ public void setCommentRef(int commentRef) {
+ this.commentRef = commentRef;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java
new file mode 100644
index 0000000..8c30b8e
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlNodeStartTag.java
@@ -0,0 +1,59 @@
+package net.dongliu.apk.parser.struct.xml;
+
+/**
+ * @author dongliu
+ */
+public class XmlNodeStartTag {
+
+ private String namespace;
+ private 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;
+ private Attributes attributes;
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Attributes getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Attributes attributes) {
+ this.attributes = attributes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append('<');
+ if (namespace != null) {
+ sb.append(namespace).append(":");
+ }
+ sb.append(name);
+ sb.append('>');
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.java b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.java
new file mode 100644
index 0000000..cbc63b3
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/xml/XmlResourceMapHeader.java
@@ -0,0 +1,13 @@
+package net.dongliu.apk.parser.struct.xml;
+
+import net.dongliu.apk.parser.struct.ChunkHeader;
+
+/**
+ * @author dongliu
+ */
+public class XmlResourceMapHeader extends ChunkHeader {
+
+ public XmlResourceMapHeader(int chunkType, int headerSize, long chunkSize) {
+ super(chunkType, headerSize, chunkSize);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java b/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java
new file mode 100644
index 0000000..4f893e0
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/struct/zip/EOCD.java
@@ -0,0 +1,82 @@
+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;
+// private List commentList;
+
+ public short getDiskNum() {
+ return diskNum;
+ }
+
+ public void setDiskNum(int diskNum) {
+ this.diskNum = (short) diskNum;
+ }
+
+ public int getCdStartDisk() {
+ return cdStartDisk & 0xffff;
+ }
+
+ public void setCdStartDisk(int cdStartDisk) {
+ this.cdStartDisk = (short) cdStartDisk;
+ }
+
+ public int getCdRecordNum() {
+ return cdRecordNum & 0xffff;
+ }
+
+ public void setCdRecordNum(int cdRecordNum) {
+ this.cdRecordNum = (short) cdRecordNum;
+ }
+
+ public int getTotalCDRecordNum() {
+ return totalCDRecordNum & 0xffff;
+ }
+
+ public void setTotalCDRecordNum(int totalCDRecordNum) {
+ this.totalCDRecordNum = (short) totalCDRecordNum;
+ }
+
+ public long getCdSize() {
+ return cdSize & 0xffffffffL;
+ }
+
+ public void setCdSize(long cdSize) {
+ this.cdSize = (int) cdSize;
+ }
+
+ public long getCdStart() {
+ return cdStart & 0xffffffffL;
+ }
+
+ public void setCdStart(long cdStart) {
+ this.cdStart = (int) cdStart;
+ }
+
+ public int getCommentLen() {
+ return commentLen & 0xffff;
+ }
+
+ public void setCommentLen(int commentLen) {
+ this.commentLen = (short) commentLen;
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/Buffers.java b/src/main/java/net/dongliu/apk/parser/utils/Buffers.java
new file mode 100644
index 0000000..cb352dc
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/Buffers.java
@@ -0,0 +1,124 @@
+package net.dongliu.apk.parser.utils;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * utils method for byte buffer
+ *
+ * @author Liu Dong dongliu@live.cn
+ */
+public class Buffers {
+
+ /**
+ * get one unsigned byte as short type
+ */
+ public static short readUByte(ByteBuffer buffer) {
+ byte b = buffer.get();
+ return (short) (b & 0xff);
+ }
+
+ /**
+ * get one unsigned short as int type
+ */
+ public static int readUShort(ByteBuffer buffer) {
+ short s = buffer.getShort();
+ return s & 0xffff;
+ }
+
+ /**
+ * get one unsigned int as long type
+ */
+ public static long readUInt(ByteBuffer buffer) {
+ int i = buffer.getInt();
+ return i & 0xffffffffL;
+ }
+
+ /**
+ * get bytes
+ */
+ public static byte[] readBytes(ByteBuffer buffer, int size) {
+ byte[] bytes = new byte[size];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ /**
+ * get all bytes remains
+ */
+ public static byte[] readBytes(ByteBuffer buffer) {
+ return readBytes(buffer, buffer.remaining());
+ }
+
+ /**
+ * Read ascii string ,by len
+ */
+ public static String readAsciiString(ByteBuffer buffer, int strLen) {
+ byte[] bytes = new byte[strLen];
+ buffer.get(bytes);
+ return new String(bytes);
+ }
+
+ /**
+ * read utf16 strings, use strLen, not ending 0 char.
+ */
+ public static String readString(ByteBuffer buffer, int strLen) {
+ 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.
+ */
+ public static String readZeroTerminatedString(ByteBuffer buffer, int strLen) {
+ StringBuilder sb = new StringBuilder(strLen);
+ for (int i = 0; i < strLen; i++) {
+ char c = buffer.getChar();
+ if (c == '\0') {
+ skip(buffer, (strLen - i - 1) * 2);
+ break;
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * skip count bytes
+ */
+ public static void skip(ByteBuffer buffer, int count) {
+ position(buffer, buffer.position() + count);
+ }
+
+ // 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.
+ /**
+ * set position
+ */
+ public static void position(ByteBuffer buffer, int position) {
+ ((Buffer) buffer).position(position);
+ }
+
+ /**
+ * set position
+ */
+ public static void position(ByteBuffer buffer, long position) {
+ 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.
+ */
+ public static ByteBuffer sliceAndSkip(ByteBuffer buffer, int size) {
+ ByteBuffer buf = buffer.slice().order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer slice = (ByteBuffer) ((Buffer) buf).limit(buf.position() + size);
+ skip(buffer, size);
+ return slice;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/Inputs.java b/src/main/java/net/dongliu/apk/parser/utils/Inputs.java
new file mode 100644
index 0000000..e997c03
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/Inputs.java
@@ -0,0 +1,27 @@
+package net.dongliu.apk.parser.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Inputs {
+
+ public static byte[] readAll(InputStream in) throws IOException {
+ byte[] buf = new byte[1024 * 8];
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ int len;
+ while ((len = in.read(buf)) != -1) {
+ bos.write(buf, 0, len);
+ }
+ return bos.toByteArray();
+ }
+ }
+
+ public static byte[] readAllAndClose(InputStream in) throws IOException {
+ try {
+ return readAll(in);
+ } finally {
+ in.close();
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/Locales.java b/src/main/java/net/dongliu/apk/parser/utils/Locales.java
new file mode 100644
index 0000000..fac7e18
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/Locales.java
@@ -0,0 +1,36 @@
+package net.dongliu.apk.parser.utils;
+
+import java.util.Locale;
+
+/**
+ * @author dongliu
+ */
+public class Locales {
+
+ /**
+ * when do localize, any locale will match this
+ */
+ public static final Locale any = new Locale("", "");
+
+ /**
+ * How much the given locale match the expected locale.
+ */
+ public static int match(Locale locale, Locale targetLocale) {
+ if (locale == null) {
+ return -1;
+ }
+ if (locale.getLanguage().equals(targetLocale.getLanguage())) {
+ if (locale.getCountry().equals(targetLocale.getCountry())) {
+ return 3;
+ } else if (targetLocale.getCountry().isEmpty()) {
+ return 2;
+ } else {
+ return 0;
+ }
+ } else if (targetLocale.getCountry().isEmpty() || targetLocale.getLanguage().isEmpty()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/Pair.java b/src/main/java/net/dongliu/apk/parser/utils/Pair.java
new file mode 100644
index 0000000..ace7f9f
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/Pair.java
@@ -0,0 +1,34 @@
+package net.dongliu.apk.parser.utils;
+
+/**
+ * @author Liu Dong {@literal }
+ */
+public class Pair {
+
+ private K left;
+ private V right;
+
+ public Pair() {
+ }
+
+ public Pair(K left, V right) {
+ this.left = left;
+ this.right = right;
+ }
+
+ public K getLeft() {
+ return left;
+ }
+
+ public void setLeft(K left) {
+ this.left = left;
+ }
+
+ public V getRight() {
+ return right;
+ }
+
+ public void setRight(V right) {
+ this.right = right;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java b/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java
new file mode 100644
index 0000000..d2fe8e3
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/ParseUtils.java
@@ -0,0 +1,194 @@
+package net.dongliu.apk.parser.utils;
+
+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 javax.annotation.Nullable;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * @author dongliu
+ */
+public class ParseUtils {
+
+ public static Charset charsetUTF8 = Charset.forName("UTF-8");
+
+ /**
+ * read string from input buffer. if get EOF before read enough data, throw IOException.
+ */
+ public static String readString(ByteBuffer buffer, 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.
+ int strLen = readLen(buffer);
+ int bytesLen = readLen(buffer);
+ byte[] bytes = Buffers.readBytes(buffer, bytesLen);
+ String str = new String(bytes, charsetUTF8);
+ // zero
+ int trailling = Buffers.readUByte(buffer);
+ return str;
+ } else {
+ // The length is encoded as either one or two 16-bit integers as per the commentRef...
+ int strLen = readLen16(buffer);
+ String str = Buffers.readString(buffer, strLen);
+ // zero
+ int trailling = Buffers.readUShort(buffer);
+ return str;
+ }
+ }
+
+ /**
+ * read utf-16 encoding str, use zero char to end str.
+ */
+ public static String readStringUTF16(ByteBuffer buffer, int strLen) {
+ String str = Buffers.readString(buffer, strLen);
+ for (int i = 0; i < str.length(); i++) {
+ 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(ByteBuffer buffer) {
+ int len = 0;
+ 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(ByteBuffer buffer) {
+ int len = 0;
+ 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.
+ */
+ public static StringPool readStringPool(ByteBuffer buffer, StringPoolHeader stringPoolHeader) {
+
+ long beginPos = buffer.position();
+ 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
+ boolean sorted = (stringPoolHeader.getFlags() & StringPoolHeader.SORTED_FLAG) != 0;
+ // string use utf-8 format if true, otherwise utf-16
+ boolean utf8 = (stringPoolHeader.getFlags() & StringPoolHeader.UTF8_FLAG) != 0;
+
+ // read strings. the head and metas have 28 bytes
+ long stringPos = beginPos + stringPoolHeader.getStringsStart() - stringPoolHeader.getHeaderSize();
+ Buffers.position(buffer, stringPos);
+
+ 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;
+ StringPool stringPool = new StringPool(stringPoolHeader.getStringCount());
+ for (StringPoolEntry entry : entries) {
+ if (entry.getOffset() == lastOffset) {
+ stringPool.set(entry.getIdx(), lastStr);
+ continue;
+ }
+
+ Buffers.position(buffer, entry.getOffset());
+ lastOffset = entry.getOffset();
+ String str = ParseUtils.readString(buffer, utf8);
+ lastStr = str;
+ stringPool.set(entry.getIdx(), str);
+ }
+
+ // read styles
+ 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(ByteBuffer buffer, StringPool stringPool) {
+// ResValue resValue = new ResValue();
+ int size = Buffers.readUShort(buffer);
+ short res0 = Buffers.readUByte(buffer);
+ 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:
+ int strRef = buffer.getInt();
+ if (strRef >= 0) {
+ return ResourceValue.string(strRef, stringPool);
+ } else {
+ return null;
+ }
+ case ResValue.ResType.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(int expected, int real) {
+ if (expected != real) {
+ throw new ParserException("Expect chunk type:" + Integer.toHexString(expected)
+ + ", but got:" + Integer.toHexString(real));
+ }
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java b/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java
new file mode 100644
index 0000000..b105eb7
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/ResourceFetcher.java
@@ -0,0 +1,131 @@
+package net.dongliu.apk.parser.utils;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+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;
+
+/**
+ * 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 {
+ String url = "https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml";
+ String html = getUrl(url);
+ String xml = retrieveCode(html);
+
+ if (xml != null) {
+ parseAttributeXml(xml);
+ }
+ }
+
+ private void parseAttributeXml(String xml)
+ throws IOException, ParserConfigurationException, SAXException {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ SAXParser parser = factory.newSAXParser();
+ final List> attrIds = new ArrayList<>();
+ DefaultHandler dh = new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ if (!qName.equals("public")) {
+ return;
+ }
+ String type = attributes.getValue("type");
+ if (type == null) {
+ return;
+ }
+ if (type.equals("attr")) {
+ //attr ids.
+ String idStr = attributes.getValue("id");
+ if (idStr == null) {
+ return;
+ }
+ String name = attributes.getValue("name");
+ if (idStr.startsWith("0x")) {
+ idStr = idStr.substring(2);
+ }
+ int id = Integer.parseInt(idStr, 16);
+ attrIds.add(new Pair<>(id, name));
+ }
+ }
+ };
+ parser.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), dh);
+ for (Pair pair : attrIds) {
+ System.out.println(String.format("%s=%d", 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 {
+ String url = "https://android.googlesource.com/platform/frameworks/base/+/master/api/current.txt";
+ String html = getUrl(url);
+ String code = retrieveCode(html);
+ if (code == null) {
+ System.err.println("code area not found");
+ return;
+ }
+ int begin = code.indexOf("R.style");
+ int end = code.indexOf("}", begin);
+ String styleCode = code.substring(begin, end);
+ 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);
+ }
+ }
+ }
+
+ private String getUrl(String url) throws IOException {
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ try {
+ conn.setRequestMethod("GET");
+ conn.setReadTimeout(10000);
+ conn.setConnectTimeout(10000);
+ byte[] bytes = Inputs.readAllAndClose(conn.getInputStream());
+ return new String(bytes, StandardCharsets.UTF_8);
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ private String retrieveCode(String html) {
+ Matcher matcher = Pattern.compile("(.*?)
").matcher(html);
+ if (matcher.find()) {
+ String codeHtml = matcher.group(1);
+ return codeHtml.replace("", "\n").replaceAll("<[^>]+>", "").replace("<", "<")
+ .replace(""", "\"").replace(">", ">");
+ } else {
+ return null;
+ }
+ }
+
+ public static void main(String[] args)
+ throws ParserConfigurationException, SAXException, IOException {
+ ResourceFetcher fetcher = new ResourceFetcher();
+ fetcher.fetchSystemAttrIds();
+ //fetcher.fetchSystemStyle();
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.java b/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.java
new file mode 100644
index 0000000..3d41d47
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/ResourceLoader.java
@@ -0,0 +1,62 @@
+package net.dongliu.apk.parser.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * methods for load resources.
+ *
+ * @author dongliu
+ */
+public class ResourceLoader {
+
+ /**
+ * load system attr ids for parse binary xml.
+ */
+ public static Map loadSystemAttrIds() {
+ try (BufferedReader reader = toReader("/r_values.ini")) {
+ Map map = new HashMap<>();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] items = line.trim().split("=");
+ if (items.length != 2) {
+ continue;
+ }
+ String name = items[0].trim();
+ Integer id = Integer.valueOf(items[1].trim());
+ map.put(id, name);
+ }
+ return map;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Map loadSystemStyles() {
+ Map map = new HashMap<>();
+ try (BufferedReader reader = toReader("/r_styles.ini")) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ String[] items = line.split("=");
+ if (items.length != 2) {
+ continue;
+ }
+ Integer id = Integer.valueOf(items[1].trim());
+ String name = items[0].trim();
+ map.put(id, name);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return map;
+ }
+
+ private static BufferedReader toReader(String path) {
+ return new BufferedReader(new InputStreamReader(
+ ResourceLoader.class.getResourceAsStream(path)));
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/Strings.java b/src/main/java/net/dongliu/apk/parser/utils/Strings.java
new file mode 100644
index 0000000..0cb907c
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/Strings.java
@@ -0,0 +1,84 @@
+package net.dongliu.apk.parser.utils;
+
+import java.util.Iterator;
+
+public class Strings {
+
+ /**
+ * Copied fom commons StringUtils
+ *
+ * Joins the elements of the provided {@code Iterable} into a single String containing the provided elements.
+ */
+ public static String join(final Iterable> iterable, final String separator) {
+ if (iterable == null) {
+ return null;
+ }
+ return join(iterable.iterator(), separator);
+ }
+
+ /**
+ * Copied fom commons StringUtils
+ */
+ public static String join(final Iterator> iterator, final String separator) {
+
+ // handle null, zero and one elements before building a buffer
+ if (iterator == null) {
+ return null;
+ }
+ if (!iterator.hasNext()) {
+ return "";
+ }
+ final Object first = iterator.next();
+ if (!iterator.hasNext()) {
+ return first == null ? null : first.toString();
+ }
+
+ // two or more elements
+ final StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small
+ if (first != null) {
+ buf.append(first);
+ }
+
+ while (iterator.hasNext()) {
+ if (separator != null) {
+ buf.append(separator);
+ }
+ final Object obj = iterator.next();
+ if (obj != null) {
+ buf.append(obj);
+ }
+ }
+ return buf.toString();
+ }
+
+ public static boolean isNumeric(final CharSequence cs) {
+ if (isEmpty(cs)) {
+ return false;
+ }
+ final int sz = cs.length();
+ for (int i = 0; i < sz; i++) {
+ if (!Character.isDigit(cs.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean isEmpty(final CharSequence cs) {
+ return cs == null || cs.length() == 0;
+ }
+
+ public static String substringBefore(final String str, final String separator) {
+ if (Strings.isEmpty(str) || separator == null) {
+ return str;
+ }
+ if (separator.isEmpty()) {
+ return "";
+ }
+ final int pos = str.indexOf(separator);
+ if (pos == -1) {
+ return str;
+ }
+ return str.substring(0, pos);
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/Unsigned.java b/src/main/java/net/dongliu/apk/parser/utils/Unsigned.java
new file mode 100644
index 0000000..5cd756b
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/Unsigned.java
@@ -0,0 +1,45 @@
+package net.dongliu.apk.parser.utils;
+
+/**
+ * Unsigned utils, for compatible with java6/java7.
+ */
+public class Unsigned {
+
+ public static long toLong(int value) {
+ return value & 0xffffffffL;
+ }
+
+ public static int toUInt(long value) {
+ return (int) value;
+ }
+
+ public static int toInt(short value) {
+ return value & 0xffff;
+ }
+
+ public static short toUShort(int value) {
+ return (short) value;
+ }
+
+ public static int ensureUInt(long value) {
+ if (value < 0 || value > Integer.MAX_VALUE) {
+ throw new ArithmeticException("unsigned integer overflow");
+ }
+ return (int) value;
+ }
+
+ public static long ensureULong(long value) {
+ if (value < 0) {
+ throw new ArithmeticException("unsigned long overflow");
+ }
+ return value;
+ }
+
+ public static short toShort(byte value) {
+ return (short) (value & 0xff);
+ }
+
+ public static byte toUByte(short value) {
+ return (byte) value;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/package-info.java b/src/main/java/net/dongliu/apk/parser/utils/package-info.java
new file mode 100644
index 0000000..3ea1fd7
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Only for internal use!
+ */
+package net.dongliu.apk.parser.utils;
diff --git a/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java b/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java
new file mode 100644
index 0000000..190620a
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/AggregateTranslator.java
@@ -0,0 +1,53 @@
+/*
+ * 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 : translators) {
+ final int consumed = translator.translate(input, index, out);
+ if (consumed != 0) {
+ return consumed;
+ }
+ }
+ return 0;
+ }
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java b/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java
new file mode 100644
index 0000000..afd00e7
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/CharSequenceTranslator.java
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+/**
+ * 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
+ */
+ public final String translate(final CharSequence input) {
+ if (input == null) {
+ return null;
+ }
+ try {
+ final StringWriter writer = new StringWriter(input.length() * 2);
+ 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 = 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
+ */
+ 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/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java b/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java
new file mode 100644
index 0000000..316ba3e
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/CodePointTranslator.java
@@ -0,0 +1,47 @@
+/*
+ * 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 = 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/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java b/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java
new file mode 100644
index 0000000..0e6049a
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/EntityArrays.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+/**
+ * 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
+ */
+ public static String[][] BASIC_ESCAPE() {
+ return 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
+ */
+ public static String[][] APOS_ESCAPE() {
+ return APOS_ESCAPE.clone();
+ }
+ private static final String[][] APOS_ESCAPE = {
+ {"'", "'"}, // XML apostrophe
+ };
+
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java b/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java
new file mode 100644
index 0000000..aed7b12
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/LookupTranslator.java
@@ -0,0 +1,79 @@
+/*
+ * 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) {
+ 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;
+ }
+ }
+ }
+ shortest = _shortest;
+ longest = _longest;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int translate(final CharSequence input, final int index, final Writer out) throws IOException {
+ int max = longest;
+ if (index + longest > input.length()) {
+ max = input.length() - index;
+ }
+ // descend so as to get a greedy algorithm
+ for (int i = max; i >= shortest; i--) {
+ final CharSequence subSeq = input.subSequence(index, index + i);
+ final CharSequence result = lookupMap.get(subSeq.toString());
+ if (result != null) {
+ out.write(result.toString());
+ return i;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java b/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java
new file mode 100644
index 0000000..57aba64
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/NumericEntityEscaper.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+/**
+ * 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
+ */
+ public static NumericEntityEscaper below(final int codepoint) {
+ return 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
+ */
+ public static NumericEntityEscaper above(final int codepoint) {
+ return 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
+ */
+ 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
+ */
+ 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 (between) {
+ if (codepoint < below || codepoint > above) {
+ return false;
+ }
+ } else {
+ if (codepoint >= below && codepoint <= above) {
+ return false;
+ }
+ }
+
+ out.write("");
+ out.write(Integer.toString(codepoint, 10));
+ out.write(';');
+ return true;
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java b/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java
new file mode 100644
index 0000000..8c49cef
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/UnicodeUnpairedSurrogateRemover.java
@@ -0,0 +1,40 @@
+/*
+ * 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 remove unpaired surrogates.
+ */
+class UnicodeUnpairedSurrogateRemover extends CodePointTranslator {
+
+ /**
+ * Implementation of translate that throws out unpaired surrogates. {@inheritDoc}
+ */
+ @Override
+ public boolean translate(int codepoint, Writer out) throws IOException {
+ if (codepoint >= Character.MIN_SURROGATE && codepoint <= Character.MAX_SURROGATE) {
+ // It's a surrogate. Write nothing and say we've translated.
+ return true;
+ } else {
+ // It's not a surrogate. Don't translate it.
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java b/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java
new file mode 100644
index 0000000..8cf9860
--- /dev/null
+++ b/src/main/java/net/dongliu/apk/parser/utils/xml/XmlEscaper.java
@@ -0,0 +1,62 @@
+package net.dongliu.apk.parser.utils.xml;
+
+import net.dongliu.apk.parser.utils.*;
+
+/**
+ * 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.
+ */
+ public static String escapeXml10(final String input) {
+ return 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/src/main/java/optic_fusion1/kitsune/parser/vbs/StatementFactory.java b/src/main/java/optic_fusion1/kitsune/parser/vbs/StatementFactory.java
index 3179fce..86dada5 100644
--- a/src/main/java/optic_fusion1/kitsune/parser/vbs/StatementFactory.java
+++ b/src/main/java/optic_fusion1/kitsune/parser/vbs/StatementFactory.java
@@ -22,7 +22,7 @@
Sub SubName( param1, param2 ) || Sub SubName(param1,param2) // Or other variants
// Statements
End Sub
-*/
+ */
// TODO: Implement support for storing the values that variables get set to
// TODO: Implement support for parsing operators (=, +, -, /, *, %)
public class StatementFactory {
@@ -33,6 +33,7 @@ public class StatementFactory {
// Statements
End Function
*/
+ // TODO: Find a way to make a private static final Pattern variable for this classes's regex
public static Function buildFunctionStatements(int index, String line) {
Pattern pattern = Pattern.compile("( +.*?)\\(?", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(line);
@@ -71,6 +72,7 @@ public static VariableInit buildVariableInitStatements(int index, String lineTri
VariableInit vinit = new VariableInit();
vinit.setText(lineTrimmed);
vinit.setLineNumber(index);
+ // TODO: Make private static final Pattern for this
Pattern pattern;
Matcher matcher = null;
if (lineTrimmed.contains("As")) {
@@ -114,9 +116,10 @@ public static LoopStatement buildLoopStatements(int index, String lineTrimmed) {
return loop;
}
+ private static final Pattern IF_STMNT_PATTERN = Pattern.compile("if(.+)then", Pattern.CASE_INSENSITIVE);
+
public static IfStatement buildIFStatements(int index, String lineTrimmed) {
- Pattern pattern = Pattern.compile("if(.+)then", Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(lineTrimmed);
+ Matcher matcher = IF_STMNT_PATTERN.matcher(lineTrimmed);
matcher.find();
IfStatement ifstmt = new IfStatement();
ifstmt.setText(lineTrimmed);
@@ -126,9 +129,10 @@ public static IfStatement buildIFStatements(int index, String lineTrimmed) {
return ifstmt;
}
+ private static final Pattern SET_STMNT_PATTERN = Pattern.compile("Set (.+) ?= ?(.+)", Pattern.CASE_INSENSITIVE);
+
public static SetStatement buildSetStatmenets(int index, String lineTrimmed) {
- Pattern pattern = Pattern.compile("set (.+) ?= ?(.+)", Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(lineTrimmed);
+ Matcher matcher = SET_STMNT_PATTERN.matcher(lineTrimmed);
matcher.find();
VBStatement stmt = new VBStatement();
stmt.setText(matcher.group(2));
@@ -136,9 +140,10 @@ public static SetStatement buildSetStatmenets(int index, String lineTrimmed) {
return setStmt;
}
+ private static final Pattern ELSE_IF_STMNT_PATTERN = Pattern.compile("elseif(.+)then", Pattern.CASE_INSENSITIVE);
+
public static ElseIfStatement buildElseIFStatements(int index, String lineTrimmed) {
- Pattern pattern = Pattern.compile("elseif(.+)then", Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(lineTrimmed);
+ Matcher matcher = ELSE_IF_STMNT_PATTERN.matcher(lineTrimmed);
matcher.find();
ElseIfStatement ifstmt = new ElseIfStatement();
ifstmt.setText(lineTrimmed);
@@ -155,17 +160,19 @@ public static ElseStatement buildElseStatements(int index, String lineTrimmed) {
return elsestmt;
}
+ private static final Pattern MSG_BOX_STMNT_PATTERN = Pattern.compile("(msg|MsgBox) (.*)", Pattern.CASE_INSENSITIVE);
+
public static MsgBoxStatement buildMsgBoxStatement(int index, String lineTrimmed) {
- Pattern pattern = Pattern.compile("(msg|MsgBox) (.*)", Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(lineTrimmed);
+ Matcher matcher = MSG_BOX_STMNT_PATTERN.matcher(lineTrimmed);
matcher.find();
MsgBoxStatement statement = new MsgBoxStatement(matcher.group(2));
return statement;
}
+ private static final Pattern CONST_STMNT_PATTERN = Pattern.compile("Const (.*) = (.*)", Pattern.CASE_INSENSITIVE);
+
public static ConstStatement buildConstStatements(int index, String lineTrimmed) {
- Pattern pattern = Pattern.compile("Const (.*) = (.*)", Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(lineTrimmed);
+ Matcher matcher = CONST_STMNT_PATTERN.matcher(lineTrimmed);
matcher.find();
String name = matcher.group(1);
String value = matcher.group(2);
@@ -173,9 +180,10 @@ public static ConstStatement buildConstStatements(int index, String lineTrimmed)
return statement;
}
+ private static final Pattern COMMENT_PATTERN = Pattern.compile("' ? (.*)");
+
public static Comment buildComments(int index, String lineTrimmed) {
- Pattern pattern = Pattern.compile("' ?(.*)");
- Matcher matcher = pattern.matcher(lineTrimmed);
+ Matcher matcher = COMMENT_PATTERN.matcher(lineTrimmed);
matcher.find();
String value = matcher.group(1);
Comment comment = new Comment(value);
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/AnalyzeTool.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/AnalyzeTool.java
index 577caa8..2860fe4 100644
--- a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/AnalyzeTool.java
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/AnalyzeTool.java
@@ -22,6 +22,7 @@
import java.util.List;
import static optic_fusion1.kitsune.Kitsune.LOGGER;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.Analyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.ApkAnalyzerTool;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.html.HTMLAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.JavaAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.vbs.VBSAnalyzer;
@@ -40,6 +41,7 @@ public AnalyzeTool() {
ANALYZERS.put("jar", javaAnalyzer);
ANALYZERS.put("html", new HTMLAnalyzer());
ANALYZERS.put("vbs", new VBSAnalyzer());
+ ANALYZERS.put("apk", new ApkAnalyzerTool());
}
@Override
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/ApkAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/ApkAnalyzer.java
new file mode 100644
index 0000000..9e26323
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/ApkAnalyzer.java
@@ -0,0 +1,9 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk;
+
+import net.dongliu.apk.parser.ApkFile;
+
+public abstract class ApkAnalyzer {
+
+ public abstract void analyze(ApkFile apkFile);
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/ApkAnalyzerTool.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/ApkAnalyzerTool.java
new file mode 100644
index 0000000..1b4e581
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/ApkAnalyzerTool.java
@@ -0,0 +1,36 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import net.dongliu.apk.parser.ApkFile;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.Analyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.file.ApkDexAnalyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.file.ApkMetaAnalyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.file.ApkSignersAnalyzer;
+
+public class ApkAnalyzerTool extends Analyzer {
+
+ private static final List ANALYZERS = new ArrayList<>();
+
+ public ApkAnalyzerTool() {
+ ANALYZERS.add(new ApkMetaAnalyzer());
+ ANALYZERS.add(new ApkSignersAnalyzer());
+ ANALYZERS.add(new ApkDexAnalyzer());
+ }
+
+ @Override
+ public void analyze(File input) {
+ try (ApkFile apkFile = new ApkFile(input)) {
+ for (ApkAnalyzer analyzer : ANALYZERS) {
+ analyzer.analyze(apkFile);
+ }
+ } catch (IOException ex) {
+ Logger.getLogger(ApkAnalyzerTool.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkDexAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkDexAnalyzer.java
new file mode 100644
index 0000000..2e9fdca
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkDexAnalyzer.java
@@ -0,0 +1,28 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.file;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import net.dongliu.apk.parser.ApkFile;
+import net.dongliu.apk.parser.bean.DexClass;
+import static optic_fusion1.kitsune.Kitsune.LOGGER;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.ApkAnalyzer;
+
+public class ApkDexAnalyzer extends ApkAnalyzer {
+
+ @Override
+ public void analyze(ApkFile apkFile) {
+ LOGGER.info("Analyzing dex classes");
+ try {
+ // TODO: Implement support for getting the code itself
+ for (DexClass dexClass : apkFile.getDexClasses()) {
+ LOGGER.info("Package Name: " + dexClass.getPackageName()
+ + " Super Class: " + dexClass.getSuperClass()
+ + " Class Type:" + dexClass.getClassType());
+ }
+ } catch (IOException ex) {
+ Logger.getLogger(ApkDexAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkMetaAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkMetaAnalyzer.java
new file mode 100644
index 0000000..74ba8ef
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkMetaAnalyzer.java
@@ -0,0 +1,41 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.file;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import net.dongliu.apk.parser.ApkFile;
+import net.dongliu.apk.parser.bean.ApkMeta;
+import net.dongliu.apk.parser.bean.Permission;
+import net.dongliu.apk.parser.bean.UseFeature;
+import static optic_fusion1.kitsune.Kitsune.LOGGER;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.ApkAnalyzer;
+
+public class ApkMetaAnalyzer extends ApkAnalyzer {
+
+ @Override
+ public void analyze(ApkFile apkFile) {
+ LOGGER.info("Analyzing ApkMeta");
+ try {
+ ApkMeta apkMeta = apkFile.getApkMeta();
+ LOGGER.info("\n" + apkMeta.toString());
+
+ LOGGER.info("Permissions:");
+ for (Permission perm : apkMeta.getPermissions()) {
+ LOGGER.info(perm.toString());
+ }
+
+ LOGGER.info("Uses Features:");
+ for (String feature : apkMeta.getUsesPermissions()) {
+ LOGGER.info(feature);
+ }
+
+ LOGGER.info("Uses Permissions:");
+ for (UseFeature useFeature : apkMeta.getUsesFeatures()) {
+ LOGGER.info(useFeature.toString());
+ }
+ } catch (IOException ex) {
+ Logger.getLogger(ApkMetaAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkSignersAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkSignersAnalyzer.java
new file mode 100644
index 0000000..9ac4473
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/apk/file/ApkSignersAnalyzer.java
@@ -0,0 +1,31 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.file;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import net.dongliu.apk.parser.ApkFile;
+import net.dongliu.apk.parser.bean.ApkSigner;
+import static optic_fusion1.kitsune.Kitsune.LOGGER;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.apk.ApkAnalyzer;
+
+public class ApkSignersAnalyzer extends ApkAnalyzer {
+
+ @Override
+ public void analyze(ApkFile apkFile) {
+ LOGGER.info("Analyzing ApkSigners");
+ try {
+ for (ApkSigner signer : apkFile.getApkSingers()) {
+ LOGGER.info(signer.toString());
+ }
+ // Currently not working
+// for (ApkV2Signer signer : apkFile.getApkV2Singers()) {
+// LOGGER.info(signer.toString());
+// }
+ } catch (IOException | CertificateException ex) {
+ Logger.getLogger(ApkSignersAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
+ }
+
+ }
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/html/HTMLAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/html/HTMLAnalyzer.java
index f8bb158..e2b2513 100644
--- a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/html/HTMLAnalyzer.java
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/html/HTMLAnalyzer.java
@@ -2,11 +2,12 @@
import java.io.File;
import java.io.IOException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.util.HashMap;
+import java.util.Map;
import static optic_fusion1.kitsune.Kitsune.LOGGER;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.Analyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.html.tags.AElementAnalyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.html.tags.HTMLElementAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.html.tags.IFrameElementAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.html.tags.LinkElementAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.html.tags.ScriptElementAnalyzer;
@@ -19,39 +20,36 @@
public class HTMLAnalyzer extends Analyzer {
+ private static final HashMap ANALYZERS = new HashMap<>();
+
+ public HTMLAnalyzer() {
+ ANALYZERS.put("script", new ScriptElementAnalyzer());
+ ANALYZERS.put("a", new AElementAnalyzer());
+ ANALYZERS.put("link", new LinkElementAnalyzer());
+ ANALYZERS.put("iframe", new IFrameElementAnalyzer());
+ }
+
@Override
public void analyze(File input) {
try {
Document document = Jsoup.parse(input, "utf-8");
- Elements scriptElements = document.select("script");
- if (scriptElements != null) {
- new ScriptElementAnalyzer().analyze(scriptElements);
- }
- Elements aElements = document.select("a");
- if (aElements != null) {
- new AElementAnalyzer().analyze(aElements);
- }
- Elements linkElements = document.select("link");
- if (linkElements != null) {
- new LinkElementAnalyzer().analyze(linkElements);
- }
- Elements iframeElements = document.select("iframe");
- if (iframeElements != null) {
- new IFrameElementAnalyzer().analyze(iframeElements);
+ for (Map.Entry entry : ANALYZERS.entrySet()) {
+ Elements elements = document.select(entry.getKey());
+ if (elements != null) {
+ entry.getValue().analyze(elements);
+ }
}
-
LOGGER.info("Logging comments");
for (Element element : document.getAllElements()) {
for (Node node : element.childNodes()) {
if (node instanceof Comment) {
- System.out.println(node);
+ System.out.println(node); // TODO: Convert this to use LOGGER
}
}
}
LOGGER.info("\t");
-
} catch (IOException ex) {
- Logger.getLogger(HTMLAnalyzer.class.getName()).log(Level.SEVERE, null, ex);
+ LOGGER.exception(ex);
}
}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/JavaAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/JavaAnalyzer.java
index 311971f..7022a4f 100644
--- a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/JavaAnalyzer.java
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/JavaAnalyzer.java
@@ -30,6 +30,7 @@
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.AWTCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.ClassCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.CodeAnalyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.CryptoAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.FileCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.FilesCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.JNativeHookAnalyzer;
@@ -37,6 +38,7 @@
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.MethodCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.RuntimeCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.SQLCodeAnalyzer;
+import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.ScriptCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.StreamCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.SystemCodeAnalyzer;
import optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code.ThreadCodeAnalyzer;
@@ -81,11 +83,14 @@ private void registerCodeAnalyzers() {
registerCodeAnalyzer("java/lang/Class", new ClassCodeAnalyzer());
registerCodeAnalyzer("java/awt/Robot", new AWTCodeAnalyzer());
registerCodeAnalyzer("java/awt/Toolkit", new AWTCodeAnalyzer());
+ registerCodeAnalyzer("java/awt/Desktop", new AWTCodeAnalyzer());
registerCodeAnalyzer("java/lang/System", new SystemCodeAnalyzer());
registerCodeAnalyzer("com/github/kwhat/jnativehook/keyboard/NativeKeyEvent", new JNativeHookAnalyzer());
registerCodeAnalyzer("com/github/kwhat/jnativehook/GlobalScreen", new JNativeHookAnalyzer());
registerCodeAnalyzer("java/nio/file/Files", new FilesCodeAnalyzer());
registerCodeAnalyzer("javassist/CtMethod", new JavassistAnalyzer());
+ registerCodeAnalyzer("javax/crypto/", new CryptoAnalyzer());
+ registerCodeAnalyzer("javax/script/ScriptEngineManager", new ScriptCodeAnalyzer());
}
private void registerCodeAnalyzer(String methodInsnNodeOwner, CodeAnalyzer analyzer) {
@@ -106,7 +111,7 @@ public void analyze(File file) {
for (FileAnalyzer analyzer : FILE_ANALYZERS) {
analyzer.analyze(file);
}
- LOGGER.info(tl("ja_gathering_class_nodes"));
+ LOGGER.info(tl("ja_gathering_class_nodes", file.toPath()));
List classNodes = getClassNodesFromFile(file);
if (classNodes.isEmpty()) {
LOGGER.warn(tl("ja_class_nodes_not_found", file.toPath()));
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/AWTCodeAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/AWTCodeAnalyzer.java
index 0270cff..bedd8d8 100644
--- a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/AWTCodeAnalyzer.java
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/AWTCodeAnalyzer.java
@@ -38,11 +38,29 @@ public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode m
log(classNode, methodNode, methodInsnNode, tl("awtca_get_default_toolkit"));
return;
}
+ if (isMethodInsnNodeCorrect(methodInsnNode, "getSystemClipboard", "()Ljava/awt/datatransfer/Clipboard")) {
+ return;
+ }
if (isMethodInsnNodeCorrect(methodInsnNode, "getScreenSize", "()Ljava/awt/Dimension;")) {
log(classNode, methodNode, methodInsnNode, tl("awtca_get_screen_size"));
return;
}
-
+ if (isMethodInsnNodeCorrect(methodInsnNode, "browse", "(Ljava/net/URI;)V")) {
+ log(classNode, methodNode, methodInsnNode, "Opens a file");
+ return;
+ }
+ if (isMethodInsnNodeCorrect(methodInsnNode, "mousePress", "(I)V;")) {
+ log(classNode, methodNode, methodInsnNode, "Presses a mouse button");
+ return;
+ }
+ if (isMethodInsnNodeCorrect(methodInsnNode, "mouseRelease", "(I)V;")) {
+ log(classNode, methodNode, methodInsnNode, "Releases a mouse button");
+ return;
+ }
+ if (isMethodInsnNodeCorrect(methodInsnNode, "mouseMove", "(II)V;")) {
+ log(classNode, methodNode, methodInsnNode, "Moves the mouse");
+ return;
+ }
}
}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/CryptoAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/CryptoAnalyzer.java
new file mode 100644
index 0000000..34a94aa
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/CryptoAnalyzer.java
@@ -0,0 +1,38 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code;
+
+import static optic_fusion1.kitsune.Kitsune.LOGGER;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+public class CryptoAnalyzer extends CodeAnalyzer {
+
+ @Override
+ public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode methodInsnNode) {
+ if (isMethodInsnNodeCorrect(methodInsnNode, "", "(Ljava/io/OutputStream;Ljavax/crypto/Cipher;)V")) {
+ LOGGER.info("Initializes a CipherOutputStream");
+ return;
+ }
+ if (isMethodInsnNodeCorrect(methodInsnNode, "getInstance", "(Ljava/lang/String;)Ljavax/crypto/Cipher;")) {
+ AbstractInsnNode previous = methodInsnNode.getPrevious();
+ if (!isAbstractNodeString(previous)) {
+ LOGGER.info("Creates a Cipher instance");
+ return;
+ }
+ LOGGER.info("Creates a Cipher instance with the transformation " + (String) ((LdcInsnNode) previous).cst);
+ }
+ if (isMethodInsnNodeCorrect(methodInsnNode, "", "([BLjava/lang/String;)V")) {
+ LOGGER.info("Initializes a SecretKeySpec");
+ AbstractInsnNode previous = methodInsnNode.getPrevious();
+ if (!isAbstractNodeString(previous)) {
+ LOGGER.info("Creates a SecretKeySpec instance");
+ return;
+ }
+ LOGGER.info("Creates a SecretKeySpec with the algorithm " + (String) ((LdcInsnNode) previous).cst);
+ return;
+ }
+ }
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/FileCodeAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/FileCodeAnalyzer.java
index bb071e6..7cb43f1 100644
--- a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/FileCodeAnalyzer.java
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/FileCodeAnalyzer.java
@@ -30,6 +30,10 @@ public class FileCodeAnalyzer extends CodeAnalyzer {
public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode methodInsnNode) {
if (isMethodInsnNodeCorrect(methodInsnNode, "", "(Ljava/lang/String;)V")) {
AbstractInsnNode minus1 = methodInsnNode.getPrevious();
+ if (!(minus1 instanceof LdcInsnNode)) {
+ log(classNode, methodNode, methodInsnNode, "File created");
+ return;
+ }
String fileName = (String) ((LdcInsnNode) minus1).cst;
log(classNode, methodNode, methodInsnNode, tl("fca_named_file_created", fileName));
return;
@@ -37,6 +41,10 @@ public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode m
if (isMethodInsnNodeCorrect(methodInsnNode, "", "(Ljava/lang/String;Ljava/lang/String;)V")) {
AbstractInsnNode minus1 = methodInsnNode.getPrevious();
AbstractInsnNode minus2 = minus1.getPrevious();
+ if (!(minus1 instanceof LdcInsnNode) || !(minus2 instanceof LdcInsnNode)) {
+ log(classNode, methodNode, methodInsnNode, "File created");
+ return;
+ }
String fileName = (String) ((LdcInsnNode) minus1).cst;
String directoryName = (String) ((LdcInsnNode) minus2).cst;
log(classNode, methodNode, methodInsnNode, tl("fca_named_file_created_dir", fileName, directoryName));
@@ -51,9 +59,6 @@ public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode m
log(classNode, methodNode, methodInsnNode, tl("fca_dir_created"));
return;
}
- if (isMethodInsnNodeCorrect(methodInsnNode, "delete", "()Z")) {
- log(classNode, methodNode, methodInsnNode, tl("fca_file_deleted"));
- }
if (isMethodInsnNodeCorrect(methodInsnNode, "createTempFile", "(Ljava/lang/String;Ljava/lang/String;)Ljava/io/File;")) {
AbstractInsnNode minus1 = methodInsnNode.getPrevious();
AbstractInsnNode minus2 = minus1.getPrevious();
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ScriptCodeAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ScriptCodeAnalyzer.java
new file mode 100644
index 0000000..b4af0dd
--- /dev/null
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ScriptCodeAnalyzer.java
@@ -0,0 +1,18 @@
+package optic_fusion1.kitsune.tool.impl.analyze.analyzer.java.code;
+
+import optic_fusion1.kitsune.util.Utils;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+public class ScriptCodeAnalyzer extends CodeAnalyzer {
+
+ @Override
+ public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode methodInsnNode) {
+ if (isMethodInsnNodeCorrect(methodInsnNode, "getEngineByName", "(Ljava/lang/String;)Ljavax/script/ScriptEngine;")) {
+ Utils.log(classNode, methodNode, methodInsnNode, "Gets a ScriptEngine instance");
+ return;
+ }
+ }
+
+}
diff --git a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ThreadCodeAnalyzer.java b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ThreadCodeAnalyzer.java
index b635f5c..fa4ec93 100644
--- a/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ThreadCodeAnalyzer.java
+++ b/src/main/java/optic_fusion1/kitsune/tool/impl/analyze/analyzer/java/code/ThreadCodeAnalyzer.java
@@ -20,6 +20,7 @@
import static optic_fusion1.kitsune.util.Utils.log;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
@@ -28,9 +29,12 @@ public class ThreadCodeAnalyzer extends CodeAnalyzer {
@Override
public void analyze(ClassNode classNode, MethodNode methodNode, MethodInsnNode methodInsnNode) {
-
if (isMethodInsnNodeCorrect(methodInsnNode, "interrupt", "()V")) {
AbstractInsnNode minus1 = methodInsnNode.getPrevious();
+ if (minus1 instanceof FieldInsnNode) {
+ log(classNode, methodNode, methodInsnNode, tl("thread_interrupted"));
+ return;
+ }
if (isMethodInsnNodeCorrect((MethodInsnNode) minus1, "currentThread", "()Ljava/lang/Thread;")) {
log(classNode, methodNode, methodInsnNode, tl("current_thread_interrupted"));
return;
diff --git a/src/main/resources/r_styles.ini b/src/main/resources/r_styles.ini
new file mode 100644
index 0000000..38d71b6
--- /dev/null
+++ b/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/src/main/resources/r_values.ini b/src/main/resources/r_values.ini
new file mode 100644
index 0000000..b0a5093
--- /dev/null
+++ b/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