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