Custom binary XML wire protocol.

We've identified that XML writing and reading uses roughly 1.5% of
all system_server CPU, and can generate many temporary objects.

Building on the recent TypedXmlSerializer/PullParser interfaces, this
change introduces new BinaryXmlSerializer/PullParser implementations
that store data using a custom binary wire protocol.  Benchmarking of
a typical packages.xml has shown this new binary approach can write
4.3x faster and read 8.5x faster, while using 2.4x less disk space:

    timeWrite_Fast_mean: 27946635
    timeWrite_Binary_mean: 6519341

    timeRead_Fast_mean: 59562531
    timeRead_Binary_mean: 7020185

A major factor in choosing to invest in this new wire protocol is
that it enables the long-tail of over 100 unique XML schemas used
across the OS internals to be transparently upgraded to gain these
benefits with only minimal changes, reducing the risks associated
with rewriting those schemas.

Finally, since the wire protocol is essentially a serialized event
stream, it's trivial to transparently convert this new protocol
into human-readable XML and vice-versa.  The tests in this change
demonstrate this translation working correctly, and future changes
will introduce new shell tools to aid development work.

Bug: 171832118
Test: atest FrameworksCoreTests:android.util.XmlTest
Test: atest FrameworksCoreTests:android.util.BinaryXmlTest
Test: atest CorePerfTests:android.util.XmlPerfTest
Change-Id: Ib9390701f09562dca952b3786622675b9c68a462
This commit is contained in:
Jeff Sharkey
2020-10-30 19:35:25 -06:00
parent b5856365b4
commit 4ccea87969
6 changed files with 1813 additions and 6 deletions

View File

