1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.apksig.internal.apk;
18 
19 import java.io.UnsupportedEncodingException;
20 import java.nio.ByteBuffer;
21 import java.nio.ByteOrder;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 
27 /**
28  * XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
29  *
30  * <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
31  * {@link #getEventType()} and {@link #next()} methods. Additional information about the current
32  * event can be obtained via an assortment of getters, for example, {@link #getName()} or
33  * {@link #getAttributeNameResourceId(int)}.
34  */
35 public class AndroidBinXmlParser {
36 
37     /** Event: start of document. */
38     public static final int EVENT_START_DOCUMENT = 1;
39 
40     /** Event: end of document. */
41     public static final int EVENT_END_DOCUMENT = 2;
42 
43     /** Event: start of an element. */
44     public static final int EVENT_START_ELEMENT = 3;
45 
46     /** Event: end of an document. */
47     public static final int EVENT_END_ELEMENT = 4;
48 
49     /** Attribute value type is not supported by this parser. */
50     public static final int VALUE_TYPE_UNSUPPORTED = 0;
51 
52     /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
53     public static final int VALUE_TYPE_STRING = 1;
54 
55     /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
56     public static final int VALUE_TYPE_INT = 2;
57 
58     /**
59      * Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
60      */
61     public static final int VALUE_TYPE_REFERENCE = 3;
62 
63     /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
64     public static final int VALUE_TYPE_BOOLEAN = 4;
65 
66     private static final long NO_NAMESPACE = 0xffffffffL;
67 
68     private final ByteBuffer mXml;
69 
70     private StringPool mStringPool;
71     private ResourceMap mResourceMap;
72     private int mDepth;
73     private int mCurrentEvent = EVENT_START_DOCUMENT;
74 
75     private String mCurrentElementName;
76     private String mCurrentElementNamespace;
77     private int mCurrentElementAttributeCount;
78     private List<Attribute> mCurrentElementAttributes;
79     private ByteBuffer mCurrentElementAttributesContents;
80     private int mCurrentElementAttrSizeBytes;
81 
82     /**
83      * Constructs a new parser for the provided document.
84      */
AndroidBinXmlParser(ByteBuffer xml)85     public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
86         xml.order(ByteOrder.LITTLE_ENDIAN);
87 
88         Chunk resXmlChunk = null;
89         while (xml.hasRemaining()) {
90             Chunk chunk = Chunk.get(xml);
91             if (chunk == null) {
92                 break;
93             }
94             if (chunk.getType() == Chunk.TYPE_RES_XML) {
95                 resXmlChunk = chunk;
96                 break;
97             }
98         }
99 
100         if (resXmlChunk == null) {
101             throw new XmlParserException("No XML chunk in file");
102         }
103         mXml = resXmlChunk.getContents();
104     }
105 
106     /**
107      * Returns the depth of the current element. Outside of the root of the document the depth is
108      * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
109      * is decremented by {@code 1} after each {@code end element} event.
110      */
getDepth()111     public int getDepth() {
112         return mDepth;
113     }
114 
115     /**
116      * Returns the type of the current event. See {@code EVENT_...} constants.
117      */
getEventType()118     public int getEventType() {
119         return mCurrentEvent;
120     }
121 
122     /**
123      * Returns the local name of the current element or {@code null} if the current event does not
124      * pertain to an element.
125      */
getName()126     public String getName() {
127         if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
128             return null;
129         }
130         return mCurrentElementName;
131     }
132 
133     /**
134      * Returns the namespace of the current element or {@code null} if the current event does not
135      * pertain to an element. Returns an empty string if the element is not associated with a
136      * namespace.
137      */
getNamespace()138     public String getNamespace() {
139         if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
140             return null;
141         }
142         return mCurrentElementNamespace;
143     }
144 
145     /**
146      * Returns the number of attributes of the element associated with the current event or
147      * {@code -1} if no element is associated with the current event.
148      */
getAttributeCount()149     public int getAttributeCount() {
150         if (mCurrentEvent != EVENT_START_ELEMENT) {
151             return -1;
152         }
153 
154         return mCurrentElementAttributeCount;
155     }
156 
157     /**
158      * Returns the resource ID corresponding to the name of the specified attribute of the current
159      * element or {@code 0} if the name is not associated with a resource ID.
160      *
161      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
162      *         {@code start element} event
163      * @throws XmlParserException if a parsing error is occurred
164      */
getAttributeNameResourceId(int index)165     public int getAttributeNameResourceId(int index) throws XmlParserException {
166         return getAttribute(index).getNameResourceId();
167     }
168 
169     /**
170      * Returns the name of the specified attribute of the current element.
171      *
172      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
173      *         {@code start element} event
174      * @throws XmlParserException if a parsing error is occurred
175      */
getAttributeName(int index)176     public String getAttributeName(int index) throws XmlParserException {
177         return getAttribute(index).getName();
178     }
179 
180     /**
181      * Returns the name of the specified attribute of the current element or an empty string if
182      * the attribute is not associated with a namespace.
183      *
184      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
185      *         {@code start element} event
186      * @throws XmlParserException if a parsing error is occurred
187      */
getAttributeNamespace(int index)188     public String getAttributeNamespace(int index) throws XmlParserException {
189         return getAttribute(index).getNamespace();
190     }
191 
192     /**
193      * Returns the value type of the specified attribute of the current element. See
194      * {@code VALUE_TYPE_...} constants.
195      *
196      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
197      *         {@code start element} event
198      * @throws XmlParserException if a parsing error is occurred
199      */
getAttributeValueType(int index)200     public int getAttributeValueType(int index) throws XmlParserException {
201         int type = getAttribute(index).getValueType();
202         switch (type) {
203             case Attribute.TYPE_STRING:
204                 return VALUE_TYPE_STRING;
205             case Attribute.TYPE_INT_DEC:
206             case Attribute.TYPE_INT_HEX:
207                 return VALUE_TYPE_INT;
208             case Attribute.TYPE_REFERENCE:
209                 return VALUE_TYPE_REFERENCE;
210             case Attribute.TYPE_INT_BOOLEAN:
211                 return VALUE_TYPE_BOOLEAN;
212             default:
213                 return VALUE_TYPE_UNSUPPORTED;
214         }
215     }
216 
217     /**
218      * Returns the integer value of the specified attribute of the current element. See
219      * {@code VALUE_TYPE_...} constants.
220      *
221      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
222      *         {@code start element} event.
223      * @throws XmlParserException if a parsing error is occurred
224      */
getAttributeIntValue(int index)225     public int getAttributeIntValue(int index) throws XmlParserException {
226         return getAttribute(index).getIntValue();
227     }
228 
229     /**
230      * Returns the boolean value of the specified attribute of the current element. See
231      * {@code VALUE_TYPE_...} constants.
232      *
233      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
234      *         {@code start element} event.
235      * @throws XmlParserException if a parsing error is occurred
236      */
getAttributeBooleanValue(int index)237     public boolean getAttributeBooleanValue(int index) throws XmlParserException {
238         return getAttribute(index).getBooleanValue();
239     }
240 
241     /**
242      * Returns the string value of the specified attribute of the current element. See
243      * {@code VALUE_TYPE_...} constants.
244      *
245      * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
246      *         {@code start element} event.
247      * @throws XmlParserException if a parsing error is occurred
248      */
getAttributeStringValue(int index)249     public String getAttributeStringValue(int index) throws XmlParserException {
250         return getAttribute(index).getStringValue();
251     }
252 
getAttribute(int index)253     private Attribute getAttribute(int index) {
254         if (mCurrentEvent != EVENT_START_ELEMENT) {
255             throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
256         }
257         if (index < 0) {
258             throw new IndexOutOfBoundsException("index must be >= 0");
259         }
260         if (index >= mCurrentElementAttributeCount) {
261             throw new IndexOutOfBoundsException(
262                     "index must be <= attr count (" + mCurrentElementAttributeCount + ")");
263         }
264         parseCurrentElementAttributesIfNotParsed();
265         return mCurrentElementAttributes.get(index);
266     }
267 
268     /**
269      * Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
270      */
next()271     public int next() throws XmlParserException {
272         // Decrement depth if the previous event was "end element".
273         if (mCurrentEvent == EVENT_END_ELEMENT) {
274             mDepth--;
275         }
276 
277         // Read events from document, ignoring events that we don't report to caller. Stop at the
278         // earliest event which we report to caller.
279         while (mXml.hasRemaining()) {
280             Chunk chunk = Chunk.get(mXml);
281             if (chunk == null) {
282                 break;
283             }
284             switch (chunk.getType()) {
285                 case Chunk.TYPE_STRING_POOL:
286                     if (mStringPool != null) {
287                         throw new XmlParserException("Multiple string pools not supported");
288                     }
289                     mStringPool = new StringPool(chunk);
290                     break;
291 
292                 case Chunk.RES_XML_TYPE_START_ELEMENT:
293                 {
294                     if (mStringPool == null) {
295                         throw new XmlParserException(
296                                 "Named element encountered before string pool");
297                     }
298                     ByteBuffer contents = chunk.getContents();
299                     if (contents.remaining() < 20) {
300                         throw new XmlParserException(
301                                 "Start element chunk too short. Need at least 20 bytes. Available: "
302                                         + contents.remaining() + " bytes");
303                     }
304                     long nsId = getUnsignedInt32(contents);
305                     long nameId = getUnsignedInt32(contents);
306                     int attrStartOffset = getUnsignedInt16(contents);
307                     int attrSizeBytes = getUnsignedInt16(contents);
308                     int attrCount = getUnsignedInt16(contents);
309                     long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
310                     contents.position(0);
311                     if (attrStartOffset > contents.remaining()) {
312                         throw new XmlParserException(
313                                 "Attributes start offset out of bounds: " + attrStartOffset
314                                     + ", max: " + contents.remaining());
315                     }
316                     if (attrEndOffset > contents.remaining()) {
317                         throw new XmlParserException(
318                                 "Attributes end offset out of bounds: " + attrEndOffset
319                                     + ", max: " + contents.remaining());
320                     }
321 
322                     mCurrentElementName = mStringPool.getString(nameId);
323                     mCurrentElementNamespace =
324                             (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
325                     mCurrentElementAttributeCount = attrCount;
326                     mCurrentElementAttributes = null;
327                     mCurrentElementAttrSizeBytes = attrSizeBytes;
328                     mCurrentElementAttributesContents =
329                             sliceFromTo(contents, attrStartOffset, attrEndOffset);
330 
331                     mDepth++;
332                     mCurrentEvent = EVENT_START_ELEMENT;
333                     return mCurrentEvent;
334                 }
335 
336                 case Chunk.RES_XML_TYPE_END_ELEMENT:
337                 {
338                     if (mStringPool == null) {
339                         throw new XmlParserException(
340                                 "Named element encountered before string pool");
341                     }
342                     ByteBuffer contents = chunk.getContents();
343                     if (contents.remaining() < 8) {
344                         throw new XmlParserException(
345                                 "End element chunk too short. Need at least 8 bytes. Available: "
346                                         + contents.remaining() + " bytes");
347                     }
348                     long nsId = getUnsignedInt32(contents);
349                     long nameId = getUnsignedInt32(contents);
350                     mCurrentElementName = mStringPool.getString(nameId);
351                     mCurrentElementNamespace =
352                             (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
353                     mCurrentEvent = EVENT_END_ELEMENT;
354                     mCurrentElementAttributes = null;
355                     mCurrentElementAttributesContents = null;
356                     return mCurrentEvent;
357                 }
358                 case Chunk.RES_XML_TYPE_RESOURCE_MAP:
359                     if (mResourceMap != null) {
360                         throw new XmlParserException("Multiple resource maps not supported");
361                     }
362                     mResourceMap = new ResourceMap(chunk);
363                     break;
364                 default:
365                     // Unknown chunk type -- ignore
366                     break;
367             }
368         }
369 
370         mCurrentEvent = EVENT_END_DOCUMENT;
371         return mCurrentEvent;
372     }
373 
parseCurrentElementAttributesIfNotParsed()374     private void parseCurrentElementAttributesIfNotParsed() {
375         if (mCurrentElementAttributes != null) {
376             return;
377         }
378         mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
379         for (int i = 0; i < mCurrentElementAttributeCount; i++) {
380             int startPosition = i * mCurrentElementAttrSizeBytes;
381             ByteBuffer attr =
382                     sliceFromTo(
383                             mCurrentElementAttributesContents,
384                             startPosition,
385                             startPosition + mCurrentElementAttrSizeBytes);
386             long nsId = getUnsignedInt32(attr);
387             long nameId = getUnsignedInt32(attr);
388             attr.position(attr.position() + 7); // skip ignored fields
389             int valueType = getUnsignedInt8(attr);
390             long valueData = getUnsignedInt32(attr);
391             mCurrentElementAttributes.add(
392                     new Attribute(
393                             nsId,
394                             nameId,
395                             valueType,
396                             (int) valueData,
397                             mStringPool,
398                             mResourceMap));
399         }
400     }
401 
402     private static class Attribute {
403         private static final int TYPE_REFERENCE = 1;
404         private static final int TYPE_STRING = 3;
405         private static final int TYPE_INT_DEC = 0x10;
406         private static final int TYPE_INT_HEX = 0x11;
407         private static final int TYPE_INT_BOOLEAN = 0x12;
408 
409         private final long mNsId;
410         private final long mNameId;
411         private final int mValueType;
412         private final int mValueData;
413         private final StringPool mStringPool;
414         private final ResourceMap mResourceMap;
415 
Attribute( long nsId, long nameId, int valueType, int valueData, StringPool stringPool, ResourceMap resourceMap)416         private Attribute(
417                 long nsId,
418                 long nameId,
419                 int valueType,
420                 int valueData,
421                 StringPool stringPool,
422                 ResourceMap resourceMap) {
423             mNsId = nsId;
424             mNameId = nameId;
425             mValueType = valueType;
426             mValueData = valueData;
427             mStringPool = stringPool;
428             mResourceMap = resourceMap;
429         }
430 
getNameResourceId()431         public int getNameResourceId() {
432             return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
433         }
434 
getName()435         public String getName() throws XmlParserException {
436             return mStringPool.getString(mNameId);
437         }
438 
getNamespace()439         public String getNamespace() throws XmlParserException {
440             return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
441         }
442 
getValueType()443         public int getValueType() {
444             return mValueType;
445         }
446 
getIntValue()447         public int getIntValue() throws XmlParserException {
448             switch (mValueType) {
449                 case TYPE_REFERENCE:
450                 case TYPE_INT_DEC:
451                 case TYPE_INT_HEX:
452                 case TYPE_INT_BOOLEAN:
453                     return mValueData;
454                 default:
455                     throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
456             }
457         }
458 
getBooleanValue()459         public boolean getBooleanValue() throws XmlParserException {
460             switch (mValueType) {
461                 case TYPE_INT_BOOLEAN:
462                     return mValueData != 0;
463                 default:
464                     throw new XmlParserException(
465                             "Cannot coerce to boolean: value type " + mValueType);
466             }
467         }
468 
getStringValue()469         public String getStringValue() throws XmlParserException {
470             switch (mValueType) {
471                 case TYPE_STRING:
472                     return mStringPool.getString(mValueData & 0xffffffffL);
473                 case TYPE_INT_DEC:
474                     return Integer.toString(mValueData);
475                 case TYPE_INT_HEX:
476                     return "0x" + Integer.toHexString(mValueData);
477                 case TYPE_INT_BOOLEAN:
478                     return Boolean.toString(mValueData != 0);
479                 case TYPE_REFERENCE:
480                     return "@" + Integer.toHexString(mValueData);
481                 default:
482                     throw new XmlParserException(
483                             "Cannot coerce to string: value type " + mValueType);
484             }
485         }
486     }
487 
488     /**
489      * Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
490      * contents.
491      */
492     private static class Chunk {
493         public static final int TYPE_STRING_POOL = 1;
494         public static final int TYPE_RES_XML = 3;
495         public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
496         public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
497         public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
498 
499         static final int HEADER_MIN_SIZE_BYTES = 8;
500 
501         private final int mType;
502         private final ByteBuffer mHeader;
503         private final ByteBuffer mContents;
504 
Chunk(int type, ByteBuffer header, ByteBuffer contents)505         public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
506             mType = type;
507             mHeader = header;
508             mContents = contents;
509         }
510 
getContents()511         public ByteBuffer getContents() {
512             ByteBuffer result = mContents.slice();
513             result.order(mContents.order());
514             return result;
515         }
516 
getHeader()517         public ByteBuffer getHeader() {
518             ByteBuffer result = mHeader.slice();
519             result.order(mHeader.order());
520             return result;
521         }
522 
getType()523         public int getType() {
524             return mType;
525         }
526 
527         /**
528          * Consumes the chunk located at the current position of the input and returns the chunk
529          * or {@code null} if there is no chunk left in the input.
530          *
531          * @throws XmlParserException if the chunk is malformed
532          */
get(ByteBuffer input)533         public static Chunk get(ByteBuffer input) throws XmlParserException {
534             if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
535                 // Android ignores the last chunk if its header is too big to fit into the file
536                 input.position(input.limit());
537                 return null;
538             }
539 
540             int originalPosition = input.position();
541             int type = getUnsignedInt16(input);
542             int headerSize = getUnsignedInt16(input);
543             long chunkSize = getUnsignedInt32(input);
544             long chunkRemaining = chunkSize - 8;
545             if (chunkRemaining > input.remaining()) {
546                 // Android ignores the last chunk if it's too big to fit into the file
547                 input.position(input.limit());
548                 return null;
549             }
550             if (headerSize < HEADER_MIN_SIZE_BYTES) {
551                 throw new XmlParserException(
552                         "Malformed chunk: header too short: " + headerSize + " bytes");
553             } else if (headerSize > chunkSize) {
554                 throw new XmlParserException(
555                         "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
556                                 + chunkSize + " bytes");
557             }
558             int contentStartPosition = originalPosition + headerSize;
559             long chunkEndPosition = originalPosition + chunkSize;
560             Chunk chunk =
561                     new Chunk(
562                             type,
563                             sliceFromTo(input, originalPosition, contentStartPosition),
564                             sliceFromTo(input, contentStartPosition, chunkEndPosition));
565             input.position((int) chunkEndPosition);
566             return chunk;
567         }
568     }
569 
570     /**
571      * String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
572      */
573     private static class StringPool {
574         private static final int FLAG_UTF8 = 1 << 8;
575 
576         private final ByteBuffer mChunkContents;
577         private final ByteBuffer mStringsSection;
578         private final int mStringCount;
579         private final boolean mUtf8Encoded;
580         private final Map<Integer, String> mCachedStrings = new HashMap<>();
581 
582         /**
583          * Constructs a new string pool from the provided chunk.
584          *
585          * @throws XmlParserException if a parsing error occurred
586          */
StringPool(Chunk chunk)587         public StringPool(Chunk chunk) throws XmlParserException {
588             ByteBuffer header = chunk.getHeader();
589             int headerSizeBytes = header.remaining();
590             header.position(Chunk.HEADER_MIN_SIZE_BYTES);
591             if (header.remaining() < 20) {
592                 throw new XmlParserException(
593                         "XML chunk's header too short. Required at least 20 bytes. Available: "
594                                 + header.remaining() + " bytes");
595             }
596             long stringCount = getUnsignedInt32(header);
597             if (stringCount > Integer.MAX_VALUE) {
598                 throw new XmlParserException("Too many strings: " + stringCount);
599             }
600             mStringCount = (int) stringCount;
601             long styleCount = getUnsignedInt32(header);
602             if (styleCount > Integer.MAX_VALUE) {
603                 throw new XmlParserException("Too many styles: " + styleCount);
604             }
605             long flags = getUnsignedInt32(header);
606             long stringsStartOffset = getUnsignedInt32(header);
607             long stylesStartOffset = getUnsignedInt32(header);
608 
609             ByteBuffer contents = chunk.getContents();
610             if (mStringCount > 0) {
611                 int stringsSectionStartOffsetInContents =
612                         (int) (stringsStartOffset - headerSizeBytes);
613                 int stringsSectionEndOffsetInContents;
614                 if (styleCount > 0) {
615                     // Styles section follows the strings section
616                     if (stylesStartOffset < stringsStartOffset) {
617                         throw new XmlParserException(
618                                 "Styles offset (" + stylesStartOffset + ") < strings offset ("
619                                         + stringsStartOffset + ")");
620                     }
621                     stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
622                 } else {
623                     stringsSectionEndOffsetInContents = contents.remaining();
624                 }
625                 mStringsSection =
626                         sliceFromTo(
627                                 contents,
628                                 stringsSectionStartOffsetInContents,
629                                 stringsSectionEndOffsetInContents);
630             } else {
631                 mStringsSection = ByteBuffer.allocate(0);
632             }
633 
634             mUtf8Encoded = (flags & FLAG_UTF8) != 0;
635             mChunkContents = contents;
636         }
637 
638         /**
639          * Returns the string located at the specified {@code 0}-based index in this pool.
640          *
641          * @throws XmlParserException if the string does not exist or cannot be decoded
642          */
getString(long index)643         public String getString(long index) throws XmlParserException {
644             if (index < 0) {
645                 throw new XmlParserException("Unsuported string index: " + index);
646             } else if (index >= mStringCount) {
647                 throw new XmlParserException(
648                         "Unsuported string index: " + index + ", max: " + (mStringCount - 1));
649             }
650 
651             int idx = (int) index;
652             String result = mCachedStrings.get(idx);
653             if (result != null) {
654                 return result;
655             }
656 
657             long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
658             if (offsetInStringsSection >= mStringsSection.capacity()) {
659                 throw new XmlParserException(
660                         "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
661                                 + ", max: " + (mStringsSection.capacity() - 1));
662             }
663             mStringsSection.position((int) offsetInStringsSection);
664             result =
665                     (mUtf8Encoded)
666                             ? getLengthPrefixedUtf8EncodedString(mStringsSection)
667                             : getLengthPrefixedUtf16EncodedString(mStringsSection);
668             mCachedStrings.put(idx, result);
669             return result;
670         }
671 
getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)672         private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
673                 throws XmlParserException {
674             // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
675             // Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
676             // of supported values is 0 to 0x7fffffff inclusive.
677             int lengthChars = getUnsignedInt16(encoded);
678             if ((lengthChars & 0x8000) != 0) {
679                 lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
680             }
681             if (lengthChars > Integer.MAX_VALUE / 2) {
682                 throw new XmlParserException("String too long: " + lengthChars + " uint16s");
683             }
684             int lengthBytes = lengthChars * 2;
685 
686             byte[] arr;
687             int arrOffset;
688             if (encoded.hasArray()) {
689                 arr = encoded.array();
690                 arrOffset = encoded.arrayOffset() + encoded.position();
691                 encoded.position(encoded.position() + lengthBytes);
692             } else {
693                 arr = new byte[lengthBytes];
694                 arrOffset = 0;
695                 encoded.get(arr);
696             }
697             // Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
698             // array of bytes is NULL terminated.
699             if ((arr[arrOffset + lengthBytes] != 0)
700                     || (arr[arrOffset + lengthBytes + 1] != 0)) {
701                 throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
702             }
703             try {
704                 return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
705             } catch (UnsupportedEncodingException e) {
706                 throw new RuntimeException("UTF-16LE character encoding not supported", e);
707             }
708         }
709 
getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)710         private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
711                 throws XmlParserException {
712             // If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
713             // it is stored as a big-endian uint16 with highest bit set. Thus, the range of
714             // supported values is 0 to 0x7fff inclusive.
715 
716             // Skip UTF-16 encoded length (in uint16s)
717             int lengthBytes = getUnsignedInt8(encoded);
718             if ((lengthBytes & 0x80) != 0) {
719                 lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
720             }
721 
722             // Read UTF-8 encoded length (in bytes)
723             lengthBytes = getUnsignedInt8(encoded);
724             if ((lengthBytes & 0x80) != 0) {
725                 lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
726             }
727 
728             byte[] arr;
729             int arrOffset;
730             if (encoded.hasArray()) {
731                 arr = encoded.array();
732                 arrOffset = encoded.arrayOffset() + encoded.position();
733                 encoded.position(encoded.position() + lengthBytes);
734             } else {
735                 arr = new byte[lengthBytes];
736                 arrOffset = 0;
737                 encoded.get(arr);
738             }
739             // Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
740             // of bytes is NULL terminated.
741             if (arr[arrOffset + lengthBytes] != 0) {
742                 throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
743             }
744             try {
745                 return new String(arr, arrOffset, lengthBytes, "UTF-8");
746             } catch (UnsupportedEncodingException e) {
747                 throw new RuntimeException("UTF-8 character encoding not supported", e);
748             }
749         }
750     }
751 
752     /**
753      * Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
754      * map.
755      */
756     private static class ResourceMap {
757         private final ByteBuffer mChunkContents;
758         private final int mEntryCount;
759 
760         /**
761          * Constructs a new resource map from the provided chunk.
762          *
763          * @throws XmlParserException if a parsing error occurred
764          */
ResourceMap(Chunk chunk)765         public ResourceMap(Chunk chunk) throws XmlParserException {
766             mChunkContents = chunk.getContents().slice();
767             mChunkContents.order(chunk.getContents().order());
768             // Each entry of the map is four bytes long, containing the int32 resource ID.
769             mEntryCount = mChunkContents.remaining() /  4;
770         }
771 
772         /**
773          * Returns the resource ID located at the specified {@code 0}-based index in this pool or
774          * {@code 0} if the index is out of range.
775          */
getResourceId(long index)776         public int getResourceId(long index) {
777             if ((index < 0) || (index >= mEntryCount)) {
778                 return 0;
779             }
780             int idx = (int) index;
781             // Each entry of the map is four bytes long, containing the int32 resource ID.
782             return mChunkContents.getInt(idx * 4);
783         }
784     }
785 
786     /**
787      * Returns new byte buffer whose content is a shared subsequence of this buffer's content
788      * between the specified start (inclusive) and end (exclusive) positions. As opposed to
789      * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
790      * buffer's byte order.
791      */
sliceFromTo(ByteBuffer source, long start, long end)792     private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
793         if (start < 0) {
794             throw new IllegalArgumentException("start: " + start);
795         }
796         if (end < start) {
797             throw new IllegalArgumentException("end < start: " + end + " < " + start);
798         }
799         int capacity = source.capacity();
800         if (end > source.capacity()) {
801             throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
802         }
803         return sliceFromTo(source, (int) start, (int) end);
804     }
805 
806     /**
807      * Returns new byte buffer whose content is a shared subsequence of this buffer's content
808      * between the specified start (inclusive) and end (exclusive) positions. As opposed to
809      * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
810      * buffer's byte order.
811      */
sliceFromTo(ByteBuffer source, int start, int end)812     private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
813         if (start < 0) {
814             throw new IllegalArgumentException("start: " + start);
815         }
816         if (end < start) {
817             throw new IllegalArgumentException("end < start: " + end + " < " + start);
818         }
819         int capacity = source.capacity();
820         if (end > source.capacity()) {
821             throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
822         }
823         int originalLimit = source.limit();
824         int originalPosition = source.position();
825         try {
826             source.position(0);
827             source.limit(end);
828             source.position(start);
829             ByteBuffer result = source.slice();
830             result.order(source.order());
831             return result;
832         } finally {
833             source.position(0);
834             source.limit(originalLimit);
835             source.position(originalPosition);
836         }
837     }
838 
getUnsignedInt8(ByteBuffer buffer)839     private static int getUnsignedInt8(ByteBuffer buffer) {
840         return buffer.get() & 0xff;
841     }
842 
getUnsignedInt16(ByteBuffer buffer)843     private static int getUnsignedInt16(ByteBuffer buffer) {
844         return buffer.getShort() & 0xffff;
845     }
846 
getUnsignedInt32(ByteBuffer buffer)847     private static long getUnsignedInt32(ByteBuffer buffer) {
848         return buffer.getInt() & 0xffffffffL;
849     }
850 
getUnsignedInt32(ByteBuffer buffer, int position)851     private static long getUnsignedInt32(ByteBuffer buffer, int position) {
852         return buffer.getInt(position) & 0xffffffffL;
853     }
854 
855     /**
856      * Indicates that an error occurred while parsing a document.
857      */
858     public static class XmlParserException extends Exception {
859         private static final long serialVersionUID = 1L;
860 
XmlParserException(String message)861         public XmlParserException(String message) {
862             super(message);
863         }
864 
XmlParserException(String message, Throwable cause)865         public XmlParserException(String message, Throwable cause) {
866             super(message, cause);
867         }
868     }
869 }
870