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