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