| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.modules.utils; |
| |
| import static com.android.modules.utils.BinaryXmlSerializer.ATTRIBUTE; |
| import static com.android.modules.utils.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BYTES_BASE64; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_BYTES_HEX; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_DOUBLE; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_FLOAT; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_INT; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_INT_HEX; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_LONG; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_LONG_HEX; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_NULL; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_STRING; |
| import static com.android.modules.utils.BinaryXmlSerializer.TYPE_STRING_INTERNED; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.text.TextUtils; |
| import android.util.Base64; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.Reader; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.Objects; |
| |
| /** |
| * Parser that reads XML documents using a custom binary wire protocol which |
| * benchmarking has shown to be 8.5x faster than {@link Xml.newFastPullParser()} |
| * for a typical {@code packages.xml}. |
| * <p> |
| * The high-level design of the wire protocol is to directly serialize the event |
| * stream, while efficiently and compactly writing strongly-typed primitives |
| * delivered through the {@link TypedXmlSerializer} interface. |
| * <p> |
| * Each serialized event is a single byte where the lower half is a normal |
| * {@link XmlPullParser} token and the upper half is an optional data type |
| * signal, such as {@link #TYPE_INT}. |
| * <p> |
| * This parser has some specific limitations: |
| * <ul> |
| * <li>Only the UTF-8 encoding is supported. |
| * <li>Variable length values, such as {@code byte[]} or {@link String}, are |
| * limited to 65,535 bytes in length. Note that {@link String} values are stored |
| * as UTF-8 on the wire. |
| * <li>Namespaces, prefixes, properties, and options are unsupported. |
| * </ul> |
| */ |
| public class BinaryXmlPullParser implements TypedXmlPullParser { |
| private FastDataInput mIn; |
| |
| private int mCurrentToken = START_DOCUMENT; |
| private int mCurrentDepth = 0; |
| private String mCurrentName; |
| private String mCurrentText; |
| |
| /** |
| * Pool of attributes parsed for the currently tag. All interactions should |
| * be done via {@link #obtainAttribute()}, {@link #findAttribute(String)}, |
| * and {@link #resetAttributes()}. |
| */ |
| private int mAttributeCount = 0; |
| private Attribute[] mAttributes; |
| |
| @Override |
| public void setInput(InputStream is, String encoding) throws XmlPullParserException { |
| if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| if (mIn != null) { |
| mIn.release(); |
| mIn = null; |
| } |
| |
| mIn = obtainFastDataInput(is); |
| |
| mCurrentToken = START_DOCUMENT; |
| mCurrentDepth = 0; |
| mCurrentName = null; |
| mCurrentText = null; |
| |
| mAttributeCount = 0; |
| mAttributes = new Attribute[8]; |
| for (int i = 0; i < mAttributes.length; i++) { |
| mAttributes[i] = new Attribute(); |
| } |
| |
| try { |
| final byte[] magic = new byte[4]; |
| mIn.readFully(magic); |
| if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) { |
| throw new IOException("Unexpected magic " + bytesToHexString(magic)); |
| } |
| |
| // We're willing to immediately consume a START_DOCUMENT if present, |
| // but we're okay if it's missing |
| if (peekNextExternalToken() == START_DOCUMENT) { |
| consumeToken(); |
| } |
| } catch (IOException e) { |
| throw new XmlPullParserException(e.toString()); |
| } |
| } |
| |
| @NonNull |
| protected FastDataInput obtainFastDataInput(@NonNull InputStream is) { |
| return FastDataInput.obtain(is); |
| } |
| |
| @Override |
| public void setInput(Reader in) throws XmlPullParserException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public int next() throws XmlPullParserException, IOException { |
| while (true) { |
| final int token = nextToken(); |
| switch (token) { |
| case START_TAG: |
| case END_TAG: |
| case END_DOCUMENT: |
| return token; |
| case TEXT: |
| consumeAdditionalText(); |
| // Per interface docs, empty text regions are skipped |
| if (mCurrentText == null || mCurrentText.length() == 0) { |
| continue; |
| } else { |
| return TEXT; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public int nextToken() throws XmlPullParserException, IOException { |
| if (mCurrentToken == XmlPullParser.END_TAG) { |
| mCurrentDepth--; |
| } |
| |
| int token; |
| try { |
| token = peekNextExternalToken(); |
| consumeToken(); |
| } catch (EOFException e) { |
| token = END_DOCUMENT; |
| } |
| switch (token) { |
| case XmlPullParser.START_TAG: |
| // We need to peek forward to find the next external token so |
| // that we parse all pending INTERNAL_ATTRIBUTE tokens |
| peekNextExternalToken(); |
| mCurrentDepth++; |
| break; |
| } |
| mCurrentToken = token; |
| return token; |
| } |
| |
| /** |
| * Peek at the next "external" token without consuming it. |
| * <p> |
| * External tokens, such as {@link #START_TAG}, are expected by typical |
| * {@link XmlPullParser} clients. In contrast, internal tokens, such as |
| * {@link #ATTRIBUTE}, are not expected by typical clients. |
| * <p> |
| * This method consumes any internal events until it reaches the next |
| * external event. |
| */ |
| private int peekNextExternalToken() throws IOException, XmlPullParserException { |
| while (true) { |
| final int token = peekNextToken(); |
| switch (token) { |
| case ATTRIBUTE: |
| consumeToken(); |
| continue; |
| default: |
| return token; |
| } |
| } |
| } |
| |
| /** |
| * Peek at the next token in the underlying stream without consuming it. |
| */ |
| private int peekNextToken() throws IOException { |
| return mIn.peekByte() & 0x0f; |
| } |
| |
| /** |
| * Parse and consume the next token in the underlying stream. |
| */ |
| private void consumeToken() throws IOException, XmlPullParserException { |
| final int event = mIn.readByte(); |
| final int token = event & 0x0f; |
| final int type = event & 0xf0; |
| switch (token) { |
| case ATTRIBUTE: { |
| final Attribute attr = obtainAttribute(); |
| attr.name = mIn.readInternedUTF(); |
| attr.type = type; |
| switch (type) { |
| case TYPE_NULL: |
| case TYPE_BOOLEAN_TRUE: |
| case TYPE_BOOLEAN_FALSE: |
| // Nothing extra to fill in |
| break; |
| case TYPE_STRING: |
| attr.valueString = mIn.readUTF(); |
| break; |
| case TYPE_STRING_INTERNED: |
| attr.valueString = mIn.readInternedUTF(); |
| break; |
| case TYPE_BYTES_HEX: |
| case TYPE_BYTES_BASE64: |
| final int len = mIn.readUnsignedShort(); |
| final byte[] res = new byte[len]; |
| mIn.readFully(res); |
| attr.valueBytes = res; |
| break; |
| case TYPE_INT: |
| case TYPE_INT_HEX: |
| attr.valueInt = mIn.readInt(); |
| break; |
| case TYPE_LONG: |
| case TYPE_LONG_HEX: |
| attr.valueLong = mIn.readLong(); |
| break; |
| case TYPE_FLOAT: |
| attr.valueFloat = mIn.readFloat(); |
| break; |
| case TYPE_DOUBLE: |
| attr.valueDouble = mIn.readDouble(); |
| break; |
| default: |
| throw new IOException("Unexpected data type " + type); |
| } |
| break; |
| } |
| case XmlPullParser.START_DOCUMENT: { |
| mCurrentName = null; |
| mCurrentText = null; |
| if (mAttributeCount > 0) resetAttributes(); |
| break; |
| } |
| case XmlPullParser.END_DOCUMENT: { |
| mCurrentName = null; |
| mCurrentText = null; |
| if (mAttributeCount > 0) resetAttributes(); |
| break; |
| } |
| case XmlPullParser.START_TAG: { |
| mCurrentName = mIn.readInternedUTF(); |
| mCurrentText = null; |
| if (mAttributeCount > 0) resetAttributes(); |
| break; |
| } |
| case XmlPullParser.END_TAG: { |
| mCurrentName = mIn.readInternedUTF(); |
| mCurrentText = null; |
| if (mAttributeCount > 0) resetAttributes(); |
| break; |
| } |
| case XmlPullParser.TEXT: |
| case XmlPullParser.CDSECT: |
| case XmlPullParser.PROCESSING_INSTRUCTION: |
| case XmlPullParser.COMMENT: |
| case XmlPullParser.DOCDECL: |
| case XmlPullParser.IGNORABLE_WHITESPACE: { |
| mCurrentName = null; |
| mCurrentText = mIn.readUTF(); |
| if (mAttributeCount > 0) resetAttributes(); |
| break; |
| } |
| case XmlPullParser.ENTITY_REF: { |
| mCurrentName = mIn.readUTF(); |
| mCurrentText = resolveEntity(mCurrentName); |
| if (mAttributeCount > 0) resetAttributes(); |
| break; |
| } |
| default: { |
| throw new IOException("Unknown token " + token + " with type " + type); |
| } |
| } |
| } |
| |
| /** |
| * When the current tag is {@link #TEXT}, consume all subsequent "text" |
| * events, as described by {@link #next}. When finished, the current event |
| * will still be {@link #TEXT}. |
| */ |
| private void consumeAdditionalText() throws IOException, XmlPullParserException { |
| String combinedText = mCurrentText; |
| while (true) { |
| final int token = peekNextExternalToken(); |
| switch (token) { |
| case COMMENT: |
| case PROCESSING_INSTRUCTION: |
| // Quietly consumed |
| consumeToken(); |
| break; |
| case TEXT: |
| case CDSECT: |
| case ENTITY_REF: |
| // Additional text regions collected |
| consumeToken(); |
| combinedText += mCurrentText; |
| break; |
| default: |
| // Next token is something non-text, so wrap things up |
| mCurrentToken = TEXT; |
| mCurrentName = null; |
| mCurrentText = combinedText; |
| return; |
| } |
| } |
| } |
| |
| static @NonNull String resolveEntity(@NonNull String entity) |
| throws XmlPullParserException { |
| switch (entity) { |
| case "lt": return "<"; |
| case "gt": return ">"; |
| case "amp": return "&"; |
| case "apos": return "'"; |
| case "quot": return "\""; |
| } |
| if (entity.length() > 1 && entity.charAt(0) == '#') { |
| final char c = (char) Integer.parseInt(entity.substring(1)); |
| return new String(new char[] { c }); |
| } |
| throw new XmlPullParserException("Unknown entity " + entity); |
| } |
| |
| @Override |
| public void require(int type, String namespace, String name) |
| throws XmlPullParserException, IOException { |
| if (namespace != null && !namespace.isEmpty()) throw illegalNamespace(); |
| if (mCurrentToken != type || !Objects.equals(mCurrentName, name)) { |
| throw new XmlPullParserException(getPositionDescription()); |
| } |
| } |
| |
| @Override |
| public String nextText() throws XmlPullParserException, IOException { |
| if (getEventType() != START_TAG) { |
| throw new XmlPullParserException(getPositionDescription()); |
| } |
| int eventType = next(); |
| if (eventType == TEXT) { |
| String result = getText(); |
| eventType = next(); |
| if (eventType != END_TAG) { |
| throw new XmlPullParserException(getPositionDescription()); |
| } |
| return result; |
| } else if (eventType == END_TAG) { |
| return ""; |
| } else { |
| throw new XmlPullParserException(getPositionDescription()); |
| } |
| } |
| |
| @Override |
| public int nextTag() throws XmlPullParserException, IOException { |
| int eventType = next(); |
| if (eventType == TEXT && isWhitespace()) { |
| eventType = next(); |
| } |
| if (eventType != START_TAG && eventType != END_TAG) { |
| throw new XmlPullParserException(getPositionDescription()); |
| } |
| return eventType; |
| } |
| |
| /** |
| * Allocate and return a new {@link Attribute} associated with the tag being |
| * currently processed. This will automatically grow the internal pool as |
| * needed. |
| */ |
| private @NonNull Attribute obtainAttribute() { |
| if (mAttributeCount == mAttributes.length) { |
| final int before = mAttributes.length; |
| final int after = before + (before >> 1); |
| mAttributes = Arrays.copyOf(mAttributes, after); |
| for (int i = before; i < after; i++) { |
| mAttributes[i] = new Attribute(); |
| } |
| } |
| return mAttributes[mAttributeCount++]; |
| } |
| |
| /** |
| * Clear any {@link Attribute} instances that have been allocated by |
| * {@link #obtainAttribute()}, returning them into the pool for recycling. |
| */ |
| private void resetAttributes() { |
| for (int i = 0; i < mAttributeCount; i++) { |
| mAttributes[i].reset(); |
| } |
| mAttributeCount = 0; |
| } |
| |
| @Override |
| public int getAttributeIndex(String namespace, String name) { |
| if (namespace != null && !namespace.isEmpty()) throw illegalNamespace(); |
| for (int i = 0; i < mAttributeCount; i++) { |
| if (Objects.equals(mAttributes[i].name, name)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| @Override |
| public String getAttributeValue(String namespace, String name) { |
| final int index = getAttributeIndex(namespace, name); |
| if (index != -1) { |
| return mAttributes[index].getValueString(); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public String getAttributeValue(int index) { |
| return mAttributes[index].getValueString(); |
| } |
| |
| @Override |
| public byte[] getAttributeBytesHex(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueBytesHex(); |
| } |
| |
| @Override |
| public byte[] getAttributeBytesBase64(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueBytesBase64(); |
| } |
| |
| @Override |
| public int getAttributeInt(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueInt(); |
| } |
| |
| @Override |
| public int getAttributeIntHex(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueIntHex(); |
| } |
| |
| @Override |
| public long getAttributeLong(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueLong(); |
| } |
| |
| @Override |
| public long getAttributeLongHex(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueLongHex(); |
| } |
| |
| @Override |
| public float getAttributeFloat(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueFloat(); |
| } |
| |
| @Override |
| public double getAttributeDouble(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueDouble(); |
| } |
| |
| @Override |
| public boolean getAttributeBoolean(int index) throws XmlPullParserException { |
| return mAttributes[index].getValueBoolean(); |
| } |
| |
| @Override |
| public String getText() { |
| return mCurrentText; |
| } |
| |
| @Override |
| public char[] getTextCharacters(int[] holderForStartAndLength) { |
| final char[] chars = mCurrentText.toCharArray(); |
| holderForStartAndLength[0] = 0; |
| holderForStartAndLength[1] = chars.length; |
| return chars; |
| } |
| |
| @Override |
| public String getInputEncoding() { |
| return StandardCharsets.UTF_8.name(); |
| } |
| |
| @Override |
| public int getDepth() { |
| return mCurrentDepth; |
| } |
| |
| @Override |
| public String getPositionDescription() { |
| // Not very helpful, but it's the best information we have |
| return "Token " + mCurrentToken + " at depth " + mCurrentDepth; |
| } |
| |
| @Override |
| public int getLineNumber() { |
| return -1; |
| } |
| |
| @Override |
| public int getColumnNumber() { |
| return -1; |
| } |
| |
| @Override |
| public boolean isWhitespace() throws XmlPullParserException { |
| switch (mCurrentToken) { |
| case IGNORABLE_WHITESPACE: |
| return true; |
| case TEXT: |
| case CDSECT: |
| return !TextUtils.isGraphic(mCurrentText); |
| default: |
| throw new XmlPullParserException("Not applicable for token " + mCurrentToken); |
| } |
| } |
| |
| @Override |
| public String getNamespace() { |
| switch (mCurrentToken) { |
| case START_TAG: |
| case END_TAG: |
| // Namespaces are unsupported |
| return NO_NAMESPACE; |
| default: |
| return null; |
| } |
| } |
| |
| @Override |
| public String getName() { |
| return mCurrentName; |
| } |
| |
| @Override |
| public String getPrefix() { |
| // Prefixes are not supported |
| return null; |
| } |
| |
| @Override |
| public boolean isEmptyElementTag() throws XmlPullParserException { |
| switch (mCurrentToken) { |
| case START_TAG: |
| try { |
| return (peekNextExternalToken() == END_TAG); |
| } catch (IOException e) { |
| throw new XmlPullParserException(e.toString()); |
| } |
| default: |
| throw new XmlPullParserException("Not at START_TAG"); |
| } |
| } |
| |
| @Override |
| public int getAttributeCount() { |
| return mAttributeCount; |
| } |
| |
| @Override |
| public String getAttributeNamespace(int index) { |
| // Namespaces are unsupported |
| return NO_NAMESPACE; |
| } |
| |
| @Override |
| public String getAttributeName(int index) { |
| return mAttributes[index].name; |
| } |
| |
| @Override |
| public String getAttributePrefix(int index) { |
| // Prefixes are not supported |
| return null; |
| } |
| |
| @Override |
| public String getAttributeType(int index) { |
| // Validation is not supported |
| return "CDATA"; |
| } |
| |
| @Override |
| public boolean isAttributeDefault(int index) { |
| // Validation is not supported |
| return false; |
| } |
| |
| @Override |
| public int getEventType() throws XmlPullParserException { |
| return mCurrentToken; |
| } |
| |
| @Override |
| public int getNamespaceCount(int depth) throws XmlPullParserException { |
| // Namespaces are unsupported |
| return 0; |
| } |
| |
| @Override |
| public String getNamespacePrefix(int pos) throws XmlPullParserException { |
| // Namespaces are unsupported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public String getNamespaceUri(int pos) throws XmlPullParserException { |
| // Namespaces are unsupported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public String getNamespace(String prefix) { |
| // Namespaces are unsupported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void defineEntityReplacementText(String entityName, String replacementText) |
| throws XmlPullParserException { |
| // Custom entities are not supported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void setFeature(String name, boolean state) throws XmlPullParserException { |
| // Features are not supported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public boolean getFeature(String name) { |
| // Features are not supported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void setProperty(String name, Object value) throws XmlPullParserException { |
| // Properties are not supported |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public Object getProperty(String name) { |
| // Properties are not supported |
| throw new UnsupportedOperationException(); |
| } |
| |
| private static IllegalArgumentException illegalNamespace() { |
| throw new IllegalArgumentException("Namespaces are not supported"); |
| } |
| |
| /** |
| * Holder representing a single attribute. This design enables object |
| * recycling without resorting to autoboxing. |
| * <p> |
| * To support conversion between human-readable XML and binary XML, the |
| * various accessor methods will transparently convert from/to |
| * human-readable values when needed. |
| */ |
| private static class Attribute { |
| public String name; |
| public int type; |
| |
| public String valueString; |
| public byte[] valueBytes; |
| public int valueInt; |
| public long valueLong; |
| public float valueFloat; |
| public double valueDouble; |
| |
| public void reset() { |
| name = null; |
| valueString = null; |
| valueBytes = null; |
| } |
| |
| public @Nullable String getValueString() { |
| switch (type) { |
| case TYPE_NULL: |
| return null; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| return valueString; |
| case TYPE_BYTES_HEX: |
| return bytesToHexString(valueBytes); |
| case TYPE_BYTES_BASE64: |
| return Base64.encodeToString(valueBytes, Base64.NO_WRAP); |
| case TYPE_INT: |
| return Integer.toString(valueInt); |
| case TYPE_INT_HEX: |
| return Integer.toString(valueInt, 16); |
| case TYPE_LONG: |
| return Long.toString(valueLong); |
| case TYPE_LONG_HEX: |
| return Long.toString(valueLong, 16); |
| case TYPE_FLOAT: |
| return Float.toString(valueFloat); |
| case TYPE_DOUBLE: |
| return Double.toString(valueDouble); |
| case TYPE_BOOLEAN_TRUE: |
| return "true"; |
| case TYPE_BOOLEAN_FALSE: |
| return "false"; |
| default: |
| // Unknown data type; null is the best we can offer |
| return null; |
| } |
| } |
| |
| public @Nullable byte[] getValueBytesHex() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_NULL: |
| return null; |
| case TYPE_BYTES_HEX: |
| case TYPE_BYTES_BASE64: |
| return valueBytes; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return hexStringToBytes(valueString); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public @Nullable byte[] getValueBytesBase64() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_NULL: |
| return null; |
| case TYPE_BYTES_HEX: |
| case TYPE_BYTES_BASE64: |
| return valueBytes; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Base64.decode(valueString, Base64.NO_WRAP); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public int getValueInt() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_INT: |
| case TYPE_INT_HEX: |
| return valueInt; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Integer.parseInt(valueString); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public int getValueIntHex() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_INT: |
| case TYPE_INT_HEX: |
| return valueInt; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Integer.parseInt(valueString, 16); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public long getValueLong() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_LONG: |
| case TYPE_LONG_HEX: |
| return valueLong; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Long.parseLong(valueString); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public long getValueLongHex() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_LONG: |
| case TYPE_LONG_HEX: |
| return valueLong; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Long.parseLong(valueString, 16); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public float getValueFloat() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_FLOAT: |
| return valueFloat; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Float.parseFloat(valueString); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public double getValueDouble() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_DOUBLE: |
| return valueDouble; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| try { |
| return Double.parseDouble(valueString); |
| } catch (Exception e) { |
| throw new XmlPullParserException("Invalid attribute " + name + ": " + e); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| |
| public boolean getValueBoolean() throws XmlPullParserException { |
| switch (type) { |
| case TYPE_BOOLEAN_TRUE: |
| return true; |
| case TYPE_BOOLEAN_FALSE: |
| return false; |
| case TYPE_STRING: |
| case TYPE_STRING_INTERNED: |
| if ("true".equalsIgnoreCase(valueString)) { |
| return true; |
| } else if ("false".equalsIgnoreCase(valueString)) { |
| return false; |
| } else { |
| throw new XmlPullParserException( |
| "Invalid attribute " + name + ": " + valueString); |
| } |
| default: |
| throw new XmlPullParserException("Invalid conversion from " + type); |
| } |
| } |
| } |
| |
| // NOTE: To support unbundled clients, we include an inlined copy |
| // of hex conversion logic from HexDump below |
| private final static char[] HEX_DIGITS = |
| { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; |
| |
| private static int toByte(char c) { |
| if (c >= '0' && c <= '9') return (c - '0'); |
| if (c >= 'A' && c <= 'F') return (c - 'A' + 10); |
| if (c >= 'a' && c <= 'f') return (c - 'a' + 10); |
| throw new IllegalArgumentException("Invalid hex char '" + c + "'"); |
| } |
| |
| static String bytesToHexString(byte[] value) { |
| final int length = value.length; |
| final char[] buf = new char[length * 2]; |
| int bufIndex = 0; |
| for (int i = 0; i < length; i++) { |
| byte b = value[i]; |
| buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F]; |
| buf[bufIndex++] = HEX_DIGITS[b & 0x0F]; |
| } |
| return new String(buf); |
| } |
| |
| static byte[] hexStringToBytes(String value) { |
| final int length = value.length(); |
| if (length % 2 != 0) { |
| throw new IllegalArgumentException("Invalid hex length " + length); |
| } |
| byte[] buffer = new byte[length / 2]; |
| for (int i = 0; i < length; i += 2) { |
| buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4) |
| | toByte(value.charAt(i + 1))); |
| } |
| return buffer; |
| } |
| } |