Skip to main content

mpeg_ps/
program_stream.rs

1//! Program Stream walker — ISO/IEC 13818-1 §2.5.3.1–2.5.3.3 (Tables 2-37, 2-38).
2//!
3//! Iterates through a Program Stream, yielding each [`Pack`], which itself
4//! carries a [`PackHeader`], an optional
5//! [`SystemHeader`], and parsed PES packets (via `mpeg-pes`).
6//!
7//! The stream terminates with the `MPEG_program_end_code` `0x000001B9`.
8
9use alloc::vec::Vec;
10
11use broadcast_common::{Parse, Serialize};
12
13use crate::Result;
14use crate::error::Error;
15use crate::pack_header::{PACK_START_CODE, PackHeader};
16use crate::program_stream_map::{MAP_STREAM_ID, ProgramStreamMap};
17use crate::system_header::{
18    PREFIX_LEN as SYSTEM_HEADER_PREFIX_LEN, SYSTEM_HEADER_START_CODE, SystemHeader,
19};
20
21/// `MPEG_program_end_code` — `0x000001B9`.
22const PROGRAM_END_CODE: u32 = 0x0000_01B9;
23
24/// A single pack within a Program Stream: a `pack_header()`, optionally a
25/// `system_header()`, an optional Program Stream Map, and zero or more PES
26/// packets.
27#[derive(Debug, Clone)]
28pub struct Pack<'a> {
29    /// The pack header (SCR, program_mux_rate, stuffing).
30    pub pack_header: PackHeader<'a>,
31    /// The optional system header (only in the first pack of a compliant stream).
32    pub system_header: Option<SystemHeader>,
33    /// The Program Stream Map (`stream_id 0xBC`), if one appears in this
34    /// pack — `stream_type → elementary_stream_id` mapping (Table 2-41). If
35    /// more than one PSM packet appears (a version-change re-announcement),
36    /// this holds the last one.
37    ///
38    /// W5 (#1119): PSM packets used to be handed to `mpeg_pes::PesPacket::parse`
39    /// like any other PES and returned as an opaque, un-mapped PES packet —
40    /// so a consumer had no way to learn `stream_type` per elementary
41    /// stream and had to guess the codec from `stream_id` alone.
42    pub psm: Option<ProgramStreamMap<'a>>,
43    /// Parsed PES packets within this pack (PSM packets are not included
44    /// here — see [`psm`](Self::psm)).
45    pub pes_packets: Vec<mpeg_pes::PesPacket<'a>>,
46}
47
48/// Parses a single pack from the start of `b`.
49///
50/// Returns `Ok((Some(pack), consumed_bytes))` on success,
51/// or `Ok((None, 4))` when `MPEG_program_end_code` `0x000001B9` is reached.
52pub fn parse_pack(b: &[u8]) -> Result<(Option<Pack<'_>>, usize)> {
53    if b.len() < 4 {
54        return Err(Error::BufferTooShort {
55            need: 4,
56            have: b.len(),
57            what: "pack start_code or end_code",
58        });
59    }
60
61    let start = u32::from_be_bytes([b[0], b[1], b[2], b[3]]);
62    if start == PROGRAM_END_CODE {
63        return Ok((None, 4));
64    }
65
66    // Parse pack header
67    let pack_header = PackHeader::parse(b)?;
68    let hdr_len = pack_header.header_len();
69    let rest = &b[hdr_len..];
70
71    // Check for optional system header (before any PES)
72    let (system_header, pes_start) = if rest.len() >= 4 {
73        let maybe_sh = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]);
74        if maybe_sh == SYSTEM_HEADER_START_CODE {
75            let sh = SystemHeader::parse(rest)?;
76            // W2 (#1119): PES data starts after the *wire* `header_length`
77            // (Table 2-40), not after however many bytes the re-serialized
78            // stream-bound loop happens to occupy. A conformant
79            // `header_length` may declare trailing bytes past the loop
80            // (the loop ends at the first byte whose MSB is 0, which the
81            // encoder is free to pad with reserved bytes); starting PES
82            // parsing early there feeds PES data into the system-header
83            // parser's leftover bytes instead.
84            let header_length = u16::from_be_bytes([rest[4], rest[5]]) as usize;
85            (Some(sh), SYSTEM_HEADER_PREFIX_LEN + header_length)
86        } else {
87            (None, 0)
88        }
89    } else {
90        (None, 0)
91    };
92
93    let pes_data = &rest[pes_start..];
94    let (pes_packets, psm, pes_consumed) = parse_pes_loop(pes_data)?;
95
96    let consumed = hdr_len + pes_start + pes_consumed;
97    Ok((
98        Some(Pack {
99            pack_header,
100            system_header,
101            psm,
102            pes_packets,
103        }),
104        consumed,
105    ))
106}
107
108/// Parse PES packets from `data` until a `pack_start_code`/`program_end_code`
109/// or non-PES-start bytes are found — always checked exactly at the boundary
110/// right after a previously-consumed *whole* PES packet, never scanned for
111/// mid-payload.
112///
113/// W3 (#1119): the caller used to pre-scan the whole buffer byte-by-byte for
114/// the next `000001BA`/`000001B9`, including inside PES payloads. Several
115/// private/audio stream types (AC-3, LPCM, DVD subpictures carried as
116/// `private_stream_1`) are not start-code-emulation-free, so a chance
117/// `00 00 01 BA` inside real payload bytes truncated the PES loop early,
118/// leaving the last real PES packet's tail unparsed and the whole pack
119/// (and, via `parse_all_packs`, the whole stream) rejected with a spurious
120/// `BufferTooShort`/`Err`. Checking for the boundary only right after a
121/// fully-parsed PES packet (by its own declared `PES_packet_length`) cannot
122/// see mid-payload emulation at all.
123fn parse_pes_loop(
124    data: &[u8],
125) -> Result<(
126    Vec<mpeg_pes::PesPacket<'_>>,
127    Option<ProgramStreamMap<'_>>,
128    usize,
129)> {
130    let mut packets = Vec::new();
131    let mut psm = None;
132    let mut pos = 0;
133
134    loop {
135        if pos + 4 <= data.len() {
136            let word = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
137            if word == PACK_START_CODE || word == PROGRAM_END_CODE {
138                break;
139            }
140        }
141        if !(pos + 6 <= data.len() && data[pos..pos + 3] == crate::PACKET_START_CODE_PREFIX) {
142            break;
143        }
144        // W5 (#1119): a Program Stream Map (`stream_id 0xBC`) is a distinct
145        // self-delimiting structure (Table 2-41, with its own CRC-32
146        // trailer, not a `PES_packet_length`) — route it to
147        // `ProgramStreamMap::parse` instead of `mpeg_pes::PesPacket::parse`,
148        // which had no way to make sense of its layout and returned it as
149        // an opaque, un-mapped PES packet.
150        if data[pos + 3] == MAP_STREAM_ID {
151            let map = ProgramStreamMap::parse(&data[pos..])?;
152            pos += map.serialized_len();
153            psm = Some(map);
154            continue;
155        }
156        match mpeg_pes::PesPacket::parse(&data[pos..]) {
157            Ok(pkt) => {
158                let pkt_len = pkt.serialized_len();
159                packets.push(pkt);
160                pos += pkt_len;
161            }
162            Err(e) => return Err(Error::Pes(e)),
163        }
164    }
165
166    Ok((packets, psm, pos))
167}
168
169/// Iterate over all packs in a Program Stream buffer.
170///
171/// Returns all packs and the remaining trailing bytes (if any).
172pub fn parse_all_packs(b: &[u8]) -> Result<(Vec<Pack<'_>>, &[u8])> {
173    let mut packs = Vec::new();
174    let mut remaining = b;
175    while remaining.len() >= 4 {
176        let (pack_opt, consumed) = parse_pack(remaining)?;
177        match pack_opt {
178            Some(pack) => {
179                remaining = &remaining[consumed..];
180                packs.push(pack);
181            }
182            None => {
183                // End code consumed 4 bytes; finish
184                remaining = &remaining[4..];
185                break;
186            }
187        }
188    }
189    Ok((packs, remaining))
190}
191
192/// A span of the input [`scan_packs`] skipped because the pack starting
193/// there did not parse.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct SkippedRegion {
196    /// Byte offset (from the start of the scanned buffer) where the failed
197    /// pack began.
198    pub offset: usize,
199    /// Bytes skipped: up to the next `pack_start_code` found after
200    /// `offset`, or to the end of the buffer if none follows.
201    pub len: usize,
202    /// Why the pack at `offset` failed to parse.
203    pub error: Error,
204}
205
206/// The outcome of [`scan_packs`].
207#[derive(Debug, Clone)]
208pub struct PackScan<'a> {
209    /// Every pack that parsed, in stream order.
210    pub packs: Vec<Pack<'a>>,
211    /// Every region skipped after a parse failure, in stream order.
212    pub skipped: Vec<SkippedRegion>,
213    /// Bytes after the last pack / `MPEG_program_end_code` (fewer than a
214    /// start code, or whatever followed the end code).
215    pub remaining: &'a [u8],
216}
217
218/// Walk a Program Stream like [`parse_all_packs`], but **resynchronise** after
219/// a pack that fails to parse instead of abandoning the whole stream.
220///
221/// On a parse failure the failed span is recorded in
222/// [`PackScan::skipped`] (with the error, never swallowed) and scanning
223/// resumes at the next `pack_start_code` (`0x000001BA`) after the failed
224/// pack's first byte. That byte search is deliberately confined to this
225/// recovery path: a well-formed stream is still walked purely by
226/// `PES_packet_length` (see [`parse_pack`]), so start-code emulation inside a
227/// payload cannot truncate a good pack — only a stream that is *already*
228/// corrupt can resync onto an emulated code (audit r14-MPS-W3, #1119).
229///
230/// ```
231/// // A garbage prefix, then a valid empty pack (pack header only).
232/// let mut data = vec![0x00, 0x00, 0x01, 0xBA, 0xFF, 0xFF, 0xFF, 0xFF];
233/// data.extend_from_slice(&[
234///     0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01,
235///     0x40, 0x00, 0x03, 0x00,
236/// ]);
237/// let scan = mpeg_ps::program_stream::scan_packs(&data);
238/// assert_eq!(scan.skipped.len(), 1);
239/// assert_eq!(scan.skipped[0].offset, 0);
240/// assert_eq!(scan.packs.len(), 1);
241/// ```
242pub fn scan_packs(b: &[u8]) -> PackScan<'_> {
243    let mut packs = Vec::new();
244    let mut skipped = Vec::new();
245    let mut pos = 0usize;
246    while b.len() - pos >= 4 {
247        match parse_pack(&b[pos..]) {
248            Ok((Some(pack), consumed)) => {
249                packs.push(pack);
250                pos += consumed;
251            }
252            Ok((None, consumed)) => {
253                pos += consumed;
254                break;
255            }
256            Err(error) => {
257                let next = find_pack_start(&b[pos + 1..]).map(|i| pos + 1 + i);
258                let end = next.unwrap_or(b.len());
259                skipped.push(SkippedRegion {
260                    offset: pos,
261                    len: end - pos,
262                    error,
263                });
264                pos = end;
265            }
266        }
267    }
268    PackScan {
269        packs,
270        skipped,
271        remaining: &b[pos..],
272    }
273}
274
275/// Offset of the first `pack_start_code` in `data`, if any.
276fn find_pack_start(data: &[u8]) -> Option<usize> {
277    let code = PACK_START_CODE.to_be_bytes();
278    data.windows(code.len()).position(|w| w == code)
279}
280
281#[cfg(test)]
282mod tests {
283    use super::parse_pack;
284    use crate::program_stream_map::{EsMapEntry, ProgramStreamMap};
285    use alloc::{vec, vec::Vec};
286    use broadcast_common::Serialize;
287
288    /// W5 (#1119): a PSM (`stream_id 0xBC`) within a pack must be surfaced
289    /// via `Pack::psm`, typed (`stream_type` per elementary stream), not
290    /// folded into `pes_packets` as an opaque, un-mapped PES packet.
291    #[test]
292    fn psm_is_surfaced_not_folded_into_pes_packets() {
293        let psm = ProgramStreamMap {
294            current_next_indicator: true,
295            single_extension_stream_flag: false,
296            version: 1,
297            program_stream_info: &[],
298            elementary_stream_map: vec![EsMapEntry {
299                stream_type: 0x02, // MPEG-2 video
300                elementary_stream_id: 0xE0,
301                stream_id_extension: None,
302                descriptors: &[],
303            }],
304            crc: 0,
305        };
306        let mut psm_bytes = vec![0u8; psm.serialized_len()];
307        psm.serialize_into(&mut psm_bytes).unwrap();
308
309        let mut b: Vec<u8> = vec![
310            0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
311        ];
312        b.extend_from_slice(&psm_bytes);
313        // One PES packet after the PSM.
314        b.extend_from_slice(&[
315            0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x80, 0x05, 0x21, 0x00, 0x01, 0x00, 0x01,
316            0xAA, 0xBB,
317        ]);
318
319        let (pack_opt, consumed) = parse_pack(&b).unwrap();
320        let pack = pack_opt.expect("not an end code");
321        let parsed_psm = pack.psm.expect("PSM must be surfaced on Pack::psm");
322        assert_eq!(parsed_psm.elementary_stream_map.len(), 1);
323        assert_eq!(parsed_psm.elementary_stream_map[0].stream_type, 0x02);
324        assert_eq!(
325            pack.pes_packets.len(),
326            1,
327            "the PSM must not appear (again, mis-typed) in pes_packets"
328        );
329        assert_eq!(consumed, b.len());
330    }
331
332    /// W2 (#1119): PES data must start after the system header's *wire*
333    /// `header_length` (Table 2-40), not after its re-serialized length.
334    /// A `header_length` of 8 (6-byte fixed body + 2 reserved padding bytes,
335    /// both MSB=0 so the `while (nextbits()=='1')` stream-bound loop parses
336    /// 0 entries) is 2 bytes longer than the 6-byte fixed body the loop
337    /// itself accounts for — the re-serialized length was used instead,
338    /// starting PES parsing 2 bytes early, inside the declared header.
339    #[test]
340    fn system_header_wire_length_used_for_pes_start_not_reserialized_length() {
341        // Pack header, byte-identical to `pack_header::pack_header_round_trip_fixture_pattern`
342        // (14 bytes, stuffing_length=0).
343        let mut b: Vec<u8> = vec![
344            0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
345        ];
346        // System header: header_length = 8 (2 bytes more than the 6-byte
347        // fixed body its own loop consumes).
348        b.extend_from_slice(&[
349            0x00, 0x00, 0x01, 0xBB, // system_header_start_code
350            0x00, 0x08, // header_length = 8
351            0x80, 0x00, 0x01, 0x00, 0x20, 0x00, // 6-byte fixed body, 0 stream bounds
352            0x00, 0x00, // 2 reserved padding bytes (MSB=0)
353        ]);
354        // A minimal PES packet (stream_id 0xE0, PES_packet_length=0x0A, PTS-only, 2 payload bytes).
355        b.extend_from_slice(&[
356            0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x80, 0x05, 0x21, 0x00, 0x01, 0x00, 0x01,
357            0xAA, 0xBB,
358        ]);
359
360        let (pack_opt, consumed) = parse_pack(&b).unwrap();
361        let pack = pack_opt.expect("not an end code");
362        assert!(pack.system_header.is_some());
363        assert_eq!(
364            pack.pes_packets.len(),
365            1,
366            "PES must be found starting after the full declared header_length"
367        );
368        assert_eq!(consumed, b.len());
369    }
370
371    /// W3 (#1119): a stray `000001BA` inside a PES packet's own (declared,
372    /// bounded) payload must not truncate the pack — `pack_start_code`/
373    /// `program_end_code` are only ever checked right after a fully-parsed
374    /// PES packet, never scanned for mid-payload.
375    #[test]
376    fn stray_pack_start_code_inside_pes_payload_does_not_truncate() {
377        let mut b: Vec<u8> = vec![
378            0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
379        ];
380        // One PES packet, PES_packet_length=0x0E (14): flags(2)+hdl(1)+PTS(5)+
381        // payload(6), where the payload embeds a stray `00 00 01 BA` at its
382        // midpoint — plausible in start-code-emulation-bearing streams (AC-3,
383        // LPCM, DVD subpicture `private_stream_1`).
384        b.extend_from_slice(&[
385            0x00, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x80, 0x80, 0x05, 0x21, 0x00, 0x01, 0x00, 0x01,
386            0xAA, 0x00, 0x00, 0x01, 0xBA, 0xBB,
387        ]);
388
389        let (pack_opt, consumed) = parse_pack(&b).unwrap();
390        let pack = pack_opt.expect("not an end code");
391        assert_eq!(
392            pack.pes_packets.len(),
393            1,
394            "the one PES packet must parse in full, not truncate at the embedded 000001BA"
395        );
396        assert_eq!(consumed, b.len(), "the whole PES packet must be consumed");
397    }
398
399    /// Regression: a system_header whose serialized length (`pes_start`) is
400    /// greater than the offset of the next boundary found in `rest` caused
401    /// `b - pes_start` to subtract with overflow (panic) on the unsigned
402    /// `pes_end` computation.  Fixed by using `saturating_sub`.
403    ///
404    /// Verbatim cargo-fuzz minimized artifact. A `system_header` declares
405    /// `header_length` = 255, so its re-serialized length (`pes_start` = 261)
406    /// covers a large parsed stream loop; meanwhile `find_next_boundary`
407    /// returns a `pack_start_code` (0x000001BA) embedded at rest-offset 142,
408    /// i.e. *before* `pes_start`. The old `b - pes_start` underflowed (panic on
409    /// unsigned subtraction). Fixed with `saturating_sub`. A truncated input
410    /// does NOT reproduce — `SystemHeader::parse` rejects it with
411    /// `HeaderLengthOverflow` before the buggy line, so the full body is needed.
412    #[test]
413    fn fuzz_regression_mpeg_ps_boundary_underflow() {
414        let crashing: &[u8] = &[
415            0x00, 0x00, 0x01, 0xba, 0x5c, 0xf5, 0xf5, 0xc0, 0xff, 0xff, 0x21, 0xf3, 0xf3, 0xf3,
416            0x90, 0xf3, 0xbb, 0x00, 0x00, 0x01, 0xbb, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
417            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
418            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
419            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
420            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
421            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
422            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
423            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
424            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
425            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
426            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xba, 0x5c,
427            0xf5, 0xf5, 0xc0, 0xff, 0xff, 0xf3, 0xf3, 0xf3, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
428            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
429            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
430            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
431            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
432            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
433            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
434            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
435            0x00, 0x00,
436        ];
437        // Must not panic — result is either Ok or Err.
438        let _ = parse_pack(crashing);
439    }
440}