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 android.util;
18 
19 import static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
22 import static org.xmlpull.v1.XmlPullParser.END_TAG;
23 import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
24 import static org.xmlpull.v1.XmlPullParser.START_TAG;
25 import static org.xmlpull.v1.XmlPullParser.TEXT;
26 
27 import android.os.PersistableBundle;
28 
29 import androidx.test.runner.AndroidJUnit4;
30 
31 import com.android.internal.util.XmlUtils;
32 
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 
36 import java.io.ByteArrayInputStream;
37 import java.io.ByteArrayOutputStream;
38 import java.nio.charset.StandardCharsets;
39 import java.util.Arrays;
40 
41 @RunWith(AndroidJUnit4.class)
42 public class XmlTest {
43     @Test
testLargeValues_Normal()44     public void testLargeValues_Normal() throws Exception {
45         doLargeValues(XmlUtils.makeTyped(Xml.newSerializer()),
46                 XmlUtils.makeTyped(Xml.newPullParser()));
47     }
48 
49     @Test
testLargeValues_Fast()50     public void testLargeValues_Fast() throws Exception {
51         doLargeValues(Xml.newFastSerializer(),
52                 Xml.newFastPullParser());
53     }
54 
55     @Test
testLargeValues_FastIndenting()56     public void testLargeValues_FastIndenting() throws Exception {
57         final TypedXmlSerializer out = Xml.newFastSerializer();
58         out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
59         doLargeValues(out,
60                 Xml.newFastPullParser());
61     }
62 
63     @Test
testLargeValues_Binary()64     public void testLargeValues_Binary() throws Exception {
65         doLargeValues(Xml.newBinarySerializer(),
66                 Xml.newBinaryPullParser());
67     }
68 
69     /**
70      * Verify that we can write and read large {@link String} and {@code byte[]}
71      * without issues.
72      */
doLargeValues(TypedXmlSerializer out, TypedXmlPullParser in)73     private static void doLargeValues(TypedXmlSerializer out, TypedXmlPullParser in)
74             throws Exception {
75         final char[] chars = new char[65_534];
76         Arrays.fill(chars, '!');
77 
78         final String string = new String(chars);
79         final byte[] bytes = string.getBytes();
80         assertEquals(chars.length, bytes.length);
81 
82         final ByteArrayOutputStream os = new ByteArrayOutputStream();
83         out.setOutput(os, StandardCharsets.UTF_8.name());
84         out.startTag(null, "tag");
85         out.attribute(null, "string", string);
86         out.attributeBytesBase64(null, "bytes", bytes);
87         out.endTag(null, "tag");
88         out.flush();
89 
90         final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
91         in.setInput(is, StandardCharsets.UTF_8.name());
92         assertNext(in, START_TAG, "tag");
93         assertEquals(2, in.getAttributeCount());
94         assertEquals(string, in.getAttributeValue(null, "string"));
95         assertArrayEquals(bytes, in.getAttributeBytesBase64(null, "bytes"));
96     }
97 
98     @Test
testPersistableBundle_Normal()99     public void testPersistableBundle_Normal() throws Exception {
100         doPersistableBundle(XmlUtils.makeTyped(Xml.newSerializer()),
101                 XmlUtils.makeTyped(Xml.newPullParser()));
102     }
103 
104     @Test
testPersistableBundle_Fast()105     public void testPersistableBundle_Fast() throws Exception {
106         doPersistableBundle(Xml.newFastSerializer(),
107                 Xml.newFastPullParser());
108     }
109 
110     @Test
testPersistableBundle_FastIndenting()111     public void testPersistableBundle_FastIndenting() throws Exception {
112         final TypedXmlSerializer out = Xml.newFastSerializer();
113         out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
114         doPersistableBundle(out,
115                 Xml.newFastPullParser());
116     }
117 
118     @Test
testPersistableBundle_Binary()119     public void testPersistableBundle_Binary() throws Exception {
120         doPersistableBundle(Xml.newBinarySerializer(),
121                 Xml.newBinaryPullParser());
122     }
123 
124     /**
125      * Verify that a complex {@link PersistableBundle} can be serialized out and
126      * then parsed in with the original structure intact.
127      */
doPersistableBundle(TypedXmlSerializer out, TypedXmlPullParser in)128     private static void doPersistableBundle(TypedXmlSerializer out, TypedXmlPullParser in)
129             throws Exception {
130         final PersistableBundle expected = buildPersistableBundle();
131         final byte[] raw = doPersistableBundleWrite(out, expected);
132 
133         // Yes, this string-based check is fragile, but kindofEquals() is broken
134         // when working with nested objects and arrays
135         final PersistableBundle actual = doPersistableBundleRead(in, raw);
136         assertEquals(expected.toString(), actual.toString());
137     }
138 
buildPersistableBundle()139     static PersistableBundle buildPersistableBundle() {
140         final PersistableBundle outer = new PersistableBundle();
141 
142         outer.putBoolean("boolean", true);
143         outer.putInt("int", 42);
144         outer.putLong("long", 43L);
145         outer.putDouble("double", 44d);
146         outer.putString("string", "com.example <and></and> &amp; more");
147 
148         outer.putBooleanArray("boolean[]", new boolean[] { true, false, true });
149         outer.putIntArray("int[]", new int[] { 42, 43, 44 });
150         outer.putLongArray("long[]", new long[] { 43L, 44L, 45L });
151         outer.putDoubleArray("double[]", new double[] { 43d, 44d, 45d });
152         outer.putStringArray("string[]", new String[] { "foo", "bar", "baz" });
153 
154         outer.putString("nullString", null);
155         outer.putObject("nullObject", null);
156         outer.putIntArray("nullArray", null);
157 
158         final PersistableBundle nested = new PersistableBundle();
159         nested.putString("nested_key", "nested_value");
160         outer.putPersistableBundle("nested", nested);
161 
162         return outer;
163     }
164 
doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)165     static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
166             throws Exception {
167         // We purposefully omit START/END_DOCUMENT events here to verify correct
168         // behavior of what PersistableBundle does internally
169         final ByteArrayOutputStream os = new ByteArrayOutputStream();
170         out.setOutput(os, StandardCharsets.UTF_8.name());
171         out.startTag(null, "bundle");
172         bundle.saveToXml(out);
173         out.endTag(null, "bundle");
174         out.flush();
175         return os.toByteArray();
176     }
177 
doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)178     static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
179             throws Exception {
180         final ByteArrayInputStream is = new ByteArrayInputStream(raw);
181         in.setInput(is, StandardCharsets.UTF_8.name());
182         in.next();
183         return PersistableBundle.restoreFromXml(in);
184     }
185 
186     @Test
testVerify_Normal()187     public void testVerify_Normal() throws Exception {
188         doVerify(XmlUtils.makeTyped(Xml.newSerializer()),
189                 XmlUtils.makeTyped(Xml.newPullParser()));
190     }
191 
192     @Test
testVerify_Fast()193     public void testVerify_Fast() throws Exception {
194         doVerify(Xml.newFastSerializer(),
195                 Xml.newFastPullParser());
196     }
197 
198     @Test
testVerify_FastIndenting()199     public void testVerify_FastIndenting() throws Exception {
200         final TypedXmlSerializer out = Xml.newFastSerializer();
201         out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
202         doVerify(out,
203                 Xml.newFastPullParser());
204     }
205 
206     @Test
testVerify_Binary()207     public void testVerify_Binary() throws Exception {
208         doVerify(Xml.newBinarySerializer(),
209                 Xml.newBinaryPullParser());
210     }
211 
212     /**
213      * Verify that example test data is correctly serialized and parsed
214      * end-to-end using the given objects.
215      */
doVerify(TypedXmlSerializer out, TypedXmlPullParser in)216     private static void doVerify(TypedXmlSerializer out, TypedXmlPullParser in) throws Exception {
217         final ByteArrayOutputStream os = new ByteArrayOutputStream();
218         out.setOutput(os, StandardCharsets.UTF_8.name());
219         doVerifyWrite(out);
220         out.flush();
221 
222         final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
223         in.setInput(is, StandardCharsets.UTF_8.name());
224         doVerifyRead(in);
225     }
226 
227     private static final String TEST_STRING = "com.example";
228     private static final String TEST_STRING_EMPTY = "";
229     private static final byte[] TEST_BYTES = new byte[] { 0, 1, 2, 3, 4, 3, 2, 1, 0 };
230     private static final byte[] TEST_BYTES_EMPTY = new byte[0];
231 
doVerifyWrite(TypedXmlSerializer out)232     static void doVerifyWrite(TypedXmlSerializer out) throws Exception {
233         out.startDocument(StandardCharsets.UTF_8.name(), true);
234         out.startTag(null, "one");
235         {
236             out.startTag(null, "two");
237             {
238                 out.attribute(null, "string", TEST_STRING);
239                 out.attribute(null, "stringEmpty", TEST_STRING_EMPTY);
240                 out.attributeBytesHex(null, "bytesHex", TEST_BYTES);
241                 out.attributeBytesHex(null, "bytesHexEmpty", TEST_BYTES_EMPTY);
242                 out.attributeBytesBase64(null, "bytesBase64", TEST_BYTES);
243                 out.attributeBytesBase64(null, "bytesBase64Empty", TEST_BYTES_EMPTY);
244                 out.attributeInt(null, "int", 43);
245                 out.attributeIntHex(null, "intHex", 44);
246                 out.attributeLong(null, "long", 45L);
247                 out.attributeLongHex(null, "longHex", 46L);
248                 out.attributeFloat(null, "float", 47f);
249                 out.attributeDouble(null, "double", 48d);
250                 out.attributeBoolean(null, "boolean", true);
251                 out.attribute(null, "stringNumber", "49");
252             }
253             out.endTag(null, "two");
254 
255             out.startTag(null, "three");
256             {
257                 out.text("foo");
258                 out.startTag(null, "four");
259                 {
260                 }
261                 out.endTag(null, "four");
262                 out.text("bar");
263                 out.text("baz");
264             }
265             out.endTag(null, "three");
266         }
267         out.endTag(null, "one");
268         out.endDocument();
269     }
270 
doVerifyRead(TypedXmlPullParser in)271     static void doVerifyRead(TypedXmlPullParser in) throws Exception {
272         assertEquals(START_DOCUMENT, in.getEventType());
273         assertDepth(in, 0);
274         assertNext(in, START_TAG, "one");
275         assertDepth(in, 1);
276         {
277             assertNext(in, START_TAG, "two");
278             assertDepth(in, 2);
279             {
280                 assertEquals(14, in.getAttributeCount());
281                 assertEquals(TEST_STRING,
282                         in.getAttributeValue(null, "string"));
283                 assertEquals(TEST_STRING_EMPTY,
284                         in.getAttributeValue(null, "stringEmpty"));
285                 assertArrayEquals(TEST_BYTES,
286                         in.getAttributeBytesHex(null, "bytesHex"));
287                 assertArrayEquals(TEST_BYTES_EMPTY,
288                         in.getAttributeBytesHex(null, "bytesHexEmpty"));
289                 assertArrayEquals(TEST_BYTES,
290                         in.getAttributeBytesBase64(null, "bytesBase64"));
291                 assertArrayEquals(TEST_BYTES_EMPTY,
292                         in.getAttributeBytesBase64(null, "bytesBase64Empty"));
293 
294                 assertEquals(43, in.getAttributeInt(null, "int"));
295                 assertEquals(44, in.getAttributeIntHex(null, "intHex"));
296                 assertEquals(45L, in.getAttributeLong(null, "long"));
297                 assertEquals(46L, in.getAttributeLongHex(null, "longHex"));
298                 assertEquals(47f, in.getAttributeFloat(null, "float"), 0.01);
299                 assertEquals(48d, in.getAttributeDouble(null, "double"), 0.01);
300                 assertEquals(true, in.getAttributeBoolean(null, "boolean"));
301 
302                 // Also verify that typed values are available as strings
303                 assertEquals("000102030403020100", in.getAttributeValue(null, "bytesHex"));
304                 assertEquals("AAECAwQDAgEA", in.getAttributeValue(null, "bytesBase64"));
305                 assertEquals("43", in.getAttributeValue(null, "int"));
306                 assertEquals("2c", in.getAttributeValue(null, "intHex"));
307                 assertEquals("45", in.getAttributeValue(null, "long"));
308                 assertEquals("2e", in.getAttributeValue(null, "longHex"));
309                 assertEquals("true", in.getAttributeValue(null, "boolean"));
310 
311                 // And that raw strings can be parsed too
312                 assertEquals("49", in.getAttributeValue(null, "stringNumber"));
313                 assertEquals(49, in.getAttributeInt(null, "stringNumber"));
314             }
315             assertNext(in, END_TAG, "two");
316             assertDepth(in, 2);
317 
318             assertNext(in, START_TAG, "three");
319             assertDepth(in, 2);
320             {
321                 assertNext(in, TEXT, null);
322                 assertDepth(in, 2);
323                 assertEquals("foo", in.getText().trim());
324                 assertNext(in, START_TAG, "four");
325                 assertDepth(in, 3);
326                 {
327                     assertEquals(0, in.getAttributeCount());
328                 }
329                 assertNext(in, END_TAG, "four");
330                 assertDepth(in, 3);
331                 assertNext(in, TEXT, null);
332                 assertDepth(in, 2);
333                 assertEquals("barbaz", in.getText().trim());
334             }
335             assertNext(in, END_TAG, "three");
336             assertDepth(in, 2);
337         }
338         assertNext(in, END_TAG, "one");
339         assertDepth(in, 1);
340         assertNext(in, END_DOCUMENT, null);
341         assertDepth(in, 0);
342     }
343 
assertNext(TypedXmlPullParser in, int token, String name)344     static void assertNext(TypedXmlPullParser in, int token, String name) throws Exception {
345         // We're willing to skip over empty text regions, which some
346         // serializers emit transparently
347         int event;
348         while ((event = in.next()) == TEXT && in.getText().trim().length() == 0) {
349         }
350         assertEquals("next", token, event);
351         assertEquals("getEventType", token, in.getEventType());
352         assertEquals("getName", name, in.getName());
353     }
354 
assertDepth(TypedXmlPullParser in, int depth)355     static void assertDepth(TypedXmlPullParser in, int depth) throws Exception {
356         assertEquals("getDepth", depth, in.getDepth());
357     }
358 }
359