1 /*
2  * Copyright (C) 2007 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 android.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.SystemProperties;
22 import android.system.ErrnoException;
23 import android.system.Os;
24 
25 import com.android.internal.util.ArtBinaryXmlPullParser;
26 import com.android.internal.util.ArtBinaryXmlSerializer;
27 import com.android.internal.util.FastXmlSerializer;
28 import com.android.internal.util.XmlUtils;
29 import com.android.modules.utils.BinaryXmlPullParser;
30 import com.android.modules.utils.BinaryXmlSerializer;
31 import com.android.modules.utils.TypedXmlPullParser;
32 import com.android.modules.utils.TypedXmlSerializer;
33 
34 import libcore.util.XmlObjectFactory;
35 
36 import org.xml.sax.ContentHandler;
37 import org.xml.sax.InputSource;
38 import org.xml.sax.SAXException;
39 import org.xml.sax.XMLReader;
40 import org.xmlpull.v1.XmlPullParser;
41 import org.xmlpull.v1.XmlPullParserException;
42 import org.xmlpull.v1.XmlPullParserFactory;
43 import org.xmlpull.v1.XmlSerializer;
44 
45 import java.io.BufferedInputStream;
46 import java.io.FileInputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.io.OutputStream;
50 import java.io.Reader;
51 import java.io.StringReader;
52 import java.io.UnsupportedEncodingException;
53 import java.nio.charset.StandardCharsets;
54 import java.util.Arrays;
55 
56 import javax.xml.parsers.SAXParserFactory;
57 
58 /**
59  * XML utility methods.
60  */
61 @android.ravenwood.annotation.RavenwoodKeepWholeClass
62 public class Xml {
Xml()63     private Xml() {}
64 
65     /**
66      * {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name.
67      *
68      * @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed">
69      *  specification</a>
70      */
71     public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed";
72 
73     /**
74      * Feature flag: when set, {@link #resolveSerializer(OutputStream)} will
75      * emit binary XML by default.
76      *
77      * @hide
78      */
79     public static final boolean ENABLE_BINARY_DEFAULT = shouldEnableBinaryDefault();
80 
81     @android.ravenwood.annotation.RavenwoodReplace
shouldEnableBinaryDefault()82     private static boolean shouldEnableBinaryDefault() {
83         return SystemProperties.getBoolean("persist.sys.binary_xml", true);
84     }
85 
shouldEnableBinaryDefault$ravenwood()86     private static boolean shouldEnableBinaryDefault$ravenwood() {
87         return true;
88     }
89 
90     /**
91      * Feature flag: when set, {@link #resolvePullParser(InputStream)}} will attempt to sniff
92      * using {@code pread} optimization.
93      *
94      * @hide
95      */
96     public static final boolean ENABLE_RESOLVE_OPTIMIZATIONS = shouldEnableResolveOptimizations();
97 
98     @android.ravenwood.annotation.RavenwoodReplace
shouldEnableResolveOptimizations()99     private static boolean shouldEnableResolveOptimizations() {
100         return true;
101     }
102 
shouldEnableResolveOptimizations$ravenwood()103     private static boolean shouldEnableResolveOptimizations$ravenwood() {
104         return false;
105     }
106 
107     /**
108      * Parses the given xml string and fires events on the given SAX handler.
109      */
parse(String xml, ContentHandler contentHandler)110     public static void parse(String xml, ContentHandler contentHandler)
111             throws SAXException {
112         try {
113             XMLReader reader = newXMLReader();
114             reader.setContentHandler(contentHandler);
115             reader.parse(new InputSource(new StringReader(xml)));
116         } catch (IOException e) {
117             throw new AssertionError(e);
118         }
119     }
120 
121     /**
122      * Parses xml from the given reader and fires events on the given SAX
123      * handler.
124      */
parse(Reader in, ContentHandler contentHandler)125     public static void parse(Reader in, ContentHandler contentHandler)
126             throws IOException, SAXException {
127         XMLReader reader = newXMLReader();
128         reader.setContentHandler(contentHandler);
129         reader.parse(new InputSource(in));
130     }
131 
132     /**
133      * Parses xml from the given input stream and fires events on the given SAX
134      * handler.
135      */
parse(InputStream in, Encoding encoding, ContentHandler contentHandler)136     public static void parse(InputStream in, Encoding encoding,
137             ContentHandler contentHandler) throws IOException, SAXException {
138         XMLReader reader = newXMLReader();
139         reader.setContentHandler(contentHandler);
140         InputSource source = new InputSource(in);
141         source.setEncoding(encoding.expatName);
142         reader.parse(source);
143     }
144 
145     /**
146      * Returns a new pull parser with namespace support.
147      */
148     @android.ravenwood.annotation.RavenwoodReplace
newPullParser()149     public static XmlPullParser newPullParser() {
150         try {
151             XmlPullParser parser = newXmlPullParser();
152             parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
153             parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
154             return parser;
155         } catch (XmlPullParserException e) {
156             throw new AssertionError(e);
157         }
158     }
159 
160     /** @hide */
newPullParser$ravenwood()161     public static XmlPullParser newPullParser$ravenwood() {
162         try {
163             // Prebuilt kxml2-android does not support FEATURE_PROCESS_DOCDECL, so omit here;
164             // it's quite rare and all tests are passing
165             XmlPullParser parser = newXmlPullParser();
166             parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
167             return parser;
168         } catch (XmlPullParserException e) {
169             throw new AssertionError(e);
170         }
171     }
172 
173     /**
174      * Creates a new {@link TypedXmlPullParser} which is optimized for use
175      * inside the system, typically by supporting only a basic set of features.
176      * <p>
177      * In particular, the returned parser does not support namespaces, prefixes,
178      * properties, or options.
179      *
180      * @hide
181      */
182     @SuppressWarnings("AndroidFrameworkEfficientXml")
newFastPullParser()183     public static @NonNull TypedXmlPullParser newFastPullParser() {
184         return XmlUtils.makeTyped(newPullParser());
185     }
186 
187     /**
188      * Creates a new {@link XmlPullParser} that reads XML documents using a
189      * custom binary wire protocol which benchmarking has shown to be 8.5x
190      * faster than {@code Xml.newFastPullParser()} for a typical
191      * {@code packages.xml}.
192      *
193      * @hide
194      */
195     @android.ravenwood.annotation.RavenwoodReplace
newBinaryPullParser()196     public static @NonNull TypedXmlPullParser newBinaryPullParser() {
197         return new ArtBinaryXmlPullParser();
198     }
199 
200     /** @hide */
newBinaryPullParser$ravenwood()201     public static TypedXmlPullParser newBinaryPullParser$ravenwood() {
202         // TODO: remove once we're linking against libcore
203         return new BinaryXmlPullParser();
204     }
205 
206     /**
207      * Creates a new {@link XmlPullParser} which is optimized for use inside the
208      * system, typically by supporting only a basic set of features.
209      * <p>
210      * This returned instance may be configured to read using an efficient
211      * binary format instead of a human-readable text format, depending on
212      * device feature flags.
213      * <p>
214      * To ensure that both formats are detected and transparently handled
215      * correctly, you must shift to using both {@link #resolveSerializer} and
216      * {@link #resolvePullParser}.
217      *
218      * @hide
219      */
resolvePullParser(@onNull InputStream in)220     public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
221             throws IOException {
222         final byte[] magic = new byte[4];
223         if (ENABLE_RESOLVE_OPTIMIZATIONS && in instanceof FileInputStream) {
224             try {
225                 Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
226             } catch (ErrnoException e) {
227                 throw e.rethrowAsIOException();
228             }
229         } else {
230             if (!in.markSupported()) {
231                 in = new BufferedInputStream(in);
232             }
233             in.mark(8);
234             in.read(magic);
235             in.reset();
236         }
237 
238         final TypedXmlPullParser xml;
239         if (Arrays.equals(magic, BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0)) {
240             xml = newBinaryPullParser();
241         } else {
242             xml = newFastPullParser();
243         }
244         try {
245             xml.setInput(in, StandardCharsets.UTF_8.name());
246         } catch (XmlPullParserException e) {
247             throw new IOException(e);
248         }
249         return xml;
250     }
251 
252     /**
253      * Creates a new xml serializer.
254      */
newSerializer()255     public static XmlSerializer newSerializer() {
256         return newXmlSerializer();
257     }
258 
259     /**
260      * Creates a new {@link XmlSerializer} which is optimized for use inside the
261      * system, typically by supporting only a basic set of features.
262      * <p>
263      * In particular, the returned parser does not support namespaces, prefixes,
264      * properties, or options.
265      *
266      * @hide
267      */
268     @SuppressWarnings("AndroidFrameworkEfficientXml")
newFastSerializer()269     public static @NonNull TypedXmlSerializer newFastSerializer() {
270         return XmlUtils.makeTyped(new FastXmlSerializer());
271     }
272 
273     /**
274      * Creates a new {@link XmlSerializer} that writes XML documents using a
275      * custom binary wire protocol which benchmarking has shown to be 4.4x
276      * faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
277      * for a typical {@code packages.xml}.
278      *
279      * @hide
280      */
281     @android.ravenwood.annotation.RavenwoodReplace
newBinarySerializer()282     public static @NonNull TypedXmlSerializer newBinarySerializer() {
283         return new ArtBinaryXmlSerializer();
284     }
285 
286     /** @hide */
newBinarySerializer$ravenwood()287     public static @NonNull TypedXmlSerializer newBinarySerializer$ravenwood() {
288         // TODO: remove once we're linking against libcore
289         return new BinaryXmlSerializer();
290     }
291 
292     /**
293      * Creates a new {@link XmlSerializer} which is optimized for use inside the
294      * system, typically by supporting only a basic set of features.
295      * <p>
296      * This returned instance may be configured to write using an efficient
297      * binary format instead of a human-readable text format, depending on
298      * device feature flags.
299      * <p>
300      * To ensure that both formats are detected and transparently handled
301      * correctly, you must shift to using both {@link #resolveSerializer} and
302      * {@link #resolvePullParser}.
303      *
304      * @hide
305      */
306     @android.ravenwood.annotation.RavenwoodReplace
resolveSerializer(@onNull OutputStream out)307     public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
308             throws IOException {
309         final TypedXmlSerializer xml;
310         if (ENABLE_BINARY_DEFAULT) {
311             xml = newBinarySerializer();
312         } else {
313             xml = newFastSerializer();
314         }
315         xml.setOutput(out, StandardCharsets.UTF_8.name());
316         return xml;
317     }
318 
319     /** @hide */
resolveSerializer$ravenwood(@onNull OutputStream out)320     public static @NonNull TypedXmlSerializer resolveSerializer$ravenwood(@NonNull OutputStream out)
321             throws IOException {
322         // TODO: remove once we're linking against libcore
323         final TypedXmlSerializer xml = new BinaryXmlSerializer();
324         xml.setOutput(out, StandardCharsets.UTF_8.name());
325         return xml;
326     }
327 
328     /**
329      * Copy the first XML document into the second document.
330      * <p>
331      * Implemented by reading all events from the given {@link XmlPullParser}
332      * and writing them directly to the given {@link XmlSerializer}. This can be
333      * useful for transparently converting between underlying wire protocols.
334      *
335      * @hide
336      */
copy(@onNull XmlPullParser in, @NonNull XmlSerializer out)337     public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
338             throws XmlPullParserException, IOException {
339         // Some parsers may have already consumed the event that starts the
340         // document, so we manually emit that event here for consistency
341         if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
342             out.startDocument(in.getInputEncoding(), true);
343         }
344 
345         while (true) {
346             final int token = in.nextToken();
347             switch (token) {
348                 case XmlPullParser.START_DOCUMENT:
349                     out.startDocument(in.getInputEncoding(), true);
350                     break;
351                 case XmlPullParser.END_DOCUMENT:
352                     out.endDocument();
353                     return;
354                 case XmlPullParser.START_TAG:
355                     out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
356                     for (int i = 0; i < in.getAttributeCount(); i++) {
357                         out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
358                                 in.getAttributeName(i), in.getAttributeValue(i));
359                     }
360                     break;
361                 case XmlPullParser.END_TAG:
362                     out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
363                     break;
364                 case XmlPullParser.TEXT:
365                     out.text(in.getText());
366                     break;
367                 case XmlPullParser.CDSECT:
368                     out.cdsect(in.getText());
369                     break;
370                 case XmlPullParser.ENTITY_REF:
371                     out.entityRef(in.getName());
372                     break;
373                 case XmlPullParser.IGNORABLE_WHITESPACE:
374                     out.ignorableWhitespace(in.getText());
375                     break;
376                 case XmlPullParser.PROCESSING_INSTRUCTION:
377                     out.processingInstruction(in.getText());
378                     break;
379                 case XmlPullParser.COMMENT:
380                     out.comment(in.getText());
381                     break;
382                 case XmlPullParser.DOCDECL:
383                     out.docdecl(in.getText());
384                     break;
385                 default:
386                     throw new IllegalStateException("Unknown token " + token);
387             }
388         }
389     }
390 
391     /**
392      * Some parsers may return an empty string {@code ""} when a namespace in
393      * unsupported, which can confuse serializers. This method normalizes empty
394      * strings to be {@code null}.
395      */
normalizeNamespace(@ullable String namespace)396     private static @Nullable String normalizeNamespace(@Nullable String namespace) {
397         if (namespace == null || namespace.isEmpty()) {
398             return null;
399         } else {
400             return namespace;
401         }
402     }
403 
404     /**
405      * Supported character encodings.
406      */
407     public enum Encoding {
408 
409         US_ASCII("US-ASCII"),
410         UTF_8("UTF-8"),
411         UTF_16("UTF-16"),
412         ISO_8859_1("ISO-8859-1");
413 
414         final String expatName;
415 
Encoding(String expatName)416         Encoding(String expatName) {
417             this.expatName = expatName;
418         }
419     }
420 
421     /**
422      * Finds an encoding by name. Returns UTF-8 if you pass {@code null}.
423      */
findEncodingByName(String encodingName)424     public static Encoding findEncodingByName(String encodingName)
425             throws UnsupportedEncodingException {
426         if (encodingName == null) {
427             return Encoding.UTF_8;
428         }
429 
430         for (Encoding encoding : Encoding.values()) {
431             if (encoding.expatName.equalsIgnoreCase(encodingName))
432                 return encoding;
433         }
434         throw new UnsupportedEncodingException(encodingName);
435     }
436 
437     /**
438      * Return an AttributeSet interface for use with the given XmlPullParser.
439      * If the given parser itself implements AttributeSet, that implementation
440      * is simply returned.  Otherwise a wrapper class is
441      * instantiated on top of the XmlPullParser, as a proxy for retrieving its
442      * attributes, and returned to you.
443      *
444      * @param parser The existing parser for which you would like an
445      *               AttributeSet.
446      *
447      * @return An AttributeSet you can use to retrieve the
448      *         attribute values at each of the tags as the parser moves
449      *         through its XML document.
450      *
451      * @see AttributeSet
452      */
asAttributeSet(XmlPullParser parser)453     public static AttributeSet asAttributeSet(XmlPullParser parser) {
454         return (parser instanceof AttributeSet)
455                 ? (AttributeSet) parser
456                 : new XmlPullAttributes(parser);
457     }
458 
459     @android.ravenwood.annotation.RavenwoodReplace
newXmlSerializer()460     private static @NonNull XmlSerializer newXmlSerializer() {
461         return XmlObjectFactory.newXmlSerializer();
462     }
463 
newXmlSerializer$ravenwood()464     private static @NonNull XmlSerializer newXmlSerializer$ravenwood() {
465         try {
466             return XmlPullParserFactory.newInstance().newSerializer();
467         } catch (XmlPullParserException e) {
468             throw new UnsupportedOperationException(e);
469         }
470     }
471 
472     @android.ravenwood.annotation.RavenwoodReplace
newXmlPullParser()473     private static @NonNull XmlPullParser newXmlPullParser() {
474         return XmlObjectFactory.newXmlPullParser();
475     }
476 
newXmlPullParser$ravenwood()477     private static @NonNull XmlPullParser newXmlPullParser$ravenwood() {
478         try {
479             return XmlPullParserFactory.newInstance().newPullParser();
480         } catch (XmlPullParserException e) {
481             throw new UnsupportedOperationException(e);
482         }
483     }
484 
485     @android.ravenwood.annotation.RavenwoodReplace
newXMLReader()486     private static @NonNull XMLReader newXMLReader() {
487         return XmlObjectFactory.newXMLReader();
488     }
489 
newXMLReader$ravenwood()490     private static @NonNull XMLReader newXMLReader$ravenwood() {
491         try {
492             final SAXParserFactory factory = SAXParserFactory.newInstance();
493             factory.setNamespaceAware(true);
494             return factory.newSAXParser().getXMLReader();
495         } catch (Exception e) {
496             throw new UnsupportedOperationException(e);
497         }
498     }
499 }
500