Files
libfreemkv/src/mux/ps.rs
T
Matthew Jackson 7abcfaab72 Stop the HD-DVD VTI read and the extract progress channel from hiding loss
Two audit fixes plus two consistency cleanups, all in the changed 1.6.5 surface:

- hddvd: the VTI clip-order read used `.ok()`, flattening an unreadable
  authored order (a scratched sector under the .vti — the name came from the
  directory, so it is never "absent") into "no order" with no diagnostic. The
  sibling clip-extent arms log every read failure with its code; this one now
  does too, then falls back to the per-clip heuristic exactly as an unauthored
  disc would. Behaviour is otherwise unchanged; the loud line is the point.

- extract: the progress sink hardcoded bytes_unreadable_total: 0 and counted
  every zero-filled hole as good, so a progress-only consumer saw a holed
  extraction climb to a clean 100%. Thread the running unreadable total through
  and report the real good/unreadable split. The authoritative ExtractResult was
  already truthful; only the live channel lied.

- labels: correct a stale comment that described testlog's old lock-based
  capture; it now installs one global subscriber and routes to a thread-local
  sink.

- ps: import SYSTEM_HEADER from consts instead of re-declaring 0xBB, matching
  its sibling stream-id constants and the file's single-source rule.
2026-08-19 09:10:26 -07:00

2098 lines
90 KiB
Rust

//! 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<u8>,
/// Presentation timestamp in 90kHz ticks.
pub pts: Option<u64>,
/// Decode timestamp in 90kHz ticks.
pub dts: Option<u64>,
/// Elementary stream payload data.
pub data: Vec<u8>,
/// 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<crate::pes::SourcePos>,
}
/// 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<u16> {
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<u16> {
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<u16> {
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<u8>,
/// 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<PsPacket> {
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<PsPacket> {
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<PsPacket> {
// 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<PsPacket> {
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>, 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<u8> {
let get = |p: usize| -> Option<u8> {
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<PsPacket> {
// 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<u64> {
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<u8>) -> 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<usize> {
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<usize> {
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<u8> = 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<u16> = (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"
);
}
}