@@ -0,0 +1,292 @@
/*
* Copyright (C) 2020 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 android.util;
import static org.junit.Assert.assertEquals;
import android.os.Bundle;
import android.os.Debug;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.HexDump;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class XmlPerfTest {
/**
* Since allocation measurement adds overhead, it's disabled by default for
* performance runs. It can be manually enabled to compare GC behavior.
*/
private static final boolean MEASURE_ALLOC = false;
@Rule
public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@Test
public void timeWrite_Fast() throws Exception {
doWrite(() -> Xml.newFastSerializer());
}
@Test
public void timeWrite_Binary() throws Exception {
doWrite(() -> Xml.newBinarySerializer());
}
private void doWrite(Supplier<TypedXmlSerializer> outFactory) throws Exception {
if (MEASURE_ALLOC) {
Debug.startAllocCounting();
}
int iterations = 0;
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
iterations++;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
final TypedXmlSerializer out = outFactory.get();
out.setOutput(os, StandardCharsets.UTF_8.name());
write(out);
}
}
if (MEASURE_ALLOC) {
Debug.stopAllocCounting();
final Bundle results = new Bundle();
results.putLong("threadAllocCount_mean", Debug.getThreadAllocCount() / iterations);
results.putLong("threadAllocSize_mean", Debug.getThreadAllocSize() / iterations);
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
}
}
@Test
public void timeRead_Fast() throws Exception {
doRead(() -> Xml.newFastSerializer(), () -> Xml.newFastPullParser());
}
@Test
public void timeRead_Binary() throws Exception {
doRead(() -> Xml.newBinarySerializer(), () -> Xml.newBinaryPullParser());
}
private void doRead(Supplier<TypedXmlSerializer> outFactory,
Supplier<TypedXmlPullParser> inFactory) throws Exception {
final byte[] raw;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
TypedXmlSerializer out = outFactory.get();
out.setOutput(os, StandardCharsets.UTF_8.name());
write(out);
raw = os.toByteArray();
}
if (MEASURE_ALLOC) {
Debug.startAllocCounting();
}
int iterations = 0;
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
iterations++;
try (ByteArrayInputStream is = new ByteArrayInputStream(raw)) {
TypedXmlPullParser xml = inFactory.get();
xml.setInput(is, StandardCharsets.UTF_8.name());
read(xml);
}
}
if (MEASURE_ALLOC) {
Debug.stopAllocCounting();
final Bundle results = new Bundle();
results.putLong("sizeBytes", raw.length);
results.putLong("threadAllocCount_mean", Debug.getThreadAllocCount() / iterations);
results.putLong("threadAllocSize_mean", Debug.getThreadAllocSize() / iterations);
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
} else {
final Bundle results = new Bundle();
results.putLong("sizeBytes", raw.length);
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
}
}
/**
* Not even joking, this is a typical public key blob stored in
* {@code packages.xml}.
*/
private static final byte[] KEY_BLOB = HexDump.hexStringToByteArray(""
+ "308204a830820390a003020102020900a1573d0f45bea193300d06092a864886f70d010105050030819"
+ "4310b3009060355040613025553311330110603550408130a43616c69666f726e696131163014060355"
+ "0407130d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e06035"
+ "5040b1307416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d"
+ "0109011613616e64726f696440616e64726f69642e636f6d301e170d3131303931393138343232355a1"
+ "70d3339303230343138343232355a308194310b3009060355040613025553311330110603550408130a"
+ "43616c69666f726e6961311630140603550407130d4d6f756e7461696e20566965773110300e0603550"
+ "40a1307416e64726f69643110300e060355040b1307416e64726f69643110300e06035504031307416e"
+ "64726f69643122302006092a864886f70d0109011613616e64726f696440616e64726f69642e636f6d3"
+ "0820120300d06092a864886f70d01010105000382010d00308201080282010100de1b51336afc909d8b"
+ "cca5920fcdc8940578ec5c253898930e985481cfdea75ba6fc54b1f7bb492a03d98db471ab4200103a8"
+ "314e60ee25fef6c8b83bc1b2b45b084874cffef148fa2001bb25c672b6beba50b7ac026b546da762ea2"
+ "23829a22b80ef286131f059d2c9b4ca71d54e515a8a3fd6bf5f12a2493dfc2619b337b032a7cf8bbd34"
+ "b833f2b93aeab3d325549a93272093943bb59dfc0197ae4861ff514e019b73f5cf10023ad1a032adb4b"
+ "9bbaeb4debecb4941d6a02381f1165e1ac884c1fca9525c5854dce2ad8ec839b8ce78442c16367efc07"
+ "778a337d3ca2cdf9792ac722b95d67c345f1c00976ec372f02bfcbef0262cc512a6845e71cfea0d0201"
+ "03a381fc3081f9301d0603551d0e0416041478a0fc4517fb70ff52210df33c8d32290a44b2bb3081c90"
+ "603551d230481c13081be801478a0fc4517fb70ff52210df33c8d32290a44b2bba1819aa48197308194"
+ "310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550"
+ "407130d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e060355"
+ "040b1307416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d0"
+ "109011613616e64726f696440616e64726f69642e636f6d820900a1573d0f45bea193300c0603551d13"
+ "040530030101ff300d06092a864886f70d01010505000382010100977302dfbf668d7c61841c9c78d25"
+ "63bcda1b199e95e6275a799939981416909722713531157f3cdcfea94eea7bb79ca3ca972bd8058a36a"
+ "d1919291df42d7190678d4ea47a4b9552c9dfb260e6d0d9129b44615cd641c1080580e8a990dd768c6a"
+ "b500c3b964e185874e4105109d94c5bd8c405deb3cf0f7960a563bfab58169a956372167a7e2674a04c"
+ "4f80015d8f7869a7a4139aecbbdca2abc294144ee01e4109f0e47a518363cf6e9bf41f7560e94bdd4a5"
+ "d085234796b05c7a1389adfd489feec2a107955129d7991daa49afb3d327dc0dc4fe959789372b093a8"
+ "9c8dbfa41554f771c18015a6cb242a17e04d19d55d3b4664eae12caf2a11cd2b836e");
/**
* Typical list of permissions referenced in {@code packages.xml}.
*/
private static final String[] PERMS = new String[] {
"android.permission.ACCESS_CACHE_FILESYSTEM",
"android.permission.WRITE_SETTINGS",
"android.permission.MANAGE_EXTERNAL_STORAGE",
"android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS",
"android.permission.FOREGROUND_SERVICE",
"android.permission.RECEIVE_BOOT_COMPLETED",
"android.permission.WRITE_MEDIA_STORAGE",
"android.permission.INTERNET",
"android.permission.UPDATE_DEVICE_STATS",
"android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY",
"android.permission.MANAGE_USB",
"android.permission.ACCESS_ALL_DOWNLOADS",
"android.permission.ACCESS_DOWNLOAD_MANAGER",
"android.permission.MANAGE_USERS",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_MTP",
"android.permission.INTERACT_ACROSS_USERS",
"android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS",
"android.permission.CLEAR_APP_CACHE",
"android.permission.CONNECTIVITY_INTERNAL",
"android.permission.START_ACTIVITIES_FROM_BACKGROUND",
"android.permission.QUERY_ALL_PACKAGES",
"android.permission.WAKE_LOCK",
"android.permission.UPDATE_APP_OPS_STATS",
};
/**
* Write a typical {@code packages.xml} file containing 100 applications,
* each of which defines signing key and permission information.
*/
private static void write(TypedXmlSerializer out) throws IOException {
out.startDocument(null, true);
out.startTag(null, "packages");
for (int i = 0; i < 100; i++) {
out.startTag(null, "package");
out.attribute(null, "name", "com.android.providers.media");
out.attribute(null, "codePath", "/system/priv-app/MediaProviderLegacy");
out.attribute(null, "nativeLibraryPath", "/system/priv-app/MediaProviderLegacy/lib");
out.attributeLong(null, "publicFlags", 944258629L);
out.attributeLong(null, "privateFlags", -1946152952L);
out.attributeLong(null, "ft", 1603899064000L);
out.attributeLong(null, "it", 1603899064000L);
out.attributeLong(null, "ut", 1603899064000L);
out.attributeInt(null, "version", 1024);
out.attributeInt(null, "sharedUserId", 10100);
out.attributeBoolean(null, "isOrphaned", true);
out.startTag(null, "sigs");
out.startTag(null, "cert");
out.attributeInt(null, "index", 10);
out.attributeBytesHex(null, "key", KEY_BLOB);
out.endTag(null, "cert");
out.endTag(null, "sigs");
out.startTag(null, "perms");
for (String perm : PERMS) {
out.startTag(null, "item");
out.attributeInterned(null, "name", perm);
out.attributeBoolean(null, "granted", true);
out.attributeInt(null, "flags", 0);
out.endTag(null, "item");
}
out.endTag(null, "perms");
out.endTag(null, "package");
}
out.endTag(null, "packages");
out.endDocument();
}
/**
* Read a typical {@code packages.xml} file containing 100 applications, and
* verify that data passes smell test.
*/
private static void read(TypedXmlPullParser xml) throws Exception {
int type;
int packages = 0;
int certs = 0;
int perms = 0;
while ((type = xml.next()) != XmlPullParser.END_DOCUMENT) {
final String tag = xml.getName();
if (type == XmlPullParser.START_TAG) {
if ("package".equals(tag)) {
xml.getAttributeValue(null, "name");
xml.getAttributeValue(null, "codePath");
xml.getAttributeValue(null, "nativeLibraryPath");
xml.getAttributeLong(null, "publicFlags");
assertEquals(-1946152952L, xml.getAttributeLong(null, "privateFlags"));
xml.getAttributeLong(null, "ft");
xml.getAttributeLong(null, "it");
xml.getAttributeLong(null, "ut");
xml.getAttributeInt(null, "version");
xml.getAttributeInt(null, "sharedUserId");
xml.getAttributeBoolean(null, "isOrphaned");
packages++;
} else if ("cert".equals(tag)) {
xml.getAttributeInt(null, "index");
xml.getAttributeBytesHex(null, "key");
certs++;
} else if ("item".equals(tag)) {
xml.getAttributeValue(null, "name");
xml.getAttributeBoolean(null, "granted");
xml.getAttributeInt(null, "flags");
perms++;
}
} else if (type == XmlPullParser.TEXT) {
xml.getText();
}
}
assertEquals(100, packages);
assertEquals(packages * 1, certs);
assertEquals(packages * PERMS.length, perms);
}
}

