Close mutation-testing gaps in the TS/PS mux (ts.rs, ps.rs, tsmux.rs)

A 12,330-mutant run left 159 survivors across these three files, all from
missing assertions rather than wrong code — every gap here is a test, no
production logic changed.

Two shapes accounted for most of them:

- Buffer-cap constants (MAX_PES_BUFFER_TOTAL, MAX_PS_BUFFER,
  MAX_BD_PES_PAYLOAD, PES_BUFFER_INIT_CAP) were only ever read by tests
  through their own symbol, so a mutated `*`/`-` in the constant's
  definition changes what the symbol itself evaluates to and every
  self-referential assertion still passes. Pinned each against a literal
  computed independently in the test.

- Several `>`/`==` boundary checks on framing lengths (MPEG-2 pack header,
  system header, BD-TS adaptation field) were only ever exercised with
  slack in the buffer, never at the exact byte the check exists for.
  Added exact-fit cases for the pack header, system header, and
  psi_payload_base's AF-consumes-everything boundary.

Real, higher-value gaps closed along the way:

- ts.rs's per-PID discontinuity_flag and the NULL-TS concealment marker
  both require adaptation_field_length > 0 before trusting the AF flags
  byte; neither branch had a test proving af_len == 0 (no flags byte at
  all, ordinary payload underneath) is left alone.
- header_remaining (PES header spillover across TS packets) only had
  single-continuation-packet coverage, which can't distinguish `-=` from
  `+=`/`*=` because the corrupted value never gets read again. Added a
  case spanning two continuations.
- ps.rs's parse_stream_id_extension (used for HD-DVD 0xFD routing) walks
  nine optional PES-header/extension fields with a `pos +=` each; only
  the PTS/DTS pair had ever been exercised. One test now arms every
  field and checks the walk lands on the right byte.
- find_ps_boundary's `sc + 3 >= len` guard had no test at sc == 0 with a
  bare 3-byte start code, the case a `+` -> `-` mutation turns into a
  debug-mode subtract-overflow panic on ordinary tail-of-buffer input.
- tsmux.rs: an oversized video access unit must go out as a single
  unbounded-length PES; the `is_video || small-enough` guard that
  enforces this had no test with a video frame actually over the
  bounded-PES threshold, so a `||` -> `&&` mutant survived (it would
  silently split a keyframe across several look-alike-independent PES
  units). Also pinned the PES-length and PTS big-endian encodes at
  values above 255 / with bit 29+ set, where a `>>`/`<<` swap first
  becomes observable.

Every test above was verified by hand: applied the exact mutation,
confirmed the test fails (or the specific panic fires), then reverted.

Left unclosed, all confirmed equivalent by hand-tracing rather than
just left alone:
- Every `<<8 | byte` PID/length bit-combine (ts.rs pid/PAT/PMT parsing,
  ps.rs dvd_audio_pid/hddvd_extended_pid/parse_pts): the two halves
  never share a bit, so `|` and `^` produce identical output for every
  input - no test can tell them apart.
- ts.rs's `af_len > 183` check in process_packet: fully subsumed by the
  `payload_start >= TS_PACKET_BYTES` check three lines later for every
  af_len that could trip it.
- ts.rs's out-of-range `pid_index` sentinel (-1 vs 1): unreachable, since
  a TS PID is masked to 13 bits (max 8191) and the table is always sized
  to at least 8192.
- A cluster of "push an empty slice on an exact boundary" mutants in
  tsmux.rs's write_pes_chain (offset < hdr_len, af_bytes stuffing
  guards): the guarded write becomes a length-0 write_all, a no-op
  either way.

