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::pack_header::{PACK_START_CODE, PackHeader};
15use crate::system_header::{SYSTEM_HEADER_START_CODE, SystemHeader};
16
17/// `MPEG_program_end_code` — `0x000001B9`.
18const PROGRAM_END_CODE: u32 = 0x0000_01B9;
19
20/// A single pack within a Program Stream: a `pack_header()`, optionally a
21/// `system_header()`, followed by zero or more PES packets.
22#[derive(Debug, Clone)]
23pub struct Pack<'a> {
24    /// The pack header (SCR, program_mux_rate, stuffing).
25    pub pack_header: PackHeader<'a>,
26    /// The optional system header (only in the first pack of a compliant stream).
27    pub system_header: Option<SystemHeader>,
28    /// Parsed PES packets within this pack.
29    pub pes_packets: Vec<mpeg_pes::PesPacket<'a>>,
30}
31
32/// Scans forward for the next pack_start_code or program_end_code boundary.
33fn find_next_boundary(b: &[u8], from: usize) -> Option<usize> {
34    let mut i = from;
35    while i + 4 <= b.len() {
36        let word = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
37        if word == PACK_START_CODE || word == PROGRAM_END_CODE {
38            return Some(i);
39        }
40        i += 1;
41    }
42    None
43}
44
45/// Parses a single pack from the start of `b`.
46///
47/// Returns `Ok((Some(pack), consumed_bytes))` on success,
48/// or `Ok((None, 4))` when `MPEG_program_end_code` `0x000001B9` is reached.
49pub fn parse_pack(b: &[u8]) -> Result<(Option<Pack<'_>>, usize)> {
50    use crate::error::Error;
51
52    if b.len() < 4 {
53        return Err(Error::BufferTooShort {
54            need: 4,
55            have: b.len(),
56            what: "pack start_code or end_code",
57        });
58    }
59
60    let start = u32::from_be_bytes([b[0], b[1], b[2], b[3]]);
61    if start == PROGRAM_END_CODE {
62        return Ok((None, 4));
63    }
64
65    // Parse pack header
66    let pack_header = PackHeader::parse(b)?;
67    let hdr_len = pack_header.header_len();
68    let rest = &b[hdr_len..];
69
70    // Find the next pack boundary or end_code to limit PES parsing
71    let boundary = find_next_boundary(rest, 0);
72
73    // Check for optional system header (before any PES)
74    let (system_header, pes_start, _sh_len) = if rest.len() >= 4 {
75        let maybe_sh = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]);
76        if maybe_sh == SYSTEM_HEADER_START_CODE {
77            let sh = SystemHeader::parse(rest)?;
78            let slen = sh.serialized_len();
79            (Some(sh), slen, slen)
80        } else {
81            (None, 0, 0)
82        }
83    } else {
84        (None, 0, 0)
85    };
86
87    let pes_data = &rest[pes_start..];
88    // `boundary` is an offset within `rest`; convert to an offset within `pes_data`.
89    // If the boundary falls at or before `pes_start` (e.g. a new pack_start_code that
90    // appears inside what we parsed as a system-header), clamp to 0 so we yield no PES
91    // data rather than underflowing.
92    let pes_end = boundary.map_or(pes_data.len(), |b| {
93        b.saturating_sub(pes_start).min(pes_data.len())
94    });
95
96    // Parse PES packets up to the boundary
97    let (pes_packets, _pes_consumed) = parse_pes_loop(&pes_data[..pes_end])?;
98
99    let consumed = hdr_len + pes_start + _pes_consumed;
100    Ok((
101        Some(Pack {
102            pack_header,
103            system_header,
104            pes_packets,
105        }),
106        consumed,
107    ))
108}
109
110fn parse_pes_loop(data: &[u8]) -> Result<(Vec<mpeg_pes::PesPacket<'_>>, usize)> {
111    use crate::error::Error;
112
113    let mut packets = Vec::new();
114    let mut pos = 0;
115
116    while pos + 6 <= data.len()
117        && data[pos] == 0x00
118        && data[pos + 1] == 0x00
119        && data[pos + 2] == 0x01
120    {
121        match mpeg_pes::PesPacket::parse(&data[pos..]) {
122            Ok(pkt) => {
123                let pkt_len = pkt.serialized_len();
124                packets.push(pkt);
125                pos += pkt_len;
126            }
127            Err(e) => return Err(Error::Pes(e)),
128        }
129    }
130
131    Ok((packets, pos))
132}
133
134/// Iterate over all packs in a Program Stream buffer.
135///
136/// Returns all packs and the remaining trailing bytes (if any).
137pub fn parse_all_packs(b: &[u8]) -> Result<(Vec<Pack<'_>>, &[u8])> {
138    let mut packs = Vec::new();
139    let mut remaining = b;
140    while remaining.len() >= 4 {
141        let (pack_opt, consumed) = parse_pack(remaining)?;
142        match pack_opt {
143            Some(pack) => {
144                remaining = &remaining[consumed..];
145                packs.push(pack);
146            }
147            None => {
148                // End code consumed 4 bytes; finish
149                remaining = &remaining[4..];
150                break;
151            }
152        }
153    }
154    Ok((packs, remaining))
155}
156
157#[cfg(test)]
158mod tests {
159    use super::parse_pack;
160
161    /// Regression: a system_header whose serialized length (`pes_start`) is
162    /// greater than the offset of the next boundary found in `rest` caused
163    /// `b - pes_start` to subtract with overflow (panic) on the unsigned
164    /// `pes_end` computation.  Fixed by using `saturating_sub`.
165    ///
166    /// Verbatim cargo-fuzz minimized artifact. A `system_header` declares
167    /// `header_length` = 255, so its re-serialized length (`pes_start` = 261)
168    /// covers a large parsed stream loop; meanwhile `find_next_boundary`
169    /// returns a `pack_start_code` (0x000001BA) embedded at rest-offset 142,
170    /// i.e. *before* `pes_start`. The old `b - pes_start` underflowed (panic on
171    /// unsigned subtraction). Fixed with `saturating_sub`. A truncated input
172    /// does NOT reproduce — `SystemHeader::parse` rejects it with
173    /// `HeaderLengthOverflow` before the buggy line, so the full body is needed.
174    #[test]
175    fn fuzz_regression_mpeg_ps_boundary_underflow() {
176        let crashing: &[u8] = &[
177            0x00, 0x00, 0x01, 0xba, 0x5c, 0xf5, 0xf5, 0xc0, 0xff, 0xff, 0x21, 0xf3, 0xf3, 0xf3,
178            0x90, 0xf3, 0xbb, 0x00, 0x00, 0x01, 0xbb, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
179            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
180            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
181            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
182            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
183            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
184            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
185            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
186            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
187            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
188            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xba, 0x5c,
189            0xf5, 0xf5, 0xc0, 0xff, 0xff, 0xf3, 0xf3, 0xf3, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
190            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
191            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
192            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
193            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
194            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
195            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
196            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
197            0x00, 0x00,
198        ];
199        // Must not panic — result is either Ok or Err.
200        let _ = parse_pack(crashing);
201    }
202}