use num_enum::TryFromPrimitive;
use crate::crc::crc8;
use crate::error::Error;
pub const BBHEADER_LEN: usize = 10;
pub const DFL_MAX_BITS: u16 = 64800;
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[repr(u8)]
pub enum TsGs {
Gfps = 0b00,
Ts = 0b11,
Gcs = 0b01,
Gse = 0b10,
}
impl From<TsGs> for u8 {
fn from(t: TsGs) -> Self {
t as u8
}
}
impl From<num_enum::TryFromPrimitiveError<TsGs>> for Error {
fn from(e: num_enum::TryFromPrimitiveError<TsGs>) -> Self {
Error::UnsupportedTsGs { ts_gs: e.number }
}
}
impl std::fmt::Display for TsGs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TsGs::{self:?}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[repr(u8)]
pub enum Mode {
Normal = 0,
HighEfficiency = 1,
}
impl From<num_enum::TryFromPrimitiveError<Mode>> for Error {
fn from(e: num_enum::TryFromPrimitiveError<Mode>) -> Self {
Error::InvalidMode { mode: e.number }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Matype {
pub ts_gs: TsGs,
pub sis: bool,
pub ccm: bool,
pub issyi: bool,
pub npd: bool,
pub ext: u8,
pub isi: u8,
}
impl Matype {
const MASK_TS_GS: u8 = 0xC0;
const MASK_SIS: u8 = 0x20;
const MASK_CCM: u8 = 0x10;
const MASK_ISSYI: u8 = 0x08;
const MASK_NPD: u8 = 0x04;
const MASK_EXT: u8 = 0x03;
}
impl TryFrom<[u8; 2]> for Matype {
type Error = Error;
fn try_from(bytes: [u8; 2]) -> Result<Self, Self::Error> {
let matype1 = bytes[0];
let matype2 = bytes[1];
let ts_gs = TsGs::try_from((matype1 & Matype::MASK_TS_GS) >> 6)?;
let sis = matype1 & Matype::MASK_SIS != 0;
let ccm = matype1 & Matype::MASK_CCM != 0;
let issyi = matype1 & Matype::MASK_ISSYI != 0;
let npd = matype1 & Matype::MASK_NPD != 0;
let ext = matype1 & Matype::MASK_EXT;
Ok(Matype {
ts_gs,
sis,
ccm,
issyi,
npd,
ext,
isi: matype2,
})
}
}
impl From<Matype> for [u8; 2] {
fn from(m: Matype) -> Self {
let mut matype1: u8 = 0;
matype1 |= (u8::from(m.ts_gs) << 6) & Matype::MASK_TS_GS;
if m.sis {
matype1 |= Matype::MASK_SIS;
}
if m.ccm {
matype1 |= Matype::MASK_CCM;
}
if m.issyi {
matype1 |= Matype::MASK_ISSYI;
}
if m.npd {
matype1 |= Matype::MASK_NPD;
}
matype1 |= m.ext & Matype::MASK_EXT;
[matype1, m.isi]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Bbheader {
pub matype: Matype,
pub upl: u16,
pub sync: u8,
pub dfl: u16,
pub syncd: u16,
pub mode: Mode,
pub issy_in_header: Option<[u8; 3]>,
}
impl Bbheader {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < BBHEADER_LEN {
return Err(Error::BufferTooShort {
need: BBHEADER_LEN,
have: bytes.len(),
});
}
let matype_bytes = [bytes[0], bytes[1]];
let matype = Matype::try_from(matype_bytes)?;
let dfl = u16::from_be_bytes([bytes[4], bytes[5]]);
let syncd = u16::from_be_bytes([bytes[7], bytes[8]]);
let crc_stored = bytes[9];
if dfl > DFL_MAX_BITS {
return Err(Error::DflOutOfRange {
dfl,
max: DFL_MAX_BITS,
});
}
let computed_crc = crc8(&bytes[..9]);
let mode_val = computed_crc ^ crc_stored;
let mode = Mode::try_from(mode_val)?;
let (upl, sync, issy_in_header) = match mode {
Mode::Normal => (u16::from_be_bytes([bytes[2], bytes[3]]), bytes[6], None),
Mode::HighEfficiency => {
(0, 0, Some([bytes[2], bytes[3], bytes[6]]))
}
};
Ok(Bbheader {
matype,
upl,
sync,
dfl,
syncd,
mode,
issy_in_header,
})
}
pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
let mut buf = [0u8; BBHEADER_LEN];
let ma = <[u8; 2]>::from(self.matype);
buf[0] = ma[0];
buf[1] = ma[1];
match self.mode {
Mode::Normal => {
let upl = self.upl.to_be_bytes();
buf[2] = upl[0];
buf[3] = upl[1];
let dfl = self.dfl.to_be_bytes();
buf[4] = dfl[0];
buf[5] = dfl[1];
buf[6] = self.sync;
let syncd = self.syncd.to_be_bytes();
buf[7] = syncd[0];
buf[8] = syncd[1];
}
Mode::HighEfficiency => {
if let Some(issy) = self.issy_in_header {
buf[2] = issy[0];
buf[3] = issy[1];
let dfl = self.dfl.to_be_bytes();
buf[4] = dfl[0];
buf[5] = dfl[1];
buf[6] = issy[2];
let syncd = self.syncd.to_be_bytes();
buf[7] = syncd[0];
buf[8] = syncd[1];
} else {
let dfl = self.dfl.to_be_bytes();
buf[4] = dfl[0];
buf[5] = dfl[1];
let syncd = self.syncd.to_be_bytes();
buf[7] = syncd[0];
buf[8] = syncd[1];
}
}
}
let computed = crc8(&buf[..9]);
buf[9] = computed ^ (self.mode as u8);
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_rejects_buffer_shorter_than_10() {
assert!(Bbheader::parse(&[0u8; 9]).is_err());
}
#[test]
fn parse_nm_ts_extracts_all_fields() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0xF0; hdr[1] = 0x00; let upl: u16 = 0x07D0; hdr[2..4].copy_from_slice(&upl.to_be_bytes());
let dfl: u16 = 0xBC00; hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
hdr[6] = 0x47; let syncd: u16 = 0x0000; hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
hdr[9] = crc8(&hdr[..9]);
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.mode, Mode::Normal);
assert_eq!(result.matype.ts_gs, TsGs::Ts);
assert!(result.matype.sis);
assert!(result.matype.ccm);
assert!(!result.matype.issyi);
assert!(!result.matype.npd);
assert_eq!(result.matype.ext, 0);
assert_eq!(result.matype.isi, 0x00);
assert_eq!(result.upl, upl);
assert_eq!(result.sync, 0x47);
assert_eq!(result.dfl, dfl);
assert_eq!(result.syncd, syncd);
}
#[test]
fn parse_nm_gcs_treats_sync_as_transport_protocol_byte() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0x50; hdr[1] = 0x00;
let upl: u16 = 0x0000; hdr[2..4].copy_from_slice(&upl.to_be_bytes());
let dfl: u16 = 0x4000; hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
hdr[6] = 0x3C; let syncd: u16 = 0x0000;
hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
hdr[9] = crc8(&hdr[..9]);
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.mode, Mode::Normal);
assert_eq!(result.matype.ts_gs, TsGs::Gcs);
assert_eq!(result.sync, 0x3C);
assert_eq!(result.upl, upl);
}
#[test]
fn parse_detects_nm_via_crc_xor_0() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0xF0;
hdr[1] = 0x00;
hdr[2] = 0x07;
hdr[3] = 0xD0; hdr[4] = 0xBC;
hdr[5] = 0x00; hdr[6] = 0x47; hdr[7] = 0x00;
hdr[8] = 0x00; hdr[9] = crc8(&hdr[..9]);
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.mode, Mode::Normal);
}
#[test]
fn parse_rejects_crc_mismatch_in_both_modes() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0xF0;
hdr[1] = 0x00;
hdr[2] = 0x07;
hdr[3] = 0xD0;
hdr[4] = 0xBC;
hdr[5] = 0x00;
hdr[6] = 0x47;
hdr[7] = 0x00;
hdr[8] = 0x00;
hdr[9] = 0xFF;
let result = Bbheader::parse(&hdr);
assert!(result.is_err());
}
#[test]
fn parse_matype_extracts_ts_gs_enum_for_each_of_gfps_ts_gcs_gse() {
for (ts_gs_val, expected) in [
(0b00, TsGs::Gfps),
(0b01, TsGs::Gcs),
(0b10, TsGs::Gse),
(0b11, TsGs::Ts),
] {
let ma1 = (ts_gs_val << 6) | 0x30; let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = ma1;
hdr[1] = 0x00;
hdr[2..9].copy_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
hdr[9] = crc8(&hdr[..9]);
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.matype.ts_gs, expected, "ts_gs=0x{:02b}", ts_gs_val);
}
}
#[test]
fn parse_matype_extracts_sis_isi_on_multi_stream() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0xD0; hdr[1] = 0xAB; hdr[2] = 0x07;
hdr[3] = 0xD0;
hdr[4] = 0xBC;
hdr[5] = 0x00;
hdr[6] = 0x47;
hdr[7] = 0x00;
hdr[8] = 0x00;
hdr[9] = crc8(&hdr[..9]);
let result = Bbheader::parse(&hdr).unwrap();
assert!(!result.matype.sis);
assert_eq!(result.matype.isi, 0xAB);
}
#[test]
fn parse_matype_extracts_roll_off_2_bits_as_ext_for_s2_context() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0xF3; hdr[1] = 0x00;
hdr[2] = 0x07;
hdr[3] = 0xD0;
hdr[4] = 0xBC;
hdr[5] = 0x00;
hdr[6] = 0x47;
hdr[7] = 0x00;
hdr[8] = 0x00;
hdr[9] = crc8(&hdr[..9]);
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.matype.ext, 0b11);
}
#[test]
fn serialize_nm_produces_expected_bytes() {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: false,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 188 * 8,
sync: 0x47,
dfl: 48328,
syncd: 0,
mode: Mode::Normal,
issy_in_header: None,
};
let buf = hdr.serialize();
let parsed = Bbheader::parse(&buf).unwrap();
assert_eq!(parsed.matype.ts_gs, TsGs::Ts);
assert!(parsed.matype.sis);
assert!(parsed.matype.ccm);
assert_eq!(parsed.upl, 188 * 8);
assert_eq!(parsed.sync, 0x47);
assert_eq!(parsed.dfl, 48328);
assert_eq!(parsed.syncd, 0);
assert_eq!(parsed.mode, Mode::Normal);
}
#[test]
fn serialize_round_trip_nm_ts_preserves_every_field() {
let orig = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: true,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 1504,
sync: 0x47,
dfl: 48328,
syncd: 0,
mode: Mode::Normal,
issy_in_header: None,
};
let buf = orig.serialize();
let parsed = Bbheader::parse(&buf).unwrap();
assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
assert_eq!(orig.matype.sis, parsed.matype.sis);
assert_eq!(orig.matype.ccm, parsed.matype.ccm);
assert_eq!(orig.matype.issyi, parsed.matype.issyi);
assert_eq!(orig.matype.npd, parsed.matype.npd);
assert_eq!(orig.matype.ext, parsed.matype.ext);
assert_eq!(orig.matype.isi, parsed.matype.isi);
assert_eq!(orig.upl, parsed.upl);
assert_eq!(orig.sync, parsed.sync);
assert_eq!(orig.dfl, parsed.dfl);
assert_eq!(orig.syncd, parsed.syncd);
assert_eq!(orig.mode, parsed.mode);
}
#[test]
fn serialize_round_trip_nm_gcs() {
let orig = Bbheader {
matype: Matype {
ts_gs: TsGs::Gcs,
sis: true,
ccm: false,
issyi: false,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 0,
sync: 0x00,
dfl: 16384,
syncd: 0,
mode: Mode::Normal,
issy_in_header: None,
};
let buf = orig.serialize();
let parsed = Bbheader::parse(&buf).unwrap();
assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
assert_eq!(orig.matype.sis, parsed.matype.sis);
assert_eq!(orig.matype.ccm, parsed.matype.ccm);
assert_eq!(orig.dfl, parsed.dfl);
assert_eq!(orig.syncd, parsed.syncd);
assert_eq!(orig.mode, parsed.mode);
}
#[test]
fn serialize_crc8_always_matches_bytes_0_to_8() {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Gse,
sis: true,
ccm: true,
issyi: true,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 0,
sync: 0xFF,
dfl: 32768,
syncd: 0,
mode: Mode::Normal,
issy_in_header: None,
};
let buf = hdr.serialize();
let computed = crc8(&buf[..9]);
assert_eq!(computed ^ buf[9], 0); assert_eq!(buf[9], computed); }
#[test]
fn parse_detects_hem_via_crc_xor_1() {
let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.mode, Mode::HighEfficiency);
}
#[test]
fn parse_hem_extracts_matype_dfl_syncd() {
let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.mode, Mode::HighEfficiency);
assert_eq!(result.matype.ts_gs, TsGs::Ts);
assert!(result.matype.sis);
assert!(result.matype.ccm);
assert!(result.matype.issyi);
assert!(!result.matype.npd);
assert_eq!(result.matype.ext, 0);
assert_eq!(result.dfl, 48328);
assert_eq!(result.syncd, 0x0350);
}
#[test]
fn parse_hem_preserves_three_issy_bytes() {
let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
let result = Bbheader::parse(&hdr).unwrap();
let issy = result.issy_in_header.unwrap();
assert_eq!(issy, [0xa4, 0x28, 0xe2]);
}
#[test]
fn parse_hem_leaves_upl_bits_as_zero_and_sync_as_zero() {
let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
let result = Bbheader::parse(&hdr).unwrap();
assert_eq!(result.upl, 0);
assert_eq!(result.sync, 0);
}
#[test]
fn parse_hem_rejects_when_mode_xor_not_0_or_1() {
let mut hdr = [0u8; BBHEADER_LEN];
hdr[0] = 0xF0;
hdr[1] = 0x00;
hdr[2] = 0x00;
hdr[3] = 0x00;
hdr[4] = 0x00;
hdr[5] = 0x00;
hdr[6] = 0x00;
hdr[7] = 0x00;
hdr[8] = 0x00;
hdr[9] = crc8(&hdr[..9]) ^ 0x02; assert!(Bbheader::parse(&hdr).is_err());
}
#[test]
fn parse_same_bytes_different_mode_byte_produces_different_bbheader() {
let mut hdr1 = [0xF8, 0x00, 0x00, 0x00, 0xBC, 0xC8, 0x00, 0x03, 0x50, 0x00];
hdr1[9] = crc8(&hdr1[..9]); let mut hdr2 = hdr1;
hdr2[9] ^= 0x01;
let result1 = Bbheader::parse(&hdr1).unwrap();
let result2 = Bbheader::parse(&hdr2).unwrap();
assert_eq!(result1.mode, Mode::Normal);
assert_eq!(result2.mode, Mode::HighEfficiency);
}
#[test]
fn serialize_hem_round_trip() {
let orig = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: true,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 0, sync: 0, dfl: 48328,
syncd: 848,
mode: Mode::HighEfficiency,
issy_in_header: Some([0xA4, 0x28, 0xE2]),
};
let buf = orig.serialize();
let parsed = Bbheader::parse(&buf).unwrap();
assert_eq!(orig.mode, parsed.mode);
assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
assert_eq!(orig.dfl, parsed.dfl);
assert_eq!(orig.syncd, parsed.syncd);
assert_eq!(orig.issy_in_header, parsed.issy_in_header);
}
#[test]
fn serialize_hem_sets_crc_xor_mode_byte_correctly() {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: false,
npd: false,
ext: 0,
isi: 0x05,
},
upl: 0,
sync: 0,
dfl: 48000,
syncd: 0,
mode: Mode::HighEfficiency,
issy_in_header: Some([0x00, 0x00, 0x00]),
};
let buf = hdr.serialize();
let computed = crc8(&buf[..9]);
assert_eq!(buf[9], computed ^ 1);
}
#[test]
fn serialize_hem_with_issy_bytes_zero_writes_expected_layout() {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: true,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 0,
sync: 0,
dfl: 50000,
syncd: 100,
mode: Mode::HighEfficiency,
issy_in_header: Some([0x00, 0x00, 0x00]),
};
let buf = hdr.serialize();
let parsed = Bbheader::parse(&buf).unwrap();
assert_eq!(parsed.mode, Mode::HighEfficiency);
assert_eq!(parsed.issy_in_header, Some([0x00, 0x00, 0x00]));
assert_eq!(parsed.dfl, 50000);
assert_eq!(parsed.syncd, 100);
}
#[test]
fn parse_valid_dvbt2_hem_bbframe_rai() {
let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
assert_eq!(Bbheader::parse(&hdr).unwrap().dfl, 48328);
}
#[test]
fn exhaustive_tsgs_sweep() {
let mut matched = 0u16;
for byte in 0u8..=0xFF {
if let Ok(v) = TsGs::try_from(byte) {
assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
matched += 1;
}
}
assert_eq!(matched, 4, "expected 4 matched variants");
}
#[test]
fn exhaustive_mode_sweep() {
let mut matched = 0u16;
for byte in 0u8..=0xFF {
if let Ok(v) = Mode::try_from(byte) {
assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
matched += 1;
}
}
assert_eq!(matched, 2, "expected 2 matched variants");
}
}