View File

@@ -17,7 +17,10 @@
package android.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.util.BinaryXmlPullParser;
import com.android.internal.util.BinaryXmlSerializer;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
@@ -120,6 +123,18 @@ public class Xml {
return XmlUtils.makeTyped(newPullParser());
}
/**
* Creates a new {@link XmlPullParser} that reads XML documents using a
* custom binary wire protocol which benchmarking has shown to be 8.5x
* faster than {@code Xml.newFastPullParser()} for a typical
* {@code packages.xml}.
*
* @hide
*/
public static @NonNull TypedXmlPullParser newBinaryPullParser() {
return new BinaryXmlPullParser();
}
/**
* Creates a new {@link XmlPullParser} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
@@ -166,6 +181,18 @@ public class Xml {
return XmlUtils.makeTyped(new FastXmlSerializer());
}
/**
* Creates a new {@link XmlSerializer} that writes XML documents using a
* custom binary wire protocol which benchmarking has shown to be 4.4x
* faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
* for a typical {@code packages.xml}.
*
* @hide
*/
public static @NonNull TypedXmlSerializer newBinarySerializer() {
return new BinaryXmlSerializer();
}
/**
* Creates a new {@link XmlSerializer} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
@@ -188,6 +215,82 @@ public class Xml {
return xml;
}
/**
* Copy the first XML document into the second document.
* <p>
* Implemented by reading all events from the given {@link XmlPullParser}
* and writing them directly to the given {@link XmlSerializer}. This can be
* useful for transparently converting between underlying wire protocols.
*
* @hide
*/
public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
throws XmlPullParserException, IOException {
// Some parsers may have already consumed the event that starts the
// document, so we manually emit that event here for consistency
if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
out.startDocument(in.getInputEncoding(), true);
}
while (true) {
final int token = in.nextToken();
switch (token) {
case XmlPullParser.START_DOCUMENT:
out.startDocument(in.getInputEncoding(), true);
break;
case XmlPullParser.END_DOCUMENT:
out.endDocument();
return;
case XmlPullParser.START_TAG:
out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
for (int i = 0; i < in.getAttributeCount(); i++) {
out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
in.getAttributeName(i), in.getAttributeValue(i));
}
break;
case XmlPullParser.END_TAG:
out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
break;
case XmlPullParser.TEXT:
out.text(in.getText());
break;
case XmlPullParser.CDSECT:
out.cdsect(in.getText());
break;
case XmlPullParser.ENTITY_REF:
out.entityRef(in.getName());
break;
case XmlPullParser.IGNORABLE_WHITESPACE:
out.ignorableWhitespace(in.getText());
break;
case XmlPullParser.PROCESSING_INSTRUCTION:
out.processingInstruction(in.getText());
break;
case XmlPullParser.COMMENT:
out.comment(in.getText());
break;
case XmlPullParser.DOCDECL:
out.docdecl(in.getText());
break;
default:
throw new IllegalStateException("Unknown token " + token);
}
}
}
/**
* Some parsers may return an empty string {@code ""} when a namespace in
* unsupported, which can confuse serializers. This method normalizes empty
* strings to be {@code null}.
*/
private static @Nullable String normalizeNamespace(@Nullable String namespace) {
if (namespace == null || namespace.isEmpty()) {
return null;
} else {
return namespace;
}
}
/**
* Supported character encodings.
*/

