package jadx.core.xmlgen;
import jadx.api.ResourcesLoader;
import jadx.core.codegen.CodeWriter;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.entry.ValuesParser;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/* TODO:
Don't die when error occurs
Check error cases, maybe checked const values are not always the same
Better error messages
What to do, when Binary XML Manifest is > size(int)?
Check for missing chunk size types
Implement missing data types
Use line numbers to recreate EXACT AndroidManifest
Check Element chunk size
*/
public class BinaryXMLParser extends CommonBinaryParser {
private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class);
private static final String ANDROID_R_STYLE_CLS = "android.R$style";
private static final boolean ATTR_NEW_LINE = false;
private CodeWriter writer;
private String[] strings;
private String nsPrefix = "ERROR";
private String nsURI = "ERROR";
private String currentTag = "ERROR";
private boolean firstElement;
private boolean wasOneLiner = false;
private final Map<Integer, String> styleMap = new HashMap<Integer, String>();
private final Map<Integer, FieldNode> localStyleMap = new HashMap<Integer, FieldNode>();
private final Map<Integer, String> resNames;
private ValuesParser valuesParser;
private final ManifestAttributes attributes;
public BinaryXMLParser(RootNode root) {
try {
try {
Class<?> rStyleCls = Class.forName(ANDROID_R_STYLE_CLS);
for (Field f : rStyleCls.getFields()) {
styleMap.put(f.getInt(f.getType()), f.getName());
}
} catch (Throwable th) {
LOG.error("R class loading failed", th);
}
// add application constants
ConstStorage constStorage = root.getConstValues();
Map<Object, FieldNode> constFields = constStorage.getGlobalConstFields();
for (Map.Entry<Object, FieldNode> entry : constFields.entrySet()) {
Object key = entry.getKey();
FieldNode field = entry.getValue();
if (field.getType().equals(ArgType.INT) && key instanceof Integer) {
localStyleMap.put((Integer) key, field);
}
}
resNames = constStorage.getResourcesNames();
attributes = new ManifestAttributes();
attributes.parseAll();
} catch (Exception e) {
throw new JadxRuntimeException("BinaryXMLParser init error", e);
}
}
public synchronized CodeWriter parse(InputStream inputStream) throws IOException {
is = new ParserStream(inputStream);
if (!isBinaryXml()) {
return ResourcesLoader.loadToCodeWriter(inputStream);
}
writer = new CodeWriter();
writer.add("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
firstElement = true;
decode();
writer.finish();
return writer;
}
private boolean isBinaryXml() throws IOException {
is.mark(4);
int v = is.readInt16(); // version
int h = is.readInt16(); // header size
if (v == 0x0003 && h == 0x0008) {
return true;
}
is.reset();
return false;
}
void decode() throws IOException {
int size = is.readInt32();
while (is.getPos() < size) {
int type = is.readInt16();
switch (type) {
case RES_NULL_TYPE:
// NullType is just doing nothing
break;
case RES_STRING_POOL_TYPE:
strings = parseStringPoolNoType();
valuesParser = new ValuesParser(strings, resNames);
break;
case RES_XML_RESOURCE_MAP_TYPE:
parseResourceMap();
break;
case RES_XML_START_NAMESPACE_TYPE:
parseNameSpace();
break;
case RES_XML_CDATA_TYPE:
parseCData();
break;
case RES_XML_END_NAMESPACE_TYPE:
parseNameSpaceEnd();
break;
case RES_XML_START_ELEMENT_TYPE:
parseElement();
break;
case RES_XML_END_ELEMENT_TYPE:
parseElementEnd();
break;
default:
die("Type: 0x" + Integer.toHexString(type) + " not yet implemented");
break;
}
}
}
private void parseResourceMap() throws IOException {
if (is.readInt16() != 0x8) {
die("Header size of resmap is not 8!");
}
int rhsize = is.readInt32();
int[] ids = new int[(rhsize - 8) / 4];
for (int i = 0; i < ids.length; i++) {
ids[i] = is.readInt32();
}
}
private void parseNameSpace() throws IOException {
if (is.readInt16() != 0x10) {
die("NAMESPACE header is not 0x0010");
}
if (is.readInt32() != 0x18) {
die("NAMESPACE header chunk is not 0x18 big");
}
int beginLineNumber = is.readInt32();
int comment = is.readInt32();
int beginPrefix = is.readInt32();
nsPrefix = strings[beginPrefix];
int beginURI = is.readInt32();
nsURI = strings[beginURI];
}
private void parseNameSpaceEnd() throws IOException {
if (is.readInt16() != 0x10) {
die("NAMESPACE header is not 0x0010");
}
if (is.readInt32() != 0x18) {
die("NAMESPACE header chunk is not 0x18 big");
}
int endLineNumber = is.readInt32();
int comment = is.readInt32();
int endPrefix = is.readInt32();
nsPrefix = strings[endPrefix];
int endURI = is.readInt32();
nsURI = strings[endURI];
}
private void parseCData() throws IOException {
if (is.readInt16() != 0x10) {
die("CDATA header is not 0x10");
}
if (is.readInt32() != 0x1C) {
die("CDATA header chunk is not 0x1C");
}
int lineNumber = is.readInt32();
is.skip(4);
int strIndex = is.readInt32();
String str = strings[strIndex];
writer.startLine().addIndent();
writer.attachSourceLine(lineNumber);
writer.add(StringUtils.escapeXML(str.trim())); // TODO: wrap into CDATA for easier reading
int size = is.readInt16();
is.skip(size - 2);
}
private void parseElement() throws IOException {
if (firstElement) {
firstElement = false;
} else {
writer.incIndent();
}
if (is.readInt16() != 0x10) {
die("ELEMENT HEADER SIZE is not 0x10");
}
// TODO: Check element chunk size
is.readInt32();
int elementBegLineNumber = is.readInt32();
int comment = is.readInt32();
int startNS = is.readInt32();
int startNSName = is.readInt32(); // actually is elementName...
if (!wasOneLiner && !"ERROR".equals(currentTag)
&& !currentTag.equals(strings[startNSName])) {
writer.add(">");
}
wasOneLiner = false;
currentTag = strings[startNSName];
writer.startLine("<").add(currentTag);
writer.attachSourceLine(elementBegLineNumber);
int attributeStart = is.readInt16();
if (attributeStart != 0x14) {
die("startNS's attributeStart is not 0x14");
}
int attributeSize = is.readInt16();
if (attributeSize != 0x14) {
die("startNS's attributeSize is not 0x14");
}
int attributeCount = is.readInt16();
int idIndex = is.readInt16();
int classIndex = is.readInt16();
int styleIndex = is.readInt16();
if ("manifest".equals(currentTag) || writer.getIndent() == 0) {
writer.add(" xmlns:android=\"").add(nsURI).add("\"");
}
boolean attrNewLine = attributeCount == 1 ? false : ATTR_NEW_LINE;
for (int i = 0; i < attributeCount; i++) {
parseAttribute(i, attrNewLine);
}
}
private void parseAttribute(int i, boolean newLine) throws IOException {
int attributeNS = is.readInt32();
int attributeName = is.readInt32();
int attributeRawValue = is.readInt32();
int attrValSize = is.readInt16();
if (attrValSize != 0x08) {
die("attrValSize != 0x08 not supported");
}
if (is.readInt8() != 0) {
die("res0 is not 0");
}
int attrValDataType = is.readInt8();
int attrValData = is.readInt32();
String attrName = strings[attributeName];
if (newLine) {
writer.startLine().addIndent();
} else {
writer.add(' ');
}
if (attributeNS != -1) {
writer.add(nsPrefix).add(':');
}
writer.add(attrName).add("=\"");
String decodedAttr = attributes.decode(attrName, attrValData);
if (decodedAttr != null) {
writer.add(decodedAttr);
} else {
decodeAttribute(attributeNS, attrValDataType, attrValData);
}
writer.add('"');
}
private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData) {
if (attrValDataType == TYPE_REFERENCE) {
// reference custom processing
String name = styleMap.get(attrValData);
if (name != null) {
writer.add("@*");
if (attributeNS != -1) {
writer.add(nsPrefix).add(':');
}
writer.add("style/").add(name.replaceAll("_", "."));
} else {
FieldNode field = localStyleMap.get(attrValData);
if (field != null) {
String cls = field.getParentClass().getShortName().toLowerCase();
writer.add("@");
if ("id".equals(cls)) {
writer.add('+');
}
writer.add(cls).add("/").add(field.getName());
} else {
String resName = resNames.get(attrValData);
if (resName != null) {
writer.add("@").add(resName);
} else {
writer.add("0x").add(Integer.toHexString(attrValData));
}
}
}
} else {
String str = valuesParser.decodeValue(attrValDataType, attrValData);
writer.add(str != null ? str : "null");
}
}
private void parseElementEnd() throws IOException {
if (is.readInt16() != 0x10) {
die("ELEMENT END header is not 0x10");
}
if (is.readInt32() != 0x18) {
die("ELEMENT END header chunk is not 0x18 big");
}
int endLineNumber = is.readInt32();
int comment = is.readInt32();
int elementNS = is.readInt32();
int elementName = is.readInt32();
if (currentTag.equals(strings[elementName])) {
writer.add(" />");
wasOneLiner = true;
} else {
writer.startLine("</");
writer.attachSourceLine(endLineNumber);
if (elementNS != -1) {
writer.add(strings[elementNS]).add(':');
}
writer.add(strings[elementName]).add(">");
}
if (writer.getIndent() != 0) {
writer.decIndent();
}
}
}