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