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                    // No ISSY in header: explicitly zero the three ISSY positions
352                    // so serialize_into is fully deterministic regardless of whether
353                    // the caller's buffer was zero-initialised or not. Without this,
354                    // stale bytes at buf[2], buf[3], buf[6] corrupt the CRC-8.
355                    buf[2] = 0;
356                    buf[3] = 0;
357                    let dfl = self.dfl.to_be_bytes();
358                    buf[4] = dfl[0];
359                    buf[5] = dfl[1];
360                    buf[6] = 0;
361                    let syncd = self.syncd.to_be_bytes();
362                    buf[7] = syncd[0];
363                    buf[8] = syncd[1];
364                }
365            }
366        }
367
368        // CRC-8 = crc8(bytes[0..9], init=0x00) XOR MODE
369        let computed = crc8(&buf[..9]);
370        buf[9] = computed ^ (self.mode as u8);
371
372        Ok(BBHEADER_LEN)
373    }
374}
375
376impl Bbheader {
377    /// Parse a 10-byte BBHEADER, detecting NM vs HEM automatically.
378    ///
379    /// Mode detection per EN 302 755 §5.1.7:
380    /// `mode = crc8(bytes[0..9]) ^ bytes[9]` (0 = NM, 1 = HEM).
381    /// Values other than 0 or 1 return `Error::InvalidMode`.
382    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
383        <Self as Parse>::parse(bytes)
384    }
385
386    /// Serialize the BBHEADER back to its 10-byte wire format.
387    pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
388        let v = <Self as Serialize>::to_bytes(self);
389        let mut buf = [0u8; BBHEADER_LEN];
390        buf.copy_from_slice(&v);
391        buf
392    }
393
394    /// Decode the ISSY field from the header bytes (HEM only).
395    ///
396    /// In HEM the 3-byte ISSY occupies the UPL/SYNC positions. This method
397    /// tries [`decode_issy_long`] first (the common case for 3-byte ISSY);
398    /// if that fails because the form bit indicates a short-form ISSY,
399    /// falls back to [`decode_issy_short`] on the first 2 bytes.
400    ///
401    /// Returns `None` in Normal Mode (no ISSY in header) or if decoding
402    /// produces an unexpected error.
403    #[must_use]
404    pub fn issy(&self) -> Option<Issy> {
405        let bytes = self.issy_in_header?;
406        decode_issy_long(bytes)
407            .ok()
408            .or_else(|| decode_issy_short([bytes[0], bytes[1]]).ok())
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn parse_rejects_buffer_shorter_than_10() {
418        assert!(Bbheader::parse(&[0u8; 9]).is_err());
419    }
420
421    #[test]
422    fn parse_nm_ts_extracts_all_fields() {
423        // Craft a valid NM BBHEADER with known values.
424        // MATYPE-1 = 0xF0: TS input (0b11), SIS (1), CCM (1), ISSYI (0), NPD (0), EXT (00)
425        // MATYPE-2 = 0x00 (single stream)
426        // UPL = 0x0718 = 1816 bits (188*8 - CRC-8 - sync = 1504-8 = 1496... let me just pick a value)
427        // DFL = 0xBC00 = 50304-50432? Let me pick simpler values.
428        let mut hdr = [0u8; BBHEADER_LEN];
429        hdr[0] = 0xF0; // MATYPE-1: TS, SIS, CCM
430        hdr[1] = 0x00; // MATYPE-2: not MIS
431        let upl: u16 = 0x07D0; // 2000 bits = 250 bytes
432        hdr[2..4].copy_from_slice(&upl.to_be_bytes());
433        let dfl: u16 = 0xBC00; // 48320 bits
434        hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
435        hdr[6] = 0x47; // SYNC byte
436        let syncd: u16 = 0x0000; // First UP aligned
437        hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
438        hdr[9] = crc8(&hdr[..9]); // CRC-8
439
440        let result = Bbheader::parse(&hdr).unwrap();
441        assert_eq!(result.mode, Mode::Normal);
442        assert_eq!(result.matype.ts_gs, TsGs::Ts);
443        assert!(result.matype.sis);
444        assert!(result.matype.ccm);
445        assert!(!result.matype.issyi);
446        assert!(!result.matype.npd);
447        assert_eq!(result.matype.ext, 0);
448        assert_eq!(result.matype.isi, 0x00);
449        assert_eq!(result.upl, upl);
450        assert_eq!(result.sync, 0x47);
451        assert_eq!(result.dfl, dfl);
452        assert_eq!(result.syncd, syncd);
453    }
454
455    #[test]
456    fn parse_nm_gcs_treats_sync_as_transport_protocol_byte() {
457        let mut hdr = [0u8; BBHEADER_LEN];
458        hdr[0] = 0x50; // MATYPE-1: GCS (0b01), SIS, CCM, ISSYI=0, NPD=0, EXT=00
459        hdr[1] = 0x00;
460        let upl: u16 = 0x0000; // GCS: UPL=0
461        hdr[2..4].copy_from_slice(&upl.to_be_bytes());
462        let dfl: u16 = 0x4000; // 16384 bits
463        hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
464        hdr[6] = 0x3C; // GCS: SYNC=0x00-0xB8 for protocol signalling
465        let syncd: u16 = 0x0000;
466        hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
467        hdr[9] = crc8(&hdr[..9]);
468
469        let result = Bbheader::parse(&hdr).unwrap();
470        assert_eq!(result.mode, Mode::Normal);
471        assert_eq!(result.matype.ts_gs, TsGs::Gcs);
472        assert_eq!(result.sync, 0x3C);
473        assert_eq!(result.upl, upl);
474    }
475
476    #[test]
477    fn parse_detects_nm_via_crc_xor_0() {
478        // When crc8(init=0) XOR byte[9] == 0, mode is NM
479        let mut hdr = [0u8; BBHEADER_LEN];
480        hdr[0] = 0xF0;
481        hdr[1] = 0x00;
482        hdr[2] = 0x07;
483        hdr[3] = 0xD0; // UPL
484        hdr[4] = 0xBC;
485        hdr[5] = 0x00; // DFL
486        hdr[6] = 0x47; // SYNC
487        hdr[7] = 0x00;
488        hdr[8] = 0x00; // SYNCD
489        hdr[9] = crc8(&hdr[..9]); // CRC matches init=0x00
490
491        let result = Bbheader::parse(&hdr).unwrap();
492        assert_eq!(result.mode, Mode::Normal);
493    }
494
495    #[test]
496    fn parse_rejects_crc_mismatch_in_both_modes() {
497        let mut hdr = [0u8; BBHEADER_LEN];
498        hdr[0] = 0xF0;
499        hdr[1] = 0x00;
500        hdr[2] = 0x07;
501        hdr[3] = 0xD0;
502        hdr[4] = 0xBC;
503        hdr[5] = 0x00;
504        hdr[6] = 0x47;
505        hdr[7] = 0x00;
506        hdr[8] = 0x00;
507        hdr[9] = 0xFF; // Wrong CRC
508
509        let result = Bbheader::parse(&hdr);
510        assert!(result.is_err());
511    }
512
513    #[test]
514    fn parse_matype_extracts_ts_gs_enum_for_each_of_gfps_ts_gcs_gse() {
515        for (ts_gs_val, expected) in [
516            (0b00, TsGs::Gfps),
517            (0b01, TsGs::Gcs),
518            (0b10, TsGs::Gse),
519            (0b11, TsGs::Ts),
520        ] {
521            let ma1 = (ts_gs_val << 6) | 0x30; // SIS=1, CCM=1, ISSYI=0, NPD=0, EXT=00
522            let mut hdr = [0u8; BBHEADER_LEN];
523            hdr[0] = ma1;
524            hdr[1] = 0x00;
525            hdr[2..9].copy_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
526            hdr[9] = crc8(&hdr[..9]);
527            let result = Bbheader::parse(&hdr).unwrap();
528            assert_eq!(result.matype.ts_gs, expected, "ts_gs=0x{:02b}", ts_gs_val);
529        }
530    }
531
532    #[test]
533    fn parse_matype_extracts_sis_isi_on_multi_stream() {
534        // sis = 0 → MIS, isi is MATYPE-2
535        let mut hdr = [0u8; BBHEADER_LEN];
536        hdr[0] = 0xD0; // TS/MIS/CCM -> not SIS
537        hdr[1] = 0xAB; // ISI = 171
538        hdr[2] = 0x07;
539        hdr[3] = 0xD0;
540        hdr[4] = 0xBC;
541        hdr[5] = 0x00;
542        hdr[6] = 0x47;
543        hdr[7] = 0x00;
544        hdr[8] = 0x00;
545        hdr[9] = crc8(&hdr[..9]);
546
547        let result = Bbheader::parse(&hdr).unwrap();
548        assert!(!result.matype.sis);
549        assert_eq!(result.matype.isi, 0xAB);
550    }
551
552    #[test]
553    fn parse_matype_extracts_roll_off_2_bits_as_ext_for_s2_context() {
554        // EXT = 0b11 in NM means reserved/S2X-low roll-off (0b00 is α0.35)
555        let mut hdr = [0u8; BBHEADER_LEN];
556        hdr[0] = 0xF3; // TS/SIS/CCM, no ISSYI, no NPD, EXT=0b11
557        hdr[1] = 0x00;
558        hdr[2] = 0x07;
559        hdr[3] = 0xD0;
560        hdr[4] = 0xBC;
561        hdr[5] = 0x00;
562        hdr[6] = 0x47;
563        hdr[7] = 0x00;
564        hdr[8] = 0x00;
565        hdr[9] = crc8(&hdr[..9]);
566
567        let result = Bbheader::parse(&hdr).unwrap();
568        assert_eq!(result.matype.ext, 0b11);
569    }
570
571    #[test]
572    fn serialize_nm_produces_expected_bytes() {
573        let hdr = Bbheader {
574            matype: Matype {
575                ts_gs: TsGs::Ts,
576                sis: true,
577                ccm: true,
578                issyi: false,
579                npd: false,
580                ext: 0,
581                isi: 0x00,
582            },
583            upl: 188 * 8,
584            sync: 0x47,
585            dfl: 48328,
586            syncd: 0,
587            mode: Mode::Normal,
588            issy_in_header: None,
589        };
590        let buf = hdr.serialize();
591
592        let parsed = Bbheader::parse(&buf).unwrap();
593        assert_eq!(parsed.matype.ts_gs, TsGs::Ts);
594        assert!(parsed.matype.sis);
595        assert!(parsed.matype.ccm);
596        assert_eq!(parsed.upl, 188 * 8);
597        assert_eq!(parsed.sync, 0x47);
598        assert_eq!(parsed.dfl, 48328);
599        assert_eq!(parsed.syncd, 0);
600        assert_eq!(parsed.mode, Mode::Normal);
601    }
602
603    #[test]
604    fn serialize_round_trip_nm_ts_preserves_every_field() {
605        let orig = Bbheader {
606            matype: Matype {
607                ts_gs: TsGs::Ts,
608                sis: true,
609                ccm: true,
610                issyi: true,
611                npd: false,
612                ext: 0,
613                isi: 0x00,
614            },
615            upl: 1504,
616            sync: 0x47,
617            dfl: 48328,
618            syncd: 0,
619            mode: Mode::Normal,
620            issy_in_header: None,
621        };
622        let buf = orig.serialize();
623        let parsed = Bbheader::parse(&buf).unwrap();
624        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
625        assert_eq!(orig.matype.sis, parsed.matype.sis);
626        assert_eq!(orig.matype.ccm, parsed.matype.ccm);
627        assert_eq!(orig.matype.issyi, parsed.matype.issyi);
628        assert_eq!(orig.matype.npd, parsed.matype.npd);
629        assert_eq!(orig.matype.ext, parsed.matype.ext);
630        assert_eq!(orig.matype.isi, parsed.matype.isi);
631        assert_eq!(orig.upl, parsed.upl);
632        assert_eq!(orig.sync, parsed.sync);
633        assert_eq!(orig.dfl, parsed.dfl);
634        assert_eq!(orig.syncd, parsed.syncd);
635        assert_eq!(orig.mode, parsed.mode);
636    }
637
638    #[test]
639    fn serialize_round_trip_nm_gcs() {
640        let orig = Bbheader {
641            matype: Matype {
642                ts_gs: TsGs::Gcs,
643                sis: true,
644                ccm: false,
645                issyi: false,
646                npd: false,
647                ext: 0,
648                isi: 0x00,
649            },
650            upl: 0,
651            sync: 0x00,
652            dfl: 16384,
653            syncd: 0,
654            mode: Mode::Normal,
655            issy_in_header: None,
656        };
657        let buf = orig.serialize();
658        let parsed = Bbheader::parse(&buf).unwrap();
659        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
660        assert_eq!(orig.matype.sis, parsed.matype.sis);
661        assert_eq!(orig.matype.ccm, parsed.matype.ccm);
662        assert_eq!(orig.dfl, parsed.dfl);
663        assert_eq!(orig.syncd, parsed.syncd);
664        assert_eq!(orig.mode, parsed.mode);
665    }
666
667    #[test]
668    fn serialize_crc8_always_matches_bytes_0_to_8() {
669        let hdr = Bbheader {
670            matype: Matype {
671                ts_gs: TsGs::Gse,
672                sis: true,
673                ccm: true,
674                issyi: true,
675                npd: false,
676                ext: 0,
677                isi: 0x00,
678            },
679            upl: 0,
680            sync: 0xFF,
681            dfl: 32768,
682            syncd: 0,
683            mode: Mode::Normal,
684            issy_in_header: None,
685        };
686        let buf = hdr.serialize();
687        let computed = crc8(&buf[..9]);
688        assert_eq!(computed ^ buf[9], 0); // XOR with MODE must give 0 for NM
689        assert_eq!(buf[9], computed); // MODE=0 means they must be equal
690    }
691
692    #[test]
693    fn parse_detects_hem_via_crc_xor_1() {
694        // Real DVB-T2 BBFRAME header from Rai T2-MI. Mode=HEM is detected by crc8(init=0) XOR stored = 1.
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    }
699
700    #[test]
701    fn parse_hem_extracts_matype_dfl_syncd() {
702        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
703        let result = Bbheader::parse(&hdr).unwrap();
704        assert_eq!(result.mode, Mode::HighEfficiency);
705        assert_eq!(result.matype.ts_gs, TsGs::Ts);
706        assert!(result.matype.sis);
707        assert!(result.matype.ccm);
708        assert!(result.matype.issyi);
709        assert!(!result.matype.npd);
710        assert_eq!(result.matype.ext, 0);
711        assert_eq!(result.dfl, 48328);
712        assert_eq!(result.syncd, 0x0350);
713    }
714
715    #[test]
716    fn parse_hem_preserves_three_issy_bytes() {
717        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
718        let result = Bbheader::parse(&hdr).unwrap();
719        let issy = result.issy_in_header.unwrap();
720        // ISSY in HEM: bytes[2..4] = ISSY_2MSB, byte[6] = ISSY_1LSB
721        assert_eq!(issy, [0xa4, 0x28, 0xe2]);
722    }
723
724    #[test]
725    fn issy_accessor_decodes_hem_iscr_long() {
726        // Real HEM fixture: ISSY bytes [0xa4, 0x28, 0xe2]
727        // byte0=0xa4, bit7=1 (long form), bit6=0 (ISCR long)
728        // payload = (0xa4 & 0x3F)<<16 | 0x28<<8 | 0xe2 = 0x2428e2
729        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
730        let result = Bbheader::parse(&hdr).unwrap();
731        let issy = result.issy().expect("ISSY decode from HEM header");
732        assert_eq!(issy, Issy::IscrLong(0x0024_28E2));
733    }
734
735    #[test]
736    fn issy_accessor_returns_none_for_nm() {
737        let mut hdr = [0u8; BBHEADER_LEN];
738        hdr[0] = 0xF0;
739        hdr[1] = 0x00;
740        hdr[2] = 0x07;
741        hdr[3] = 0xD0;
742        hdr[4] = 0xBC;
743        hdr[5] = 0x00;
744        hdr[6] = 0x47;
745        hdr[7] = 0x00;
746        hdr[8] = 0x00;
747        hdr[9] = crc8(&hdr[..9]);
748        let result = Bbheader::parse(&hdr).unwrap();
749        assert_eq!(result.mode, Mode::Normal);
750        assert!(result.issy().is_none());
751    }
752
753    #[test]
754    fn issy_accessor_falls_back_to_short_form() {
755        // HEM with ISSY bytes [0x7A, 0xBC, 0x00]: bit7=0 → short form.
756        // decode_issy_long fails, falls back to decode_issy_short([0x7A, 0xBC])
757        // → IscrShort(0x7ABC)
758        let hdr = Bbheader {
759            matype: Matype {
760                ts_gs: TsGs::Ts,
761                sis: true,
762                ccm: true,
763                issyi: true,
764                npd: false,
765                ext: 0,
766                isi: 0x00,
767            },
768            upl: 0,
769            sync: 0,
770            dfl: 50000,
771            syncd: 100,
772            mode: Mode::HighEfficiency,
773            issy_in_header: Some([0x7A, 0xBC, 0x00]),
774        };
775        let issy = hdr.issy().expect("short-form ISSY fallback");
776        assert_eq!(issy, Issy::IscrShort(0x7ABC));
777    }
778
779    #[test]
780    fn parse_hem_leaves_upl_bits_as_zero_and_sync_as_zero() {
781        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
782        let result = Bbheader::parse(&hdr).unwrap();
783        assert_eq!(result.upl, 0);
784        assert_eq!(result.sync, 0);
785    }
786
787    #[test]
788    fn parse_hem_rejects_when_mode_xor_not_0_or_1() {
789        // Create a header where crc8^byte[9] gives 2 (reserved)
790        let mut hdr = [0u8; BBHEADER_LEN];
791        hdr[0] = 0xF0;
792        hdr[1] = 0x00;
793        hdr[2] = 0x00;
794        hdr[3] = 0x00;
795        hdr[4] = 0x00;
796        hdr[5] = 0x00;
797        hdr[6] = 0x00;
798        hdr[7] = 0x00;
799        hdr[8] = 0x00;
800        hdr[9] = crc8(&hdr[..9]) ^ 0x02; // XOR with reserved value 2
801        assert!(Bbheader::parse(&hdr).is_err());
802    }
803
804    #[test]
805    fn parse_same_bytes_different_mode_byte_produces_different_bbheader() {
806        // Two headers that differ only in byte[9] (CRC-8 MODE byte)
807        let mut hdr1 = [0xF8, 0x00, 0x00, 0x00, 0xBC, 0xC8, 0x00, 0x03, 0x50, 0x00];
808        hdr1[9] = crc8(&hdr1[..9]); // NM
809        let mut hdr2 = hdr1;
810        hdr2[9] ^= 0x01; // HEM
811
812        let result1 = Bbheader::parse(&hdr1).unwrap();
813        let result2 = Bbheader::parse(&hdr2).unwrap();
814        assert_eq!(result1.mode, Mode::Normal);
815        assert_eq!(result2.mode, Mode::HighEfficiency);
816    }
817
818    #[test]
819    fn serialize_hem_round_trip() {
820        let orig = Bbheader {
821            matype: Matype {
822                ts_gs: TsGs::Ts,
823                sis: true,
824                ccm: true,
825                issyi: true,
826                npd: false,
827                ext: 0,
828                isi: 0x00,
829            },
830            upl: 0,  // not used in HEM
831            sync: 0, // not used in HEM
832            dfl: 48328,
833            syncd: 848,
834            mode: Mode::HighEfficiency,
835            issy_in_header: Some([0xA4, 0x28, 0xE2]),
836        };
837        let buf = orig.serialize();
838        let parsed = Bbheader::parse(&buf).unwrap();
839        assert_eq!(orig.mode, parsed.mode);
840        assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
841        assert_eq!(orig.dfl, parsed.dfl);
842        assert_eq!(orig.syncd, parsed.syncd);
843        assert_eq!(orig.issy_in_header, parsed.issy_in_header);
844    }
845
846    #[test]
847    fn serialize_hem_sets_crc_xor_mode_byte_correctly() {
848        let hdr = Bbheader {
849            matype: Matype {
850                ts_gs: TsGs::Ts,
851                sis: true,
852                ccm: true,
853                issyi: false,
854                npd: false,
855                ext: 0,
856                isi: 0x05,
857            },
858            upl: 0,
859            sync: 0,
860            dfl: 48000,
861            syncd: 0,
862            mode: Mode::HighEfficiency,
863            issy_in_header: Some([0x00, 0x00, 0x00]),
864        };
865        let buf = hdr.serialize();
866        let computed = crc8(&buf[..9]);
867        // MODE=1: stored = computed XOR 1
868        assert_eq!(buf[9], computed ^ 1);
869    }
870
871    #[test]
872    fn serialize_hem_with_issy_bytes_zero_writes_expected_layout() {
873        let hdr = Bbheader {
874            matype: Matype {
875                ts_gs: TsGs::Ts,
876                sis: true,
877                ccm: true,
878                issyi: true,
879                npd: false,
880                ext: 0,
881                isi: 0x00,
882            },
883            upl: 0,
884            sync: 0,
885            dfl: 50000,
886            syncd: 100,
887            mode: Mode::HighEfficiency,
888            issy_in_header: Some([0x00, 0x00, 0x00]),
889        };
890        let buf = hdr.serialize();
891        let parsed = Bbheader::parse(&buf).unwrap();
892        assert_eq!(parsed.mode, Mode::HighEfficiency);
893        assert_eq!(parsed.issy_in_header, Some([0x00, 0x00, 0x00]));
894        assert_eq!(parsed.dfl, 50000);
895        assert_eq!(parsed.syncd, 100);
896    }
897
898    #[test]
899    fn parse_valid_dvbt2_hem_bbframe_rai() {
900        // Real DVB-T2 BBFRAME header from Rai T2-MI (12606V, ISI 5, PLP 0).
901        let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
902        assert_eq!(Bbheader::parse(&hdr).unwrap().dfl, 48328);
903    }
904
905    #[test]
906    fn exhaustive_tsgs_sweep() {
907        let mut matched = 0u16;
908        for byte in 0u8..=0xFF {
909            if let Ok(v) = TsGs::try_from(byte) {
910                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
911                matched += 1;
912            }
913        }
914        assert_eq!(matched, 4, "expected 4 matched variants");
915    }
916
917    #[test]
918    fn exhaustive_mode_sweep() {
919        let mut matched = 0u16;
920        for byte in 0u8..=0xFF {
921            if let Ok(v) = Mode::try_from(byte) {
922                assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
923                matched += 1;
924            }
925        }
926        assert_eq!(matched, 2, "expected 2 matched variants");
927    }
928
929    #[test]
930    fn trait_parse_and_serialize_round_trip() {
931        let orig = Bbheader {
932            matype: Matype {
933                ts_gs: TsGs::Gse,
934                sis: true,
935                ccm: true,
936                issyi: true,
937                npd: false,
938                ext: 0,
939                isi: 0x00,
940            },
941            upl: 0,
942            sync: 0xFF,
943            dfl: 32768,
944            syncd: 0,
945            mode: Mode::Normal,
946            issy_in_header: None,
947        };
948        let v = <Bbheader as Serialize>::to_bytes(&orig);
949        let parsed = <Bbheader as Parse>::parse(&v).unwrap();
950        assert_eq!(orig.matype, parsed.matype);
951        assert_eq!(orig.upl, parsed.upl);
952        assert_eq!(orig.sync, parsed.sync);
953        assert_eq!(orig.dfl, parsed.dfl);
954        assert_eq!(orig.syncd, parsed.syncd);
955        assert_eq!(orig.mode, parsed.mode);
956    }
957
958    #[test]
959    fn serialize_into_hem_no_issy_is_deterministic_regardless_of_buffer_content() {
960        // BUG 1 regression: HEM + issy_in_header=None leaves buf[2], buf[3], buf[6]
961        // untouched. A pre-filled (0xFF) buffer must produce the same bytes as
962        // to_bytes() which zero-inits.
963        let hdr = Bbheader {
964            matype: Matype {
965                ts_gs: TsGs::Ts,
966                sis: true,
967                ccm: true,
968                issyi: false,
969                npd: false,
970                ext: 0,
971                isi: 0x00,
972            },
973            upl: 0,
974            sync: 0,
975            dfl: 48000,
976            syncd: 256,
977            mode: Mode::HighEfficiency,
978            issy_in_header: None,
979        };
980
981        // Reference: to_bytes() zero-inits so those bytes come out 0.
982        let clean = <Bbheader as Serialize>::to_bytes(&hdr);
983
984        // Dirty buffer pre-filled with 0xFF — buf[2], buf[3], buf[6] would keep
985        // 0xFF if the else-branch doesn't explicitly zero them.
986        let mut dirty = [0xFFu8; BBHEADER_LEN];
987        hdr.serialize_into(&mut dirty).unwrap();
988
989        assert_eq!(
990            clean.as_slice(),
991            dirty.as_slice(),
992            "serialize_into into dirty buffer must produce identical bytes to to_bytes()"
993        );
994
995        // Also verify re-parsing succeeds with correct fields.
996        // In HEM the parser always fills issy_in_header — zeros here because we
997        // set None (no ISSY) in the struct, which serialises the ISSY bytes as 0.
998        let parsed = Bbheader::parse(&dirty).unwrap();
999        assert_eq!(parsed.mode, Mode::HighEfficiency);
1000        assert_eq!(parsed.issy_in_header, Some([0, 0, 0]));
1001        assert_eq!(parsed.dfl, 48000);
1002        assert_eq!(parsed.syncd, 256);
1003    }
1004
1005    #[test]
1006    fn serialize_into_rejects_buffer_too_small() {
1007        let hdr = Bbheader {
1008            matype: Matype {
1009                ts_gs: TsGs::Ts,
1010                sis: true,
1011                ccm: true,
1012                issyi: false,
1013                npd: false,
1014                ext: 0,
1015                isi: 0x00,
1016            },
1017            upl: 0,
1018            sync: 0x47,
1019            dfl: 0,
1020            syncd: 0,
1021            mode: Mode::Normal,
1022            issy_in_header: None,
1023        };
1024        let mut small = [0u8; BBHEADER_LEN - 1];
1025        let err = hdr.serialize_into(&mut small).unwrap_err();
1026        assert_eq!(
1027            err,
1028            Error::OutputBufferTooSmall {
1029                need: BBHEADER_LEN,
1030                have: small.len(),
1031            }
1032        );
1033    }
1034}