Skip to main content

mpeg_ps/
program_stream_map.rs

1//! Program Stream Map — ISO/IEC 13818-1 §2.5.4, Table 2-41.
2//!
3//! The PSM provides a mapping from `stream_type`/`elementary_stream_id`
4//! to descriptors, tied to the Program Stream with a CRC-32 trailer.
5//! It uses start-code prefix `0x000001` + `map_stream_id` `0xBC`.
6
7use alloc::vec::Vec;
8use broadcast_common::len::{need_in, need_out};
9
10use crate::error::{Error, Result};
11use broadcast_common::{Parse, Serialize, crc32_mpeg2};
12
13/// `packet_start_code_prefix` — `0x000001`.
14pub const PACKET_START_CODE_PREFIX: u32 = 0x00_0001;
15/// `map_stream_id` — `0xBC`, combined with the prefix forms the PSM start code.
16pub const MAP_STREAM_ID: u8 = 0xBC;
17/// The combined PSM start code: `packet_start_code_prefix` + `map_stream_id`
18/// (`0x000001BC`).
19pub const PSM_START_CODE: u32 = (PACKET_START_CODE_PREFIX << 8) | MAP_STREAM_ID as u32;
20
21/// Bytes before the map body: prefix(3) + map_stream_id(1) + psm_length(2) = 6.
22const PREFIX_LEN: usize = 6;
23
24/// `program_stream_map_length` upper bound (ISO/IEC 13818-1 Table 2-41).
25const MAP_LENGTH_MAX: usize = 1018;
26/// Bytes after the prefix that are always present: flags(1) + reserved(1) +
27/// `program_stream_info_length`(2) + `elementary_stream_map_length`(2).
28const FIXED_BODY_LEN: usize = 6;
29/// CRC-32 trailer length (Table 2-41).
30const CRC_LEN: usize = 4;
31/// `stream_type`(1) + `elementary_stream_id`(1) + `ES_info_length`(2).
32const ES_ENTRY_HEADER_LEN: usize = 4;
33/// Pseudo descriptor carrying `elementary_stream_id_extension` (Table 2-41):
34/// tag(1) + length(1) + marker+extension(1).
35const PSEUDO_DESCRIPTOR_LEN: usize = 3;
36/// `pseudo_descriptor_tag` — Table 2-41 allows any value.
37const PSEUDO_DESCRIPTOR_TAG: u8 = 0x00;
38/// Marker bit in the `elementary_stream_id_extension` byte (Table 2-41).
39const MARKER_BIT: u8 = 0x80;
40/// 7-bit `elementary_stream_id_extension`.
41const EXTENSION_MASK: u8 = 0x7F;
42/// `flags` byte reserved bit 5 — conventionally set (Table 2-41).
43const FLAGS_RESERVED_BIT: u8 = 0x20;
44/// `program_stream_map_version` width in bits (Table 2-41).
45const VERSION_BITS: u32 = 5;
46/// Mask of the 5-bit `program_stream_map_version` in the flags byte.
47const VERSION_MASK: u8 = 0x1F;
48
49/// An elementary stream descriptor entry with optional stream_id_extension.
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize))]
52pub struct EsMapEntry<'a> {
53    /// `stream_type` per ISO/IEC 13818-1 Table 2-29.
54    pub stream_type: u8,
55    /// `elementary_stream_id`.
56    pub elementary_stream_id: u8,
57    /// If `elementary_stream_id == 0xFD` and `single_extension_stream_flag == 0`,
58    /// this holds the `elementary_stream_id_extension`. Otherwise `None`.
59    pub stream_id_extension: Option<u8>,
60    /// Descriptor bytes for this elementary stream.
61    #[cfg_attr(feature = "serde", serde(skip))]
62    pub descriptors: &'a [u8],
63}
64
65/// A parsed Program Stream Map.
66#[derive(Debug, Clone, PartialEq, Eq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize))]
68pub struct ProgramStreamMap<'a> {
69    /// `current_next_indicator` (1 bit).
70    pub current_next_indicator: bool,
71    /// `single_extension_stream_flag` (1 bit).
72    pub single_extension_stream_flag: bool,
73    /// `program_stream_map_version` (5 bits).
74    pub version: u8,
75    /// Descriptors for the program stream itself.
76    #[cfg_attr(feature = "serde", serde(skip))]
77    pub program_stream_info: &'a [u8],
78    /// Per-elementary-stream entries.
79    pub elementary_stream_map: Vec<EsMapEntry<'a>>,
80    /// The raw CRC-32 value in the trailer (validated on parse).
81    pub crc: u32,
82}
83
84impl<'a> Parse<'a> for ProgramStreamMap<'a> {
85    type Error = Error;
86
87    fn parse(b: &'a [u8]) -> Result<Self> {
88        if b.len() < PREFIX_LEN + 2 + 4 {
89            // prefix + psm_length(2) + CRC(4) = minimum
90            return Err(Error::BufferTooShort(
91                broadcast_common::error::BufferTooShort {
92                    need: PREFIX_LEN + 2 + 4,
93                    have: b.len(),
94                    what: "program_stream_map",
95                },
96            ));
97        }
98
99        // packet_start_code_prefix (3 bytes) + map_stream_id (1 byte)
100        let start = u32::from_be_bytes([0, b[0], b[1], b[2]]);
101        if start != PACKET_START_CODE_PREFIX {
102            return Err(Error::BadMapStreamId(b[3]));
103        }
104        if b[3] != MAP_STREAM_ID {
105            return Err(Error::BadMapStreamId(b[3]));
106        }
107
108        let map_length = u16::from_be_bytes([b[4], b[5]]) as usize;
109        let crc_end = PREFIX_LEN + map_length + 4;
110        if b.len() < crc_end {
111            return Err(Error::MapLengthOverflow {
112                map_length,
113                available: b.len().saturating_sub(PREFIX_LEN),
114            });
115        }
116
117        let crc_data_end = crc_end - 4;
118        let crc_data = &b[0..crc_data_end];
119        let stored_crc = u32::from_be_bytes([
120            b[crc_data_end],
121            b[crc_data_end + 1],
122            b[crc_data_end + 2],
123            b[crc_data_end + 3],
124        ]);
125        let computed_crc = crc32_mpeg2::compute(crc_data);
126        if computed_crc != stored_crc {
127            return Err(Error::CrcMismatch(broadcast_common::error::CrcMismatch {
128                what: "PSM",
129                computed: computed_crc,
130                expected: stored_crc,
131            }));
132        }
133
134        // flags byte: current_next(1) | single_extension(1) | reserved(1) | version(5)
135        let flags = b[6];
136        let current_next_indicator = flags & 0x80 != 0;
137        let single_extension_stream_flag = flags & 0x40 != 0;
138        let version = flags & VERSION_MASK;
139
140        // reserved byte (7 bits reserved + marker_bit)
141        if b[7] & 0x01 == 0 {
142            return Err(Error::BadMarker("program_stream_map marker_bit"));
143        }
144
145        let program_stream_info_length = u16::from_be_bytes([b[8], b[9]]) as usize;
146
147        let info_start = PREFIX_LEN + 4; // after flags(1)+reserved(1)+prog_info_len(2)
148        let info_end = info_start + program_stream_info_length;
149
150        // Table 2-41 order: program_stream_info_length(16), descriptor()×N,
151        // THEN elementary_stream_map_length(16) — the length field follows
152        // the program descriptor loop, not the reverse (#1050).
153        if crc_data_end < info_end + 2 {
154            return Err(Error::MapLengthOverflow {
155                map_length,
156                available: b.len().saturating_sub(PREFIX_LEN),
157            });
158        }
159        let elementary_stream_map_length =
160            u16::from_be_bytes([b[info_end], b[info_end + 1]]) as usize;
161
162        let es_start = info_end + 2; // after elementary_stream_map_length(2)
163        let es_end = es_start + elementary_stream_map_length;
164
165        if crc_data_end < es_end {
166            return Err(Error::MapLengthOverflow {
167                map_length,
168                available: b.len().saturating_sub(PREFIX_LEN),
169            });
170        }
171
172        let program_stream_info = &b[info_start..info_end];
173
174        let es_data = &b[es_start..es_end];
175        let elementary_stream_map = parse_es_loop(es_data, single_extension_stream_flag)?;
176
177        Ok(ProgramStreamMap {
178            current_next_indicator,
179            single_extension_stream_flag,
180            version,
181            program_stream_info,
182            elementary_stream_map,
183            crc: stored_crc,
184        })
185    }
186}
187
188fn parse_es_loop(data: &[u8], single_flag: bool) -> Result<Vec<EsMapEntry<'_>>> {
189    let mut entries = Vec::new();
190    let mut pos = 0;
191    while pos + 4 <= data.len() {
192        let stream_type = data[pos];
193        let elementary_stream_id = data[pos + 1];
194        let es_info_length = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
195        let entry_hdr_end = pos + 4;
196        let entry_end = entry_hdr_end + es_info_length;
197        need_in(data, entry_end, "elementary_stream_map entry descriptors")?;
198
199        let (stream_id_extension, descriptors) = if elementary_stream_id == 0xFD && !single_flag {
200            // Extension form with pseudo descriptor
201            if es_info_length < 3 {
202                return Err(Error::BufferTooShort(
203                    broadcast_common::error::BufferTooShort {
204                        need: 3,
205                        have: es_info_length,
206                        what: "pseudo descriptor for stream_id_extension",
207                    },
208                ));
209            }
210            // pseudo_descriptor_tag + pseudo_descriptor_length + marker+extension
211            if data[entry_hdr_end + 2] & 0x80 == 0 {
212                return Err(Error::BadMarker("elementary_stream_id_extension marker"));
213            }
214            let ext = data[entry_hdr_end + 2] & 0x7F;
215            let desc = &data[entry_hdr_end + 3..entry_end];
216            (Some(ext), desc)
217        } else {
218            let desc = &data[entry_hdr_end..entry_end];
219            (None, desc)
220        };
221
222        entries.push(EsMapEntry {
223            stream_type,
224            elementary_stream_id,
225            stream_id_extension,
226            descriptors,
227        });
228
229        pos = entry_end;
230    }
231    Ok(entries)
232}
233
234/// Bytes one elementary-stream entry occupies on the wire:
235/// `stream_type(1) + elementary_stream_id(1) + ES_info_length(2)` + the
236/// optional 3-byte pseudo descriptor + the descriptor bytes.
237fn es_entry_len(e: &EsMapEntry<'_>) -> usize {
238    ES_ENTRY_HEADER_LEN
239        + if e.stream_id_extension.is_some() {
240            PSEUDO_DESCRIPTOR_LEN
241        } else {
242            0
243        }
244        + e.descriptors.len()
245}
246
247/// Write the elementary-stream loop straight into `out` (exactly the sum of
248/// [`es_entry_len`], checked by the caller), no intermediate owned copies
249/// (audit r14-MPS-O1).
250fn write_es_loop(entries: &[EsMapEntry<'_>], out: &mut [u8]) -> Result<()> {
251    let mut at = 0usize;
252    for e in entries {
253        let entry_len = es_entry_len(e);
254        let dst = &mut out[at..at + entry_len];
255        dst[0] = e.stream_type;
256        dst[1] = e.elementary_stream_id;
257        let info_len = entry_len - ES_ENTRY_HEADER_LEN;
258        let info_len =
259            broadcast_common::len::fit_u16(info_len, "elementary_stream_map.ES_info_length")?;
260        dst[2..4].copy_from_slice(&info_len.to_be_bytes());
261        let mut c = ES_ENTRY_HEADER_LEN;
262        if let Some(ext) = e.stream_id_extension {
263            dst[c] = PSEUDO_DESCRIPTOR_TAG;
264            // pseudo_descriptor_length: 8-bit field covering the marker+
265            // extension byte plus the descriptors. Checked in usize before
266            // narrowing — `1 + len as u8` used to overflow at 255 (#1129).
267            dst[c + 1] = broadcast_common::len::fit_u8(
268                1 + e.descriptors.len(),
269                "elementary_stream_map.pseudo_descriptor_length",
270            )?;
271            dst[c + 2] = MARKER_BIT | (ext & EXTENSION_MASK);
272            c += PSEUDO_DESCRIPTOR_LEN;
273        }
274        dst[c..].copy_from_slice(e.descriptors);
275        at += entry_len;
276    }
277    Ok(())
278}
279
280impl Serialize for ProgramStreamMap<'_> {
281    type Error = Error;
282
283    fn serialized_len(&self) -> usize {
284        let prog_info_len = self.program_stream_info.len();
285        let es_loop_len: usize = self.elementary_stream_map.iter().map(es_entry_len).sum();
286        // PREFIX_LEN + flags(1) + reserved(1) + prog_info_len(2) + prog_info
287        // + es_map_len(2) + es_loop + CRC(4)
288        PREFIX_LEN + FIXED_BODY_LEN + prog_info_len + es_loop_len + CRC_LEN
289    }
290
291    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
292        let total = self.serialized_len();
293        need_out(buf, total)?;
294
295        // packet_start_code_prefix (3 bytes)
296        buf[0..3].copy_from_slice(&PACKET_START_CODE_PREFIX.to_be_bytes()[1..]);
297        // map_stream_id
298        buf[3] = MAP_STREAM_ID;
299
300        let prog_info_len = self.program_stream_info.len();
301        let es_loop_len: usize = self.elementary_stream_map.iter().map(es_entry_len).sum();
302
303        // map_length = flags(1) + reserved(1) + prog_info_len(2) + es_loop_len(2) + prog_info + es_loop
304        let map_length = FIXED_BODY_LEN + prog_info_len + es_loop_len;
305        // program_stream_map_length max is 1018 (0x3FA) per Table 2-41 — checked
306        // in usize before any narrowing (#1129).
307        if map_length > MAP_LENGTH_MAX {
308            return Err(broadcast_common::len::FieldOverflow {
309                field: "program_stream_map.program_stream_map_length",
310                value: map_length as u64,
311                max: MAP_LENGTH_MAX as u64,
312            }
313            .into());
314        }
315        let map_length = broadcast_common::len::fit_u16(
316            map_length,
317            "program_stream_map.program_stream_map_length",
318        )?;
319        buf[4..6].copy_from_slice(&map_length.to_be_bytes());
320
321        // flags: current_next(1) + single_extension(1) + reserved(1, convention
322        // is set) + version(5). W8 (#1119): the reserved bit (bit 5) was
323        // always written 0 instead of the conventional 1.
324        let version = broadcast_common::len::fit_bits(
325            u64::from(self.version),
326            VERSION_BITS,
327            "program_stream_map.program_stream_map_version",
328        )? as u8;
329        buf[6] = (u8::from(self.current_next_indicator) << 7)
330            | (u8::from(self.single_extension_stream_flag) << 6)
331            | FLAGS_RESERVED_BIT
332            | version;
333
334        // reserved(7, convention is all set) + marker_bit(1).
335        // W8 (#1119): `0x7F | 0x01` is a no-op (`0x7F` already has bit 0
336        // set) and leaves the MSB reserved bit `0` instead of the
337        // conventional `1` — real encoders write this byte `0xFF`, so this
338        // did not round-trip byte-identically against a real capture.
339        buf[7] = 0xFF;
340
341        // program_stream_info_length and elementary_stream_map_length are
342        // both 16-bit fields; `map_length <= MAP_LENGTH_MAX` above already
343        // bounds each of them well under `u16::MAX`, but check explicitly
344        // rather than relying on that aggregate bound holding (#1129).
345        let prog_info_len_u16 = broadcast_common::len::fit_u16(
346            prog_info_len,
347            "program_stream_map.program_stream_info_length",
348        )?;
349        let es_loop_len_u16 = broadcast_common::len::fit_u16(
350            es_loop_len,
351            "program_stream_map.elementary_stream_map_length",
352        )?;
353        buf[8..10].copy_from_slice(&prog_info_len_u16.to_be_bytes());
354
355        // program_stream_info descriptors (Table 2-41: the program descriptor
356        // loop comes immediately after program_stream_info_length, at byte
357        // 10, BEFORE elementary_stream_map_length — #1050)
358        buf[10..10 + prog_info_len].copy_from_slice(self.program_stream_info);
359
360        // elementary_stream_map_length, then the elementary stream loop
361        let es_len_start = 10 + prog_info_len;
362        buf[es_len_start..es_len_start + 2].copy_from_slice(&es_loop_len_u16.to_be_bytes());
363        let es_start = es_len_start + 2;
364        write_es_loop(
365            &self.elementary_stream_map,
366            &mut buf[es_start..es_start + es_loop_len],
367        )?;
368
369        // CRC-32 over everything before it
370        let crc_offset = es_start + es_loop_len;
371        let crc = crc32_mpeg2::compute(&buf[0..crc_offset]);
372        buf[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_be_bytes());
373
374        Ok(total)
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use alloc::vec;
382
383    #[test]
384    fn serialize_rejects_version_over_5_bits() {
385        let psm = ProgramStreamMap {
386            current_next_indicator: true,
387            single_extension_stream_flag: false,
388            version: 0x20,
389            program_stream_info: &[],
390            elementary_stream_map: vec![],
391            crc: 0,
392        };
393        let mut buf = vec![0u8; 64];
394        assert!(matches!(
395            psm.serialize_into(&mut buf),
396            Err(Error::FieldOverflow(_))
397        ));
398    }
399
400    /// Build a valid PSM programmatically and verify round-trip.
401    #[test]
402    fn psm_build_and_round_trip() {
403        let entries = vec![EsMapEntry {
404            stream_type: 0x02, // MPEG-2 video
405            elementary_stream_id: 0xE0,
406            stream_id_extension: None,
407            descriptors: &[0x0A, 0x04, b'H', b'E', b'L', b'L'], // registration descriptor
408        }];
409
410        let psm = ProgramStreamMap {
411            current_next_indicator: true,
412            single_extension_stream_flag: false,
413            version: 3,
414            program_stream_info: &[],
415            elementary_stream_map: entries,
416            crc: 0, // will be overwritten by serialize
417        };
418
419        let mut buf = vec![0u8; psm.serialized_len()];
420        psm.serialize_into(&mut buf).unwrap();
421        assert_eq!(buf[..4], PSM_START_CODE.to_be_bytes());
422        assert_eq!(PSM_START_CODE, 0x0000_01BC);
423
424        // Parse back
425        let parsed = ProgramStreamMap::parse(&buf).unwrap();
426        assert!(parsed.current_next_indicator);
427        assert!(!parsed.single_extension_stream_flag);
428        assert_eq!(parsed.version, 3);
429        assert!(parsed.program_stream_info.is_empty());
430        assert_eq!(parsed.elementary_stream_map.len(), 1);
431        assert_eq!(parsed.elementary_stream_map[0].stream_type, 0x02);
432        assert_eq!(parsed.elementary_stream_map[0].elementary_stream_id, 0xE0);
433        assert!(
434            parsed.elementary_stream_map[0]
435                .stream_id_extension
436                .is_none()
437        );
438        assert_eq!(
439            parsed.elementary_stream_map[0].descriptors,
440            &[0x0A, 0x04, b'H', b'E', b'L', b'L']
441        );
442
443        // Byte-exact round-trip
444        let mut out2 = vec![0u8; parsed.serialized_len()];
445        parsed.serialize_into(&mut out2).unwrap();
446        assert_eq!(&out2[..], &buf[..], "byte-exact round-trip mismatch");
447
448        // Mutation test: change version, output must differ
449        let psm_mut = ProgramStreamMap {
450            current_next_indicator: true,
451            single_extension_stream_flag: false,
452            version: 7,
453            program_stream_info: &[],
454            elementary_stream_map: vec![EsMapEntry {
455                stream_type: 0x02,
456                elementary_stream_id: 0xE0,
457                stream_id_extension: None,
458                descriptors: &[0x0A, 0x04, b'H', b'E', b'L', b'L'],
459            }],
460            crc: 0,
461        };
462        let mut out3 = vec![0u8; psm_mut.serialized_len()];
463        psm_mut.serialize_into(&mut out3).unwrap();
464        assert_ne!(&buf[..], &out3[..]);
465    }
466
467    #[test]
468    fn psm_with_stream_id_extension() {
469        let entries = vec![EsMapEntry {
470            stream_type: 0x06, // subtitles
471            elementary_stream_id: 0xFD,
472            stream_id_extension: Some(0x0F),
473            descriptors: &[0x59, 0x02, 0x01, 0x02], // subtitling descriptor
474        }];
475
476        let psm = ProgramStreamMap {
477            current_next_indicator: true,
478            single_extension_stream_flag: false,
479            version: 1,
480            program_stream_info: &[],
481            elementary_stream_map: entries,
482            crc: 0,
483        };
484
485        let mut buf = vec![0u8; psm.serialized_len()];
486        psm.serialize_into(&mut buf).unwrap();
487
488        let parsed = ProgramStreamMap::parse(&buf).unwrap();
489        assert_eq!(parsed.elementary_stream_map.len(), 1);
490        assert_eq!(
491            parsed.elementary_stream_map[0].stream_id_extension,
492            Some(0x0F)
493        );
494        assert_eq!(
495            parsed.elementary_stream_map[0].descriptors,
496            &[0x59, 0x02, 0x01, 0x02]
497        );
498
499        // Byte-exact round-trip
500        let mut out2 = vec![0u8; parsed.serialized_len()];
501        parsed.serialize_into(&mut out2).unwrap();
502        assert_eq!(&out2[..], &buf[..]);
503    }
504
505    /// MPS-W6 (#1129): `pseudo_descriptor_length` is `1 + descriptors.len()`,
506    /// written as `1 + (descriptors.len() as u8)` — the cast happened before
507    /// the add, so a 255-byte descriptor overflowed the `u8` addition
508    /// (panicking in debug, wrapping in release) rather than being checked.
509    #[test]
510    fn pseudo_descriptor_length_overflow_is_rejected_not_wrapped() {
511        let entries = vec![EsMapEntry {
512            stream_type: 0x06,
513            elementary_stream_id: 0xFD,
514            stream_id_extension: Some(0x01),
515            descriptors: &[0u8; 255],
516        }];
517        let psm = ProgramStreamMap {
518            current_next_indicator: true,
519            single_extension_stream_flag: false,
520            version: 0,
521            program_stream_info: &[],
522            elementary_stream_map: entries,
523            crc: 0,
524        };
525        let mut buf = vec![0u8; psm.serialized_len()];
526        assert!(matches!(
527            psm.serialize_into(&mut buf),
528            Err(Error::FieldOverflow(_))
529        ));
530    }
531
532    /// MPS-W6 (#1129): `program_stream_map_length` (spec max 1018, Table
533    /// 2-41) was written unchecked with `as u16`.
534    #[test]
535    fn program_stream_map_length_over_spec_max_is_rejected() {
536        let psm = ProgramStreamMap {
537            current_next_indicator: true,
538            single_extension_stream_flag: false,
539            version: 0,
540            program_stream_info: &[0u8; 1019],
541            elementary_stream_map: vec![],
542            crc: 0,
543        };
544        let mut buf = vec![0u8; psm.serialized_len()];
545        assert!(matches!(
546            psm.serialize_into(&mut buf),
547            Err(Error::FieldOverflow(_))
548        ));
549    }
550
551    /// The boundary: exactly the spec max still serializes and round-trips.
552    #[test]
553    fn program_stream_map_length_at_spec_max_round_trips() {
554        // map_length = 6 + prog_info_len + es_loop_len; want map_length == 1018.
555        let psm = ProgramStreamMap {
556            current_next_indicator: true,
557            single_extension_stream_flag: false,
558            version: 0,
559            program_stream_info: &[0u8; 1012],
560            elementary_stream_map: vec![],
561            crc: 0,
562        };
563        let mut buf = vec![0u8; psm.serialized_len()];
564        psm.serialize_into(&mut buf).unwrap();
565        let parsed = ProgramStreamMap::parse(&buf).unwrap();
566        assert_eq!(parsed.program_stream_info.len(), 1012);
567    }
568
569    /// #1050 regression: a conformant PSM with a non-empty program
570    /// descriptor loop. TSDuck cannot generate PS, and the committed
571    /// ffmpeg fixture (`fixtures/ps/ffmpeg-mpeg2-ps.mpg`) carries no
572    /// PSM at all (VOB/MPEG-PS from ffmpeg's muxer omits it), so this is
573    /// hand-built directly from ISO/IEC 13818-1 Table 2-41's field order —
574    /// the oracle: `program_stream_info_length(16)`, `descriptor()×N`,
575    /// THEN `elementary_stream_map_length(16)`, `stream_map()×N`, `CRC_32`.
576    /// Before the fix, parse took the program loop's first descriptor
577    /// bytes (tag=0x05 'C'=0x43 here) as `elementary_stream_map_length`
578    /// (0x0543 = 1347), which overflows the buffer and fails with
579    /// `MapLengthOverflow` instead of parsing the one ES entry.
580    #[test]
581    fn psm_with_nonempty_program_descriptor_loop() {
582        // One program-level registration descriptor (tag 0x05, len 4, "TEST").
583        let prog_descriptor: &[u8] = &[0x05, 0x04, b'T', b'E', b'S', b'T'];
584        let entries = vec![EsMapEntry {
585            stream_type: 0x02, // MPEG-2 video
586            elementary_stream_id: 0xE0,
587            stream_id_extension: None,
588            descriptors: &[],
589        }];
590
591        let psm = ProgramStreamMap {
592            current_next_indicator: true,
593            single_extension_stream_flag: false,
594            version: 5,
595            program_stream_info: prog_descriptor,
596            elementary_stream_map: entries,
597            crc: 0,
598        };
599
600        let mut buf = vec![0u8; psm.serialized_len()];
601        psm.serialize_into(&mut buf).unwrap();
602
603        // Table 2-41 byte layout check: the program descriptor loop (6 bytes)
604        // sits at buf[10..16], directly after program_stream_info_length;
605        // elementary_stream_map_length sits at buf[16..18], AFTER it.
606        assert_eq!(&buf[10..16], prog_descriptor);
607        let es_map_len = u16::from_be_bytes([buf[16], buf[17]]);
608        assert_eq!(es_map_len, 4, "one ES entry: stream_type+id+info_len(0)");
609
610        let parsed = ProgramStreamMap::parse(&buf).unwrap();
611        assert_eq!(parsed.program_stream_info, prog_descriptor);
612        assert_eq!(parsed.elementary_stream_map.len(), 1);
613        assert_eq!(parsed.elementary_stream_map[0].stream_type, 0x02);
614        assert_eq!(parsed.elementary_stream_map[0].elementary_stream_id, 0xE0);
615
616        // Byte-exact round-trip
617        let mut out2 = vec![0u8; parsed.serialized_len()];
618        parsed.serialize_into(&mut out2).unwrap();
619        assert_eq!(&out2[..], &buf[..]);
620    }
621
622    /// W8 (#1119): reserved bits in the flags byte (bit 5) and the
623    /// following reserved+marker byte (bits 7-1) are conventionally `1` —
624    /// real encoders write byte 6's reserved bit set and byte 7 as `0xFF`.
625    /// The flags byte used to always clear bit 5, and byte 7 was written
626    /// `0x7F | 0x01` (a no-op, since `0x7F` already has bit 0 set) instead
627    /// of `0xFF`, leaving the MSB reserved bit `0`.
628    #[test]
629    fn reserved_bits_are_set_by_convention() {
630        let psm = ProgramStreamMap {
631            current_next_indicator: false,
632            single_extension_stream_flag: false,
633            version: 0,
634            program_stream_info: &[],
635            elementary_stream_map: vec![],
636            crc: 0,
637        };
638        let mut buf = vec![0u8; psm.serialized_len()];
639        psm.serialize_into(&mut buf).unwrap();
640        assert_eq!(
641            buf[6], 0x20,
642            "flags byte reserved bit (0x20) must be set even with both flags clear"
643        );
644        assert_eq!(buf[7], 0xFF, "reserved(7)+marker_bit(1) byte must be 0xFF");
645    }
646
647    #[test]
648    fn psm_bad_crc_rejected() {
649        let psm = ProgramStreamMap {
650            current_next_indicator: true,
651            single_extension_stream_flag: false,
652            version: 0,
653            program_stream_info: &[],
654            elementary_stream_map: vec![],
655            crc: 0,
656        };
657        let mut buf = vec![0u8; psm.serialized_len()];
658        psm.serialize_into(&mut buf).unwrap();
659        // Corrupt CRC (last 4 bytes)
660        let crc_off = buf.len() - 4;
661        buf[crc_off] ^= 0xFF;
662        assert!(matches!(
663            ProgramStreamMap::parse(&buf),
664            Err(Error::CrcMismatch(..))
665        ));
666    }
667}