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 num_enum::TryFromPrimitive;
7
8use crate::crc::crc8;
9use crate::error::Error;
10
11/// Total bytes in a BBHEADER.
12pub const BBHEADER_LEN: usize = 10;
13/// Loosest valid DFL upper bound in bits across the standards this crate parses.
14///
15/// DVB-S2 normal FECFRAME caps the data field near 64800 bits; DVB-T2 is tighter
16/// (EN 302 755 Table 2: DFL in [0, 53760]). A BBHEADER does not by itself say
17/// which standard produced it, so this generous bound avoids rejecting any valid
18/// S2/S2X/T2 frame.
19pub const DFL_MAX_BITS: u16 = 64800;
20
21/// Input stream format as described by the TS/GS field (MATYPE-1 bits `[7:6]`).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[repr(u8)]
25pub enum TsGs {
26    /// Generic Packetized Stream.
27    Gfps = 0b00,
28    /// Transport Stream (MPEG-2 TS, 188-byte packets).
29    Ts = 0b11,
30    /// Generic Continuous Stream.
31    Gcs = 0b01,
32    /// Generic Encapsulated Stream.
33    Gse = 0b10,
34}
35
36impl From<TsGs> for u8 {
37    fn from(t: TsGs) -> Self {
38        t as u8
39    }
40}
41
42impl From<num_enum::TryFromPrimitiveError<TsGs>> for Error {
43    fn from(e: num_enum::TryFromPrimitiveError<TsGs>) -> Self {
44        Error::UnsupportedTsGs { ts_gs: e.number }
45    }
46}
47
48impl std::fmt::Display for TsGs {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "TsGs::{self:?}")
51    }
52}
53
54/// Operating mode: Normal or High Efficiency.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[repr(u8)]
58pub enum Mode {
59    /// Normal Mode — UPL/SYNC/SYNCD present, CRC-8 per UP.
60    Normal = 0,
61    /// High Efficiency Mode — ISSY replaces UPL/SYNC, no per-UP CRC-8.
62    HighEfficiency = 1,
63}
64
65impl From<num_enum::TryFromPrimitiveError<Mode>> for Error {
66    fn from(e: num_enum::TryFromPrimitiveError<Mode>) -> Self {
67        Error::InvalidMode { mode: e.number }
68    }
69}
70
71/// The pair of MATYPE bytes describing the input stream format and mode adaptation.
72///
73/// Per EN 302 755 Table 1:
74/// - MATYPE-1 (byte 0): TS/GS, SIS/MIS, CCM/ACM, ISSYI, NPD, `EXT[1:0]`
75/// - MATYPE-2 (byte 1): ISI (0-255) or reserved
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct Matype {
79    /// Input stream format — see [`TsGs`].
80    pub ts_gs: TsGs,
81    /// Single-input stream (true) or multi-input stream (false).
82    pub sis: bool,
83    /// Constant coding and modulation (true) or adaptive (false).
84    pub ccm: bool,
85    /// Input Stream Synchronization Indicator — ISSY field is active.
86    pub issyi: bool,
87    /// Null Packet Deletion is active.
88    pub npd: bool,
89    /// Extension bits — RO in DVB-S2, reserved in DVB-T2.
90    pub ext: u8,
91    /// Input Stream Identifier — meaningful only if `sis == false`.
92    pub isi: u8,
93}
94
95impl Matype {
96    const MASK_TS_GS: u8 = 0xC0;
97    const MASK_SIS: u8 = 0x20;
98    const MASK_CCM: u8 = 0x10;
99    const MASK_ISSYI: u8 = 0x08;
100    const MASK_NPD: u8 = 0x04;
101    const MASK_EXT: u8 = 0x03;
102}
103
104impl TryFrom<[u8; 2]> for Matype {
105    type Error = Error;
106
107    fn try_from(bytes: [u8; 2]) -> Result<Self, Self::Error> {
108        let matype1 = bytes[0];
109        let matype2 = bytes[1];
110
111        let ts_gs = TsGs::try_from((matype1 & Matype::MASK_TS_GS) >> 6)?;
112        let sis = matype1 & Matype::MASK_SIS != 0;
113        let ccm = matype1 & Matype::MASK_CCM != 0;
114        let issyi = matype1 & Matype::MASK_ISSYI != 0;
115        let npd = matype1 & Matype::MASK_NPD != 0;
116        let ext = matype1 & Matype::MASK_EXT;
117
118        Ok(Matype {
119            ts_gs,
120            sis,
121            ccm,
122            issyi,
123            npd,
124            ext,
125            isi: matype2,
126        })
127    }
128}
129
130impl From<Matype> for [u8; 2] {
131    fn from(m: Matype) -> Self {
132        // MATYPE-1 layout per EN 302 755 Table 1:
133        //   bits 7..6 TS/GS, bit 5 SIS/MIS, bit 4 CCM/ACM,
134        //   bit 3 ISSYI, bit 2 NPD, bits 1..0 EXT.
135        let mut matype1: u8 = 0;
136        matype1 |= (u8::from(m.ts_gs) << 6) & Matype::MASK_TS_GS;
137        if m.sis {
138            matype1 |= Matype::MASK_SIS;
139        }
140        if m.ccm {
141            matype1 |= Matype::MASK_CCM;
142        }
143        if m.issyi {
144            matype1 |= Matype::MASK_ISSYI;
145        }
146        if m.npd {
147            matype1 |= Matype::MASK_NPD;
148        }
149        matype1 |= m.ext & Matype::MASK_EXT;
150        [matype1, m.isi]
151    }
152}
153
154/// Parsed 10-byte BBHEADER.
155///
156/// Fields vary based on [`Mode`]:
157/// - **NM** (MODE=0): `upl`, `sync` are from the header; `issy_in_header` is None.
158/// - **HEM** (MODE=1): `upl` and `sync` are both zero; `issy_in_header` carries the
159///   3 ISSY bytes that reuse the NM UPL/SYNC layout.
160///
161/// Detection: `crc8(bytes[0..9]) ^ bytes[9]` yields 0 for NM, 1 for HEM.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub struct Bbheader {
165    /// MATYPE field.
166    pub matype: Matype,
167    /// User Packet Length in bits (NM only; 0 in HEM).
168    pub upl: u16,
169    /// Copy of the User Packet Sync-byte (NM only; 0 in HEM).
170    pub sync: u8,
171    /// Data Field Length in bits.
172    pub dfl: u16,
173    /// Distance in bits from DATA FIELD start to the first complete UP beginning.
174    pub syncd: u16,
175    /// Detected mode (NM vs HEM).
176    pub mode: Mode,
177    /// 3-byte ISSY from header bytes (HEM only; None in NM).
178    pub issy_in_header: Option<[u8; 3]>,
179}
180
181impl Bbheader {
182    /// Parse a 10-byte BBHEADER, detecting NM vs HEM automatically.
183    ///
184    /// Mode detection per EN 302 755 §5.1.7:
185    /// `mode = crc8(bytes[0..9]) ^ bytes[9]` (0 = NM, 1 = HEM).
186    /// Values other than 0 or 1 return `Error::InvalidMode`.
187    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
188        if bytes.len() < BBHEADER_LEN {
189            return Err(Error::BufferTooShort {
190                need: BBHEADER_LEN,
191                have: bytes.len(),
192            });
193        }
194
195        let matype_bytes = [bytes[0], bytes[1]];
196        let matype = Matype::try_from(matype_bytes)?;
197        let dfl = u16::from_be_bytes([bytes[4], bytes[5]]);
198        let syncd = u16::from_be_bytes([bytes[7], bytes[8]]);
199        let crc_stored = bytes[9];
200
201        if dfl > DFL_MAX_BITS {
202            return Err(Error::DflOutOfRange {
203                dfl,
204                max: DFL_MAX_BITS,
205            });
206        }
207
208        // Mode detection per EN 302 755 §5.1.7: the byte on the wire is
209        // `crc8(bytes[0..9]) XOR MODE` (MODE: 0 = NM, 1 = HEM). The XOR is
210        // itself the integrity check — corruption that lands `mode_val`
211        // outside {0, 1} is rejected by `Mode::try_from` as InvalidMode; a
212        // residual flip into the other valid mode is undetectable by design
213        // of the spec's scheme (there is no separate "HEM CRC init").
214        let computed_crc = crc8(&bytes[..9]);
215        let mode_val = computed_crc ^ crc_stored;
216        let mode = Mode::try_from(mode_val)?;
217
218        let (upl, sync, issy_in_header) = match mode {
219            Mode::Normal => (u16::from_be_bytes([bytes[2], bytes[3]]), bytes[6], None),
220            Mode::HighEfficiency => {
221                // In HEM, bytes[2..4] are ISSY_2MSB, byte[6] is ISSY_1LSB —
222                // UPL and SYNC are repurposed for ISSY.
223                (0, 0, Some([bytes[2], bytes[3], bytes[6]]))
224            }
225        };
226
227        Ok(Bbheader {
228            matype,
229            upl,
230            sync,
231            dfl,
232            syncd,
233            mode,
234            issy_in_header,
235        })
236    }
237
238    /// Serialize the BBHEADER back to its 10-byte wire format.
239    pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
240        let mut buf = [0u8; BBHEADER_LEN];
241        let ma = <[u8; 2]>::from(self.matype);
242        buf[0] = ma[0];
243        buf[1] = ma[1];
244
245        match self.mode {
246            Mode::Normal => {
247                let upl = self.upl.to_be_bytes();
248                buf[2] = upl[0];
249                buf[3] = upl[1];
250                let dfl = self.dfl.to_be_bytes();
251                buf[4] = dfl[0];
252                buf[5] = dfl[1];
253                buf[6] = self.sync;
254                let syncd = self.syncd.to_be_bytes();
255                buf[7] = syncd[0];
256                buf[8] = syncd[1];
257            }
258            Mode::HighEfficiency => {
259                if let Some(issy) = self.issy_in_header {
260                    buf[2] = issy[0];
261                    buf[3] = issy[1];
262                    // byte 4-5 = DFL
263                    let dfl = self.dfl.to_be_bytes();
264                    buf[4] = dfl[0];
265                    buf[5] = dfl[1];
266                    buf[6] = issy[2];
267                    // byte 7-8 = SYNCD
268                    let syncd = self.syncd.to_be_bytes();
269                    buf[7] = syncd[0];
270                    buf[8] = syncd[1];
271                } else {
272                    // HEM without ISSY — zero the ISSY positions
273                    let dfl = self.dfl.to_be_bytes();
274                    buf[4] = dfl[0];
275                    buf[5] = dfl[1];
276                    let syncd = self.syncd.to_be_bytes();
277                    buf[7] = syncd[0];
278                    buf[8] = syncd[1];
279                }
280            }
281        }
282
283        // CRC-8 = crc8(bytes[0..9], init=0x00) XOR MODE
284        let computed = crc8(&buf[..9]);
285        buf[9] = computed ^ (self.mode as u8);
286
287        buf
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn parse_rejects_buffer_shorter_than_10() {
297        assert!(Bbheader::parse(&[0u8; 9]).is_err());
298    }
299
300    #[test]
301    fn parse_nm_ts_extracts_all_fields() {
302        // Craft a valid NM BBHEADER with known values.
303        // MATYPE-1 = 0xF0: TS input (0b11), SIS (1), CCM (1), ISSYI (0), NPD (0), EXT (00)
304        // MATYPE-2 = 0x00 (single stream)
305        // UPL = 0x0718 = 1816 bits (188*8 - CRC-8 - sync = 1504-8 = 1496... let me just pick a value)
306        // DFL = 0xBC00 = 50304-50432? Let me pick simpler values.
307        let mut hdr = [0u8; BBHEADER_LEN];
308        hdr[0] = 0xF0; // MATYPE-1: TS, SIS, CCM
309        hdr[1] = 0x00; // MATYPE-2: not MIS
310        let upl: u16 = 0x07D0; // 2000 bits = 250 bytes
311        hdr[2..4].copy_from_slice(&upl.to_be_bytes());
312        let dfl: u16 = 0xBC00; // 48320 bits
313        hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
314        hdr[6] = 0x47; // SYNC byte
315        let syncd: u16 = 0x0000; // First UP aligned
316        hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
317        hdr[9] = crc8(&hdr[..9]); // CRC-8
318
319        let result = Bbheader::parse(&hdr).unwrap();
320        assert_eq!(result.mode, Mode::Normal);
321        assert_eq!(result.matype.ts_gs, TsGs::Ts);
322        assert!(result.matype.sis);
323        assert!(result.matype.ccm);
324        assert!(!result.matype.issyi);
325        assert!(!result.matype.npd);
326        assert_eq!(result.matype.ext, 0);
327        assert_eq!(result.matype.isi, 0x00);
328        assert_eq!(result.upl, upl);
329        assert_eq!(result.sync, 0x47);
330        assert_eq!(result.dfl, dfl);
331        assert_eq!(result.syncd, syncd);
332    }
333
334    #[test]
335    fn parse_nm_gcs_treats_sync_as_transport_protocol_byte() {
336        let mut hdr = [0u8; BBHEADER_LEN];
337        hdr[0] = 0x50; // MATYPE-1: GCS (0b01), SIS, CCM, ISSYI=0, NPD=0, EXT=00
338        hdr[1] = 0x00;
339        let upl: u16 = 0x0000; // GCS: UPL=0
340        hdr[2..4].copy_from_slice(&upl.to_be_bytes());
341        let dfl: u16 = 0x4000; // 16384 bits
342        hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
343        hdr[6] = 0x3C; // GCS: SYNC=0x00-0xB8 for protocol signalling
344        let syncd: u16 = 0x0000;
345        hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
346        hdr[9] = crc8(&hdr[..9]);
347
348        let result = Bbheader::parse(&hdr).unwrap();
349        assert_eq!(result.mode, Mode::Normal);
350        assert_eq!(result.matype.ts_gs, TsGs::Gcs);
351        assert_eq!(result.sync, 0x3C);
352        assert_eq!(result.upl, upl);
353    }
354
355    #[test]
356    fn parse_detects_nm_via_crc_xor_0() {
357        // When crc8(init=0) XOR byte[9] == 0, mode is NM
358        let mut hdr = [0u8; BBHEADER_LEN];
359        hdr[0] = 0xF0;
360        hdr[1] = 0x00;
361        hdr[2] = 0x07;
362        hdr[3] = 0xD0; // UPL
363        hdr[4] = 0xBC;
364        hdr[5] = 0x00; // DFL
365        hdr[6] = 0x47; // SYNC
366        hdr[7] = 0x00;
367        hdr[8] = 0x00; // SYNCD
368        hdr[9] = crc8(&hdr[..9]); // CRC matches init=0x00
369
370        let result = Bbheader::parse(&hdr).unwrap();
371        assert_eq!(result.mode, Mode::Normal);
372    }
373
374    #[test]
375    fn parse_rejects_crc_mismatch_in_both_modes() {
376        let mut hdr = [0u8; BBHEADER_LEN];
377        hdr[0] = 0xF0;
378        hdr[1] = 0x00;
379        hdr[2] = 0x07;
380        hdr[3] = 0xD0;
381        hdr[4] = 0xBC;
382        hdr[5] = 0x00;
383        hdr[6] = 0x47;
384        hdr[7] = 0x00;
385        hdr[8] = 0x00;
386        hdr[9] = 0xFF; // Wrong CRC
387
388        let result = Bbheader::parse(&hdr);
389        assert!(result.is_err());
390    }
391
392    #[test]
393    fn parse_matype_extracts_ts_gs_enum_for_each_of_gfps_ts_gcs_gse() {
394        for (ts_gs_val, expected) in [
395            (0b00, TsGs::Gfps),
396            (0b01, TsGs::Gcs),
397            (0b10, TsGs::Gse),
398            (0b11, TsGs::Ts),
399        ] {
400            let ma1 = (ts_gs_val << 6) | 0x30; // SIS=1, CCM=1, ISSYI=0, NPD=0, EXT=00
401            let mut hdr = [0u8; BBHEADER_LEN];
402            hdr[0] = ma1;
403            hdr[1] = 0x00;
404            hdr[2..9].copy_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
405            hdr[9] = crc8(&hdr[..9]);
406            let result = Bbheader::parse(&hdr).unwrap();
407            assert_eq!(result.matype.ts_gs, expected, "ts_gs=0x{:02b}", ts_gs_val);
408        }
409    }
410
411    #[test]
412    fn parse_matype_extracts_sis_isi_on_multi_stream() {
413        // sis = 0 → MIS, isi is MATYPE-2
414        let mut hdr = [0u8; BBHEADER_LEN];
415        hdr[0] = 0xD0; // TS/MIS/CCM -> not SIS
416        hdr[1] = 0xAB; // ISI = 171
417        hdr[2] = 0x07;
418        hdr[3] = 0xD0;
419        hdr[4] = 0xBC;
420        hdr[5] = 0x00;
421        hdr[6] = 0x47;
422        hdr[7] = 0x00;
423        hdr[8] = 0x00;
424        hdr[9] = crc8(&hdr[..9]);
425
426        let result = Bbheader::parse(&hdr).unwrap();
427        assert!(!result.matype.sis);
428        assert_eq!(result.matype.isi, 0xAB);
429    }
430
431    #[test]
432    fn parse_matype_extracts_roll_off_2_bits_as_ext_for_s2_context() {
433        // EXT = 0b11 in NM means roll-off α=0.35 for DVB-S2
434        let mut hdr = [0u8; BBHEADER_LEN];
435        hdr[0] = 0xF3; // TS/SIS/CCM, no ISSYI, no NPD, EXT=0b11
436        hdr[1] = 0x00;
437        hdr[2] = 0x07;
438        hdr[3] = 0xD0;
439        hdr[4] = 0xBC;
440        hdr[5] = 0x00;
441        hdr[6] = 0x47;
442        hdr[7] = 0x00;
443        hdr[8] = 0x00;
444        hdr[9] = crc8(&hdr[..9]);
445
446        let result = Bbheader::parse(&hdr).unwrap();
447        assert_eq!(result.matype.ext, 0b11);
448    }
449
450    #[test]
451    fn serialize_nm_produces_expected_bytes() {
452        let hdr = Bbheader {
453            matype: Matype {
454                ts_gs: TsGs::Ts,
455                sis: true,
456                ccm: true,
457                issyi: false,
458                npd: false,
459                ext: 0,
460                isi: 0x00,
461            },
462            upl: 188 * 8,
463            sync: 0x47,
464            dfl: 48328,
465            syncd: 0,
466            mode: Mode::Normal,
467            issy_in_header: None,
468        };
469        let buf = hdr.serialize();
470
471        let parsed = Bbheader::parse(&buf).unwrap();
472        assert_eq!(parsed.matype.ts_gs, TsGs::Ts);
473        assert!(parsed.matype.sis);
474        assert!(parsed.matype.ccm);
475        assert_eq!(parsed.upl, 188 * 8);
476        assert_eq!(parsed.sync, 0x47);
477        assert_eq!(parsed.dfl, 48328);
478        assert_eq!(parsed.syncd, 0);
479        assert_eq!(parsed.mode, Mode::Normal);
480    }
481
482    #[test]
483    fn serialize_round_trip_nm_ts_preserves_every_field() {
484        let orig = Bbheader {
485            matype: Matype {
486                ts_gs: TsGs::Ts,
487                sis: true,
488                ccm: true,
489                issyi: true,
490                npd: false,
491                ext: 0,
492                isi: 0x00,
493            },
494            upl: 1504,
495            sync: 0x47,
496            dfl: 48328,
497            syncd: 0,
498            mode: Mode::Normal,
499            issy_in_header: None,
500        };
501        let buf = orig.serialize();
502        let parsed = Bbheader::parse(&buf).unwrap();
503        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
504        assert_eq!(orig.matype.sis, parsed.matype.sis);
505        assert_eq!(orig.matype.ccm, parsed.matype.ccm);
506        assert_eq!(orig.matype.issyi, parsed.matype.issyi);
507        assert_eq!(orig.matype.npd, parsed.matype.npd);
508        assert_eq!(orig.matype.ext, parsed.matype.ext);
509        assert_eq!(orig.matype.isi, parsed.matype.isi);
510        assert_eq!(orig.upl, parsed.upl);
511        assert_eq!(orig.sync, parsed.sync);
512        assert_eq!(orig.dfl, parsed.dfl);
513        assert_eq!(orig.syncd, parsed.syncd);
514        assert_eq!(orig.mode, parsed.mode);
515    }
516
517    #[test]
518    fn serialize_round_trip_nm_gcs() {
519        let orig = Bbheader {
520            matype: Matype {
521                ts_gs: TsGs::Gcs,
522                sis: true,
523                ccm: false,
524                issyi: false,
525                npd: false,
526                ext: 0,
527                isi: 0x00,
528            },
529            upl: 0,
530            sync: 0x00,
531            dfl: 16384,
532            syncd: 0,
533            mode: Mode::Normal,
534            issy_in_header: None,
535        };
536        let buf = orig.serialize();
537        let parsed = Bbheader::parse(&buf).unwrap();
538        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
539        assert_eq!(orig.matype.sis, parsed.matype.sis);
540        assert_eq!(orig.matype.ccm, parsed.matype.ccm);
541        assert_eq!(orig.dfl, parsed.dfl);
542        assert_eq!(orig.syncd, parsed.syncd);
543        assert_eq!(orig.mode, parsed.mode);
544    }
545
546    #[test]
547    fn serialize_crc8_always_matches_bytes_0_to_8() {
548        let hdr = Bbheader {
549            matype: Matype {
550                ts_gs: TsGs::Gse,
551                sis: true,
552                ccm: true,
553                issyi: true,
554                npd: false,
555                ext: 0,
556                isi: 0x00,
557            },
558            upl: 0,
559            sync: 0xFF,
560            dfl: 32768,
561            syncd: 0,
562            mode: Mode::Normal,
563            issy_in_header: None,
564        };
565        let buf = hdr.serialize();
566        let computed = crc8(&buf[..9]);
567        assert_eq!(computed ^ buf[9], 0); // XOR with MODE must give 0 for NM
568        assert_eq!(buf[9], computed); // MODE=0 means they must be equal
569    }
570
571    #[test]
572    fn parse_detects_hem_via_crc_xor_1() {
573        // Real DVB-T2 BBFRAME header from Rai T2-MI. Mode=HEM is detected by crc8(init=0) XOR stored = 1.
574        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
575        let result = Bbheader::parse(&hdr).unwrap();
576        assert_eq!(result.mode, Mode::HighEfficiency);
577    }
578
579    #[test]
580    fn parse_hem_extracts_matype_dfl_syncd() {
581        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
582        let result = Bbheader::parse(&hdr).unwrap();
583        assert_eq!(result.mode, Mode::HighEfficiency);
584        assert_eq!(result.matype.ts_gs, TsGs::Ts);
585        assert!(result.matype.sis);
586        assert!(result.matype.ccm);
587        assert!(result.matype.issyi);
588        assert!(!result.matype.npd);
589        assert_eq!(result.matype.ext, 0);
590        assert_eq!(result.dfl, 48328);
591        assert_eq!(result.syncd, 0x0350);
592    }
593
594    #[test]
595    fn parse_hem_preserves_three_issy_bytes() {
596        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
597        let result = Bbheader::parse(&hdr).unwrap();
598        let issy = result.issy_in_header.unwrap();
599        // ISSY in HEM: bytes[2..4] = ISSY_2MSB, byte[6] = ISSY_1LSB
600        assert_eq!(issy, [0xa4, 0x28, 0xe2]);
601    }
602
603    #[test]
604    fn parse_hem_leaves_upl_bits_as_zero_and_sync_as_zero() {
605        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
606        let result = Bbheader::parse(&hdr).unwrap();
607        assert_eq!(result.upl, 0);
608        assert_eq!(result.sync, 0);
609    }
610
611    #[test]
612    fn parse_hem_rejects_when_mode_xor_not_0_or_1() {
613        // Create a header where crc8^byte[9] gives 2 (reserved)
614        let mut hdr = [0u8; BBHEADER_LEN];
615        hdr[0] = 0xF0;
616        hdr[1] = 0x00;
617        hdr[2] = 0x00;
618        hdr[3] = 0x00;
619        hdr[4] = 0x00;
620        hdr[5] = 0x00;
621        hdr[6] = 0x00;
622        hdr[7] = 0x00;
623        hdr[8] = 0x00;
624        hdr[9] = crc8(&hdr[..9]) ^ 0x02; // XOR with reserved value 2
625        assert!(Bbheader::parse(&hdr).is_err());
626    }
627
628    #[test]
629    fn parse_same_bytes_different_mode_byte_produces_different_bbheader() {
630        // Two headers that differ only in byte[9] (CRC-8 MODE byte)
631        let mut hdr1 = [0xF8, 0x00, 0x00, 0x00, 0xBC, 0xC8, 0x00, 0x03, 0x50, 0x00];
632        hdr1[9] = crc8(&hdr1[..9]); // NM
633        let mut hdr2 = hdr1;
634        hdr2[9] ^= 0x01; // HEM
635
636        let result1 = Bbheader::parse(&hdr1).unwrap();
637        let result2 = Bbheader::parse(&hdr2).unwrap();
638        assert_eq!(result1.mode, Mode::Normal);
639        assert_eq!(result2.mode, Mode::HighEfficiency);
640    }
641
642    #[test]
643    fn serialize_hem_round_trip() {
644        let orig = Bbheader {
645            matype: Matype {
646                ts_gs: TsGs::Ts,
647                sis: true,
648                ccm: true,
649                issyi: true,
650                npd: false,
651                ext: 0,
652                isi: 0x00,
653            },
654            upl: 0,  // not used in HEM
655            sync: 0, // not used in HEM
656            dfl: 48328,
657            syncd: 848,
658            mode: Mode::HighEfficiency,
659            issy_in_header: Some([0xA4, 0x28, 0xE2]),
660        };
661        let buf = orig.serialize();
662        let parsed = Bbheader::parse(&buf).unwrap();
663        assert_eq!(orig.mode, parsed.mode);
664        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
665        assert_eq!(orig.dfl, parsed.dfl);
666        assert_eq!(orig.syncd, parsed.syncd);
667        assert_eq!(orig.issy_in_header, parsed.issy_in_header);
668    }
669
670    #[test]
671    fn serialize_hem_sets_crc_xor_mode_byte_correctly() {
672        let hdr = Bbheader {
673            matype: Matype {
674                ts_gs: TsGs::Ts,
675                sis: true,
676                ccm: true,
677                issyi: false,
678                npd: false,
679                ext: 0,
680                isi: 0x05,
681            },
682            upl: 0,
683            sync: 0,
684            dfl: 48000,
685            syncd: 0,
686            mode: Mode::HighEfficiency,
687            issy_in_header: Some([0x00, 0x00, 0x00]),
688        };
689        let buf = hdr.serialize();
690        let computed = crc8(&buf[..9]);
691        // MODE=1: stored = computed XOR 1
692        assert_eq!(buf[9], computed ^ 1);
693    }
694
695    #[test]
696    fn serialize_hem_with_issy_bytes_zero_writes_expected_layout() {
697        let hdr = Bbheader {
698            matype: Matype {
699                ts_gs: TsGs::Ts,
700                sis: true,
701                ccm: true,
702                issyi: true,
703                npd: false,
704                ext: 0,
705                isi: 0x00,
706            },
707            upl: 0,
708            sync: 0,
709            dfl: 50000,
710            syncd: 100,
711            mode: Mode::HighEfficiency,
712            issy_in_header: Some([0x00, 0x00, 0x00]),
713        };
714        let buf = hdr.serialize();
715        let parsed = Bbheader::parse(&buf).unwrap();
716        assert_eq!(parsed.mode, Mode::HighEfficiency);
717        assert_eq!(parsed.issy_in_header, Some([0x00, 0x00, 0x00]));
718        assert_eq!(parsed.dfl, 50000);
719        assert_eq!(parsed.syncd, 100);
720    }
721
722    #[test]
723    fn parse_valid_dvbt2_hem_bbframe_rai() {
724        // Real DVB-T2 BBFRAME header from Rai T2-MI (12606V, ISI 5, PLP 0).
725        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
726        assert_eq!(Bbheader::parse(&hdr).unwrap().dfl, 48328);
727    }
728
729    #[test]
730    fn exhaustive_tsgs_sweep() {
731        let mut matched = 0u16;
732        for byte in 0u8..=0xFF {
733            if let Ok(v) = TsGs::try_from(byte) {
734                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
735                matched += 1;
736            }
737        }
738        assert_eq!(matched, 4, "expected 4 matched variants");
739    }
740
741    #[test]
742    fn exhaustive_mode_sweep() {
743        let mut matched = 0u16;
744        for byte in 0u8..=0xFF {
745            if let Ok(v) = Mode::try_from(byte) {
746                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
747                matched += 1;
748            }
749        }
750        assert_eq!(matched, 2, "expected 2 matched variants");
751    }
752}