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 remaining = &remaining[4..];
178 break;
179 }
180 }
181 }
182 Ok((packs, remaining))
183}
184
185/// A span of the input [`scan_packs`] skipped because the pack starting
186/// there did not parse.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct SkippedRegion {
189 /// Byte offset (from the start of the scanned buffer) where the failed
190 /// pack began.
191 pub offset: usize,
192 /// Bytes skipped: up to the next `pack_start_code` found after
193 /// `offset`, or to the end of the buffer if none follows.
194 pub len: usize,
195 /// Why the pack at `offset` failed to parse.
196 pub error: Error,
197}
198
199/// The outcome of [`scan_packs`].
200#[derive(Debug, Clone)]
201pub struct PackScan<'a> {
202 /// Every pack that parsed, in stream order.
203 pub packs: Vec<Pack<'a>>,
204 /// Every region skipped after a parse failure, in stream order.
205 pub skipped: Vec<SkippedRegion>,
206 /// Bytes after the last pack / `MPEG_program_end_code` (fewer than a
207 /// start code, or whatever followed the end code).
208 pub remaining: &'a [u8],
209}
210
211/// Walk a Program Stream like [`parse_all_packs`], but **resynchronise** after
212/// a pack that fails to parse instead of abandoning the whole stream.
213///
214/// On a parse failure the failed span is recorded in
215/// [`PackScan::skipped`] (with the error, never swallowed) and scanning
216/// resumes at the next `pack_start_code` (`0x000001BA`) after the failed
217/// pack's first byte. That byte search is deliberately confined to this
218/// recovery path: a well-formed stream is still walked purely by
219/// `PES_packet_length` (see [`parse_pack`]), so start-code emulation inside a
220/// payload cannot truncate a good pack — only a stream that is *already*
221/// corrupt can resync onto an emulated code (audit r14-MPS-W3, #1119).
222///
223/// ```
224/// // A garbage prefix, then a valid empty pack (pack header only).
225/// let mut data = vec![0x00, 0x00, 0x01, 0xBA, 0xFF, 0xFF, 0xFF, 0xFF];
226/// data.extend_from_slice(&[
227/// 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01,
228/// 0x40, 0x00, 0x03, 0x00,
229/// ]);
230/// let scan = mpeg_ps::program_stream::scan_packs(&data);
231/// assert_eq!(scan.skipped.len(), 1);
232/// assert_eq!(scan.skipped[0].offset, 0);
233/// assert_eq!(scan.packs.len(), 1);
234/// ```
235pub fn scan_packs(b: &[u8]) -> PackScan<'_> {
236 let mut packs = Vec::new();
237 let mut skipped = Vec::new();
238 let mut pos = 0usize;
239 while b.len() - pos >= 4 {
240 match parse_pack(&b[pos..]) {
241 Ok((Some(pack), consumed)) => {
242 packs.push(pack);
243 pos += consumed;
244 }
245 Ok((None, consumed)) => {
246 pos += consumed;
247 break;
248 }
249 Err(error) => {
250 let next = find_pack_start(&b[pos + 1..]).map(|i| pos + 1 + i);
251 let end = next.unwrap_or(b.len());
252 skipped.push(SkippedRegion {
253 offset: pos,
254 len: end - pos,
255 error,
256 });
257 pos = end;
258 }
259 }
260 }
261 PackScan {
262 packs,
263 skipped,
264 remaining: &b[pos..],
265 }
266}
267
268/// Offset of the first `pack_start_code` in `data`, if any.
269fn find_pack_start(data: &[u8]) -> Option<usize> {
270 let code = PACK_START_CODE.to_be_bytes();
271 data.windows(code.len()).position(|w| w == code)
272}
273
274#[cfg(test)]
275mod tests {
276 use super::parse_pack;
277 use crate::program_stream_map::{EsMapEntry, ProgramStreamMap};
278 use alloc::{vec, vec::Vec};
279 use broadcast_common::Serialize;
280
281 /// W5 (#1119): a PSM (`stream_id 0xBC`) within a pack must be surfaced
282 /// via `Pack::psm`, typed (`stream_type` per elementary stream), not
283 /// folded into `pes_packets` as an opaque, un-mapped PES packet.
284 #[test]
285 fn psm_is_surfaced_not_folded_into_pes_packets() {
286 let psm = ProgramStreamMap {
287 current_next_indicator: true,
288 single_extension_stream_flag: false,
289 version: 1,
290 program_stream_info: &[],
291 elementary_stream_map: vec![EsMapEntry {
292 stream_type: 0x02, // MPEG-2 video
293 elementary_stream_id: 0xE0,
294 stream_id_extension: None,
295 descriptors: &[],
296 }],
297 crc: 0,
298 };
299 let mut psm_bytes = vec![0u8; psm.serialized_len()];
300 psm.serialize_into(&mut psm_bytes).unwrap();
301
302 let mut b: Vec<u8> = vec![
303 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
304 ];
305 b.extend_from_slice(&psm_bytes);
306 // One PES packet after the PSM.
307 b.extend_from_slice(&[
308 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x80, 0x05, 0x21, 0x00, 0x01, 0x00, 0x01,
309 0xAA, 0xBB,
310 ]);
311
312 let (pack_opt, consumed) = parse_pack(&b).unwrap();
313 let pack = pack_opt.expect("not an end code");
314 let parsed_psm = pack.psm.expect("PSM must be surfaced on Pack::psm");
315 assert_eq!(parsed_psm.elementary_stream_map.len(), 1);
316 assert_eq!(parsed_psm.elementary_stream_map[0].stream_type, 0x02);
317 assert_eq!(
318 pack.pes_packets.len(),
319 1,
320 "the PSM must not appear (again, mis-typed) in pes_packets"
321 );
322 assert_eq!(consumed, b.len());
323 }
324
325 /// W2 (#1119): PES data must start after the system header's *wire*
326 /// `header_length` (Table 2-40), not after its re-serialized length.
327 /// A `header_length` of 8 (6-byte fixed body + 2 reserved padding bytes,
328 /// both MSB=0 so the `while (nextbits()=='1')` stream-bound loop parses
329 /// 0 entries) is 2 bytes longer than the 6-byte fixed body the loop
330 /// itself accounts for — the re-serialized length was used instead,
331 /// starting PES parsing 2 bytes early, inside the declared header.
332 #[test]
333 fn system_header_wire_length_used_for_pes_start_not_reserialized_length() {
334 // Pack header, byte-identical to `pack_header::pack_header_round_trip_fixture_pattern`
335 // (14 bytes, stuffing_length=0).
336 let mut b: Vec<u8> = vec![
337 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
338 ];
339 // System header: header_length = 8 (2 bytes more than the 6-byte
340 // fixed body its own loop consumes).
341 b.extend_from_slice(&[
342 0x00, 0x00, 0x01, 0xBB, // system_header_start_code
343 0x00, 0x08, // header_length = 8
344 0x80, 0x00, 0x01, 0x00, 0x20, 0x00, // 6-byte fixed body, 0 stream bounds
345 0x00, 0x00, // 2 reserved padding bytes (MSB=0)
346 ]);
347 // A minimal PES packet (stream_id 0xE0, PES_packet_length=0x0A, PTS-only, 2 payload bytes).
348 b.extend_from_slice(&[
349 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80, 0x80, 0x05, 0x21, 0x00, 0x01, 0x00, 0x01,
350 0xAA, 0xBB,
351 ]);
352
353 let (pack_opt, consumed) = parse_pack(&b).unwrap();
354 let pack = pack_opt.expect("not an end code");
355 assert!(pack.system_header.is_some());
356 assert_eq!(
357 pack.pes_packets.len(),
358 1,
359 "PES must be found starting after the full declared header_length"
360 );
361 assert_eq!(consumed, b.len());
362 }
363
364 /// W3 (#1119): a stray `000001BA` inside a PES packet's own (declared,
365 /// bounded) payload must not truncate the pack — `pack_start_code`/
366 /// `program_end_code` are only ever checked right after a fully-parsed
367 /// PES packet, never scanned for mid-payload.
368 #[test]
369 fn stray_pack_start_code_inside_pes_payload_does_not_truncate() {
370 let mut b: Vec<u8> = vec![
371 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
372 ];
373 // One PES packet, PES_packet_length=0x0E (14): flags(2)+hdl(1)+PTS(5)+
374 // payload(6), where the payload embeds a stray `00 00 01 BA` at its
375 // midpoint — plausible in start-code-emulation-bearing streams (AC-3,
376 // LPCM, DVD subpicture `private_stream_1`).
377 b.extend_from_slice(&[
378 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x80, 0x80, 0x05, 0x21, 0x00, 0x01, 0x00, 0x01,
379 0xAA, 0x00, 0x00, 0x01, 0xBA, 0xBB,
380 ]);
381
382 let (pack_opt, consumed) = parse_pack(&b).unwrap();
383 let pack = pack_opt.expect("not an end code");
384 assert_eq!(
385 pack.pes_packets.len(),
386 1,
387 "the one PES packet must parse in full, not truncate at the embedded 000001BA"
388 );
389 assert_eq!(consumed, b.len(), "the whole PES packet must be consumed");
390 }
391
392 /// Regression: a system_header whose serialized length (`pes_start`) is
393 /// greater than the offset of the next boundary found in `rest` caused
394 /// `b - pes_start` to subtract with overflow (panic) on the unsigned
395 /// `pes_end` computation. Fixed by using `saturating_sub`.
396 ///
397 /// Verbatim cargo-fuzz minimized artifact. A `system_header` declares
398 /// `header_length` = 255, so its re-serialized length (`pes_start` = 261)
399 /// covers a large parsed stream loop; meanwhile `find_next_boundary`
400 /// returns a `pack_start_code` (0x000001BA) embedded at rest-offset 142,
401 /// i.e. *before* `pes_start`. The old `b - pes_start` underflowed (panic on
402 /// unsigned subtraction). Fixed with `saturating_sub`. A truncated input
403 /// does NOT reproduce — `SystemHeader::parse` rejects it with
404 /// `HeaderLengthOverflow` before the buggy line, so the full body is needed.
405 #[test]
406 fn fuzz_regression_mpeg_ps_boundary_underflow() {
407 let crashing: &[u8] = &[
408 0x00, 0x00, 0x01, 0xba, 0x5c, 0xf5, 0xf5, 0xc0, 0xff, 0xff, 0x21, 0xf3, 0xf3, 0xf3,
409 0x90, 0xf3, 0xbb, 0x00, 0x00, 0x01, 0xbb, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
410 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 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, 0x00, 0x00, 0x01, 0xba, 0x5c,
420 0xf5, 0xf5, 0xc0, 0xff, 0xff, 0xf3, 0xf3, 0xf3, 0xf3, 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, 0xff, 0xff, 0xff, 0xff, 0xff,
427 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
428 0x00, 0x00,
429 ];
430 // Must not panic — result is either Ok or Err.
431 let _ = parse_pack(crashing);
432 }
433
434 #[test]
435 fn a_truncated_pes_packet_inside_a_pack_is_a_pes_error() {
436 let mut b: Vec<u8> = vec![
437 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x43, 0x36, 0x3B, 0xF8,
438 ];
439 let pes = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x0A, 0x80];
440 b.extend_from_slice(&pes);
441 let expected = mpeg_pes::PesPacket::parse(&pes).unwrap_err();
442 assert_eq!(parse_pack(&b).unwrap_err(), crate::Error::Pes(expected));
443 }
444}