943 lines
32 KiB
Java
943 lines
32 KiB
Java
/*
|
|
* 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;
|
|
}
|
|
}
|