Skip to main content

dvb_bbframe/
header.rs

1//! BBHEADER (Base-Band Header) parser and builder.
2//!
3//! Supports both Normal Mode (NM) and High Efficiency Mode (HEM)
4//! per EN 302 755 v1.4.1 §5.1.7.
5
6use broadcast_common::{Parse, Serialize};
7use num_enum::TryFromPrimitive;
8
9use crate::crc::crc8;
10use crate::error::Error;
11use crate::issy::{Issy, decode_issy_long, decode_issy_short};
12
13/// Total bytes in a BBHEADER.
14pub const BBHEADER_LEN: usize = 10;
15/// Loosest valid DFL upper bound in bits across the standards this crate parses.
16///
17/// DVB-S2 normal FECFRAME caps the data field near 64800 bits; DVB-T2 is tighter
18/// (EN 302 755 Table 2: DFL in [0, 53760]). A BBHEADER does not by itself say
19/// which standard produced it, so this generous bound avoids rejecting any valid
20/// S2/S2X/T2 frame.
21pub const DFL_MAX_BITS: u16 = 64800;
22
23/// Input stream format as described by the TS/GS field (MATYPE-1 bits `[7:6]`).
24#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26#[repr(u8)]
27#[non_exhaustive]
28pub enum TsGs {
29    /// Generic Packetized Stream.
30    Gfps = 0b00,
31    /// Transport Stream (MPEG-2 TS, 188-byte packets).
32    Ts = 0b11,
33    /// Generic Continuous Stream.
34    Gcs = 0b01,
35    /// Generic Encapsulated Stream.
36    Gse = 0b10,
37}
38
39impl From<TsGs> for u8 {
40    fn from(t: TsGs) -> Self {
41        t as u8
42    }
43}
44
45impl From<num_enum::TryFromPrimitiveError<TsGs>> for Error {
46    fn from(e: num_enum::TryFromPrimitiveError<TsGs>) -> Self {
47        Error::UnsupportedTsGs { ts_gs: e.number }
48    }
49}
50
51impl TsGs {
52    #[must_use]
53    /// Human-readable spec display name.
54    pub fn name(&self) -> &'static str {
55        match self {
56            Self::Gfps => "GFPS",
57            Self::Ts => "TS",
58            Self::Gcs => "GCS",
59            Self::Gse => "GSE",
60        }
61    }
62}
63
64broadcast_common::impl_spec_display!(TsGs);
65
66/// Operating mode: Normal or High Efficiency.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize))]
69#[repr(u8)]
70#[non_exhaustive]
71pub enum Mode {
72    /// Normal Mode — UPL/SYNC/SYNCD present, CRC-8 per UP.
73    Normal = 0,
74    /// High Efficiency Mode — ISSY replaces UPL/SYNC, no per-UP CRC-8.
75    HighEfficiency = 1,
76}
77
78impl From<num_enum::TryFromPrimitiveError<Mode>> for Error {
79    fn from(e: num_enum::TryFromPrimitiveError<Mode>) -> Self {
80        Error::InvalidMode { mode: e.number }
81    }
82}
83
84impl Mode {
85    #[must_use]
86    /// Human-readable spec display name.
87    pub fn name(&self) -> &'static str {
88        match self {
89            Self::Normal => "Normal",
90            Self::HighEfficiency => "High Efficiency",
91        }
92    }
93}
94
95broadcast_common::impl_spec_display!(Mode);
96
97/// The pair of MATYPE bytes describing the input stream format and mode adaptation.
98///
99/// Per EN 302 755 Table 1:
100/// - MATYPE-1 (byte 0): TS/GS, SIS/MIS, CCM/ACM, ISSYI, NPD, `EXT[1:0]`
101/// - MATYPE-2 (byte 1): ISI (0-255) or reserved
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize))]
104pub struct Matype {
105    /// Input stream format — see [`TsGs`].
106    pub ts_gs: TsGs,
107    /// Single-input stream (true) or multi-input stream (false).
108    pub sis: bool,
109    /// Constant coding and modulation (true) or adaptive (false).
110    pub ccm: bool,
111    /// Input Stream Synchronization Indicator — ISSY field is active.
112    pub issyi: bool,
113    /// Null Packet Deletion is active.
114    pub npd: bool,
115    /// Extension bits — RO in DVB-S2, reserved in DVB-T2.
116    pub ext: u8,
117    /// Input Stream Identifier — meaningful only if `sis == false`.
118    pub isi: u8,
119}
120
121impl Matype {
122    const MASK_TS_GS: u8 = 0xC0;
123    const MASK_SIS: u8 = 0x20;
124    const MASK_CCM: u8 = 0x10;
125    const MASK_ISSYI: u8 = 0x08;
126    const MASK_NPD: u8 = 0x04;
127    const MASK_EXT: u8 = 0x03;
128}
129
130/// Roll-off factor encoded in MATYPE-1 EXT bits `[1:0]` (DVB-S2/S2X context).
131///
132/// EN 302 307 / EN 302 755 — roll-off is signalled in the MATYPE-1 extension
133/// bits when the TS/GS field indicates TS or GFPS.  In DVB-T2 the EXT field is
134/// reserved.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize))]
137#[non_exhaustive]
138pub enum RollOff {
139    /// 0b00 — α0.35 (0.35).
140    Alpha035,
141    /// 0b01 — α0.25 (0.25).
142    Alpha025,
143    /// 0b10 — α0.20 (0.20).
144    Alpha020,
145    /// 0b11 — reserved / S2X low roll-off.
146    Reserved,
147}
148
149impl RollOff {
150    /// Decode from 2-bit EXT field.
151    /// Decode from 2-bit EXT field.
152    #[must_use]
153    pub fn from_bits(bits: u8) -> Self {
154        match bits & 0x03 {
155            0 => Self::Alpha035,
156            1 => Self::Alpha025,
157            2 => Self::Alpha020,
158            _ => Self::Reserved,
159        }
160    }
161
162    /// Encode to 2-bit value.
163    /// Encode to 2-bit value.
164    #[must_use]
165    pub fn to_bits(self) -> u8 {
166        match self {
167            Self::Alpha035 => 0,
168            Self::Alpha025 => 1,
169            Self::Alpha020 => 2,
170            Self::Reserved => 3,
171        }
172    }
173
174    /// Human-readable roll-off label.
175    /// Human-readable spec display name.
176    #[must_use]
177    pub fn name(self) -> &'static str {
178        match self {
179            Self::Alpha035 => "α=0.35",
180            Self::Alpha025 => "α=0.25",
181            Self::Alpha020 => "α=0.20",
182            Self::Reserved => "reserved/S2X-low",
183        }
184    }
185}
186
187broadcast_common::impl_spec_display!(RollOff);
188
189impl Matype {
190    /// Decode the roll-off factor from the EXT bits `[1:0]`.
191    ///
192    /// Meaningful in DVB-S2/S2X context; reserved in DVB-T2.
193    #[must_use]
194    pub fn roll_off(&self) -> RollOff {
195        RollOff::from_bits(self.ext & 0x03)
196    }
197}
198
199impl TryFrom<[u8; 2]> for Matype {
200    type Error = Error;
201
202    fn try_from(bytes: [u8; 2]) -> Result<Self, Self::Error> {
203        let matype1 = bytes[0];
204        let matype2 = bytes[1];
205
206        let ts_gs = TsGs::try_from((matype1 & Matype::MASK_TS_GS) >> 6)?;
207        let sis = matype1 & Matype::MASK_SIS != 0;
208        let ccm = matype1 & Matype::MASK_CCM != 0;
209        let issyi = matype1 & Matype::MASK_ISSYI != 0;
210        let npd = matype1 & Matype::MASK_NPD != 0;
211        let ext = matype1 & Matype::MASK_EXT;
212
213        Ok(Matype {
214            ts_gs,
215            sis,
216            ccm,
217            issyi,
218            npd,
219            ext,
220            isi: matype2,
221        })
222    }
223}
224
225impl From<Matype> for [u8; 2] {
226    fn from(m: Matype) -> Self {
227        // MATYPE-1 layout per EN 302 755 Table 1:
228        //   bits 7..6 TS/GS, bit 5 SIS/MIS, bit 4 CCM/ACM,
229        //   bit 3 ISSYI, bit 2 NPD, bits 1..0 EXT.
230        let mut matype1: u8 = 0;
231        matype1 |= (u8::from(m.ts_gs) << 6) & Matype::MASK_TS_GS;
232        if m.sis {
233            matype1 |= Matype::MASK_SIS;
234        }
235        if m.ccm {
236            matype1 |= Matype::MASK_CCM;
237        }
238        if m.issyi {
239            matype1 |= Matype::MASK_ISSYI;
240        }
241        if m.npd {
242            matype1 |= Matype::MASK_NPD;
243        }
244        matype1 |= m.ext & Matype::MASK_EXT;
245        [matype1, m.isi]
246    }
247}
248
249/// Parsed 10-byte BBHEADER.
250///
251/// Fields vary based on [`Mode`]:
252/// - **NM** (MODE=0): `upl`, `sync` are from the header; `issy_in_header` is None.
253/// - **HEM** (MODE=1): `upl` and `sync` are both zero; `issy_in_header` carries the
254///   3 ISSY bytes that reuse the NM UPL/SYNC layout.
255///
256/// Detection: `crc8(bytes[0..9]) ^ bytes[9]` yields 0 for NM, 1 for HEM.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258#[cfg_attr(feature = "serde", derive(serde::Serialize))]
259pub struct Bbheader {
260    /// MATYPE field.
261    pub matype: Matype,
262    /// User Packet Length in bits (NM only; 0 in HEM).
263    pub upl: u16,
264    /// Copy of the User Packet Sync-byte (NM only; 0 in HEM).
265    pub sync: u8,
266    /// Data Field Length in bits.
267    pub dfl: u16,
268    /// Distance in bits from DATA FIELD start to the first complete UP beginning.
269    pub syncd: u16,
270    /// Detected mode (NM vs HEM).
271    pub mode: Mode,
272    /// 3-byte ISSY from header bytes (HEM only; None in NM).
273    pub issy_in_header: Option<[u8; 3]>,
274}
275
276impl<'a> Parse<'a> for Bbheader {
277    type Error = Error;
278
279    fn parse(bytes: &'a [u8]) -> Result<Self, Self::Error> {
280        let (hdr, _) = bytes
281            .split_first_chunk::<BBHEADER_LEN>()
282            .ok_or(Error::BufferTooShort {
283                need: BBHEADER_LEN,
284                have: bytes.len(),
285                what: "BBHEADER",
286            })?;
287
288        let matype_bytes = [hdr[0], hdr[1]];
289        let matype = Matype::try_from(matype_bytes)?;
290        let dfl = u16::from_be_bytes([hdr[4], hdr[5]]);
291        let syncd = u16::from_be_bytes([hdr[7], hdr[8]]);
292        let crc_stored = hdr[9];
293
294        if dfl > DFL_MAX_BITS {
295            return Err(Error::DflOutOfRange {
296                dfl,
297                max: DFL_MAX_BITS,
298            });
299        }
300
301        // Mode detection per EN 302 755 §5.1.7: the byte on the wire is
302        // `crc8(bytes[0..9]) XOR MODE` (MODE: 0 = NM, 1 = HEM). The XOR is
303        // itself the integrity check — corruption that lands `mode_val`
304        // outside {0, 1} is rejected by `Mode::try_from` as InvalidMode; a
305        // residual flip into the other valid mode is undetectable by design
306        // of the spec's scheme (there is no separate "HEM CRC init").
307        let computed_crc = crc8(&hdr[..9]);
308        let mode_val = computed_crc ^ crc_stored;
309        let mode = Mode::try_from(mode_val)?;
310
311        let (upl, sync, issy_in_header) = match mode {
312            Mode::Normal => (u16::from_be_bytes([hdr[2], hdr[3]]), hdr[6], None),
313            Mode::HighEfficiency => {
314                // In HEM, bytes[2..4] are ISSY_2MSB, byte[6] is ISSY_1LSB —
315                // UPL and SYNC are repurposed for ISSY.
316                (0, 0, Some([hdr[2], hdr[3], hdr[6]]))
317            }
318        };
319
320        Ok(Bbheader {
321            matype,
322            upl,
323            sync,
324            dfl,
325            syncd,
326            mode,
327            issy_in_header,
328        })
329    }
330}
331
332impl Serialize for Bbheader {
333    type Error = Error;
334
335    fn serialized_len(&self) -> usize {
336        BBHEADER_LEN
337    }
338
339    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, Self::Error> {
340        if buf.len() < BBHEADER_LEN {
341            return Err(Error::OutputBufferTooSmall {
342                need: BBHEADER_LEN,
343                have: buf.len(),
344            });
345        }
346
347        let ma = <[u8; 2]>::from(self.matype);
348        buf[0] = ma[0];
349        buf[1] = ma[1];
350
351        match self.mode {
352            Mode::Normal => {
353                let upl = self.upl.to_be_bytes();
354                buf[2] = upl[0];
355                buf[3] = upl[1];
356                let dfl = self.dfl.to_be_bytes();
357                buf[4] = dfl[0];
358                buf[5] = dfl[1];
359                buf[6] = self.sync;
360                let syncd = self.syncd.to_be_bytes();
361                buf[7] = syncd[0];
362                buf[8] = syncd[1];
363            }
364            Mode::HighEfficiency => {
365                if let Some(issy) = self.issy_in_header {
366                    buf[2] = issy[0];
367                    buf[3] = issy[1];
368                    let dfl = self.dfl.to_be_bytes();
369                    buf[4] = dfl[0];
370                    buf[5] = dfl[1];
371                    buf[6] = issy[2];
372                    let syncd = self.syncd.to_be_bytes();
373                    buf[7] = syncd[0];
374                    buf[8] = syncd[1];
375                } else {
376                    // No ISSY in header: explicitly zero the three ISSY positions
377                    // so serialize_into is fully deterministic regardless of whether
378                    // the caller's buffer was zero-initialised or not. Without this,
379                    // stale bytes at buf[2], buf[3], buf[6] corrupt the CRC-8.
380                    buf[2] = 0;
381                    buf[3] = 0;
382                    let dfl = self.dfl.to_be_bytes();
383                    buf[4] = dfl[0];
384                    buf[5] = dfl[1];
385                    buf[6] = 0;
386                    let syncd = self.syncd.to_be_bytes();
387                    buf[7] = syncd[0];
388                    buf[8] = syncd[1];
389                }
390            }
391        }
392
393        // CRC-8 = crc8(bytes[0..9], init=0x00) XOR MODE
394        let computed = crc8(&buf[..9]);
395        buf[9] = computed ^ (self.mode as u8);
396
397        Ok(BBHEADER_LEN)
398    }
399}
400
401impl Bbheader {
402    /// Parse a 10-byte BBHEADER, detecting NM vs HEM automatically.
403    ///
404    /// Mode detection per EN 302 755 §5.1.7:
405    /// `mode = crc8(bytes[0..9]) ^ bytes[9]` (0 = NM, 1 = HEM).
406    /// Values other than 0 or 1 return `Error::InvalidMode`.
407    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
408        <Self as Parse>::parse(bytes)
409    }
410
411    /// Serialize the BBHEADER back to its 10-byte wire format.
412    pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
413        let v = <Self as Serialize>::to_bytes(self);
414        let mut buf = [0u8; BBHEADER_LEN];
415        buf.copy_from_slice(&v);
416        buf
417    }
418
419    /// Decode the ISSY field from the header bytes (HEM only).
420    ///
421    /// In HEM the 3-byte ISSY occupies the UPL/SYNC positions. This method
422    /// tries [`decode_issy_long`] first (the common case for 3-byte ISSY);
423    /// if that fails because the form bit indicates a short-form ISSY,
424    /// falls back to [`decode_issy_short`] on the first 2 bytes.
425    ///
426    /// Returns `None` in Normal Mode (no ISSY in header) or if decoding
427    /// produces an unexpected error.
428    #[must_use]
429    pub fn issy(&self) -> Option<Issy> {
430        let bytes = self.issy_in_header?;
431        decode_issy_long(bytes)
432            .ok()
433            .or_else(|| decode_issy_short([bytes[0], bytes[1]]).ok())
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn parse_rejects_buffer_shorter_than_10() {
443        assert!(Bbheader::parse(&[0u8; 9]).is_err());
444    }
445
446    #[test]
447    fn parse_nm_ts_extracts_all_fields() {
448        // Craft a valid NM BBHEADER with known values.
449        // MATYPE-1 = 0xF0: TS input (0b11), SIS (1), CCM (1), ISSYI (0), NPD (0), EXT (00)
450        // MATYPE-2 = 0x00 (single stream)
451        // UPL = 0x0718 = 1816 bits (188*8 - CRC-8 - sync = 1504-8 = 1496... let me just pick a value)
452        // DFL = 0xBC00 = 50304-50432? Let me pick simpler values.
453        let mut hdr = [0u8; BBHEADER_LEN];
454        hdr[0] = 0xF0; // MATYPE-1: TS, SIS, CCM
455        hdr[1] = 0x00; // MATYPE-2: not MIS
456        let upl: u16 = 0x07D0; // 2000 bits = 250 bytes
457        hdr[2..4].copy_from_slice(&upl.to_be_bytes());
458        let dfl: u16 = 0xBC00; // 48320 bits
459        hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
460        hdr[6] = 0x47; // SYNC byte
461        let syncd: u16 = 0x0000; // First UP aligned
462        hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
463        hdr[9] = crc8(&hdr[..9]); // CRC-8
464
465        let result = Bbheader::parse(&hdr).unwrap();
466        assert_eq!(result.mode, Mode::Normal);
467        assert_eq!(result.matype.ts_gs, TsGs::Ts);
468        assert!(result.matype.sis);
469        assert!(result.matype.ccm);
470        assert!(!result.matype.issyi);
471        assert!(!result.matype.npd);
472        assert_eq!(result.matype.ext, 0);
473        assert_eq!(result.matype.isi, 0x00);
474        assert_eq!(result.upl, upl);
475        assert_eq!(result.sync, 0x47);
476        assert_eq!(result.dfl, dfl);
477        assert_eq!(result.syncd, syncd);
478    }
479
480    #[test]
481    fn parse_nm_gcs_treats_sync_as_transport_protocol_byte() {
482        let mut hdr = [0u8; BBHEADER_LEN];
483        hdr[0] = 0x50; // MATYPE-1: GCS (0b01), SIS, CCM, ISSYI=0, NPD=0, EXT=00
484        hdr[1] = 0x00;
485        let upl: u16 = 0x0000; // GCS: UPL=0
486        hdr[2..4].copy_from_slice(&upl.to_be_bytes());
487        let dfl: u16 = 0x4000; // 16384 bits
488        hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
489        hdr[6] = 0x3C; // GCS: SYNC=0x00-0xB8 for protocol signalling
490        let syncd: u16 = 0x0000;
491        hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
492        hdr[9] = crc8(&hdr[..9]);
493
494        let result = Bbheader::parse(&hdr).unwrap();
495        assert_eq!(result.mode, Mode::Normal);
496        assert_eq!(result.matype.ts_gs, TsGs::Gcs);
497        assert_eq!(result.sync, 0x3C);
498        assert_eq!(result.upl, upl);
499    }
500
501    #[test]
502    fn parse_detects_nm_via_crc_xor_0() {
503        // When crc8(init=0) XOR byte[9] == 0, mode is NM
504        let mut hdr = [0u8; BBHEADER_LEN];
505        hdr[0] = 0xF0;
506        hdr[1] = 0x00;
507        hdr[2] = 0x07;
508        hdr[3] = 0xD0; // UPL
509        hdr[4] = 0xBC;
510        hdr[5] = 0x00; // DFL
511        hdr[6] = 0x47; // SYNC
512        hdr[7] = 0x00;
513        hdr[8] = 0x00; // SYNCD
514        hdr[9] = crc8(&hdr[..9]); // CRC matches init=0x00
515
516        let result = Bbheader::parse(&hdr).unwrap();
517        assert_eq!(result.mode, Mode::Normal);
518    }
519
520    #[test]
521    fn parse_rejects_crc_mismatch_in_both_modes() {
522        let mut hdr = [0u8; BBHEADER_LEN];
523        hdr[0] = 0xF0;
524        hdr[1] = 0x00;
525        hdr[2] = 0x07;
526        hdr[3] = 0xD0;
527        hdr[4] = 0xBC;
528        hdr[5] = 0x00;
529        hdr[6] = 0x47;
530        hdr[7] = 0x00;
531        hdr[8] = 0x00;
532        hdr[9] = 0xFF; // Wrong CRC
533
534        let result = Bbheader::parse(&hdr);
535        assert!(result.is_err());
536    }
537
538    #[test]
539    fn parse_matype_extracts_ts_gs_enum_for_each_of_gfps_ts_gcs_gse() {
540        for (ts_gs_val, expected) in [
541            (0b00, TsGs::Gfps),
542            (0b01, TsGs::Gcs),
543            (0b10, TsGs::Gse),
544            (0b11, TsGs::Ts),
545        ] {
546            let ma1 = (ts_gs_val << 6) | 0x30; // SIS=1, CCM=1, ISSYI=0, NPD=0, EXT=00
547            let mut hdr = [0u8; BBHEADER_LEN];
548            hdr[0] = ma1;
549            hdr[1] = 0x00;
550            hdr[2..9].copy_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
551            hdr[9] = crc8(&hdr[..9]);
552            let result = Bbheader::parse(&hdr).unwrap();
553            assert_eq!(result.matype.ts_gs, expected, "ts_gs=0x{ts_gs_val:02b}");
554        }
555    }
556
557    #[test]
558    fn parse_matype_extracts_sis_isi_on_multi_stream() {
559        // sis = 0 → MIS, isi is MATYPE-2
560        let mut hdr = [0u8; BBHEADER_LEN];
561        hdr[0] = 0xD0; // TS/MIS/CCM -> not SIS
562        hdr[1] = 0xAB; // ISI = 171
563        hdr[2] = 0x07;
564        hdr[3] = 0xD0;
565        hdr[4] = 0xBC;
566        hdr[5] = 0x00;
567        hdr[6] = 0x47;
568        hdr[7] = 0x00;
569        hdr[8] = 0x00;
570        hdr[9] = crc8(&hdr[..9]);
571
572        let result = Bbheader::parse(&hdr).unwrap();
573        assert!(!result.matype.sis);
574        assert_eq!(result.matype.isi, 0xAB);
575    }
576
577    #[test]
578    fn parse_matype_extracts_roll_off_2_bits_as_ext_for_s2_context() {
579        // EXT = 0b11 in NM means reserved/S2X-low roll-off (0b00 is α0.35)
580        let mut hdr = [0u8; BBHEADER_LEN];
581        hdr[0] = 0xF3; // TS/SIS/CCM, no ISSYI, no NPD, EXT=0b11
582        hdr[1] = 0x00;
583        hdr[2] = 0x07;
584        hdr[3] = 0xD0;
585        hdr[4] = 0xBC;
586        hdr[5] = 0x00;
587        hdr[6] = 0x47;
588        hdr[7] = 0x00;
589        hdr[8] = 0x00;
590        hdr[9] = crc8(&hdr[..9]);
591
592        let result = Bbheader::parse(&hdr).unwrap();
593        assert_eq!(result.matype.ext, 0b11);
594    }
595
596    #[test]
597    fn serialize_nm_produces_expected_bytes() {
598        let hdr = Bbheader {
599            matype: Matype {
600                ts_gs: TsGs::Ts,
601                sis: true,
602                ccm: true,
603                issyi: false,
604                npd: false,
605                ext: 0,
606                isi: 0x00,
607            },
608            upl: 188 * 8,
609            sync: 0x47,
610            dfl: 48328,
611            syncd: 0,
612            mode: Mode::Normal,
613            issy_in_header: None,
614        };
615        let buf = hdr.serialize();
616
617        let parsed = Bbheader::parse(&buf).unwrap();
618        assert_eq!(parsed.matype.ts_gs, TsGs::Ts);
619        assert!(parsed.matype.sis);
620        assert!(parsed.matype.ccm);
621        assert_eq!(parsed.upl, 188 * 8);
622        assert_eq!(parsed.sync, 0x47);
623        assert_eq!(parsed.dfl, 48328);
624        assert_eq!(parsed.syncd, 0);
625        assert_eq!(parsed.mode, Mode::Normal);
626    }
627
628    #[test]
629    fn serialize_round_trip_nm_ts_preserves_every_field() {
630        let orig = Bbheader {
631            matype: Matype {
632                ts_gs: TsGs::Ts,
633                sis: true,
634                ccm: true,
635                issyi: true,
636                npd: false,
637                ext: 0,
638                isi: 0x00,
639            },
640            upl: 1504,
641            sync: 0x47,
642            dfl: 48328,
643            syncd: 0,
644            mode: Mode::Normal,
645            issy_in_header: None,
646        };
647        let buf = orig.serialize();
648        let parsed = Bbheader::parse(&buf).unwrap();
649        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
650        assert_eq!(orig.matype.sis, parsed.matype.sis);
651        assert_eq!(orig.matype.ccm, parsed.matype.ccm);
652        assert_eq!(orig.matype.issyi, parsed.matype.issyi);
653        assert_eq!(orig.matype.npd, parsed.matype.npd);
654        assert_eq!(orig.matype.ext, parsed.matype.ext);
655        assert_eq!(orig.matype.isi, parsed.matype.isi);
656        assert_eq!(orig.upl, parsed.upl);
657        assert_eq!(orig.sync, parsed.sync);
658        assert_eq!(orig.dfl, parsed.dfl);
659        assert_eq!(orig.syncd, parsed.syncd);
660        assert_eq!(orig.mode, parsed.mode);
661    }
662
663    #[test]
664    fn serialize_round_trip_nm_gcs() {
665        let orig = Bbheader {
666            matype: Matype {
667                ts_gs: TsGs::Gcs,
668                sis: true,
669                ccm: false,
670                issyi: false,
671                npd: false,
672                ext: 0,
673                isi: 0x00,
674            },
675            upl: 0,
676            sync: 0x00,
677            dfl: 16384,
678            syncd: 0,
679            mode: Mode::Normal,
680            issy_in_header: None,
681        };
682        let buf = orig.serialize();
683        let parsed = Bbheader::parse(&buf).unwrap();
684        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
685        assert_eq!(orig.matype.sis, parsed.matype.sis);
686        assert_eq!(orig.matype.ccm, parsed.matype.ccm);
687        assert_eq!(orig.dfl, parsed.dfl);
688        assert_eq!(orig.syncd, parsed.syncd);
689        assert_eq!(orig.mode, parsed.mode);
690    }
691
692    #[test]
693    fn serialize_crc8_always_matches_bytes_0_to_8() {
694        let hdr = Bbheader {
695            matype: Matype {
696                ts_gs: TsGs::Gse,
697                sis: true,
698                ccm: true,
699                issyi: true,
700                npd: false,
701                ext: 0,
702                isi: 0x00,
703            },
704            upl: 0,
705            sync: 0xFF,
706            dfl: 32768,
707            syncd: 0,
708            mode: Mode::Normal,
709            issy_in_header: None,
710        };
711        let buf = hdr.serialize();
712        let computed = crc8(&buf[..9]);
713        assert_eq!(computed ^ buf[9], 0); // XOR with MODE must give 0 for NM
714        assert_eq!(buf[9], computed); // MODE=0 means they must be equal
715    }
716
717    #[test]
718    fn parse_detects_hem_via_crc_xor_1() {
719        // Real DVB-T2 BBFRAME header from Rai T2-MI. Mode=HEM is detected by crc8(init=0) XOR stored = 1.
720        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
721        let result = Bbheader::parse(&hdr).unwrap();
722        assert_eq!(result.mode, Mode::HighEfficiency);
723    }
724
725    #[test]
726    fn parse_hem_extracts_matype_dfl_syncd() {
727        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
728        let result = Bbheader::parse(&hdr).unwrap();
729        assert_eq!(result.mode, Mode::HighEfficiency);
730        assert_eq!(result.matype.ts_gs, TsGs::Ts);
731        assert!(result.matype.sis);
732        assert!(result.matype.ccm);
733        assert!(result.matype.issyi);
734        assert!(!result.matype.npd);
735        assert_eq!(result.matype.ext, 0);
736        assert_eq!(result.dfl, 48328);
737        assert_eq!(result.syncd, 0x0350);
738    }
739
740    #[test]
741    fn parse_hem_preserves_three_issy_bytes() {
742        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
743        let result = Bbheader::parse(&hdr).unwrap();
744        let issy = result.issy_in_header.unwrap();
745        // ISSY in HEM: bytes[2..4] = ISSY_2MSB, byte[6] = ISSY_1LSB
746        assert_eq!(issy, [0xa4, 0x28, 0xe2]);
747    }
748
749    #[test]
750    fn issy_accessor_decodes_hem_iscr_long() {
751        // Real HEM fixture: ISSY bytes [0xa4, 0x28, 0xe2]
752        // byte0=0xa4, bit7=1 (long form), bit6=0 (ISCR long)
753        // payload = (0xa4 & 0x3F)<<16 | 0x28<<8 | 0xe2 = 0x2428e2
754        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
755        let result = Bbheader::parse(&hdr).unwrap();
756        let issy = result.issy().expect("ISSY decode from HEM header");
757        assert_eq!(issy, Issy::IscrLong(0x0024_28E2));
758    }
759
760    #[test]
761    fn issy_accessor_returns_none_for_nm() {
762        let mut hdr = [0u8; BBHEADER_LEN];
763        hdr[0] = 0xF0;
764        hdr[1] = 0x00;
765        hdr[2] = 0x07;
766        hdr[3] = 0xD0;
767        hdr[4] = 0xBC;
768        hdr[5] = 0x00;
769        hdr[6] = 0x47;
770        hdr[7] = 0x00;
771        hdr[8] = 0x00;
772        hdr[9] = crc8(&hdr[..9]);
773        let result = Bbheader::parse(&hdr).unwrap();
774        assert_eq!(result.mode, Mode::Normal);
775        assert!(result.issy().is_none());
776    }
777
778    #[test]
779    fn issy_accessor_falls_back_to_short_form() {
780        // HEM with ISSY bytes [0x7A, 0xBC, 0x00]: bit7=0 → short form.
781        // decode_issy_long fails, falls back to decode_issy_short([0x7A, 0xBC])
782        // → IscrShort(0x7ABC)
783        let hdr = Bbheader {
784            matype: Matype {
785                ts_gs: TsGs::Ts,
786                sis: true,
787                ccm: true,
788                issyi: true,
789                npd: false,
790                ext: 0,
791                isi: 0x00,
792            },
793            upl: 0,
794            sync: 0,
795            dfl: 50000,
796            syncd: 100,
797            mode: Mode::HighEfficiency,
798            issy_in_header: Some([0x7A, 0xBC, 0x00]),
799        };
800        let issy = hdr.issy().expect("short-form ISSY fallback");
801        assert_eq!(issy, Issy::IscrShort(0x7ABC));
802    }
803
804    #[test]
805    fn parse_hem_leaves_upl_bits_as_zero_and_sync_as_zero() {
806        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
807        let result = Bbheader::parse(&hdr).unwrap();
808        assert_eq!(result.upl, 0);
809        assert_eq!(result.sync, 0);
810    }
811
812    #[test]
813    fn parse_hem_rejects_when_mode_xor_not_0_or_1() {
814        // Create a header where crc8^byte[9] gives 2 (reserved)
815        let mut hdr = [0u8; BBHEADER_LEN];
816        hdr[0] = 0xF0;
817        hdr[1] = 0x00;
818        hdr[2] = 0x00;
819        hdr[3] = 0x00;
820        hdr[4] = 0x00;
821        hdr[5] = 0x00;
822        hdr[6] = 0x00;
823        hdr[7] = 0x00;
824        hdr[8] = 0x00;
825        hdr[9] = crc8(&hdr[..9]) ^ 0x02; // XOR with reserved value 2
826        assert!(Bbheader::parse(&hdr).is_err());
827    }
828
829    #[test]
830    fn parse_same_bytes_different_mode_byte_produces_different_bbheader() {
831        // Two headers that differ only in byte[9] (CRC-8 MODE byte)
832        let mut hdr1 = [0xF8, 0x00, 0x00, 0x00, 0xBC, 0xC8, 0x00, 0x03, 0x50, 0x00];
833        hdr1[9] = crc8(&hdr1[..9]); // NM
834        let mut hdr2 = hdr1;
835        hdr2[9] ^= 0x01; // HEM
836
837        let result1 = Bbheader::parse(&hdr1).unwrap();
838        let result2 = Bbheader::parse(&hdr2).unwrap();
839        assert_eq!(result1.mode, Mode::Normal);
840        assert_eq!(result2.mode, Mode::HighEfficiency);
841    }
842
843    #[test]
844    fn serialize_hem_round_trip() {
845        let orig = Bbheader {
846            matype: Matype {
847                ts_gs: TsGs::Ts,
848                sis: true,
849                ccm: true,
850                issyi: true,
851                npd: false,
852                ext: 0,
853                isi: 0x00,
854            },
855            upl: 0,  // not used in HEM
856            sync: 0, // not used in HEM
857            dfl: 48328,
858            syncd: 848,
859            mode: Mode::HighEfficiency,
860            issy_in_header: Some([0xA4, 0x28, 0xE2]),
861        };
862        let buf = orig.serialize();
863        let parsed = Bbheader::parse(&buf).unwrap();
864        assert_eq!(orig.mode, parsed.mode);
865        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
866        assert_eq!(orig.dfl, parsed.dfl);
867        assert_eq!(orig.syncd, parsed.syncd);
868        assert_eq!(orig.issy_in_header, parsed.issy_in_header);
869    }
870
871    #[test]
872    fn serialize_hem_sets_crc_xor_mode_byte_correctly() {
873        let hdr = Bbheader {
874            matype: Matype {
875                ts_gs: TsGs::Ts,
876                sis: true,
877                ccm: true,
878                issyi: false,
879                npd: false,
880                ext: 0,
881                isi: 0x05,
882            },
883            upl: 0,
884            sync: 0,
885            dfl: 48000,
886            syncd: 0,
887            mode: Mode::HighEfficiency,
888            issy_in_header: Some([0x00, 0x00, 0x00]),
889        };
890        let buf = hdr.serialize();
891        let computed = crc8(&buf[..9]);
892        // MODE=1: stored = computed XOR 1
893        assert_eq!(buf[9], computed ^ 1);
894    }
895
896    #[test]
897    fn serialize_hem_with_issy_bytes_zero_writes_expected_layout() {
898        let hdr = Bbheader {
899            matype: Matype {
900                ts_gs: TsGs::Ts,
901                sis: true,
902                ccm: true,
903                issyi: true,
904                npd: false,
905                ext: 0,
906                isi: 0x00,
907            },
908            upl: 0,
909            sync: 0,
910            dfl: 50000,
911            syncd: 100,
912            mode: Mode::HighEfficiency,
913            issy_in_header: Some([0x00, 0x00, 0x00]),
914        };
915        let buf = hdr.serialize();
916        let parsed = Bbheader::parse(&buf).unwrap();
917        assert_eq!(parsed.mode, Mode::HighEfficiency);
918        assert_eq!(parsed.issy_in_header, Some([0x00, 0x00, 0x00]));
919        assert_eq!(parsed.dfl, 50000);
920        assert_eq!(parsed.syncd, 100);
921    }
922
923    #[test]
924    fn parse_valid_dvbt2_hem_bbframe_rai() {
925        // Real DVB-T2 BBFRAME header from Rai T2-MI (12606V, ISI 5, PLP 0).
926        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
927        assert_eq!(Bbheader::parse(&hdr).unwrap().dfl, 48328);
928    }
929
930    #[test]
931    fn exhaustive_tsgs_sweep() {
932        let mut matched = 0u16;
933        for byte in 0u8..=0xFF {
934            if let Ok(v) = TsGs::try_from(byte) {
935                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
936                matched += 1;
937            }
938        }
939        assert_eq!(matched, 4, "expected 4 matched variants");
940    }
941
942    #[test]
943    fn exhaustive_mode_sweep() {
944        let mut matched = 0u16;
945        for byte in 0u8..=0xFF {
946            if let Ok(v) = Mode::try_from(byte) {
947                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
948                matched += 1;
949            }
950        }
951        assert_eq!(matched, 2, "expected 2 matched variants");
952    }
953
954    #[test]
955    fn trait_parse_and_serialize_round_trip() {
956        let orig = Bbheader {
957            matype: Matype {
958                ts_gs: TsGs::Gse,
959                sis: true,
960                ccm: true,
961                issyi: true,
962                npd: false,
963                ext: 0,
964                isi: 0x00,
965            },
966            upl: 0,
967            sync: 0xFF,
968            dfl: 32768,
969            syncd: 0,
970            mode: Mode::Normal,
971            issy_in_header: None,
972        };
973        let v = <Bbheader as Serialize>::to_bytes(&orig);
974        let parsed = <Bbheader as Parse>::parse(&v).unwrap();
975        assert_eq!(orig.matype, parsed.matype);
976        assert_eq!(orig.upl, parsed.upl);
977        assert_eq!(orig.sync, parsed.sync);
978        assert_eq!(orig.dfl, parsed.dfl);
979        assert_eq!(orig.syncd, parsed.syncd);
980        assert_eq!(orig.mode, parsed.mode);
981    }
982
983    #[test]
984    fn serialize_into_hem_no_issy_is_deterministic_regardless_of_buffer_content() {
985        // BUG 1 regression: HEM + issy_in_header=None leaves buf[2], buf[3], buf[6]
986        // untouched. A pre-filled (0xFF) buffer must produce the same bytes as
987        // to_bytes() which zero-inits.
988        let hdr = Bbheader {
989            matype: Matype {
990                ts_gs: TsGs::Ts,
991                sis: true,
992                ccm: true,
993                issyi: false,
994                npd: false,
995                ext: 0,
996                isi: 0x00,
997            },
998            upl: 0,
999            sync: 0,
1000            dfl: 48000,
1001            syncd: 256,
1002            mode: Mode::HighEfficiency,
1003            issy_in_header: None,
1004        };
1005
1006        // Reference: to_bytes() zero-inits so those bytes come out 0.
1007        let clean = <Bbheader as Serialize>::to_bytes(&hdr);
1008
1009        // Dirty buffer pre-filled with 0xFF — buf[2], buf[3], buf[6] would keep
1010        // 0xFF if the else-branch doesn't explicitly zero them.
1011        let mut dirty = [0xFFu8; BBHEADER_LEN];
1012        hdr.serialize_into(&mut dirty).unwrap();
1013
1014        assert_eq!(
1015            clean.as_slice(),
1016            dirty.as_slice(),
1017            "serialize_into into dirty buffer must produce identical bytes to to_bytes()"
1018        );
1019
1020        // Also verify re-parsing succeeds with correct fields.
1021        // In HEM the parser always fills issy_in_header — zeros here because we
1022        // set None (no ISSY) in the struct, which serialises the ISSY bytes as 0.
1023        let parsed = Bbheader::parse(&dirty).unwrap();
1024        assert_eq!(parsed.mode, Mode::HighEfficiency);
1025        assert_eq!(parsed.issy_in_header, Some([0, 0, 0]));
1026        assert_eq!(parsed.dfl, 48000);
1027        assert_eq!(parsed.syncd, 256);
1028    }
1029
1030    #[test]
1031    fn serialize_into_rejects_buffer_too_small() {
1032        let hdr = Bbheader {
1033            matype: Matype {
1034                ts_gs: TsGs::Ts,
1035                sis: true,
1036                ccm: true,
1037                issyi: false,
1038                npd: false,
1039                ext: 0,
1040                isi: 0x00,
1041            },
1042            upl: 0,
1043            sync: 0x47,
1044            dfl: 0,
1045            syncd: 0,
1046            mode: Mode::Normal,
1047            issy_in_header: None,
1048        };
1049        let mut small = [0u8; BBHEADER_LEN - 1];
1050        let err = hdr.serialize_into(&mut small).unwrap_err();
1051        assert_eq!(
1052            err,
1053            Error::OutputBufferTooSmall {
1054                need: BBHEADER_LEN,
1055                have: small.len(),
1056            }
1057        );
1058    }
1059}