| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * 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 |
| * |
| * http://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 com.android.apksig.internal.apk; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}. |
| * |
| * <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via |
| * {@link #getEventType()} and {@link #next()} methods. Additional information about the current |
| * event can be obtained via an assortment of getters, for example, {@link #getName()} or |
| * {@link #getAttributeNameResourceId(int)}. |
| */ |
| public class AndroidBinXmlParser { |
| |
| /** Event: start of document. */ |
| public static final int EVENT_START_DOCUMENT = 1; |
| |
| /** Event: end of document. */ |
| public static final int EVENT_END_DOCUMENT = 2; |
| |
| /** Event: start of an element. */ |
| public static final int EVENT_START_ELEMENT = 3; |
| |
| /** Event: end of an document. */ |
| public static final int EVENT_END_ELEMENT = 4; |
| |
| /** Attribute value type is not supported by this parser. */ |
| public static final int VALUE_TYPE_UNSUPPORTED = 0; |
| |
| /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */ |
| public static final int VALUE_TYPE_STRING = 1; |
| |
| /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */ |
| public static final int VALUE_TYPE_INT = 2; |
| |
| /** |
| * Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it. |
| */ |
| public static final int VALUE_TYPE_REFERENCE = 3; |
| |
| /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */ |
| public static final int VALUE_TYPE_BOOLEAN = 4; |
| |
| private static final long NO_NAMESPACE = 0xffffffffL; |
| |
| private final ByteBuffer mXml; |
| |
| private StringPool mStringPool; |
| private ResourceMap mResourceMap; |
| private int mDepth; |
| private int mCurrentEvent = EVENT_START_DOCUMENT; |
| |
| private String mCurrentElementName; |
| private String mCurrentElementNamespace; |
| private int mCurrentElementAttributeCount; |
| private List<Attribute> mCurrentElementAttributes; |
| private ByteBuffer mCurrentElementAttributesContents; |
| private int mCurrentElementAttrSizeBytes; |
| |
| /** |
| * Constructs a new parser for the provided document. |
| */ |
| public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException { |
| xml.order(ByteOrder.LITTLE_ENDIAN); |
| |
| Chunk resXmlChunk = null; |
| while (xml.hasRemaining()) { |
| Chunk chunk = Chunk.get(xml); |
| if (chunk == null) { |
| break; |
| } |
| if (chunk.getType() == Chunk.TYPE_RES_XML) { |
| resXmlChunk = chunk; |
| break; |
| } |
| } |
| |
| if (resXmlChunk == null) { |
| throw new XmlParserException("No XML chunk in file"); |
| } |
| mXml = resXmlChunk.getContents(); |
| } |
| |
| /** |
| * Returns the depth of the current element. Outside of the root of the document the depth is |
| * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and |
| * is decremented by {@code 1} after each {@code end element} event. |
| */ |
| public int getDepth() { |
| return mDepth; |
| } |
| |
| /** |
| * Returns the type of the current event. See {@code EVENT_...} constants. |
| */ |
| public int getEventType() { |
| return mCurrentEvent; |
| } |
| |
| /** |
| * Returns the local name of the current element or {@code null} if the current event does not |
| * pertain to an element. |
| */ |
| public String getName() { |
| if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) { |
| return null; |
| } |
| return mCurrentElementName; |
| } |
| |
| /** |
| * Returns the namespace of the current element or {@code null} if the current event does not |
| * pertain to an element. Returns an empty string if the element is not associated with a |
| * namespace. |
| */ |
| public String getNamespace() { |
| if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) { |
| return null; |
| } |
| return mCurrentElementNamespace; |
| } |
| |
| /** |
| * Returns the number of attributes of the element associated with the current event or |
| * {@code -1} if no element is associated with the current event. |
| */ |
| public int getAttributeCount() { |
| if (mCurrentEvent != EVENT_START_ELEMENT) { |
| return -1; |
| } |
| |
| return mCurrentElementAttributeCount; |
| } |
| |
| /** |
| * Returns the resource ID corresponding to the name of the specified attribute of the current |
| * element or {@code 0} if the name is not associated with a resource ID. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public int getAttributeNameResourceId(int index) throws XmlParserException { |
| return getAttribute(index).getNameResourceId(); |
| } |
| |
| /** |
| * Returns the name of the specified attribute of the current element. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public String getAttributeName(int index) throws XmlParserException { |
| return getAttribute(index).getName(); |
| } |
| |
| /** |
| * Returns the name of the specified attribute of the current element or an empty string if |
| * the attribute is not associated with a namespace. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public String getAttributeNamespace(int index) throws XmlParserException { |
| return getAttribute(index).getNamespace(); |
| } |
| |
| /** |
| * Returns the value type of the specified attribute of the current element. See |
| * {@code VALUE_TYPE_...} constants. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public int getAttributeValueType(int index) throws XmlParserException { |
| int type = getAttribute(index).getValueType(); |
| switch (type) { |
| case Attribute.TYPE_STRING: |
| return VALUE_TYPE_STRING; |
| case Attribute.TYPE_INT_DEC: |
| case Attribute.TYPE_INT_HEX: |
| return VALUE_TYPE_INT; |
| case Attribute.TYPE_REFERENCE: |
| return VALUE_TYPE_REFERENCE; |
| case Attribute.TYPE_INT_BOOLEAN: |
| return VALUE_TYPE_BOOLEAN; |
| default: |
| return VALUE_TYPE_UNSUPPORTED; |
| } |
| } |
| |
| /** |
| * Returns the integer value of the specified attribute of the current element. See |
| * {@code VALUE_TYPE_...} constants. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event. |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public int getAttributeIntValue(int index) throws XmlParserException { |
| return getAttribute(index).getIntValue(); |
| } |
| |
| /** |
| * Returns the boolean value of the specified attribute of the current element. See |
| * {@code VALUE_TYPE_...} constants. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event. |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public boolean getAttributeBooleanValue(int index) throws XmlParserException { |
| return getAttribute(index).getBooleanValue(); |
| } |
| |
| /** |
| * Returns the string value of the specified attribute of the current element. See |
| * {@code VALUE_TYPE_...} constants. |
| * |
| * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a |
| * {@code start element} event. |
| * @throws XmlParserException if a parsing error is occurred |
| */ |
| public String getAttributeStringValue(int index) throws XmlParserException { |
| return getAttribute(index).getStringValue(); |
| } |
| |
| private Attribute getAttribute(int index) { |
| if (mCurrentEvent != EVENT_START_ELEMENT) { |
| throw new IndexOutOfBoundsException("Current event not a START_ELEMENT"); |
| } |
| if (index < 0) { |
| throw new IndexOutOfBoundsException("index must be >= 0"); |
| } |
| if (index >= mCurrentElementAttributeCount) { |
| throw new IndexOutOfBoundsException( |
| "index must be <= attr count (" + mCurrentElementAttributeCount + ")"); |
| } |
| parseCurrentElementAttributesIfNotParsed(); |
| return mCurrentElementAttributes.get(index); |
| } |
| |
| /** |
| * Advances to the next parsing event and returns its type. See {@code EVENT_...} constants. |
| */ |
| public int next() throws XmlParserException { |
| // Decrement depth if the previous event was "end element". |
| if (mCurrentEvent == EVENT_END_ELEMENT) { |
| mDepth--; |
| } |
| |
| // Read events from document, ignoring events that we don't report to caller. Stop at the |
| // earliest event which we report to caller. |
| while (mXml.hasRemaining()) { |
| Chunk chunk = Chunk.get(mXml); |
| if (chunk == null) { |
| break; |
| } |
| switch (chunk.getType()) { |
| case Chunk.TYPE_STRING_POOL: |
| if (mStringPool != null) { |
| throw new XmlParserException("Multiple string pools not supported"); |
| } |
| mStringPool = new StringPool(chunk); |
| break; |
| |
| case Chunk.RES_XML_TYPE_START_ELEMENT: |
| { |
| if (mStringPool == null) { |
| throw new XmlParserException( |
| "Named element encountered before string pool"); |
| } |
| ByteBuffer contents = chunk.getContents(); |
| if (contents.remaining() < 20) { |
| throw new XmlParserException( |
| "Start element chunk too short. Need at least 20 bytes. Available: " |
| + contents.remaining() + " bytes"); |
| } |
| long nsId = getUnsignedInt32(contents); |
| long nameId = getUnsignedInt32(contents); |
| int attrStartOffset = getUnsignedInt16(contents); |
| int attrSizeBytes = getUnsignedInt16(contents); |
| int attrCount = getUnsignedInt16(contents); |
| long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes; |
| contents.position(0); |
| if (attrStartOffset > contents.remaining()) { |
| throw new XmlParserException( |
| "Attributes start offset out of bounds: " + attrStartOffset |
| + ", max: " + contents.remaining()); |
| } |
| if (attrEndOffset > contents.remaining()) { |
| throw new XmlParserException( |
| "Attributes end offset out of bounds: " + attrEndOffset |
| + ", max: " + contents.remaining()); |
| } |
| |
| mCurrentElementName = mStringPool.getString(nameId); |
| mCurrentElementNamespace = |
| (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId); |
| mCurrentElementAttributeCount = attrCount; |
| mCurrentElementAttributes = null; |
| mCurrentElementAttrSizeBytes = attrSizeBytes; |
| mCurrentElementAttributesContents = |
| sliceFromTo(contents, attrStartOffset, attrEndOffset); |
| |
| mDepth++; |
| mCurrentEvent = EVENT_START_ELEMENT; |
| return mCurrentEvent; |
| } |
| |
| case Chunk.RES_XML_TYPE_END_ELEMENT: |
| { |
| if (mStringPool == null) { |
| throw new XmlParserException( |
| "Named element encountered before string pool"); |
| } |
| ByteBuffer contents = chunk.getContents(); |
| if (contents.remaining() < 8) { |
| throw new XmlParserException( |
| "End element chunk too short. Need at least 8 bytes. Available: " |
| + contents.remaining() + " bytes"); |
| } |
| long nsId = getUnsignedInt32(contents); |
| long nameId = getUnsignedInt32(contents); |
| mCurrentElementName = mStringPool.getString(nameId); |
| mCurrentElementNamespace = |
| (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId); |
| mCurrentEvent = EVENT_END_ELEMENT; |
| mCurrentElementAttributes = null; |
| mCurrentElementAttributesContents = null; |
| return mCurrentEvent; |
| } |
| case Chunk.RES_XML_TYPE_RESOURCE_MAP: |
| if (mResourceMap != null) { |
| throw new XmlParserException("Multiple resource maps not supported"); |
| } |
| mResourceMap = new ResourceMap(chunk); |
| break; |
| default: |
| // Unknown chunk type -- ignore |
| break; |
| } |
| } |
| |
| mCurrentEvent = EVENT_END_DOCUMENT; |
| return mCurrentEvent; |
| } |
| |
| private void parseCurrentElementAttributesIfNotParsed() { |
| if (mCurrentElementAttributes != null) { |
| return; |
| } |
| mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount); |
| for (int i = 0; i < mCurrentElementAttributeCount; i++) { |
| int startPosition = i * mCurrentElementAttrSizeBytes; |
| ByteBuffer attr = |
| sliceFromTo( |
| mCurrentElementAttributesContents, |
| startPosition, |
| startPosition + mCurrentElementAttrSizeBytes); |
| long nsId = getUnsignedInt32(attr); |
| long nameId = getUnsignedInt32(attr); |
| attr.position(attr.position() + 7); // skip ignored fields |
| int valueType = getUnsignedInt8(attr); |
| long valueData = getUnsignedInt32(attr); |
| mCurrentElementAttributes.add( |
| new Attribute( |
| nsId, |
| nameId, |
| valueType, |
| (int) valueData, |
| mStringPool, |
| mResourceMap)); |
| } |
| } |
| |
| private static class Attribute { |
| private static final int TYPE_REFERENCE = 1; |
| private static final int TYPE_STRING = 3; |
| private static final int TYPE_INT_DEC = 0x10; |
| private static final int TYPE_INT_HEX = 0x11; |
| private static final int TYPE_INT_BOOLEAN = 0x12; |
| |
| private final long mNsId; |
| private final long mNameId; |
| private final int mValueType; |
| private final int mValueData; |
| private final StringPool mStringPool; |
| private final ResourceMap mResourceMap; |
| |
| private Attribute( |
| long nsId, |
| long nameId, |
| int valueType, |
| int valueData, |
| StringPool stringPool, |
| ResourceMap resourceMap) { |
| mNsId = nsId; |
| mNameId = nameId; |
| mValueType = valueType; |
| mValueData = valueData; |
| mStringPool = stringPool; |
| mResourceMap = resourceMap; |
| } |
| |
| public int getNameResourceId() { |
| return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0; |
| } |
| |
| public String getName() throws XmlParserException { |
| return mStringPool.getString(mNameId); |
| } |
| |
| public String getNamespace() throws XmlParserException { |
| return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : ""; |
| } |
| |
| public int getValueType() { |
| return mValueType; |
| } |
| |
| public int getIntValue() throws XmlParserException { |
| switch (mValueType) { |
| case TYPE_REFERENCE: |
| case TYPE_INT_DEC: |
| case TYPE_INT_HEX: |
| case TYPE_INT_BOOLEAN: |
| return mValueData; |
| default: |
| throw new XmlParserException("Cannot coerce to int: value type " + mValueType); |
| } |
| } |
| |
| public boolean getBooleanValue() throws XmlParserException { |
| switch (mValueType) { |
| case TYPE_INT_BOOLEAN: |
| return mValueData != 0; |
| default: |
| throw new XmlParserException( |
| "Cannot coerce to boolean: value type " + mValueType); |
| } |
| } |
| |
| public String getStringValue() throws XmlParserException { |
| switch (mValueType) { |
| case TYPE_STRING: |
| return mStringPool.getString(mValueData & 0xffffffffL); |
| case TYPE_INT_DEC: |
| return Integer.toString(mValueData); |
| case TYPE_INT_HEX: |
| return "0x" + Integer.toHexString(mValueData); |
| case TYPE_INT_BOOLEAN: |
| return Boolean.toString(mValueData != 0); |
| case TYPE_REFERENCE: |
| return "@" + Integer.toHexString(mValueData); |
| default: |
| throw new XmlParserException( |
| "Cannot coerce to string: value type " + mValueType); |
| } |
| } |
| } |
| |
| /** |
| * Chunk of a document. Each chunk is tagged with a type and consists of a header followed by |
| * contents. |
| */ |
| private static class Chunk { |
| public static final int TYPE_STRING_POOL = 1; |
| public static final int TYPE_RES_XML = 3; |
| public static final int RES_XML_TYPE_START_ELEMENT = 0x0102; |
| public static final int RES_XML_TYPE_END_ELEMENT = 0x0103; |
| public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180; |
| |
| static final int HEADER_MIN_SIZE_BYTES = 8; |
| |
| private final int mType; |
| private final ByteBuffer mHeader; |
| private final ByteBuffer mContents; |
| |
| public Chunk(int type, ByteBuffer header, ByteBuffer contents) { |
| mType = type; |
| mHeader = header; |
| mContents = contents; |
| } |
| |
| public ByteBuffer getContents() { |
| ByteBuffer result = mContents.slice(); |
| result.order(mContents.order()); |
| return result; |
| } |
| |
| public ByteBuffer getHeader() { |
| ByteBuffer result = mHeader.slice(); |
| result.order(mHeader.order()); |
| return result; |
| } |
| |
| public int getType() { |
| return mType; |
| } |
| |
| /** |
| * Consumes the chunk located at the current position of the input and returns the chunk |
| * or {@code null} if there is no chunk left in the input. |
| * |
| * @throws XmlParserException if the chunk is malformed |
| */ |
| public static Chunk get(ByteBuffer input) throws XmlParserException { |
| if (input.remaining() < HEADER_MIN_SIZE_BYTES) { |
| // Android ignores the last chunk if its header is too big to fit into the file |
| input.position(input.limit()); |
| return null; |
| } |
| |
| int originalPosition = input.position(); |
| int type = getUnsignedInt16(input); |
| int headerSize = getUnsignedInt16(input); |
| long chunkSize = getUnsignedInt32(input); |
| long chunkRemaining = chunkSize - 8; |
| if (chunkRemaining > input.remaining()) { |
| // Android ignores the last chunk if it's too big to fit into the file |
| input.position(input.limit()); |
| return null; |
| } |
| if (headerSize < HEADER_MIN_SIZE_BYTES) { |
| throw new XmlParserException( |
| "Malformed chunk: header too short: " + headerSize + " bytes"); |
| } else if (headerSize > chunkSize) { |
| throw new XmlParserException( |
| "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: " |
| + chunkSize + " bytes"); |
| } |
| int contentStartPosition = originalPosition + headerSize; |
| long chunkEndPosition = originalPosition + chunkSize; |
| Chunk chunk = |
| new Chunk( |
| type, |
| sliceFromTo(input, originalPosition, contentStartPosition), |
| sliceFromTo(input, contentStartPosition, chunkEndPosition)); |
| input.position((int) chunkEndPosition); |
| return chunk; |
| } |
| } |
| |
| /** |
| * String pool of a document. Strings are referenced by their {@code 0}-based index in the pool. |
| */ |
| private static class StringPool { |
| private static final int FLAG_UTF8 = 1 << 8; |
| |
| private final ByteBuffer mChunkContents; |
| private final ByteBuffer mStringsSection; |
| private final int mStringCount; |
| private final boolean mUtf8Encoded; |
| private final Map<Integer, String> mCachedStrings = new HashMap<>(); |
| |
| /** |
| * Constructs a new string pool from the provided chunk. |
| * |
| * @throws XmlParserException if a parsing error occurred |
| */ |
| public StringPool(Chunk chunk) throws XmlParserException { |
| ByteBuffer header = chunk.getHeader(); |
| int headerSizeBytes = header.remaining(); |
| header.position(Chunk.HEADER_MIN_SIZE_BYTES); |
| if (header.remaining() < 20) { |
| throw new XmlParserException( |
| "XML chunk's header too short. Required at least 20 bytes. Available: " |
| + header.remaining() + " bytes"); |
| } |
| long stringCount = getUnsignedInt32(header); |
| if (stringCount > Integer.MAX_VALUE) { |
| throw new XmlParserException("Too many strings: " + stringCount); |
| } |
| mStringCount = (int) stringCount; |
| long styleCount = getUnsignedInt32(header); |
| if (styleCount > Integer.MAX_VALUE) { |
| throw new XmlParserException("Too many styles: " + styleCount); |
| } |
| long flags = getUnsignedInt32(header); |
| long stringsStartOffset = getUnsignedInt32(header); |
| long stylesStartOffset = getUnsignedInt32(header); |
| |
| ByteBuffer contents = chunk.getContents(); |
| if (mStringCount > 0) { |
| int stringsSectionStartOffsetInContents = |
| (int) (stringsStartOffset - headerSizeBytes); |
| int stringsSectionEndOffsetInContents; |
| if (styleCount > 0) { |
| // Styles section follows the strings section |
| if (stylesStartOffset < stringsStartOffset) { |
| throw new XmlParserException( |
| "Styles offset (" + stylesStartOffset + ") < strings offset (" |
| + stringsStartOffset + ")"); |
| } |
| stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes); |
| } else { |
| stringsSectionEndOffsetInContents = contents.remaining(); |
| } |
| mStringsSection = |
| sliceFromTo( |
| contents, |
| stringsSectionStartOffsetInContents, |
| stringsSectionEndOffsetInContents); |
| } else { |
| mStringsSection = ByteBuffer.allocate(0); |
| } |
| |
| mUtf8Encoded = (flags & FLAG_UTF8) != 0; |
| mChunkContents = contents; |
| } |
| |
| /** |
| * Returns the string located at the specified {@code 0}-based index in this pool. |
| * |
| * @throws XmlParserException if the string does not exist or cannot be decoded |
| */ |
| public String getString(long index) throws XmlParserException { |
| if (index < 0) { |
| throw new XmlParserException("Unsuported string index: " + index); |
| } else if (index >= mStringCount) { |
| throw new XmlParserException( |
| "Unsuported string index: " + index + ", max: " + (mStringCount - 1)); |
| } |
| |
| int idx = (int) index; |
| String result = mCachedStrings.get(idx); |
| if (result != null) { |
| return result; |
| } |
| |
| long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4); |
| if (offsetInStringsSection >= mStringsSection.capacity()) { |
| throw new XmlParserException( |
| "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection |
| + ", max: " + (mStringsSection.capacity() - 1)); |
| } |
| mStringsSection.position((int) offsetInStringsSection); |
| result = |
| (mUtf8Encoded) |
| ? getLengthPrefixedUtf8EncodedString(mStringsSection) |
| : getLengthPrefixedUtf16EncodedString(mStringsSection); |
| mCachedStrings.put(idx, result); |
| return result; |
| } |
| |
| private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded) |
| throws XmlParserException { |
| // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16. |
| // Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range |
| // of supported values is 0 to 0x7fffffff inclusive. |
| int lengthChars = getUnsignedInt16(encoded); |
| if ((lengthChars & 0x8000) != 0) { |
| lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded); |
| } |
| if (lengthChars > Integer.MAX_VALUE / 2) { |
| throw new XmlParserException("String too long: " + lengthChars + " uint16s"); |
| } |
| int lengthBytes = lengthChars * 2; |
| |
| byte[] arr; |
| int arrOffset; |
| if (encoded.hasArray()) { |
| arr = encoded.array(); |
| arrOffset = encoded.arrayOffset() + encoded.position(); |
| encoded.position(encoded.position() + lengthBytes); |
| } else { |
| arr = new byte[lengthBytes]; |
| arrOffset = 0; |
| encoded.get(arr); |
| } |
| // Reproduce the behavior of Android runtime which requires that the UTF-16 encoded |
| // array of bytes is NULL terminated. |
| if ((arr[arrOffset + lengthBytes] != 0) |
| || (arr[arrOffset + lengthBytes + 1] != 0)) { |
| throw new XmlParserException("UTF-16 encoded form of string not NULL terminated"); |
| } |
| try { |
| return new String(arr, arrOffset, lengthBytes, "UTF-16LE"); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException("UTF-16LE character encoding not supported", e); |
| } |
| } |
| |
| private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded) |
| throws XmlParserException { |
| // If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise, |
| // it is stored as a big-endian uint16 with highest bit set. Thus, the range of |
| // supported values is 0 to 0x7fff inclusive. |
| |
| // Skip UTF-16 encoded length (in uint16s) |
| int lengthBytes = getUnsignedInt8(encoded); |
| if ((lengthBytes & 0x80) != 0) { |
| lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded); |
| } |
| |
| // Read UTF-8 encoded length (in bytes) |
| lengthBytes = getUnsignedInt8(encoded); |
| if ((lengthBytes & 0x80) != 0) { |
| lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded); |
| } |
| |
| byte[] arr; |
| int arrOffset; |
| if (encoded.hasArray()) { |
| arr = encoded.array(); |
| arrOffset = encoded.arrayOffset() + encoded.position(); |
| encoded.position(encoded.position() + lengthBytes); |
| } else { |
| arr = new byte[lengthBytes]; |
| arrOffset = 0; |
| encoded.get(arr); |
| } |
| // Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array |
| // of bytes is NULL terminated. |
| if (arr[arrOffset + lengthBytes] != 0) { |
| throw new XmlParserException("UTF-8 encoded form of string not NULL terminated"); |
| } |
| try { |
| return new String(arr, arrOffset, lengthBytes, "UTF-8"); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException("UTF-8 character encoding not supported", e); |
| } |
| } |
| } |
| |
| /** |
| * Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the |
| * map. |
| */ |
| private static class ResourceMap { |
| private final ByteBuffer mChunkContents; |
| private final int mEntryCount; |
| |
| /** |
| * Constructs a new resource map from the provided chunk. |
| * |
| * @throws XmlParserException if a parsing error occurred |
| */ |
| public ResourceMap(Chunk chunk) throws XmlParserException { |
| mChunkContents = chunk.getContents().slice(); |
| mChunkContents.order(chunk.getContents().order()); |
| // Each entry of the map is four bytes long, containing the int32 resource ID. |
| mEntryCount = mChunkContents.remaining() / 4; |
| } |
| |
| /** |
| * Returns the resource ID located at the specified {@code 0}-based index in this pool or |
| * {@code 0} if the index is out of range. |
| */ |
| public int getResourceId(long index) { |
| if ((index < 0) || (index >= mEntryCount)) { |
| return 0; |
| } |
| int idx = (int) index; |
| // Each entry of the map is four bytes long, containing the int32 resource ID. |
| return mChunkContents.getInt(idx * 4); |
| } |
| } |
| |
| /** |
| * Returns new byte buffer whose content is a shared subsequence of this buffer's content |
| * between the specified start (inclusive) and end (exclusive) positions. As opposed to |
| * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source |
| * buffer's byte order. |
| */ |
| private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) { |
| if (start < 0) { |
| throw new IllegalArgumentException("start: " + start); |
| } |
| if (end < start) { |
| throw new IllegalArgumentException("end < start: " + end + " < " + start); |
| } |
| int capacity = source.capacity(); |
| if (end > source.capacity()) { |
| throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); |
| } |
| return sliceFromTo(source, (int) start, (int) end); |
| } |
| |
| /** |
| * Returns new byte buffer whose content is a shared subsequence of this buffer's content |
| * between the specified start (inclusive) and end (exclusive) positions. As opposed to |
| * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source |
| * buffer's byte order. |
| */ |
| private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { |
| if (start < 0) { |
| throw new IllegalArgumentException("start: " + start); |
| } |
| if (end < start) { |
| throw new IllegalArgumentException("end < start: " + end + " < " + start); |
| } |
| int capacity = source.capacity(); |
| if (end > source.capacity()) { |
| throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); |
| } |
| int originalLimit = source.limit(); |
| int originalPosition = source.position(); |
| try { |
| source.position(0); |
| source.limit(end); |
| source.position(start); |
| ByteBuffer result = source.slice(); |
| result.order(source.order()); |
| return result; |
| } finally { |
| source.position(0); |
| source.limit(originalLimit); |
| source.position(originalPosition); |
| } |
| } |
| |
| private static int getUnsignedInt8(ByteBuffer buffer) { |
| return buffer.get() & 0xff; |
| } |
| |
| private static int getUnsignedInt16(ByteBuffer buffer) { |
| return buffer.getShort() & 0xffff; |
| } |
| |
| private static long getUnsignedInt32(ByteBuffer buffer) { |
| return buffer.getInt() & 0xffffffffL; |
| } |
| |
| private static long getUnsignedInt32(ByteBuffer buffer, int position) { |
| return buffer.getInt(position) & 0xffffffffL; |
| } |
| |
| /** |
| * Indicates that an error occurred while parsing a document. |
| */ |
| public static class XmlParserException extends Exception { |
| private static final long serialVersionUID = 1L; |
| |
| public XmlParserException(String message) { |
| super(message); |
| } |
| |
| public XmlParserException(String message, Throwable cause) { |
| super(message, cause); |
| } |
| } |
| } |