From e0ffb01852d61181a586c00af1233225147cdd98 Mon Sep 17 00:00:00 2001 From: Skylot Date: Tue, 6 Jan 2015 19:21:10 +0300 Subject: [PATCH] core: first implementation of '.arsc' parser --- .../main/java/jadx/api/ResourcesLoader.java | 9 +- .../jadx/core/xmlgen/BinaryXMLParser.java | 83 +---- .../jadx/core/xmlgen/CommonBinaryParser.java | 62 ++++ .../jadx/core/xmlgen/ParserConstants.java | 190 ++++++++++++ .../java/jadx/core/xmlgen/ParserStream.java | 94 +++++- .../java/jadx/core/xmlgen/ResTableParser.java | 284 ++++++++++++++++++ .../jadx/core/xmlgen/ResourceStorage.java | 43 +++ .../jadx/core/xmlgen/entry/EntryConfig.java | 38 +++ .../jadx/core/xmlgen/entry/RawNamedValue.java | 19 ++ .../java/jadx/core/xmlgen/entry/RawValue.java | 24 ++ .../jadx/core/xmlgen/entry/ResourceEntry.java | 88 ++++++ .../jadx/core/xmlgen/entry/ValuesParser.java | 168 +++++++++++ .../java/jadx/gui/treemodel/JResource.java | 2 +- 13 files changed, 1020 insertions(+), 84 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/entry/RawValue.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java create mode 100644 jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index a114ffa29..b1497ca1c 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -2,7 +2,9 @@ package jadx.api; import jadx.api.ResourceFile.ZipRef; import jadx.core.codegen.CodeWriter; +import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.files.InputFile; +import jadx.core.xmlgen.ResTableParser; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; @@ -19,11 +21,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; // TODO: move to core package -final class ResourcesLoader { +public final class ResourcesLoader { private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); private static final int READ_BUFFER_SIZE = 8 * 1024; - private static final int LOAD_SIZE_LIMIT = 500 * 1024; + private static final int LOAD_SIZE_LIMIT = 10 * 1024 * 1024; private JadxDecompiler jadxRef; @@ -81,6 +83,9 @@ final class ResourcesLoader { case MANIFEST: case XML: return jadxRef.getXmlParser().parse(inputStream); + + case ARSC: + return new ResTableParser().decodeToCodeWriter(inputStream); } return loadToCodeWriter(inputStream); } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java index 17f1e4df7..fed045b61 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java @@ -11,7 +11,6 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; -import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; @@ -29,37 +28,11 @@ import org.slf4j.LoggerFactory; Check Element chunk size */ -public class BinaryXMLParser { +public class BinaryXMLParser extends CommonBinaryParser { private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class); - private static final Charset STRING_CHARSET_UTF16 = Charset.forName("UTF-16LE"); - private static final Charset STRING_CHARSET_UTF8 = Charset.forName("UTF-8"); - - private static final int RES_NULL_TYPE = 0x0000; - private static final int RES_STRING_POOL_TYPE = 0x0001; - private static final int RES_TABLE_TYPE = 0x0002; - - private static final int RES_XML_TYPE = 0x0003; - private static final int RES_XML_FIRST_CHUNK_TYPE = 0x0100; - private static final int RES_XML_START_NAMESPACE_TYPE = 0x0100; - private static final int RES_XML_END_NAMESPACE_TYPE = 0x0101; - private static final int RES_XML_START_ELEMENT_TYPE = 0x0102; - private static final int RES_XML_END_ELEMENT_TYPE = 0x0103; - private static final int RES_XML_CDATA_TYPE = 0x0104; - private static final int RES_XML_LAST_CHUNK_TYPE = 0x017f; - private static final int RES_XML_RESOURCE_MAP_TYPE = 0x0180; - - private static final int RES_TABLE_PACKAGE_TYPE = 0x0200; - private static final int RES_TABLE_TYPE_TYPE = 0x0201; - private static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; - - // string pool flags - private static final int SORTED_FLAG = 1; - private static final int UTF8_FLAG = 1 << 8; - private CodeWriter writer; - private ParserStream is; private String[] strings; private String nsPrefix = "ERROR"; @@ -129,7 +102,7 @@ public class BinaryXMLParser { // NullType is just doing nothing break; case RES_STRING_POOL_TYPE: - parseStringPool(); + strings = parseStringPoolNoType(); break; case RES_XML_RESOURCE_MAP_TYPE: parseResourceMap(); @@ -154,51 +127,6 @@ public class BinaryXMLParser { } } - private void parseStringPool() throws IOException { - if (is.readInt16() != 0x001c) { - die("Header header size not 28"); - } - int hsize = is.readInt32(); - int stringCount = is.readInt32(); - int styleCount = is.readInt32(); - int flags = is.readInt32(); - int stringsStart = is.readInt32(); - int stylesStart = is.readInt32(); - // skip string offsets - is.skip(stringCount * 4); - strings = new String[stringCount]; - if ((flags & UTF8_FLAG) != 0) { - // UTF-8 - long start = is.getPos(); - for (int i = 0; i < stringCount; i++) { - int charsCount = is.decodeLength8(); - int len = is.decodeLength8(); - strings[i] = new String(is.readArray(len), STRING_CHARSET_UTF8); - int zero = is.readInt8(); - if (zero != 0) { - die("Not a trailing zero at string end: " + zero + ", " + strings[i]); - } - } - long shift = is.getPos() - start; - if (shift % 2 != 0) { - is.skip(1); - } - } else { - // UTF-16 - for (int i = 0; i < stringCount; i++) { - int len = is.decodeLength16(); - strings[i] = new String(is.readArray(len * 2), STRING_CHARSET_UTF16); - int zero = is.readInt16(); - if (zero != 0) { - die("Not a trailing zero at string end: " + zero + ", " + strings[i]); - } - } - } - if (styleCount != 0) { - die("Styles parsing in string pool not yet implemented"); - } - } - private void parseResourceMap() throws IOException { if (is.readInt16() != 0x8) { die("Header size of resmap is not 8!"); @@ -356,7 +284,7 @@ public class BinaryXMLParser { break; default: - writer.add("UNKNOWN_DATA_TYPE_" + attrValDataType); + writer.add("UNKNOWN_DATA_TYPE_0x" + Integer.toHexString(attrValDataType)); break; } } @@ -387,9 +315,4 @@ public class BinaryXMLParser { writer.decIndent(); } } - - private void die(String message) { - throw new JadxRuntimeException("Decode error: " + message - + ", position: 0x" + Long.toHexString(is.getPos())); - } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java new file mode 100644 index 000000000..0e2cc68a2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java @@ -0,0 +1,62 @@ +package jadx.core.xmlgen; + +import java.io.IOException; + +public class CommonBinaryParser extends ParserConstants { + + protected ParserStream is; + + protected String[] parseStringPool() throws IOException { + is.checkInt16(RES_STRING_POOL_TYPE, "String pool expected"); + return parseStringPoolNoType(); + } + + protected String[] parseStringPoolNoType() throws IOException { + long start = is.getPos() - 2; + is.checkInt16(0x001c, "String pool header size not 0x001c"); + long size = is.readUInt32(); + + int stringCount = is.readInt32(); + int styleCount = is.readInt32(); + int flags = is.readInt32(); + long stringsStart = is.readInt32(); + long stylesStart = is.readInt32(); + + int[] stringsOffset = is.readInt32Array(stringCount); + int[] stylesOffset = is.readInt32Array(styleCount); + + is.checkPos(start + stringsStart, "Expected strings start"); + String[] strings = new String[stringCount]; + if ((flags & UTF8_FLAG) != 0) { + // UTF-8 + for (int i = 0; i < stringCount; i++) { + // is.checkPos(start + stringsStart + stringsOffset[i], "Expected string start"); + strings[i] = is.readString8(); + } + } else { + // UTF-16 + long stringsStartOffset = start + stringsStart; + for (int i = 0; i < stringCount; i++) { + // is.checkPos(stringsStartOffset + stringsOffset[i], "Expected string start"); + // TODO: don't trust specified string length, read until \0 + // TODO: stringsOffset can be same for different indexes + strings[i] = is.readString16(); + } + } + if (stylesStart != 0) { + is.checkPos(start + stylesStart, "Expected styles start"); + if (styleCount != 0) { + // TODO: implement styles parsing + } + } + // skip padding zeroes + is.skip(start + size - is.getPos()); + return strings; + } + + protected void die(String message) throws IOException { + throw new IOException("Decode error: " + message + + ", position: 0x" + Long.toHexString(is.getPos())); + } + +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java b/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java new file mode 100644 index 000000000..36e9e7102 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java @@ -0,0 +1,190 @@ +package jadx.core.xmlgen; + +public class ParserConstants { + + /** + * Chunk types + */ + protected static final int RES_NULL_TYPE = 0x0000; + protected static final int RES_STRING_POOL_TYPE = 0x0001; + protected static final int RES_TABLE_TYPE = 0x0002; + + protected static final int RES_XML_TYPE = 0x0003; + protected static final int RES_XML_FIRST_CHUNK_TYPE = 0x0100; + protected static final int RES_XML_START_NAMESPACE_TYPE = 0x0100; + protected static final int RES_XML_END_NAMESPACE_TYPE = 0x0101; + protected static final int RES_XML_START_ELEMENT_TYPE = 0x0102; + protected static final int RES_XML_END_ELEMENT_TYPE = 0x0103; + protected static final int RES_XML_CDATA_TYPE = 0x0104; + protected static final int RES_XML_LAST_CHUNK_TYPE = 0x017f; + protected static final int RES_XML_RESOURCE_MAP_TYPE = 0x0180; + + protected static final int RES_TABLE_PACKAGE_TYPE = 0x0200; + protected static final int RES_TABLE_TYPE_TYPE = 0x0201; + protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; + + /** + * Type constants + */ + // Contains no data. + protected static final int TYPE_NULL = 0x00; + // The 'data' holds a ResTable_ref, a reference to another resource table entry. + protected static final int TYPE_REFERENCE = 0x01; + // The 'data' holds an attribute resource identifier. + protected static final int TYPE_ATTRIBUTE = 0x02; + // The 'data' holds an index into the containing resource table's global value string pool. + protected static final int TYPE_STRING = 0x03; + // The 'data' holds a single-precision floating point number. + protected static final int TYPE_FLOAT = 0x04; + // The 'data' holds a complex number encoding a dimension value, such as "100in". + protected static final int TYPE_DIMENSION = 0x05; + // The 'data' holds a complex number encoding a fraction of a container. + protected static final int TYPE_FRACTION = 0x06; + // Beginning of integer flavors... + protected static final int TYPE_FIRST_INT = 0x10; + // The 'data' is a raw integer value of the form n..n. + protected static final int TYPE_INT_DEC = 0x10; + // The 'data' is a raw integer value of the form 0xn..n. + protected static final int TYPE_INT_HEX = 0x11; + // The 'data' is either 0 or 1, for input "false" or "true" respectively. + protected static final int TYPE_INT_BOOLEAN = 0x12; + // Beginning of color integer flavors... + protected static final int TYPE_FIRST_COLOR_INT = 0x1c; + // The 'data' is a raw integer value of the form #aarrggbb. + protected static final int TYPE_INT_COLOR_ARGB8 = 0x1c; + // The 'data' is a raw integer value of the form #rrggbb. + protected static final int TYPE_INT_COLOR_RGB8 = 0x1d; + // The 'data' is a raw integer value of the form #argb. + protected static final int TYPE_INT_COLOR_ARGB4 = 0x1e; + // The 'data' is a raw integer value of the form #rgb. + protected static final int TYPE_INT_COLOR_RGB4 = 0x1f; + // ...end of integer flavors. + protected static final int TYPE_LAST_COLOR_INT = 0x1f; + // ...end of integer flavors. + protected static final int TYPE_LAST_INT = 0x1f; + + // Where the unit type information is. This gives us 16 possible + // types, as defined below. + protected static final int COMPLEX_UNIT_SHIFT = 0; + protected static final int COMPLEX_UNIT_MASK = 0xf; + + // TYPE_DIMENSION: Value is raw pixels. + protected static final int COMPLEX_UNIT_PX = 0; + // TYPE_DIMENSION: Value is Device Independent Pixels. + protected static final int COMPLEX_UNIT_DIP = 1; + // TYPE_DIMENSION: Value is a Scaled device independent Pixels. + protected static final int COMPLEX_UNIT_SP = 2; + // TYPE_DIMENSION: Value is in points. + protected static final int COMPLEX_UNIT_PT = 3; + // TYPE_DIMENSION: Value is in inches. + protected static final int COMPLEX_UNIT_IN = 4; + // TYPE_DIMENSION: Value is in millimeters. + protected static final int COMPLEX_UNIT_MM = 5; + + // TYPE_FRACTION: A basic fraction of the overall size. + protected static final int COMPLEX_UNIT_FRACTION = 0; + // TYPE_FRACTION: A fraction of the parent size. + protected static final int COMPLEX_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. + protected static final int COMPLEX_RADIX_SHIFT = 4; + protected static final int COMPLEX_RADIX_MASK = 0x3; + + // The mantissa is an integral number -- i.e., 0xnnnnnn.0 + protected static final int COMPLEX_RADIX_23p0 = 0; + // The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn + protected static final int COMPLEX_RADIX_16p7 = 1; + // The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn + protected static final int COMPLEX_RADIX_8p15 = 2; + // The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn + protected static final int COMPLEX_RADIX_0p23 = 3; + + // Where the actual value is. This gives us 23 bits of + // precision. The top bit is the sign. + protected static final int COMPLEX_MANTISSA_SHIFT = 8; + protected static final int COMPLEX_MANTISSA_MASK = 0xffffff; + + protected static final double MANTISSA_MULT = 1.0f / (1 << COMPLEX_MANTISSA_SHIFT); + protected static final double[] RADIX_MULTS = new double[]{ + 1.0f * MANTISSA_MULT, + 1.0f / (1 << 7) * MANTISSA_MULT, + 1.0f / (1 << 15) * MANTISSA_MULT, + 1.0f / (1 << 23) * MANTISSA_MULT + }; + + /** + * String pool flags + */ + protected static final int SORTED_FLAG = 1; + protected static final int UTF8_FLAG = 1 << 8; + + protected static final int NO_ENTRY = 0xFFFFFFFF; + + /** + * ResTable_entry + */ + // If set, this is a complex entry, holding a set of name/value mappings. + // It is followed by an array of ResTable_map structures. + protected static final int FLAG_COMPLEX = 0x0001; + // If set, this resource has been declared public, so libraries are allowed to reference it. + protected static final int FLAG_PUBLIC = 0x0002; + + /** + * ResTable_map + */ + protected static final int ATTR_TYPE = ResMakeInternal(0); + // For integral attributes, this is the minimum value it can hold. + protected static final int ATTR_MIN = ResMakeInternal(1); + // For integral attributes, this is the maximum value it can hold. + protected static final int ATTR_MAX = ResMakeInternal(2); + // Localization of this resource is can be encouraged or required with an aapt flag if this is set + protected static final int ATTR_L10N = ResMakeInternal(3); + // for plural support, see android.content.res.PluralRules#attrForQuantity(int) + protected static final int ATTR_OTHER = ResMakeInternal(4); + protected static final int ATTR_ZERO = ResMakeInternal(5); + protected static final int ATTR_ONE = ResMakeInternal(6); + protected static final int ATTR_TWO = ResMakeInternal(7); + protected static final int ATTR_FEW = ResMakeInternal(8); + protected static final int ATTR_MANY = ResMakeInternal(9); + + private static int ResMakeInternal(int entry) { + return 0x01000000 | (entry & 0xFFFF); + } + + protected static boolean isResInternalId(int resid) { + return ((resid & 0xFFFF0000) != 0 && (resid & 0xFF0000) == 0); + } + + // Bit mask of allowed types, for use with ATTR_TYPE. + protected static final int ATTR_TYPE_ANY = 0x0000FFFF; + // Attribute holds a references to another resource. + protected static final int ATTR_TYPE_REFERENCE = 1; + // Attribute holds a generic string. + protected static final int ATTR_TYPE_STRING = 1 << 1; + // Attribute holds an integer value. ATTR_MIN and ATTR_MIN can + // optionally specify a constrained range of possible integer values. + protected static final int ATTR_TYPE_INTEGER = 1 << 2; + // Attribute holds a boolean integer. + protected static final int ATTR_TYPE_BOOLEAN = 1 << 3; + // Attribute holds a color value. + protected static final int ATTR_TYPE_COLOR = 1 << 4; + // Attribute holds a floating point value. + protected static final int ATTR_TYPE_FLOAT = 1 << 5; + // Attribute holds a dimension value, such as "20px". + protected static final int ATTR_TYPE_DIMENSION = 1 << 6; + // Attribute holds a fraction value, such as "20%". + protected static final int ATTR_TYPE_FRACTION = 1 << 7; + // Attribute holds an enumeration. The enumeration values are + // supplied as additional entries in the map. + protected static final int ATTR_TYPE_ENUM = 1 << 16; + // Attribute holds a bitmaks of flags. The flag bit values are + // supplied as additional entries in the map. + protected static final int ATTR_TYPE_FLAGS = 1 << 17; + + // Enum of localization modes, for use with ATTR_L10N + protected static final int ATTR_L10N_NOT_REQUIRED = 0; + protected static final int ATTR_L10N_SUGGESTED = 1; + +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java b/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java index e1fd2d5e6..8c51c9909 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java @@ -2,9 +2,13 @@ package jadx.core.xmlgen; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; public class ParserStream { + protected static final Charset STRING_CHARSET_UTF16 = Charset.forName("UTF-16LE"); + protected static final Charset STRING_CHARSET_UTF8 = Charset.forName("UTF-8"); + private final InputStream input; private long readPos = 0; @@ -38,7 +42,50 @@ public class ParserStream { return b4 << 24 | (b3 & 0xFF) << 16 | (b2 & 0xFF) << 8 | (b1 & 0xFF); } - public byte[] readArray(int count) throws IOException { + public long readUInt32() throws IOException { + return readInt32() & 0xFFFFFFFFL; + } + + public String readString8Fixed(int len) throws IOException { + String str = new String(readInt8Array(len), STRING_CHARSET_UTF8); + return str.trim(); + } + + public String readString16Fixed(int len) throws IOException { + String str = new String(readInt8Array(len * 2), STRING_CHARSET_UTF16); + return str.trim(); + } + + public String readString8() throws IOException { + decodeLength8(); + int len = decodeLength8(); + String str = new String(readInt8Array(len), STRING_CHARSET_UTF8); + checkInt8(0, "Not a trailing zero at string8 end"); + return str; + } + + public String readString16() throws IOException { + int len = decodeLength16(); + String str = new String(readInt8Array(len), STRING_CHARSET_UTF16); + checkInt16(0, "Not a trailing zero at string16 end"); + return str; + } + + public int[] readInt32Array(int count) throws IOException { + if (count == 0) { + return new int[0]; + } + int[] arr = new int[count]; + for (int i = 0; i < count; i++) { + arr[i] = readInt32(); + } + return arr; + } + + public byte[] readInt8Array(int count) throws IOException { + if (count == 0) { + return new byte[0]; + } readPos += count; byte[] arr = new byte[count]; int pos = input.read(arr, 0, count); @@ -64,6 +111,46 @@ public class ParserStream { } } + public void checkInt8(int expected, String error) throws IOException { + int v = readInt8(); + if (v != expected) { + throwException(error, expected, v); + } + } + + public void checkInt16(int expected, String error) throws IOException { + int v = readInt16(); + if (v != expected) { + throwException(error, expected, v); + } + } + + private void throwException(String error, int expected, int actual) throws IOException { + throw new IOException(error + + ", expected: 0x" + Integer.toHexString(expected) + + ", actual: 0x" + Integer.toHexString(actual) + + ", offset: 0x" + Long.toHexString(getPos())); + } + + public void checkPos(long expectedOffset, String error) throws IOException { + if (getPos() != expectedOffset) { + throw new IOException(error + ", expected offset: 0x" + Long.toHexString(expectedOffset) + + ", actual: 0x" + Long.toHexString(getPos())); + } + } + + public void skipToPos(long expectedOffset, String error) throws IOException { + long pos = getPos(); + if (pos < expectedOffset) { + skip(expectedOffset - pos); + pos = getPos(); + } + if (pos != expectedOffset) { + throw new IOException(error + ", expected offset: 0x" + Long.toHexString(expectedOffset) + + ", actual: 0x" + Long.toHexString(pos)); + } + } + public int decodeLength8() throws IOException { int len = readInt8(); if ((len & 0x80) != 0) { @@ -79,4 +166,9 @@ public class ParserStream { } return len; } + + @Override + public String toString() { + return "pos: 0x" + Long.toHexString(readPos); + } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java new file mode 100644 index 000000000..17da9e263 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -0,0 +1,284 @@ +package jadx.core.xmlgen; + +import jadx.core.codegen.CodeWriter; +import jadx.core.utils.Utils; +import jadx.core.xmlgen.entry.EntryConfig; +import jadx.core.xmlgen.entry.RawNamedValue; +import jadx.core.xmlgen.entry.RawValue; +import jadx.core.xmlgen.entry.ResourceEntry; +import jadx.core.xmlgen.entry.ValuesParser; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ResTableParser extends CommonBinaryParser { + + private static final Logger LOG = LoggerFactory.getLogger(ResTableParser.class); + + private static final class PackageChunk { + private final int id; + private final String name; + private final String[] typeStrings; + private final String[] keyStrings; + + private PackageChunk(int id, String name, String[] typeStrings, String[] keyStrings) { + this.id = id; + this.name = name; + this.typeStrings = typeStrings; + this.keyStrings = keyStrings; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String[] getTypeStrings() { + return typeStrings; + } + + public String[] getKeyStrings() { + return keyStrings; + } + } + + private String[] strings; + private final ResourceStorage resStorage = new ResourceStorage(); + + public void decode(InputStream inputStream) throws IOException { + is = new ParserStream(inputStream); + decodeTableChunk(); + resStorage.finish(); + } + + public CodeWriter decodeToCodeWriter(InputStream inputStream) { + try { + decode(inputStream); + } catch (IOException e) { + LOG.debug("arsc decode failed", e); + CodeWriter cw = new CodeWriter(); + cw.add("Error decode arsc"); + cw.startLine(Utils.getStackTrace(e)); + return cw; + } + + CodeWriter writer = new CodeWriter(); + ValuesParser vp = new ValuesParser(strings, resStorage); + for (ResourceEntry ri : resStorage.getResources()) { + writer.startLine(ri + ": " + vp.getValueString(ri)); + } + writer.finish(); + return writer; + } + + public ResourceStorage getResStorage() { + return resStorage; + } + + void decodeTableChunk() throws IOException { + is.checkInt16(RES_TABLE_TYPE, "Not a table chunk"); + is.checkInt16(0x000c, "Unexpected table header size"); + /*int size = */ + is.readInt32(); + int pkgCount = is.readInt32(); + + strings = parseStringPool(); + for (int i = 0; i < pkgCount; i++) { + parsePackage(); + } + } + + private PackageChunk parsePackage() throws IOException { + long start = is.getPos(); + is.checkInt16(RES_TABLE_PACKAGE_TYPE, "Not a table chunk"); + int headerSize = is.readInt16(); + if (headerSize != 0x011c && headerSize != 0x0120) { + die("Unexpected package header size"); + } + long size = is.readUInt32(); + long endPos = start + size; + + int id = is.readInt32(); + String name = is.readString16Fixed(128); + + long typeStringsOffset = start + is.readInt32(); + /* int lastPublicType = */ + is.readInt32(); + long keyStringsOffset = start + is.readInt32(); + /* int lastPublicKey = */ + is.readInt32(); + if (headerSize == 0x0120) { + /* int typeIdOffset = */ + is.readInt32(); + } + + String[] typeStrings = null; + if (typeStringsOffset != 0) { + is.skipToPos(typeStringsOffset, "Expected typeStrings string pool"); + typeStrings = parseStringPool(); + } + String[] keyStrings = null; + if (keyStringsOffset != 0) { + is.skipToPos(keyStringsOffset, "Expected keyStrings string pool"); + keyStrings = parseStringPool(); + } + + PackageChunk pkg = new PackageChunk(id, name, typeStrings, keyStrings); + + while (is.getPos() < endPos) { + long chunkStart = is.getPos(); + int type = is.readInt16(); + if (type == RES_NULL_TYPE) { + continue; + } + if (type == RES_TABLE_TYPE_SPEC_TYPE) { + parseTypeSpecChunk(); + } else if (type == RES_TABLE_TYPE_TYPE) { + parseTypeChunk(chunkStart, pkg); + } + } + return pkg; + } + + private void parseTypeSpecChunk() throws IOException { + is.checkInt16(0x0010, "Unexpected type spec header size"); + /*int size = */ + is.readInt32(); + + int id = is.readInt8(); + is.skip(3); + int entryCount = is.readInt32(); + for (int i = 0; i < entryCount; i++) { + int entryFlag = is.readInt32(); + } + } + + private void parseTypeChunk(long start, PackageChunk pkg) throws IOException { + int headerSize = is.readInt16(); + if (headerSize != 0x34 && headerSize != 0x38 && headerSize != 0x44) { + die("Unexpected type header size: 0x" + Integer.toHexString(headerSize)); + } + /*int size =*/ + is.readInt32(); + + int id = is.readInt8(); + is.checkInt8(0, "type chunk, res0"); + is.checkInt16(0, "type chunk, res1"); + int entryCount = is.readInt32(); + long entriesStart = start + is.readInt32(); + + EntryConfig config = parseConfig(); + + int[] entryIndexes = new int[entryCount]; + for (int i = 0; i < entryCount; i++) { + entryIndexes[i] = is.readInt32(); + } + + is.checkPos(entriesStart, "Expected entry start"); + for (int i = 0; i < entryCount; i++) { + if (entryIndexes[i] != NO_ENTRY) { + parseEntry(pkg, id, i, config); + } + } + } + + private void parseEntry(PackageChunk pkg, int typeId, int entryId, EntryConfig config) throws IOException { + /* int size = */ + is.readInt16(); + int flags = is.readInt16(); + int key = is.readInt32(); + + int resRef = pkg.getId() << 24 | typeId << 16 | entryId; + String typeName = pkg.getTypeStrings()[typeId - 1]; + String keyName = pkg.getKeyStrings()[key]; + ResourceEntry ri = new ResourceEntry(resRef, pkg.getName(), typeName, keyName); + ri.setConfig(config); + + if ((flags & FLAG_COMPLEX) == 0) { + ri.setSimpleValue(parseValue()); + } else { + int parentRef = is.readInt32(); + ri.setParentRef(parentRef); + int count = is.readInt32(); + List values = new ArrayList(count); + for (int i = 0; i < count; i++) { + values.add(parseValueMap()); + } + ri.setNamedValues(values); + } + resStorage.add(ri); + } + + private RawNamedValue parseValueMap() throws IOException { + int nameRef = is.readInt32(); + return new RawNamedValue(nameRef, parseValue()); + } + + private RawValue parseValue() throws IOException { + is.checkInt16(8, "value size"); + is.checkInt8(0, "value res0 not 0"); + int dataType = is.readInt8(); + int data = is.readInt32(); + return new RawValue(dataType, data); + } + + private EntryConfig parseConfig() throws IOException { + long start = is.getPos(); + int size = is.readInt32(); + + EntryConfig config = new EntryConfig(); + + is.readInt16(); //mcc + is.readInt16(); //mnc + + config.setLanguage(parseLocale()); + config.setCountry(parseLocale()); + + int orientation = is.readInt8(); + int touchscreen = is.readInt8(); + int density = is.readInt16(); + /* + is.readInt8(); // keyboard + is.readInt8(); // navigation + is.readInt8(); // inputFlags + is.readInt8(); // inputPad0 + + is.readInt16(); // screenWidth + is.readInt16(); // screenHeight + + is.readInt16(); // sdkVersion + is.readInt16(); // minorVersion + + is.readInt8(); // screenLayout + is.readInt8(); // uiMode + is.readInt16(); // smallestScreenWidthDp + + is.readInt16(); // screenWidthDp + is.readInt16(); // screenHeightDp + */ + is.skipToPos(start + size, "Skip config parsing"); + return config; + } + + private String parseLocale() throws IOException { + int b1 = is.readInt8(); + int b2 = is.readInt8(); + String str = null; + if (b1 != 0 && b2 != 0) { + if ((b1 & 0x80) == 0) { + str = new String(new char[]{(char) b1, (char) b2}); + } else { + LOG.warn("TODO: parse locale: 0x" + Integer.toHexString(b1) + Integer.toHexString(b1)); + } + } + return str; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java new file mode 100644 index 000000000..2d6affd84 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java @@ -0,0 +1,43 @@ +package jadx.core.xmlgen; + +import jadx.core.utils.Utils; +import jadx.core.xmlgen.entry.ResourceEntry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ResourceStorage { + + private static final Comparator COMPARATOR = new Comparator() { + @Override + public int compare(ResourceEntry a, ResourceEntry b) { + return Utils.compare(a.getId(), b.getId()); + } + }; + + private final List list = new ArrayList(); + + public Collection getResources() { + return list; + } + + public void add(ResourceEntry ri) { + list.add(ri); + } + + public void finish() { + Collections.sort(list, COMPARATOR); + } + + public ResourceEntry getByRef(int refId) { + ResourceEntry key = new ResourceEntry(refId); + int index = Collections.binarySearch(list, key, COMPARATOR); + if (index < 0) { + return null; + } + return list.get(index); + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java new file mode 100644 index 000000000..99731003c --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java @@ -0,0 +1,38 @@ +package jadx.core.xmlgen.entry; + +public class EntryConfig { + private String language; + private String country; + + public void setLanguage(String language) { + this.language = language; + } + + public String getLanguage() { + return language; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCountry() { + return country; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (language != null) { + sb.append(language); + } + if (country != null) { + sb.append("-r").append(country); + } + if (sb.length() != 0) { + sb.insert(0, " ["); + sb.append(']'); + } + return sb.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java new file mode 100644 index 000000000..c75da989f --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java @@ -0,0 +1,19 @@ +package jadx.core.xmlgen.entry; + +public class RawNamedValue { + private final int nameRef; + private final RawValue rawValue; + + public RawNamedValue(int nameRef, RawValue rawValue) { + this.nameRef = nameRef; + this.rawValue = rawValue; + } + + public int getNameRef() { + return nameRef; + } + + public RawValue getRawValue() { + return rawValue; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawValue.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawValue.java new file mode 100644 index 000000000..3aa522a94 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawValue.java @@ -0,0 +1,24 @@ +package jadx.core.xmlgen.entry; + +public final class RawValue { + private final int dataType; + private final int data; + + public RawValue(int dataType, int data) { + this.dataType = dataType; + this.data = data; + } + + public int getDataType() { + return dataType; + } + + public int getData() { + return data; + } + + @Override + public String toString() { + return "RawValue: type=0x" + Integer.toHexString(dataType) + ", value=" + data; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java new file mode 100644 index 000000000..72b01be17 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java @@ -0,0 +1,88 @@ +package jadx.core.xmlgen.entry; + +import java.util.List; + +public final class ResourceEntry { + + private final int id; + private final String pkgName; + private final String typeName; + private final String keyName; + + private int parentRef; + private RawValue simpleValue; + private List namedValues; + private EntryConfig config; + + public ResourceEntry(int id, String pkgName, String typeName, String keyName) { + this.id = id; + this.pkgName = pkgName; + this.typeName = typeName; + this.keyName = keyName; + } + + public ResourceEntry(int id) { + this(id, "", "", ""); + } + + public int getId() { + return id; + } + + public String getPkgName() { + return pkgName; + } + + public String getTypeName() { + return typeName; + } + + public String getKeyName() { + return keyName; + } + + public void setParentRef(int parentRef) { + this.parentRef = parentRef; + } + + public int getParentRef() { + return parentRef; + } + + public RawValue getSimpleValue() { + return simpleValue; + } + + public void setSimpleValue(RawValue simpleValue) { + this.simpleValue = simpleValue; + } + + public void setNamedValues(List namedValues) { + this.namedValues = namedValues; + } + + public List getNamedValues() { + return namedValues; + } + + public void setConfig(EntryConfig config) { + this.config = config; + } + + public EntryConfig getConfig() { + return config; + } + + public String formatAsRef() { + return "@" + typeName + "/" + keyName; + } + + public String formatAsAttribute() { + return "?" + typeName + "/" + keyName; + } + + @Override + public String toString() { + return " 0x" + Integer.toHexString(id) + " (" + id + ")" + config + " = " + typeName + "." + keyName; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java new file mode 100644 index 000000000..ad372576e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java @@ -0,0 +1,168 @@ +package jadx.core.xmlgen.entry; + +import jadx.core.xmlgen.ParserConstants; +import jadx.core.xmlgen.ResourceStorage; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ValuesParser extends ParserConstants { + private static final Logger LOG = LoggerFactory.getLogger(ValuesParser.class); + + private final String[] strings; + private final ResourceStorage resStorage; + + public ValuesParser(String[] strings, ResourceStorage resourceStorage) { + this.strings = strings; + this.resStorage = resourceStorage; + } + + public String getValueString(ResourceEntry ri) { + RawValue simpleValue = ri.getSimpleValue(); + if (simpleValue != null) { + return decodeValue(simpleValue); + } + List namedValues = ri.getNamedValues(); + List strList = new ArrayList(namedValues.size()); + for (RawNamedValue value : namedValues) { + String nameStr = decodeNameRef(value.getNameRef()); + String valueStr = decodeValue(value.getRawValue()); + if (nameStr == null) { + strList.add(valueStr); + } else { + strList.add(nameStr + "=" + valueStr); + } + } + return strList.toString(); + } + + public String decodeValue(RawValue value) { + int dataType = value.getDataType(); + int data = value.getData(); + switch (dataType) { + case TYPE_NULL: + return null; + case TYPE_STRING: + return strings[data]; + case TYPE_INT_DEC: + return Integer.toString(data); + case TYPE_INT_HEX: + return Integer.toHexString(data); + case TYPE_INT_BOOLEAN: + return data == 0 ? "false" : "true"; + case TYPE_FLOAT: + return Float.toString(Float.intBitsToFloat(data)); + + case TYPE_INT_COLOR_ARGB8: + return String.format("#%08x", data); + case TYPE_INT_COLOR_RGB8: + return String.format("#%06x", data & 0xFFFFFF); + case TYPE_INT_COLOR_ARGB4: + return String.format("#%04x", data & 0xFFFF); + case TYPE_INT_COLOR_RGB4: + return String.format("#%03x", data & 0xFFF); + + case TYPE_REFERENCE: { + ResourceEntry ri = resStorage.getByRef(data); + if (ri == null) { + return "?unknown_ref: " + Integer.toHexString(data); + } + return ri.formatAsRef(); + } + + case TYPE_ATTRIBUTE: { + ResourceEntry ri = resStorage.getByRef(data); + if (ri == null) { + return "?unknown_ref: " + Integer.toHexString(data); + } + return ri.formatAsAttribute(); + } + + case TYPE_DIMENSION: + return decodeComplex(data, false); + case TYPE_FRACTION: + return decodeComplex(data, true); + + default: + LOG.warn("Unknown data type: 0x" + Integer.toHexString(dataType) + " " + data); + return " ?0x" + Integer.toHexString(dataType) + " " + data; + } + } + + private String decodeNameRef(int nameRef) { + int ref = nameRef; + if (isResInternalId(nameRef)) { + ref = nameRef & ATTR_TYPE_ANY; + if (ref == 0) { + return null; + } + } + ResourceEntry ri = resStorage.getByRef(ref); + if (ri != null) { + return ri.getTypeName() + "." + ri.getKeyName(); + } + return "?0x" + Integer.toHexString(nameRef); + } + + private String decodeComplex(int data, boolean isFraction) { + double value = (data & (COMPLEX_MANTISSA_MASK << COMPLEX_MANTISSA_SHIFT)) + * RADIX_MULTS[(data >> COMPLEX_RADIX_SHIFT) & COMPLEX_RADIX_MASK]; + int unitType = data & COMPLEX_UNIT_MASK; + String unit; + if (isFraction) { + value *= 100; + switch (unitType) { + case COMPLEX_UNIT_FRACTION: + unit = "%"; + break; + case COMPLEX_UNIT_FRACTION_PARENT: + unit = "%p"; + break; + + default: + unit = "?f" + Integer.toHexString(unitType); + } + } else { + switch (unitType) { + case COMPLEX_UNIT_PX: + unit = "px"; + break; + case COMPLEX_UNIT_DIP: + unit = "dp"; + break; + case COMPLEX_UNIT_SP: + unit = "sp"; + break; + case COMPLEX_UNIT_PT: + unit = "pt"; + break; + case COMPLEX_UNIT_IN: + unit = "in"; + break; + case COMPLEX_UNIT_MM: + unit = "mm"; + break; + + default: + unit = "?d" + Integer.toHexString(unitType); + } + } + return doubleToString(value) + unit; + } + + private static String doubleToString(double value) { + if (value == Math.ceil(value)) { + return Integer.toString((int) value); + } else { + // remove trailing zeroes + NumberFormat f = NumberFormat.getInstance(); + f.setMaximumFractionDigits(4); + f.setMinimumIntegerDigits(1); + return f.format(value); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index 188306eb9..ca54bdfb9 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -144,7 +144,6 @@ public class JResource extends JNode implements Comparable { private boolean isSupportedForView(ResourceType type) { switch (type) { case CODE: - case ARSC: case FONT: case IMG: case LIB: @@ -152,6 +151,7 @@ public class JResource extends JNode implements Comparable { case MANIFEST: case XML: + case ARSC: case UNKNOWN: return true; }