use crate::descriptors::rnt::RntDescriptorLoop;
use crate::error::{Error, Result};
use crate::text::DvbText;
use alloc::vec::Vec;
use broadcast_common::{Parse, Serialize};
pub const TABLE_ID: u8 = 0x79;
pub const PID: mpeg_ts::pid::Pid = mpeg_ts::pid::Pid::new(0x0016);
const HEADER_LEN: usize = 3;
const EXTENSION_HEADER_LEN: usize = 6;
const COMMON_DESC_LEN_FIELD: usize = 2;
use super::CRC_LEN;
const MIN_LEN: usize = HEADER_LEN + EXTENSION_HEADER_LEN + COMMON_DESC_LEN_FIELD + CRC_LEN;
const RP_INFO_LEN_FIELD: usize = 2;
const RP_NAME_LEN_FIELD: usize = 1;
const RP_DESC_LEN_FIELD: usize = 2;
const CA_NAME_LEN_FIELD: usize = 1;
const CA_HEADER_LEN: usize = 2;
const CRID_POLICY_MASK: u8 = 0x03;
const CRID_POLICY_RESERVED_SHIFT: u32 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CridAuthorityPolicy {
Permanent,
Transient,
Either,
Reserved,
}
impl CridAuthorityPolicy {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v & 0x03 {
0 => Self::Permanent,
1 => Self::Transient,
2 => Self::Either,
_ => Self::Reserved,
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Permanent => 0,
Self::Transient => 1,
Self::Either => 2,
Self::Reserved => 3,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Permanent => "Permanent",
Self::Transient => "Transient",
Self::Either => "Either",
Self::Reserved => "Reserved",
}
}
}
broadcast_common::impl_spec_display!(CridAuthorityPolicy);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum ContextIdType {
BouquetId,
OriginalNetworkId,
NetworkId,
DvbReserved(u8),
UserDefined(u8),
}
impl ContextIdType {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v {
0x00 => Self::BouquetId,
0x01 => Self::OriginalNetworkId,
0x02 => Self::NetworkId,
v @ 0x03..0x80 => Self::DvbReserved(v),
_ => Self::UserDefined(v),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::BouquetId => 0x00,
Self::OriginalNetworkId => 0x01,
Self::NetworkId => 0x02,
Self::DvbReserved(v) | Self::UserDefined(v) => v,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::BouquetId => "Bouquet ID",
Self::OriginalNetworkId => "Original Network ID",
Self::NetworkId => "Network ID",
Self::DvbReserved(_) => "DVB Reserved",
Self::UserDefined(_) => "User Defined",
}
}
}
broadcast_common::impl_spec_display!(ContextIdType, DvbReserved, UserDefined);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct CridAuthority<'a> {
pub name: DvbText<'a>,
pub crid_authority_policy: CridAuthorityPolicy,
pub descriptors: RntDescriptorLoop<'a>,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved: u8,
}
impl<'a> CridAuthority<'a> {
#[must_use]
pub fn new(
name: DvbText<'a>,
crid_authority_policy: CridAuthorityPolicy,
descriptors: RntDescriptorLoop<'a>,
) -> Self {
Self {
name,
crid_authority_policy,
descriptors,
reserved: super::RESERVED_2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct ResolutionProvider<'a> {
pub name: DvbText<'a>,
pub descriptors: RntDescriptorLoop<'a>,
pub crid_authorities: Vec<CridAuthority<'a>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_info_length: u8,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_descriptors_length: u8,
}
impl<'a> ResolutionProvider<'a> {
#[must_use]
pub fn new(
name: DvbText<'a>,
descriptors: RntDescriptorLoop<'a>,
crid_authorities: Vec<CridAuthority<'a>>,
) -> Self {
Self {
name,
descriptors,
crid_authorities,
reserved_info_length: super::RESERVED_4,
reserved_descriptors_length: super::RESERVED_4,
}
}
}
fn crid_authority_serialized_len(ca: &CridAuthority) -> usize {
CA_NAME_LEN_FIELD + ca.name.len() + CA_HEADER_LEN + ca.descriptors.len()
}
fn resolution_provider_serialized_len(rp: &ResolutionProvider) -> usize {
RP_NAME_LEN_FIELD
+ rp.name.len()
+ RP_DESC_LEN_FIELD
+ rp.descriptors.len()
+ rp.crid_authorities
.iter()
.map(crid_authority_serialized_len)
.sum::<usize>()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
#[non_exhaustive]
pub struct RntSection<'a> {
pub context_id: u16,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub context_id_type: ContextIdType,
pub common_descriptors: RntDescriptorLoop<'a>,
pub resolution_providers: Vec<ResolutionProvider<'a>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub bits: super::SectionBits,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_common_descriptors_length: u8,
}
impl<'a> RntSection<'a> {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
context_id: u16,
version_number: u8,
current_next_indicator: bool,
section_number: u8,
last_section_number: u8,
context_id_type: ContextIdType,
common_descriptors: RntDescriptorLoop<'a>,
resolution_providers: Vec<ResolutionProvider<'a>>,
) -> Self {
Self {
context_id,
version_number,
current_next_indicator,
section_number,
last_section_number,
context_id_type,
common_descriptors,
resolution_providers,
bits: super::SectionBits::dvb(),
reserved_common_descriptors_length: super::RESERVED_4,
}
}
}
impl<'a> Parse<'a> for RntSection<'a> {
type Error = crate::error::Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
if bytes.len() < MIN_LEN {
return Err(Error::BufferTooShort {
need: MIN_LEN,
have: bytes.len(),
what: "RntSection",
});
}
if bytes[0] != TABLE_ID {
return Err(Error::UnexpectedTableId {
table_id: bytes[0],
what: "RntSection",
expected: &[TABLE_ID],
});
}
let section_length = super::section_length_of(bytes);
let total = super::check_section_length(bytes.len(), HEADER_LEN, section_length, MIN_LEN)?;
let context_id = crate::wire::be_u16(bytes, 3, "rnt")?;
let version_number = super::version_number_of(bytes[5]);
let current_next_indicator = super::current_next_of(bytes[5]);
let section_number = bytes[6];
let last_section_number = bytes[7];
let context_id_type = ContextIdType::from_u8(bytes[8]);
let common_desc_len_pos = HEADER_LEN + EXTENSION_HEADER_LEN;
let common_descriptors_length =
super::desc_loop_len_of(bytes[common_desc_len_pos], bytes[common_desc_len_pos + 1]);
let common_desc_start = common_desc_len_pos + COMMON_DESC_LEN_FIELD;
let common_desc_end = common_desc_start + common_descriptors_length;
if common_desc_end > total - CRC_LEN {
return Err(Error::SectionLengthOverflow {
declared: common_descriptors_length,
available: (total - CRC_LEN).saturating_sub(common_desc_start),
});
}
let common_descriptors = RntDescriptorLoop::new(&bytes[common_desc_start..common_desc_end]);
let payload_end = total - CRC_LEN;
let mut pos = common_desc_end;
let mut resolution_providers = Vec::new();
while pos < payload_end {
if pos + RP_INFO_LEN_FIELD > payload_end {
return Err(Error::BufferTooShort {
need: pos + RP_INFO_LEN_FIELD,
have: payload_end,
what: "RntSection resolution_provider_info_length",
});
}
let rp_info_length = super::desc_loop_len_of(bytes[pos], bytes[pos + 1]);
let reserved_info_length = super::len_reserved_of(bytes[pos]);
pos += RP_INFO_LEN_FIELD;
let rp_end = pos + rp_info_length;
if rp_end > payload_end {
return Err(Error::SectionLengthOverflow {
declared: rp_info_length,
available: payload_end.saturating_sub(pos),
});
}
if pos + RP_NAME_LEN_FIELD > rp_end {
return Err(Error::BufferTooShort {
need: pos + RP_NAME_LEN_FIELD,
have: rp_end,
what: "RntSection resolution_provider_name_length",
});
}
let name_len = bytes[pos] as usize;
pos += RP_NAME_LEN_FIELD;
if pos + name_len > rp_end {
return Err(Error::BufferTooShort {
need: pos + name_len,
have: rp_end,
what: "RntSection resolution_provider_name",
});
}
let name = DvbText::new(&bytes[pos..pos + name_len]);
pos += name_len;
if pos + RP_DESC_LEN_FIELD > rp_end {
return Err(Error::BufferTooShort {
need: pos + RP_DESC_LEN_FIELD,
have: rp_end,
what: "RntSection resolution_provider_descriptors_length",
});
}
let rp_desc_len = super::desc_loop_len_of(bytes[pos], bytes[pos + 1]);
let reserved_descriptors_length = super::len_reserved_of(bytes[pos]);
pos += RP_DESC_LEN_FIELD;
let rp_desc_start = pos;
let rp_desc_end = rp_desc_start + rp_desc_len;
if rp_desc_end > rp_end {
return Err(Error::SectionLengthOverflow {
declared: rp_desc_len,
available: rp_end.saturating_sub(rp_desc_start),
});
}
let descriptors = RntDescriptorLoop::new(&bytes[rp_desc_start..rp_desc_end]);
pos = rp_desc_end;
let mut crid_authorities = Vec::new();
while pos < rp_end {
if pos + CA_NAME_LEN_FIELD > rp_end {
return Err(Error::BufferTooShort {
need: pos + CA_NAME_LEN_FIELD,
have: rp_end,
what: "RntSection CRID_authority_name_length",
});
}
let ca_name_len = bytes[pos] as usize;
pos += CA_NAME_LEN_FIELD;
if pos + ca_name_len > rp_end {
return Err(Error::BufferTooShort {
need: pos + ca_name_len,
have: rp_end,
what: "RntSection CRID_authority_name",
});
}
let ca_name = DvbText::new(&bytes[pos..pos + ca_name_len]);
pos += ca_name_len;
if pos + CA_HEADER_LEN > rp_end {
return Err(Error::BufferTooShort {
need: pos + CA_HEADER_LEN,
have: rp_end,
what: "RntSection CRID_authority header",
});
}
let ca_packed = bytes[pos];
let crid_authority_policy =
CridAuthorityPolicy::from_u8((ca_packed >> 4) & CRID_POLICY_MASK);
let ca_desc_len = super::desc_loop_len_of(ca_packed, bytes[pos + 1]);
pos += CA_HEADER_LEN;
let ca_desc_start = pos;
let ca_desc_end = ca_desc_start + ca_desc_len;
if ca_desc_end > rp_end {
return Err(Error::SectionLengthOverflow {
declared: ca_desc_len,
available: rp_end.saturating_sub(ca_desc_start),
});
}
let ca_descriptors = RntDescriptorLoop::new(&bytes[ca_desc_start..ca_desc_end]);
pos = ca_desc_end;
let mut authority =
CridAuthority::new(ca_name, crid_authority_policy, ca_descriptors);
authority.reserved = ca_packed >> CRID_POLICY_RESERVED_SHIFT;
crid_authorities.push(authority);
}
let mut provider = ResolutionProvider::new(name, descriptors, crid_authorities);
provider.reserved_info_length = reserved_info_length;
provider.reserved_descriptors_length = reserved_descriptors_length;
resolution_providers.push(provider);
pos = rp_end;
}
Ok(RntSection {
context_id,
version_number,
current_next_indicator,
section_number,
last_section_number,
context_id_type,
common_descriptors,
resolution_providers,
bits: super::SectionBits::parse(bytes[1], Some(bytes[5])),
reserved_common_descriptors_length: super::len_reserved_of(bytes[common_desc_len_pos]),
})
}
}
impl Serialize for RntSection<'_> {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
HEADER_LEN
+ EXTENSION_HEADER_LEN
+ COMMON_DESC_LEN_FIELD
+ self.common_descriptors.len()
+ self
.resolution_providers
.iter()
.map(|rp| RP_INFO_LEN_FIELD + resolution_provider_serialized_len(rp))
.sum::<usize>()
+ CRC_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len = self.serialized_len();
if buf.len() < len {
return Err(Error::OutputBufferTooSmall {
need: len,
have: buf.len(),
});
}
buf[0] = TABLE_ID;
buf[1] = self.bits.b1()?;
super::write_section_length(buf, len - HEADER_LEN)?;
buf[3..5].copy_from_slice(&self.context_id.to_be_bytes());
buf[5] = self
.bits
.b5(self.version_number, self.current_next_indicator)?;
buf[6] = self.section_number;
buf[7] = self.last_section_number;
buf[8] = self.context_id_type.to_u8();
let cdl = broadcast_common::len::fit_bits(
self.common_descriptors.len() as u64,
12,
"common_descriptors_length",
)?;
let cdl_pos = HEADER_LEN + EXTENSION_HEADER_LEN;
super::write_desc_loop_len_r(
&mut buf[cdl_pos..cdl_pos + 2],
cdl as usize,
self.reserved_common_descriptors_length,
)?;
let cd_start = cdl_pos + COMMON_DESC_LEN_FIELD;
let cd_end = cd_start + self.common_descriptors.len();
buf[cd_start..cd_end].copy_from_slice(self.common_descriptors.raw());
let mut pos = cd_end;
for rp in &self.resolution_providers {
let rp_body_len = resolution_provider_serialized_len(rp);
let rp_info_length = broadcast_common::len::fit_bits(
rp_body_len as u64,
12,
"resolution_provider_loop_length",
)?;
super::write_desc_loop_len_r(
&mut buf[pos..pos + 2],
rp_info_length as usize,
rp.reserved_info_length,
)?;
pos += RP_INFO_LEN_FIELD;
let rp_name_len =
broadcast_common::len::fit_u8(rp.name.len(), "resolution_provider_name_length")?;
buf[pos] = rp_name_len;
pos += RP_NAME_LEN_FIELD;
buf[pos..pos + rp.name.len()].copy_from_slice(rp.name.raw());
pos += rp.name.len();
let rdl = broadcast_common::len::fit_bits(
rp.descriptors.len() as u64,
12,
"resolution_provider_descriptors_length",
)?;
super::write_desc_loop_len_r(
&mut buf[pos..pos + 2],
rdl as usize,
rp.reserved_descriptors_length,
)?;
pos += RP_DESC_LEN_FIELD;
buf[pos..pos + rp.descriptors.len()].copy_from_slice(rp.descriptors.raw());
pos += rp.descriptors.len();
for ca in &rp.crid_authorities {
let ca_name_len =
broadcast_common::len::fit_u8(ca.name.len(), "crid_authority_name_length")?;
buf[pos] = ca_name_len;
pos += CA_NAME_LEN_FIELD;
buf[pos..pos + ca.name.len()].copy_from_slice(ca.name.raw());
pos += ca.name.len();
let adl = broadcast_common::len::fit_bits(
ca.descriptors.len() as u64,
12,
"crid_authority_descriptors_length",
)?;
buf[pos] = (broadcast_common::len::fit_mask(
ca.reserved,
super::RESERVED_2,
"reserved(2)",
)? << CRID_POLICY_RESERVED_SHIFT)
| (broadcast_common::len::fit_mask(
ca.crid_authority_policy.to_u8(),
CRID_POLICY_MASK,
"crid_authority_policy",
)? << 4)
| ((adl >> 8) as u8);
buf[pos + 1] = adl as u8;
pos += CA_HEADER_LEN;
buf[pos..pos + ca.descriptors.len()].copy_from_slice(ca.descriptors.raw());
pos += ca.descriptors.len();
}
}
let crc_pos = len - CRC_LEN;
super::write_crc(buf, crc_pos);
Ok(len)
}
}
impl<'a> crate::traits::TableDef<'a> for RntSection<'a> {
const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
const NAME: &'static str = "RESOLUTION_PROVIDER_NOTIFICATION";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_happy_path() {
let common_desc = [0x83u8, 0x02, 0xAB, 0xCD];
let rp = ResolutionProvider::new(
DvbText::new(b"bb"),
RntDescriptorLoop::new(&[]),
vec![CridAuthority::new(
DvbText::new(b"au"),
CridAuthorityPolicy::Transient,
RntDescriptorLoop::new(&[]),
)],
);
let rnt = RntSection::new(
0x0042,
3,
true,
0,
0,
ContextIdType::BouquetId,
RntDescriptorLoop::new(&common_desc),
vec![rp],
);
let mut buf = vec![0u8; rnt.serialized_len()];
rnt.serialize_into(&mut buf).unwrap();
let parsed = RntSection::parse(&buf).unwrap();
assert_eq!(parsed.context_id, 0x0042);
assert_eq!(parsed.version_number, 3);
assert!(parsed.current_next_indicator);
assert_eq!(parsed.context_id_type, ContextIdType::BouquetId);
assert_eq!(parsed.resolution_providers.len(), 1);
assert_eq!(parsed.resolution_providers[0].name.raw(), b"bb");
assert_eq!(parsed.resolution_providers[0].crid_authorities.len(), 1);
assert_eq!(
parsed.resolution_providers[0].crid_authorities[0].crid_authority_policy,
CridAuthorityPolicy::Transient
);
assert_eq!(
parsed.resolution_providers[0].crid_authorities[0]
.name
.raw(),
b"au"
);
}
#[test]
fn parse_no_descriptors_no_providers() {
let rnt = RntSection::new(
0x0000,
0,
false,
0,
0,
ContextIdType::BouquetId,
RntDescriptorLoop::new(&[]),
Vec::new(),
);
let mut buf = vec![0u8; rnt.serialized_len()];
rnt.serialize_into(&mut buf).unwrap();
let parsed = RntSection::parse(&buf).unwrap();
assert_eq!(parsed.common_descriptors.len(), 0);
assert!(parsed.resolution_providers.is_empty());
}
#[test]
fn byte_exact_round_trip() {
let rp = ResolutionProvider::new(
DvbText::new(b"provider"),
RntDescriptorLoop::new(&[0x40, 0x03, b'R', b'N', b'T']),
vec![
CridAuthority::new(
DvbText::new(b"auth1"),
CridAuthorityPolicy::Permanent,
RntDescriptorLoop::new(&[]),
),
CridAuthority::new(
DvbText::new(b"auth2"),
CridAuthorityPolicy::Either,
RntDescriptorLoop::new(&[0x42, 0x00]),
),
],
);
let rnt = RntSection::new(
0xABCD,
15,
true,
1,
2,
ContextIdType::NetworkId,
RntDescriptorLoop::new(&[0x40, 0x03, b'R', b'N', b'T']),
vec![rp],
);
let mut buf = vec![0u8; rnt.serialized_len()];
rnt.serialize_into(&mut buf).unwrap();
let re = RntSection::parse(&buf).unwrap();
let mut buf2 = vec![0u8; re.serialized_len()];
re.serialize_into(&mut buf2).unwrap();
assert_eq!(buf, buf2, "byte-exact re-serialize");
let re = RntSection::parse(&buf).unwrap();
assert_eq!(re.resolution_providers.len(), 1);
assert_eq!(re.resolution_providers[0].name.raw(), b"provider");
assert_eq!(re.resolution_providers[0].crid_authorities.len(), 2);
assert_eq!(
re.resolution_providers[0].crid_authorities[1].crid_authority_policy,
CridAuthorityPolicy::Either
);
}
#[test]
fn parse_rejects_wrong_table_id() {
let rnt = RntSection::new(
0x0001,
0,
true,
0,
0,
ContextIdType::BouquetId,
RntDescriptorLoop::new(&[]),
Vec::new(),
);
let mut buf = vec![0u8; rnt.serialized_len()];
rnt.serialize_into(&mut buf).unwrap();
buf[0] = 0x70;
assert!(matches!(
RntSection::parse(&buf).unwrap_err(),
Error::UnexpectedTableId { table_id: 0x70, .. }
));
}
#[test]
fn parse_rejects_short_buffer() {
assert!(matches!(
RntSection::parse(&[0x79, 0x00]).unwrap_err(),
Error::BufferTooShort { .. }
));
}
#[test]
fn serialize_rejects_too_small_buffer() {
let rnt = RntSection::new(
0x0001,
0,
true,
0,
0,
ContextIdType::BouquetId,
RntDescriptorLoop::new(&[]),
Vec::new(),
);
let mut buf = vec![0u8; 2];
assert!(matches!(
rnt.serialize_into(&mut buf).unwrap_err(),
Error::OutputBufferTooSmall { .. }
));
}
#[test]
fn parse_rejects_zero_section_length() {
let mut buf = vec![0u8; 64];
buf[0] = TABLE_ID;
buf[1] = 0xF0;
buf[2] = 0x00;
for b in &mut buf[3..] {
*b = 0xFF;
}
assert!(matches!(
RntSection::parse(&buf).unwrap_err(),
Error::SectionLengthOverflow { .. }
));
}
#[test]
fn serialize_rejects_common_descriptors_exceeding_section_length() {
let desc = vec![0xAAu8; 4084];
let rnt = RntSection::new(
1,
0,
true,
0,
0,
ContextIdType::BouquetId,
RntDescriptorLoop::new(&desc),
vec![],
);
let mut buf = vec![0u8; rnt.serialized_len()];
assert!(matches!(
rnt.serialize_into(&mut buf).unwrap_err(),
Error::FieldOverflow(_)
));
}
#[test]
fn serialize_accepts_common_descriptors_at_section_length_boundary() {
let desc = vec![0xAAu8; 4083];
let rnt = RntSection::new(
1,
0,
true,
0,
0,
ContextIdType::BouquetId,
RntDescriptorLoop::new(&desc),
vec![],
);
let mut buf = vec![0u8; rnt.serialized_len()];
rnt.serialize_into(&mut buf).expect("boundary fits");
let reparsed = RntSection::parse(&buf).expect("reparse");
assert_eq!(rnt, reparsed);
}
#[test]
fn parse_handwritten_rnt_no_providers() {
let mut bytes: Vec<u8> = vec![
0x79, 0xF0, 0x0C, 0x00, 0x42, 0xC7, 0x00, 0x00, 0x01, 0xF0, 0x00,
];
let crc = broadcast_common::crc32_mpeg2::compute(&bytes);
bytes.extend_from_slice(&crc.to_be_bytes());
let rnt = RntSection::parse(&bytes).unwrap();
assert_eq!(rnt.context_id, 0x0042);
assert_eq!(rnt.version_number, 3);
assert!(rnt.current_next_indicator);
assert!(rnt.resolution_providers.is_empty());
}
#[test]
fn crid_authority_policy_round_trip() {
for v in 0u8..=3 {
let p = CridAuthorityPolicy::from_u8(v);
assert_eq!(p.to_u8(), v, "CridAuthorityPolicy round-trip for {v}");
}
}
#[test]
fn context_id_type_full_range_round_trip() {
for byte in 0u8..=0xFF {
let ct = ContextIdType::from_u8(byte);
assert_eq!(
ct.to_u8(),
byte,
"ContextIdType round-trip failed for {byte:#04x}"
);
}
}
}