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}