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:
+177
@@ -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
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user