|
7 | 7 | import io.github.dfa1.vortex.core.array.LongArray; |
8 | 8 | import io.github.dfa1.vortex.core.array.MaskedArray; |
9 | 9 | import io.github.dfa1.vortex.proto.EncodingProtos; |
| 10 | +import net.jqwik.api.ForAll; |
| 11 | +import net.jqwik.api.Property; |
| 12 | +import net.jqwik.api.constraints.Size; |
10 | 13 | import org.junit.jupiter.api.Nested; |
11 | 14 | import org.junit.jupiter.api.Test; |
12 | 15 | import org.junit.jupiter.params.ParameterizedTest; |
13 | 16 | import org.junit.jupiter.params.provider.EnumSource; |
| 17 | +import org.junit.jupiter.params.provider.ValueSource; |
14 | 18 |
|
15 | 19 | import java.lang.foreign.Arena; |
16 | 20 | import java.lang.foreign.MemorySegment; |
@@ -396,4 +400,122 @@ void decode_nullable_allValid_returnsMaskedWithAllValues() { |
396 | 400 | assertThat(((LongArray) masked.child(0)).getLong(1)).isEqualTo(20L); |
397 | 401 | } |
398 | 402 | } |
| 403 | + |
| 404 | + /// Adversarial coverage: malformed inputs must throw VortexException — never AIOOBE, NPE, or OOM. |
| 405 | + @Nested |
| 406 | + class Adversarial { |
| 407 | + |
| 408 | + /// Random chunk-meta bytes — any exception must be a VortexException, not a JVM crash exception. |
| 409 | + @Property(tries = 50) |
| 410 | + void randomChunkMetaBytes_neverThrowsJvmException( |
| 411 | + @ForAll @Size(min = 1, max = 64) byte[] chunkMetaBytes) { |
| 412 | + // Given — valid pco header + 1 chunk with 1 page of 1 value; garbage chunk-meta bytes. |
| 413 | + var sut = new PcoEncoding(); |
| 414 | + DecodeContext ctx = ctxWith( |
| 415 | + metaWithOneChunk(1), |
| 416 | + new DType.Primitive(PType.U64, false), |
| 417 | + 1, |
| 418 | + new MemorySegment[]{segmentOf(chunkMetaBytes), segmentOf((byte) 0x00)}); |
| 419 | + |
| 420 | + // When / Then — either succeeds or throws VortexException; never AIOOBE/NPE/OOM |
| 421 | + try { |
| 422 | + sut.decode(ctx); |
| 423 | + } catch (VortexException ignored) { |
| 424 | + // expected — malformed input |
| 425 | + } |
| 426 | + } |
| 427 | + |
| 428 | + /// Random page bytes after a valid Classic-mode chunk meta — must not crash the JVM. |
| 429 | + @Property(tries = 50) |
| 430 | + void randomPageBytes_classicMode_neverThrowsJvmException( |
| 431 | + @ForAll @Size(min = 4, max = 128) byte[] pageBytes) { |
| 432 | + // Given — Classic mode, delta=NoOp, ansSizeLog=0, nBins=0 chunk meta. |
| 433 | + var sut = new PcoEncoding(); |
| 434 | + // byte0: mode=0 (bits3:0), deltaVariant=0 (bits7:4) → 0x00 |
| 435 | + // byte1: ansSizeLog=0 (bits3:0), nBins low bits = 0 |
| 436 | + // bytes 2-3: nBins high bits = 0 |
| 437 | + DecodeContext ctx = ctxWith( |
| 438 | + metaWithOneChunk(1), |
| 439 | + new DType.Primitive(PType.U64, false), |
| 440 | + 1, |
| 441 | + new MemorySegment[]{segmentOf((byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00), |
| 442 | + segmentOf(pageBytes)}); |
| 443 | + |
| 444 | + // When / Then |
| 445 | + try { |
| 446 | + sut.decode(ctx); |
| 447 | + } catch (VortexException ignored) { |
| 448 | + // expected — malformed page data |
| 449 | + } |
| 450 | + } |
| 451 | + |
| 452 | + /// Invalid mode nibbles (5–15) must produce a VortexException naming the mode number. |
| 453 | + @ParameterizedTest |
| 454 | + @ValueSource(ints = {5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}) |
| 455 | + void invalidModeNibble_throwsVortexException(int modeNibble) { |
| 456 | + // Given — chunk meta with unsupported mode nibble in bits[3:0]. |
| 457 | + var sut = new PcoEncoding(); |
| 458 | + // bits[3:0] = modeNibble, delta nibble doesn't matter (won't be reached) |
| 459 | + byte modeByte = (byte) (modeNibble & 0x0F); |
| 460 | + DecodeContext ctx = ctxWith( |
| 461 | + metaWithOneChunk(1), |
| 462 | + new DType.Primitive(PType.U64, false), |
| 463 | + 1, |
| 464 | + new MemorySegment[]{ |
| 465 | + segmentOf(modeByte, (byte) 0x00, (byte) 0x00, (byte) 0x00), |
| 466 | + segmentOf((byte) 0x00)}); |
| 467 | + |
| 468 | + // When / Then |
| 469 | + assertThatThrownBy(() -> sut.decode(ctx)) |
| 470 | + .isInstanceOf(VortexException.class) |
| 471 | + .hasMessageContaining("pco mode " + modeNibble); |
| 472 | + } |
| 473 | + |
| 474 | + /// Invalid delta variants (4–15) must produce a VortexException naming the variant number. |
| 475 | + @ParameterizedTest |
| 476 | + @ValueSource(ints = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}) |
| 477 | + void invalidDeltaVariant_throwsVortexException(int deltaVariant) { |
| 478 | + // Given — Classic mode (nibble=0) + invalid delta nibble in bits[7:4]. |
| 479 | + var sut = new PcoEncoding(); |
| 480 | + // byte0: bits[3:0]=mode=0, bits[7:4]=deltaVariant |
| 481 | + byte modeDeltaByte = (byte) ((deltaVariant & 0x0F) << 4); |
| 482 | + DecodeContext ctx = ctxWith( |
| 483 | + metaWithOneChunk(1), |
| 484 | + new DType.Primitive(PType.U64, false), |
| 485 | + 1, |
| 486 | + new MemorySegment[]{ |
| 487 | + segmentOf(modeDeltaByte, (byte) 0x00, (byte) 0x00, (byte) 0x00), |
| 488 | + segmentOf((byte) 0x00)}); |
| 489 | + |
| 490 | + // When / Then |
| 491 | + assertThatThrownBy(() -> sut.decode(ctx)) |
| 492 | + .isInstanceOf(VortexException.class) |
| 493 | + .hasMessageContaining("delta variant " + deltaVariant); |
| 494 | + } |
| 495 | + |
| 496 | + /// Conv1 delta with 64-bit dtype must throw VortexException; pcodec only supports 16/32-bit Conv1. |
| 497 | + @ParameterizedTest |
| 498 | + @EnumSource(value = PType.class, names = {"I64", "U64", "F64"}) |
| 499 | + void conv1Delta_with64BitDtype_throwsVortexException(PType ptype) { |
| 500 | + // Given — Conv1 delta variant (nibble=3 in bits[7:4]), Classic mode (nibble=0 in bits[3:0]). |
| 501 | + // byte0: bits[3:0]=0 (Classic), bits[7:4]=3 (Conv1) → 0x30 |
| 502 | + // Remaining bytes: conv1 bit fields (don't matter — error fires before parsing them). |
| 503 | + var sut = new PcoEncoding(); |
| 504 | + DecodeContext ctx = ctxWith( |
| 505 | + metaWithOneChunk(1), |
| 506 | + new DType.Primitive(ptype, false), |
| 507 | + 1, |
| 508 | + new MemorySegment[]{ |
| 509 | + segmentOf((byte) 0x30, (byte) 0x00, (byte) 0x00, (byte) 0x00, |
| 510 | + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, |
| 511 | + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, |
| 512 | + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00), |
| 513 | + segmentOf((byte) 0x00)}); |
| 514 | + |
| 515 | + // When / Then |
| 516 | + assertThatThrownBy(() -> sut.decode(ctx)) |
| 517 | + .isInstanceOf(VortexException.class) |
| 518 | + .hasMessageContaining("Conv1"); |
| 519 | + } |
| 520 | + } |
399 | 521 | } |
0 commit comments