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