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
+592
View File
@@ -990,4 +990,596 @@ mod tests {
"trailing audio entry from the continuation packet survives"
);
}
// ════════════════════════════════════════════════════════════════════
// Added hardening tests
// ════════════════════════════════════════════════════════════════════
/// Build a 192-byte BD-TS packet whose TS payload region is EXACTLY
/// `payload` (no trailing zero padding). When `payload` is shorter than
/// the 184-byte TS payload area, the remainder is consumed by a
/// stuffing adaptation field (AFC 0b11) — the standard BD-TS way to
/// fill a short payload packet. This lets a test assert the exact ES
/// bytes the demuxer must produce, unlike `data_packet` which leaves
/// zero padding that a length-0 (unbounded) PES would absorb as ES.
fn es_packet_exact(pid: u16, pusi: bool, payload: &[u8]) -> Vec<u8> {
const TS_PAYLOAD: usize = 184;
assert!(payload.len() <= TS_PAYLOAD);
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
if pusi {
pkt[5] |= 0x40;
}
pkt[6] = (pid & 0xFF) as u8;
let pad = TS_PAYLOAD - payload.len();
if pad == 0 {
pkt[7] = 0x10; // payload only
pkt[8..8 + payload.len()].copy_from_slice(payload);
} else {
pkt[7] = 0x30; // AFC 0b11: adaptation + payload
// adaptation_field consumes `pad` bytes total: 1 length byte +
// (pad-1) of [flags + stuffing]. payload starts at 8 + pad.
let af_field_len = pad - 1; // bytes after the length byte
pkt[8] = af_field_len as u8;
if af_field_len >= 1 {
pkt[9] = 0x00; // AF flags (all zero)
for b in pkt.iter_mut().skip(10).take(af_field_len - 1) {
*b = 0xFF; // stuffing
}
}
let payload_off = 8 + pad;
pkt[payload_off..payload_off + payload.len()].copy_from_slice(payload);
}
pkt
}
// ── parse_timestamp: marker bits + 33-bit field (ISO 13818-1 Tbl 2-17) ─
/// Encode a 33-bit PTS/DTS value into the 5-byte field with the
/// standard 4-bit prefix and all three marker bits (LSB of bytes
/// 0, 2, 4) set to 1, per ISO/IEC 13818-1 Table 2-17.
fn encode_pts_i64(pts: i64, prefix: u8) -> [u8; 5] {
let p = pts as u64;
[
prefix | (((p >> 30) as u8) & 0x07) << 1 | 1,
((p >> 22) & 0xFF) as u8,
(((p >> 15) & 0x7F) as u8) << 1 | 1,
((p >> 7) & 0xFF) as u8,
(((p) & 0x7F) as u8) << 1 | 1,
]
}
#[test]
fn parse_timestamp_decodes_known_value_90000() {
// 1 second @ 90 kHz = 90000 ticks. Round-trip through the canonical
// encoder (markers set) so the bit layout is grounded in the spec,
// not in whatever the parser happens to emit.
let enc = encode_pts_i64(90_000, 0x20);
assert_eq!(parse_timestamp(&enc), Some(90_000));
}
#[test]
fn parse_timestamp_max_33bit_value() {
// 33-bit max is 2^33-1 = 8_589_934_591. The field carries exactly
// 33 bits, so the maximum representable PTS must round-trip.
let max = (1i64 << 33) - 1;
let enc = encode_pts_i64(max, 0x20);
assert_eq!(parse_timestamp(&enc), Some(max));
}
#[test]
fn parse_timestamp_rejects_each_missing_marker_bit() {
// ISO 13818-1 Table 2-17: marker bit (LSB) of bytes 0, 2 and 4 must
// each be 1. A zero in ANY of the three is an invalid encoding and
// must yield None — not a misparsed timestamp.
let good = encode_pts_i64(12_345, 0x20);
for &byte_idx in &[0usize, 2, 4] {
let mut bad = good;
bad[byte_idx] &= 0xFE; // clear the marker bit
assert_eq!(
parse_timestamp(&bad),
None,
"marker bit cleared in byte {byte_idx} must reject"
);
}
// Bytes 1 and 3 have NO marker bit — clearing their LSB is legal and
// must still parse.
for &byte_idx in &[1usize, 3] {
let mut still_ok = good;
still_ok[byte_idx] &= 0xFE;
assert!(
parse_timestamp(&still_ok).is_some(),
"byte {byte_idx} has no marker bit; clearing LSB must still parse"
);
}
}
#[test]
fn parse_timestamp_too_short_returns_none() {
// The PTS/DTS field is fixed 5 bytes; fewer than 5 cannot be parsed.
assert_eq!(parse_timestamp(&[0x21, 0x00, 0x01, 0x00]), None);
assert_eq!(parse_timestamp(&[]), None);
}
// ── parse_pes_header: stream-id classes, flags, lengths ───────────────
#[test]
fn parse_pes_header_rejects_bad_start_code() {
// Per ISO 13818-1 the PES start prefix is exactly 00 00 01. Any
// other leading bytes → header_len 0 (not a PES start). A wrong
// first byte must be rejected so garbage isn't injected as ES.
let mut buf = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05];
buf.extend_from_slice(&encode_pts_i64(0, 0x20));
let (pts, dts, hl) = parse_pes_header(&buf);
assert!(pts.is_some() && dts.is_none() && hl == 14);
// Corrupt the prefix.
buf[2] = 0x02;
assert_eq!(parse_pes_header(&buf), (None, None, 0));
}
#[test]
fn parse_pes_header_too_short_is_malformed() {
// < 9 bytes cannot hold the fixed PES header — must report
// header_len 0 rather than reading past the slice.
let short = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80];
assert_eq!(parse_pes_header(&short), (None, None, 0));
}
#[test]
fn parse_pes_header_extension_less_stream_ids_report_len_6() {
// ISO 13818-1 Table 2-22: program_stream_map(0xBC), padding(0xBE),
// private_stream_2(0xBF), ECM(0xF0), EMM(0xF1), DSMCC(0xF2),
// H.222.1 type E(0xF8), program_stream_directory(0xFF) carry NO
// standard PES header extension → header_len 6, no PTS/DTS.
for sid in [0xBCu8, 0xBE, 0xBF, 0xF0, 0xF1, 0xF2, 0xF8, 0xFF] {
let buf = [0x00, 0x00, 0x01, sid, 0x00, 0x00, 0x80, 0xC0, 0x0A];
let (pts, dts, hl) = parse_pes_header(&buf);
assert_eq!(
(pts, dts, hl),
(None, None, 6),
"stream_id {sid:#04x} must be extension-less (len 6, no timestamps)"
);
}
}
#[test]
fn parse_pes_header_pts_only_vs_pts_dts() {
// pts_dts_flags (bits 7:6 of flags2 / data[7]): 0b10 = PTS only,
// 0b11 = PTS+DTS. header_data_length must cover the fields (>=5 PTS,
// >=10 PTS+DTS) per Table 2-21.
let mut pts_only = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05];
pts_only.extend_from_slice(&encode_pts_i64(90_000, 0x20));
let (p, d, hl) = parse_pes_header(&pts_only);
assert_eq!((p, d, hl), (Some(90_000), None, 14));
let mut both = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0xC0, 0x0A];
both.extend_from_slice(&encode_pts_i64(180_000, 0x30));
both.extend_from_slice(&encode_pts_i64(90_000, 0x10));
let (p, d, hl) = parse_pes_header(&both);
assert_eq!((p, d, hl), (Some(180_000), Some(90_000), 19));
}
#[test]
fn parse_pes_header_dts_flag_without_room_skips_dts() {
// pts_dts_flags == 0b11 but header_data_length only 5 (< 10) — the
// declared header cannot hold the DTS field, so DTS must be dropped
// (reading data[14..19] would consume payload as a bogus timestamp).
let mut buf = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0xC0, 0x05];
buf.extend_from_slice(&encode_pts_i64(90_000, 0x30));
// pad so data.len() >= 19 to prove the gate is on header_data_len,
// not on slice length.
buf.extend_from_slice(&[0xAA; 10]);
let (p, d, hl) = parse_pes_header(&buf);
assert_eq!(p, Some(90_000), "PTS present");
assert_eq!(d, None, "DTS dropped: header_data_length too short for it");
assert_eq!(hl, 14, "header_len = 9 + header_data_length(5)");
}
#[test]
fn parse_pes_header_len_is_uncapped() {
// header_len must be the FULL 9 + header_data_length even when it
// exceeds the slice — the caller relies on this to skip header bytes
// that spill into continuation packets. A capped length would leak
// header bytes into the ES.
let buf = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 200];
let (_, _, hl) = parse_pes_header(&buf);
assert_eq!(
hl,
9 + 200,
"header_len uncapped at 209 even though slice is 9"
);
}
// ── process_packet routing: sync, PID, AFC, PUSI ──────────────────────
#[test]
fn untracked_pid_produces_nothing() {
// A demuxer tracking only PID 0x1011 must ignore packets on any
// other PID — they belong to other elementary streams.
let mut demux = TsDemuxer::new(&[0x1011]);
let mut pes = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
pes.extend_from_slice(&[0xDE, 0xAD]);
let out = demux.feed(&data_packet(0x1012, true, &pes)); // wrong PID
assert!(out.is_empty());
assert!(demux.flush().is_empty());
}
#[test]
fn bad_sync_byte_skips_packet() {
// TS sync byte (ISO 13818-1) is 0x47 at TS offset 0 (= BD offset 4).
// A packet with the wrong sync byte must be discarded, not parsed.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pkt = data_packet(pid, true, &{
let mut v = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
v.extend_from_slice(&[0x11, 0x22, 0x33]);
v
});
pkt[4] = 0x46; // corrupt sync byte
let out = demux.feed(&pkt);
assert!(
out.is_empty(),
"bad sync byte must drop the packet entirely"
);
assert!(demux.flush().is_empty());
}
#[test]
fn afc_reserved_zero_drops_payload() {
// adaptation_field_control == 0b00 is reserved (ISO 13818-1
// Table 2-5) and carries no payload — its 184 bytes must NOT be
// injected into the assembler.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pkt = data_packet(pid, true, &{
let mut v = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
v.extend_from_slice(&[0xCA, 0xFE]);
v
});
// Force AFC = 0b00 while keeping PUSI: byte 5 (TS byte1) holds PUSI;
// byte 7 (TS byte3) holds scrambling(2) AFC(2) CC(4).
pkt[7] = 0x00; // AFC 0b00, CC 0
let out = demux.feed(&pkt);
assert!(out.is_empty());
assert!(demux.flush().is_empty(), "reserved AFC contributes no ES");
}
#[test]
fn afc_adaptation_only_carries_no_payload() {
// AFC == 0b10 = adaptation field only, no payload (ISO 13818-1).
// Even with a valid AF length, no ES bytes may be produced.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
// Build a PUSI packet that starts a PES…
let mut start = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
start.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
demux.feed(&es_packet_exact(pid, true, &start));
// …then an AF-only continuation packet whose "payload" bytes must
// be discarded.
let mut afonly = vec![0u8; BD_TS_PACKET_SIZE];
afonly[4] = SYNC_BYTE;
afonly[5] = ((pid >> 8) as u8) & 0x1F; // no PUSI
afonly[6] = (pid & 0xFF) as u8;
afonly[7] = 0x20; // AFC = 0b10 (AF only)
afonly[8] = 5; // adaptation_field_length
for b in afonly.iter_mut().skip(9).take(183) {
*b = 0xEE; // would be ES if (wrongly) treated as payload
}
demux.feed(&afonly);
let out = demux.flush();
assert_eq!(out.len(), 1);
// None of the 0xEE AF-only bytes may appear.
assert!(
!out[0].data.iter().any(|&b| b == 0xEE),
"AF-only packet bytes must never be appended as ES"
);
assert_eq!(out[0].data, vec![0x01, 0x02, 0x03, 0x04]);
}
#[test]
fn adaptation_field_len_skipped_before_payload() {
// AFC == 0b11: payload starts at 5 + adaptation_field_length within
// the TS packet. The AF bytes must NOT appear in the ES.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
pkt[4] = SYNC_BYTE;
pkt[5] = (((pid >> 8) as u8) & 0x1F) | 0x40; // PUSI
pkt[6] = (pid & 0xFF) as u8;
pkt[7] = 0x30; // AFC = 0b11
let pes = [
0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00, // PES header (hdr_len 0)
0x77, 0x88,
];
// TS payload area is 184 bytes. Size the AF so it consumes exactly
// everything except the PES, leaving no zero padding for the
// length-0 (unbounded) video PES to absorb. AF stuffing = 0xBB to
// prove it never leaks into the ES.
let payload_area = 184usize;
let af_total = payload_area - pes.len(); // bytes incl. length byte
let af_field_len = af_total - 1; // bytes after the length byte
pkt[8] = af_field_len as u8;
pkt[9] = 0x00; // AF flags
for b in pkt.iter_mut().skip(10).take(af_field_len - 1) {
*b = 0xBB; // AF stuffing (must not leak)
}
// Payload (PES) begins at 4 + 4 + af_total.
let payload_off = 4 + 4 + af_total;
pkt[payload_off..payload_off + pes.len()].copy_from_slice(&pes);
demux.feed(&pkt);
let out = demux.flush();
assert_eq!(out.len(), 1);
assert_eq!(out[0].data, vec![0x77, 0x88]);
assert!(
!out[0].data.iter().any(|&b| b == 0xBB),
"adaptation-field stuffing must not appear in the ES"
);
}
#[test]
fn malformed_af_length_over_183_drops_packet() {
// adaptation_field_length can be at most 183 (the TS payload area).
// A larger value runs past the packet and must be discarded.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pkt = vec![0u8; BD_TS_PACKET_SIZE];
pkt[4] = SYNC_BYTE;
pkt[5] = (((pid >> 8) as u8) & 0x1F) | 0x40;
pkt[6] = (pid & 0xFF) as u8;
pkt[7] = 0x30; // AFC 0b11
pkt[8] = 184; // > 183 — malformed
let out = demux.feed(&pkt);
assert!(out.is_empty());
assert!(demux.flush().is_empty());
}
// ── PES reassembly across packets ─────────────────────────────────────
#[test]
fn pes_reassembled_from_continuation_packets() {
// A PES spanning multiple TS packets: PUSI starts it, subsequent
// no-PUSI packets append payload, and the NEXT PUSI completes the
// previous PES (ISO 13818-1 §2.4.3.6 PUSI semantics).
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut start = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
start.extend_from_slice(&[0xA1, 0xA2]);
let mut out = demux.feed(&es_packet_exact(pid, true, &start));
assert!(out.is_empty(), "first PES not yet completed");
out.extend(demux.feed(&es_packet_exact(pid, false, &[0xB1, 0xB2])));
out.extend(demux.feed(&es_packet_exact(pid, false, &[0xC1, 0xC2])));
// New PUSI completes the previous PES.
let mut start2 = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
start2.extend_from_slice(&[0xD1]);
out.extend(demux.feed(&es_packet_exact(pid, true, &start2)));
assert_eq!(out.len(), 1, "previous PES completed by new PUSI");
assert_eq!(out[0].data, vec![0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]);
out.extend(demux.flush());
assert_eq!(out.last().unwrap().data, vec![0xD1]);
}
#[test]
fn pes_header_spanning_two_packets_is_fully_skipped() {
// A PES header (9 + header_data_length) can exceed the 184-byte
// payload of one TS packet. The spillover header bytes on the next
// continuation packet must be skipped, NOT appended as ES — else a
// bogus 00 00 01 start code corrupts the codec stream.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
// header_data_length = 184 → header_len = 193 > 184 payload.
// Fill the declared header area with 0xAA filler.
let mut start = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 184];
start.extend(std::iter::repeat_n(0xAAu8, 175)); // 9 + 175 = 184 bytes in pkt
demux.feed(&es_packet_exact(pid, true, &start));
// header_remaining = 193 - 184 = 9 bytes spill into the next packet.
// Continuation: 9 header-spill bytes (0xAA) then real ES.
let mut cont = vec![0xAAu8; 9]; // remaining header bytes
cont.extend_from_slice(&[0xEF, 0xBE]); // real ES
demux.feed(&es_packet_exact(pid, false, &cont));
let out = demux.flush();
assert_eq!(out.len(), 1);
assert_eq!(
out[0].data,
vec![0xEF, 0xBE],
"only post-header ES survives; spillover header bytes skipped"
);
}
#[test]
fn unaligned_feed_reassembles_across_call_boundary() {
// 16 MiB ISO batches never divide evenly into 192-byte BD-TS
// packets, so a packet may straddle two feed() calls. The remainder
// buffer must splice the boundary packet without losing data.
let pid = 0x1011;
let mut full = es_packet_exact(pid, true, &{
let mut v = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
v.extend_from_slice(&[0x10, 0x20, 0x30, 0x40]);
v
});
full.extend(es_packet_exact(pid, false, &[0x50, 0x60]));
// Split mid-first-packet (not on a 192 boundary).
let mut demux = TsDemuxer::new(&[pid]);
let cut = 100;
let mut out = demux.feed(&full[..cut]);
out.extend(demux.feed(&full[cut..]));
out.extend(demux.flush());
assert_eq!(out.len(), 1);
assert_eq!(out[0].data, vec![0x10, 0x20, 0x30, 0x40, 0x50, 0x60]);
}
#[test]
fn feed_holds_sub_packet_remainder_without_emitting() {
// A feed() shorter than one full boundary packet must buffer and
// emit nothing until the rest arrives — never emit a truncated PES.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
// Seed a remainder by feeding most of a packet, then feed < need.
let pkt = es_packet_exact(pid, true, &{
let mut v = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
v.extend_from_slice(&[0xAB, 0xCD]);
v
});
let out1 = demux.feed(&pkt[..50]); // partial: 50 < 192
assert!(out1.is_empty());
let out2 = demux.feed(&pkt[50..100]); // still partial: 100 < 192
assert!(out2.is_empty(), "sub-packet remainder must not emit");
let mut out = demux.feed(&pkt[100..]);
out.extend(demux.flush());
assert_eq!(out.len(), 1);
assert_eq!(out[0].data, vec![0xAB, 0xCD]);
}
#[test]
fn two_pids_route_independently_no_collision() {
// Distinct PIDs route to distinct assemblers; interleaved packets on
// two PIDs must not cross-contaminate (ISO 13818-1 PID demux).
let (v, a) = (0x1011u16, 0x1100u16);
let mut demux = TsDemuxer::new(&[v, a]);
let mut vstart = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
vstart.extend_from_slice(&[0x11, 0x11]);
let mut astart = vec![0x00, 0x00, 0x01, 0xBD, 0x00, 0x00, 0x80, 0x00, 0x00];
astart.extend_from_slice(&[0x22, 0x22]);
let mut out = Vec::new();
out.extend(demux.feed(&es_packet_exact(v, true, &vstart)));
out.extend(demux.feed(&es_packet_exact(a, true, &astart)));
out.extend(demux.feed(&es_packet_exact(v, false, &[0x33])));
out.extend(demux.feed(&es_packet_exact(a, false, &[0x44])));
out.extend(demux.flush());
let vpes = out.iter().find(|p| p.pid == v).unwrap();
let apes = out.iter().find(|p| p.pid == a).unwrap();
assert_eq!(
vpes.data,
vec![0x11, 0x11, 0x33],
"video ES not contaminated"
);
assert_eq!(
apes.data,
vec![0x22, 0x22, 0x44],
"audio ES not contaminated"
);
}
#[test]
fn pusi_with_pts_is_extracted() {
// A PUSI PES carrying a PTS must surface that PTS on the completed
// packet (ISO 13818-1 §2.4.3.7). Grounds the PTS path in process_packet.
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
let mut pes = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05];
pes.extend_from_slice(&encode_pts_i64(90_000, 0x20));
pes.extend_from_slice(&[0xFE, 0xED]);
demux.feed(&es_packet_exact(pid, true, &pes));
let out = demux.flush();
assert_eq!(out.len(), 1);
assert_eq!(out[0].pts, Some(90_000));
assert_eq!(out[0].data, vec![0xFE, 0xED]);
}
#[test]
fn flush_on_empty_assembler_yields_nothing() {
// Flushing a demuxer that never saw a started PES must yield no
// packets — never a spurious empty PES.
let mut demux = TsDemuxer::new(&[0x1011]);
assert!(demux.flush().is_empty());
}
#[test]
fn new_with_empty_pids_tracks_nothing() {
// Empty PID list → max_pid 0, table floored to 8192, all untracked.
// Feeding well-formed packets must produce nothing and not panic.
let mut demux = TsDemuxer::new(&[]);
let mut pes = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
pes.extend_from_slice(&[0xAA]);
assert!(demux.feed(&data_packet(0x1011, true, &pes)).is_empty());
assert!(demux.flush().is_empty());
}
#[test]
fn high_pid_above_table_floor_is_tracked() {
// The flat PID table is sized to max(8192, max_pid+1). A PID at the
// top of the 13-bit BD-TS space (0x1FFF) must still route correctly.
let pid = 0x1FFFu16; // 13-bit max
let mut demux = TsDemuxer::new(&[pid]);
let mut pes = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
pes.extend_from_slice(&[0x5A, 0xA5]);
demux.feed(&es_packet_exact(pid, true, &pes));
let out = demux.flush();
assert_eq!(out.len(), 1);
assert_eq!(out[0].pid, pid);
assert_eq!(out[0].data, vec![0x5A, 0xA5]);
}
// ── scan_streams error / boundary paths ───────────────────────────────
#[test]
fn scan_streams_no_pat_returns_none() {
// Without a PAT (table_id 0x00 on PID 0) there is no program to find.
let data = vec![0u8; BD_TS_PACKET_SIZE * 2]; // all zero, no sync bytes
assert!(scan_streams(&data).is_none());
}
#[test]
fn scan_streams_pat_but_no_pmt_returns_none() {
// PAT points at a PMT PID, but no PMT section is present in the
// stream → scan must return None, not a partial/garbage stream list.
let pmt_pid = 0x0100;
let mut data = pat_packet(pmt_pid);
data.extend(pat_packet(pmt_pid)); // follower sync corroboration
assert!(scan_streams(&data).is_none());
}
#[test]
fn scan_streams_drops_unknown_stream_type() {
// A PMT entry with an unknown stream_type maps to Codec::Unknown
// (CodecKind::Unknown) and must be dropped, not emitted as a stream.
use crate::disc::Stream;
let pmt_pid = 0x0100;
let mut data = pat_packet(pmt_pid);
// 0x1B = H.264 (kept), 0x7F = unassigned/unknown (dropped).
data.extend(pmt_packet(pmt_pid, &[(0x1B, 0x1011), (0x7F, 0x1500)]));
data.extend(pat_packet(pmt_pid)); // follower
let streams = scan_streams(&data).expect("known stream survives");
assert_eq!(streams.len(), 1, "unknown stream_type entry dropped");
assert!(matches!(streams[0], Stream::Video(_)));
}
#[test]
fn scan_streams_hevc_defaults_to_uhd_resolution() {
// scan_streams seeds a default resolution by codec generation:
// HEVC → R2160p (UHD). Grounded in the resolution-seed branch.
use crate::disc::{Resolution, Stream};
let pmt_pid = 0x0100;
let mut data = pat_packet(pmt_pid);
data.extend(pmt_packet(pmt_pid, &[(0x24, 0x1011)])); // 0x24 = HEVC
data.extend(pat_packet(pmt_pid));
let streams = scan_streams(&data).expect("HEVC video parses");
let v = streams
.iter()
.find_map(|s| match s {
Stream::Video(v) => Some(v),
_ => None,
})
.expect("video present");
assert_eq!(v.resolution, Resolution::R2160p, "HEVC defaults to UHD");
}
#[test]
fn scan_streams_mpeg2_defaults_to_1080i() {
// MPEG-2 video (stream_type 0x02) defaults to R1080i in scan_streams.
use crate::disc::{Resolution, Stream};
let pmt_pid = 0x0100;
let mut data = pat_packet(pmt_pid);
data.extend(pmt_packet(pmt_pid, &[(0x02, 0x1011)])); // 0x02 = MPEG-2
data.extend(pat_packet(pmt_pid));
let streams = scan_streams(&data).expect("MPEG-2 video parses");
let v = streams
.iter()
.find_map(|s| match s {
Stream::Video(v) => Some(v),
_ => None,
})
.expect("video present");
assert_eq!(v.resolution, Resolution::R1080i, "MPEG-2 defaults to 1080i");
}
}