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