labels: apply_labels integration tests + class_reader robustness fuzz tests
Closes the final two audit items from this session. labels::apply_labels: factored out of apply() so the matching logic is unit-testable without needing a SectorReader / UdfFs. 11 new tests in apply_tests cover: - codec_hint + variant flow through to AudioStream.label - purpose set on audio with no label English text - name fallback only when purpose=Normal (CLI owns purpose i18n) - subtitle SDH qualifier set; forced flag flipped on Forced - per-type 1-based indexing (audio #2 maps to 2nd audio stream, not 2nd stream overall) - labels for nonexistent streams are no-ops - empty labels list leaves streams untouched - fill_defaults generates audio + video labels; preserves existing class_reader: robustness smoke tests. ClassFile::parse must NEVER panic on adversarial input — only return Err. 9 new tests: - empty input - short magic (0..4 bytes) - wrong magic - truncated after magic - bad CP tag - truncated UTF-8 in CP - 200 random byte buffers (deterministic xorshift) - 100 magic + random tail (most adversarial — magic check passes, everything else garbage) - instructions iter on random code (200 buffers) - instruction_size on every opcode 0..255 with varied tail buffers - modified_utf8 on random byte buffers (500) The xorshift PRNG keeps the tests deterministic (no rand dep) and reproducible — failures will be the same buffer every time. This is the lightweight alternative to a cargo-fuzz setup; if/when we adopt cargo-fuzz, these tests stay as regression cases. All 451 tests passing on cargo +1.86 fmt + clippy + test.
This commit is contained in:
@@ -1132,4 +1132,168 @@ mod tests {
|
||||
};
|
||||
assert_eq!(i.cp_index(), None);
|
||||
}
|
||||
|
||||
// ── Robustness smoke tests ──────────────────────────────────────────────
|
||||
//
|
||||
// ClassFile::parse must NEVER panic on adversarial input, only
|
||||
// return Err. These tests feed a battery of malformed byte
|
||||
// sequences and assert Err results — they're the lightweight
|
||||
// alternative to a full cargo-fuzz target (which would need
|
||||
// nightly + separate crate). If we adopt cargo-fuzz later, these
|
||||
// tests stay as deterministic regression cases.
|
||||
|
||||
/// Tiny pseudo-random byte generator — deterministic + reproducible
|
||||
/// without needing a `rand` dep. xorshift64*; good enough for
|
||||
/// generating adversarial byte payloads.
|
||||
fn xorshift(state: &mut u64) -> u64 {
|
||||
let mut x = *state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
*state = x;
|
||||
x
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_empty_input() {
|
||||
assert!(ClassFile::parse(&[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_short_magic() {
|
||||
for n in 0..4 {
|
||||
let buf = vec![0u8; n];
|
||||
assert!(ClassFile::parse(&buf).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_wrong_magic() {
|
||||
let buf = vec![0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0];
|
||||
match ClassFile::parse(&buf) {
|
||||
Err(Error::BadMagic(0xDEADBEEF)) => {}
|
||||
Err(other) => panic!("expected BadMagic, got {:?}", other),
|
||||
Ok(_) => panic!("expected BadMagic error, got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_truncated_after_magic() {
|
||||
// CAFEBABE + 1 byte = not enough for minor_version (u16).
|
||||
let buf = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00];
|
||||
assert!(ClassFile::parse(&buf).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_bad_cp_tag() {
|
||||
// CAFEBABE + minor/major(0,0,0,52) + cp_count=2 + tag=99 (unknown).
|
||||
let buf = vec![
|
||||
0xCA, 0xFE, 0xBA, 0xBE, // magic
|
||||
0x00, 0x00, // minor
|
||||
0x00, 0x34, // major
|
||||
0x00, 0x02, // cp_count = 2 (one entry)
|
||||
99, // unknown tag
|
||||
];
|
||||
match ClassFile::parse(&buf) {
|
||||
Err(_) => {} // BadCpTag, BadMagic, or any other malformed-input err
|
||||
Ok(_) => panic!("expected error on unknown CP tag"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_truncated_utf8() {
|
||||
// CAFEBABE + minor/major + cp_count=2 + tag=1 (Utf8) + length=10 + 3 bytes (< 10).
|
||||
let buf = vec![
|
||||
0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34, 0x00, 0x02, // cp_count=2
|
||||
1, // Utf8 tag
|
||||
0x00, 10, // length=10
|
||||
b'h', b'i', b'!', // only 3 bytes (truncated)
|
||||
];
|
||||
assert!(ClassFile::parse(&buf).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_does_not_panic_on_random_bytes() {
|
||||
// 200 deterministic-pseudo-random byte buffers of varying
|
||||
// lengths. The contract: never panic, only return Err (or in
|
||||
// the vanishingly unlikely case of a coincidentally-valid
|
||||
// buffer, Ok — we don't assert one or the other).
|
||||
let mut state: u64 = 0xDEADBEEF_DEADBEEF;
|
||||
for _ in 0..200 {
|
||||
let len = (xorshift(&mut state) % 256) as usize;
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
buf.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
// No panic. Result doesn't matter — Err is expected for
|
||||
// 99%+ of inputs.
|
||||
let _ = ClassFile::parse(&buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_does_not_panic_on_valid_magic_random_tail() {
|
||||
// 100 buffers that start with the magic + plausible
|
||||
// minor/major but have garbage afterwards. These are the
|
||||
// most adversarial — they pass the magic check and then
|
||||
// exercise every other parser path.
|
||||
let mut state: u64 = 0xCAFEBABE_DEADBEEF;
|
||||
for _ in 0..100 {
|
||||
let mut buf = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34];
|
||||
let tail_len = (xorshift(&mut state) % 512) as usize;
|
||||
for _ in 0..tail_len {
|
||||
buf.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
let _ = ClassFile::parse(&buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instructions_never_panic_on_random_code() {
|
||||
// Bytecode iterator must not panic on any byte sequence.
|
||||
let mut state: u64 = 0x12345678_87654321;
|
||||
for _ in 0..200 {
|
||||
let len = (xorshift(&mut state) % 256) as usize;
|
||||
let mut code = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
code.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
let attr = CodeAttribute {
|
||||
max_stack: 0,
|
||||
max_locals: 0,
|
||||
code: &code,
|
||||
};
|
||||
// Bounded — iterator stops on truncated/unknown opcodes.
|
||||
let _: Vec<_> = attr.instructions().collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_size_never_panics() {
|
||||
// Cover every opcode byte 0..=255 with various code-buffer
|
||||
// shapes. instruction_size returns Option but must not panic.
|
||||
for op in 0u8..=255 {
|
||||
for tail_len in [0usize, 1, 2, 3, 7, 16, 32] {
|
||||
let mut buf = vec![op];
|
||||
for i in 0..tail_len {
|
||||
buf.push((i as u8).wrapping_mul(31));
|
||||
}
|
||||
let _ = instruction_size(&buf, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_utf8_never_panics_on_random_bytes() {
|
||||
let mut state: u64 = 0xABCDEF12_34567890;
|
||||
for _ in 0..500 {
|
||||
let len = (xorshift(&mut state) % 64) as usize;
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
buf.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
// Either Ok or Err; never a panic.
|
||||
let _ = decode_modified_utf8(&buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user