diff --git a/src/mux/ps.rs b/src/mux/ps.rs index 7a6b530..b0da29f 100644 --- a/src/mux/ps.rs +++ b/src/mux/ps.rs @@ -1639,4 +1639,181 @@ mod tests { assert_eq!(p[1].stream_id, 0xC0); 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); + } } diff --git a/src/mux/ts.rs b/src/mux/ts.rs index 3fd6085..24f26df 100644 --- a/src/mux/ts.rs +++ b/src/mux/ts.rs @@ -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, payload: &[u8]) -> Vec { + 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 = (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 = (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" + ); + } } diff --git a/src/mux/tsmux.rs b/src/mux/tsmux.rs index eb48983..97ae727 100644 --- a/src/mux/tsmux.rs +++ b/src/mux/tsmux.rs @@ -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 = 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 = 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 = vec![0xAB; 2000]; + let mut sink: Vec = 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 = 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" + ); + } }