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