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
+177
View File
@@ -1639,4 +1639,181 @@ mod tests {
assert_eq!(p[1].stream_id, 0xC0); assert_eq!(p[1].stream_id, 0xC0);
assert_eq!(p[1].data, vec![0x99, 0x88]); assert_eq!(p[1].data, vec![0x99, 0x88]);
} }
// ════════════════════════════════════════════════════════════════════
// Mutation-gap hardening (mux-ts pass)
// ════════════════════════════════════════════════════════════════════
/// `MAX_PS_BUFFER` is read by its own tests only through the same
/// symbol, so a mutated arithmetic expression in its definition changes
/// what the symbol itself evaluates to and every self-referential
/// assertion still passes. Pin the compiled value against a literal
/// computed independently.
#[test]
fn max_ps_buffer_has_the_documented_value() {
assert_eq!(MAX_PS_BUFFER, 4 * 1024 * 1024);
}
/// Pack-header framing (`sc + 14 > len`, then `sc + pack_len > len`) must
/// accept an EXACT fit — the whole pack (mandatory 14 bytes, or with
/// stuffing) present and not one byte more — rather than waiting for
/// data that will never come. Both checks are preceded by an unrelated
/// start code so `sc != 0`: at `sc == 0` a `sc + pack_len` vs.
/// `sc * pack_len` mutant collapses to the same value (`0`) and the
/// bound stays unreachable from any input.
#[test]
fn pack_header_exact_fit_is_consumed_not_awaited() {
// Case 1: mandatory 14 bytes, no stuffing, nothing else buffered.
let mut demuxer = PsDemuxer::new();
let mut data = vec![0x00, 0x00, 0x01, 0xB0]; // unknown SC -> sc == 4 below
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3,
0xF8, // stuffing_length = 0
]);
assert!(demuxer.feed(&data).is_empty(), "a pack yields no PES");
assert!(
demuxer.buffer.is_empty(),
"an exact-fit pack (no stuffing) must be fully consumed, not held \
waiting for bytes that will never arrive"
);
// Case 2: with 3 stuffing bytes — exercises `pack_len = 14 + stuffing`
// at a non-zero `sc`, where a `+` -> `*` mutation diverges sharply
// from the correct sum.
let mut demuxer2 = PsDemuxer::new();
let mut data2 = vec![0x00, 0x00, 0x01, 0xB0];
data2.extend_from_slice(&[
0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3,
0xFB, // stuffing_length = 3
0xFF, 0xFF, 0xFF,
]);
assert!(demuxer2.feed(&data2).is_empty());
assert!(
demuxer2.buffer.is_empty(),
"an exact-fit pack WITH stuffing must be fully consumed"
);
}
/// System-header framing needs exactly `6 + header_length` bytes
/// (`sc + 6 > len`, then `sc + total > len`). At `header_length == 0`
/// both boundaries coincide at `len == 6`, so one buffer exercises both
/// checks' `>` vs `==`/`>=` mutants at once.
#[test]
fn system_header_zero_length_exact_fit_is_consumed_not_awaited() {
let mut demuxer = PsDemuxer::new();
let data = vec![0x00, 0x00, 0x01, 0xBB, 0x00, 0x00]; // header_length = 0
assert!(demuxer.feed(&data).is_empty());
assert!(
demuxer.buffer.is_empty(),
"a zero-length system header, fully present, must not be held awaiting more data"
);
}
/// `header_len` is a 16-bit big-endian field (`buffer[sc+4] << 8 |
/// buffer[sc+5]`). A `<<` -> `>>` mutation collapses the high byte to
/// zero, so any `header_length > 255` is misread as just its low byte —
/// here 300 (`0x012C`) misread as 44. A start code embedded 50 bytes in
/// (well inside the true 306-byte unit but exactly where the
/// mis-parsed 50-byte unit would end) must stay buried in the skipped
/// body under correct parsing, and surface as a bogus extra PES under
/// the mutant.
#[test]
fn system_header_length_high_byte_is_not_dropped() {
let mut demuxer = PsDemuxer::new();
let mut data = vec![0x00, 0x00, 0x01, 0xBB, 0x01, 0x2C]; // header_length = 300
let mut body = vec![0xCCu8; 300];
// Decoy PES start code at body offset 44 -> absolute offset 50,
// exactly where a misread length of 44 (0x2C) would resume scanning
// (6 + 44 == 50).
let decoy = [
0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x99, 0x99,
];
body[44..44 + decoy.len()].copy_from_slice(&decoy);
data.extend_from_slice(&body);
// The real PES follows the full (306-byte) system header.
data.extend_from_slice(&[
0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x77, 0x88,
]);
data.extend_from_slice(&PROGRAM_END);
let packets = demuxer.feed(&data);
assert_eq!(
packets.len(),
1,
"the decoy start code embedded in the system header body must stay \
buried in the skipped body, not surface as a second PES"
);
assert_eq!(packets[0].data, vec![0x77, 0x88]);
}
/// `find_ps_boundary`'s bounds check (`sc + 3 >= data.len()`) must stay
/// an ADDITION: a `+` -> `-` mutation at `sc == 0` underflows the `usize`
/// subtraction and panics on a plain 3-byte start code with nothing
/// after it — exactly the tail a real feed can end on.
#[test]
fn find_ps_boundary_handles_a_bare_start_code_at_the_buffer_head() {
assert_eq!(find_ps_boundary(&[0x00, 0x00, 0x01], 0), None);
}
/// The boundary-ID check is a 4-way `||`; a mutant that turns the FIRST
/// `||` into `&&` makes a lone pack-header start code (which can never
/// also equal `SYSTEM_HEADER_ID`) fail to register as a boundary at all.
#[test]
fn find_ps_boundary_recognises_a_lone_pack_header() {
let data = [0x00, 0x00, 0x01, PACK_HEADER_ID, 0xAA];
assert_eq!(
find_ps_boundary(&data, 0),
Some(0),
"a pack header start code alone must register as a PS-layer boundary"
);
}
/// `parse_stream_id_extension` walks every optional PES-header field
/// (PTS/DTS, ESCR, ES_rate, DSM_trick_mode, additional_copy_info,
/// PES_CRC) and every optional PES_extension sub-field (PES_private_data,
/// pack_header_field, program_packet_sequence_counter, P-STD_buffer)
/// before reaching `stream_id_extension`. Every one of those skips is a
/// `pos +=`; a single mutated increment (`-=`/`*=`) misaligns every read
/// after it. This test arms EVERY optional field at once with a known
/// byte count, so any single wrong skip anywhere in the chain lands on
/// the wrong byte and the assertion fails — one test proving the whole
/// walk, rather than one per field.
#[test]
fn parse_stream_id_extension_walks_every_optional_field_to_the_right_offset() {
// flags2: PTS/DTS absent (00), ESCR/ES_rate/DSM_trick_mode/
// additional_copy_info/PES_CRC all present, PES_extension present.
let flags2 = 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01; // 0x3F
let mut opt = Vec::new();
opt.extend_from_slice(&[0u8; 6]); // ESCR
opt.extend_from_slice(&[0u8; 3]); // ES_rate
opt.push(0); // DSM_trick_mode
opt.push(0); // additional_copy_info
opt.extend_from_slice(&[0u8; 2]); // PES_CRC
// PES_extension: every optional sub-field present + extension_flag_2.
let ext_flags = 0x80 | 0x40 | 0x20 | 0x10 | 0x01;
opt.push(ext_flags);
opt.extend_from_slice(&[0u8; 16]); // PES_private_data
opt.push(2); // pack_header_field length
opt.extend_from_slice(&[0u8; 2]); // pack_header_field data
opt.extend_from_slice(&[0u8; 2]); // program_packet_sequence_counter
opt.extend_from_slice(&[0u8; 2]); // P-STD_buffer
opt.push(0x81); // PES_extension_field_length (marker + 7 bits, value unused)
opt.push(0x55); // stream_id_extension (top bit clear)
let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID];
let es = [0xDEu8, 0xAD];
let len = (3 + opt.len() + es.len()) as u16;
pkt.extend_from_slice(&len.to_be_bytes());
pkt.extend_from_slice(&[0x80, flags2, opt.len() as u8]);
pkt.extend_from_slice(&opt);
pkt.extend_from_slice(&es);
let parsed = parse_pes_packet(&pkt).expect("parses");
assert_eq!(
parsed.sub_stream_id,
Some(0x55),
"stream_id_extension reached correctly after walking every optional field"
);
assert_eq!(parsed.data, es);
}
} }
+221
View File
@@ -2845,4 +2845,225 @@ mod tests {
"an undersized PAT section declares no program" "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"
);
}
} }
+158
View File
@@ -1289,4 +1289,162 @@ mod tests {
} }
mux.finish().unwrap(); 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"
);
}
} }