Apktool/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java

805 lines
26 KiB
Java

/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package brut.androlib.res.decoder;
import android.content.res.XmlResourceParser;
import android.util.TypedValue;
import brut.androlib.exceptions.AndrolibException;
import brut.androlib.res.data.ResID;
import brut.androlib.res.data.arsc.ARSCHeader;
import brut.androlib.res.data.axml.NamespaceStack;
import brut.androlib.res.xml.ResXmlEncoders;
import brut.util.ExtDataInput;
import com.google.common.io.LittleEndianDataInputStream;
import org.xmlpull.v1.XmlPullParserException;
import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Binary xml files parser.
*
* <p>Parser has only two states: (1) Operational state, which parser
* obtains after first successful call to next() and retains until
* open(), close(), or failed call to next(). (2) Closed state, which
* parser obtains after open(), close(), or failed call to next(). In
* this state methods return invalid values or throw exceptions.
*/
public class AXmlResourceParser implements XmlResourceParser {
public AXmlResourceParser() {
resetEventInfo();
}
public AndrolibException getFirstError() {
return mFirstError;
}
public ResAttrDecoder getAttrDecoder() {
return mAttrDecoder;
}
public void setAttrDecoder(ResAttrDecoder attrDecoder) {
mAttrDecoder = attrDecoder;
}
public void open(InputStream stream) {
close();
if (stream != null) {
// We need to explicitly cast to DataInput as otherwise the constructor is ambiguous.
// We choose DataInput instead of InputStream as ExtDataInput wraps an InputStream in
// a DataInputStream which is big-endian and ignores the little-endian behavior.
mIn = new ExtDataInput((DataInput) new LittleEndianDataInputStream(stream));
}
}
@Override
public void close() {
if (!isOperational) {
return;
}
isOperational = false;
mIn = null;
mStringBlock = null;
mResourceIds = null;
mNamespaces.reset();
resetEventInfo();
}
@Override
public int next() throws XmlPullParserException, IOException {
if (mIn == null) {
throw new XmlPullParserException("Parser is not opened.", this, null);
}
try {
doNext();
return mEvent;
} catch (IOException e) {
close();
throw e;
}
}
@Override
public int nextToken() throws XmlPullParserException, IOException {
return next();
}
@Override
public int nextTag() throws XmlPullParserException, IOException {
int eventType = next();
if (eventType == TEXT && isWhitespace()) {
eventType = next();
}
if (eventType != START_TAG && eventType != END_TAG) {
throw new XmlPullParserException("Expected start or end tag.", this, null);
}
return eventType;
}
@Override
public String nextText() throws XmlPullParserException, IOException {
if (getEventType() != START_TAG) {
throw new XmlPullParserException("Parser must be on START_TAG to read next text.", this, null);
}
int eventType = next();
if (eventType == TEXT) {
String result = getText();
eventType = next();
if (eventType != END_TAG) {
throw new XmlPullParserException("Event TEXT must be immediately followed by END_TAG.", this, null);
}
return result;
} else if (eventType == END_TAG) {
return "";
} else {
throw new XmlPullParserException("Parser must be on START_TAG or TEXT to read text.", this, null);
}
}
@Override
public void require(int type, String namespace, String name) throws XmlPullParserException {
if (type != getEventType() || (namespace != null && !namespace.equals(getNamespace()))
|| (name != null && !name.equals(getName()))) {
throw new XmlPullParserException(TYPES[type] + " is expected.", this, null);
}
}
@Override
public int getDepth() {
return mNamespaces.getDepth() - 1;
}
@Override
public int getEventType(){
return mEvent;
}
@Override
public int getLineNumber() {
return mLineNumber;
}
@Override
public String getName() {
if (mNameIndex == -1 || (mEvent != START_TAG && mEvent != END_TAG)) {
return null;
}
return mStringBlock.getString(mNameIndex);
}
@Override
public String getText() {
if (mNameIndex == -1 || mEvent != TEXT) {
return null;
}
return mStringBlock.getString(mNameIndex);
}
@Override
public char[] getTextCharacters(int[] holderForStartAndLength) {
String text = getText();
if (text == null) {
return null;
}
holderForStartAndLength[0] = 0;
holderForStartAndLength[1] = text.length();
char[] chars = new char[text.length()];
text.getChars(0, text.length(), chars, 0);
return chars;
}
@Override
public String getNamespace() {
return mStringBlock.getString(mNamespaceIndex);
}
@Override
public String getPrefix() {
int prefix = mNamespaces.findPrefix(mNamespaceIndex);
return mStringBlock.getString(prefix);
}
@Override
public String getPositionDescription() {
return "XML line #" + getLineNumber();
}
@Override
public int getNamespaceCount(int depth) {
return mNamespaces.getAccumulatedCount(depth);
}
@Override
public String getNamespacePrefix(int pos) {
int prefix = mNamespaces.getPrefix(pos);
return mStringBlock.getString(prefix);
}
@Override
public String getNamespaceUri(int pos) {
int uri = mNamespaces.getUri(pos);
return mStringBlock.getString(uri);
}
@Override
public String getClassAttribute() {
if (mClassIndex == -1) {
return null;
}
int offset = getAttributeOffset(mClassIndex);
int value = mAttributes[offset + ATTRIBUTE_IX_VALUE_STRING];
return mStringBlock.getString(value);
}
@Override
public String getIdAttribute() {
if (mIdIndex == -1) {
return null;
}
int offset = getAttributeOffset(mIdIndex);
int value = mAttributes[offset + ATTRIBUTE_IX_VALUE_STRING];
return mStringBlock.getString(value);
}
@Override
public int getIdAttributeResourceValue(int defaultValue) {
if (mIdIndex == -1) {
return defaultValue;
}
int offset = getAttributeOffset(mIdIndex);
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
if (valueType != TypedValue.TYPE_REFERENCE) {
return defaultValue;
}
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
@Override
public int getStyleAttribute() {
if (mStyleIndex == -1) {
return 0;
}
int offset = getAttributeOffset(mStyleIndex);
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
@Override
public int getAttributeCount() {
if (mEvent != START_TAG) {
return -1;
}
return mAttributes.length / ATTRIBUTE_LENGTH;
}
@Override
public String getAttributeNamespace(int index) {
int offset = getAttributeOffset(index);
int namespace = mAttributes[offset + ATTRIBUTE_IX_NAMESPACE_URI];
if (namespace == -1) {
return "";
}
// Minifiers like removing the namespace, so we will default to default namespace
// unless the pkgId of the resource is private. We will grab the non-standard one.
String value = mStringBlock.getString(namespace);
if (value == null || value.length() == 0) {
ResID resId = new ResID(getAttributeNameResource(index));
if (resId.pkgId == PRIVATE_PKG_ID) {
value = getNonDefaultNamespaceUri(offset);
} else {
value = "http://schemas.android.com/apk/res/android";
}
}
return value;
}
private String getNonDefaultNamespaceUri(int offset) {
String prefix = mStringBlock.getString(mNamespaces.getPrefix(offset));
if (prefix != null) {
return mStringBlock.getString(mNamespaces.getUri(offset));
}
// If we are here. There is some clever obfuscation going on. Our reference points to the namespace are gone.
// Normally we could take the index * attributeCount to get an offset.
// That would point to the URI in the StringBlock table, but that is empty.
// We have the namespaces that can't be touched in the opening tag.
// Though no known way to correlate them at this time.
// So return the res-auto namespace.
return "http://schemas.android.com/apk/res-auto";
}
@Override
public String getAttributePrefix(int index) {
int offset = getAttributeOffset(index);
int uri = mAttributes[offset + ATTRIBUTE_IX_NAMESPACE_URI];
int prefix = mNamespaces.findPrefix(uri);
if (prefix == -1) {
return "";
}
return mStringBlock.getString(prefix);
}
@Override
public String getAttributeName(int index) {
int offset = getAttributeOffset(index);
int name = mAttributes[offset + ATTRIBUTE_IX_NAME];
if (name == -1) {
return "";
}
String resourceMapValue;
String stringBlockValue = mStringBlock.getString(name);
int resourceId = getAttributeNameResource(index);
try {
resourceMapValue = mAttrDecoder.decodeFromResourceId(resourceId);
} catch (AndrolibException ignored) {
resourceMapValue = null;
}
// Android prefers the resource map value over what the String block has.
// This can be seen quite often in obfuscated apps where values such as:
// <item android:state_enabled="true" app:state_collapsed="false" app:state_collapsible="true">
// Are improperly decoded when trusting the String block.
// Leveraging the resource map allows us to get the proper value.
// <item android:state_enabled="true" app:d2="false" app:d3="true">
if (resourceMapValue != null) {
return resourceMapValue;
}
if (stringBlockValue != null) {
return stringBlockValue;
}
// In this case we have a bogus resource. If it was not found in either.
return "APKTOOL_MISSING_" + Integer.toHexString(resourceId);
}
@Override
public int getAttributeNameResource(int index) {
int offset = getAttributeOffset(index);
int name = mAttributes[offset + ATTRIBUTE_IX_NAME];
if (mResourceIds == null || name < 0 || name >= mResourceIds.length) {
return 0;
}
return mResourceIds[name];
}
@Override
public int getAttributeValueType(int index) {
int offset = getAttributeOffset(index);
return mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
}
@Override
public int getAttributeValueData(int index) {
int offset = getAttributeOffset(index);
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
@Override
public String getAttributeValue(int index) {
int offset = getAttributeOffset(index);
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
int valueData = mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
int valueRaw = mAttributes[offset + ATTRIBUTE_IX_VALUE_STRING];
if (mAttrDecoder != null) {
try {
String stringBlockValue = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(mStringBlock.getString(valueRaw));
String resourceMapValue = mAttrDecoder.decodeFromResourceId(valueData);
String value = stringBlockValue;
if (stringBlockValue != null && resourceMapValue != null) {
int slashPos = stringBlockValue.lastIndexOf("/");
int colonPos = stringBlockValue.lastIndexOf(":");
// Handle a value with a format of "@yyy/xxx", but avoid "@yyy/zzz:xxx"
if (slashPos != -1) {
if (colonPos == -1) {
String type = stringBlockValue.substring(0, slashPos);
value = type + "/" + resourceMapValue;
}
} else if (! stringBlockValue.equals(resourceMapValue)) {
value = resourceMapValue;
}
}
return mAttrDecoder.decode(
valueType,
valueData,
value,
getAttributeNameResource(index)
);
} catch (AndrolibException ex) {
setFirstError(ex);
LOGGER.log(Level.WARNING, String.format("Could not decode attr value, using undecoded value "
+ "instead: ns=%s, name=%s, value=0x%08x",
getAttributePrefix(index),
getAttributeName(index),
valueData), ex);
}
}
return TypedValue.coerceToString(valueType, valueData);
}
@Override
public boolean getAttributeBooleanValue(int index, boolean defaultValue) {
return getAttributeIntValue(index, defaultValue ? 1 : 0) != 0;
}
@Override
public float getAttributeFloatValue(int index, float defaultValue) {
int offset = getAttributeOffset(index);
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
if (valueType == TypedValue.TYPE_FLOAT) {
int valueData = mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
return Float.intBitsToFloat(valueData);
}
return defaultValue;
}
@Override
public int getAttributeIntValue(int index, int defaultValue) {
int offset = getAttributeOffset(index);
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
if (valueType >= TypedValue.TYPE_FIRST_INT && valueType <= TypedValue.TYPE_LAST_INT) {
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
return defaultValue;
}
@Override
public int getAttributeUnsignedIntValue(int index, int defaultValue) {
return getAttributeIntValue(index, defaultValue);
}
@Override
public int getAttributeResourceValue(int index, int defaultValue) {
int offset = getAttributeOffset(index);
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
if (valueType == TypedValue.TYPE_REFERENCE) {
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
return defaultValue;
}
@Override
public String getAttributeValue(String namespace, String attribute) {
int index = findAttribute(namespace, attribute);
if (index == -1) {
return "";
}
return getAttributeValue(index);
}
@Override
public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue) {
int index = findAttribute(namespace, attribute);
if (index == -1) {
return defaultValue;
}
return getAttributeBooleanValue(index, defaultValue);
}
@Override
public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) {
int index = findAttribute(namespace, attribute);
if (index == -1) {
return defaultValue;
}
return getAttributeFloatValue(index, defaultValue);
}
@Override
public int getAttributeIntValue(String namespace, String attribute, int defaultValue) {
int index = findAttribute(namespace, attribute);
if (index == -1) {
return defaultValue;
}
return getAttributeIntValue(index, defaultValue);
}
@Override
public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) {
int index = findAttribute(namespace, attribute);
if (index == -1) {
return defaultValue;
}
return getAttributeUnsignedIntValue(index, defaultValue);
}
@Override
public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) {
int index = findAttribute(namespace, attribute);
if (index == -1) {
return defaultValue;
}
return getAttributeResourceValue(index, defaultValue);
}
@Override
public int getAttributeListValue(int index, String[] options, int defaultValue) {
return 0;
}
@Override
public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue) {
return 0;
}
@Override
public String getAttributeType(int index) {
return "CDATA";
}
@Override
public boolean isAttributeDefault(int index) {
return false;
}
@Override
public void setInput(InputStream stream, String inputEncoding) {
open(stream);
}
@Override
public void setInput(Reader reader) throws XmlPullParserException {
throw new XmlPullParserException(E_NOT_SUPPORTED);
}
@Override
public String getInputEncoding() {
return null;
}
@Override
public int getColumnNumber() {
return -1;
}
@Override
public boolean isEmptyElementTag() {
return false;
}
@Override
public boolean isWhitespace() {
return false;
}
@Override
public void defineEntityReplacementText(String entityName, String replacementText)
throws XmlPullParserException {
throw new XmlPullParserException(E_NOT_SUPPORTED);
}
@Override
public String getNamespace(String prefix) {
throw new RuntimeException(E_NOT_SUPPORTED);
}
@Override
public Object getProperty(String name) {
return null;
}
@Override
public void setProperty(String name, Object value) throws XmlPullParserException {
throw new XmlPullParserException(E_NOT_SUPPORTED);
}
@Override
public boolean getFeature(String feature) {
return false;
}
@Override
public void setFeature(String name, boolean value) throws XmlPullParserException {
throw new XmlPullParserException(E_NOT_SUPPORTED);
}
private int getAttributeOffset(int index) {
if (mEvent != START_TAG) {
throw new IndexOutOfBoundsException("Current event is not START_TAG.");
}
int offset = index * ATTRIBUTE_LENGTH;
if (offset >= mAttributes.length) {
throw new IndexOutOfBoundsException("Invalid attribute index (" + index + ").");
}
return offset;
}
private int findAttribute(String namespace, String attribute) {
if (mStringBlock == null || attribute == null) {
return -1;
}
int name = mStringBlock.find(attribute);
if (name == -1) {
return -1;
}
int uri = (namespace != null) ? mStringBlock.find(namespace) : -1;
for (int o = 0; o != mAttributes.length; o += ATTRIBUTE_LENGTH) {
if (name == mAttributes[o + ATTRIBUTE_IX_NAME]
&& (uri == -1 || uri == mAttributes[o + ATTRIBUTE_IX_NAMESPACE_URI])) {
return o / ATTRIBUTE_LENGTH;
}
}
return -1;
}
private void resetEventInfo() {
mEvent = -1;
mLineNumber = -1;
mNameIndex = -1;
mNamespaceIndex = -1;
mAttributes = null;
mIdIndex = -1;
mClassIndex = -1;
mStyleIndex = -1;
}
private void doNext() throws IOException {
if (mStringBlock == null) {
mIn.skipInt(); // XML Chunk AXML Type
mIn.skipInt(); // Chunk Size
mStringBlock = StringBlock.readWithChunk(mIn);
mNamespaces.increaseDepth();
isOperational = true;
}
if (mEvent == END_DOCUMENT) {
return;
}
int event = mEvent;
resetEventInfo();
while (true) {
if (m_decreaseDepth) {
m_decreaseDepth = false;
mNamespaces.decreaseDepth();
}
// Fake END_DOCUMENT event.
if (event == END_TAG && mNamespaces.getDepth() == 1 && mNamespaces.getCurrentCount() == 0) {
mEvent = END_DOCUMENT;
break;
}
int chunkType;
if (event == START_DOCUMENT) {
// Fake event, see CHUNK_XML_START_TAG handler.
chunkType = ARSCHeader.RES_XML_START_ELEMENT_TYPE;
} else {
chunkType = mIn.readShort();
mIn.skipShort(); // headerSize
}
if (chunkType == ARSCHeader.RES_XML_RESOURCE_MAP_TYPE) {
int chunkSize = mIn.readInt();
if (chunkSize < 8 || (chunkSize % 4) != 0) {
throw new IOException("Invalid resource ids size (" + chunkSize + ").");
}
mResourceIds = mIn.readIntArray(chunkSize / 4 - 2);
continue;
}
if (chunkType < ARSCHeader.RES_XML_FIRST_CHUNK_TYPE || chunkType > ARSCHeader.RES_XML_LAST_CHUNK_TYPE) {
throw new IOException("Invalid chunk type (" + chunkType + ").");
}
// Fake START_DOCUMENT event.
if (chunkType == ARSCHeader.RES_XML_START_ELEMENT_TYPE && event == -1) {
mEvent = START_DOCUMENT;
break;
}
// Read remainder of ResXMLTree_node
mIn.skipInt(); // chunkSize
mLineNumber = mIn.readInt();
mIn.skipInt(); // Optional XML Comment
if (chunkType == ARSCHeader.RES_XML_START_NAMESPACE_TYPE || chunkType == ARSCHeader.RES_XML_END_NAMESPACE_TYPE) {
if (chunkType == ARSCHeader.RES_XML_START_NAMESPACE_TYPE) {
int prefix = mIn.readInt();
int uri = mIn.readInt();
mNamespaces.push(prefix, uri);
} else {
mIn.skipInt(); // prefix
mIn.skipInt(); // uri
mNamespaces.pop();
}
continue;
}
if (chunkType == ARSCHeader.RES_XML_START_ELEMENT_TYPE) {
mNamespaceIndex = mIn.readInt();
mNameIndex = mIn.readInt();
mIn.skipShort(); // attributeStart
int attributeSize = mIn.readShort();
int attributeCount = mIn.readShort();
mIdIndex = mIn.readShort();
mClassIndex = mIn.readShort();
mStyleIndex = mIn.readShort();
mAttributes = mIn.readIntArray(attributeCount * ATTRIBUTE_LENGTH);
for (int i = ATTRIBUTE_IX_VALUE_TYPE; i < mAttributes.length; ) {
mAttributes[i] = (mAttributes[i] >>> 24);
i += ATTRIBUTE_LENGTH;
}
int byteAttrSizeRead = (attributeCount * ATTRIBUTE_LENGTH) * 4;
int byteAttrSizeReported = (attributeSize * attributeCount);
// Check for misleading chunk sizes
if (byteAttrSizeRead < byteAttrSizeReported) {
int bytesToSkip = byteAttrSizeReported - byteAttrSizeRead;
mIn.skipBytes(bytesToSkip);
LOGGER.fine("Skipping " + bytesToSkip + " unknown bytes in attributes area.");
}
mNamespaces.increaseDepth();
mEvent = START_TAG;
break;
}
if (chunkType == ARSCHeader.RES_XML_END_ELEMENT_TYPE) {
mNamespaceIndex = mIn.readInt();
mNameIndex = mIn.readInt();
mEvent = END_TAG;
m_decreaseDepth = true;
break;
}
if (chunkType == ARSCHeader.RES_XML_CDATA_TYPE) {
mNameIndex = mIn.readInt();
mIn.skipInt();
mIn.skipInt();
mEvent = TEXT;
break;
}
}
}
private void setFirstError(AndrolibException error) {
if (mFirstError == null) {
mFirstError = error;
}
}
private ExtDataInput mIn;
private ResAttrDecoder mAttrDecoder;
private AndrolibException mFirstError;
private boolean isOperational = false;
private StringBlock mStringBlock;
private int[] mResourceIds;
private final NamespaceStack mNamespaces = new NamespaceStack();
private boolean m_decreaseDepth;
// All values are essentially indices, e.g. mNameIndex is an index of name in mStringBlock.
private int mEvent;
private int mLineNumber;
private int mNameIndex;
private int mNamespaceIndex;
private int[] mAttributes;
private int mIdIndex;
private int mClassIndex;
private int mStyleIndex;
private final static Logger LOGGER = Logger.getLogger(AXmlResourceParser.class.getName());
private static final String E_NOT_SUPPORTED = "Method is not supported.";
// ResXMLTree_attribute
private static final int ATTRIBUTE_IX_NAMESPACE_URI = 0; // ns
private static final int ATTRIBUTE_IX_NAME = 1; // name
private static final int ATTRIBUTE_IX_VALUE_STRING = 2; // rawValue
private static final int ATTRIBUTE_IX_VALUE_TYPE = 3; // (size/res0/dataType)
private static final int ATTRIBUTE_IX_VALUE_DATA = 4; // data
private static final int ATTRIBUTE_LENGTH = 5;
private static final int PRIVATE_PKG_ID = 0x7F;
}