package com.wj.dexknife.shell.apkparser.parser; import com.wj.dexknife.shell.apkparser.bean.AttributeValues; import com.wj.dexknife.shell.apkparser.bean.Locales; import com.wj.dexknife.shell.apkparser.exception.ParserException; import com.wj.dexknife.shell.apkparser.struct.ChunkHeader; import com.wj.dexknife.shell.apkparser.struct.ChunkType; import com.wj.dexknife.shell.apkparser.struct.ResourceEntity; import com.wj.dexknife.shell.apkparser.struct.StringPool; import com.wj.dexknife.shell.apkparser.struct.StringPoolHeader; import com.wj.dexknife.shell.apkparser.struct.resource.ResourceTable; import com.wj.dexknife.shell.apkparser.struct.xml.Attribute; import com.wj.dexknife.shell.apkparser.struct.xml.Attributes; import com.wj.dexknife.shell.apkparser.struct.xml.XmlCData; import com.wj.dexknife.shell.apkparser.struct.xml.XmlHeader; import com.wj.dexknife.shell.apkparser.struct.xml.XmlNamespaceEndTag; import com.wj.dexknife.shell.apkparser.struct.xml.XmlNamespaceStartTag; import com.wj.dexknife.shell.apkparser.struct.xml.XmlNodeEndTag; import com.wj.dexknife.shell.apkparser.struct.xml.XmlNodeHeader; import com.wj.dexknife.shell.apkparser.struct.xml.XmlNodeStartTag; import com.wj.dexknife.shell.apkparser.struct.xml.XmlResourceMapHeader; import com.wj.dexknife.shell.apkparser.utils.Buffers; import com.wj.dexknife.shell.apkparser.utils.ParseUtils; import com.wj.dexknife.shell.apkparser.utils.Utils; 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 chunkHeader = readChunkHeader(); if (chunkHeader == null) { return; } if (chunkHeader.getChunkType() != ChunkType.XML) { //TODO: may be a plain xml file. return; } XmlHeader xmlHeader = (XmlHeader) chunkHeader; // read string pool chunk chunkHeader = readChunkHeader(); if (chunkHeader == null) { return; } ParseUtils.checkChunkType(ChunkType.STRING_POOL, chunkHeader.getChunkType()); stringPool = ParseUtils.readStringPool(buffer, (StringPoolHeader) chunkHeader); // read on chunk, check if it was an optional XMLResourceMap chunk 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()); } } buffer.position((int) (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()) && Utils.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<String> 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)); } ResourceEntity 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(chunkType, 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)); buffer.position((int) (begin + headerSize)); return stringPoolHeader; case ChunkType.XML_RESOURCE_MAP: buffer.position((int) (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)); buffer.position((int) (begin + headerSize)); return header; case ChunkType.NULL: //buffer.advanceTo(begin + headerSize); //buffer.skip((int) (chunkSize - headerSize)); 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; } }