/* * 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.FastDataOutput.MAX_UNSIGNED_SHORT; 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}. *

* 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. *

* 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}. *

* This serializer has some specific limitations: *

*/ 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); if (value.length > MAX_UNSIGNED_SHORT) { throw new IOException("attributeBytesHex: input size (" + value.length + ") exceeds maximum allowed size (" + MAX_UNSIGNED_SHORT + ")"); } 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); if (value.length > MAX_UNSIGNED_SHORT) { throw new IOException("attributeBytesBase64: input size (" + value.length + ") exceeds maximum allowed size (" + MAX_UNSIGNED_SHORT + ")"); } 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"); } }