//! MPEG-2 Program Stream (PS) demuxer. //! //! DVDs use MPEG-2 Program Stream, which has: //! - Pack headers (00 00 01 BA) with SCR timestamps //! - PES packets (00 00 01 [stream_id]) with variable length //! - System headers (00 00 01 BB) //! - Program end code (00 00 01 B9) //! //! Stream IDs: //! - 0xE0-0xEF: video (usually 0xE0) //! - 0xC0-0xDF: MPEG audio //! - 0xBD: private stream 1 (AC3, DTS, LPCM, subtitles via sub-stream ID) use super::codec::startcode::find_start_code; /// Pack header start code suffix. const PACK_HEADER_ID: u8 = 0xBA; /// System header start code suffix. const SYSTEM_HEADER_ID: u8 = crate::consts::pes_stream_id::SYSTEM_HEADER; /// Program end start code suffix. const PROGRAM_END_ID: u8 = 0xB9; /// Private stream 1 (AC3, DTS, LPCM, subtitles). const PRIVATE_STREAM_1: u8 = crate::consts::pes_stream_id::PRIVATE_STREAM_1; /// Private stream 2 (0xBF) — DVD navigation (PCI/DSI). Carries no muxable /// elementary stream; expected to be dropped on every disc. const PRIVATE_STREAM_2: u8 = crate::consts::pes_stream_id::PRIVATE_STREAM_2; /// Extended stream id (0xFD) — the H.222.0 escape whereby the real stream id is /// the `stream_id_extension` carried in the PES extension. HD-DVD `.evo` puts its /// VC-1 video (and HD audio) here (Shaun of the Dead: VC-1 on `0xFD` ext `0x55`); /// a transport stream never uses it. The elementary-stream bytes follow the PES /// header exactly like any other PES — only the routing key differs. const EXTENDED_STREAM_ID: u8 = 0xFD; /// Hard cap on the demuxer's reassembly buffer. A length-0 (unbounded) video /// PES is delimited by the next PS-layer boundary; if a corrupt stream declares /// an unbounded PES and never follows it with a boundary, `feed()` would /// otherwise accumulate the entire input. Past this cap we force the in-progress /// unbounded PES to flush at the buffer end so untrusted input cannot drive /// unbounded allocation. A real DVD pack/PES is at most a few KB; this leaves /// generous slack while still bounding worst-case memory. const MAX_PS_BUFFER: usize = 4 * 1024 * 1024; /// A demuxed PES packet from the Program Stream. #[derive(Debug, Clone)] pub struct PsPacket { /// PES stream ID (0xE0 for video, 0xC0 for audio, 0xBD for private, etc.). pub stream_id: u8, /// Sub-stream ID for private stream 1 (AC3: 0x80-0x87, DTS: 0x88-0x8F, /// LPCM: 0xA0-0xA7, subtitles: 0x20-0x3F). pub sub_stream_id: Option, /// Presentation timestamp in 90kHz ticks. pub pts: Option, /// Decode timestamp in 90kHz ticks. pub dts: Option, /// Elementary stream payload data. pub data: Vec, /// Source position of this PES's first ES byte, stamped at the demux seam /// from the producer's known stream offset. `None` when the demuxer was fed /// without a base offset. pub source: Option, } /// Canonical DVD video PID. DVD-Video carries a single MPEG-2 video /// elementary stream; both the scanner and the muxer use this PID. pub const DVD_VIDEO_PID: u16 = 0xE0; /// Canonical PID for a `private_stream_1` audio stream identified by its /// on-wire sub-stream id. Returns `None` for sub-ids outside the AC-3 / /// DTS / LPCM audio ranges. /// /// The PID is `0xBD00 | sub_stream_id`, which is unique per sub-stream id /// (AC-3 / DTS `0x80..=0x8F`, LPCM `0xA0..=0xA7`). Unlike the old /// per-codec relative arithmetic, distinct sub-ids therefore always yield /// distinct PIDs — so a mixed-codec title (e.g. AC-3 + DTS, whose sub-ids /// are 0x80 and 0x88) can never collide on one PID. This is the single /// source of truth shared with `Disc::scan_dvd_titles` /// (`src/disc/dvd.rs`), which sets each `AudioStream.pid` from the same /// function so demuxer output routes through the title's `pid_to_track`. /// /// HD-DVD (`.evo` Enhanced VOB) carries Dolby Digital Plus (E-AC-3) on /// `private_stream_1` sub-stream ids `0xC0..=0xC7` — a range DVD never uses /// (DVD audio is `0x80..=0x8F` / `0xA0..=0xA7`), so admitting it here is purely /// additive and cannot change any DVD mapping. The PID is `0xBD00 | sub` just /// like the DVD audio ranges, so a mixed HD-DVD title (four DD+ tracks /// `0xC0..0xC3`) routes each track to its own distinct PID. pub fn dvd_audio_pid(sub_stream_id: u8) -> Option { match sub_stream_id { 0x80..=0x8F | 0xA0..=0xA7 | 0xC0..=0xC7 => Some(0xBD00 | sub_stream_id as u16), _ => None, } } /// Canonical PID for a VobSub subtitle stream identified by its on-wire /// sub-stream id (`0x20..=0x3F`). The PID is the sub-id itself (identity), /// which never overlaps the `0xBD..` audio PID space. pub fn dvd_subtitle_pid(sub_stream_id: u8) -> Option { match sub_stream_id { 0x20..=0x3F => Some(sub_stream_id as u16), _ => None, } } /// Canonical PID for an HD-DVD extended-stream-id (`0xFD`) stream, keyed by its /// `stream_id_extension`: `0xFD00 | ext`. Disjoint from the DVD video (`0xE0`) and /// `private_stream_1` (`0xBD00..`) PID spaces, so several elementary streams /// multiplexed on `0xFD` (VC-1 video, MLP/TrueHD audio) never collide. The /// scanner's head probe and `PsPacket::dvd_pid` derive the same PID from the same /// `stream_id_extension`, so demux output routes through the title's /// `pid_to_track`. pub fn hddvd_extended_pid(stream_id_extension: u8) -> u16 { 0xFD00 | stream_id_extension as u16 } impl PsPacket { /// Map this packet to the canonical DVD PID assigned by /// `Disc::scan_dvd_titles` (`src/disc/dvd.rs`), so demux output can /// be looked up in the title's `pid_to_track` map. /// /// Routes by the REAL on-wire `(stream_id, sub_stream_id)` via the /// shared [`dvd_audio_pid`] / [`dvd_subtitle_pid`] tables the scanner /// also uses — never per-codec relative arithmetic, which collided on /// mixed-codec audio (AC-3 0x80 and DTS 0x88 both mapping to 0xBD00). /// /// Returns `None` for stream/sub-stream combinations the DVD title /// scanner does not assign a PID to (e.g. MPEG audio 0xC0-0xDF, /// private stream 2, unrecognized sub-stream ranges). The caller is /// expected to WARN-and-drop in that case rather than silently /// mis-routing the packet. pub fn dvd_pid(&self) -> Option { match self.stream_id { crate::consts::pes_stream_id::VIDEO..=0xEF => Some(DVD_VIDEO_PID), PRIVATE_STREAM_1 => { let sub = self.sub_stream_id?; dvd_audio_pid(sub).or_else(|| dvd_subtitle_pid(sub)) } // HD-DVD extended-stream-id (0xFD): route by the stream_id_extension // (carried in `sub_stream_id`) to a distinct `0xFD00 | ext` PID, so a // disc that puts several elementary streams on 0xFD keeps them apart. // The codec (VC-1 etc.) is decided by the scanner's head probe, not // here — this only assigns a stable routing key. EXTENDED_STREAM_ID => self.sub_stream_id.map(hddvd_extended_pid), _ => None, } } /// Whether this is a DVD navigation packet (private_stream_2, 0xBF — /// PCI/DSI). These carry no muxable elementary stream and are EXPECTED to /// be dropped on every DVD, so a per-packet WARN is noise: the mux loops /// count them and emit one finalize summary instead. A `dvd_pid()` of /// `None` for any OTHER stream_id is unexpected (a possibly-dropped real /// stream) and stays an individual WARN. pub fn is_nav(&self) -> bool { self.stream_id == PRIVATE_STREAM_2 } } /// MPEG-2 Program Stream demuxer. /// /// Accepts raw PS bytes via `feed()` and produces demuxed PES packets. /// Handles non-aligned input by buffering leftover bytes between calls. pub struct PsDemuxer { buffer: Vec, /// Absolute source byte offset of `buffer[0]` — the running base that turns /// an in-buffer unit position into a [`crate::pes::SourcePos`]. Advanced as /// the buffer drains. `has_base` gates stamping so non-provenance callers /// stay byte-identical. buffer_base: u64, has_base: bool, /// Boundary-scan cursor for an unbounded (length-0) PES still waiting for /// its terminating PS-layer unit: `(buffer offset of the PES start code, /// buffer offset up to which the search has already proved there is no /// boundary)`. Both are buffer-relative and are rebased when the buffer /// drains. /// /// Without it, every `feed` re-searches the WHOLE accumulated payload from /// the PES header: the buffer only stops growing at [`MAX_PS_BUFFER`], so a /// stream that declares an unbounded PES and then never emits a PS-layer /// start code (a corrupt or crafted VOB) makes the demuxer scan up to 4 MiB /// per call, quadratic in the bytes fed. Cleared whenever the PES is /// emitted, so it can never outlive the packet it describes. pending_scan: Option<(usize, usize)>, /// Test-only: total bytes examined by `find_ps_boundary`. Pins the cursor /// above — the property it exists for is a WORK bound, which no /// packet-level assertion can observe. #[cfg(test)] boundary_bytes_scanned: u64, } impl Default for PsDemuxer { fn default() -> Self { Self::new() } } impl PsDemuxer { /// Create a new Program Stream demuxer. pub fn new() -> Self { Self { buffer: Vec::with_capacity(64 * 1024), buffer_base: 0, has_base: false, pending_scan: None, #[cfg(test)] boundary_bytes_scanned: 0, } } /// Feed raw MPEG-2 PS bytes, returning any completely parsed PES packets. pub fn feed(&mut self, data: &[u8]) -> Vec { self.buffer.extend_from_slice(data); self.extract_packets(false) } /// Like [`feed`](Self::feed) but records the absolute source byte offset of /// `data[0]`, so every PES this call completes is stamped with a /// [`crate::pes::SourcePos`]. The provenance-stamping entry point; the /// highway calls this with each batch's known source offset. The base must /// be the offset of the FIRST byte appended (i.e. of `data[0]`), which lines /// up with the current buffer tail. pub fn feed_at(&mut self, base_offset: u64, data: &[u8]) -> Vec { if !self.has_base { // First base seen: the offset of data[0] is base_offset, and data[0] // lands at buffer[buffer.len()], so buffer[0] is base_offset minus // the bytes already buffered. self.buffer_base = base_offset.saturating_sub(self.buffer.len() as u64); self.has_base = true; } self.buffer.extend_from_slice(data); self.extract_packets(false) } /// Flush remaining buffered data, returning any final PES packets. pub fn flush(&mut self) -> Vec { // At EOF, an unbounded (length 0) PES with no trailing start code is // a complete-but-unterminated final packet — emit it rather than // dropping the tail of the last frame. Genuinely incomplete packets // (a length-bounded PES short of its declared size) are still // discarded. let packets = self.extract_packets(true); self.buffer.clear(); // The buffer the cursor indexes into is gone. self.pending_scan = None; packets } /// Scan the buffer for complete start-code-delimited units and parse /// them. When `flushing` is true, a trailing unbounded PES that has no /// following start code is emitted using the rest of the buffer as its /// payload (EOF terminates it). fn extract_packets(&mut self, flushing: bool) -> Vec { let mut packets = Vec::with_capacity(4); let mut pos = 0; while let Some(sc) = find_start_code(&self.buffer, pos) { if sc + 3 >= self.buffer.len() { // Not enough bytes to read the start code ID. break; } let code = self.buffer[sc + 3]; match code { PROGRAM_END_ID => { // 00 00 01 B9 — 4 bytes, no payload. pos = sc + 4; } PACK_HEADER_ID => { // Pack header: need at least 14 bytes for MPEG-2 pack. if sc + 14 > self.buffer.len() { break; // wait for more data } // DVD-Video is always MPEG-2 PS, so every 0xBA is treated // as a 14-byte MPEG-2 pack: the low 3 bits of byte 13 are // pack_stuffing_length. (An MPEG-1 pack would be 12 bytes // with no stuffing field, but DVD never emits one.) let stuffing = (self.buffer[sc + 13] & 0x07) as usize; let pack_len = 14 + stuffing; if sc + pack_len > self.buffer.len() { break; } pos = sc + pack_len; } SYSTEM_HEADER_ID => { // System header: 00 00 01 BB [length:2] ... if sc + 6 > self.buffer.len() { break; } let header_len = ((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize; let total = 6 + header_len; if sc + total > self.buffer.len() { break; } pos = sc + total; } id if is_pes_stream_id(id) => { // PES packet: 00 00 01 [stream_id] [length:2] ... if sc + 6 > self.buffer.len() { break; } let pes_packet_len = ((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize; // Total bytes = 6 (start code + stream_id + length) + pes_packet_len. // A length of 0 means unbounded (video streams); in that // case the packet runs to the next PS-LAYER boundary (pack / // system header / program end / next PES), NOT the next raw // start code — the video ES payload is itself full of // 00 00 01 xx codes that would otherwise cut the PES short. let end = if pes_packet_len == 0 { // Resume where the last call stopped searching for // THIS PES's terminating unit; anything before that is // already proved boundary-free. let from = match self.pending_scan { Some((pes_at, searched_to)) if pes_at == sc => searched_to, _ => sc + 4, }; let (found, searched_to) = find_ps_boundary(&self.buffer, from); #[cfg(test)] { self.boundary_bytes_scanned += searched_to.saturating_sub(from) as u64; } match found { Some(next) => { self.pending_scan = None; next } // At EOF the rest of the buffer is this PES's // payload — emit it. None if flushing => { self.pending_scan = None; self.buffer.len() } None => { // No boundary buffered yet. Normally wait for // more data, but a corrupt stream could declare // an unbounded PES followed by endless non- // boundary bytes — bounding the buffer here // stops untrusted input forcing unbounded // allocation. Past the cap, flush what we have. if self.buffer.len() - sc > MAX_PS_BUFFER { self.pending_scan = None; self.buffer.len() } else { self.pending_scan = Some((sc, searched_to)); break; // wait for more data } } } } else { let e = sc + 6 + pes_packet_len; if e > self.buffer.len() { break; // wait for more data } e }; if let Some(mut pkt) = parse_pes_packet(&self.buffer[sc..end]) { if self.has_base { pkt.source = Some(crate::pes::SourcePos::at_byte(self.buffer_base + sc as u64)); } packets.push(pkt); } pos = end; } _ => { // Unknown start code — skip past it. pos = sc + 4; } } } if pos > 0 { self.buffer.drain(..pos); // Advance the absolute base past the drained bytes so subsequent // units stamp from the correct offset. if self.has_base { self.buffer_base += pos as u64; } // The cursor is a BUFFER offset, so it moves with the drain. A // pending PES always starts at or after `pos` (the loop broke on // it, having already consumed everything before it), so neither // component can underflow. self.pending_scan = self .pending_scan .map(|(pes_at, searched_to)| (pes_at - pos, searched_to - pos)); } // Trim a start-code-free tail. Every other exit from the loop above // leaves the buffer bounded (a pack, system header or length-bounded // PES is at most ~64 KiB; a length-0 PES is force-flushed at // MAX_PS_BUFFER), but a buffer that holds no `00 00 01` at all never // reaches any of those branches: `find_start_code` returns None, `pos` // stays 0 and nothing drains. Input that never contains a start code — // a zero-filled VOB extent, or an AACS-encrypted clip probed as // ciphertext — would then grow the buffer to the size of the whole // title. Nothing in such a buffer can begin a PS unit except a 2-byte // `00 00` prefix of a start code straddling the feed boundary, so keep // exactly that and drop the rest. Lossless: the retained bytes are the // only ones a later feed could complete into a start code. if self.buffer.len() > START_CODE_PREFIX_KEEP && find_start_code(&self.buffer, 0).is_none() { let drop = self.buffer.len() - START_CODE_PREFIX_KEEP; self.buffer.drain(..drop); if self.has_base { self.buffer_base += drop as u64; } // A pending PES implies a start code IS in the buffer, so this // branch cannot run while one is open; drop the cursor anyway // rather than leave a stale offset behind this drain. self.pending_scan = None; } packets } } /// Bytes retained when the buffer holds no start code: a `00 00 01` prefix can /// straddle a feed boundary by at most its first two bytes. const START_CODE_PREFIX_KEEP: usize = 2; /// Find the next PS-layer unit boundary at or after `from`: a start code whose /// ID byte is a pack (0xBA), system header (0xBB), program-end (0xB9), or a /// payload-carrying PES stream ID (0xBD..=0xEF). /// /// A length-0 (unbounded) video PES must be delimited by the next PS-layer unit /// — NOT by the next raw `00 00 01`. The MPEG-2 video elementary stream inside /// the PES is itself full of `00 00 01 xx` start codes (picture 0x00, slices /// 0x01..=0xAF, GOP 0xB8, sequence 0xB3); a plain start-code scan would cut the /// PES inside its own payload and re-scan the discarded video bytes as bogus PS /// units. Restricting the search to PS-layer IDs (>= 0xB9, excluding the video /// ES codes below it) frames the unbounded PES at the right boundary. /// Returns `(boundary, searched_to)`. `searched_to` is the offset up to which /// every byte has been PROVED not to begin a PS-layer boundary start code, so /// a later call over the same buffer (grown at the tail) may resume there /// instead of re-scanning the payload from the PES header. When the scan runs /// off the end, the last two bytes are NOT proved: a `00 00 01` prefix can /// straddle the next feed's boundary by up to two bytes. fn find_ps_boundary(data: &[u8], from: usize) -> (Option, usize) { let mut pos = from; while let Some(sc) = find_start_code(data, pos) { if sc + 3 >= data.len() { // A start code whose ID byte has not arrived yet: undecided, so // the next scan must look at it again. return (None, sc); } let id = data[sc + 3]; if id == PACK_HEADER_ID || id == SYSTEM_HEADER_ID || id == PROGRAM_END_ID || is_pes_stream_id(id) { return (Some(sc), sc); } pos = sc + 4; } (None, data.len().saturating_sub(2).max(from)) } /// Check whether a start code byte is a valid PES stream ID that carries payload. fn is_pes_stream_id(id: u8) -> bool { // Video: 0xE0-0xEF, MPEG audio: 0xC0-0xDF, private stream 1: 0xBD, // private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc. — plus the HD-DVD // extended-stream-id (0xFD), which carries VC-1 video / HD audio. crate::consts::pes_stream_id::PAYLOAD_RANGE.contains(&id) || id == EXTENDED_STREAM_ID } /// For an extended-stream-id (`0xFD`) PES, walk the optional PES-header fields to /// the PES extension and read the 7-bit `stream_id_extension` — the real stream /// id. `data` starts at the PES start code; the optional fields live in /// `data[9..header_end]` (all bounds-checked against `header_end`). Returns `None` /// if the extension is absent or malformed. fn parse_stream_id_extension(data: &[u8], flags2: u8, header_end: usize) -> Option { let get = |p: usize| -> Option { if p < header_end { data.get(p).copied() } else { None } }; let mut pos = 9usize; let pts_dts = (flags2 >> 6) & 0x03; if pts_dts & 0x02 != 0 { pos += 5; // PTS } if pts_dts == 0x03 { pos += 5; // DTS } if flags2 & 0x20 != 0 { pos += 6; // ESCR } if flags2 & 0x10 != 0 { pos += 3; // ES_rate } if flags2 & 0x08 != 0 { pos += 1; // DSM_trick_mode } if flags2 & 0x04 != 0 { pos += 1; // additional_copy_info } if flags2 & 0x02 != 0 { pos += 2; // PES_CRC } if flags2 & 0x01 == 0 { return None; // no PES_extension } let ext_flags = get(pos)?; pos += 1; if ext_flags & 0x80 != 0 { pos += 16; // PES_private_data } if ext_flags & 0x40 != 0 { // pack_header_field: 1-byte length + that many bytes. pos += 1 + get(pos)? as usize; } if ext_flags & 0x20 != 0 { pos += 2; // program_packet_sequence_counter } if ext_flags & 0x10 != 0 { pos += 2; // P-STD_buffer } if ext_flags & 0x01 == 0 { return None; // no PES_extension_flag_2 } // PES_extension_field_length (7 bits, marker in the top bit), then the // stream_id_extension byte: present when its top bit (the extension flag) is 0. let _field_len = get(pos)? & 0x7F; pos += 1; let b = get(pos)?; (b & 0x80 == 0).then_some(b & 0x7F) } /// Parse a single PES packet from a byte slice that starts at the start code. fn parse_pes_packet(data: &[u8]) -> Option { // Minimum: 00 00 01 [id] [len:2] = 6 bytes if data.len() < 6 { return None; } if data[0] != 0x00 || data[1] != 0x00 || data[2] != 0x01 { return None; } let stream_id = data[3]; // Padding stream — skip entirely. if stream_id == crate::consts::pes_stream_id::PADDING_STREAM { return None; } // Streams without standard PES header extension. if stream_id == PRIVATE_STREAM_2 { let payload = if data.len() > 6 { &data[6..] } else { &[] }; return Some(PsPacket { stream_id, sub_stream_id: None, pts: None, dts: None, data: payload.to_vec(), // Stamped by the demuxer (extract_packets) when a source base is // threaded; the free function has no absolute offset of its own. source: None, }); } // Standard PES header: [6]=flags1, [7]=flags2, [8]=header_data_length if data.len() < 9 { return None; } let pts_dts_flags = (data[7] >> 6) & 0x03; let header_data_len = data[8] as usize; let header_end = 9 + header_data_len; if header_end > data.len() { return None; } let mut pts = None; let mut dts = None; // The PTS (5 bytes at data[9..14]) and DTS (5 bytes at data[14..19]) // live INSIDE the PES header, so gate on header_data_len covering them // (>=5 for PTS, >=10 for PTS+DTS), not merely on total length. A // non-conformant packet that sets the flags but declares a too-short // header would otherwise read payload bytes as a bogus timestamp. if pts_dts_flags >= 2 && header_data_len >= 5 && data.len() >= 14 { pts = parse_pts(&data[9..14]); } if pts_dts_flags == 3 && header_data_len >= 10 && data.len() >= 19 { dts = parse_pts(&data[14..19]); } let payload = &data[header_end..]; // For private stream 1, the first payload byte is the sub-stream ID, // followed by a sub-header whose length depends on the sub-stream type. let (sub_stream_id, es_data) = if stream_id == EXTENDED_STREAM_ID { // HD-DVD extended-stream-id: the real stream id lives in the // stream_id_extension inside the PES extension. There is no leading // sub-header byte on the payload (unlike private_stream_1), so the ES // is the payload verbatim. ( parse_stream_id_extension(data, data[7], header_end), payload.to_vec(), ) } else if stream_id == PRIVATE_STREAM_1 && !payload.is_empty() { let sub_id = payload[0]; let skip = match sub_id { 0x80..=0x8F => 4, // AC3/DTS: sub_id + frame_count + access_unit_ptr(2) // HD-DVD Dolby Digital Plus (E-AC-3): the sub-header is the same // 4-byte shape as DVD AC-3 — sub_id + number_of_frames(1) + // first_access_unit_pointer(2). Verified empirically on ANCHORMAN // EVO: across every 0xC0..=0xC7 packet the 0x0B77 E-AC-3 syncword // sits `first_access_unit_pointer` bytes past this 4-byte header // (the leading bytes are the tail of the previous frame). Stripping // exactly these 4 bytes on EVERY packet yields a clean, continuous // E-AC-3 elementary stream that the ac3 parser reassembles across // PES boundaries; a shorter skip would splice the sub-header bytes // into a straddling frame and corrupt it. 0xC0..=0xC7 => 4, 0xA0..=0xA7 => 7, // LPCM: sub_id + frames + ptr(2) + emphasis + quant_freq + channels _ => 1, }; let start = skip.min(payload.len()); (Some(sub_id), payload[start..].to_vec()) } else { (None, payload.to_vec()) }; Some(PsPacket { stream_id, sub_stream_id, pts, dts, data: es_data, // Stamped by the demuxer (extract_packets) when a source base is threaded. source: None, }) } /// Parse a 5-byte PTS/DTS timestamp field (33 bits at 90kHz). /// /// Layout (ISO/IEC 13818-1 Table 2-17): /// ```text /// byte0: [prefix:4][pts 32..30:3][marker:1] /// byte1: [pts 29..22:8] /// byte2: [pts 21..15:7][marker:1] /// byte3: [pts 14..7:8] /// byte4: [pts 6..0:7][marker:1] /// ``` fn parse_pts(buf: &[u8]) -> Option { debug_assert!(buf.len() >= 5); // Validate the three marker bits (bit 0 of bytes 0, 2, 4) per MPEG-2 // Systems Table 2-17. A timestamp with a cleared marker is malformed — // matching ts.rs::parse_timestamp, reject it rather than decode garbage. if (buf[0] & 0x01) == 0 || (buf[2] & 0x01) == 0 || (buf[4] & 0x01) == 0 { return None; } let b0 = buf[0] as u64; let b1 = buf[1] as u64; let b2 = buf[2] as u64; let b3 = buf[3] as u64; let b4 = buf[4] as u64; Some(((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1) } #[cfg(test)] mod tests { use super::*; // --- Pack header detection --- #[test] fn detect_pack_header() { let mut demuxer = PsDemuxer::new(); // MPEG-2 pack header: 14 bytes, stuffing_length = 0 let mut pack = vec![ 0x00, 0x00, 0x01, 0xBA, // start code 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, // SCR (6 bytes) 0x01, 0x89, 0xC3, // mux_rate (3 bytes) 0xF8, // stuffing_length = 0 (lower 3 bits) ]; // Follow with a PES packet so we have a delimiter pack.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, // video stream 0x00, 0x08, // length = 8 0x80, 0x00, 0x00, // flags: no PTS/DTS, header_data_length = 0 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, // payload (5 bytes) ]); let packets = demuxer.feed(&pack); assert_eq!(packets.len(), 1); assert_eq!(packets[0].stream_id, 0xE0); assert_eq!(packets[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]); } #[test] fn pack_header_with_stuffing() { let mut demuxer = PsDemuxer::new(); // Pack header with 3 stuffing bytes let mut data = vec![ 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3, 0xFB, // stuffing_length = 3 0xFF, 0xFF, 0xFF, // stuffing bytes ]; // Followed by a PES packet data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xC0, // audio stream 0x00, 0x05, // length = 5 0x80, 0x00, 0x00, // flags: no PTS, header_data_len=0 0x11, 0x22, // payload ]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].stream_id, 0xC0); assert_eq!(packets[0].data, vec![0x11, 0x22]); } // --- PES header + PTS parsing --- #[test] fn pes_header_with_pts() { let mut demuxer = PsDemuxer::new(); // PTS = 90000 (1 second at 90kHz) // 90000 = 0x15F90 // bit32=0, bits 29-15 = 0x0002BF, bits 14-0 = 0x1F90 // byte0: 0010_0_1 = 0x21 ... actually let's encode properly: // // pts = 90000 // byte0: (0010 << 4) | ((pts >> 29) & 0x0E) | 1 // = 0x20 | ((90000 >> 29) & 0x0E) | 1 = 0x20 | 0 | 1 = 0x21 // byte1: (pts >> 22) & 0xFF = (90000 >> 22) & 0xFF = 0 // byte2: ((pts >> 14) & 0xFE) | 1 = ((90000 >> 14) & 0xFE) | 1 = (0x0A & 0xFE) | 1 = 0x0B // byte3: (pts >> 7) & 0xFF = (90000 >> 7) & 0xFF = (703) & 0xFF = 0xBF // byte4: ((pts & 0x7F) << 1) | 1 = ((90000 & 0x7F) << 1) | 1 = (0x10 << 1) | 1 = 0x21 let pts_bytes = encode_pts(90000, 0x20); let mut data = vec![ 0x00, 0x00, 0x01, 0xE0, // video stream 0x00, 0x0D, // length = 13 0x80, 0x80, 0x05, // flags: PTS only, header_data_len=5 ]; data.extend_from_slice(&pts_bytes); data.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x00]); // payload // Add a delimiter data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); // program end let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].stream_id, 0xE0); assert_eq!(packets[0].pts, Some(90000)); assert!(packets[0].dts.is_none()); assert_eq!(packets[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00]); } #[test] fn pes_header_with_pts_and_dts() { let mut demuxer = PsDemuxer::new(); let pts_bytes = encode_pts(180000, 0x30); // PTS marker = 0x30 let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10 let mut data = vec![ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x11, // length = 17 0x80, 0xC0, 0x0A, // flags: PTS+DTS, header_data_len=10 ]; data.extend_from_slice(&pts_bytes); data.extend_from_slice(&dts_bytes); data.extend_from_slice(&[0xCA, 0xFE]); // payload data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].pts, Some(180000)); assert_eq!(packets[0].dts, Some(90000)); } // --- Private stream 1 sub-stream extraction --- #[test] fn private_stream_1_ac3_substream() { let mut demuxer = PsDemuxer::new(); // AC3 sub-header: sub_id(1) + frame_count(1) + access_unit_ptr(2) = 4 bytes let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, // private stream 1 0x00, 0x0B, // length = 11 0x80, 0x00, 0x00, // no PTS, header_data_len=0 0x80, // sub-stream ID: AC3 stream 0 0x01, 0x00, 0x02, // frame_count + access_unit_ptr (sub-header bytes) 0xAA, 0xBB, 0xCC, 0xDD, // AC3 payload ]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].stream_id, 0xBD); assert_eq!(packets[0].sub_stream_id, Some(0x80)); assert_eq!(packets[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]); } #[test] fn private_stream_1_dts_substream() { let mut demuxer = PsDemuxer::new(); // DTS sub-header: sub_id(1) + frame_count(1) + access_unit_ptr(2) = 4 bytes let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, 0x00, 0x09, // length = 9 0x80, 0x00, 0x00, // no PTS, header_data_len=0 0x88, // sub-stream ID: DTS stream 0 0x01, 0x00, 0x00, // sub-header (frame_count + access_unit_ptr) 0x11, 0x22, ]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].sub_stream_id, Some(0x88)); assert_eq!(packets[0].data, vec![0x11, 0x22]); } #[test] fn private_stream_1_subtitle_substream() { let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00, 0x20, // sub-stream ID: subtitle stream 0 0xFF, 0xFE, ]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].sub_stream_id, Some(0x20)); } #[test] fn private_stream_1_lpcm_substream() { let mut demuxer = PsDemuxer::new(); // LPCM sub-header: sub_id(1) + frames(1) + ptr(2) + emphasis(1) + quant_freq(1) + channels(1) = 7 bytes let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, 0x00, 0x0C, // length = 12 0x80, 0x00, 0x00, // no PTS, header_data_len=0 0xA0, // sub-stream ID: LPCM stream 0 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // LPCM sub-header (6 bytes after sub_id) 0x01, 0x02, // LPCM payload ]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1); assert_eq!(packets[0].sub_stream_id, Some(0xA0)); assert_eq!(packets[0].data, vec![0x01, 0x02]); } // --- Incremental feeding --- #[test] fn incremental_feed() { let mut demuxer = PsDemuxer::new(); let mut full = vec![ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x06, // length = 6 0x80, 0x00, 0x00, // no PTS, header_data_len=0 0xAA, 0xBB, 0xCC, ]; full.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); // Feed in two halves let mid = full.len() / 2; let p1 = demuxer.feed(&full[..mid]); assert!(p1.is_empty(), "first half should not produce packets"); let p2 = demuxer.feed(&full[mid..]); assert_eq!(p2.len(), 1); assert_eq!(p2[0].data, vec![0xAA, 0xBB, 0xCC]); } #[test] fn flush_emits_trailing_unbounded_video_pes() { let mut demuxer = PsDemuxer::new(); // Unbounded (length 0) video PES with no trailing start code — the // common EOF case. feed() must not emit it (awaiting a delimiter), // but flush() must emit the tail rather than discarding it. let data = vec![ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, // video, length 0 (unbounded) 0x80, 0x00, 0x00, // no PTS, header_data_len = 0 0xAA, 0xBB, 0xCC, 0xDD, ]; let fed = demuxer.feed(&data); assert!(fed.is_empty(), "unbounded PES not emitted until delimited"); let flushed = demuxer.flush(); assert_eq!(flushed.len(), 1, "flush emits the trailing PES"); assert_eq!(flushed[0].stream_id, 0xE0); assert_eq!(flushed[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]); } // --- Multiple PES packets --- #[test] fn multiple_pes_packets() { let mut demuxer = PsDemuxer::new(); let mut data = Vec::new(); // First PES: video data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22, ]); // Second PES: audio data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44, ]); // Delimiter data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 2); assert_eq!(packets[0].stream_id, 0xE0); assert_eq!(packets[1].stream_id, 0xC0); } // --- unbounded (length-0) video PES framing --- #[test] fn unbounded_video_pes_not_cut_by_embedded_start_codes() { // A length-0 video PES whose ES payload contains embedded MPEG start // codes (picture 0x00, slice 0x01, GOP 0xB8, sequence 0xB3) must be // delimited by the NEXT PS-layer boundary (here a program-end 0xB9), // not by the first embedded 00 00 01 inside the payload. let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xE0, // video stream 0x00, 0x00, // length = 0 (unbounded) 0x80, 0x00, 0x00, // flags: no PTS, header_data_len = 0 ]; // ES payload with embedded MPEG-2 start codes. let payload = [ 0x00, 0x00, 0x01, 0xB3, // sequence header 0x11, 0x22, 0x00, 0x00, 0x01, 0x00, // picture start code 0x33, 0x44, 0x00, 0x00, 0x01, 0x01, // slice 0x55, 0x66, ]; data.extend_from_slice(&payload); // PS-layer boundary that closes the unbounded PES. data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 1, "one PES, not several payload fragments"); assert_eq!(packets[0].stream_id, 0xE0); // The whole ES payload survives — none of it discarded as bogus units. assert_eq!(packets[0].data, payload.to_vec()); } #[test] fn unbounded_video_pes_waits_for_boundary() { // Without a following PS-layer boundary the unbounded PES is held // (waiting for more data), not emitted truncated. let mut demuxer = PsDemuxer::new(); let mut data = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0xAA, 0xBB]); // picture SC, no PS boundary let packets = demuxer.feed(&data); assert!(packets.is_empty(), "no PS boundary yet → hold the PES"); } #[test] fn input_with_no_start_code_at_all_is_bounded() { // An extent that never contains a 00 00 01 start code — a zero-filled // VOB extent, or an AACS-encrypted clip probed as ciphertext (see // src/disc/hddvd.rs) — must not accumulate. The whole-title feed in // src/mux/disc.rs would otherwise grow the buffer to the size of the // title (up to ~90 GB for UHD). let mut demuxer = PsDemuxer::new(); let chunk = vec![0u8; 1024 * 1024]; for _ in 0..(MAX_PS_BUFFER / chunk.len() + 8) { assert!(demuxer.feed(&chunk).is_empty(), "no start code → no PES"); } assert!( demuxer.buffer.len() <= MAX_PS_BUFFER, "buffer grew to {} with no start code ever seen (cap {})", demuxer.buffer.len(), MAX_PS_BUFFER ); // Nothing in a start-code-free buffer can ever begin a unit except a // 2-byte 00 00 prefix, so the retained tail is tiny. assert!( demuxer.buffer.len() <= 2, "start-code-free tail retained {} bytes", demuxer.buffer.len() ); } #[test] fn start_code_split_across_feeds_still_parses() { // The start-code-free trim must keep the 2 bytes that can be the // prefix of a start code straddling a feed boundary. let mut demuxer = PsDemuxer::new(); assert!(demuxer.feed(&[0xFF, 0xFF, 0x00, 0x00]).is_empty()); let mut rest = vec![0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00, 0xAA, 0xBB]; rest.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); // PS-layer boundary let packets = demuxer.feed(&rest); assert_eq!(packets.len(), 1, "split start code must still be found"); assert_eq!(packets[0].stream_id, 0xE0); assert_eq!(packets[0].data, vec![0xAA, 0xBB]); } #[test] fn unbounded_video_pes_over_cap_is_force_flushed() { // A corrupt stream declaring an unbounded PES followed by endless // non-boundary bytes must not grow the buffer without limit. // NOTE: this case feeds a real start code first, so it exercises only // the in-PES cap; the no-start-code path is covered by // `input_with_no_start_code_at_all_is_bounded`. let mut demuxer = PsDemuxer::new(); let header = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00]; let packets = demuxer.feed(&header); assert!(packets.is_empty()); // Feed >MAX_PS_BUFFER of bytes containing no PS-layer boundary. let chunk = vec![0x55u8; 1024 * 1024]; let mut emitted = 0; for _ in 0..(MAX_PS_BUFFER / chunk.len() + 4) { emitted += demuxer.feed(&chunk).len(); } assert!( demuxer.buffer.len() <= MAX_PS_BUFFER + chunk.len(), "buffer grew to {} (cap {})", demuxer.buffer.len(), MAX_PS_BUFFER ); // The force-flush emits the over-long PES rather than accumulating it. assert!(emitted >= 1, "over-cap unbounded PES is force-flushed"); } // --- PTS parsing edge cases --- #[test] fn pts_zero() { // PTS = 0 encoded let pts = parse_pts(&encode_pts(0, 0x20)); assert_eq!(pts, Some(0)); } #[test] fn pts_large_value() { // Test a large PTS value (close to 33-bit max) let val: u64 = (1 << 32) - 1; // 0xFFFFFFFF let encoded = encode_pts(val, 0x20); let decoded = parse_pts(&encoded); assert_eq!(decoded, Some(val)); } // --- DVD PID mapping (track-routing collision regression) --- fn mk(stream_id: u8, sub: Option) -> PsPacket { PsPacket { stream_id, sub_stream_id: sub, pts: None, dts: None, data: vec![0xAA], source: None, } } #[test] fn parse_extended_stream_id_extracts_stream_id_extension() { // SHAUN's VC-1 video PES: stream_id 0xFD, flags2=0x01 (PES_extension // only), header_data_length=3, optional bytes 0x0F/... — build the // minimal well-formed variant: ext_flags=0x01 (PES_extension_flag_2), // field_len=0x81, stream_id_extension=0x55. Payload is the ES. let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID]; let opt = [0x01u8, 0x81, 0x55]; let es = [0xDEu8, 0xAD, 0xBE, 0xEF]; let len = (3 + opt.len() + es.len()) as u16; pkt.extend_from_slice(&len.to_be_bytes()); pkt.extend_from_slice(&[0x80, 0x01, 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.stream_id, EXTENDED_STREAM_ID); assert_eq!( parsed.sub_stream_id, Some(0x55), "stream_id_extension extracted from PES extension" ); // ES is the payload verbatim — no leading sub-header byte stripped. assert_eq!(parsed.data, es); // Routes to the extended-stream-id PID space. assert_eq!(parsed.dvd_pid(), Some(hddvd_extended_pid(0x55))); assert_eq!(parsed.dvd_pid(), Some(0xFD55)); } #[test] fn parse_extended_stream_id_skips_pts_and_dts_before_the_extension() { // The common real case: an AU-opening 0xFD VC-1 video PES carries a PTS // (and often DTS) in the optional-header region, which the parser must // SKIP (PTS +5, DTS +5) to reach the PES_extension → stream_id_extension. // Both branches were previously untested (flags2 there was 0x01, skipping // everything), so an off-by-one in the skip would silently misroute video. let build = |flags2: u8, skip: usize| { let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID]; // optional region: `skip` bytes (PTS/DTS placeholders) then // ext_flags=0x01, field_len=0x81, stream_id_extension=0x55. let mut opt = vec![0xFFu8; skip]; opt.extend_from_slice(&[0x01, 0x81, 0x55]); let es = [0xDEu8, 0xAD]; let len = (3 + opt.len() + es.len()) as u16; pkt.extend_from_slice(&len.to_be_bytes()); // flags1=0x80, flags2, header_data_length = optional region length. pkt.extend_from_slice(&[0x80, flags2, opt.len() as u8]); pkt.extend_from_slice(&opt); pkt.extend_from_slice(&es); pkt }; // PTS present (pts_dts bits = 10 → flags2 0x80) + PES_extension (0x01). let pts_only = parse_pes_packet(&build(0x81, 5)).expect("parses"); assert_eq!( pts_only.sub_stream_id, Some(0x55), "extension found after skipping a 5-byte PTS" ); // PTS+DTS present (pts_dts bits = 11 → flags2 0xC0) + PES_extension. let pts_dts = parse_pes_packet(&build(0xC1, 10)).expect("parses"); assert_eq!( pts_dts.sub_stream_id, Some(0x55), "extension found after skipping a 10-byte PTS+DTS" ); } #[test] fn parse_extended_stream_id_without_extension_yields_no_sub_id() { // A 0xFD PES that declares no PES_extension (flags2=0x00) can't carry a // stream_id_extension → sub_stream_id None, and dvd_pid falls through. let mut pkt = vec![0x00, 0x00, 0x01, EXTENDED_STREAM_ID]; let es = [0x11u8, 0x22]; let len = (3 + es.len()) as u16; pkt.extend_from_slice(&len.to_be_bytes()); pkt.extend_from_slice(&[0x80, 0x00, 0x00]); pkt.extend_from_slice(&es); let parsed = parse_pes_packet(&pkt).expect("parses"); assert_eq!(parsed.sub_stream_id, None); assert_eq!(parsed.dvd_pid(), None); assert_eq!(parsed.data, es); } #[test] fn dvd_pid_matches_scanner_assignment() { // Video → 0xE0 (matches dvd.rs VideoStream pid). assert_eq!(mk(0xE0, None).dvd_pid(), Some(DVD_VIDEO_PID)); // PID = 0xBD00 | sub_stream_id — unique per sub-id, no collision. assert_eq!(mk(0xBD, Some(0x80)).dvd_pid(), Some(0xBD80)); // AC-3 #0 assert_eq!(mk(0xBD, Some(0x81)).dvd_pid(), Some(0xBD81)); // AC-3 #1 assert_eq!(mk(0xBD, Some(0x88)).dvd_pid(), Some(0xBD88)); // DTS #0 assert_eq!(mk(0xBD, Some(0xA0)).dvd_pid(), Some(0xBDA0)); // LPCM #0 // VobSub subtitle 0x20/0x21 → 0x20 / 0x21 (identity). assert_eq!(mk(0xBD, Some(0x20)).dvd_pid(), Some(0x20)); assert_eq!(mk(0xBD, Some(0x21)).dvd_pid(), Some(0x21)); // Unmappable: MPEG audio, private stream 2, bogus sub-id. assert_eq!(mk(0xC0, None).dvd_pid(), None); assert_eq!(mk(0xBF, None).dvd_pid(), None); assert_eq!(mk(0xBD, Some(0x10)).dvd_pid(), None); } #[test] fn mixed_codec_audio_does_not_collide() { // The core regression: a title mixing AC-3 (0x80), DTS (0x88) and // LPCM (0xA0) audio. The old per-codec relative arithmetic mapped // all three to 0xBD00. They must now get distinct PIDs that match // what dvd.rs assigns from the same dvd_audio_pid() table. let ac3 = mk(0xBD, Some(0x80)).dvd_pid().unwrap(); let dts = mk(0xBD, Some(0x88)).dvd_pid().unwrap(); let lpcm = mk(0xBD, Some(0xA0)).dvd_pid().unwrap(); assert_ne!(ac3, dts, "AC-3 and DTS must not collide"); assert_ne!(ac3, lpcm, "AC-3 and LPCM must not collide"); assert_ne!(dts, lpcm, "DTS and LPCM must not collide"); // Scanner side uses the same table; build a pid_to_track for a // mixed-codec title [video, AC-3, DTS, LPCM, sub] and route every // PS packet to its own distinct track. let pid_to_track: Vec<(u16, usize)> = vec![ (DVD_VIDEO_PID, 0), (dvd_audio_pid(0x80).unwrap(), 1), (dvd_audio_pid(0x88).unwrap(), 2), (dvd_audio_pid(0xA0).unwrap(), 3), (dvd_subtitle_pid(0x20).unwrap(), 4), ]; let route = |p: PsPacket| -> Option { let pid = p.dvd_pid()?; pid_to_track .iter() .find(|(x, _)| *x == pid) .map(|(_, t)| *t) }; assert_eq!(route(mk(0xE0, None)), Some(0)); assert_eq!(route(mk(0xBD, Some(0x80))), Some(1)); // AC-3 → its own track assert_eq!(route(mk(0xBD, Some(0x88))), Some(2)); // DTS → its own track assert_eq!(route(mk(0xBD, Some(0xA0))), Some(3)); // LPCM → its own track assert_eq!(route(mk(0xBD, Some(0x20))), Some(4)); // sub → its own track } #[test] fn subtitle_does_not_collide_with_audio_track() { // Subtitle sub-id 0x20 routes to its own subtitle PID (0x20), // distinct from any audio PID (0xBD80+). let audio0 = mk(0xBD, Some(0x80)).dvd_pid().unwrap(); // 0xBD80 let sub0 = mk(0xBD, Some(0x20)).dvd_pid().unwrap(); // 0x20 assert_ne!( audio0, sub0, "subtitle sub-id 0x20 must NOT map to the audio PID" ); let pid_to_track: Vec<(u16, usize)> = vec![ (DVD_VIDEO_PID, 0), (dvd_audio_pid(0x80).unwrap(), 1), (dvd_audio_pid(0x81).unwrap(), 2), (dvd_subtitle_pid(0x20).unwrap(), 3), (dvd_subtitle_pid(0x21).unwrap(), 4), ]; let route = |p: PsPacket| -> Option { let pid = p.dvd_pid()?; pid_to_track .iter() .find(|(x, _)| *x == pid) .map(|(_, t)| *t) }; assert_eq!(route(mk(0xE0, None)), Some(0)); assert_eq!(route(mk(0xBD, Some(0x80))), Some(1)); assert_eq!(route(mk(0xBD, Some(0x81))), Some(2)); assert_eq!(route(mk(0xBD, Some(0x20))), Some(3)); // sub0 → track 3, NOT 1 assert_eq!(route(mk(0xBD, Some(0x21))), Some(4)); // sub1 → track 4, NOT 2 } // --- Helper: encode PTS for tests --- fn encode_pts(pts: u64, marker_prefix: u8) -> [u8; 5] { let mut buf = [0u8; 5]; buf[0] = marker_prefix | (((pts >> 30) as u8) & 0x07) << 1 | 1; buf[1] = ((pts >> 22) & 0xFF) as u8; buf[2] = (((pts >> 15) & 0x7F) as u8) << 1 | 1; buf[3] = ((pts >> 7) & 0xFF) as u8; buf[4] = (((pts) & 0x7F) as u8) << 1 | 1; buf } // ════════════════════════════════════════════════════════════════════ // Added hardening tests // ════════════════════════════════════════════════════════════════════ /// Program-end start code (00 00 01 B9) — used as a delimiter so a /// bounded or unbounded PES preceding it is fully framed. const PROGRAM_END: [u8; 4] = [0x00, 0x00, 0x01, 0xB9]; // ── parse_pts: full 33-bit field round trip (ISO 13818-1 Table 2-17) ── #[test] fn parse_pts_max_33bit() { // The PTS field is exactly 33 bits; 2^33-1 must round-trip — a // truncated shift/mask would lose the top bits. let max = (1u64 << 33) - 1; assert_eq!(parse_pts(&encode_pts(max, 0x20)), Some(max)); } #[test] fn parse_pts_rejects_bad_marker_bits() { // A timestamp with any marker bit (bit 0 of bytes 0/2/4) cleared is // malformed and must be rejected, matching ts.rs::parse_timestamp. let mut buf = encode_pts(90000, 0x20); assert!(parse_pts(&buf).is_some()); buf[0] &= !0x01; assert_eq!(parse_pts(&buf), None); let mut buf = encode_pts(90000, 0x20); buf[2] &= !0x01; assert_eq!(parse_pts(&buf), None); let mut buf = encode_pts(90000, 0x20); buf[4] &= !0x01; assert_eq!(parse_pts(&buf), None); } // ── pack header (0xBA) framing ──────────────────────────────────────── #[test] fn pack_header_waits_for_full_14_bytes() { // A pack header needs 14 bytes (MPEG-2). A buffer with only the // start code + a few bytes must NOT advance past it — the demuxer // waits for more data rather than misframing. let mut demuxer = PsDemuxer::new(); // 00 00 01 BA then only 6 of the 10 remaining pack bytes. let partial = vec![0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01]; let p = demuxer.feed(&partial); assert!(p.is_empty()); // Now supply the rest of the pack (stuffing=0) plus a PES + delimiter. let mut rest = vec![0x01, 0x89, 0xC3, 0xF8]; // mux_rate(3) + stuffing byte rest.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0xAB, 0xCD, ]); rest.extend_from_slice(&PROGRAM_END); let p2 = demuxer.feed(&rest); assert_eq!(p2.len(), 1, "PES after a now-complete pack header parses"); assert_eq!(p2[0].data, vec![0xAB, 0xCD]); } #[test] fn pack_header_stuffing_length_consumed() { // pack_stuffing_length = low 3 bits of byte 13 (ISO 13818-1 // §2.5.3.4). The demuxer must skip exactly 14 + stuffing bytes. The // stuffing region here holds a DECOY PES start code (00 00 01 E0…); // if the stuffing count is under-consumed the scanner would re-sync // onto that decoy and emit a bogus PES. Correct skip lands directly // on the REAL PES. let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3, 0xFD, // stuffing_length = 5 (low 3 bits of 0xFD = 0b101) // 5 stuffing bytes containing a decoy PES start code. 0x00, 0x00, 0x01, 0xE0, 0xDE, ]; // Real PES carries 0x11 0x22; the decoy (if mis-parsed) would carry // garbage with a different/short payload. data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22, ]); data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1, "exactly the real PES; the decoy was skipped"); assert_eq!(p[0].data, vec![0x11, 0x22]); } // ── system header (0xBB) framing ────────────────────────────────────── #[test] fn system_header_length_skipped() { // System header: 00 00 01 BB [header_length:2] body. The demuxer // must skip 6 + header_length bytes (ISO 13818-1 §2.5.3.5), even // though the body contains bytes that look like PES IDs. let mut demuxer = PsDemuxer::new(); let body = [0x00, 0x00, 0x01, 0xE0, 0xFF, 0xFF]; // decoy PES-looking bytes let mut data = vec![0x00, 0x00, 0x01, 0xBB]; data.extend_from_slice(&(body.len() as u16).to_be_bytes()); data.extend_from_slice(&body); // Real PES after the system header. data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44, ]); data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!( p.len(), 1, "decoy bytes inside system header not parsed as PES" ); assert_eq!(p[0].stream_id, 0xC0); assert_eq!(p[0].data, vec![0x33, 0x44]); } #[test] fn system_header_waits_for_full_body() { // System header declaring a body longer than buffered must not // advance — wait for more data. let mut demuxer = PsDemuxer::new(); let mut data = vec![0x00, 0x00, 0x01, 0xBB, 0x00, 0x20]; // len=32 data.extend_from_slice(&[0xAA; 4]); // only 4 of 32 body bytes assert!(demuxer.feed(&data).is_empty()); } // ── PES length / boundary handling ──────────────────────────────────── #[test] fn bounded_pes_waits_for_full_declared_length() { // A PES with a non-zero PES_packet_length must not be emitted until // all 6 + length bytes are buffered — never emit a short frame. let mut demuxer = PsDemuxer::new(); // length = 5 → total 11 bytes, supply only 9. let head = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00]; assert!(demuxer.feed(&head).is_empty()); // supply the remaining 2 payload bytes. let p = demuxer.feed(&[0xEE, 0xFF]); assert_eq!(p.len(), 1); assert_eq!(p[0].data, vec![0xEE, 0xFF]); } #[test] fn padding_stream_0xbe_is_dropped() { // Padding stream (0xBE) carries no ES (ISO 13818-1 Table 2-22) and // must produce no PsPacket — only the real PES survives. let mut demuxer = PsDemuxer::new(); let mut data = vec![0x00, 0x00, 0x01, 0xBE, 0x00, 0x04, 0xFF, 0xFF, 0xFF, 0xFF]; data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x01, 0x02, ]); data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1, "padding stream dropped; only real PES emitted"); assert_eq!(p[0].stream_id, 0xE0); } #[test] fn private_stream_2_0xbf_has_no_pes_extension() { // private_stream_2 (0xBF) carries no standard PES header extension // (ISO 13818-1 Table 2-22): the bytes after the 6-byte prefix are // raw payload, NOT flags/header_data_length. No PTS, no sub-stream. let mut demuxer = PsDemuxer::new(); let mut data = vec![0x00, 0x00, 0x01, 0xBF, 0x00, 0x04, 0xDE, 0xAD, 0xBE, 0xEF]; data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1); assert_eq!(p[0].stream_id, 0xBF); assert_eq!(p[0].pts, None, "0xBF carries no PTS"); assert_eq!(p[0].sub_stream_id, None); assert_eq!(p[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF]); } /// `is_nav()` exists to separate the ONE unmappable stream a DVD is expected /// to contain — private_stream_2 (0xBF), the PCI/DSI navigation packs /// (ISO/IEC 13818-1 Table 2-22) — from every other packet whose `dvd_pid()` /// comes back `None`, which is an unexpected, possibly-lost real stream. The /// mux loops use the distinction to choose between a silent tally and a /// per-packet WARN, so collapsing it to a constant either buries a genuine /// stream loss in the nav tally, or floods the log with one warning per /// navigation pack on every DVD ever ripped. /// /// The invariant that ties the two together: `is_nav()` may only ever be true /// where `dvd_pid()` is `None` — a packet that routes to a real track must /// never be silently classified as navigation. #[test] fn only_private_stream_2_is_navigation_and_never_a_routable_stream() { // Demux a program stream carrying, in order: a navigation pack, MPEG-2 // video, an AC-3 audio substream, and an MPEG audio stream (unmappable on // DVD, but NOT navigation). let mut demuxer = PsDemuxer::new(); let mut data = Vec::new(); // private_stream_2: no PES extension, payload follows the 6-byte prefix. data.extend_from_slice(&[0x00, 0x00, 0x01, 0xBF, 0x00, 0x02, 0x00, 0x01]); // video 0xE0 data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22, ]); // private_stream_1 with AC-3 sub-stream 0x80 (4 bytes of substream header // follow the sub-id on DVD). data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xBD, 0x00, 0x0A, 0x80, 0x00, 0x00, 0x80, 0x01, 0x00, 0x03, 0x00, 0xAA, 0xBB, ]); // MPEG audio 0xC0 data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44, ]); data.extend_from_slice(&PROGRAM_END); let packets = demuxer.feed(&data); assert_eq!(packets.len(), 4, "four PES packets demuxed"); let nav: Vec = packets .iter() .filter(|p| p.is_nav()) .map(|p| p.stream_id) .collect(); assert_eq!( nav, vec![0xBF], "exactly the private_stream_2 pack is navigation" ); for p in &packets { if p.is_nav() { assert_eq!( p.dvd_pid(), None, "a navigation pack must not also route to a track" ); } } // The MPEG-audio packet is equally unroutable on DVD, yet must NOT be // absorbed into the nav tally — that is the distinction being drawn. let mpa = packets.iter().find(|p| p.stream_id == 0xC0).unwrap(); assert_eq!(mpa.dvd_pid(), None, "MPEG audio is unmappable on DVD"); assert!(!mpa.is_nav(), "...but it is a lost stream, not navigation"); } #[test] fn unknown_start_code_is_skipped_not_parsed() { // A start code with an ID outside the known PS-layer set // (e.g. 0xB0, reserved) must be skipped 4 bytes and not derail // the following real PES. let mut demuxer = PsDemuxer::new(); let mut data = vec![0x00, 0x00, 0x01, 0xB0]; // unknown/reserved code data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x9A, 0xBC, ]); data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1); assert_eq!(p[0].data, vec![0x9A, 0xBC]); } // ── private_stream_1 sub-header skip lengths ────────────────────────── #[test] fn private_stream_1_unknown_subid_skips_one_byte() { // For a private_stream_1 sub-id outside the AC3/DTS/LPCM ranges the // skip is 1 (just the sub-id byte). All remaining bytes are ES. let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00, // 0x70, // sub-id outside known ranges → skip 1 0x55, 0x66, ]; data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1); assert_eq!(p[0].sub_stream_id, Some(0x70)); assert_eq!(p[0].data, vec![0x55, 0x66], "only sub-id byte skipped"); } #[test] fn private_stream_1_short_payload_does_not_underflow_skip() { // If the sub-header skip exceeds the payload length, `skip.min(len)` // clamps so ES is empty rather than panicking on an out-of-range // slice. AC3 skip is 4 but only 2 payload bytes present. let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, 0x00, 0x04, 0x80, 0x00, 0x00, // 0x80, // AC3 sub-id, skip=4 0x01, // only 1 byte after sub-id (total payload 2 < skip 4) ]; data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1); assert_eq!(p[0].sub_stream_id, Some(0x80)); assert!( p[0].data.is_empty(), "clamped skip yields empty ES, no panic" ); } // ── dvd_audio_pid / dvd_subtitle_pid range boundaries ───────────────── #[test] fn dvd_audio_pid_range_boundaries() { // AC3/DTS audio sub-ids 0x80..=0x8F and LPCM 0xA0..=0xA7 map to // 0xBD00|sub. Just-outside values must return None. assert_eq!(dvd_audio_pid(0x80), Some(0xBD80)); assert_eq!(dvd_audio_pid(0x8F), Some(0xBD8F)); assert_eq!(dvd_audio_pid(0xA0), Some(0xBDA0)); assert_eq!(dvd_audio_pid(0xA7), Some(0xBDA7)); // Boundaries just outside the ranges. assert_eq!(dvd_audio_pid(0x7F), None); assert_eq!(dvd_audio_pid(0x90), None); assert_eq!(dvd_audio_pid(0x9F), None); assert_eq!(dvd_audio_pid(0xA8), None); } #[test] fn hddvd_ddplus_substream_maps_to_bd_pid() { // HD-DVD Dolby Digital Plus sub-ids 0xC0..=0xC7 map to 0xBD00|sub, // distinct per track and disjoint from the DVD audio space. A DVD never // emits these, so the range is purely additive. assert_eq!(dvd_audio_pid(0xC0), Some(0xBDC0)); assert_eq!(dvd_audio_pid(0xC3), Some(0xBDC3)); assert_eq!(dvd_audio_pid(0xC7), Some(0xBDC7)); // Just outside the range. assert_eq!(dvd_audio_pid(0xBF), None); assert_eq!(dvd_audio_pid(0xC8), None); // Four DD+ tracks (ANCHORMAN) get four distinct PIDs. let pids: Vec = (0xC0u8..=0xC3).map(|s| dvd_audio_pid(s).unwrap()).collect(); assert_eq!(pids, vec![0xBDC0, 0xBDC1, 0xBDC2, 0xBDC3]); // And route through dvd_pid on a private_stream_1 packet. assert_eq!(mk(0xBD, Some(0xC0)).dvd_pid(), Some(0xBDC0)); assert_eq!(mk(0xBD, Some(0xC3)).dvd_pid(), Some(0xBDC3)); } #[test] fn hddvd_ddplus_pes_strips_4byte_subheader_to_syncword() { // A private_stream_1 PES carrying DD+ (sub-id 0xC0) has a 4-byte // sub-header (sub_id + num_frames(1) + access_unit_ptr(2)); the demuxer // must strip exactly those 4 bytes so es_data begins at the E-AC-3 // payload — here the 0x0B77 syncword sits right after the sub-header. let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xBD, // private stream 1 0x00, 0x0B, // PES_packet_length = 11 (flags2 + hdl1 + 8 payload) 0x80, 0x00, 0x00, // no PTS, header_data_len = 0 0xC0, // sub-stream id: DD+ track 0 0x01, 0x00, 0x00, // num_frames(1) + access_unit_ptr(2) 0x0B, 0x77, 0xDE, 0xAD, // E-AC-3 syncword + payload ]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); let p = demuxer.feed(&data); assert_eq!(p.len(), 1); assert_eq!(p[0].sub_stream_id, Some(0xC0)); assert_eq!( p[0].data, vec![0x0B, 0x77, 0xDE, 0xAD], "4-byte DD+ sub-header stripped; es_data starts at the syncword" ); } #[test] fn dvd_subtitle_pid_range_boundaries() { // VobSub subtitle sub-ids 0x20..=0x3F map to the identity PID. assert_eq!(dvd_subtitle_pid(0x20), Some(0x20)); assert_eq!(dvd_subtitle_pid(0x3F), Some(0x3F)); assert_eq!(dvd_subtitle_pid(0x1F), None); assert_eq!(dvd_subtitle_pid(0x40), None); } #[test] fn dvd_pid_all_video_stream_ids_map_to_video() { // ISO 13818-1: 0xE0..=0xEF are all video streams. DVD collapses // them onto the single canonical video PID. for sid in 0xE0u8..=0xEF { assert_eq!( mk(sid, None).dvd_pid(), Some(DVD_VIDEO_PID), "stream_id {sid:#04x} must map to video" ); } } // ── flushing semantics ──────────────────────────────────────────────── #[test] fn flush_discards_incomplete_bounded_pes() { // A bounded PES short of its declared length is genuinely incomplete // and must be DROPPED at flush — not emitted with a truncated payload. let mut demuxer = PsDemuxer::new(); // length=10 but only 2 payload bytes supplied. let head = vec![ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x00, 0x00, 0xAA, 0xBB, ]; assert!(demuxer.feed(&head).is_empty()); let flushed = demuxer.flush(); assert!( flushed.is_empty(), "incomplete bounded PES must not be emitted on flush" ); } #[test] fn pes_header_data_length_skips_pts_when_flag_unset() { // If pts_dts_flags == 0 the 5 "PTS" bytes after the fixed header are // ES, not a timestamp. A PES with header_data_length=0 and no PTS // flag must surface no PTS and keep all payload bytes. let mut demuxer = PsDemuxer::new(); let mut data = vec![ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x06, 0x80, 0x00, 0x00, 0x21, 0x00, 0x01, ]; // 0x21 0x00 0x01 look like the start of a PTS field but must NOT be // parsed as one (flags2 = 0x00 ⇒ no PTS). data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 1); assert_eq!(p[0].pts, None); assert_eq!(p[0].data, vec![0x21, 0x00, 0x01]); } #[test] fn unbounded_video_pes_framed_by_next_pes_not_embedded_audio_code() { // An unbounded (length 0) video PES must be delimited by the next // PS-layer unit. A following AUDIO PES (0xC0) is a valid boundary, // so the video ES must include its embedded 00 00 01 00 picture // code but stop at the audio PES start. let mut demuxer = PsDemuxer::new(); let mut data = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00]; let video_payload = [0x11, 0x00, 0x00, 0x01, 0x00, 0x22]; // embedded picture SC data.extend_from_slice(&video_payload); // Next PS-layer unit: an audio PES (bounded). data.extend_from_slice(&[ 0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x99, 0x88, ]); data.extend_from_slice(&PROGRAM_END); let p = demuxer.feed(&data); assert_eq!(p.len(), 2, "video PES + audio PES"); assert_eq!(p[0].stream_id, 0xE0); assert_eq!( p[0].data, video_payload, "video ES keeps its embedded start code, stops at the audio PES" ); 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, 0), "an undecided trailing start code is not proved boundary-free" ); } /// An unbounded (length-0) PES is terminated by the next PS-LAYER unit, and /// until one arrives the payload accumulates in the buffer. The search for /// that unit must not restart at the PES header on every feed: the buffer /// only stops growing at `MAX_PS_BUFFER` (4 MiB), and a feed is one read /// batch (at most 510 sectors ≈ 1 MiB, 60 sectors ≈ 120 KiB on an optical /// drive), so re-scanning from byte 0 costs work quadratic in the bytes /// fed — up to 4 MiB of scanning per call, for as long as a corrupt or /// crafted VOB withholds the boundary. A conformant DVD ends every pack /// within 2048 bytes and never reaches this state. /// /// Measured directly, because a work bound has no packet-level shadow: /// `boundary_bytes_scanned` counts the bytes `find_ps_boundary` examines. /// 256 chunks x 4 KiB of boundary-free payload is 1 MiB of input; /// re-scanning from the header on every call examines /// 4 KiB * 256*257/2 = ~128 MiB. /// /// Mutation: drop the `Some((pes_at, searched_to)) if pes_at == sc` arm so /// `from` is always `sc + 4`. #[test] fn an_unterminated_pes_is_not_rescanned_from_its_header_every_feed() { const CHUNKS: usize = 256; const CHUNK: usize = 4096; let mut demuxer = PsDemuxer::new(); // Unbounded PES header (length 0), then payload that carries no start // code at all, so no PS-layer boundary is ever found. demuxer.feed(&[0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00]); for _ in 0..CHUNKS { assert!( demuxer.feed(&[0xFFu8; CHUNK]).is_empty(), "no boundary yet, so no PES can be emitted" ); } let fed = (CHUNKS * CHUNK) as u64; assert!( demuxer.boundary_bytes_scanned <= 2 * fed, "boundary search examined {} bytes over {fed} bytes of payload — \ the scan must advance with the buffer, not restart at the PES header", demuxer.boundary_bytes_scanned ); // ...and the cursor must not have cost correctness: the PES still ends // at the pack header that finally arrives, with its whole payload. let pack = [ 0x00, 0x00, 0x01, PACK_HEADER_ID, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x00, 0x00, 0x03, 0xF8, ]; let packets = demuxer.feed(&pack); assert_eq!(packets.len(), 1, "the pack header terminates the PES"); assert_eq!( packets[0].data.len(), CHUNKS * CHUNK, "the whole accumulated payload belongs to the PES" ); } /// The resume cursor is a BUFFER offset, so it must be rebased when the /// buffer drains — and this is the only test in which a drain actually /// happens while a cursor is live. /// /// Mutations this catches, both halves of /// `self.pending_scan.map(|(pes_at, searched_to)| (pes_at - pos, searched_to - pos))` /// in `extract_packets`: /// * `searched_to - pos` -> `searched_to`: the next call resumes `pos` /// bytes PAST where the previous scan actually stopped, so that window /// is never examined. Here the terminating pack header lands inside it /// and is missed outright — the unbounded PES runs on past its real /// end, swallowing the following unit. That is a CORRECTNESS failure, /// not a slow path, and the assertion on the emitted packet catches it. /// * `pes_at - pos` -> `pes_at`: the stale offset no longer equals the /// PES's post-drain `sc`, the resume arm stops matching and the search /// restarts at the PES header. Caught by `boundary_bytes_scanned`, /// which is why the fixture puts 64 KiB of payload in the SAME chunk /// that opens the PES: that is exactly the span a restart re-examines, /// so the mutant roughly doubles the bytes scanned. /// /// `an_unterminated_pes_is_not_rescanned_from_its_header_every_feed` cannot /// reach either: it opens the unbounded PES as the very FIRST bytes of the /// very first feed, so nothing ever drains ahead of it, `pos` stays 0 and /// the subtraction is a no-op. The comment above it asserts neither /// component can underflow; nothing exercised the arithmetic at all. /// /// So this fixture puts COMPLETE PS units — a pack header and a /// length-bounded PES — ahead of the unbounded video PES *in the same /// chunk*. The loop consumes them, breaks on the unbounded PES, and drains /// `pos` bytes with `pending_scan` live: exactly the real DVD shape, where /// a video PES opens partway through a read batch. #[test] fn a_resume_cursor_survives_the_drain_of_units_ahead_of_the_unbounded_pes() { // Payload fed in the SAME chunk that opens the PES. Large enough that // re-scanning it is unmistakable in `boundary_bytes_scanned`, and it is // the exact span the un-rebased `pes_at` mutant re-examines. const PAYLOAD: usize = 64 * 1024; // A 14-byte MPEG-2 pack header with pack_stuffing_length 0. const PACK: [u8; 14] = [ 0x00, 0x00, 0x01, PACK_HEADER_ID, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x00, 0x00, 0x03, 0xF8, ]; // A length-BOUNDED PES — a complete unit, so the loop consumes it and // `pos` advances past it before breaking on the unbounded PES. const BOUNDED_PES: [u8; 11] = [ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0xAA, 0xBB, ]; // The unbounded (length-0) video PES whose scan must be resumed. const OPEN_PES: [u8; 9] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00]; // Bytes drained ahead of the unbounded PES on the first feed — the // `pos` the cursor must be rebased by. const DRAINED: usize = PACK.len() + BOUNDED_PES.len(); let mut demuxer = PsDemuxer::new(); let mut first = PACK.to_vec(); first.extend_from_slice(&BOUNDED_PES); first.extend_from_slice(&OPEN_PES); first.extend_from_slice(&[0xFFu8; PAYLOAD]); let head = demuxer.feed(&first); assert_eq!( head.len(), 1, "the bounded PES ahead of the open one is emitted immediately, \ which is what makes the buffer drain with a cursor live" ); // The terminating pack arrives at the head of the next feed — i.e. // within `DRAINED` bytes of where the previous scan stopped, which is // precisely the window an un-rebased `searched_to` skips over. assert!( PACK.len() <= DRAINED, "the terminating pack must fit inside the window a stale \ `searched_to` would skip, or the mutant survives" ); let packets = demuxer.feed(&PACK); assert_eq!( packets.len(), 1, "the pack header terminates the open PES; a scan resumed past it \ never sees it and the PES runs on" ); assert_eq!( packets[0].data.len(), PAYLOAD, "exactly the payload fed belongs to the PES" ); // Work bound: the payload is proved boundary-free ONCE. Re-scanning it // after the drain roughly doubles this. assert!( demuxer.boundary_bytes_scanned <= (PAYLOAD + 1024) as u64, "boundary search examined {} bytes over {PAYLOAD} bytes of payload — \ a cursor left un-rebased across the drain never matches the PES's \ new offset, so the scan restarts at the header", demuxer.boundary_bytes_scanned ); } /// 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), 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); } /// A length-bounded PES (`pes_packet_len != 0`) must be emitted the /// moment its declared length is EXACTLY satisfied by the buffer /// (`sc + 6 > len`, then `e = sc + 6 + pes_packet_len; e > len`), not /// held back waiting for a byte that will never arrive. Feed nothing /// after the packet and don't flush — if the boundary checks were /// `>=` instead of `>`, an exact fit would incorrectly be treated as /// "not enough data yet" and the packet would never be produced. #[test] fn length_bounded_pes_exact_fit_is_emitted_not_awaited() { let mut demuxer = PsDemuxer::new(); let payload = [0x11u8, 0x22, 0x33, 0x44, 0x55]; let mut data = vec![0x00, 0x00, 0x01, 0xC0]; // audio stream id let pes_packet_len = (3 + payload.len()) as u16; // flags+header_len byte + payload data.extend_from_slice(&pes_packet_len.to_be_bytes()); data.extend_from_slice(&[0x80, 0x00, 0x00]); // no PTS/DTS, header_data_len = 0 data.extend_from_slice(&payload); assert_eq!(data.len(), 6 + pes_packet_len as usize, "sanity: exact fit"); let packets = demuxer.feed(&data); assert_eq!( packets.len(), 1, "an exact-fit length-bounded PES must be emitted immediately, \ not held awaiting a byte that will never come" ); assert_eq!(packets[0].data, payload); assert!( demuxer.buffer.is_empty(), "the exact-fit PES must be fully consumed, leaving nothing buffered" ); } }