View File

@@ -0,0 +1,899 @@
/*
* Copyright (C) 2020 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.internal.util;
import static com.android.internal.util.BinaryXmlSerializer.ATTRIBUTE;
import static com.android.internal.util.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_BASE64;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_HEX;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_DOUBLE;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_FLOAT;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT_HEX;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG_HEX;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_NULL;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING_INTERNED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
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 final class BinaryXmlPullParser implements TypedXmlPullParser {
/**
* Default buffer size, which matches {@code FastXmlSerializer}. This should
* be kept in sync with {@link BinaryXmlPullParser}.
*/
private static final int BUFFER_SIZE = 32_768;
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 inputEncoding) throws XmlPullParserException {
if (inputEncoding != null && !StandardCharsets.UTF_8.name().equals(inputEncoding)) {
throw new UnsupportedOperationException();
}
mIn = new FastDataInput(is, BUFFER_SIZE);
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());
}
}
@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: {
break;
}
case XmlPullParser.END_DOCUMENT: {
break;
}
case XmlPullParser.START_TAG: {
mCurrentName = mIn.readInternedUTF();
resetAttributes();
break;
}
case XmlPullParser.END_TAG: {
mCurrentName = mIn.readInternedUTF();
resetAttributes();
break;
}
case XmlPullParser.TEXT:
case XmlPullParser.CDSECT:
case XmlPullParser.PROCESSING_INSTRUCTION:
case XmlPullParser.COMMENT:
case XmlPullParser.DOCDECL:
case XmlPullParser.IGNORABLE_WHITESPACE: {
mCurrentText = mIn.readUTF();
break;
}
case XmlPullParser.ENTITY_REF: {
mCurrentName = mIn.readUTF();
mCurrentText = resolveEntity(mCurrentName);
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;
}
/**
* Search through the pool of currently allocated {@link Attribute}
* instances for one that matches the given name.
*/
private @NonNull Attribute findAttribute(@NonNull String name) throws IOException {
for (int i = 0; i < mAttributeCount; i++) {
if (Objects.equals(mAttributes[i].name, name)) {
return mAttributes[i];
}
}
throw new IOException("Missing attribute " + name);
}
@Override
public String getAttributeValue(String namespace, String name) {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
try {
return findAttribute(name).getValueString();
} catch (IOException e) {
// Missing attributes default to null
return null;
}
}
@Override
public String getAttributeValue(int index) {
return mAttributes[index].getValueString();
}
@Override
public byte[] getAttributeBytesHex(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueBytesHex();
}
@Override
public byte[] getAttributeBytesBase64(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueBytesBase64();
}
@Override
public int getAttributeInt(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueInt();
}
@Override
public int getAttributeIntHex(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueIntHex();
}
@Override
public long getAttributeLong(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueLong();
}
@Override
public long getAttributeLongHex(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueLongHex();
}
@Override
public float getAttributeFloat(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueFloat();
}
@Override
public double getAttributeDouble(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueDouble();
}
@Override
public boolean getAttributeBoolean(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).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 IOException {
switch (type) {
case TYPE_NULL:
return null;
case TYPE_BYTES_HEX:
case TYPE_BYTES_BASE64:
return valueBytes;
case TYPE_STRING:
return hexStringToBytes(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public @Nullable byte[] getValueBytesBase64() throws IOException {
switch (type) {
case TYPE_NULL:
return null;
case TYPE_BYTES_HEX:
case TYPE_BYTES_BASE64:
return valueBytes;
case TYPE_STRING:
return Base64.decode(valueString, Base64.NO_WRAP);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public int getValueInt() throws IOException {
switch (type) {
case TYPE_INT:
case TYPE_INT_HEX:
return valueInt;
case TYPE_STRING:
return Integer.parseInt(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public int getValueIntHex() throws IOException {
switch (type) {
case TYPE_INT:
case TYPE_INT_HEX:
return valueInt;
case TYPE_STRING:
return Integer.parseInt(valueString, 16);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public long getValueLong() throws IOException {
switch (type) {
case TYPE_LONG:
case TYPE_LONG_HEX:
return valueLong;
case TYPE_STRING:
return Long.parseLong(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public long getValueLongHex() throws IOException {
switch (type) {
case TYPE_LONG:
case TYPE_LONG_HEX:
return valueLong;
case TYPE_STRING:
return Long.parseLong(valueString, 16);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public float getValueFloat() throws IOException {
switch (type) {
case TYPE_FLOAT:
return valueFloat;
case TYPE_STRING:
return Float.parseFloat(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public double getValueDouble() throws IOException {
switch (type) {
case TYPE_DOUBLE:
return valueDouble;
case TYPE_STRING:
return Double.parseDouble(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public boolean getValueBoolean() throws IOException {
switch (type) {
case TYPE_BOOLEAN_TRUE:
return true;
case TYPE_BOOLEAN_FALSE:
return false;
case TYPE_STRING:
if ("true".equalsIgnoreCase(valueString)) {
return true;
} else if ("false".equalsIgnoreCase(valueString)) {
return false;
} else {
throw new IOException("Invalid boolean: " + valueString);
}
default:
throw new IOException("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) throws IOException {
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 IOException("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) throws IOException {
final int length = value.length();
if (length % 2 != 0) {
throw new IOException("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;
}
}

View File

@@ -0,0 +1,396 @@
/*
* Copyright (C) 2020 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.internal.util;
import static org.xmlpull.v1.XmlPullParser.CDSECT;
import static org.xmlpull.v1.XmlPullParser.COMMENT;
import static org.xmlpull.v1.XmlPullParser.DOCDECL;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static org.xmlpull.v1.XmlPullParser.TEXT;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.TypedXmlSerializer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Serializer that writes XML documents using a custom binary wire protocol
* which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
* than {@code Xml.newFastSerializer()} 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 serializer 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 final class BinaryXmlSerializer implements TypedXmlSerializer {
/**
* The wire protocol always begins with a well-known magic value of
* {@code ABX_}, representing "Android Binary XML." The final byte is a
* version number which may be incremented as the protocol changes.
*/
static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[] { 0x41, 0x42, 0x58, 0x00 };
/**
* Internal token which represents an attribute associated with the most
* recent {@link #START_TAG} token.
*/
static final int ATTRIBUTE = 15;
static final int TYPE_NULL = 1 << 4;
static final int TYPE_STRING = 2 << 4;
static final int TYPE_STRING_INTERNED = 3 << 4;
static final int TYPE_BYTES_HEX = 4 << 4;
static final int TYPE_BYTES_BASE64 = 5 << 4;
static final int TYPE_INT = 6 << 4;
static final int TYPE_INT_HEX = 7 << 4;
static final int TYPE_LONG = 8 << 4;
static final int TYPE_LONG_HEX = 9 << 4;
static final int TYPE_FLOAT = 10 << 4;
static final int TYPE_DOUBLE = 11 << 4;
static final int TYPE_BOOLEAN_TRUE = 12 << 4;
static final int TYPE_BOOLEAN_FALSE = 13 << 4;
/**
* Default buffer size, which matches {@code FastXmlSerializer}. This should
* be kept in sync with {@link BinaryXmlPullParser}.
*/
private static final int BUFFER_SIZE = 32_768;
private FastDataOutput mOut;
/**
* Stack of tags which are currently active via {@link #startTag} and which
* haven't been terminated via {@link #endTag}.
*/
private int mTagCount = 0;
private String[] mTagNames;
/**
* Write the given token and optional {@link String} into our buffer.
*/
private void writeToken(int token, @Nullable String text) throws IOException {
if (text != null) {
mOut.writeByte(token | TYPE_STRING);
mOut.writeUTF(text);
} else {
mOut.writeByte(token | TYPE_NULL);
}
}
@Override
public void setOutput(@NonNull OutputStream os, @Nullable String encoding) throws IOException {
if (encoding != null && !StandardCharsets.UTF_8.name().equals(encoding)) {
throw new UnsupportedOperationException();
}
mOut = new FastDataOutput(os, BUFFER_SIZE);
mOut.write(PROTOCOL_MAGIC_VERSION_0);
mTagCount = 0;
mTagNames = new String[8];
}
@Override
public void setOutput(Writer writer) {
throw new UnsupportedOperationException();
}
@Override
public void flush() throws IOException {
mOut.flush();
}
@Override
public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
throws IOException {
if (encoding != null && !StandardCharsets.UTF_8.name().equals(encoding)) {
throw new UnsupportedOperationException();
}
mOut.writeByte(START_DOCUMENT | TYPE_NULL);
}
@Override
public void endDocument() throws IOException {
mOut.writeByte(END_DOCUMENT | TYPE_NULL);
flush();
}
@Override
public int getDepth() {
return mTagCount;
}
@Override
public String getNamespace() {
// Namespaces are unsupported
return XmlPullParser.NO_NAMESPACE;
}
@Override
public String getName() {
return mTagNames[mTagCount - 1];
}
@Override
public XmlSerializer startTag(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
if (mTagCount == mTagNames.length) {
mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
}
mTagNames[mTagCount++] = name;
mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
mOut.writeInternedUTF(name);
return this;
}
@Override
public XmlSerializer endTag(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mTagCount--;
mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
mOut.writeInternedUTF(name);
return this;
}
@Override
public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_STRING);
mOut.writeInternedUTF(name);
mOut.writeUTF(value);
return this;
}
@Override
public XmlSerializer attributeInterned(String namespace, String name, String value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
mOut.writeInternedUTF(name);
mOut.writeInternedUTF(value);
return this;
}
@Override
public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
@Override
public XmlSerializer attributeInt(String namespace, String name, int value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_INT);
mOut.writeInternedUTF(name);
mOut.writeInt(value);
return this;
}
@Override
public XmlSerializer attributeIntHex(String namespace, String name, int value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
mOut.writeInternedUTF(name);
mOut.writeInt(value);
return this;
}
@Override
public XmlSerializer attributeLong(String namespace, String name, long value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_LONG);
mOut.writeInternedUTF(name);
mOut.writeLong(value);
return this;
}
@Override
public XmlSerializer attributeLongHex(String namespace, String name, long value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
mOut.writeInternedUTF(name);
mOut.writeLong(value);
return this;
}
@Override
public XmlSerializer attributeFloat(String namespace, String name, float value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
mOut.writeInternedUTF(name);
mOut.writeFloat(value);
return this;
}
@Override
public XmlSerializer attributeDouble(String namespace, String name, double value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
mOut.writeInternedUTF(name);
mOut.writeDouble(value);
return this;
}
@Override
public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
if (value) {
mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
mOut.writeInternedUTF(name);
} else {
mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
mOut.writeInternedUTF(name);
}
return this;
}
@Override
public XmlSerializer text(char[] buf, int start, int len) throws IOException {
writeToken(TEXT, new String(buf, start, len));
return this;
}
@Override
public XmlSerializer text(String text) throws IOException {
writeToken(TEXT, text);
return this;
}
@Override
public void cdsect(String text) throws IOException {
writeToken(CDSECT, text);
}
@Override
public void entityRef(String text) throws IOException {
writeToken(ENTITY_REF, text);
}
@Override
public void processingInstruction(String text) throws IOException {
writeToken(PROCESSING_INSTRUCTION, text);
}
@Override
public void comment(String text) throws IOException {
writeToken(COMMENT, text);
}
@Override
public void docdecl(String text) throws IOException {
writeToken(DOCDECL, text);
}
@Override
public void ignorableWhitespace(String text) throws IOException {
writeToken(IGNORABLE_WHITESPACE, text);
}
@Override
public void setFeature(String name, boolean state) {
// Quietly handle no-op features
if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
return;
}
// 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) {
// Properties are not supported
throw new UnsupportedOperationException();
}
@Override
public Object getProperty(String name) {
// Properties are not supported
throw new UnsupportedOperationException();
}
@Override
public void setPrefix(String prefix, String namespace) {
// Prefixes are not supported
throw new UnsupportedOperationException();
}
@Override
public String getPrefix(String namespace, boolean generatePrefix) {
// Prefixes are not supported
throw new UnsupportedOperationException();
}
private static IllegalArgumentException illegalNamespace() {
throw new IllegalArgumentException("Namespaces are not supported");
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2020 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 android.util;
import static android.util.XmlTest.assertNext;
import static android.util.XmlTest.buildPersistableBundle;
import static android.util.XmlTest.doPersistableBundleRead;
import static android.util.XmlTest.doPersistableBundleWrite;
import static org.junit.Assert.assertEquals;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.os.PersistableBundle;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
@RunWith(AndroidJUnit4.class)
public class BinaryXmlTest {
/**
* Verify that we can write and read large numbers of interned
* {@link String} values.
*/
@Test
public void testLargeInterned_Binary() throws Exception {
// We're okay with the tag itself being interned
final int count = (1 << 16) - 2;
final TypedXmlSerializer out = Xml.newBinarySerializer();
final ByteArrayOutputStream os = new ByteArrayOutputStream();
out.setOutput(os, StandardCharsets.UTF_8.name());
out.startTag(null, "tag");
for (int i = 0; i < count; i++) {
out.attribute(null, "name" + i, "value");
}
out.endTag(null, "tag");
out.flush();
final TypedXmlPullParser in = Xml.newBinaryPullParser();
final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
in.setInput(is, StandardCharsets.UTF_8.name());
assertNext(in, START_TAG, "tag", 1);
assertEquals(count, in.getAttributeCount());
}
@Test
public void testTranscode_FastToBinary() throws Exception {
doTranscode(Xml.newFastSerializer(), Xml.newFastPullParser(),
Xml.newBinarySerializer(), Xml.newBinaryPullParser());
}
@Test
public void testTranscode_BinaryToFast() throws Exception {
doTranscode(Xml.newBinarySerializer(), Xml.newBinaryPullParser(),
Xml.newFastSerializer(), Xml.newFastPullParser());
}
/**
* Verify that a complex {@link PersistableBundle} can be transcoded using
* the two given formats with the original structure intact.
*/
private static void doTranscode(TypedXmlSerializer firstOut, TypedXmlPullParser firstIn,
TypedXmlSerializer secondOut, TypedXmlPullParser secondIn) throws Exception {
final PersistableBundle expected = buildPersistableBundle();
final byte[] firstRaw = doPersistableBundleWrite(firstOut, expected);
// Perform actual transcoding between the two formats
final ByteArrayInputStream is = new ByteArrayInputStream(firstRaw);
firstIn.setInput(is, StandardCharsets.UTF_8.name());
final ByteArrayOutputStream os = new ByteArrayOutputStream();
secondOut.setOutput(os, StandardCharsets.UTF_8.name());
Xml.copy(firstIn, secondOut);
// Yes, this string-based check is fragile, but kindofEquals() is broken
// when working with nested objects and arrays
final PersistableBundle actual = doPersistableBundleRead(secondIn, os.toByteArray());
assertEquals(expected.toString(), actual.toString());
}
}

View File

@@ -52,13 +52,19 @@ public class XmlTest {
Xml.newFastPullParser());
}
@Test
public void testLargeValues_Binary() throws Exception {
doLargeValues(Xml.newBinarySerializer(),
Xml.newBinaryPullParser());
}
/**
* Verify that we can write and read large {@link String} and {@code byte[]}
* without issues.
*/
private static void doLargeValues(TypedXmlSerializer out, TypedXmlPullParser in)
throws Exception {
final char[] chars = new char[32_768];
final char[] chars = new char[(1 << 16) - 1];
Arrays.fill(chars, '!');
final String string = new String(chars);
@@ -93,6 +99,12 @@ public class XmlTest {
Xml.newFastPullParser());
}
@Test
public void testPersistableBundle_Binary() throws Exception {
doPersistableBundle(Xml.newBinarySerializer(),
Xml.newBinaryPullParser());
}
/**
* Verify that a complex {@link PersistableBundle} can be serialized out and
* then parsed in with the original structure intact.
@@ -108,7 +120,7 @@ public class XmlTest {
assertEquals(expected.toString(), actual.toString());
}
private static PersistableBundle buildPersistableBundle() {
static PersistableBundle buildPersistableBundle() {
final PersistableBundle outer = new PersistableBundle();
outer.putBoolean("boolean", true);
@@ -130,7 +142,7 @@ public class XmlTest {
return outer;
}
private static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
throws Exception {
// We purposefully omit START/END_DOCUMENT events here to verify correct
// behavior of what PersistableBundle does internally
@@ -143,7 +155,7 @@ public class XmlTest {
return os.toByteArray();
}
private static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
throws Exception {
final ByteArrayInputStream is = new ByteArrayInputStream(raw);
in.setInput(is, StandardCharsets.UTF_8.name());
@@ -163,6 +175,12 @@ public class XmlTest {
Xml.newFastPullParser());
}
@Test
public void testVerify_Binary() throws Exception {
doVerify(Xml.newBinarySerializer(),
Xml.newBinaryPullParser());
}
/**
* Verify that example test data is correctly serialized and parsed
* end-to-end using the given objects.
@@ -268,7 +286,7 @@ public class XmlTest {
assertNext(in, END_DOCUMENT);
}
private static void assertNext(TypedXmlPullParser in, int token) throws Exception {
static void assertNext(TypedXmlPullParser in, int token) throws Exception {
// We're willing to skip over empty text regions, which some
// serializers emit transparently
int event;
@@ -278,7 +296,7 @@ public class XmlTest {
assertEquals("getEventType", token, in.getEventType());
}
private static void assertNext(TypedXmlPullParser in, int token, String name, int depth)
static void assertNext(TypedXmlPullParser in, int token, String name, int depth)
throws Exception {
assertNext(in, token);
assertEquals("getName", name, in.getName());