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