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