1 /* 2 * Copyright (C) 2022 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.modules.utils; 18 19 import static com.android.modules.utils.BinaryXmlSerializer.ATTRIBUTE; 20 import static com.android.modules.utils.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0; 21 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE; 22 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE; 23 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BYTES_BASE64; 24 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BYTES_HEX; 25 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_DOUBLE; 26 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_FLOAT; 27 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_INT; 28 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_INT_HEX; 29 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_LONG; 30 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_LONG_HEX; 31 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_NULL; 32 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_STRING; 33 import static com.android.modules.utils.BinaryXmlSerializer.TYPE_STRING_INTERNED; 34 35 import android.annotation.NonNull; 36 import android.annotation.Nullable; 37 import android.text.TextUtils; 38 import android.util.Base64; 39 40 import org.xmlpull.v1.XmlPullParser; 41 import org.xmlpull.v1.XmlPullParserException; 42 43 import java.io.EOFException; 44 import java.io.IOException; 45 import java.io.InputStream; 46 import java.io.Reader; 47 import java.nio.charset.StandardCharsets; 48 import java.util.Arrays; 49 import java.util.Objects; 50 51 /** 52 * Parser that reads XML documents using a custom binary wire protocol which 53 * benchmarking has shown to be 8.5x faster than {@link Xml.newFastPullParser()} 54 * for a typical {@code packages.xml}. 55 * <p> 56 * The high-level design of the wire protocol is to directly serialize the event 57 * stream, while efficiently and compactly writing strongly-typed primitives 58 * delivered through the {@link TypedXmlSerializer} interface. 59 * <p> 60 * Each serialized event is a single byte where the lower half is a normal 61 * {@link XmlPullParser} token and the upper half is an optional data type 62 * signal, such as {@link #TYPE_INT}. 63 * <p> 64 * This parser has some specific limitations: 65 * <ul> 66 * <li>Only the UTF-8 encoding is supported. 67 * <li>Variable length values, such as {@code byte[]} or {@link String}, are 68 * limited to 65,535 bytes in length. Note that {@link String} values are stored 69 * as UTF-8 on the wire. 70 * <li>Namespaces, prefixes, properties, and options are unsupported. 71 * </ul> 72 */ 73 public class BinaryXmlPullParser implements TypedXmlPullParser { 74 private FastDataInput mIn; 75 76 private int mCurrentToken = START_DOCUMENT; 77 private int mCurrentDepth = 0; 78 private String mCurrentName; 79 private String mCurrentText; 80 81 /** 82 * Pool of attributes parsed for the currently tag. All interactions should 83 * be done via {@link #obtainAttribute()}, {@link #findAttribute(String)}, 84 * and {@link #resetAttributes()}. 85 */ 86 private int mAttributeCount = 0; 87 private Attribute[] mAttributes; 88 89 @Override setInput(InputStream is, String encoding)90 public void setInput(InputStream is, String encoding) throws XmlPullParserException { 91 if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) { 92 throw new UnsupportedOperationException(); 93 } 94 95 if (mIn != null) { 96 mIn.release(); 97 mIn = null; 98 } 99 100 mIn = obtainFastDataInput(is); 101 102 mCurrentToken = START_DOCUMENT; 103 mCurrentDepth = 0; 104 mCurrentName = null; 105 mCurrentText = null; 106 107 mAttributeCount = 0; 108 mAttributes = new Attribute[8]; 109 for (int i = 0; i < mAttributes.length; i++) { 110 mAttributes[i] = new Attribute(); 111 } 112 113 try { 114 final byte[] magic = new byte[4]; 115 mIn.readFully(magic); 116 if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) { 117 throw new IOException("Unexpected magic " + bytesToHexString(magic)); 118 } 119 120 // We're willing to immediately consume a START_DOCUMENT if present, 121 // but we're okay if it's missing 122 if (peekNextExternalToken() == START_DOCUMENT) { 123 consumeToken(); 124 } 125 } catch (IOException e) { 126 throw new XmlPullParserException(e.toString()); 127 } 128 } 129 130 @NonNull obtainFastDataInput(@onNull InputStream is)131 protected FastDataInput obtainFastDataInput(@NonNull InputStream is) { 132 return FastDataInput.obtain(is); 133 } 134 135 @Override setInput(Reader in)136 public void setInput(Reader in) throws XmlPullParserException { 137 throw new UnsupportedOperationException(); 138 } 139 140 @Override next()141 public int next() throws XmlPullParserException, IOException { 142 while (true) { 143 final int token = nextToken(); 144 switch (token) { 145 case START_TAG: 146 case END_TAG: 147 case END_DOCUMENT: 148 return token; 149 case TEXT: 150 consumeAdditionalText(); 151 // Per interface docs, empty text regions are skipped 152 if (mCurrentText == null || mCurrentText.length() == 0) { 153 continue; 154 } else { 155 return TEXT; 156 } 157 } 158 } 159 } 160 161 @Override nextToken()162 public int nextToken() throws XmlPullParserException, IOException { 163 if (mCurrentToken == XmlPullParser.END_TAG) { 164 mCurrentDepth--; 165 } 166 167 int token; 168 try { 169 token = peekNextExternalToken(); 170 consumeToken(); 171 } catch (EOFException e) { 172 token = END_DOCUMENT; 173 } 174 switch (token) { 175 case XmlPullParser.START_TAG: 176 // We need to peek forward to find the next external token so 177 // that we parse all pending INTERNAL_ATTRIBUTE tokens 178 peekNextExternalToken(); 179 mCurrentDepth++; 180 break; 181 } 182 mCurrentToken = token; 183 return token; 184 } 185 186 /** 187 * Peek at the next "external" token without consuming it. 188 * <p> 189 * External tokens, such as {@link #START_TAG}, are expected by typical 190 * {@link XmlPullParser} clients. In contrast, internal tokens, such as 191 * {@link #ATTRIBUTE}, are not expected by typical clients. 192 * <p> 193 * This method consumes any internal events until it reaches the next 194 * external event. 195 */ peekNextExternalToken()196 private int peekNextExternalToken() throws IOException, XmlPullParserException { 197 while (true) { 198 final int token = peekNextToken(); 199 switch (token) { 200 case ATTRIBUTE: 201 consumeToken(); 202 continue; 203 default: 204 return token; 205 } 206 } 207 } 208 209 /** 210 * Peek at the next token in the underlying stream without consuming it. 211 */ peekNextToken()212 private int peekNextToken() throws IOException { 213 return mIn.peekByte() & 0x0f; 214 } 215 216 /** 217 * Parse and consume the next token in the underlying stream. 218 */ consumeToken()219 private void consumeToken() throws IOException, XmlPullParserException { 220 final int event = mIn.readByte(); 221 final int token = event & 0x0f; 222 final int type = event & 0xf0; 223 switch (token) { 224 case ATTRIBUTE: { 225 final Attribute attr = obtainAttribute(); 226 attr.name = mIn.readInternedUTF(); 227 attr.type = type; 228 switch (type) { 229 case TYPE_NULL: 230 case TYPE_BOOLEAN_TRUE: 231 case TYPE_BOOLEAN_FALSE: 232 // Nothing extra to fill in 233 break; 234 case TYPE_STRING: 235 attr.valueString = mIn.readUTF(); 236 break; 237 case TYPE_STRING_INTERNED: 238 attr.valueString = mIn.readInternedUTF(); 239 break; 240 case TYPE_BYTES_HEX: 241 case TYPE_BYTES_BASE64: 242 final int len = mIn.readUnsignedShort(); 243 final byte[] res = new byte[len]; 244 mIn.readFully(res); 245 attr.valueBytes = res; 246 break; 247 case TYPE_INT: 248 case TYPE_INT_HEX: 249 attr.valueInt = mIn.readInt(); 250 break; 251 case TYPE_LONG: 252 case TYPE_LONG_HEX: 253 attr.valueLong = mIn.readLong(); 254 break; 255 case TYPE_FLOAT: 256 attr.valueFloat = mIn.readFloat(); 257 break; 258 case TYPE_DOUBLE: 259 attr.valueDouble = mIn.readDouble(); 260 break; 261 default: 262 throw new IOException("Unexpected data type " + type); 263 } 264 break; 265 } 266 case XmlPullParser.START_DOCUMENT: { 267 mCurrentName = null; 268 mCurrentText = null; 269 if (mAttributeCount > 0) resetAttributes(); 270 break; 271 } 272 case XmlPullParser.END_DOCUMENT: { 273 mCurrentName = null; 274 mCurrentText = null; 275 if (mAttributeCount > 0) resetAttributes(); 276 break; 277 } 278 case XmlPullParser.START_TAG: { 279 mCurrentName = mIn.readInternedUTF(); 280 mCurrentText = null; 281 if (mAttributeCount > 0) resetAttributes(); 282 break; 283 } 284 case XmlPullParser.END_TAG: { 285 mCurrentName = mIn.readInternedUTF(); 286 mCurrentText = null; 287 if (mAttributeCount > 0) resetAttributes(); 288 break; 289 } 290 case XmlPullParser.TEXT: 291 case XmlPullParser.CDSECT: 292 case XmlPullParser.PROCESSING_INSTRUCTION: 293 case XmlPullParser.COMMENT: 294 case XmlPullParser.DOCDECL: 295 case XmlPullParser.IGNORABLE_WHITESPACE: { 296 mCurrentName = null; 297 mCurrentText = mIn.readUTF(); 298 if (mAttributeCount > 0) resetAttributes(); 299 break; 300 } 301 case XmlPullParser.ENTITY_REF: { 302 mCurrentName = mIn.readUTF(); 303 mCurrentText = resolveEntity(mCurrentName); 304 if (mAttributeCount > 0) resetAttributes(); 305 break; 306 } 307 default: { 308 throw new IOException("Unknown token " + token + " with type " + type); 309 } 310 } 311 } 312 313 /** 314 * When the current tag is {@link #TEXT}, consume all subsequent "text" 315 * events, as described by {@link #next}. When finished, the current event 316 * will still be {@link #TEXT}. 317 */ consumeAdditionalText()318 private void consumeAdditionalText() throws IOException, XmlPullParserException { 319 String combinedText = mCurrentText; 320 while (true) { 321 final int token = peekNextExternalToken(); 322 switch (token) { 323 case COMMENT: 324 case PROCESSING_INSTRUCTION: 325 // Quietly consumed 326 consumeToken(); 327 break; 328 case TEXT: 329 case CDSECT: 330 case ENTITY_REF: 331 // Additional text regions collected 332 consumeToken(); 333 combinedText += mCurrentText; 334 break; 335 default: 336 // Next token is something non-text, so wrap things up 337 mCurrentToken = TEXT; 338 mCurrentName = null; 339 mCurrentText = combinedText; 340 return; 341 } 342 } 343 } 344 resolveEntity(@onNull String entity)345 static @NonNull String resolveEntity(@NonNull String entity) 346 throws XmlPullParserException { 347 switch (entity) { 348 case "lt": return "<"; 349 case "gt": return ">"; 350 case "amp": return "&"; 351 case "apos": return "'"; 352 case "quot": return "\""; 353 } 354 if (entity.length() > 1 && entity.charAt(0) == '#') { 355 final char c = (char) Integer.parseInt(entity.substring(1)); 356 return new String(new char[] { c }); 357 } 358 throw new XmlPullParserException("Unknown entity " + entity); 359 } 360 361 @Override require(int type, String namespace, String name)362 public void require(int type, String namespace, String name) 363 throws XmlPullParserException, IOException { 364 if (namespace != null && !namespace.isEmpty()) throw illegalNamespace(); 365 if (mCurrentToken != type || !Objects.equals(mCurrentName, name)) { 366 throw new XmlPullParserException(getPositionDescription()); 367 } 368 } 369 370 @Override nextText()371 public String nextText() throws XmlPullParserException, IOException { 372 if (getEventType() != START_TAG) { 373 throw new XmlPullParserException(getPositionDescription()); 374 } 375 int eventType = next(); 376 if (eventType == TEXT) { 377 String result = getText(); 378 eventType = next(); 379 if (eventType != END_TAG) { 380 throw new XmlPullParserException(getPositionDescription()); 381 } 382 return result; 383 } else if (eventType == END_TAG) { 384 return ""; 385 } else { 386 throw new XmlPullParserException(getPositionDescription()); 387 } 388 } 389 390 @Override nextTag()391 public int nextTag() throws XmlPullParserException, IOException { 392 int eventType = next(); 393 if (eventType == TEXT && isWhitespace()) { 394 eventType = next(); 395 } 396 if (eventType != START_TAG && eventType != END_TAG) { 397 throw new XmlPullParserException(getPositionDescription()); 398 } 399 return eventType; 400 } 401 402 /** 403 * Allocate and return a new {@link Attribute} associated with the tag being 404 * currently processed. This will automatically grow the internal pool as 405 * needed. 406 */ obtainAttribute()407 private @NonNull Attribute obtainAttribute() { 408 if (mAttributeCount == mAttributes.length) { 409 final int before = mAttributes.length; 410 final int after = before + (before >> 1); 411 mAttributes = Arrays.copyOf(mAttributes, after); 412 for (int i = before; i < after; i++) { 413 mAttributes[i] = new Attribute(); 414 } 415 } 416 return mAttributes[mAttributeCount++]; 417 } 418 419 /** 420 * Clear any {@link Attribute} instances that have been allocated by 421 * {@link #obtainAttribute()}, returning them into the pool for recycling. 422 */ resetAttributes()423 private void resetAttributes() { 424 for (int i = 0; i < mAttributeCount; i++) { 425 mAttributes[i].reset(); 426 } 427 mAttributeCount = 0; 428 } 429 430 @Override getAttributeIndex(String namespace, String name)431 public int getAttributeIndex(String namespace, String name) { 432 if (namespace != null && !namespace.isEmpty()) throw illegalNamespace(); 433 for (int i = 0; i < mAttributeCount; i++) { 434 if (Objects.equals(mAttributes[i].name, name)) { 435 return i; 436 } 437 } 438 return -1; 439 } 440 441 @Override getAttributeValue(String namespace, String name)442 public String getAttributeValue(String namespace, String name) { 443 final int index = getAttributeIndex(namespace, name); 444 if (index != -1) { 445 return mAttributes[index].getValueString(); 446 } else { 447 return null; 448 } 449 } 450 451 @Override getAttributeValue(int index)452 public String getAttributeValue(int index) { 453 return mAttributes[index].getValueString(); 454 } 455 456 @Override getAttributeBytesHex(int index)457 public byte[] getAttributeBytesHex(int index) throws XmlPullParserException { 458 return mAttributes[index].getValueBytesHex(); 459 } 460 461 @Override getAttributeBytesBase64(int index)462 public byte[] getAttributeBytesBase64(int index) throws XmlPullParserException { 463 return mAttributes[index].getValueBytesBase64(); 464 } 465 466 @Override getAttributeInt(int index)467 public int getAttributeInt(int index) throws XmlPullParserException { 468 return mAttributes[index].getValueInt(); 469 } 470 471 @Override getAttributeIntHex(int index)472 public int getAttributeIntHex(int index) throws XmlPullParserException { 473 return mAttributes[index].getValueIntHex(); 474 } 475 476 @Override getAttributeLong(int index)477 public long getAttributeLong(int index) throws XmlPullParserException { 478 return mAttributes[index].getValueLong(); 479 } 480 481 @Override getAttributeLongHex(int index)482 public long getAttributeLongHex(int index) throws XmlPullParserException { 483 return mAttributes[index].getValueLongHex(); 484 } 485 486 @Override getAttributeFloat(int index)487 public float getAttributeFloat(int index) throws XmlPullParserException { 488 return mAttributes[index].getValueFloat(); 489 } 490 491 @Override getAttributeDouble(int index)492 public double getAttributeDouble(int index) throws XmlPullParserException { 493 return mAttributes[index].getValueDouble(); 494 } 495 496 @Override getAttributeBoolean(int index)497 public boolean getAttributeBoolean(int index) throws XmlPullParserException { 498 return mAttributes[index].getValueBoolean(); 499 } 500 501 @Override getText()502 public String getText() { 503 return mCurrentText; 504 } 505 506 @Override getTextCharacters(int[] holderForStartAndLength)507 public char[] getTextCharacters(int[] holderForStartAndLength) { 508 final char[] chars = mCurrentText.toCharArray(); 509 holderForStartAndLength[0] = 0; 510 holderForStartAndLength[1] = chars.length; 511 return chars; 512 } 513 514 @Override getInputEncoding()515 public String getInputEncoding() { 516 return StandardCharsets.UTF_8.name(); 517 } 518 519 @Override getDepth()520 public int getDepth() { 521 return mCurrentDepth; 522 } 523 524 @Override getPositionDescription()525 public String getPositionDescription() { 526 // Not very helpful, but it's the best information we have 527 return "Token " + mCurrentToken + " at depth " + mCurrentDepth; 528 } 529 530 @Override getLineNumber()531 public int getLineNumber() { 532 return -1; 533 } 534 535 @Override getColumnNumber()536 public int getColumnNumber() { 537 return -1; 538 } 539 540 @Override isWhitespace()541 public boolean isWhitespace() throws XmlPullParserException { 542 switch (mCurrentToken) { 543 case IGNORABLE_WHITESPACE: 544 return true; 545 case TEXT: 546 case CDSECT: 547 return !TextUtils.isGraphic(mCurrentText); 548 default: 549 throw new XmlPullParserException("Not applicable for token " + mCurrentToken); 550 } 551 } 552 553 @Override getNamespace()554 public String getNamespace() { 555 switch (mCurrentToken) { 556 case START_TAG: 557 case END_TAG: 558 // Namespaces are unsupported 559 return NO_NAMESPACE; 560 default: 561 return null; 562 } 563 } 564 565 @Override getName()566 public String getName() { 567 return mCurrentName; 568 } 569 570 @Override getPrefix()571 public String getPrefix() { 572 // Prefixes are not supported 573 return null; 574 } 575 576 @Override isEmptyElementTag()577 public boolean isEmptyElementTag() throws XmlPullParserException { 578 switch (mCurrentToken) { 579 case START_TAG: 580 try { 581 return (peekNextExternalToken() == END_TAG); 582 } catch (IOException e) { 583 throw new XmlPullParserException(e.toString()); 584 } 585 default: 586 throw new XmlPullParserException("Not at START_TAG"); 587 } 588 } 589 590 @Override getAttributeCount()591 public int getAttributeCount() { 592 return mAttributeCount; 593 } 594 595 @Override getAttributeNamespace(int index)596 public String getAttributeNamespace(int index) { 597 // Namespaces are unsupported 598 return NO_NAMESPACE; 599 } 600 601 @Override getAttributeName(int index)602 public String getAttributeName(int index) { 603 return mAttributes[index].name; 604 } 605 606 @Override getAttributePrefix(int index)607 public String getAttributePrefix(int index) { 608 // Prefixes are not supported 609 return null; 610 } 611 612 @Override getAttributeType(int index)613 public String getAttributeType(int index) { 614 // Validation is not supported 615 return "CDATA"; 616 } 617 618 @Override isAttributeDefault(int index)619 public boolean isAttributeDefault(int index) { 620 // Validation is not supported 621 return false; 622 } 623 624 @Override getEventType()625 public int getEventType() throws XmlPullParserException { 626 return mCurrentToken; 627 } 628 629 @Override getNamespaceCount(int depth)630 public int getNamespaceCount(int depth) throws XmlPullParserException { 631 // Namespaces are unsupported 632 return 0; 633 } 634 635 @Override getNamespacePrefix(int pos)636 public String getNamespacePrefix(int pos) throws XmlPullParserException { 637 // Namespaces are unsupported 638 throw new UnsupportedOperationException(); 639 } 640 641 @Override getNamespaceUri(int pos)642 public String getNamespaceUri(int pos) throws XmlPullParserException { 643 // Namespaces are unsupported 644 throw new UnsupportedOperationException(); 645 } 646 647 @Override getNamespace(String prefix)648 public String getNamespace(String prefix) { 649 // Namespaces are unsupported 650 throw new UnsupportedOperationException(); 651 } 652 653 @Override defineEntityReplacementText(String entityName, String replacementText)654 public void defineEntityReplacementText(String entityName, String replacementText) 655 throws XmlPullParserException { 656 // Custom entities are not supported 657 throw new UnsupportedOperationException(); 658 } 659 660 @Override setFeature(String name, boolean state)661 public void setFeature(String name, boolean state) throws XmlPullParserException { 662 // Features are not supported 663 throw new UnsupportedOperationException(); 664 } 665 666 @Override getFeature(String name)667 public boolean getFeature(String name) { 668 // Features are not supported 669 throw new UnsupportedOperationException(); 670 } 671 672 @Override setProperty(String name, Object value)673 public void setProperty(String name, Object value) throws XmlPullParserException { 674 // Properties are not supported 675 throw new UnsupportedOperationException(); 676 } 677 678 @Override getProperty(String name)679 public Object getProperty(String name) { 680 // Properties are not supported 681 throw new UnsupportedOperationException(); 682 } 683 illegalNamespace()684 private static IllegalArgumentException illegalNamespace() { 685 throw new IllegalArgumentException("Namespaces are not supported"); 686 } 687 688 /** 689 * Holder representing a single attribute. This design enables object 690 * recycling without resorting to autoboxing. 691 * <p> 692 * To support conversion between human-readable XML and binary XML, the 693 * various accessor methods will transparently convert from/to 694 * human-readable values when needed. 695 */ 696 private static class Attribute { 697 public String name; 698 public int type; 699 700 public String valueString; 701 public byte[] valueBytes; 702 public int valueInt; 703 public long valueLong; 704 public float valueFloat; 705 public double valueDouble; 706 reset()707 public void reset() { 708 name = null; 709 valueString = null; 710 valueBytes = null; 711 } 712 getValueString()713 public @Nullable String getValueString() { 714 switch (type) { 715 case TYPE_NULL: 716 return null; 717 case TYPE_STRING: 718 case TYPE_STRING_INTERNED: 719 return valueString; 720 case TYPE_BYTES_HEX: 721 return bytesToHexString(valueBytes); 722 case TYPE_BYTES_BASE64: 723 return Base64.encodeToString(valueBytes, Base64.NO_WRAP); 724 case TYPE_INT: 725 return Integer.toString(valueInt); 726 case TYPE_INT_HEX: 727 return Integer.toString(valueInt, 16); 728 case TYPE_LONG: 729 return Long.toString(valueLong); 730 case TYPE_LONG_HEX: 731 return Long.toString(valueLong, 16); 732 case TYPE_FLOAT: 733 return Float.toString(valueFloat); 734 case TYPE_DOUBLE: 735 return Double.toString(valueDouble); 736 case TYPE_BOOLEAN_TRUE: 737 return "true"; 738 case TYPE_BOOLEAN_FALSE: 739 return "false"; 740 default: 741 // Unknown data type; null is the best we can offer 742 return null; 743 } 744 } 745 getValueBytesHex()746 public @Nullable byte[] getValueBytesHex() throws XmlPullParserException { 747 switch (type) { 748 case TYPE_NULL: 749 return null; 750 case TYPE_BYTES_HEX: 751 case TYPE_BYTES_BASE64: 752 return valueBytes; 753 case TYPE_STRING: 754 case TYPE_STRING_INTERNED: 755 try { 756 return hexStringToBytes(valueString); 757 } catch (Exception e) { 758 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 759 } 760 default: 761 throw new XmlPullParserException("Invalid conversion from " + type); 762 } 763 } 764 getValueBytesBase64()765 public @Nullable byte[] getValueBytesBase64() throws XmlPullParserException { 766 switch (type) { 767 case TYPE_NULL: 768 return null; 769 case TYPE_BYTES_HEX: 770 case TYPE_BYTES_BASE64: 771 return valueBytes; 772 case TYPE_STRING: 773 case TYPE_STRING_INTERNED: 774 try { 775 return Base64.decode(valueString, Base64.NO_WRAP); 776 } catch (Exception e) { 777 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 778 } 779 default: 780 throw new XmlPullParserException("Invalid conversion from " + type); 781 } 782 } 783 getValueInt()784 public int getValueInt() throws XmlPullParserException { 785 switch (type) { 786 case TYPE_INT: 787 case TYPE_INT_HEX: 788 return valueInt; 789 case TYPE_STRING: 790 case TYPE_STRING_INTERNED: 791 try { 792 return Integer.parseInt(valueString); 793 } catch (Exception e) { 794 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 795 } 796 default: 797 throw new XmlPullParserException("Invalid conversion from " + type); 798 } 799 } 800 getValueIntHex()801 public int getValueIntHex() throws XmlPullParserException { 802 switch (type) { 803 case TYPE_INT: 804 case TYPE_INT_HEX: 805 return valueInt; 806 case TYPE_STRING: 807 case TYPE_STRING_INTERNED: 808 try { 809 return Integer.parseInt(valueString, 16); 810 } catch (Exception e) { 811 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 812 } 813 default: 814 throw new XmlPullParserException("Invalid conversion from " + type); 815 } 816 } 817 getValueLong()818 public long getValueLong() throws XmlPullParserException { 819 switch (type) { 820 case TYPE_LONG: 821 case TYPE_LONG_HEX: 822 return valueLong; 823 case TYPE_STRING: 824 case TYPE_STRING_INTERNED: 825 try { 826 return Long.parseLong(valueString); 827 } catch (Exception e) { 828 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 829 } 830 default: 831 throw new XmlPullParserException("Invalid conversion from " + type); 832 } 833 } 834 getValueLongHex()835 public long getValueLongHex() throws XmlPullParserException { 836 switch (type) { 837 case TYPE_LONG: 838 case TYPE_LONG_HEX: 839 return valueLong; 840 case TYPE_STRING: 841 case TYPE_STRING_INTERNED: 842 try { 843 return Long.parseLong(valueString, 16); 844 } catch (Exception e) { 845 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 846 } 847 default: 848 throw new XmlPullParserException("Invalid conversion from " + type); 849 } 850 } 851 getValueFloat()852 public float getValueFloat() throws XmlPullParserException { 853 switch (type) { 854 case TYPE_FLOAT: 855 return valueFloat; 856 case TYPE_STRING: 857 case TYPE_STRING_INTERNED: 858 try { 859 return Float.parseFloat(valueString); 860 } catch (Exception e) { 861 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 862 } 863 default: 864 throw new XmlPullParserException("Invalid conversion from " + type); 865 } 866 } 867 getValueDouble()868 public double getValueDouble() throws XmlPullParserException { 869 switch (type) { 870 case TYPE_DOUBLE: 871 return valueDouble; 872 case TYPE_STRING: 873 case TYPE_STRING_INTERNED: 874 try { 875 return Double.parseDouble(valueString); 876 } catch (Exception e) { 877 throw new XmlPullParserException("Invalid attribute " + name + ": " + e); 878 } 879 default: 880 throw new XmlPullParserException("Invalid conversion from " + type); 881 } 882 } 883 getValueBoolean()884 public boolean getValueBoolean() throws XmlPullParserException { 885 switch (type) { 886 case TYPE_BOOLEAN_TRUE: 887 return true; 888 case TYPE_BOOLEAN_FALSE: 889 return false; 890 case TYPE_STRING: 891 case TYPE_STRING_INTERNED: 892 if ("true".equalsIgnoreCase(valueString)) { 893 return true; 894 } else if ("false".equalsIgnoreCase(valueString)) { 895 return false; 896 } else { 897 throw new XmlPullParserException( 898 "Invalid attribute " + name + ": " + valueString); 899 } 900 default: 901 throw new XmlPullParserException("Invalid conversion from " + type); 902 } 903 } 904 } 905 906 // NOTE: To support unbundled clients, we include an inlined copy 907 // of hex conversion logic from HexDump below 908 private final static char[] HEX_DIGITS = 909 { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; 910 toByte(char c)911 private static int toByte(char c) { 912 if (c >= '0' && c <= '9') return (c - '0'); 913 if (c >= 'A' && c <= 'F') return (c - 'A' + 10); 914 if (c >= 'a' && c <= 'f') return (c - 'a' + 10); 915 throw new IllegalArgumentException("Invalid hex char '" + c + "'"); 916 } 917 bytesToHexString(byte[] value)918 static String bytesToHexString(byte[] value) { 919 final int length = value.length; 920 final char[] buf = new char[length * 2]; 921 int bufIndex = 0; 922 for (int i = 0; i < length; i++) { 923 byte b = value[i]; 924 buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F]; 925 buf[bufIndex++] = HEX_DIGITS[b & 0x0F]; 926 } 927 return new String(buf); 928 } 929 hexStringToBytes(String value)930 static byte[] hexStringToBytes(String value) { 931 final int length = value.length(); 932 if (length % 2 != 0) { 933 throw new IllegalArgumentException("Invalid hex length " + length); 934 } 935 byte[] buffer = new byte[length / 2]; 936 for (int i = 0; i < length; i += 2) { 937 buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4) 938 | toByte(value.charAt(i + 1))); 939 } 940 return buffer; 941 } 942 } 943