libfreemkv 0.31.2: comprehensive spec-grounded test suite (~950 tests)
Test-hardening release, no runtime changes. Adds spec-grounded unit tests across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers, MKV/EBML container output, the mux pipeline, sector prefetch + decrypt decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each test is grounded in the format spec or real on-disc behavior and verified to fail under a targeted source mutation. No behavior changed.
This commit is contained in:
+392
@@ -781,4 +781,396 @@ mod tests {
|
||||
assert_eq!(size, u64::MAX);
|
||||
assert_eq!(consumed, 8);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// write_id — exact width selection per EBML element-ID ranges
|
||||
// (Matroska/EBML spec: an element ID is written verbatim; its
|
||||
// declared width is implied by the position of the leading 1 bit.
|
||||
// write_id must pick the minimal whole-byte encoding so the ID
|
||||
// round-trips and parsers see the same width.)
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn write_id_exact_bytes_per_width() {
|
||||
// 1-byte ID (high bit set): emitted as a single byte verbatim.
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0xA3).unwrap(); // SimpleBlock
|
||||
assert_eq!(b, [0xA3]);
|
||||
|
||||
// The boundary just above 1 byte: 0x100 must be a 2-byte ID. A
|
||||
// mutation that widened the 1-byte branch (id <= 0x1FF) would drop
|
||||
// the high byte here.
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0x0100).unwrap();
|
||||
assert_eq!(b, [0x01, 0x00]);
|
||||
|
||||
// 2-byte ID written MSB-first.
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0x4286).unwrap(); // EBMLVersion
|
||||
assert_eq!(b, [0x42, 0x86]);
|
||||
|
||||
// 3-byte boundary: 0x1_0000 must be 3 bytes.
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0x01_0000).unwrap();
|
||||
assert_eq!(b, [0x01, 0x00, 0x00]);
|
||||
|
||||
// 3-byte ID (Language = 0x22B59C).
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0x22_B59C).unwrap();
|
||||
assert_eq!(b, [0x22, 0xB5, 0x9C]);
|
||||
|
||||
// 4-byte boundary: 0x100_0000 must be 4 bytes.
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0x0100_0000).unwrap();
|
||||
assert_eq!(b, [0x01, 0x00, 0x00, 0x00]);
|
||||
|
||||
// 4-byte ID (Segment = 0x18538067) MSB-first.
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, 0x1853_8067).unwrap();
|
||||
assert_eq!(b, [0x18, 0x53, 0x80, 0x67]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_id_rejects_zero_first_byte() {
|
||||
// A first byte of 0x00 has no length marker in any of bits 7..4, so
|
||||
// read_id falls through to the else branch and must reject it (an
|
||||
// EBML ID wider than 4 bytes is not representable here). Otherwise the
|
||||
// parser would desync.
|
||||
let mut c = Cursor::new(&[0x00u8, 0x11, 0x22, 0x33]);
|
||||
let e = read_id(&mut c).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// write_uint — the SIZE byte must reflect the minimal big-endian
|
||||
// value width (1/2/3/4/8). The Matroska spec stores unsigned ints
|
||||
// big-endian with no leading-zero bytes; the declared element size
|
||||
// is exactly that width. A boundary bug would write the wrong size
|
||||
// and desync every following element.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn write_uint_size_byte_matches_value_width() {
|
||||
// (value, expected_size_byte, expected_payload)
|
||||
// size byte is a 1-byte VINT: 0x80 | len.
|
||||
let cases: &[(u64, u8, &[u8])] = &[
|
||||
(0x00, 0x81, &[0x00]), // 1 byte
|
||||
(0xFF, 0x81, &[0xFF]), // 1 byte (boundary high)
|
||||
(0x0100, 0x82, &[0x01, 0x00]), // 2 bytes (just over u8)
|
||||
(0xFFFF, 0x82, &[0xFF, 0xFF]), // 2 bytes (boundary high)
|
||||
(0x01_0000, 0x83, &[0x01, 0x00, 0x00]), // 3 bytes
|
||||
(0xFF_FFFF, 0x83, &[0xFF, 0xFF, 0xFF]), // 3 bytes (boundary high)
|
||||
(0x0100_0000, 0x84, &[0x01, 0x00, 0x00, 0x00]), // 4 bytes
|
||||
(0xFFFF_FFFF, 0x84, &[0xFF, 0xFF, 0xFF, 0xFF]), // 4 bytes (boundary high)
|
||||
// Just over u32 → jumps straight to 8 bytes (no 5/6/7 path).
|
||||
(
|
||||
0x1_0000_0000,
|
||||
0x88,
|
||||
&[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00],
|
||||
),
|
||||
];
|
||||
let id = EBML_VERSION; // 2-byte ID 0x4286
|
||||
for (val, size_byte, payload) in cases {
|
||||
let mut buf = Vec::new();
|
||||
write_uint(&mut buf, id, *val).unwrap();
|
||||
assert_eq!(&buf[0..2], &[0x42, 0x86], "ID prefix for val {val:#x}");
|
||||
assert_eq!(buf[2], *size_byte, "size byte for val {val:#x}");
|
||||
assert_eq!(&buf[3..], *payload, "payload for val {val:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_uint_zero_is_one_byte_not_zero_length() {
|
||||
// EBML stores 0 as a single 0x00 byte (size 1), NOT a zero-length
|
||||
// element. A muxer reader expects to consume exactly one payload byte.
|
||||
let mut buf = Vec::new();
|
||||
write_uint(&mut buf, EBML_VERSION, 0).unwrap();
|
||||
// ID(2) + size(1=0x81) + one payload byte 0x00.
|
||||
assert_eq!(buf, [0x42, 0x86, 0x81, 0x00]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// write_float — EBML floats here are always 8-byte IEEE-754 doubles,
|
||||
// big-endian (Matroska SamplingFrequency/Duration). size byte = 0x88.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn write_float_is_8_byte_big_endian_double() {
|
||||
let mut buf = Vec::new();
|
||||
write_float(&mut buf, DURATION, 48000.0).unwrap();
|
||||
// ID DURATION = 0x4489 (2 bytes), size = 0x88 (8), then BE f64.
|
||||
assert_eq!(&buf[0..2], &[0x44, 0x89]);
|
||||
assert_eq!(buf[2], 0x88, "float element must declare 8-byte size");
|
||||
assert_eq!(&buf[3..11], &48000.0f64.to_be_bytes());
|
||||
// The reader (4-byte path) must yield an f32-promoted value, while the
|
||||
// 8-byte path yields the exact double.
|
||||
let got = read_float_val(&mut Cursor::new(&buf[3..11]), 8).unwrap();
|
||||
assert_eq!(got.to_bits(), 48000.0f64.to_bits());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// write_string / write_binary — declared size must equal the byte
|
||||
// length (UTF-8 byte count, not char count) so the reader consumes
|
||||
// exactly the payload and no more.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn write_string_size_is_utf8_byte_count_not_char_count() {
|
||||
// "é" is 2 UTF-8 bytes; the size field must be 2, not 1.
|
||||
let mut buf = Vec::new();
|
||||
write_string(&mut buf, EBML_DOC_TYPE, "é").unwrap();
|
||||
assert_eq!(&buf[0..2], &[0x42, 0x82]); // DocType ID
|
||||
assert_eq!(buf[2], 0x80 | 2, "size must be UTF-8 byte length (2)");
|
||||
assert_eq!(&buf[3..], "é".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_binary_declares_exact_length() {
|
||||
let data = [0xDE, 0xAD, 0xBE, 0xEF, 0x00];
|
||||
let mut buf = Vec::new();
|
||||
write_binary(&mut buf, CODEC_PRIVATE, &data).unwrap();
|
||||
// CODEC_PRIVATE id 0x63A2 (2 bytes), size 0x85 (len 5), then data.
|
||||
assert_eq!(&buf[0..2], &[0x63, 0xA2]);
|
||||
assert_eq!(buf[2], 0x80 | 5);
|
||||
assert_eq!(&buf[3..], &data);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// read_string_val — Matroska strings may be null-padded; the reader
|
||||
// strips trailing NULs but must preserve interior content and the
|
||||
// payload byte-count consumed.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn read_string_val_strips_only_trailing_nulls() {
|
||||
// "ab\0\0" → "ab"; interior content must not be touched.
|
||||
let raw = b"ab\0\0";
|
||||
let s = read_string_val(&mut Cursor::new(raw), raw.len()).unwrap();
|
||||
assert_eq!(s, "ab");
|
||||
// A string that is ALL nulls collapses to empty (every byte popped).
|
||||
let raw = b"\0\0\0";
|
||||
let s = read_string_val(&mut Cursor::new(raw), raw.len()).unwrap();
|
||||
assert_eq!(s, "");
|
||||
// An interior NUL is NOT a terminator for the strip loop (it only pops
|
||||
// from the tail), so "a\0b" keeps the interior NUL.
|
||||
let raw = b"a\0b";
|
||||
let s = read_string_val(&mut Cursor::new(raw), raw.len()).unwrap();
|
||||
assert_eq!(s.as_bytes(), b"a\0b");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// read_uint_val — big-endian assembly; an EBML uint never exceeds 8
|
||||
// bytes (the reader rejects len>8 to avoid a stack OOB).
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn read_uint_val_big_endian_and_len_zero() {
|
||||
// Big-endian: 0x01 0x02 0x03 → 0x010203.
|
||||
let v = read_uint_val(&mut Cursor::new(&[0x01u8, 0x02, 0x03]), 3).unwrap();
|
||||
assert_eq!(v, 0x01_0203);
|
||||
// len 0 yields 0 with no read.
|
||||
let v = read_uint_val(&mut Cursor::new(&[] as &[u8]), 0).unwrap();
|
||||
assert_eq!(v, 0);
|
||||
// Full 8-byte width assembles correctly (no truncation).
|
||||
let bytes = [0x12u8, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
|
||||
let v = read_uint_val(&mut Cursor::new(&bytes), 8).unwrap();
|
||||
assert_eq!(v, 0x1234_5678_9ABC_DEF0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_uint_val_rejects_len_above_8() {
|
||||
// len 9 would index past the [0u8; 8] buffer → OOB/DoS on untrusted
|
||||
// input. Must be a clean MkvInvalid.
|
||||
let e = read_uint_val(&mut Cursor::new(&[0u8; 16]), 9).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// read_float_val — exactly 0/4/8 byte widths; 4-byte is an f32
|
||||
// promoted to f64, 8-byte is an exact f64.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn read_float_val_4_byte_is_f32_promoted() {
|
||||
// 1.5 as a 32-bit float → 0x3FC00000.
|
||||
let bytes = 1.5f32.to_be_bytes();
|
||||
let v = read_float_val(&mut Cursor::new(&bytes), 4).unwrap();
|
||||
assert_eq!(v, 1.5f64);
|
||||
// A value with no exact f32 representation loses precision exactly as
|
||||
// f32→f64 would (proves the 4-byte branch uses f32, not f64).
|
||||
let bytes = 0.1f32.to_be_bytes();
|
||||
let v = read_float_val(&mut Cursor::new(&bytes), 4).unwrap();
|
||||
assert_eq!(v, 0.1f32 as f64);
|
||||
assert_ne!(v, 0.1f64, "4-byte path must be f32, losing f64 precision");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_float_val_rejects_odd_widths() {
|
||||
// Only 0/4/8 are valid; 1,2,3,5,6,7 must error (never over/under-read).
|
||||
for len in [1usize, 2, 3, 5, 6, 7] {
|
||||
let e = read_float_val(&mut Cursor::new(&[0u8; 8]), len).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::InvalidData, "len {len}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// read_binary_val / read_exact_bounded — a declared length that
|
||||
// exceeds the bytes actually present is a truncated (malformed)
|
||||
// element and must error without allocating the full declared size.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn read_binary_val_short_read_errors() {
|
||||
// Declare 100 bytes but supply 4 → MkvInvalid (truncated element).
|
||||
let e = read_binary_val(&mut Cursor::new(&[1u8, 2, 3, 4]), 100).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
|
||||
// Exact-length read returns the bytes verbatim.
|
||||
let v = read_binary_val(&mut Cursor::new(&[1u8, 2, 3, 4]), 4).unwrap();
|
||||
assert_eq!(v, vec![1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// read_element_header — header_bytes is id_len + size_len, and a
|
||||
// truncated header (EOF mid-size) surfaces as an error.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn read_element_header_reports_total_header_len() {
|
||||
// 4-byte ID (Segment) + 8-byte unknown size = 12 header bytes.
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, SEGMENT).unwrap();
|
||||
write_unknown_size(&mut buf).unwrap();
|
||||
let (id, size, hdr) = read_element_header(&mut Cursor::new(&buf)).unwrap();
|
||||
assert_eq!(id, SEGMENT);
|
||||
assert_eq!(size, u64::MAX);
|
||||
assert_eq!(hdr, 12, "4-byte id + 8-byte size = 12 header bytes");
|
||||
|
||||
// 1-byte ID (SimpleBlock 0xA3) + 1-byte size = 2 header bytes.
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, SIMPLE_BLOCK).unwrap();
|
||||
write_size(&mut buf, 10).unwrap();
|
||||
let (id, size, hdr) = read_element_header(&mut Cursor::new(&buf)).unwrap();
|
||||
assert_eq!(id, SIMPLE_BLOCK);
|
||||
assert_eq!(size, 10);
|
||||
assert_eq!(hdr, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_id_truncated_after_marker_errors() {
|
||||
// First byte 0x40 promises a 2-byte ID but the second byte is missing.
|
||||
// read_exact must surface EOF, never silently produce a 1-byte ID.
|
||||
let e = read_id(&mut Cursor::new(&[0x40u8])).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// write_size — every declared width-boundary, asserting the exact
|
||||
// VINT bytes (length marker + payload). Grounded in the EBML VINT
|
||||
// spec: width W encodes 7*W payload bits, the highest value of each
|
||||
// width being reserved as the unknown-size sentinel.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn write_size_exact_bytes_at_width_boundaries() {
|
||||
// Largest 1-byte value (126 = 0x7E): marker 0x80 | value.
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 0x7E).unwrap();
|
||||
assert_eq!(b, [0x80 | 0x7E]);
|
||||
// 0x7F is NOT 1-byte here (reserved sentinel region) → 2 bytes.
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 0x7F).unwrap();
|
||||
assert_eq!(b, [0x40, 0x7F]);
|
||||
// Largest 2-byte value below the 0x3FFF sentinel.
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 0x3FFE).unwrap();
|
||||
assert_eq!(b, [0x40 | 0x3F, 0xFE]);
|
||||
// First 3-byte value (0x3FFF goes 3-byte because `< 0x3FFF` is false).
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 0x3FFF).unwrap();
|
||||
assert_eq!(b, [0x20, 0x3F, 0xFF]);
|
||||
// First 4-byte value: 0x1F_FFFF is not < 0x1F_FFFF.
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 0x1F_FFFF).unwrap();
|
||||
assert_eq!(b, [0x10, 0x1F, 0xFF, 0xFF]);
|
||||
// First 8-byte value: 0x0FFF_FFFF is not < 0x0FFF_FFFF.
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 0x0FFF_FFFF).unwrap();
|
||||
assert_eq!(b, [0x01, 0, 0, 0, 0x0F, 0xFF, 0xFF, 0xFF]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// start_master / end_master — the size placeholder is an 8-byte VINT
|
||||
// (0x01 + 7 payload bytes), and end_master must back-patch the exact
|
||||
// body byte count (end - start - 8). This is the core of every nested
|
||||
// Matroska master element; a wrong subtraction silently corrupts the
|
||||
// declared size of EVERY master element in the file.
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn end_master_backpatches_exact_body_size() {
|
||||
let mut c = Cursor::new(Vec::new());
|
||||
let pos = start_master(&mut c, SEGMENT).unwrap();
|
||||
// Body: a 4-byte uint element (ID 0x4286, size 0x81, payload 0x01).
|
||||
write_uint(&mut c, EBML_VERSION, 1).unwrap();
|
||||
end_master(&mut c, pos).unwrap();
|
||||
let data = c.into_inner();
|
||||
// Layout: SEGMENT id (4 bytes) | 8-byte size VINT | body (4 bytes).
|
||||
assert_eq!(&data[0..4], &SEGMENT.to_be_bytes());
|
||||
// The size field is an 8-byte VINT; its payload must equal the body
|
||||
// length (4). 0x01 marker then 7 payload bytes ending in 0x04.
|
||||
assert_eq!(data[4], 0x01);
|
||||
assert_eq!(&data[5..12], &[0, 0, 0, 0, 0, 0, 4]);
|
||||
// Read it back: the header parser sees the exact body size.
|
||||
let (id, size, hdr) = read_element_header(&mut Cursor::new(&data)).unwrap();
|
||||
assert_eq!(id, SEGMENT);
|
||||
assert_eq!(size, 4, "back-patched size must equal body byte count");
|
||||
assert_eq!(hdr, 12);
|
||||
assert_eq!(data.len() as u64, hdr as u64 + size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_master_empty_body_is_zero_size() {
|
||||
// A master with no body must declare size 0 (end == start + 8).
|
||||
let mut c = Cursor::new(Vec::new());
|
||||
let pos = start_master(&mut c, INFO).unwrap();
|
||||
end_master(&mut c, pos).unwrap();
|
||||
let data = c.into_inner();
|
||||
let (id, size, _) = read_element_header(&mut Cursor::new(&data)).unwrap();
|
||||
assert_eq!(id, INFO);
|
||||
assert_eq!(size, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_masters_each_get_correct_size() {
|
||||
// Outer master containing an inner master + a sibling uint. Each
|
||||
// declared size must bound exactly its own body. This is the nested
|
||||
// sizing that mkv.rs relies on for Segment→Tracks→TrackEntry.
|
||||
let mut c = Cursor::new(Vec::new());
|
||||
let outer = start_master(&mut c, TRACKS).unwrap();
|
||||
let inner = start_master(&mut c, TRACK_ENTRY).unwrap();
|
||||
write_uint(&mut c, TRACK_NUMBER, 1).unwrap();
|
||||
end_master(&mut c, inner).unwrap();
|
||||
write_uint(&mut c, TRACK_NUMBER, 2).unwrap();
|
||||
end_master(&mut c, outer).unwrap();
|
||||
let data = c.into_inner();
|
||||
|
||||
let mut cur = Cursor::new(&data);
|
||||
let (oid, osize, _) = read_element_header(&mut cur).unwrap();
|
||||
assert_eq!(oid, TRACKS);
|
||||
let outer_body_start = cur.position();
|
||||
// First child of TRACKS is TRACK_ENTRY.
|
||||
let (iid, isize, _) = read_element_header(&mut cur).unwrap();
|
||||
assert_eq!(iid, TRACK_ENTRY);
|
||||
// Skip TRACK_ENTRY body; the next element must be the sibling uint.
|
||||
cur.set_position(cur.position() + isize);
|
||||
let (sid, ssize, _) = read_element_header(&mut cur).unwrap();
|
||||
assert_eq!(sid, TRACK_NUMBER, "sibling after inner master");
|
||||
// Skip the sibling's body too, then total bytes consumed inside the
|
||||
// outer master must exactly equal its declared size.
|
||||
cur.set_position(cur.position() + ssize);
|
||||
let consumed = cur.position() - outer_body_start;
|
||||
assert_eq!(consumed, osize, "outer size must bound both children");
|
||||
// And the whole buffer is exactly the outer element.
|
||||
assert_eq!(data.len() as u64, outer_body_start + osize);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user