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:
Matthew Jackson
2026-06-07 22:28:29 -07:00
parent 2a55bab3ed
commit 8000bae177
85 changed files with 22998 additions and 1 deletions
+194
View File
@@ -479,4 +479,198 @@ mod tests {
let text = String::from_utf8(result).unwrap();
assert_eq!(text, "palette: 808080\n");
}
// --- SPU_size boundary: completes exactly at declared size ---
#[test]
fn spu_completes_exactly_at_declared_size() {
// SPU_size is the total byte length including the 2-byte header. When the
// accumulated bytes reach exactly the declared size, the unit emits.
// Declared = 6, head carries all 6 → emits immediately on the head PES.
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x06, 0xAA, 0xBB, 0xCC, 0xDD]; // 6 bytes, declared 6
let f = parser.parse(&make_pes(head.clone(), Some(90000)));
assert_eq!(f.len(), 1, "complete-on-arrival SPU emits at once");
assert_eq!(f[0].data, head);
assert!(parser.pending.is_none(), "nothing left pending");
}
#[test]
fn spu_one_byte_short_waits_then_completes() {
// Declared 7 but head has 6 → held; a 1-byte continuation completes it.
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x07, 0xAA, 0xBB, 0xCC, 0xDD]; // 6 of 7
assert!(
parser
.parse(&make_pes(head.clone(), Some(90000)))
.is_empty()
);
let f = parser.parse(&make_pes(vec![0xEE], None)); // continuation
assert_eq!(f.len(), 1);
let mut expect = head;
expect.push(0xEE);
assert_eq!(
f[0].data, expect,
"reassembled to exactly the declared size"
);
}
#[test]
fn spu_overshoot_emits_all_buffered_bytes() {
// If a continuation pushes the buffer PAST the declared size, the unit
// still emits with all buffered bytes (>= size triggers emit). Declared
// 5, head 4, continuation 4 → 8 buffered, emits all 8.
let mut parser = DvdSubParser::new(None);
let head = vec![0x00, 0x05, 0xAA, 0xBB]; // 4 of 5
assert!(parser.parse(&make_pes(head, Some(90000))).is_empty());
let f = parser.parse(&make_pes(vec![0xCC, 0xDD, 0xEE, 0xFF], None));
assert_eq!(f.len(), 1);
assert_eq!(
f[0].data,
vec![0x00, 0x05, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
"all buffered bytes emitted, not truncated to declared size"
);
}
// --- MAX_SPU_BYTES bound ---
#[test]
fn head_pes_larger_than_max_spu_is_truncated() {
// A head PES larger than MAX_SPU_BYTES (0xFFFF) is truncated to the cap
// when buffered. Declared size in the first 2 bytes = 0xFFFF.
let mut parser = DvdSubParser::new(None);
let mut head = vec![0xFF, 0xFF]; // declared 0xFFFF
head.extend(std::iter::repeat_n(0xAB, MAX_SPU_BYTES + 100));
// The declared size 0xFFFF == buffered cap, so it completes at the cap.
let f = parser.parse(&make_pes(head, Some(90000)));
assert_eq!(f.len(), 1);
assert_eq!(
f[0].data.len(),
MAX_SPU_BYTES,
"head buffer truncated to MAX_SPU_BYTES"
);
}
#[test]
fn continuation_appends_bounded_by_max_spu() {
// A continuation must not push the buffer past MAX_SPU_BYTES. Declared a
// huge size so it never completes naturally, then flood continuations.
let mut parser = DvdSubParser::new(None);
let mut head = vec![0xFF, 0xFE]; // declared 0xFFFE
head.extend(std::iter::repeat_n(0x11, 1000));
assert!(parser.parse(&make_pes(head, Some(90000))).is_empty());
// Flood continuations far exceeding the cap.
for _ in 0..100 {
let _ = parser.parse(&make_pes(vec![0x22u8; 2000], None));
}
let pending_len = parser
.pending
.as_ref()
.map(|(_, _, b)| b.len())
.unwrap_or(0);
assert!(
pending_len <= MAX_SPU_BYTES,
"pending {pending_len} exceeded MAX_SPU_BYTES {MAX_SPU_BYTES}"
);
}
// --- one-byte head: too short to carry SPU_size ---
#[test]
fn single_byte_head_passes_through_as_lone_frame() {
// < 2 bytes can't carry the SPU_size field → passed through as a lone
// frame, not stored pending.
let mut parser = DvdSubParser::new(None);
let f = parser.parse(&make_pes(vec![0xAB], Some(90000)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].data, vec![0xAB]);
assert!(parser.pending.is_none());
}
#[test]
fn declared_size_one_passes_through() {
// declared = 1 < 2 (the 2-byte header itself) is malformed → lone frame.
let mut parser = DvdSubParser::new(None);
let data = vec![0x00, 0x01, 0xAB];
let f = parser.parse(&make_pes(data.clone(), Some(90000)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].data, data);
assert!(parser.pending.is_none());
}
#[test]
fn no_pts_short_segment_without_pending_passes_through() {
// A no-PTS segment with NO pending SPU and too few bytes to carry an
// SPU_size (< 2) has nothing to attach to and can't start a unit → passed
// through as a lone frame at pts 0 (the documented fallback).
let mut parser = DvdSubParser::new(None);
let f = parser.parse(&make_pes(vec![0xAA], None));
assert_eq!(f.len(), 1);
assert_eq!(f[0].pts_ns, 0, "lone no-PTS segment falls back to pts 0");
assert_eq!(f[0].data, vec![0xAA]);
}
#[test]
fn no_pts_sized_segment_without_pending_starts_new_spu() {
// A no-PTS segment with no pending but a valid SPU_size (>= 2) and an
// incomplete length begins a fresh pending SPU (the demuxer may have
// dropped the PTS, but the size field is authoritative for boundary).
// declared = 16, only 3 bytes present → held pending, no emit.
let mut parser = DvdSubParser::new(None);
let f = parser.parse(&make_pes(vec![0x00, 0x10, 0xAA], None));
assert!(f.is_empty(), "incomplete sized segment held, not emitted");
assert!(parser.pending.is_some(), "started a new pending SPU");
assert_eq!(parser.pending.as_ref().unwrap().0, 0, "pts 0 (no PTS)");
}
#[test]
fn flush_empty_when_nothing_pending() {
let mut parser = DvdSubParser::new(None);
assert!(parser.flush().is_empty());
}
// --- YCbCr → RGB green channel + neutral chroma ---
#[test]
fn ycbcr_green_channel_formula() {
// G = Y - 0.344*(Cb-128) - 0.714*(Cr-128). For pure-ish green choose
// Y=145, Cb=54, Cr=34: G should be high, R and B low. (Full-range BT.601
// per the module's deliberate convention.)
let [r, g, b] = ycbcr_to_rgb(&[0x00, 145, 54, 34]);
assert!(g > 200, "G high for green, got {g}");
assert!(r < 80, "R low for green, got {r}");
assert!(b < 80, "B low for green, got {b}");
}
#[test]
fn ycbcr_neutral_chroma_is_grey() {
// Cb=Cr=128 (neutral) → R=G=B=Y for any Y. (Confirms the chroma terms
// vanish at 128.)
for y in [0u8, 64, 128, 200, 255] {
let [r, g, b] = ycbcr_to_rgb(&[0x00, y, 128, 128]);
assert_eq!([r, g, b], [y, y, y], "neutral chroma → grey at Y={y}");
}
}
#[test]
fn ycbcr_blue_channel_clamps_high() {
// B = Y + 1.772*(Cb-128). Y=128, Cb=255 → 128 + 1.772*127 ≈ 353 → clamp 255.
let [_r, _g, b] = ycbcr_to_rgb(&[0x00, 128, 255, 128]);
assert_eq!(b, 255, "blue clamps at 255");
}
#[test]
fn format_palette_empty_is_just_prefix() {
// An empty palette yields "palette: \n" (prefix + newline, no entries).
let result = format_palette(&[]);
assert_eq!(String::from_utf8(result).unwrap(), "palette: \n");
}
#[test]
fn format_palette_pads_each_channel_to_two_hex_digits() {
// Each RGB channel is formatted as exactly 2 hex digits (zero-padded).
// Y=16,neutral → 0x10 → "101010" (each channel two digits).
let result = format_palette(&[[0x00, 16, 128, 128]]);
assert_eq!(String::from_utf8(result).unwrap(), "palette: 101010\n");
}
}