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