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.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.fail;
22 
23 import android.annotation.NonNull;
24 import android.util.ExceptionUtils;
25 
26 import com.android.modules.utils.FastDataInput;
27 import com.android.modules.utils.FastDataOutput;
28 
29 import libcore.util.HexEncoding;
30 
31 import org.junit.Assume;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 import org.junit.runners.Parameterized;
35 import org.junit.runners.Parameterized.Parameters;
36 
37 import java.io.ByteArrayInputStream;
38 import java.io.ByteArrayOutputStream;
39 import java.io.DataInput;
40 import java.io.DataInputStream;
41 import java.io.DataOutput;
42 import java.io.DataOutputStream;
43 import java.io.EOFException;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.nio.charset.StandardCharsets;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.function.Consumer;
51 
52 @RunWith(Parameterized.class)
53 public class FastDataTest {
54     private final boolean use4ByteSequence;
55 
56     private static final String TEST_SHORT_STRING = "a";
57     private static final String TEST_LONG_STRING = "com☃example��typical☃package��name";
58     private static final byte[] TEST_BYTES = TEST_LONG_STRING.getBytes(StandardCharsets.UTF_16LE);
59 
60     @Parameters(name = "use4ByteSequence={0}")
data()61     public static Collection<Object[]> data() {
62         return Arrays.asList(new Object[][] { {true}, {false} });
63     }
64 
FastDataTest(boolean use4ByteSequence)65     public FastDataTest(boolean use4ByteSequence) {
66         this.use4ByteSequence = use4ByteSequence;
67     }
68 
69     @NonNull
createFastDataInput(@onNull InputStream in, int bufferSize)70     private FastDataInput createFastDataInput(@NonNull InputStream in, int bufferSize) {
71         if (use4ByteSequence) {
72             return new ArtFastDataInput(in, bufferSize);
73         } else {
74             return new FastDataInput(in, bufferSize);
75         }
76     }
77 
78     @NonNull
createFastDataOutput(@onNull OutputStream out, int bufferSize)79     private FastDataOutput createFastDataOutput(@NonNull OutputStream out, int bufferSize) {
80         if (use4ByteSequence) {
81             return new ArtFastDataOutput(out, bufferSize);
82         } else {
83             return new FastDataOutput(out, bufferSize);
84         }
85     }
86 
87     @Test
testEndOfFile_Int()88     public void testEndOfFile_Int() throws Exception {
89         try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
90                 new byte[] { 1 }), 1000)) {
91             assertThrows(EOFException.class, () -> in.readInt());
92         }
93         try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
94                 new byte[] { 1, 1, 1, 1 }), 1000)) {
95             assertEquals(1, in.readByte());
96             assertThrows(EOFException.class, () -> in.readInt());
97         }
98     }
99 
100     @Test
testEndOfFile_String()101     public void testEndOfFile_String() throws Exception {
102         try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
103                 new byte[] { 1 }), 1000)) {
104             assertThrows(EOFException.class, () -> in.readUTF());
105         }
106         try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
107                 new byte[] { 1, 1, 1, 1 }), 1000)) {
108             assertThrows(EOFException.class, () -> in.readUTF());
109         }
110     }
111 
112     @Test
testEndOfFile_Bytes_Small()113     public void testEndOfFile_Bytes_Small() throws Exception {
114         try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
115                 new byte[] { 1, 1, 1, 1 }), 1000)) {
116             final byte[] tmp = new byte[10];
117             assertThrows(EOFException.class, () -> in.readFully(tmp));
118         }
119         try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
120                 new byte[] { 1, 1, 1, 1 }), 1000)) {
121             final byte[] tmp = new byte[10_000];
122             assertThrows(EOFException.class, () -> in.readFully(tmp));
123         }
124     }
125 
126     @Test
testUTF_Bounds()127     public void testUTF_Bounds() throws Exception {
128         final char[] buf = new char[65_534];
129         try (FastDataOutput out = createFastDataOutput(new ByteArrayOutputStream(), BOUNCE_SIZE)) {
130             // Writing simple string will fit fine
131             Arrays.fill(buf, '!');
132             final String simple = new String(buf);
133             out.writeUTF(simple);
134             out.writeInternedUTF(simple);
135 
136             // Just one complex char will cause it to overflow
137             buf[0] = '☃';
138             final String complex = new String(buf);
139             assertThrows(IOException.class, () -> out.writeUTF(complex));
140             assertThrows(IOException.class, () -> out.writeInternedUTF(complex));
141 
142             out.flush();
143         }
144     }
145 
146     @Test
testTranscode()147     public void testTranscode() throws Exception {
148         Assume.assumeFalse(use4ByteSequence);
149 
150         // Verify that upstream data can be read by fast
151         {
152             final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
153             final DataOutputStream out = new DataOutputStream(outStream);
154             doTranscodeWrite(out);
155             out.flush();
156 
157             final FastDataInput in = createFastDataInput(
158                     new ByteArrayInputStream(outStream.toByteArray()), BOUNCE_SIZE);
159             doTranscodeRead(in);
160         }
161 
162         // Verify that fast data can be read by upstream
163         {
164             final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
165             final FastDataOutput out = createFastDataOutput(outStream, BOUNCE_SIZE);
166             doTranscodeWrite(out);
167             out.flush();
168 
169             final DataInputStream in = new DataInputStream(
170                     new ByteArrayInputStream(outStream.toByteArray()));
171             doTranscodeRead(in);
172         }
173     }
174 
doTranscodeWrite(DataOutput out)175     private static void doTranscodeWrite(DataOutput out) throws IOException {
176         out.writeBoolean(true);
177         out.writeBoolean(false);
178         out.writeByte(1);
179         out.writeShort(2);
180         out.writeInt(4);
181         out.writeUTF("foo\0bar");
182         out.writeUTF(TEST_SHORT_STRING);
183         out.writeUTF(TEST_LONG_STRING);
184         out.writeLong(8L);
185         out.writeFloat(16f);
186         out.writeDouble(32d);
187     }
188 
doTranscodeRead(DataInput in)189     private static void doTranscodeRead(DataInput in) throws IOException {
190         assertEquals(true, in.readBoolean());
191         assertEquals(false, in.readBoolean());
192         assertEquals(1, in.readByte());
193         assertEquals(2, in.readShort());
194         assertEquals(4, in.readInt());
195         assertEquals("foo\0bar", in.readUTF());
196         assertEquals(TEST_SHORT_STRING, in.readUTF());
197         assertEquals(TEST_LONG_STRING, in.readUTF());
198         assertEquals(8L, in.readLong());
199         assertEquals(16f, in.readFloat(), 0.01);
200         assertEquals(32d, in.readDouble(), 0.01);
201     }
202 
203     @Test
testBounce_Char()204     public void testBounce_Char() throws Exception {
205         doBounce((out) -> {
206             out.writeChar('\0');
207             out.writeChar('☃');
208         }, (in) -> {
209             assertEquals('\0', in.readChar());
210             assertEquals('☃', in.readChar());
211         });
212     }
213 
214     @Test
testBounce_Short()215     public void testBounce_Short() throws Exception {
216         doBounce((out) -> {
217             out.writeShort(0);
218             out.writeShort((short) 0x0f0f);
219             out.writeShort((short) 0xf0f0);
220             out.writeShort(Short.MIN_VALUE);
221             out.writeShort(Short.MAX_VALUE);
222         }, (in) -> {
223             assertEquals(0, in.readShort());
224             assertEquals((short) 0x0f0f, in.readShort());
225             assertEquals((short) 0xf0f0, in.readShort());
226             assertEquals(Short.MIN_VALUE, in.readShort());
227             assertEquals(Short.MAX_VALUE, in.readShort());
228         });
229     }
230 
231     @Test
testBounce_Int()232     public void testBounce_Int() throws Exception {
233         doBounce((out) -> {
234             out.writeInt(0);
235             out.writeInt(0x0f0f0f0f);
236             out.writeInt(0xf0f0f0f0);
237             out.writeInt(Integer.MIN_VALUE);
238             out.writeInt(Integer.MAX_VALUE);
239         }, (in) -> {
240             assertEquals(0, in.readInt());
241             assertEquals(0x0f0f0f0f, in.readInt());
242             assertEquals(0xf0f0f0f0, in.readInt());
243             assertEquals(Integer.MIN_VALUE, in.readInt());
244             assertEquals(Integer.MAX_VALUE, in.readInt());
245         });
246     }
247 
248     @Test
testBounce_Long()249     public void testBounce_Long() throws Exception {
250         doBounce((out) -> {
251             out.writeLong(0);
252             out.writeLong(0x0f0f0f0f0f0f0f0fL);
253             out.writeLong(0xf0f0f0f0f0f0f0f0L);
254             out.writeLong(Long.MIN_VALUE);
255             out.writeLong(Long.MAX_VALUE);
256         }, (in) -> {
257             assertEquals(0, in.readLong());
258             assertEquals(0x0f0f0f0f0f0f0f0fL, in.readLong());
259             assertEquals(0xf0f0f0f0f0f0f0f0L, in.readLong());
260             assertEquals(Long.MIN_VALUE, in.readLong());
261             assertEquals(Long.MAX_VALUE, in.readLong());
262         });
263     }
264 
265     @Test
testBounce_UTF()266     public void testBounce_UTF() throws Exception {
267         doBounce((out) -> {
268             out.writeUTF("");
269             out.writeUTF("☃");
270             out.writeUTF("��");
271             out.writeUTF("example");
272         }, (in) -> {
273             assertEquals("", in.readUTF());
274             assertEquals("☃", in.readUTF());
275             assertEquals("��", in.readUTF());
276             assertEquals("example", in.readUTF());
277         });
278     }
279 
280     @Test
testBounce_UTF_Exact()281     public void testBounce_UTF_Exact() throws Exception {
282         final char[] expectedBuf = new char[BOUNCE_SIZE];
283         Arrays.fill(expectedBuf, '!');
284         final String expected = new String(expectedBuf);
285 
286         doBounce((out) -> {
287             out.writeUTF(expected);
288         }, (in) -> {
289             final String actual = in.readUTF();
290             assertEquals(expected.length(), actual.length());
291             assertEquals(expected, actual);
292         });
293     }
294 
295     @Test
testBounce_UTF_Maximum()296     public void testBounce_UTF_Maximum() throws Exception {
297         final char[] expectedBuf = new char[65_534];
298         Arrays.fill(expectedBuf, '!');
299         final String expected = new String(expectedBuf);
300 
301         doBounce((out) -> {
302             out.writeUTF(expected);
303         }, (in) -> {
304             final String actual = in.readUTF();
305             assertEquals(expected.length(), actual.length());
306             assertEquals(expected, actual);
307         }, 1);
308     }
309 
310     /**
311      * Verify that we encode every valid code-point identically to RI when
312      * running in 3-byte mode.
313      */
314     @Test
testBounce_UTF_Exhaustive()315     public void testBounce_UTF_Exhaustive() throws Exception {
316         Assume.assumeFalse(use4ByteSequence);
317 
318         final ByteArrayOutputStream slowStream = new ByteArrayOutputStream();
319         final DataOutput slowData = new DataOutputStream(slowStream);
320 
321         final ByteArrayOutputStream fastStream = new ByteArrayOutputStream();
322         final FastDataOutput fastData = FastDataOutput.obtain(fastStream);
323 
324         for (int cp = Character.MIN_CODE_POINT; cp < Character.MAX_CODE_POINT; cp++) {
325             if (Character.isValidCodePoint(cp)) {
326                 final String cpString = new String(Character.toChars(cp));
327                 slowStream.reset();
328                 slowData.writeUTF(cpString);
329                 fastStream.reset();
330                 fastData.writeUTF(cpString);
331                 fastData.flush();
332                 assertEquals("Bad encoding for code-point " + Integer.toHexString(cp),
333                         HexEncoding.encodeToString(slowStream.toByteArray()),
334                         HexEncoding.encodeToString(fastStream.toByteArray()));
335             }
336         }
337     }
338 
339     @Test
testBounce_InternedUTF()340     public void testBounce_InternedUTF() throws Exception {
341         doBounce((out) -> {
342             out.writeInternedUTF("foo");
343             out.writeInternedUTF("bar");
344             out.writeInternedUTF("baz");
345             out.writeInternedUTF("bar");
346             out.writeInternedUTF("foo");
347         }, (in) -> {
348             assertEquals("foo", in.readInternedUTF());
349             assertEquals("bar", in.readInternedUTF());
350             assertEquals("baz", in.readInternedUTF());
351             assertEquals("bar", in.readInternedUTF());
352             assertEquals("foo", in.readInternedUTF());
353         });
354     }
355 
356     /**
357      * Verify that when we overflow the maximum number of interned string
358      * references, we still transport the raw string values successfully.
359      */
360     @Test
testBounce_InternedUTF_Maximum()361     public void testBounce_InternedUTF_Maximum() throws Exception {
362         final int num = 70_000;
363         doBounce((out) -> {
364             for (int i = 0; i < num; i++) {
365                 out.writeInternedUTF("foo" + i);
366             }
367         }, (in) -> {
368             for (int i = 0; i < num; i++) {
369                 assertEquals("foo" + i, in.readInternedUTF());
370             }
371         }, 1);
372     }
373 
374     @Test
testBounce_Bytes()375     public void testBounce_Bytes() throws Exception {
376         doBounce((out) -> {
377             out.write(TEST_BYTES, 8, 32);
378             out.writeInt(64);
379         }, (in) -> {
380             final byte[] tmp = new byte[128];
381             in.readFully(tmp, 8, 32);
382             assertArrayEquals(Arrays.copyOfRange(TEST_BYTES, 8, 8 + 32),
383                     Arrays.copyOfRange(tmp, 8, 8 + 32));
384             assertEquals(64, in.readInt());
385         });
386     }
387 
388     @Test
testBounce_Mixed()389     public void testBounce_Mixed() throws Exception {
390         doBounce((out) -> {
391             out.writeBoolean(true);
392             out.writeBoolean(false);
393             out.writeByte(1);
394             out.writeShort(2);
395             out.writeInt(4);
396             out.writeUTF(TEST_SHORT_STRING);
397             out.writeUTF(TEST_LONG_STRING);
398             out.writeLong(8L);
399             out.writeFloat(16f);
400             out.writeDouble(32d);
401         }, (in) -> {
402             assertEquals(true, in.readBoolean());
403             assertEquals(false, in.readBoolean());
404             assertEquals(1, in.readByte());
405             assertEquals(2, in.readShort());
406             assertEquals(4, in.readInt());
407             assertEquals(TEST_SHORT_STRING, in.readUTF());
408             assertEquals(TEST_LONG_STRING, in.readUTF());
409             assertEquals(8L, in.readLong());
410             assertEquals(16f, in.readFloat(), 0.01);
411             assertEquals(32d, in.readDouble(), 0.01);
412         });
413     }
414 
415     /**
416      * Buffer size to use for {@link #doBounce}; purposefully chosen to be a
417      * small prime number to help uncover edge cases.
418      */
419     private static final int BOUNCE_SIZE = 11;
420 
421     /**
422      * Number of times to repeat message when bouncing; repeating is used to
423      * help uncover edge cases.
424      */
425     private static final int BOUNCE_REPEAT = 1_000;
426 
427     /**
428      * Verify that some common data can be written and read back, effectively
429      * "bouncing" it through a serialized representation.
430      */
doBounce(@onNull ThrowingConsumer<FastDataOutput> out, @NonNull ThrowingConsumer<FastDataInput> in)431     private void doBounce(@NonNull ThrowingConsumer<FastDataOutput> out,
432             @NonNull ThrowingConsumer<FastDataInput> in) throws Exception {
433         doBounce(out, in, BOUNCE_REPEAT);
434     }
435 
doBounce(@onNull ThrowingConsumer<FastDataOutput> out, @NonNull ThrowingConsumer<FastDataInput> in, int count)436     private void doBounce(@NonNull ThrowingConsumer<FastDataOutput> out,
437             @NonNull ThrowingConsumer<FastDataInput> in, int count) throws Exception {
438         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
439         final FastDataOutput outData = createFastDataOutput(outStream, BOUNCE_SIZE);
440         for (int i = 0; i < count; i++) {
441             out.accept(outData);
442         }
443         outData.flush();
444 
445         final ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
446         final FastDataInput inData = createFastDataInput(inStream, BOUNCE_SIZE);
447         for (int i = 0; i < count; i++) {
448             in.accept(inData);
449         }
450     }
451 
assertThrows(Class<T> clazz, ThrowingRunnable r)452     private static <T extends Exception> void assertThrows(Class<T> clazz, ThrowingRunnable r)
453             throws Exception {
454         try {
455             r.run();
456             fail("Expected " + clazz + " to be thrown");
457         } catch (Exception e) {
458             if (!clazz.isAssignableFrom(e.getClass())) {
459                 throw e;
460             }
461         }
462     }
463 
464     public interface ThrowingRunnable {
run()465         void run() throws Exception;
466     }
467 
468     public interface ThrowingConsumer<T> extends Consumer<T> {
acceptOrThrow(T t)469         void acceptOrThrow(T t) throws Exception;
470 
471         @Override
accept(T t)472         default void accept(T t) {
473             try {
474                 acceptOrThrow(t);
475             } catch (Exception ex) {
476                 throw ExceptionUtils.propagate(ex);
477             }
478         }
479     }
480 }
481