use broadcast_common::{Parse, Serialize};
use num_enum::TryFromPrimitive;
use crate::crc::crc8;
use crate::error::Error;
use crate::issy::{Issy, decode_issy_long, decode_issy_short};
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)]
#[non_exhaustive]
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 TsGs {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Gfps => "GFPS",
Self::Ts => "TS",
Self::Gcs => "GCS",
Self::Gse => "GSE",
}
}
}
broadcast_common::impl_spec_display!(TsGs);
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[repr(u8)]
#[non_exhaustive]
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 }
}
}
impl Mode {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Normal => "Normal",
Self::HighEfficiency => "High Efficiency",
}
}
}
broadcast_common::impl_spec_display!(Mode);
#[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;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum RollOff {
Alpha035,
Alpha025,
Alpha020,
Reserved,
}
impl RollOff {
#[must_use]
pub fn from_bits(bits: u8) -> Self {
match bits & 0x03 {
0 => Self::Alpha035,
1 => Self::Alpha025,
2 => Self::Alpha020,
_ => Self::Reserved,
}
}
#[must_use]
pub fn to_bits(self) -> u8 {
match self {
Self::Alpha035 => 0,
Self::Alpha025 => 1,
Self::Alpha020 => 2,
Self::Reserved => 3,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Alpha035 => "α=0.35",
Self::Alpha025 => "α=0.25",
Self::Alpha020 => "α=0.20",
Self::Reserved => "reserved/S2X-low",
}
}
}
broadcast_common::impl_spec_display!(RollOff);
impl Matype {
#[must_use]
pub fn roll_off(&self) -> RollOff {
RollOff::from_bits(self.ext & 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<'a> Parse<'a> for Bbheader {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let (hdr, _) = bytes
.split_first_chunk::<BBHEADER_LEN>()
.ok_or(Error::BufferTooShort {
need: BBHEADER_LEN,
have: bytes.len(),
what: "BBHEADER",
})?;
let matype_bytes = [hdr[0], hdr[1]];
let matype = Matype::try_from(matype_bytes)?;
let dfl = u16::from_be_bytes([hdr[4], hdr[5]]);
let syncd = u16::from_be_bytes([hdr[7], hdr[8]]);
let crc_stored = hdr[9];
if dfl > DFL_MAX_BITS {
return Err(Error::DflOutOfRange {
dfl,
max: DFL_MAX_BITS,
});
}
let computed_crc = crc8(&hdr[..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([hdr[2], hdr[3]]), hdr[6], None),
Mode::HighEfficiency => {
(0, 0, Some([hdr[2], hdr[3], hdr[6]]))
}
};
Ok(Bbheader {
matype,
upl,
sync,
dfl,
syncd,
mode,
issy_in_header,
})
}
}
impl Serialize for Bbheader {
type Error = Error;
fn serialized_len(&self) -> usize {
BBHEADER_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, Self::Error> {
if buf.len() < BBHEADER_LEN {
return Err(Error::OutputBufferTooSmall {
need: BBHEADER_LEN,
have: buf.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 {
buf[2] = 0;
buf[3] = 0;
let dfl = self.dfl.to_be_bytes();
buf[4] = dfl[0];
buf[5] = dfl[1];
buf[6] = 0;
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);
Ok(BBHEADER_LEN)
}
}
impl Bbheader {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
<Self as Parse>::parse(bytes)
}
pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
let v = <Self as Serialize>::to_bytes(self);
let mut buf = [0u8; BBHEADER_LEN];
buf.copy_from_slice(&v);
buf
}
#[must_use]
pub fn issy(&self) -> Option<Issy> {
let bytes = self.issy_in_header?;
decode_issy_long(bytes)
.ok()
.or_else(|| decode_issy_short([bytes[0], bytes[1]]).ok())
}
}
#[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{ts_gs_val:02b}");
}
}
#[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 issy_accessor_decodes_hem_iscr_long() {
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().expect("ISSY decode from HEM header");
assert_eq!(issy, Issy::IscrLong(0x0024_28E2));
}
#[test]
fn issy_accessor_returns_none_for_nm() {
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);
assert!(result.issy().is_none());
}
#[test]
fn issy_accessor_falls_back_to_short_form() {
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([0x7A, 0xBC, 0x00]),
};
let issy = hdr.issy().expect("short-form ISSY fallback");
assert_eq!(issy, Issy::IscrShort(0x7ABC));
}
#[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");
}
#[test]
fn trait_parse_and_serialize_round_trip() {
let orig = 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 v = <Bbheader as Serialize>::to_bytes(&orig);
let parsed = <Bbheader as Parse>::parse(&v).unwrap();
assert_eq!(orig.matype, parsed.matype);
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_into_hem_no_issy_is_deterministic_regardless_of_buffer_content() {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: false,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 0,
sync: 0,
dfl: 48000,
syncd: 256,
mode: Mode::HighEfficiency,
issy_in_header: None,
};
let clean = <Bbheader as Serialize>::to_bytes(&hdr);
let mut dirty = [0xFFu8; BBHEADER_LEN];
hdr.serialize_into(&mut dirty).unwrap();
assert_eq!(
clean.as_slice(),
dirty.as_slice(),
"serialize_into into dirty buffer must produce identical bytes to to_bytes()"
);
let parsed = Bbheader::parse(&dirty).unwrap();
assert_eq!(parsed.mode, Mode::HighEfficiency);
assert_eq!(parsed.issy_in_header, Some([0, 0, 0]));
assert_eq!(parsed.dfl, 48000);
assert_eq!(parsed.syncd, 256);
}
#[test]
fn serialize_into_rejects_buffer_too_small() {
let hdr = Bbheader {
matype: Matype {
ts_gs: TsGs::Ts,
sis: true,
ccm: true,
issyi: false,
npd: false,
ext: 0,
isi: 0x00,
},
upl: 0,
sync: 0x47,
dfl: 0,
syncd: 0,
mode: Mode::Normal,
issy_in_header: None,
};
let mut small = [0u8; BBHEADER_LEN - 1];
let err = hdr.serialize_into(&mut small).unwrap_err();
assert_eq!(
err,
Error::OutputBufferTooSmall {
need: BBHEADER_LEN,
have: small.len(),
}
);
}
}