1use num_enum::TryFromPrimitive;
7
8use crate::crc::crc8;
9use crate::error::Error;
10
11pub const BBHEADER_LEN: usize = 10;
13pub const DFL_MAX_BITS: u16 = 64800;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[repr(u8)]
25pub enum TsGs {
26 Gfps = 0b00,
28 Ts = 0b11,
30 Gcs = 0b01,
32 Gse = 0b10,
34}
35
36impl From<TsGs> for u8 {
37 fn from(t: TsGs) -> Self {
38 t as u8
39 }
40}
41
42impl From<num_enum::TryFromPrimitiveError<TsGs>> for Error {
43 fn from(e: num_enum::TryFromPrimitiveError<TsGs>) -> Self {
44 Error::UnsupportedTsGs { ts_gs: e.number }
45 }
46}
47
48impl std::fmt::Display for TsGs {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "TsGs::{self:?}")
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[repr(u8)]
58pub enum Mode {
59 Normal = 0,
61 HighEfficiency = 1,
63}
64
65impl From<num_enum::TryFromPrimitiveError<Mode>> for Error {
66 fn from(e: num_enum::TryFromPrimitiveError<Mode>) -> Self {
67 Error::InvalidMode { mode: e.number }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct Matype {
79 pub ts_gs: TsGs,
81 pub sis: bool,
83 pub ccm: bool,
85 pub issyi: bool,
87 pub npd: bool,
89 pub ext: u8,
91 pub isi: u8,
93}
94
95impl Matype {
96 const MASK_TS_GS: u8 = 0xC0;
97 const MASK_SIS: u8 = 0x20;
98 const MASK_CCM: u8 = 0x10;
99 const MASK_ISSYI: u8 = 0x08;
100 const MASK_NPD: u8 = 0x04;
101 const MASK_EXT: u8 = 0x03;
102}
103
104impl TryFrom<[u8; 2]> for Matype {
105 type Error = Error;
106
107 fn try_from(bytes: [u8; 2]) -> Result<Self, Self::Error> {
108 let matype1 = bytes[0];
109 let matype2 = bytes[1];
110
111 let ts_gs = TsGs::try_from((matype1 & Matype::MASK_TS_GS) >> 6)?;
112 let sis = matype1 & Matype::MASK_SIS != 0;
113 let ccm = matype1 & Matype::MASK_CCM != 0;
114 let issyi = matype1 & Matype::MASK_ISSYI != 0;
115 let npd = matype1 & Matype::MASK_NPD != 0;
116 let ext = matype1 & Matype::MASK_EXT;
117
118 Ok(Matype {
119 ts_gs,
120 sis,
121 ccm,
122 issyi,
123 npd,
124 ext,
125 isi: matype2,
126 })
127 }
128}
129
130impl From<Matype> for [u8; 2] {
131 fn from(m: Matype) -> Self {
132 let mut matype1: u8 = 0;
136 matype1 |= (u8::from(m.ts_gs) << 6) & Matype::MASK_TS_GS;
137 if m.sis {
138 matype1 |= Matype::MASK_SIS;
139 }
140 if m.ccm {
141 matype1 |= Matype::MASK_CCM;
142 }
143 if m.issyi {
144 matype1 |= Matype::MASK_ISSYI;
145 }
146 if m.npd {
147 matype1 |= Matype::MASK_NPD;
148 }
149 matype1 |= m.ext & Matype::MASK_EXT;
150 [matype1, m.isi]
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub struct Bbheader {
165 pub matype: Matype,
167 pub upl: u16,
169 pub sync: u8,
171 pub dfl: u16,
173 pub syncd: u16,
175 pub mode: Mode,
177 pub issy_in_header: Option<[u8; 3]>,
179}
180
181impl Bbheader {
182 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
188 if bytes.len() < BBHEADER_LEN {
189 return Err(Error::BufferTooShort {
190 need: BBHEADER_LEN,
191 have: bytes.len(),
192 });
193 }
194
195 let matype_bytes = [bytes[0], bytes[1]];
196 let matype = Matype::try_from(matype_bytes)?;
197 let dfl = u16::from_be_bytes([bytes[4], bytes[5]]);
198 let syncd = u16::from_be_bytes([bytes[7], bytes[8]]);
199 let crc_stored = bytes[9];
200
201 if dfl > DFL_MAX_BITS {
202 return Err(Error::DflOutOfRange {
203 dfl,
204 max: DFL_MAX_BITS,
205 });
206 }
207
208 let computed_crc = crc8(&bytes[..9]);
215 let mode_val = computed_crc ^ crc_stored;
216 let mode = Mode::try_from(mode_val)?;
217
218 let (upl, sync, issy_in_header) = match mode {
219 Mode::Normal => (u16::from_be_bytes([bytes[2], bytes[3]]), bytes[6], None),
220 Mode::HighEfficiency => {
221 (0, 0, Some([bytes[2], bytes[3], bytes[6]]))
224 }
225 };
226
227 Ok(Bbheader {
228 matype,
229 upl,
230 sync,
231 dfl,
232 syncd,
233 mode,
234 issy_in_header,
235 })
236 }
237
238 pub fn serialize(&self) -> [u8; BBHEADER_LEN] {
240 let mut buf = [0u8; BBHEADER_LEN];
241 let ma = <[u8; 2]>::from(self.matype);
242 buf[0] = ma[0];
243 buf[1] = ma[1];
244
245 match self.mode {
246 Mode::Normal => {
247 let upl = self.upl.to_be_bytes();
248 buf[2] = upl[0];
249 buf[3] = upl[1];
250 let dfl = self.dfl.to_be_bytes();
251 buf[4] = dfl[0];
252 buf[5] = dfl[1];
253 buf[6] = self.sync;
254 let syncd = self.syncd.to_be_bytes();
255 buf[7] = syncd[0];
256 buf[8] = syncd[1];
257 }
258 Mode::HighEfficiency => {
259 if let Some(issy) = self.issy_in_header {
260 buf[2] = issy[0];
261 buf[3] = issy[1];
262 let dfl = self.dfl.to_be_bytes();
264 buf[4] = dfl[0];
265 buf[5] = dfl[1];
266 buf[6] = issy[2];
267 let syncd = self.syncd.to_be_bytes();
269 buf[7] = syncd[0];
270 buf[8] = syncd[1];
271 } else {
272 let dfl = self.dfl.to_be_bytes();
274 buf[4] = dfl[0];
275 buf[5] = dfl[1];
276 let syncd = self.syncd.to_be_bytes();
277 buf[7] = syncd[0];
278 buf[8] = syncd[1];
279 }
280 }
281 }
282
283 let computed = crc8(&buf[..9]);
285 buf[9] = computed ^ (self.mode as u8);
286
287 buf
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn parse_rejects_buffer_shorter_than_10() {
297 assert!(Bbheader::parse(&[0u8; 9]).is_err());
298 }
299
300 #[test]
301 fn parse_nm_ts_extracts_all_fields() {
302 let mut hdr = [0u8; BBHEADER_LEN];
308 hdr[0] = 0xF0; hdr[1] = 0x00; let upl: u16 = 0x07D0; hdr[2..4].copy_from_slice(&upl.to_be_bytes());
312 let dfl: u16 = 0xBC00; hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
314 hdr[6] = 0x47; let syncd: u16 = 0x0000; hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
317 hdr[9] = crc8(&hdr[..9]); let result = Bbheader::parse(&hdr).unwrap();
320 assert_eq!(result.mode, Mode::Normal);
321 assert_eq!(result.matype.ts_gs, TsGs::Ts);
322 assert!(result.matype.sis);
323 assert!(result.matype.ccm);
324 assert!(!result.matype.issyi);
325 assert!(!result.matype.npd);
326 assert_eq!(result.matype.ext, 0);
327 assert_eq!(result.matype.isi, 0x00);
328 assert_eq!(result.upl, upl);
329 assert_eq!(result.sync, 0x47);
330 assert_eq!(result.dfl, dfl);
331 assert_eq!(result.syncd, syncd);
332 }
333
334 #[test]
335 fn parse_nm_gcs_treats_sync_as_transport_protocol_byte() {
336 let mut hdr = [0u8; BBHEADER_LEN];
337 hdr[0] = 0x50; hdr[1] = 0x00;
339 let upl: u16 = 0x0000; hdr[2..4].copy_from_slice(&upl.to_be_bytes());
341 let dfl: u16 = 0x4000; hdr[4..6].copy_from_slice(&dfl.to_be_bytes());
343 hdr[6] = 0x3C; let syncd: u16 = 0x0000;
345 hdr[7..9].copy_from_slice(&syncd.to_be_bytes());
346 hdr[9] = crc8(&hdr[..9]);
347
348 let result = Bbheader::parse(&hdr).unwrap();
349 assert_eq!(result.mode, Mode::Normal);
350 assert_eq!(result.matype.ts_gs, TsGs::Gcs);
351 assert_eq!(result.sync, 0x3C);
352 assert_eq!(result.upl, upl);
353 }
354
355 #[test]
356 fn parse_detects_nm_via_crc_xor_0() {
357 let mut hdr = [0u8; BBHEADER_LEN];
359 hdr[0] = 0xF0;
360 hdr[1] = 0x00;
361 hdr[2] = 0x07;
362 hdr[3] = 0xD0; hdr[4] = 0xBC;
364 hdr[5] = 0x00; hdr[6] = 0x47; hdr[7] = 0x00;
367 hdr[8] = 0x00; hdr[9] = crc8(&hdr[..9]); let result = Bbheader::parse(&hdr).unwrap();
371 assert_eq!(result.mode, Mode::Normal);
372 }
373
374 #[test]
375 fn parse_rejects_crc_mismatch_in_both_modes() {
376 let mut hdr = [0u8; BBHEADER_LEN];
377 hdr[0] = 0xF0;
378 hdr[1] = 0x00;
379 hdr[2] = 0x07;
380 hdr[3] = 0xD0;
381 hdr[4] = 0xBC;
382 hdr[5] = 0x00;
383 hdr[6] = 0x47;
384 hdr[7] = 0x00;
385 hdr[8] = 0x00;
386 hdr[9] = 0xFF; let result = Bbheader::parse(&hdr);
389 assert!(result.is_err());
390 }
391
392 #[test]
393 fn parse_matype_extracts_ts_gs_enum_for_each_of_gfps_ts_gcs_gse() {
394 for (ts_gs_val, expected) in [
395 (0b00, TsGs::Gfps),
396 (0b01, TsGs::Gcs),
397 (0b10, TsGs::Gse),
398 (0b11, TsGs::Ts),
399 ] {
400 let ma1 = (ts_gs_val << 6) | 0x30; let mut hdr = [0u8; BBHEADER_LEN];
402 hdr[0] = ma1;
403 hdr[1] = 0x00;
404 hdr[2..9].copy_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
405 hdr[9] = crc8(&hdr[..9]);
406 let result = Bbheader::parse(&hdr).unwrap();
407 assert_eq!(result.matype.ts_gs, expected, "ts_gs=0x{:02b}", ts_gs_val);
408 }
409 }
410
411 #[test]
412 fn parse_matype_extracts_sis_isi_on_multi_stream() {
413 let mut hdr = [0u8; BBHEADER_LEN];
415 hdr[0] = 0xD0; hdr[1] = 0xAB; hdr[2] = 0x07;
418 hdr[3] = 0xD0;
419 hdr[4] = 0xBC;
420 hdr[5] = 0x00;
421 hdr[6] = 0x47;
422 hdr[7] = 0x00;
423 hdr[8] = 0x00;
424 hdr[9] = crc8(&hdr[..9]);
425
426 let result = Bbheader::parse(&hdr).unwrap();
427 assert!(!result.matype.sis);
428 assert_eq!(result.matype.isi, 0xAB);
429 }
430
431 #[test]
432 fn parse_matype_extracts_roll_off_2_bits_as_ext_for_s2_context() {
433 let mut hdr = [0u8; BBHEADER_LEN];
435 hdr[0] = 0xF3; hdr[1] = 0x00;
437 hdr[2] = 0x07;
438 hdr[3] = 0xD0;
439 hdr[4] = 0xBC;
440 hdr[5] = 0x00;
441 hdr[6] = 0x47;
442 hdr[7] = 0x00;
443 hdr[8] = 0x00;
444 hdr[9] = crc8(&hdr[..9]);
445
446 let result = Bbheader::parse(&hdr).unwrap();
447 assert_eq!(result.matype.ext, 0b11);
448 }
449
450 #[test]
451 fn serialize_nm_produces_expected_bytes() {
452 let hdr = Bbheader {
453 matype: Matype {
454 ts_gs: TsGs::Ts,
455 sis: true,
456 ccm: true,
457 issyi: false,
458 npd: false,
459 ext: 0,
460 isi: 0x00,
461 },
462 upl: 188 * 8,
463 sync: 0x47,
464 dfl: 48328,
465 syncd: 0,
466 mode: Mode::Normal,
467 issy_in_header: None,
468 };
469 let buf = hdr.serialize();
470
471 let parsed = Bbheader::parse(&buf).unwrap();
472 assert_eq!(parsed.matype.ts_gs, TsGs::Ts);
473 assert!(parsed.matype.sis);
474 assert!(parsed.matype.ccm);
475 assert_eq!(parsed.upl, 188 * 8);
476 assert_eq!(parsed.sync, 0x47);
477 assert_eq!(parsed.dfl, 48328);
478 assert_eq!(parsed.syncd, 0);
479 assert_eq!(parsed.mode, Mode::Normal);
480 }
481
482 #[test]
483 fn serialize_round_trip_nm_ts_preserves_every_field() {
484 let orig = Bbheader {
485 matype: Matype {
486 ts_gs: TsGs::Ts,
487 sis: true,
488 ccm: true,
489 issyi: true,
490 npd: false,
491 ext: 0,
492 isi: 0x00,
493 },
494 upl: 1504,
495 sync: 0x47,
496 dfl: 48328,
497 syncd: 0,
498 mode: Mode::Normal,
499 issy_in_header: None,
500 };
501 let buf = orig.serialize();
502 let parsed = Bbheader::parse(&buf).unwrap();
503 assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
504 assert_eq!(orig.matype.sis, parsed.matype.sis);
505 assert_eq!(orig.matype.ccm, parsed.matype.ccm);
506 assert_eq!(orig.matype.issyi, parsed.matype.issyi);
507 assert_eq!(orig.matype.npd, parsed.matype.npd);
508 assert_eq!(orig.matype.ext, parsed.matype.ext);
509 assert_eq!(orig.matype.isi, parsed.matype.isi);
510 assert_eq!(orig.upl, parsed.upl);
511 assert_eq!(orig.sync, parsed.sync);
512 assert_eq!(orig.dfl, parsed.dfl);
513 assert_eq!(orig.syncd, parsed.syncd);
514 assert_eq!(orig.mode, parsed.mode);
515 }
516
517 #[test]
518 fn serialize_round_trip_nm_gcs() {
519 let orig = Bbheader {
520 matype: Matype {
521 ts_gs: TsGs::Gcs,
522 sis: true,
523 ccm: false,
524 issyi: false,
525 npd: false,
526 ext: 0,
527 isi: 0x00,
528 },
529 upl: 0,
530 sync: 0x00,
531 dfl: 16384,
532 syncd: 0,
533 mode: Mode::Normal,
534 issy_in_header: None,
535 };
536 let buf = orig.serialize();
537 let parsed = Bbheader::parse(&buf).unwrap();
538 assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
539 assert_eq!(orig.matype.sis, parsed.matype.sis);
540 assert_eq!(orig.matype.ccm, parsed.matype.ccm);
541 assert_eq!(orig.dfl, parsed.dfl);
542 assert_eq!(orig.syncd, parsed.syncd);
543 assert_eq!(orig.mode, parsed.mode);
544 }
545
546 #[test]
547 fn serialize_crc8_always_matches_bytes_0_to_8() {
548 let hdr = Bbheader {
549 matype: Matype {
550 ts_gs: TsGs::Gse,
551 sis: true,
552 ccm: true,
553 issyi: true,
554 npd: false,
555 ext: 0,
556 isi: 0x00,
557 },
558 upl: 0,
559 sync: 0xFF,
560 dfl: 32768,
561 syncd: 0,
562 mode: Mode::Normal,
563 issy_in_header: None,
564 };
565 let buf = hdr.serialize();
566 let computed = crc8(&buf[..9]);
567 assert_eq!(computed ^ buf[9], 0); assert_eq!(buf[9], computed); }
570
571 #[test]
572 fn parse_detects_hem_via_crc_xor_1() {
573 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
575 let result = Bbheader::parse(&hdr).unwrap();
576 assert_eq!(result.mode, Mode::HighEfficiency);
577 }
578
579 #[test]
580 fn parse_hem_extracts_matype_dfl_syncd() {
581 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
582 let result = Bbheader::parse(&hdr).unwrap();
583 assert_eq!(result.mode, Mode::HighEfficiency);
584 assert_eq!(result.matype.ts_gs, TsGs::Ts);
585 assert!(result.matype.sis);
586 assert!(result.matype.ccm);
587 assert!(result.matype.issyi);
588 assert!(!result.matype.npd);
589 assert_eq!(result.matype.ext, 0);
590 assert_eq!(result.dfl, 48328);
591 assert_eq!(result.syncd, 0x0350);
592 }
593
594 #[test]
595 fn parse_hem_preserves_three_issy_bytes() {
596 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
597 let result = Bbheader::parse(&hdr).unwrap();
598 let issy = result.issy_in_header.unwrap();
599 assert_eq!(issy, [0xa4, 0x28, 0xe2]);
601 }
602
603 #[test]
604 fn parse_hem_leaves_upl_bits_as_zero_and_sync_as_zero() {
605 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
606 let result = Bbheader::parse(&hdr).unwrap();
607 assert_eq!(result.upl, 0);
608 assert_eq!(result.sync, 0);
609 }
610
611 #[test]
612 fn parse_hem_rejects_when_mode_xor_not_0_or_1() {
613 let mut hdr = [0u8; BBHEADER_LEN];
615 hdr[0] = 0xF0;
616 hdr[1] = 0x00;
617 hdr[2] = 0x00;
618 hdr[3] = 0x00;
619 hdr[4] = 0x00;
620 hdr[5] = 0x00;
621 hdr[6] = 0x00;
622 hdr[7] = 0x00;
623 hdr[8] = 0x00;
624 hdr[9] = crc8(&hdr[..9]) ^ 0x02; assert!(Bbheader::parse(&hdr).is_err());
626 }
627
628 #[test]
629 fn parse_same_bytes_different_mode_byte_produces_different_bbheader() {
630 let mut hdr1 = [0xF8, 0x00, 0x00, 0x00, 0xBC, 0xC8, 0x00, 0x03, 0x50, 0x00];
632 hdr1[9] = crc8(&hdr1[..9]); let mut hdr2 = hdr1;
634 hdr2[9] ^= 0x01; let result1 = Bbheader::parse(&hdr1).unwrap();
637 let result2 = Bbheader::parse(&hdr2).unwrap();
638 assert_eq!(result1.mode, Mode::Normal);
639 assert_eq!(result2.mode, Mode::HighEfficiency);
640 }
641
642 #[test]
643 fn serialize_hem_round_trip() {
644 let orig = Bbheader {
645 matype: Matype {
646 ts_gs: TsGs::Ts,
647 sis: true,
648 ccm: true,
649 issyi: true,
650 npd: false,
651 ext: 0,
652 isi: 0x00,
653 },
654 upl: 0, sync: 0, dfl: 48328,
657 syncd: 848,
658 mode: Mode::HighEfficiency,
659 issy_in_header: Some([0xA4, 0x28, 0xE2]),
660 };
661 let buf = orig.serialize();
662 let parsed = Bbheader::parse(&buf).unwrap();
663 assert_eq!(orig.mode, parsed.mode);
664 assert_eq!(orig.matype.ts_gs, parsed.matype.ts_gs);
665 assert_eq!(orig.dfl, parsed.dfl);
666 assert_eq!(orig.syncd, parsed.syncd);
667 assert_eq!(orig.issy_in_header, parsed.issy_in_header);
668 }
669
670 #[test]
671 fn serialize_hem_sets_crc_xor_mode_byte_correctly() {
672 let hdr = Bbheader {
673 matype: Matype {
674 ts_gs: TsGs::Ts,
675 sis: true,
676 ccm: true,
677 issyi: false,
678 npd: false,
679 ext: 0,
680 isi: 0x05,
681 },
682 upl: 0,
683 sync: 0,
684 dfl: 48000,
685 syncd: 0,
686 mode: Mode::HighEfficiency,
687 issy_in_header: Some([0x00, 0x00, 0x00]),
688 };
689 let buf = hdr.serialize();
690 let computed = crc8(&buf[..9]);
691 assert_eq!(buf[9], computed ^ 1);
693 }
694
695 #[test]
696 fn serialize_hem_with_issy_bytes_zero_writes_expected_layout() {
697 let hdr = Bbheader {
698 matype: Matype {
699 ts_gs: TsGs::Ts,
700 sis: true,
701 ccm: true,
702 issyi: true,
703 npd: false,
704 ext: 0,
705 isi: 0x00,
706 },
707 upl: 0,
708 sync: 0,
709 dfl: 50000,
710 syncd: 100,
711 mode: Mode::HighEfficiency,
712 issy_in_header: Some([0x00, 0x00, 0x00]),
713 };
714 let buf = hdr.serialize();
715 let parsed = Bbheader::parse(&buf).unwrap();
716 assert_eq!(parsed.mode, Mode::HighEfficiency);
717 assert_eq!(parsed.issy_in_header, Some([0x00, 0x00, 0x00]));
718 assert_eq!(parsed.dfl, 50000);
719 assert_eq!(parsed.syncd, 100);
720 }
721
722 #[test]
723 fn parse_valid_dvbt2_hem_bbframe_rai() {
724 let hdr: [u8; BBHEADER_LEN] = [0xf8, 0x00, 0xa4, 0x28, 0xbc, 0xc8, 0xe2, 0x03, 0x50, 0x1f];
726 assert_eq!(Bbheader::parse(&hdr).unwrap().dfl, 48328);
727 }
728
729 #[test]
730 fn exhaustive_tsgs_sweep() {
731 let mut matched = 0u16;
732 for byte in 0u8..=0xFF {
733 if let Ok(v) = TsGs::try_from(byte) {
734 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
735 matched += 1;
736 }
737 }
738 assert_eq!(matched, 4, "expected 4 matched variants");
739 }
740
741 #[test]
742 fn exhaustive_mode_sweep() {
743 let mut matched = 0u16;
744 for byte in 0u8..=0xFF {
745 if let Ok(v) = Mode::try_from(byte) {
746 assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
747 matched += 1;
748 }
749 }
750 assert_eq!(matched, 2, "expected 2 matched variants");
751 }
752}