Commit e0ffb018 authored by Skylot's avatar Skylot

core: first implementation of '.arsc' parser

parent 53be92c6
......@@ -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);
}
......
......@@ -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()));
}
}
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()));
}
}
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;
}
......@@ -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);
}
}
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<RawNamedValue> values = new ArrayList<RawNamedValue>(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;
}
}
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<ResourceEntry> COMPARATOR = new Comparator<ResourceEntry>() {
@Override
public int compare(ResourceEntry a, ResourceEntry b) {
return Utils.compare(a.getId(), b.getId());
}
};
private final List<ResourceEntry> list = new ArrayList<ResourceEntry>();
public Collection<ResourceEntry> 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);
}
}
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();
}
}
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;
}
}
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;
}
}
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<RawNamedValue> 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<RawNamedValue> namedValues) {
this.namedValues = namedValues;
}
public List<RawNamedValue> 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;
}
}
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<RawNamedValue> namedValues = ri.getNamedValues();
List<String> strList = new ArrayList<String>(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);
}
}
}
......@@ -144,7 +144,6 @@ public class JResource extends JNode implements Comparable<JResource> {
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<JResource> {
case MANIFEST:
case XML:
case ARSC:
case UNKNOWN:
return true;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment