1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.modules.utils;
18 
19 import static com.android.modules.utils.FastDataOutput.MAX_UNSIGNED_SHORT;
20 
21 import static org.xmlpull.v1.XmlPullParser.CDSECT;
22 import static org.xmlpull.v1.XmlPullParser.COMMENT;
23 import static org.xmlpull.v1.XmlPullParser.DOCDECL;
24 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
25 import static org.xmlpull.v1.XmlPullParser.END_TAG;
26 import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
27 import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
28 import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
29 import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
30 import static org.xmlpull.v1.XmlPullParser.START_TAG;
31 import static org.xmlpull.v1.XmlPullParser.TEXT;
32 
33 import android.annotation.NonNull;
34 import android.annotation.Nullable;
35 
36 import org.xmlpull.v1.XmlPullParser;
37 import org.xmlpull.v1.XmlSerializer;
38 
39 import java.io.IOException;
40 import java.io.OutputStream;
41 import java.io.Writer;
42 import java.nio.charset.StandardCharsets;
43 import java.util.Arrays;
44 
45 /**
46  * Serializer that writes XML documents using a custom binary wire protocol
47  * which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
48  * than {@code Xml.newFastSerializer()} for a typical {@code packages.xml}.
49  * <p>
50  * The high-level design of the wire protocol is to directly serialize the event
51  * stream, while efficiently and compactly writing strongly-typed primitives
52  * delivered through the {@link TypedXmlSerializer} interface.
53  * <p>
54  * Each serialized event is a single byte where the lower half is a normal
55  * {@link XmlPullParser} token and the upper half is an optional data type
56  * signal, such as {@link #TYPE_INT}.
57  * <p>
58  * This serializer has some specific limitations:
59  * <ul>
60  * <li>Only the UTF-8 encoding is supported.
61  * <li>Variable length values, such as {@code byte[]} or {@link String}, are
62  * limited to 65,535 bytes in length. Note that {@link String} values are stored
63  * as UTF-8 on the wire.
64  * <li>Namespaces, prefixes, properties, and options are unsupported.
65  * </ul>
66  */
67 public class BinaryXmlSerializer implements TypedXmlSerializer {
68     /**
69      * The wire protocol always begins with a well-known magic value of
70      * {@code ABX_}, representing "Android Binary XML." The final byte is a
71      * version number which may be incremented as the protocol changes.
72      */
73     public static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[] { 0x41, 0x42, 0x58, 0x00 };
74 
75     /**
76      * Internal token which represents an attribute associated with the most
77      * recent {@link #START_TAG} token.
78      */
79     static final int ATTRIBUTE = 15;
80 
81     static final int TYPE_NULL = 1 << 4;
82     static final int TYPE_STRING = 2 << 4;
83     static final int TYPE_STRING_INTERNED = 3 << 4;
84     static final int TYPE_BYTES_HEX = 4 << 4;
85     static final int TYPE_BYTES_BASE64 = 5 << 4;
86     static final int TYPE_INT = 6 << 4;
87     static final int TYPE_INT_HEX = 7 << 4;
88     static final int TYPE_LONG = 8 << 4;
89     static final int TYPE_LONG_HEX = 9 << 4;
90     static final int TYPE_FLOAT = 10 << 4;
91     static final int TYPE_DOUBLE = 11 << 4;
92     static final int TYPE_BOOLEAN_TRUE = 12 << 4;
93     static final int TYPE_BOOLEAN_FALSE = 13 << 4;
94 
95     private FastDataOutput mOut;
96 
97     /**
98      * Stack of tags which are currently active via {@link #startTag} and which
99      * haven't been terminated via {@link #endTag}.
100      */
101     private int mTagCount = 0;
102     private String[] mTagNames;
103 
104     /**
105      * Write the given token and optional {@link String} into our buffer.
106      */
writeToken(int token, @Nullable String text)107     private void writeToken(int token, @Nullable String text) throws IOException {
108         if (text != null) {
109             mOut.writeByte(token | TYPE_STRING);
110             mOut.writeUTF(text);
111         } else {
112             mOut.writeByte(token | TYPE_NULL);
113         }
114     }
115 
116     @Override
setOutput(@onNull OutputStream os, @Nullable String encoding)117     public void setOutput(@NonNull OutputStream os, @Nullable String encoding) throws IOException {
118         if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
119             throw new UnsupportedOperationException();
120         }
121 
122         mOut = obtainFastDataOutput(os);
123         mOut.write(PROTOCOL_MAGIC_VERSION_0);
124 
125         mTagCount = 0;
126         mTagNames = new String[8];
127     }
128 
129     @NonNull
obtainFastDataOutput(@onNull OutputStream os)130     protected FastDataOutput obtainFastDataOutput(@NonNull OutputStream os) {
131         return FastDataOutput.obtain(os);
132     }
133 
134     @Override
setOutput(Writer writer)135     public void setOutput(Writer writer) {
136         throw new UnsupportedOperationException();
137     }
138 
139     @Override
flush()140     public void flush() throws IOException {
141         if (mOut != null) {
142             mOut.flush();
143         }
144     }
145 
146     @Override
startDocument(@ullable String encoding, @Nullable Boolean standalone)147     public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
148             throws IOException {
149         if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
150             throw new UnsupportedOperationException();
151         }
152         if (standalone != null && !standalone) {
153             throw new UnsupportedOperationException();
154         }
155         mOut.writeByte(START_DOCUMENT | TYPE_NULL);
156     }
157 
158     @Override
endDocument()159     public void endDocument() throws IOException {
160         mOut.writeByte(END_DOCUMENT | TYPE_NULL);
161         flush();
162 
163         mOut.release();
164         mOut = null;
165     }
166 
167     @Override
getDepth()168     public int getDepth() {
169         return mTagCount;
170     }
171 
172     @Override
getNamespace()173     public String getNamespace() {
174         // Namespaces are unsupported
175         return XmlPullParser.NO_NAMESPACE;
176     }
177 
178     @Override
getName()179     public String getName() {
180         return mTagNames[mTagCount - 1];
181     }
182 
183     @Override
startTag(String namespace, String name)184     public XmlSerializer startTag(String namespace, String name) throws IOException {
185         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
186         if (mTagCount == mTagNames.length) {
187             mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
188         }
189         mTagNames[mTagCount++] = name;
190         mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
191         mOut.writeInternedUTF(name);
192         return this;
193     }
194 
195     @Override
endTag(String namespace, String name)196     public XmlSerializer endTag(String namespace, String name) throws IOException {
197         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
198         mTagCount--;
199         mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
200         mOut.writeInternedUTF(name);
201         return this;
202     }
203 
204     @Override
attribute(String namespace, String name, String value)205     public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
206         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
207         mOut.writeByte(ATTRIBUTE | TYPE_STRING);
208         mOut.writeInternedUTF(name);
209         mOut.writeUTF(value);
210         return this;
211     }
212 
213     @Override
attributeInterned(String namespace, String name, String value)214     public XmlSerializer attributeInterned(String namespace, String name, String value)
215             throws IOException {
216         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
217         mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
218         mOut.writeInternedUTF(name);
219         mOut.writeInternedUTF(value);
220         return this;
221     }
222 
223     @Override
attributeBytesHex(String namespace, String name, byte[] value)224     public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
225             throws IOException {
226         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
227         mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
228         mOut.writeInternedUTF(name);
229         if (value.length > MAX_UNSIGNED_SHORT) {
230             throw new IOException("attributeBytesHex: input size (" + value.length
231                     + ") exceeds maximum allowed size (" + MAX_UNSIGNED_SHORT + ")");
232         }
233         mOut.writeShort(value.length);
234         mOut.write(value);
235         return this;
236     }
237 
238     @Override
attributeBytesBase64(String namespace, String name, byte[] value)239     public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
240             throws IOException {
241         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
242         mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
243         mOut.writeInternedUTF(name);
244         if (value.length > MAX_UNSIGNED_SHORT) {
245             throw new IOException("attributeBytesBase64: input size (" + value.length
246                     + ") exceeds maximum allowed size (" + MAX_UNSIGNED_SHORT + ")");
247         }
248         mOut.writeShort(value.length);
249         mOut.write(value);
250         return this;
251     }
252 
253     @Override
attributeInt(String namespace, String name, int value)254     public XmlSerializer attributeInt(String namespace, String name, int value)
255             throws IOException {
256         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
257         mOut.writeByte(ATTRIBUTE | TYPE_INT);
258         mOut.writeInternedUTF(name);
259         mOut.writeInt(value);
260         return this;
261     }
262 
263     @Override
attributeIntHex(String namespace, String name, int value)264     public XmlSerializer attributeIntHex(String namespace, String name, int value)
265             throws IOException {
266         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
267         mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
268         mOut.writeInternedUTF(name);
269         mOut.writeInt(value);
270         return this;
271     }
272 
273     @Override
attributeLong(String namespace, String name, long value)274     public XmlSerializer attributeLong(String namespace, String name, long value)
275             throws IOException {
276         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
277         mOut.writeByte(ATTRIBUTE | TYPE_LONG);
278         mOut.writeInternedUTF(name);
279         mOut.writeLong(value);
280         return this;
281     }
282 
283     @Override
attributeLongHex(String namespace, String name, long value)284     public XmlSerializer attributeLongHex(String namespace, String name, long value)
285             throws IOException {
286         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
287         mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
288         mOut.writeInternedUTF(name);
289         mOut.writeLong(value);
290         return this;
291     }
292 
293     @Override
attributeFloat(String namespace, String name, float value)294     public XmlSerializer attributeFloat(String namespace, String name, float value)
295             throws IOException {
296         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
297         mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
298         mOut.writeInternedUTF(name);
299         mOut.writeFloat(value);
300         return this;
301     }
302 
303     @Override
attributeDouble(String namespace, String name, double value)304     public XmlSerializer attributeDouble(String namespace, String name, double value)
305             throws IOException {
306         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
307         mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
308         mOut.writeInternedUTF(name);
309         mOut.writeDouble(value);
310         return this;
311     }
312 
313     @Override
attributeBoolean(String namespace, String name, boolean value)314     public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
315             throws IOException {
316         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
317         if (value) {
318             mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
319             mOut.writeInternedUTF(name);
320         } else {
321             mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
322             mOut.writeInternedUTF(name);
323         }
324         return this;
325     }
326 
327     @Override
text(char[] buf, int start, int len)328     public XmlSerializer text(char[] buf, int start, int len) throws IOException {
329         writeToken(TEXT, new String(buf, start, len));
330         return this;
331     }
332 
333     @Override
text(String text)334     public XmlSerializer text(String text) throws IOException {
335         writeToken(TEXT, text);
336         return this;
337     }
338 
339     @Override
cdsect(String text)340     public void cdsect(String text) throws IOException {
341         writeToken(CDSECT, text);
342     }
343 
344     @Override
entityRef(String text)345     public void entityRef(String text) throws IOException {
346         writeToken(ENTITY_REF, text);
347     }
348 
349     @Override
processingInstruction(String text)350     public void processingInstruction(String text) throws IOException {
351         writeToken(PROCESSING_INSTRUCTION, text);
352     }
353 
354     @Override
comment(String text)355     public void comment(String text) throws IOException {
356         writeToken(COMMENT, text);
357     }
358 
359     @Override
docdecl(String text)360     public void docdecl(String text) throws IOException {
361         writeToken(DOCDECL, text);
362     }
363 
364     @Override
ignorableWhitespace(String text)365     public void ignorableWhitespace(String text) throws IOException {
366         writeToken(IGNORABLE_WHITESPACE, text);
367     }
368 
369     @Override
setFeature(String name, boolean state)370     public void setFeature(String name, boolean state) {
371         // Quietly handle no-op features
372         if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
373             return;
374         }
375         // Features are not supported
376         throw new UnsupportedOperationException();
377     }
378 
379     @Override
getFeature(String name)380     public boolean getFeature(String name) {
381         // Features are not supported
382         throw new UnsupportedOperationException();
383     }
384 
385     @Override
setProperty(String name, Object value)386     public void setProperty(String name, Object value) {
387         // Properties are not supported
388         throw new UnsupportedOperationException();
389     }
390 
391     @Override
getProperty(String name)392     public Object getProperty(String name) {
393         // Properties are not supported
394         throw new UnsupportedOperationException();
395     }
396 
397     @Override
setPrefix(String prefix, String namespace)398     public void setPrefix(String prefix, String namespace) {
399         // Prefixes are not supported
400         throw new UnsupportedOperationException();
401     }
402 
403     @Override
getPrefix(String namespace, boolean generatePrefix)404     public String getPrefix(String namespace, boolean generatePrefix) {
405         // Prefixes are not supported
406         throw new UnsupportedOperationException();
407     }
408 
illegalNamespace()409     private static IllegalArgumentException illegalNamespace() {
410         throw new IllegalArgumentException("Namespaces are not supported");
411     }
412 }
413