package com.googlecode.d2j.ai; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; /** * Minimal, dependency-free parser for Android binary XML (AXML) — the on-disk * format of a compiled {@code AndroidManifest.xml}. * *

This is intentionally a thin event reader: it decodes the string pool and * walks the XML chunk stream, emitting START/END element and attribute events. * It does not resolve resource references against {@code resources.arsc} — for a * manifest that is rarely needed, and string/int/boolean values (the ones that * matter for triage: package, sdk levels, permissions, exported flags) decode * directly from the manifest itself. * *

Format reference: AOSP {@code ResourceTypes.h}. */ public final class AxmlParser { // Chunk types. private static final int RES_STRING_POOL_TYPE = 0x0001; private static final int RES_XML_TYPE = 0x0003; 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_RESOURCE_MAP_TYPE = 0x0180; // String pool flags. private static final int UTF8_FLAG = 0x00000100; // Res_value data types. private static final int TYPE_REFERENCE = 0x01; private static final int TYPE_STRING = 0x03; private static final int TYPE_INT_DEC = 0x10; private static final int TYPE_INT_HEX = 0x11; private static final int TYPE_INT_BOOLEAN = 0x12; /** A decoded attribute on a start element. */ public static final class Attribute { public final String namespace; public final String name; public final String value; Attribute(String namespace, String name, String value) { this.namespace = namespace; this.name = name; this.value = value; } } /** Callback for the streaming walk. */ public interface Handler { void startElement(String name, List attrs); void endElement(String name); } private final ByteBuffer buf; private String[] stringPool; private int[] resourceMap = new int[0]; public AxmlParser(byte[] data) { this.buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); } public void parse(Handler handler) { int fileType = buf.getShort(0) & 0xFFFF; if (fileType != RES_XML_TYPE) { throw new IllegalArgumentException( "Not a binary XML file (chunk type 0x" + Integer.toHexString(fileType) + ")"); } // Skip the file header (headerSize at offset 2). int headerSize = buf.getShort(2) & 0xFFFF; int pos = headerSize; while (pos + 8 <= buf.limit()) { buf.position(pos); int type = buf.getShort() & 0xFFFF; int chunkHeaderSize = buf.getShort() & 0xFFFF; long chunkSize = buf.getInt() & 0xFFFFFFFFL; if (chunkSize < 8) { break; } switch (type) { case RES_STRING_POOL_TYPE: parseStringPool(pos); break; case RES_XML_RESOURCE_MAP_TYPE: parseResourceMap(pos, (int) chunkSize, chunkHeaderSize); break; case RES_XML_START_ELEMENT_TYPE: parseStartElement(pos, chunkHeaderSize, handler); break; case RES_XML_END_ELEMENT_TYPE: parseEndElement(pos, chunkHeaderSize, handler); break; case RES_XML_START_NAMESPACE_TYPE: case RES_XML_END_NAMESPACE_TYPE: case RES_XML_CDATA_TYPE: default: break; // ignored } pos += (int) chunkSize; } } private void parseResourceMap(int chunkStart, int chunkSize, int headerSize) { int count = (chunkSize - headerSize) / 4; resourceMap = new int[count]; buf.position(chunkStart + headerSize); for (int i = 0; i < count; i++) { resourceMap[i] = buf.getInt(); } } private void parseStringPool(int chunkStart) { buf.position(chunkStart + 8); // skip ResChunk_header int stringCount = buf.getInt(); buf.getInt(); // styleCount int flags = buf.getInt(); int stringsStart = buf.getInt(); buf.getInt(); // stylesStart boolean utf8 = (flags & UTF8_FLAG) != 0; int[] offsets = new int[stringCount]; for (int i = 0; i < stringCount; i++) { offsets[i] = buf.getInt(); } stringPool = new String[stringCount]; int dataBase = chunkStart + stringsStart; for (int i = 0; i < stringCount; i++) { stringPool[i] = readPoolString(dataBase + offsets[i], utf8); } } private String readPoolString(int offset, boolean utf8) { buf.position(offset); if (utf8) { skipUtf8Len(); // char count (unused) int byteLen = readUtf8Len(); byte[] bytes = new byte[byteLen]; buf.get(bytes); try { return new String(bytes, "UTF-8"); } catch (Exception e) { return new String(bytes); } } else { int charLen = readUtf16Len(); char[] chars = new char[charLen]; for (int i = 0; i < charLen; i++) { chars[i] = buf.getChar(); } return new String(chars); } } private void skipUtf8Len() { int len = buf.get() & 0xFF; if ((len & 0x80) != 0) { buf.get(); } } private int readUtf8Len() { int len = buf.get() & 0xFF; if ((len & 0x80) != 0) { len = ((len & 0x7F) << 8) | (buf.get() & 0xFF); } return len; } private int readUtf16Len() { int len = buf.getChar(); if ((len & 0x8000) != 0) { len = ((len & 0x7FFF) << 16) | buf.getChar(); } return len; } private void parseStartElement(int chunkStart, int headerSize, Handler handler) { // After ResChunk_header(8) + lineNumber(4) + comment(4) == headerSize (16). buf.position(chunkStart + headerSize); buf.getInt(); // ns int nameRef = buf.getInt(); buf.getShort(); // attributeStart (== 20) buf.getShort(); // attributeSize (== 20) int attributeCount = buf.getShort() & 0xFFFF; buf.getShort(); // idIndex buf.getShort(); // classIndex buf.getShort(); // styleIndex List attrs = new ArrayList<>(attributeCount); for (int i = 0; i < attributeCount; i++) { int nsRef = buf.getInt(); int attrNameRef = buf.getInt(); int rawValueRef = buf.getInt(); buf.getShort(); // value size buf.get(); // res0 int dataType = buf.get() & 0xFF; int data = buf.getInt(); String ns = poolStr(nsRef); String name = resolveAttrName(attrNameRef); String value = decodeValue(dataType, data, rawValueRef); attrs.add(new Attribute(ns, name, value)); } handler.startElement(poolStr(nameRef), attrs); } private void parseEndElement(int chunkStart, int headerSize, Handler handler) { buf.position(chunkStart + headerSize); buf.getInt(); // ns int nameRef = buf.getInt(); handler.endElement(poolStr(nameRef)); } private String resolveAttrName(int nameRef) { String name = poolStr(nameRef); if (name != null && !name.isEmpty()) { return name; } // Empty name string: fall back to the resource-map id (common for android:* attrs). if (nameRef >= 0 && nameRef < resourceMap.length) { String known = AndroidAttrs.name(resourceMap[nameRef]); if (known != null) { return known; } return "0x" + Integer.toHexString(resourceMap[nameRef]); } return name == null ? "" : name; } private String decodeValue(int dataType, int data, int rawValueRef) { switch (dataType) { case TYPE_STRING: String s = poolStr(data); return s != null ? s : poolStr(rawValueRef); case TYPE_INT_BOOLEAN: return data != 0 ? "true" : "false"; case TYPE_REFERENCE: return "@0x" + Integer.toHexString(data); case TYPE_INT_HEX: return "0x" + Integer.toHexString(data); case TYPE_INT_DEC: return Integer.toString(data); default: if (rawValueRef != -1) { String raw = poolStr(rawValueRef); if (raw != null) { return raw; } } return Integer.toString(data); } } private String poolStr(int ref) { if (stringPool == null || ref < 0 || ref >= stringPool.length) { return null; } return stringPool[ref]; } }