Skip to main content

mpeg_ps/
system_header.rs

1//! System Header — ISO/IEC 13818-1 §2.5.3.5, Table 2-40.
2//!
3//! The (optional) system header follows immediately after the pack header
4//! stuffing bytes, in the first pack of the stream. It constrains the P-STD
5//! model: `rate_bound`, `audio_bound`/`video_bound`, and per-stream P-STD
6//! buffer-size bounds.
7
8use alloc::vec::Vec;
9use broadcast_common::len::{need_in, need_out};
10
11use crate::error::{Error, Result};
12use broadcast_common::{Parse, Serialize};
13
14/// `system_header_start_code` — `0x000001BB`.
15pub const SYSTEM_HEADER_START_CODE: u32 = 0x0000_01BB;
16
17/// `stream_id` value that triggers the extension (extended_stream_id) form.
18const EXT_STREAM_ID: u8 = 0xB7;
19
20/// Fixed bytes before the stream loop: start_code(4) + header_length(2).
21pub(crate) const PREFIX_LEN: usize = 6;
22/// Mask of the 7 `reserved_bits` below `packet_rate_restriction_flag` (Table 2-40).
23const RESERVED_BITS_MASK: u8 = 0x7F;
24
25/// A per-stream P-STD buffer bound entry.
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize))]
28pub struct StdBufferBound {
29    /// The stream's `stream_id` (ISO/IEC 13818-1 Table 2-22).
30    pub stream_id: u8,
31    /// If `stream_id == 0xB7`, this is the extended `stream_id_extension` field.
32    /// Otherwise `None`.
33    pub stream_id_extension: Option<u8>,
34    /// `P-STD_buffer_bound_scale`: `false` = 128 bytes, `true` = 1024 bytes.
35    pub buffer_bound_scale: bool,
36    /// `P-STD_buffer_size_bound` in units of `buffer_bound_scale`.
37    pub buffer_size_bound: u16,
38}
39
40/// A parsed system header.
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize))]
43pub struct SystemHeader {
44    /// Upper bound on `program_mux_rate` across all packs (22-bit, 50 B/s units).
45    pub rate_bound: u32,
46    /// Upper bound on simultaneously-active audio streams (6-bit).
47    pub audio_bound: u8,
48    /// Fixed / variable bitrate indicator.
49    pub fixed_flag: bool,
50    /// Constrained system parameters flag.
51    pub csps_flag: bool,
52    /// System audio lock flag.
53    pub system_audio_lock_flag: bool,
54    /// System video lock flag.
55    pub system_video_lock_flag: bool,
56    /// Upper bound on simultaneously-active video streams (5-bit).
57    pub video_bound: u8,
58    /// Packet rate restriction flag.
59    pub packet_rate_restriction_flag: bool,
60    /// The 7 `reserved_bits` after `packet_rate_restriction_flag`
61    /// (Table 2-40), preserved so a header whose encoder wrote something
62    /// other than the conventional all-ones (`0x7F`) re-serializes
63    /// byte-identically (audit r14-MPS-W8).
64    pub reserved_bits: u8,
65    /// Per-stream P-STD buffer bounds (the `while (nextbits() == '1')` loop).
66    pub std_buffer_bounds: Vec<StdBufferBound>,
67    /// Bytes of `header_length` that follow the stream-bound loop. Table
68    /// 2-40 defines none, but a real encoder can pad the header; they are
69    /// preserved verbatim so the header re-serializes byte-identically
70    /// (audit r14-MPS-W8, #1119). Empty for a conformant header.
71    pub trailing_reserved: Vec<u8>,
72}
73
74/// Bytes the stream loop occupies. Accumulated in `usize` (#1129): the
75/// previous `u16` accumulator overflowed for more than 10 921
76/// caller-constructed bounds, wrapping `header_length` and `serialized_len()`
77/// together so they stayed mutually consistent but both silently wrong,
78/// which then indexed past the (too-small) allocated buffer in
79/// `serialize_into`. `serialize_into` separately validates that this fits
80/// the 16-bit `header_length` wire field before writing it.
81fn stream_loop_len(system_header: &SystemHeader) -> usize {
82    let mut len: usize = 0;
83    for b in &system_header.std_buffer_bounds {
84        if b.stream_id_extension.is_some() {
85            len += 6;
86        } else {
87            len += 3;
88        }
89    }
90    len + system_header.trailing_reserved.len()
91}
92
93impl<'a> Parse<'a> for SystemHeader {
94    type Error = Error;
95
96    fn parse(b: &'a [u8]) -> Result<Self> {
97        need_in(b, PREFIX_LEN, "system_header prefix")?;
98
99        if u32::from_be_bytes([b[0], b[1], b[2], b[3]]) != SYSTEM_HEADER_START_CODE {
100            return Err(Error::BadSystemHeaderStartCode(u32::from_be_bytes([
101                b[0], b[1], b[2], b[3],
102            ])));
103        }
104
105        let header_length = u16::from_be_bytes([b[4], b[5]]) as usize;
106        let body_end = PREFIX_LEN + header_length;
107        if b.len() < body_end {
108            return Err(Error::HeaderLengthOverflow {
109                header_length,
110                available: b.len().saturating_sub(PREFIX_LEN),
111            });
112        }
113        let body = &b[PREFIX_LEN..body_end];
114
115        // Wire layout (Table 2-40, after header_length):
116        // byte 0: marker(1)  | rate_bound[21:15](7)
117        // byte 1: rate_bound[14:7](8)
118        // byte 2: rate_bound[6:0](7) | marker(1)
119        // byte 3: audio_bound[5:0](6) | fixed_flag(1) | CSPS_flag(1)
120        // byte 4: system_audio_lock_flag(1) | system_video_lock_flag(1) | marker(1) | video_bound[4:0](5)
121        // byte 5: packet_rate_restriction_flag(1) | reserved(7)
122
123        if body.len() < 6 {
124            return Err(Error::BufferTooShort(
125                broadcast_common::error::BufferTooShort {
126                    need: 6 + PREFIX_LEN,
127                    have: b.len(),
128                    what: "system_header fixed body",
129                },
130            ));
131        }
132
133        // Marker bit checks
134        if body[0] & 0x80 == 0 {
135            return Err(Error::BadMarker("system_header rate_bound marker 1"));
136        }
137        if body[2] & 0x01 == 0 {
138            return Err(Error::BadMarker("system_header rate_bound marker 2"));
139        }
140        if body[4] & 0x20 == 0 {
141            return Err(Error::BadMarker("system_header video_bound marker"));
142        }
143
144        // rate_bound: 22 bits
145        let rate_bound = ((u32::from(body[0] & 0x7F) << 15)
146            | (u32::from(body[1]) << 7)
147            | u32::from(body[2] >> 1))
148            & 0x3F_FFFF;
149
150        // byte 3: audio_bound(6) | fixed_flag(1) | CSPS_flag(1)
151        let audio_bound = (body[3] >> 2) & 0x3F;
152        let fixed_flag = body[3] & 0x02 != 0;
153        let csps_flag = body[3] & 0x01 != 0;
154
155        // byte 4: system_audio_lock_flag(1) | system_video_lock_flag(1) | marker(1) | video_bound[4:0](5)
156        let system_audio_lock_flag = body[4] & 0x80 != 0;
157        let system_video_lock_flag = body[4] & 0x40 != 0;
158        // W1 (#1119): `video_bound` is the low 5 bits of byte 4, not 4 —
159        // `& 0x0F` dropped bit 4, so `video_bound` in 16..=31 round-tripped
160        // to `video_bound - 16` (serialize already used the correct `0x1F`,
161        // Table 2-40).
162        let video_bound = body[4] & 0x1F;
163
164        // byte 5: packet_rate_restriction_flag(1) | reserved(7)
165        let packet_rate_restriction_flag = body[5] & 0x80 != 0;
166        let reserved_bits = body[5] & RESERVED_BITS_MASK;
167
168        // Stream loop — each entry starts with MSB=1 (nextbits()=='1')
169        let mut pos = 6;
170        let mut std_buffer_bounds = Vec::new();
171        while pos < body.len() && body[pos] & 0x80 != 0 {
172            let stream_id = body[pos];
173            if stream_id == EXT_STREAM_ID {
174                // Extension form (6 bytes)
175                need_in(body, pos + 6, "system_header extended stream entry")?;
176                // byte pos+1: '11' + '000000'(6 bits)
177                if body[pos + 1] & 0xC0 != 0xC0 {
178                    return Err(Error::BadStreamIdExtensionPrefix(body[pos + 1]));
179                }
180                // byte pos+2: '0'(1) + stream_id_extension(7)
181                let stream_id_extension = body[pos + 2] & 0x7F;
182                // byte pos+3: '1011 0110'
183                if body[pos + 3] != 0xB6 {
184                    return Err(Error::BadStreamIdExtensionPrefix(body[pos + 3]));
185                }
186                // byte pos+4: '11' + scale(1) + size[12:7](5)
187                if body[pos + 4] & 0xC0 != 0xC0 {
188                    return Err(Error::BadMarker("P-STD_buffer_bound_scale prefix (ext)"));
189                }
190                let buffer_bound_scale = body[pos + 4] & 0x20 != 0;
191                // bytes pos+4/pos+5: '11'(2) | scale(1) | size[12:8](5), then size[7:0](8):
192                // the 13-bit size has no trailing marker bit (Table 2-40).
193                let buffer_size_bound =
194                    (u16::from(body[pos + 4] & 0x1F) << 8) | u16::from(body[pos + 5]);
195                std_buffer_bounds.push(StdBufferBound {
196                    stream_id,
197                    stream_id_extension: Some(stream_id_extension),
198                    buffer_bound_scale,
199                    buffer_size_bound,
200                });
201                pos += 6;
202            } else {
203                // Normal form (3 bytes)
204                need_in(body, pos + 3, "system_header stream entry")?;
205                // byte pos+1: '11' + scale(1) + size[12:7](5)
206                if body[pos + 1] & 0xC0 != 0xC0 {
207                    return Err(Error::BadMarker("P-STD_buffer_bound_scale prefix"));
208                }
209                let buffer_bound_scale = body[pos + 1] & 0x20 != 0;
210                // bytes pos+1/pos+2: '11'(2) | scale(1) | size[12:8](5), then size[7:0](8).
211                let buffer_size_bound =
212                    (u16::from(body[pos + 1] & 0x1F) << 8) | u16::from(body[pos + 2]);
213                std_buffer_bounds.push(StdBufferBound {
214                    stream_id,
215                    stream_id_extension: None,
216                    buffer_bound_scale,
217                    buffer_size_bound,
218                });
219                pos += 3;
220            }
221        }
222
223        Ok(SystemHeader {
224            rate_bound,
225            audio_bound,
226            fixed_flag,
227            csps_flag,
228            system_audio_lock_flag,
229            system_video_lock_flag,
230            video_bound,
231            packet_rate_restriction_flag,
232            reserved_bits,
233            std_buffer_bounds,
234            trailing_reserved: body[pos..].to_vec(),
235        })
236    }
237}
238
239impl SystemHeader {
240    /// Checks that the stream loop and `trailing_reserved` would read back
241    /// unchanged: the parser's loop runs while `nextbits() == '1'` (Table
242    /// 2-40), and `stream_id == 0xB7` selects the 6-byte extension form.
243    fn validate_stream_loop(&self) -> Result<()> {
244        for b in &self.std_buffer_bounds {
245            if b.stream_id & 0x80 == 0 {
246                return Err(Error::InvalidField(broadcast_common::error::InvalidField {
247                    field: "system_header.stream_id",
248                    reason: "stream_id MSB is 0, so the entry would end the stream loop on parse",
249                }));
250            }
251            match b.stream_id_extension {
252                Some(ext) => {
253                    if b.stream_id != EXT_STREAM_ID {
254                        return Err(Error::InvalidField(broadcast_common::error::InvalidField {
255                            field: "system_header.stream_id_extension",
256                            reason: "stream_id_extension requires stream_id 0xB7",
257                        }));
258                    }
259                    broadcast_common::len::fit_bits(
260                        u64::from(ext),
261                        7,
262                        "system_header.stream_id_extension",
263                    )?;
264                }
265                None => {
266                    if b.stream_id == EXT_STREAM_ID {
267                        return Err(Error::InvalidField(broadcast_common::error::InvalidField {
268                            field: "system_header.stream_id",
269                            reason: "stream_id 0xB7 requires stream_id_extension",
270                        }));
271                    }
272                }
273            }
274            broadcast_common::len::fit_bits(
275                u64::from(b.buffer_size_bound),
276                13,
277                "system_header.P-STD_buffer_size_bound",
278            )?;
279        }
280        if self
281            .trailing_reserved
282            .first()
283            .is_some_and(|f| f & 0x80 != 0)
284        {
285            return Err(Error::InvalidField(broadcast_common::error::InvalidField {
286                field: "system_header.trailing_reserved",
287                reason: "first byte has its MSB set, so it would be re-read as a stream-bound entry",
288            }));
289        }
290        Ok(())
291    }
292}
293
294impl Serialize for SystemHeader {
295    type Error = Error;
296
297    fn serialized_len(&self) -> usize {
298        PREFIX_LEN + 6 + stream_loop_len(self)
299    }
300
301    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
302        let total = self.serialized_len();
303        need_out(buf, total)?;
304
305        // system_header_start_code
306        buf[0..4].copy_from_slice(&SYSTEM_HEADER_START_CODE.to_be_bytes());
307
308        // header_length (bytes after this field)
309        let header_length = broadcast_common::len::fit_u16(
310            6 + stream_loop_len(self),
311            "system_header.header_length",
312        )?;
313        buf[4..6].copy_from_slice(&header_length.to_be_bytes());
314
315        let rate_bound = broadcast_common::len::fit_bits(
316            u64::from(self.rate_bound),
317            22,
318            "system_header.rate_bound",
319        )? as u32;
320        let audio_bound = broadcast_common::len::fit_bits(
321            u64::from(self.audio_bound),
322            6,
323            "system_header.audio_bound",
324        )? as u8;
325        let video_bound = broadcast_common::len::fit_bits(
326            u64::from(self.video_bound),
327            5,
328            "system_header.video_bound",
329        )? as u8;
330        let reserved_bits = broadcast_common::len::fit_mask(
331            self.reserved_bits,
332            RESERVED_BITS_MASK,
333            "system_header.reserved_bits",
334        )?;
335        self.validate_stream_loop()?;
336        // byte 6: marker(1) | rate_bound[21:15](7)
337        buf[6] = 0x80 | ((rate_bound >> 15) & 0x7F) as u8;
338        // byte 7: rate_bound[14:7]
339        buf[7] = ((rate_bound >> 7) & 0xFF) as u8;
340        // byte 8: rate_bound[6:0](7) | marker(1)
341        buf[8] = (((rate_bound & 0x7F) as u8) << 1) | 0x01;
342        // byte 9: audio_bound[5:0](6) | fixed_flag(1) | CSPS_flag(1)
343        buf[9] = audio_bound << 2 | (u8::from(self.fixed_flag) << 1) | u8::from(self.csps_flag);
344        // byte 10: system_audio_lock_flag(1) | system_video_lock_flag(1) | marker(1) | video_bound[4:0](5)
345        buf[10] = (u8::from(self.system_audio_lock_flag) << 7)
346            | (u8::from(self.system_video_lock_flag) << 6)
347            | 0x20 // marker_bit (bit5 of byte 10)
348            | video_bound;
349        // byte 11: packet_rate_restriction_flag(1) | reserved(7)
350        buf[11] = (u8::from(self.packet_rate_restriction_flag) << 7) | reserved_bits;
351
352        // Stream loop
353        let mut pos = 12;
354        for bound in &self.std_buffer_bounds {
355            if let Some(ext) = bound.stream_id_extension {
356                buf[pos] = bound.stream_id;
357                buf[pos + 1] = 0xC0;
358                buf[pos + 2] = ext;
359                buf[pos + 3] = 0xB6;
360                buf[pos + 4] = 0xC0
361                    | (u8::from(bound.buffer_bound_scale) << 5)
362                    | (bound.buffer_size_bound >> 8) as u8;
363                buf[pos + 5] = (bound.buffer_size_bound & 0xFF) as u8;
364                pos += 6;
365            } else {
366                buf[pos] = bound.stream_id;
367                buf[pos + 1] = 0xC0
368                    | (u8::from(bound.buffer_bound_scale) << 5)
369                    | (bound.buffer_size_bound >> 8) as u8;
370                buf[pos + 2] = (bound.buffer_size_bound & 0xFF) as u8;
371                pos += 3;
372            }
373        }
374        buf[pos..pos + self.trailing_reserved.len()].copy_from_slice(&self.trailing_reserved);
375
376        Ok(total)
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use alloc::vec;
384
385    /// #2269: a too-small output buffer is an output-buffer error, not a
386    /// parse-side input-truncation error.
387    #[test]
388    fn serialize_into_too_small_output_reports_output_buffer_error() {
389        let bytes = vec![
390            0x00, 0x00, 0x01, 0xBB, // start_code
391            0x00, 0x06, // header_length = 6 (just the fixed part)
392            0x80, 0x00, 0x01, // rate_bound=0, markers
393            0x04, // audio_bound=1, fixed=0, CSPS=0
394            0x20, // audio_lock=0, video_lock=0, marker=1, video_bound=0
395            0xFF, // packet_rate=1, reserved=0x7F
396        ];
397        let h = SystemHeader::parse(&bytes).unwrap();
398        let mut out = vec![0u8; h.serialized_len() - 1];
399        assert!(matches!(
400            h.serialize_into(&mut out),
401            Err(Error::OutputBufferTooSmall(_))
402        ));
403    }
404
405    #[test]
406    fn system_header_round_trip_no_streams() {
407        let bytes = vec![
408            0x00, 0x00, 0x01, 0xBB, // start_code
409            0x00, 0x06, // header_length = 6 (just the fixed part)
410            0x80, 0x00, 0x01, // rate_bound=0, markers
411            0x04, // audio_bound=1, fixed=0, CSPS=0
412            0x20, // audio_lock=0, video_lock=0, marker=1, video_bound=0
413            0xFF, // packet_rate=1, reserved=0x7F
414        ];
415        let h = SystemHeader::parse(&bytes).unwrap();
416        assert_eq!(h.rate_bound, 0);
417        assert_eq!(h.audio_bound, 1);
418        assert!(!h.fixed_flag);
419        assert!(!h.csps_flag);
420        assert!(!h.system_audio_lock_flag);
421        assert!(!h.system_video_lock_flag);
422        assert_eq!(h.video_bound, 0);
423        assert!(h.packet_rate_restriction_flag);
424        assert!(h.std_buffer_bounds.is_empty());
425
426        let mut out = vec![0u8; h.serialized_len()];
427        h.serialize_into(&mut out).unwrap();
428        assert_eq!(&out[..], &bytes[..]);
429
430        let h2 = SystemHeader::parse(&out).unwrap();
431        assert_eq!(h, h2);
432    }
433
434    /// W1 (#1119): `video_bound` (byte 4 bits `[4:0]`, Table 2-40) was
435    /// masked with `0x0F` (4 bits) on parse but `0x1F` (5 bits) on
436    /// serialize. `video_bound` in `16..=31` sets bit 4, which the old
437    /// parse dropped — `video_bound - 16` came back instead.
438    #[test]
439    fn video_bound_bit_4_round_trips() {
440        let bytes = vec![
441            0x00,
442            0x00,
443            0x01,
444            0xBB, // start_code
445            0x00,
446            0x06, // header_length = 6
447            0x80,
448            0x00,
449            0x01,        // rate_bound=0, markers
450            0x04,        // audio_bound=1, fixed=0, CSPS=0
451            0x20 | 0x1F, // audio_lock=0, video_lock=0, marker=1, video_bound=31 (bit 4 set)
452            0xFF,
453        ];
454        let h = SystemHeader::parse(&bytes).unwrap();
455        assert_eq!(
456            h.video_bound, 31,
457            "bit 4 of video_bound must not be dropped"
458        );
459
460        let mut out = vec![0u8; h.serialized_len()];
461        h.serialize_into(&mut out).unwrap();
462        assert_eq!(&out[..], &bytes[..]);
463    }
464
465    #[test]
466    fn system_header_round_trip_with_streams() {
467        let bytes = vec![
468            0x00, 0x00, 0x01, 0xBB, 0x00,
469            0x0C, // header_length = 12 (6 fixed + 6 for 2 streams)
470            0x80, 0x00, 0x01, 0x04, 0x20, 0xFF,
471            // stream 1: stream_id=0xE0, scale=1, size=0x1FFF
472            0xE0, 0xFF, 0xFF, // stream 2: stream_id=0xC0, scale=0, size=0x0100
473            0xC0, 0xC1, 0x00,
474        ];
475        let h = SystemHeader::parse(&bytes).unwrap();
476        assert_eq!(h.std_buffer_bounds.len(), 2);
477        assert_eq!(h.std_buffer_bounds[0].stream_id, 0xE0);
478        assert!(h.std_buffer_bounds[0].stream_id_extension.is_none());
479        assert!(h.std_buffer_bounds[0].buffer_bound_scale);
480        assert_eq!(h.std_buffer_bounds[0].buffer_size_bound, 0x1FFF);
481
482        assert_eq!(h.std_buffer_bounds[1].stream_id, 0xC0);
483        assert!(h.std_buffer_bounds[1].stream_id_extension.is_none());
484        assert!(!h.std_buffer_bounds[1].buffer_bound_scale);
485        assert_eq!(h.std_buffer_bounds[1].buffer_size_bound, 0x0100);
486
487        let mut out = vec![0u8; h.serialized_len()];
488        h.serialize_into(&mut out).unwrap();
489        assert_eq!(&out[..], &bytes[..]);
490
491        let h2 = SystemHeader::parse(&out).unwrap();
492        assert_eq!(h, h2);
493
494        // Mutation test
495        let h_mut = SystemHeader {
496            rate_bound: 12345,
497            ..h.clone()
498        };
499        let mut out2 = vec![0u8; h_mut.serialized_len()];
500        h_mut.serialize_into(&mut out2).unwrap();
501        assert_ne!(&out[..], &out2[..]);
502    }
503
504    /// r14-MPS-W8 (#1119): padding that `header_length` declares after the
505    /// stream-bound loop is preserved, not dropped (which shrank
506    /// `header_length` on re-serialize).
507    #[test]
508    fn trailing_bytes_after_stream_loop_round_trip() {
509        let bytes = vec![
510            0x00,
511            0x00,
512            0x01,
513            0xBB, // start_code
514            0x00,
515            0x0C, // header_length = 12: 6 fixed + 3 stream entry + 3 trailing
516            0x80,
517            0x00,
518            0x01,
519            0x04,
520            0x20 | 0x01,
521            0xFF, // fixed body
522            0xE0,
523            0xE0,
524            0x80, // one stream entry: stream_id 0xE0, bound
525            0x00,
526            0x7F,
527            0xAB, // trailing, not a stream entry (MSB clear)
528        ];
529        let h = SystemHeader::parse(&bytes).unwrap();
530        assert_eq!(h.std_buffer_bounds.len(), 1);
531        assert_eq!(h.trailing_reserved, vec![0x00, 0x7F, 0xAB]);
532        let mut out = vec![0u8; h.serialized_len()];
533        h.serialize_into(&mut out).unwrap();
534        assert_eq!(out, bytes);
535    }
536
537    /// r14-MPS-W8: an encoder that wrote `reserved_bits` other than the
538    /// conventional `0x7F` must re-serialize byte-identically.
539    #[test]
540    fn non_conventional_reserved_bits_round_trip() {
541        let bytes = vec![
542            0x00, 0x00, 0x01, 0xBB, // start_code
543            0x00, 0x06, // header_length
544            0x80, 0x00, 0x01, // rate_bound=0, markers
545            0x04, // audio_bound=1
546            0x20, // marker=1, video_bound=0
547            0x2A, // packet_rate=0, reserved_bits=0b010_1010
548        ];
549        let h = SystemHeader::parse(&bytes).unwrap();
550        assert_eq!(h.reserved_bits, 0x2A);
551        assert!(!h.packet_rate_restriction_flag);
552        let mut out = vec![0u8; h.serialized_len()];
553        h.serialize_into(&mut out).unwrap();
554        assert_eq!(out, bytes);
555    }
556
557    /// MPS-W7 (#1129): `stream_loop_len` accumulated in `u16`, overflowing
558    /// (debug panic / release wrap) for more than 10 921 extension-form
559    /// bounds (6 bytes each: true total 66 006 bytes > `u16::MAX`). The old
560    /// accumulator's overflow point is the same 65 535 boundary the wire
561    /// field itself has, so the only fixable difference is *how* an
562    /// over-limit count fails: the old code panicked (arithmetic overflow in
563    /// debug; an out-of-bounds buffer write in release, because
564    /// `serialized_len()` used the same wrapped, too-small value the loop
565    /// then wrote past). The fix must return a controlled `Err` here
566    /// instead — never panic. Confirmed pre-fix: this exact case panicked
567    /// with "attempt to add with overflow".
568    #[test]
569    fn many_bounds_past_old_u16_overflow_point_errs_not_panics() {
570        let std_buffer_bounds = (0..11_000)
571            .map(|_| StdBufferBound {
572                stream_id: EXT_STREAM_ID,
573                stream_id_extension: Some(0x05),
574                buffer_bound_scale: false,
575                buffer_size_bound: 0,
576            })
577            .collect::<Vec<_>>();
578        let h = SystemHeader {
579            rate_bound: 0,
580            audio_bound: 0,
581            fixed_flag: false,
582            csps_flag: false,
583            system_audio_lock_flag: false,
584            system_video_lock_flag: false,
585            video_bound: 0,
586            packet_rate_restriction_flag: false,
587            reserved_bits: RESERVED_BITS_MASK,
588            std_buffer_bounds,
589            trailing_reserved: Vec::new(),
590        };
591        // Must not panic building the (correctly-sized-in-usize) buffer or
592        // writing into it; the real total exceeds u16::MAX so this is a
593        // controlled Err, not a silently wrapped Ok.
594        let mut buf = vec![0u8; h.serialized_len()];
595        assert!(matches!(
596            h.serialize_into(&mut buf),
597            Err(Error::FieldOverflow(_))
598        ));
599    }
600
601    /// A bound count whose real `header_length` exceeds the 16-bit wire
602    /// field (`6 + 25_000*3 = 75_006 > 65_535`) must error rather than wrap.
603    #[test]
604    fn header_length_over_u16_is_rejected_not_wrapped() {
605        let std_buffer_bounds = (0..25_000)
606            .map(|_| StdBufferBound {
607                stream_id: 0xC0,
608                stream_id_extension: None,
609                buffer_bound_scale: false,
610                buffer_size_bound: 0,
611            })
612            .collect::<Vec<_>>();
613        let h = SystemHeader {
614            rate_bound: 0,
615            audio_bound: 0,
616            fixed_flag: false,
617            csps_flag: false,
618            system_audio_lock_flag: false,
619            system_video_lock_flag: false,
620            video_bound: 0,
621            packet_rate_restriction_flag: false,
622            reserved_bits: RESERVED_BITS_MASK,
623            std_buffer_bounds,
624            trailing_reserved: Vec::new(),
625        };
626        let mut buf = vec![0u8; h.serialized_len()];
627        assert!(matches!(
628            h.serialize_into(&mut buf),
629            Err(Error::FieldOverflow(_))
630        ));
631    }
632
633    /// The boundary: a bound count whose `header_length` still fits u16
634    /// serializes and round-trips.
635    #[test]
636    fn many_bounds_within_u16_header_length_round_trips() {
637        let std_buffer_bounds = (0..1000)
638            .map(|_| StdBufferBound {
639                stream_id: 0xC0,
640                stream_id_extension: None,
641                buffer_bound_scale: false,
642                buffer_size_bound: 0x0100,
643            })
644            .collect::<Vec<_>>();
645        let h = SystemHeader {
646            rate_bound: 0,
647            audio_bound: 0,
648            fixed_flag: false,
649            csps_flag: false,
650            system_audio_lock_flag: false,
651            system_video_lock_flag: false,
652            video_bound: 0,
653            packet_rate_restriction_flag: false,
654            reserved_bits: RESERVED_BITS_MASK,
655            std_buffer_bounds,
656            trailing_reserved: Vec::new(),
657        };
658        let mut buf = vec![0u8; h.serialized_len()];
659        h.serialize_into(&mut buf).unwrap();
660        let back = SystemHeader::parse(&buf).unwrap();
661        assert_eq!(back.std_buffer_bounds.len(), 1000);
662    }
663
664    #[test]
665    fn system_header_round_trip_extended_stream_id() {
666        let bytes = vec![
667            0x00, 0x00, 0x01, 0xBB, 0x00, 0x0C, // header_length = 12
668            0x80, 0x00, 0x01, 0x04, 0x20, 0xFF,
669            // extended stream: stream_id=0xB7, ext=0x05, scale=1, size=0x0100
670            0xB7, 0xC0, 0x05, 0xB6, 0xE1, 0x00,
671        ];
672        let h = SystemHeader::parse(&bytes).unwrap();
673        assert_eq!(h.std_buffer_bounds.len(), 1);
674        assert_eq!(h.std_buffer_bounds[0].stream_id, 0xB7);
675        assert_eq!(h.std_buffer_bounds[0].stream_id_extension, Some(0x05));
676        assert!(h.std_buffer_bounds[0].buffer_bound_scale);
677        assert_eq!(h.std_buffer_bounds[0].buffer_size_bound, 0x100);
678
679        let mut out = vec![0u8; h.serialized_len()];
680        h.serialize_into(&mut out).unwrap();
681        assert_eq!(&out[..], &bytes[..]);
682    }
683
684    fn ext_entry_header(entry: [u8; 6]) -> Vec<u8> {
685        let mut bytes = vec![
686            0x00, 0x00, 0x01, 0xBB, 0x00, 0x0C, 0x80, 0x00, 0x01, 0x04, 0x20, 0xFF,
687        ];
688        bytes.extend_from_slice(&entry);
689        bytes
690    }
691
692    #[test]
693    fn a_bad_stream_id_extension_prefix_is_reported_with_the_offending_byte() {
694        assert_eq!(
695            SystemHeader::parse(&ext_entry_header([0xB7, 0x00, 0x00, 0xB6, 0xC0, 0x00])),
696            Err(Error::BadStreamIdExtensionPrefix(0x00))
697        );
698        assert_eq!(
699            SystemHeader::parse(&ext_entry_header([0xB7, 0xC0, 0x00, 0x11, 0xC0, 0x00])),
700            Err(Error::BadStreamIdExtensionPrefix(0x11))
701        );
702    }
703
704    fn bare_header() -> SystemHeader {
705        let bytes = vec![
706            0x00, 0x00, 0x01, 0xBB, 0x00, 0x06, 0x80, 0x00, 0x01, 0x04, 0x20, 0xFF,
707        ];
708        SystemHeader::parse(&bytes).unwrap()
709    }
710
711    fn serialize_error(h: &SystemHeader) -> Error {
712        let mut out = vec![0u8; h.serialized_len()];
713        h.serialize_into(&mut out).unwrap_err()
714    }
715
716    fn invalid(field: &'static str, reason: &'static str) -> Error {
717        Error::InvalidField(broadcast_common::error::InvalidField { field, reason })
718    }
719
720    #[test]
721    fn a_stream_loop_that_would_not_read_back_is_an_invalid_field() {
722        let bound = |stream_id, stream_id_extension| StdBufferBound {
723            stream_id,
724            stream_id_extension,
725            buffer_bound_scale: false,
726            buffer_size_bound: 1,
727        };
728        let mut h = bare_header();
729        h.std_buffer_bounds = vec![bound(0x7F, None)];
730        assert_eq!(
731            serialize_error(&h),
732            invalid(
733                "system_header.stream_id",
734                "stream_id MSB is 0, so the entry would end the stream loop on parse"
735            )
736        );
737        h.std_buffer_bounds = vec![bound(0xE0, Some(1))];
738        assert_eq!(
739            serialize_error(&h),
740            invalid(
741                "system_header.stream_id_extension",
742                "stream_id_extension requires stream_id 0xB7"
743            )
744        );
745        h.std_buffer_bounds = vec![bound(0xB7, None)];
746        assert_eq!(
747            serialize_error(&h),
748            invalid(
749                "system_header.stream_id",
750                "stream_id 0xB7 requires stream_id_extension"
751            )
752        );
753        h.std_buffer_bounds = Vec::new();
754        h.trailing_reserved = vec![0x80];
755        assert_eq!(
756            serialize_error(&h),
757            invalid(
758                "system_header.trailing_reserved",
759                "first byte has its MSB set, so it would be re-read as a stream-bound entry"
760            )
761        );
762    }
763}