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
+158
View File
@@ -1289,4 +1289,162 @@ mod tests {
}
mux.finish().unwrap();
}
// ════════════════════════════════════════════════════════════════════
// Mutation-gap hardening (mux-ts pass)
// ════════════════════════════════════════════════════════════════════
/// `MAX_BD_PES_PAYLOAD` is read by its own tests (the oversized-split
/// tests) only through the same symbol, so a mutated arithmetic
/// expression in its definition changes what the symbol itself
/// evaluates to and those assertions still pass. Pin the compiled value
/// against a literal computed independently.
#[test]
fn max_bd_pes_payload_has_the_documented_value() {
assert_eq!(MAX_BD_PES_PAYLOAD, u16::MAX as usize - 8);
}
/// A video access unit larger than `MAX_BD_PES_PAYLOAD` (the bound that
/// exists ONLY because a bounded `private_stream_1` PES can't exceed a
/// `u16` length) must still go out as ONE PES using the video-only
/// unbounded-length form — never split into several independent PES
/// chunks the way an oversized audio/subtitle access unit is. A
/// splitting bug here would emit several PUSI packets that each look
/// like a complete, independent video access unit (RAI + PTS on each),
/// corrupting any large keyframe.
#[test]
fn oversized_video_frame_is_one_pes_not_split() {
let big = fake_hevc_nal(19, MAX_BD_PES_PAYLOAD + 5000);
let mut sink: Vec<u8> = Vec::new();
{
let mut mux = TsMuxer::new(&mut sink, &[VIDEO_PID]);
mux.write_frame(0, 0, true, &big).unwrap();
mux.finish().unwrap();
}
let packets = parse_bd_ts(&sink);
let pusi_count = packets
.iter()
.filter(|p| p.pid == VIDEO_PID && p.pusi)
.count();
assert_eq!(
pusi_count, 1,
"an oversized video access unit must still be exactly one PES \
(one PUSI packet), using the unbounded length form, not split \
into several PES the way bounded private_stream_1 data is"
);
}
/// The RAI-carrying first packet of a keyframe video PES needs only the
/// MINIMUM adaptation field (2 bytes: length + RAI flag) before payload
/// resumes — `max_payload = TS_PAYLOAD_BYTES - 2`. A `-` -> `/` mutation
/// collapses that to `184 / 2 = 92`, wasting 90 bytes of every keyframe's
/// first packet as pointless AF stuffing. Pin the AF to its true minimum
/// length when there is enough data to fill the rest as payload.
#[test]
fn rai_adaptation_field_uses_the_minimum_two_bytes() {
let mut sink: Vec<u8> = Vec::new();
{
let mut mux = TsMuxer::new(&mut sink, &[VIDEO_PID]);
// Comfortably larger than one TS payload so the first packet is
// entirely full: AF(2) + payload(182) = 184.
let idr = fake_hevc_nal(19, 1000);
mux.write_frame(0, 0, true, &idr).unwrap();
mux.finish().unwrap();
}
let packets = parse_bd_ts(&sink);
let first_pusi = packets
.iter()
.find(|p| p.pid == VIDEO_PID && p.pusi)
.expect("video PUSI packet exists");
let af = first_pusi.af.as_ref().expect("AF present on keyframe PES");
assert_eq!(
af.len(),
1,
"AF body (length byte stripped) must be exactly [flags] = 1 byte \
(2 total with the length byte) when there is enough data to fill \
the rest of the packet as payload"
);
assert_eq!(first_pusi.payload.len(), 182);
}
/// `build_pes_header`'s bounded-length field is big-endian 16-bit
/// (`(len >> 8) as u8`, then `len as u8`). A `>>` -> `<<` mutation
/// zeroes the high byte for every length (shifting left by 8 then
/// truncating to `u8` always yields 0), so any PES longer than 255
/// bytes gets a silently wrong (far too small) declared length. Use an
/// audio frame comfortably over 255 bytes but under the oversized-split
/// threshold so exactly one bounded PES is produced.
#[test]
fn bounded_pes_length_field_encodes_the_high_byte() {
let es: Vec<u8> = vec![0xAB; 2000];
let mut sink: Vec<u8> = Vec::new();
{
let mut mux = TsMuxer::new(&mut sink, &[AUDIO_PID]);
mux.write_frame(0, 0, false, &es).unwrap();
mux.finish().unwrap();
}
let packets = parse_bd_ts(&sink);
let pusi = packets
.iter()
.find(|p| p.pid == AUDIO_PID && p.pusi)
.unwrap();
let len = u16::from_be_bytes([pusi.payload[4], pusi.payload[5]]);
// pes_data_len = data_len + 8 (3 optional-header bytes + 5 PTS bytes).
assert_eq!(
len as usize,
es.len() + 8,
"PES_packet_length high byte must survive the encode"
);
assert!(
pusi.payload[4] != 0,
"a length > 255 must set a nonzero high byte"
);
}
/// The PTS encoding's top byte carries bits 29..32 of the 33-bit
/// timestamp (`(pts >> 29) & 0x0E`). A `>>` -> `<<` mutation there always
/// yields 0 regardless of `pts` (shifting left by 29 then masking the
/// low 4 bits always sees zeros shifted in), which a small test PTS
/// (whose true bits 29..32 are already 0) cannot distinguish from
/// correct code. Use a PTS large enough that bits 29..32 are nonzero.
#[test]
fn pts_high_bits_survive_encoding() {
// Choose pts_ns as an exact multiple of 100_000 so `pts_ns * 9 /
// 100_000` (the muxer's ns -> 90kHz-tick conversion) is exact, no
// truncation to account for. N * 9 lands just above 2^31, so bit 31
// of the 33-bit PTS field is set — well above anything a small-PTS
// test would exercise.
const N: u64 = 238_609_295;
let big_pts_ticks: u64 = N * 9;
let big_pts_ns = (N * 100_000) as i64;
let mut sink: Vec<u8> = Vec::new();
{
let mut mux = TsMuxer::new(&mut sink, &[VIDEO_PID]);
let idr = fake_hevc_nal(19, 50);
mux.write_frame(0, 0, true, &idr).unwrap(); // base = 0
let p = fake_hevc_nal(1, 50);
mux.write_frame(0, big_pts_ns, false, &p).unwrap();
mux.finish().unwrap();
}
let packets = parse_bd_ts(&sink);
let video_pusi: Vec<&TsPacket> = packets
.iter()
.filter(|p| p.pid == VIDEO_PID && p.pusi)
.collect();
assert!(video_pusi.len() >= 2);
let decoded = first_pts_90k(&packets, VIDEO_PID);
// first_pts_90k always reads the FIRST pusi packet, which is the
// base (0); decode the SECOND PES's PTS by hand instead.
let p = &video_pusi[1].payload;
let pts = ((((p[9] >> 1) & 0x07) as u64) << 30)
| ((p[10] as u64) << 22)
| (((p[11] >> 1) as u64) << 15)
| ((p[12] as u64) << 7)
| ((p[13] >> 1) as u64);
assert_eq!(decoded, 0, "base video frame stays at relative PTS 0");
assert_eq!(
pts, big_pts_ticks,
"the high bits (29..32) of a large PTS must round-trip through encoding"
);
}
}