1use broadcast_common::{Parse, Serialize};
7use num_enum::TryFromPrimitive;
8
9use crate::crc::crc8;
10use crate::error::Error;
11use crate::issy::{Issy, decode_issy_long, decode_issy_short};
12
13pub const BBHEADER_LEN: usize = 10;
15pub const DFL_MAX_BITS: u16 = 64800;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26#[repr(u8)]
27#[non_exhaustive]
28pub enum TsGs {
29 Gfps = 0b00,
31 Ts = 0b11,
33 Gcs = 0b01,
35 Gse = 0b10,
37}
38
39impl From<TsGs> for u8 {
40 fn from(t: TsGs) -> Self {
41 t as u8
42 }
43}
44
45impl From<num_enum::TryFromPrimitiveError<TsGs>> for Error {
46 fn from(e: num_enum::TryFromPrimitiveError<TsGs>) -> Self {
47 Error::UnsupportedTsGs { ts_gs: e.number }
48 }
49}
50
51impl TsGs {
52 #[must_use]
53 pub fn name(&self) -> &'static str {
55 match self {
56 Self::Gfps => "GFPS",
57 Self::Ts => "TS",
58 Self::Gcs => "GCS",
59 Self::Gse => "GSE",
60 }
61 }
62}
63
64broadcast_common::impl_spec_display!(TsGs);
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize))]
69#[repr(u8)]
70#[non_exhaustive]
71pub enum Mode {
72 Normal = 0,
74 HighEfficiency = 1,
76}
77
78impl From<num_enum::TryFromPrimitiveError<Mode>> for Error {
79 fn from(e: num_enum::TryFromPrimitiveError<Mode>) -> Self {
80 Error::InvalidMode { mode: e.number }
81 }
82}
83
84impl Mode {
85 #[must_use]
86 pub fn name(&self) -> &'static str {
88 match self {
89 Self::Normal => "Normal",
90 Self::HighEfficiency => "High Efficiency",
91 }
92 }
93}
94
95broadcast_common::impl_spec_display!(Mode);
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize))]
104pub struct Matype {
105 pub ts_gs: TsGs,
107 pub sis: bool,
109 pub ccm: bool,
111 pub issyi: bool,
113 pub npd: bool,
115 pub ext: u8,
117 pub isi: u8,
119}
120
121impl Matype {
122 const MASK_TS_GS: u8 = 0xC0;
123 const MASK_SIS: u8 = 0x20;
124 const MASK_CCM: u8 = 0x10;
125 const MASK_ISSYI: u8 = 0x08;
126 const MASK_NPD: u8 = 0x04;
127 const MASK_EXT: u8 = 0x03;
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize))]
137#[non_exhaustive]
138pub enum RollOff {
139 Alpha035,
141 Alpha025,
143 Alpha020,
145 Reserved,
147}
148
149impl RollOff {
150 #[must_use]
153 pub fn from_bits(bits: u8) -> Self {
154 match bits & 0x03 {
155 0 => Self::Alpha035,
156 1 => Self::Alpha025,
157 2 => Self::Alpha020,
158 _ => Self::Reserved,
159 }
160 }
161
162 #[must_use]
165 pub fn to_bits(self) -> u8 {
166 match self {
167 Self::Alpha035 => 0,
168 Self::Alpha025 => 1,
169 Self::Alpha020 => 2,
170 Self::Reserved => 3,
171 }
172 }
173
174 #[must_use]
177 pub fn name(self) -> &'static str {
178 match self {
179 Self::Alpha035 => "α=0.35",
180 Self::Alpha025 => "α=0.25",
181 Self::Alpha020 => "α=0.20",
182 Self::Reserved => "reserved/S2X-low",
183 }
184 }
185}
186
187broadcast_common::impl_spec_display!(RollOff);
188
189impl Matype {
190 #[must_use]
194 pub fn roll_off(&self) -> RollOff {
195 RollOff::from_bits(self.ext & 0x03)
196 }
197}
198
199impl TryFrom<[u8; 2]> for Matype {
200 type Error = Error;
201
202 fn try_from(bytes: [u8; 2]) -> Result<Self, Self::Error> {
203 let matype1 = bytes[0];
204 let matype2 = bytes[1];
205
206 let ts_gs = TsGs::try_from((matype1 & Matype::MASK_TS_GS) >> 6)?;
207 let sis = matype1 & Matype::MASK_SIS != 0;
208 let ccm = matype1 & Matype::MASK_CCM != 0;
209 let issyi = matype1 & Matype::MASK_ISSYI != 0;
210 let npd = matype1 & Matype::MASK_NPD != 0;
211 let ext = matype1 & Matype::MASK_EXT;
212
213 Ok(Matype {
214 ts_gs,
215 sis,
216 ccm,
217 issyi,
218 npd,
219 ext,
220 isi: matype2,
221 })
222 }
223}
224
225impl From<Matype> for [u8; 2] {
226 fn from(m: Matype) -> Self {
227 let mut matype1: u8 = 0;
231 matype1 |= (u8::from(m.ts_gs) << 6) & Matype::MASK_TS_GS;
232 if m.sis {
233 matype1 |= Matype::MASK_SIS;
234 }
235 if m.ccm {
236 matype1 |= Matype::MASK_CCM;
237 }
238 if m.issyi {
239 matype1 |= Matype::MASK_ISSYI;
240 }
241 if m.npd {
242 matype1 |= Matype::MASK_NPD;
243 }
244 matype1 |= m.ext & Matype::MASK_EXT;
245 [matype1, m.isi]
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258#[cfg_attr(feature = "serde", derive(serde::Serialize))]
259pub struct Bbheader {
260 pub matype: Matype,
262 pub upl: u16,
264 pub sync: u8,
266 pub dfl: u16,
268 pub syncd: u16,
270 pub mode: Mode,
272 pub issy_in_header: Option<[u8; 3]>,
274}
275
276impl<'a> Parse<'a> for Bbheader {
277 type Error = Error;
278
279 fn parse(bytes: &'a [u8]) -> Result<Self, Self::Error> {
280 let (hdr, _) = bytes
281 .split_first_chunk::<BBHEADER_LEN>()
282 .ok_or(Error::BufferTooShort {
283 need: BBHEADER_LEN,
284 have: bytes.len(),
285 what: "BBHEADER",
286 })?;
287
288 let matype_bytes = [hdr[0], hdr[1]];
289 let matype = Matype::try_from(matype_bytes)?;
290 let dfl = u16::from_be_bytes([hdr[4], hdr[5]]);
291 let syncd = u16::from_be_bytes([hdr[7], hdr[8]]);
292 let crc_stored = hdr[9];
293
294 if dfl > DFL_MAX_BITS {
295 return Err(Error::DflOutOfRange {
296 dfl,
297 max: DFL_MAX_BITS,
298 });
299 }
300
301 let computed_crc = crc8(&hdr[..9]);
308 let mode_val = computed_crc ^ crc_stored;
309 let mode = Mode::try_from(mode_val)?;
310
311 let (upl, sync, issy_in_header) = match mode {
312 Mode::Normal => (u16::from_be_bytes([hdr[2], hdr[3]]), hdr[6], None),
313 Mode::HighEfficiency => {
314 (0, 0, Some([hdr[2], hdr[3], hdr[6]]))
317 }
318 };
319
320 Ok(Bbheader {
321 matype,
322 upl,
323 sync,
324 dfl,
325 syncd,
326 mode,
327 issy_in_header,
328 })
329 }
330}
331
332impl Serialize for Bbheader {
333 type Error = Error;
334
335 fn serialized_len(&self) -> usize {
336 BBHEADER_LEN
337 }
338
339 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, Self::Error> {
340 if buf.len() < BBHEADER_LEN {
341 return Err(Error::OutputBufferTooSmall {
342 need: BBHEADER_LEN,
343 have: buf.len(),
344 });
345 }
346
347 let ma = <[u8; 2]>::from(self.matype);
348 buf[0] = ma[0];
349 buf[1] = ma[1];
350
351 match self.mode {
352 Mode::Normal => {
353 let upl = self.upl.to_be_bytes();
354 buf[2] = upl[0];
355 buf[3] = upl[1];
356 let dfl = self.dfl.to_be_bytes();
357 buf[4] = dfl[0];
358 buf[5] = dfl[1];
359 buf[6] = self.sync;
360 let syncd = self.syncd.to_be_bytes();
361 buf[7] = syncd[0];
362 buf[8] = syncd[1];
363 }
364 Mode::HighEfficiency => {
365 if let Some(issy) = self.issy_in_header {
366 buf[2] = issy[0];
367 buf[3] = issy[1];
368 let dfl = self.dfl.to_be_bytes();
369 buf[4] = dfl[0];
370 buf[5] = dfl[1];
371 buf[6] = issy[2];
372 let syncd = self.syncd.to_be_bytes();
373 buf[7] = syncd[0];
374 buf[8] = syncd[1];
375 } else {
376 buf[2] = 0;
381 buf[3] = 0;
382 let dfl = self.dfl.to_be_bytes();
383 buf[4] = dfl[0];
384 buf[5] = dfl[1];
385 buf[6] = 0;
386 let syncd = self.syncd.to_be_bytes();
387 buf[7] = syncd[0];
388 buf[8] = syncd[1];
389 }
390 }
391 }
392
393 let computed = crc8(&buf[..9]);
395 buf[9] = computed ^ (self.mode as u8);
396
397 Ok(BBHEADER_LEN)
398 }
399}
400
401impl Bbheader {
402 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
408 <Self as Parse>::parse(bytes)
409 }
410
411 pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
413 let v = <Self as Serialize>::to_bytes(self);
414 let mut buf = [0u8; BBHEADER_LEN];
415 buf.copy_from_slice(&v);
416 buf
417 }
418
419 #[must_use]
429 pub fn issy(&self) -> Option<Issy> {
430 let bytes = self.issy_in_header?;
431 decode_issy_long(bytes)
432 .ok()
433 .or_else(|| decode_issy_short([bytes[0], bytes[1]]).ok())
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn parse_rejects_buffer_shorter_than_10() {
443 assert!(Bbheader::parse(&[0u8; 9]).is_err());
444 }
445
446 #[test]
447 fn parse_nm_ts_extracts_all_fields() {
448 let mut hdr = [0u8; BBHEADER_LEN];
454 hdr[0] = 0xF0; hdr[1] = 0x00; let upl: u16 = 0x07D0; hdr[2..4].copy_from_slice(&upl.to_be_bytes());
458 let dfl: u16 = 0xBC00; hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
460 hdr[6] = 0x47; let syncd: u16 = 0x0000; hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
463 hdr[9] = crc8(&hdr[..9]); let result = Bbheader::parse(&hdr).unwrap();
466 assert_eq!(result.mode, Mode::Normal);
467 assert_eq!(result.matype.ts_gs, TsGs::Ts);
468 assert!(result.matype.sis);
469 assert!(result.matype.ccm);
470 assert!(!result.matype.issyi);
471 assert!(!result.matype.npd);
472 assert_eq!(result.matype.ext, 0);
473 assert_eq!(result.matype.isi, 0x00);
474 assert_eq!(result.upl, upl);
475 assert_eq!(result.sync, 0x47);
476 assert_eq!(result.dfl, dfl);
477 assert_eq!(result.syncd, syncd);
478 }
479
480 #[test]
481 fn parse_nm_gcs_treats_sync_as_transport_protocol_byte() {
482 let mut hdr = [0u8; BBHEADER_LEN];
483 hdr[0] = 0x50; hdr[1] = 0x00;
485 let upl: u16 = 0x0000; hdr[2..4].copy_from_slice(&upl.to_be_bytes());
487 let dfl: u16 = 0x4000; hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
489 hdr[6] = 0x3C; let syncd: u16 = 0x0000;
491 hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
492 hdr[9] = crc8(&hdr[..9]);
493
494 let result = Bbheader::parse(&hdr).unwrap();
495 assert_eq!(result.mode, Mode::Normal);
496 assert_eq!(result.matype.ts_gs, TsGs::Gcs);
497 assert_eq!(result.sync, 0x3C);
498 assert_eq!(result.upl, upl);
499 }
500
501 #[test]
502 fn parse_detects_nm_via_crc_xor_0() {
503 let mut hdr = [0u8; BBHEADER_LEN];
505 hdr[0] = 0xF0;
506 hdr[1] = 0x00;
507 hdr[2] = 0x07;
508 hdr[3] = 0xD0; hdr[4] = 0xBC;
510 hdr[5] = 0x00; hdr[6] = 0x47; hdr[7] = 0x00;
513 hdr[8] = 0x00; hdr[9] = crc8(&hdr[..9]); let result = Bbheader::parse(&hdr).unwrap();
517 assert_eq!(result.mode, Mode::Normal);
518 }
519
520 #[test]
521 fn parse_rejects_crc_mismatch_in_both_modes() {
522 let mut hdr = [0u8; BBHEADER_LEN];
523 hdr[0] = 0xF0;
524 hdr[1] = 0x00;
525 hdr[2] = 0x07;
526 hdr[3] = 0xD0;
527 hdr[4] = 0xBC;
528 hdr[5] = 0x00;
529 hdr[6] = 0x47;
530 hdr[7] = 0x00;
531 hdr[8] = 0x00;
532 hdr[9] = 0xFF; let result = Bbheader::parse(&hdr);
535 assert!(result.is_err());
536 }
537
538 #[test]
539 fn parse_matype_extracts_ts_gs_enum_for_each_of_gfps_ts_gcs_gse() {
540 for (ts_gs_val, expected) in [
541 (0b00, TsGs::Gfps),
542 (0b01, TsGs::Gcs),
543 (0b10, TsGs::Gse),
544 (0b11, TsGs::Ts),
545 ] {
546 let ma1 = (ts_gs_val << 6) | 0x30; let mut hdr = [0u8; BBHEADER_LEN];
548 hdr[0] = ma1;
549 hdr[1] = 0x00;
550 hdr[2..9].copy_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
551 hdr[9] = crc8(&hdr[..9]);
552 let result = Bbheader::parse(&hdr).unwrap();
553 assert_eq!(result.matype.ts_gs, expected, "ts_gs=0x{ts_gs_val:02b}");
554 }
555 }
556
557 #[test]
558 fn parse_matype_extracts_sis_isi_on_multi_stream() {
559 let mut hdr = [0u8; BBHEADER_LEN];
561 hdr[0] = 0xD0; hdr[1] = 0xAB; hdr[2] = 0x07;
564 hdr[3] = 0xD0;
565 hdr[4] = 0xBC;
566 hdr[5] = 0x00;
567 hdr[6] = 0x47;
568 hdr[7] = 0x00;
569 hdr[8] = 0x00;
570 hdr[9] = crc8(&hdr[..9]);
571
572 let result = Bbheader::parse(&hdr).unwrap();
573 assert!(!result.matype.sis);
574 assert_eq!(result.matype.isi, 0xAB);
575 }
576
577 #[test]
578 fn parse_matype_extracts_roll_off_2_bits_as_ext_for_s2_context() {
579 let mut hdr = [0u8; BBHEADER_LEN];
581 hdr[0] = 0xF3; hdr[1] = 0x00;
583 hdr[2] = 0x07;
584 hdr[3] = 0xD0;
585 hdr[4] = 0xBC;
586 hdr[5] = 0x00;
587 hdr[6] = 0x47;
588 hdr[7] = 0x00;
589 hdr[8] = 0x00;
590 hdr[9] = crc8(&hdr[..9]);
591
592 let result = Bbheader::parse(&hdr).unwrap();
593 assert_eq!(result.matype.ext, 0b11);
594 }
595
596 #[test]
597 fn serialize_nm_produces_expected_bytes() {
598 let hdr = Bbheader {
599 matype: Matype {
600 ts_gs: TsGs::Ts,
601 sis: true,
602 ccm: true,
603 issyi: false,
604 npd: false,
605 ext: 0,
606 isi: 0x00,
607 },
608 upl: 188 * 8,
609 sync: 0x47,
610 dfl: 48328,
611 syncd: 0,
612 mode: Mode::Normal,
613 issy_in_header: None,
614 };
615 let buf = hdr.serialize();
616
617 let parsed = Bbheader::parse(&buf).unwrap();
618 assert_eq!(parsed.matype.ts_gs, TsGs::Ts);
619 assert!(parsed.matype.sis);
620 assert!(parsed.matype.ccm);
621 assert_eq!(parsed.upl, 188 * 8);
622 assert_eq!(parsed.sync, 0x47);
623 assert_eq!(parsed.dfl, 48328);
624 assert_eq!(parsed.syncd, 0);
625 assert_eq!(parsed.mode, Mode::Normal);
626 }
627
628 #[test]
629 fn serialize_round_trip_nm_ts_preserves_every_field() {
630 let orig = Bbheader {
631 matype: Matype {
632 ts_gs: TsGs::Ts,
633 sis: true,
634 ccm: true,
635 issyi: true,
636 npd: false,
637 ext: 0,
638 isi: 0x00,
639 },
640 upl: 1504,
641 sync: 0x47,
642 dfl: 48328,
643 syncd: 0,
644 mode: Mode::Normal,
645 issy_in_header: None,
646 };
647 let buf = orig.serialize();
648 let parsed = Bbheader::parse(&buf).unwrap();
649 assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
650 assert_eq!(orig.matype.sis, parsed.matype.sis);
651 assert_eq!(orig.matype.ccm, parsed.matype.ccm);
652 assert_eq!(orig.matype.issyi, parsed.matype.issyi);
653 assert_eq!(orig.matype.npd, parsed.matype.npd);
654 assert_eq!(orig.matype.ext, parsed.matype.ext);
655 assert_eq!(orig.matype.isi, parsed.matype.isi);
656 assert_eq!(orig.upl, parsed.upl);
657 assert_eq!(orig.sync, parsed.sync);
658 assert_eq!(orig.dfl, parsed.dfl);
659 assert_eq!(orig.syncd, parsed.syncd);
660 assert_eq!(orig.mode, parsed.mode);
661 }
662
663 #[test]
664 fn serialize_round_trip_nm_gcs() {
665 let orig = Bbheader {
666 matype: Matype {
667 ts_gs: TsGs::Gcs,
668 sis: true,
669 ccm: false,
670 issyi: false,
671 npd: false,
672 ext: 0,
673 isi: 0x00,
674 },
675 upl: 0,
676 sync: 0x00,
677 dfl: 16384,
678 syncd: 0,
679 mode: Mode::Normal,
680 issy_in_header: None,
681 };
682 let buf = orig.serialize();
683 let parsed = Bbheader::parse(&buf).unwrap();
684 assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
685 assert_eq!(orig.matype.sis, parsed.matype.sis);
686 assert_eq!(orig.matype.ccm, parsed.matype.ccm);
687 assert_eq!(orig.dfl, parsed.dfl);
688 assert_eq!(orig.syncd, parsed.syncd);
689 assert_eq!(orig.mode, parsed.mode);
690 }
691
692 #[test]
693 fn serialize_crc8_always_matches_bytes_0_to_8() {
694 let hdr = Bbheader {
695 matype: Matype {
696 ts_gs: TsGs::Gse,
697 sis: true,
698 ccm: true,
699 issyi: true,
700 npd: false,
701 ext: 0,
702 isi: 0x00,
703 },
704 upl: 0,
705 sync: 0xFF,
706 dfl: 32768,
707 syncd: 0,
708 mode: Mode::Normal,
709 issy_in_header: None,
710 };
711 let buf = hdr.serialize();
712 let computed = crc8(&buf[..9]);
713 assert_eq!(computed ^ buf[9], 0); assert_eq!(buf[9], computed); }
716
717 #[test]
718 fn parse_detects_hem_via_crc_xor_1() {
719 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
721 let result = Bbheader::parse(&hdr).unwrap();
722 assert_eq!(result.mode, Mode::HighEfficiency);
723 }
724
725 #[test]
726 fn parse_hem_extracts_matype_dfl_syncd() {
727 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
728 let result = Bbheader::parse(&hdr).unwrap();
729 assert_eq!(result.mode, Mode::HighEfficiency);
730 assert_eq!(result.matype.ts_gs, TsGs::Ts);
731 assert!(result.matype.sis);
732 assert!(result.matype.ccm);
733 assert!(result.matype.issyi);
734 assert!(!result.matype.npd);
735 assert_eq!(result.matype.ext, 0);
736 assert_eq!(result.dfl, 48328);
737 assert_eq!(result.syncd, 0x0350);
738 }
739
740 #[test]
741 fn parse_hem_preserves_three_issy_bytes() {
742 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
743 let result = Bbheader::parse(&hdr).unwrap();
744 let issy = result.issy_in_header.unwrap();
745 assert_eq!(issy, [0xa4, 0x28, 0xe2]);
747 }
748
749 #[test]
750 fn issy_accessor_decodes_hem_iscr_long() {
751 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
755 let result = Bbheader::parse(&hdr).unwrap();
756 let issy = result.issy().expect("ISSY decode from HEM header");
757 assert_eq!(issy, Issy::IscrLong(0x0024_28E2));
758 }
759
760 #[test]
761 fn issy_accessor_returns_none_for_nm() {
762 let mut hdr = [0u8; BBHEADER_LEN];
763 hdr[0] = 0xF0;
764 hdr[1] = 0x00;
765 hdr[2] = 0x07;
766 hdr[3] = 0xD0;
767 hdr[4] = 0xBC;
768 hdr[5] = 0x00;
769 hdr[6] = 0x47;
770 hdr[7] = 0x00;
771 hdr[8] = 0x00;
772 hdr[9] = crc8(&hdr[..9]);
773 let result = Bbheader::parse(&hdr).unwrap();
774 assert_eq!(result.mode, Mode::Normal);
775 assert!(result.issy().is_none());
776 }
777
778 #[test]
779 fn issy_accessor_falls_back_to_short_form() {
780 let hdr = Bbheader {
784 matype: Matype {
785 ts_gs: TsGs::Ts,
786 sis: true,
787 ccm: true,
788 issyi: true,
789 npd: false,
790 ext: 0,
791 isi: 0x00,
792 },
793 upl: 0,
794 sync: 0,
795 dfl: 50000,
796 syncd: 100,
797 mode: Mode::HighEfficiency,
798 issy_in_header: Some([0x7A, 0xBC, 0x00]),
799 };
800 let issy = hdr.issy().expect("short-form ISSY fallback");
801 assert_eq!(issy, Issy::IscrShort(0x7ABC));
802 }
803
804 #[test]
805 fn parse_hem_leaves_upl_bits_as_zero_and_sync_as_zero() {
806 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
807 let result = Bbheader::parse(&hdr).unwrap();
808 assert_eq!(result.upl, 0);
809 assert_eq!(result.sync, 0);
810 }
811
812 #[test]
813 fn parse_hem_rejects_when_mode_xor_not_0_or_1() {
814 let mut hdr = [0u8; BBHEADER_LEN];
816 hdr[0] = 0xF0;
817 hdr[1] = 0x00;
818 hdr[2] = 0x00;
819 hdr[3] = 0x00;
820 hdr[4] = 0x00;
821 hdr[5] = 0x00;
822 hdr[6] = 0x00;
823 hdr[7] = 0x00;
824 hdr[8] = 0x00;
825 hdr[9] = crc8(&hdr[..9]) ^ 0x02; assert!(Bbheader::parse(&hdr).is_err());
827 }
828
829 #[test]
830 fn parse_same_bytes_different_mode_byte_produces_different_bbheader() {
831 let mut hdr1 = [0xF8, 0x00, 0x00, 0x00, 0xBC, 0xC8, 0x00, 0x03, 0x50, 0x00];
833 hdr1[9] = crc8(&hdr1[..9]); let mut hdr2 = hdr1;
835 hdr2[9] ^= 0x01; let result1 = Bbheader::parse(&hdr1).unwrap();
838 let result2 = Bbheader::parse(&hdr2).unwrap();
839 assert_eq!(result1.mode, Mode::Normal);
840 assert_eq!(result2.mode, Mode::HighEfficiency);
841 }
842
843 #[test]
844 fn serialize_hem_round_trip() {
845 let orig = Bbheader {
846 matype: Matype {
847 ts_gs: TsGs::Ts,
848 sis: true,
849 ccm: true,
850 issyi: true,
851 npd: false,
852 ext: 0,
853 isi: 0x00,
854 },
855 upl: 0, sync: 0, dfl: 48328,
858 syncd: 848,
859 mode: Mode::HighEfficiency,
860 issy_in_header: Some([0xA4, 0x28, 0xE2]),
861 };
862 let buf = orig.serialize();
863 let parsed = Bbheader::parse(&buf).unwrap();
864 assert_eq!(orig.mode, parsed.mode);
865 assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
866 assert_eq!(orig.dfl, parsed.dfl);
867 assert_eq!(orig.syncd, parsed.syncd);
868 assert_eq!(orig.issy_in_header, parsed.issy_in_header);
869 }
870
871 #[test]
872 fn serialize_hem_sets_crc_xor_mode_byte_correctly() {
873 let hdr = Bbheader {
874 matype: Matype {
875 ts_gs: TsGs::Ts,
876 sis: true,
877 ccm: true,
878 issyi: false,
879 npd: false,
880 ext: 0,
881 isi: 0x05,
882 },
883 upl: 0,
884 sync: 0,
885 dfl: 48000,
886 syncd: 0,
887 mode: Mode::HighEfficiency,
888 issy_in_header: Some([0x00, 0x00, 0x00]),
889 };
890 let buf = hdr.serialize();
891 let computed = crc8(&buf[..9]);
892 assert_eq!(buf[9], computed ^ 1);
894 }
895
896 #[test]
897 fn serialize_hem_with_issy_bytes_zero_writes_expected_layout() {
898 let hdr = Bbheader {
899 matype: Matype {
900 ts_gs: TsGs::Ts,
901 sis: true,
902 ccm: true,
903 issyi: true,
904 npd: false,
905 ext: 0,
906 isi: 0x00,
907 },
908 upl: 0,
909 sync: 0,
910 dfl: 50000,
911 syncd: 100,
912 mode: Mode::HighEfficiency,
913 issy_in_header: Some([0x00, 0x00, 0x00]),
914 };
915 let buf = hdr.serialize();
916 let parsed = Bbheader::parse(&buf).unwrap();
917 assert_eq!(parsed.mode, Mode::HighEfficiency);
918 assert_eq!(parsed.issy_in_header, Some([0x00, 0x00, 0x00]));
919 assert_eq!(parsed.dfl, 50000);
920 assert_eq!(parsed.syncd, 100);
921 }
922
923 #[test]
924 fn parse_valid_dvbt2_hem_bbframe_rai() {
925 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
927 assert_eq!(Bbheader::parse(&hdr).unwrap().dfl, 48328);
928 }
929
930 #[test]
931 fn exhaustive_tsgs_sweep() {
932 let mut matched = 0u16;
933 for byte in 0u8..=0xFF {
934 if let Ok(v) = TsGs::try_from(byte) {
935 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
936 matched += 1;
937 }
938 }
939 assert_eq!(matched, 4, "expected 4 matched variants");
940 }
941
942 #[test]
943 fn exhaustive_mode_sweep() {
944 let mut matched = 0u16;
945 for byte in 0u8..=0xFF {
946 if let Ok(v) = Mode::try_from(byte) {
947 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
948 matched += 1;
949 }
950 }
951 assert_eq!(matched, 2, "expected 2 matched variants");
952 }
953
954 #[test]
955 fn trait_parse_and_serialize_round_trip() {
956 let orig = Bbheader {
957 matype: Matype {
958 ts_gs: TsGs::Gse,
959 sis: true,
960 ccm: true,
961 issyi: true,
962 npd: false,
963 ext: 0,
964 isi: 0x00,
965 },
966 upl: 0,
967 sync: 0xFF,
968 dfl: 32768,
969 syncd: 0,
970 mode: Mode::Normal,
971 issy_in_header: None,
972 };
973 let v = <Bbheader as Serialize>::to_bytes(&orig);
974 let parsed = <Bbheader as Parse>::parse(&v).unwrap();
975 assert_eq!(orig.matype, parsed.matype);
976 assert_eq!(orig.upl, parsed.upl);
977 assert_eq!(orig.sync, parsed.sync);
978 assert_eq!(orig.dfl, parsed.dfl);
979 assert_eq!(orig.syncd, parsed.syncd);
980 assert_eq!(orig.mode, parsed.mode);
981 }
982
983 #[test]
984 fn serialize_into_hem_no_issy_is_deterministic_regardless_of_buffer_content() {
985 let hdr = Bbheader {
989 matype: Matype {
990 ts_gs: TsGs::Ts,
991 sis: true,
992 ccm: true,
993 issyi: false,
994 npd: false,
995 ext: 0,
996 isi: 0x00,
997 },
998 upl: 0,
999 sync: 0,
1000 dfl: 48000,
1001 syncd: 256,
1002 mode: Mode::HighEfficiency,
1003 issy_in_header: None,
1004 };
1005
1006 let clean = <Bbheader as Serialize>::to_bytes(&hdr);
1008
1009 let mut dirty = [0xFFu8; BBHEADER_LEN];
1012 hdr.serialize_into(&mut dirty).unwrap();
1013
1014 assert_eq!(
1015 clean.as_slice(),
1016 dirty.as_slice(),
1017 "serialize_into into dirty buffer must produce identical bytes to to_bytes()"
1018 );
1019
1020 let parsed = Bbheader::parse(&dirty).unwrap();
1024 assert_eq!(parsed.mode, Mode::HighEfficiency);
1025 assert_eq!(parsed.issy_in_header, Some([0, 0, 0]));
1026 assert_eq!(parsed.dfl, 48000);
1027 assert_eq!(parsed.syncd, 256);
1028 }
1029
1030 #[test]
1031 fn serialize_into_rejects_buffer_too_small() {
1032 let hdr = Bbheader {
1033 matype: Matype {
1034 ts_gs: TsGs::Ts,
1035 sis: true,
1036 ccm: true,
1037 issyi: false,
1038 npd: false,
1039 ext: 0,
1040 isi: 0x00,
1041 },
1042 upl: 0,
1043 sync: 0x47,
1044 dfl: 0,
1045 syncd: 0,
1046 mode: Mode::Normal,
1047 issy_in_header: None,
1048 };
1049 let mut small = [0u8; BBHEADER_LEN - 1];
1050 let err = hdr.serialize_into(&mut small).unwrap_err();
1051 assert_eq!(
1052 err,
1053 Error::OutputBufferTooSmall {
1054 need: BBHEADER_LEN,
1055 have: small.len(),
1056 }
1057 );
1058 }
1059}