• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = FastDataOutput.obtain(os);
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         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         mOut.writeShort(value.length);
230         mOut.write(value);
231         return this;
232     }
233 
234     @Override
attributeBytesBase64(String namespace, String name, byte[] value)235     public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
236             throws IOException {
237         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
238         mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
239         mOut.writeInternedUTF(name);
240         mOut.writeShort(value.length);
241         mOut.write(value);
242         return this;
243     }
244 
245     @Override
attributeInt(String namespace, String name, int value)246     public XmlSerializer attributeInt(String namespace, String name, int value)
247             throws IOException {
248         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
249         mOut.writeByte(ATTRIBUTE | TYPE_INT);
250         mOut.writeInternedUTF(name);
251         mOut.writeInt(value);
252         return this;
253     }
254 
255     @Override
attributeIntHex(String namespace, String name, int value)256     public XmlSerializer attributeIntHex(String namespace, String name, int value)
257             throws IOException {
258         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
259         mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
260         mOut.writeInternedUTF(name);
261         mOut.writeInt(value);
262         return this;
263     }
264 
265     @Override
attributeLong(String namespace, String name, long value)266     public XmlSerializer attributeLong(String namespace, String name, long value)
267             throws IOException {
268         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
269         mOut.writeByte(ATTRIBUTE | TYPE_LONG);
270         mOut.writeInternedUTF(name);
271         mOut.writeLong(value);
272         return this;
273     }
274 
275     @Override
attributeLongHex(String namespace, String name, long value)276     public XmlSerializer attributeLongHex(String namespace, String name, long value)
277             throws IOException {
278         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
279         mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
280         mOut.writeInternedUTF(name);
281         mOut.writeLong(value);
282         return this;
283     }
284 
285     @Override
attributeFloat(String namespace, String name, float value)286     public XmlSerializer attributeFloat(String namespace, String name, float value)
287             throws IOException {
288         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
289         mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
290         mOut.writeInternedUTF(name);
291         mOut.writeFloat(value);
292         return this;
293     }
294 
295     @Override
attributeDouble(String namespace, String name, double value)296     public XmlSerializer attributeDouble(String namespace, String name, double value)
297             throws IOException {
298         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
299         mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
300         mOut.writeInternedUTF(name);
301         mOut.writeDouble(value);
302         return this;
303     }
304 
305     @Override
attributeBoolean(String namespace, String name, boolean value)306     public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
307             throws IOException {
308         if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
309         if (value) {
310             mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
311             mOut.writeInternedUTF(name);
312         } else {
313             mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
314             mOut.writeInternedUTF(name);
315         }
316         return this;
317     }
318 
319     @Override
text(char[] buf, int start, int len)320     public XmlSerializer text(char[] buf, int start, int len) throws IOException {
321         writeToken(TEXT, new String(buf, start, len));
322         return this;
323     }
324 
325     @Override
text(String text)326     public XmlSerializer text(String text) throws IOException {
327         writeToken(TEXT, text);
328         return this;
329     }
330 
331     @Override
cdsect(String text)332     public void cdsect(String text) throws IOException {
333         writeToken(CDSECT, text);
334     }
335 
336     @Override
entityRef(String text)337     public void entityRef(String text) throws IOException {
338         writeToken(ENTITY_REF, text);
339     }
340 
341     @Override
processingInstruction(String text)342     public void processingInstruction(String text) throws IOException {
343         writeToken(PROCESSING_INSTRUCTION, text);
344     }
345 
346     @Override
comment(String text)347     public void comment(String text) throws IOException {
348         writeToken(COMMENT, text);
349     }
350 
351     @Override
docdecl(String text)352     public void docdecl(String text) throws IOException {
353         writeToken(DOCDECL, text);
354     }
355 
356     @Override
ignorableWhitespace(String text)357     public void ignorableWhitespace(String text) throws IOException {
358         writeToken(IGNORABLE_WHITESPACE, text);
359     }
360 
361     @Override
setFeature(String name, boolean state)362     public void setFeature(String name, boolean state) {
363         // Quietly handle no-op features
364         if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
365             return;
366         }
367         // Features are not supported
368         throw new UnsupportedOperationException();
369     }
370 
371     @Override
getFeature(String name)372     public boolean getFeature(String name) {
373         // Features are not supported
374         throw new UnsupportedOperationException();
375     }
376 
377     @Override
setProperty(String name, Object value)378     public void setProperty(String name, Object value) {
379         // Properties are not supported
380         throw new UnsupportedOperationException();
381     }
382 
383     @Override
getProperty(String name)384     public Object getProperty(String name) {
385         // Properties are not supported
386         throw new UnsupportedOperationException();
387     }
388 
389     @Override
setPrefix(String prefix, String namespace)390     public void setPrefix(String prefix, String namespace) {
391         // Prefixes are not supported
392         throw new UnsupportedOperationException();
393     }
394 
395     @Override
getPrefix(String namespace, boolean generatePrefix)396     public String getPrefix(String namespace, boolean generatePrefix) {
397         // Prefixes are not supported
398         throw new UnsupportedOperationException();
399     }
400 
illegalNamespace()401     private static IllegalArgumentException illegalNamespace() {
402         throw new IllegalArgumentException("Namespaces are not supported");
403     }
404 }
405