403 lines
13 KiB
Java
403 lines
13 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 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 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 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.
|
|
*/
|
|
public 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;
|
|
|
|
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().equalsIgnoreCase(encoding)) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
mOut = obtainFastDataOutput(os);
|
|
mOut.write(PROTOCOL_MAGIC_VERSION_0);
|
|
|
|
mTagCount = 0;
|
|
mTagNames = new String[8];
|
|
}
|
|
|
|
@NonNull
|
|
protected FastDataOutput obtainFastDataOutput(@NonNull OutputStream os) {
|
|
return FastDataOutput.obtain(os);
|
|
}
|
|
|
|
@Override
|
|
public void setOutput(Writer writer) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public void flush() throws IOException {
|
|
if (mOut != null) {
|
|
mOut.flush();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
|
|
throws IOException {
|
|
if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
if (standalone != null && !standalone) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
mOut.writeByte(START_DOCUMENT | TYPE_NULL);
|
|
}
|
|
|
|
@Override
|
|
public void endDocument() throws IOException {
|
|
mOut.writeByte(END_DOCUMENT | TYPE_NULL);
|
|
flush();
|
|
|
|
mOut.release();
|
|
mOut = null;
|
|
}
|
|
|
|
@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");
|
|
}
|
|
}
|