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