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