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