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