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☃exampletypical☃packagename"; 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