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