Not reached this pass, for lack of a clean seam within the time
available - ps.rs's extract_packets bounded-PES-length exact-fit
checks (lines 278/282/303, the `sc+6>len` / `sc+6+pes_len>len` /
force-flush cap arithmetic). The first two need a scenario where
"proceed vs. wait one more byte" is observable in the packet list, and
the third only shows up at a start-code offset (sc) that survives to
the moment the cap check runs - in this code path sc is always 0 once
an unbounded PES buffer starts accumulating, since nothing before it
ever drains. Didn't find a construction in the time available; flagged
rather than papered over with a self-referential assert.
This commit is contained in:
Matthew Jackson
2026-08-01 13:34:48 -07:00
parent b002da4221
commit 65dbcb1ca6
3 changed files with 556 additions and 0 deletions
+221
View File
@@ -2845,4 +2845,225 @@ mod tests {
"an undersized PAT section declares no program"
);
}
// ════════════════════════════════════════════════════════════════════
// Mutation-gap hardening (mux-ts pass)
// ════════════════════════════════════════════════════════════════════
/// The buffer-cap constants are read by tests only through their own
/// symbol (e.g. `MAX_PES_BUFFER_TOTAL`), so a mutated arithmetic
/// expression in the constant's definition changes what that symbol
/// itself evaluates to and every self-referential assertion still
/// passes. Pin the compiled values against a literal computed
/// independently here, so a mutation to the `*` in the definition is
/// caught directly.
#[test]
fn buffer_cap_constants_have_the_documented_values() {
assert_eq!(PES_BUFFER_INIT_CAP, 16 * 1024);
assert_eq!(MAX_PES_BUFFER, 64 * 1024 * 1024);
assert_eq!(MAX_PES_BUFFER_TOTAL, 512 * 1024 * 1024);
}
/// `psi_payload_base` must reject an adaptation field that consumes the
/// entire 184-byte payload area (`af_len == 183`, so `base == 192`),
/// leaving zero bytes for the pointer_field. `collect_psi_section` reads
/// `payload[0]` unconditionally once `psi_payload_base` returns `Some`,
/// so admitting this boundary (an off-by-one `<=`) would hand back an
/// empty payload slice and the very next line would index-panic on
/// disc-derived data instead of the packet being cleanly rejected.
#[test]
fn psi_payload_base_rejects_af_that_consumes_the_whole_payload() {
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[7] = 0x30; // AFC 0b11 in the TS-header byte at pkt[7] (4+3)
pkt[8] = 183; // af_len: base = 9 + 183 = 192, exactly BD_SOURCE_PACKET_BYTES
assert_eq!(
psi_payload_base(&pkt),
None,
"an AF that fills the whole payload area leaves no pointer_field byte"
);
// One less: base = 191, still inside the packet — must be accepted.
pkt[8] = 182;
assert_eq!(psi_payload_base(&pkt), Some(191));
}
/// The P3/B1 concealment marker on `NULL_PID` requires a NON-ZERO
/// adaptation_field_length before it may read `ts[5]` as the
/// discontinuity_indicator byte: at `af_len == 0` there is no AF flags
/// byte at all, and `ts[5]` is actually the first byte of TS payload (or
/// meaningless stuffing) that must never be mistaken for it. Crafting
/// that byte with the high bit set must NOT trip the marker.
#[test]
fn null_pid_marker_requires_nonzero_af_len_to_read_discontinuity_byte() {
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(b"AAAA");
demux.feed(&ts_payload_packet(pid, true, 0, &start));
// NULL_PID (0x1FFF), AFC = 0b11, af_len = 0: ts[5] (packet index 9)
// is ordinary payload, crafted here to look like a set
// discontinuity_indicator bit.
let mut null_pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
null_pkt[4] = SYNC_BYTE;
null_pkt[5] = 0x1F;
null_pkt[6] = 0xFF;
null_pkt[7] = 0x30;
null_pkt[8] = 0; // af_len = 0 — no flags byte exists
null_pkt[9] = 0x80; // this is payload, not a discontinuity flag
demux.feed(&null_pkt);
demux.feed(&ts_payload_packet(pid, false, 1, b"BBBB"));
let out = demux.flush();
assert_eq!(
out.len(),
1,
"the open PES must survive an af_len==0 NULL-TS packet unharmed"
);
assert_eq!(&out[0].data[..4], b"AAAA");
assert!(
!out[0].discontinuity,
"af_len==0 must not be read as a discontinuity_indicator"
);
}
/// Build a raw BD-TS packet with AFC = 0b11 (AF + payload). `af_flags`
/// is `None` for `af_len == 0` (no flags byte at all — `payload` starts
/// immediately after the length byte) or `Some(byte)` for `af_len == 1`
/// (that byte is the AF flags byte, `payload` follows it).
fn ts_af_packet(pid: u16, pusi: bool, cc: u8, af_flags: Option<u8>, payload: &[u8]) -> Vec<u8> {
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
pkt[4] = SYNC_BYTE;
pkt[5] = ((pid >> 8) as u8) & 0x1F;
if pusi {
pkt[5] |= 0x40;
}
pkt[6] = (pid & 0xFF) as u8;
pkt[7] = 0x30 | (cc & 0x0F);
match af_flags {
None => {
pkt[8] = 0;
let n = payload.len().min(183);
pkt[9..9 + n].copy_from_slice(&payload[..n]);
}
Some(flags) => {
pkt[8] = 1;
pkt[9] = flags;
let n = payload.len().min(182);
pkt[10..10 + n].copy_from_slice(&payload[..n]);
}
}
pkt
}
/// Same guard as the NULL_PID marker (`af_len > 0` before trusting the AF
/// flags byte) applies to the PER-PID `discontinuity_flag` used by the
/// ordinary continuity check. `af_len == 0` must never be read as a set
/// discontinuity_indicator even when the following payload byte happens
/// to have the high bit set — that byte is real elementary-stream data.
#[test]
fn discontinuity_flag_requires_nonzero_af_len() {
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(b"AAAA");
demux.feed(&ts_payload_packet(pid, true, 0, &start));
// Continuation: AFC 0b11, af_len == 0, cc sequential (no CC gap).
// The payload's first byte (0x80) must NOT be read as a
// discontinuity_indicator.
demux.feed(&ts_af_packet(pid, false, 1, None, &[0x80, 0x11, 0x22]));
let out = demux.flush();
assert_eq!(
out.len(),
1,
"af_len==0 must not falsely trigger a continuity break"
);
assert_eq!(&out[0].data[..4], b"AAAA");
assert!(!out[0].discontinuity);
}
/// The counterpart: a REAL adaptation-field discontinuity_indicator
/// (`af_len == 1`, flags byte `0x80`) must still be honoured. Mutating
/// `ts[4] > 0` to `ts[4] < 0` (always false for a `u8`) would silently
/// disable this path entirely — the dropped partial would instead be
/// spliced into the next PES and the resulting stream would carry
/// corrupt data with no discontinuity flag raised to warn the codec
/// consumer.
#[test]
fn discontinuity_flag_honours_a_real_af_indicator() {
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(b"AAAA");
demux.feed(&ts_payload_packet(pid, true, 0, &start));
// A genuine AF discontinuity_indicator: af_len == 1, flags == 0x80.
// cc is sequential (1), so this is NOT a CC gap — only the AF flag
// drives the drop.
demux.feed(&ts_af_packet(pid, false, 1, Some(0x80), b"XXXX"));
// A fresh PUSI (cc == 2, still sequential) starts the next PES. The
// dropped partial must not be flushed by it, and the pending
// discontinuity must ride onto this new PES.
let mut next = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
next.extend_from_slice(b"CCCC");
demux.feed(&ts_payload_packet(pid, true, 2, &next));
let out = demux.flush();
assert_eq!(
out.len(),
1,
"the AF-flagged partial must be dropped, not flushed as its own PES"
);
assert_eq!(&out[0].data[..4], b"CCCC");
assert!(
out[0].discontinuity,
"a real AF discontinuity_indicator must flag the next completed PES"
);
}
/// A PES header can spill across MORE THAN ONE continuation packet
/// (`header_data_length` up to 255 gives a header up to 264 bytes,
/// almost 1.5 TS payloads). `header_remaining` must be decremented by
/// exactly the bytes consumed on EACH continuation, not reset or
/// corrupted, or the second continuation's real ES bytes get
/// misattributed as header spillover (or vice versa).
#[test]
fn header_remaining_decrements_correctly_across_two_continuations() {
let pid = 0x1011;
let mut demux = TsDemuxer::new(&[pid]);
// header_data_length = 255 -> header_len = 9 + 255 = 264.
// First (PUSI) packet's payload is entirely header: 184 bytes of it.
// header_remaining after packet 1 = 264 - 184 = 80.
let mut start = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 255];
start.extend(std::iter::repeat_n(0xAAu8, 175)); // 9 + 175 = 184
demux.feed(&ts_payload_packet(pid, true, 0, &start));
// Continuation 1: 80 more header-spillover bytes, then 104 bytes of
// real ES. header_remaining must land at exactly 0 afterwards.
let mut cont1 = vec![0xAAu8; 80];
let es1: Vec<u8> = (0u8..104).collect();
cont1.extend_from_slice(&es1);
demux.feed(&ts_payload_packet(pid, false, 1, &cont1));
// Continuation 2: header_remaining is (correctly) already 0, so this
// ENTIRE 184-byte payload must be real ES — none of it skipped as
// leftover header.
let es2: Vec<u8> = (0u8..184).collect();
demux.feed(&ts_payload_packet(pid, false, 2, &es2));
let out = demux.flush();
assert_eq!(out.len(), 1);
let mut expected = es1.clone();
expected.extend_from_slice(&es2);
assert_eq!(
out[0].data, expected,
"every post-header byte from both continuations must survive, \
in order, with none mistaken for header spillover"
);
}
}