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}