1use crate::descriptors::DescriptorLoop;
10use crate::error::{Error, Result};
11use crate::text::DvbText;
12use alloc::vec::Vec;
13use dvb_common::{Parse, Serialize};
14
15pub const TABLE_ID: u8 = 0x79;
17pub const PID: u16 = 0x0016;
19
20const HEADER_LEN: usize = 3;
21const EXTENSION_HEADER_LEN: usize = 6;
22const COMMON_DESC_LEN_FIELD: usize = 2;
23const CRC_LEN: usize = 4;
24const MIN_LEN: usize = HEADER_LEN + EXTENSION_HEADER_LEN + COMMON_DESC_LEN_FIELD + CRC_LEN;
25
26const RP_INFO_LEN_FIELD: usize = 2;
27const RP_NAME_LEN_FIELD: usize = 1;
28const RP_DESC_LEN_FIELD: usize = 2;
29const CA_NAME_LEN_FIELD: usize = 1;
30const CA_HEADER_LEN: usize = 2;
31
32const RESERVED_NIBBLE: u8 = 0xF0;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37#[non_exhaustive]
38pub enum CridAuthorityPolicy {
39 Permanent,
41 Transient,
43 Either,
45 Reserved,
47}
48
49impl CridAuthorityPolicy {
50 #[must_use]
51 pub fn from_u8(v: u8) -> Self {
53 match v & 0x03 {
54 0 => Self::Permanent,
55 1 => Self::Transient,
56 2 => Self::Either,
57 _ => Self::Reserved,
58 }
59 }
60
61 #[must_use]
62 pub fn to_u8(self) -> u8 {
64 match self {
65 Self::Permanent => 0,
66 Self::Transient => 1,
67 Self::Either => 2,
68 Self::Reserved => 3,
69 }
70 }
71
72 #[must_use]
73 pub fn name(self) -> &'static str {
75 match self {
76 Self::Permanent => "Permanent",
77 Self::Transient => "Transient",
78 Self::Either => "Either",
79 Self::Reserved => "Reserved",
80 }
81 }
82}
83dvb_common::impl_spec_display!(CridAuthorityPolicy);
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize))]
88#[non_exhaustive]
89pub enum ContextIdType {
90 BouquetId,
92 OriginalNetworkId,
94 NetworkId,
96 DvbReserved(u8),
98 UserDefined(u8),
100}
101
102impl ContextIdType {
103 #[must_use]
104 pub fn from_u8(v: u8) -> Self {
106 match v {
107 0x00 => Self::BouquetId,
108 0x01 => Self::OriginalNetworkId,
109 0x02 => Self::NetworkId,
110 v if v < 0x80 => Self::DvbReserved(v),
111 _ => Self::UserDefined(v),
112 }
113 }
114
115 #[must_use]
116 pub fn to_u8(self) -> u8 {
118 match self {
119 Self::BouquetId => 0x00,
120 Self::OriginalNetworkId => 0x01,
121 Self::NetworkId => 0x02,
122 Self::DvbReserved(v) | Self::UserDefined(v) => v,
123 }
124 }
125
126 #[must_use]
127 pub fn name(self) -> &'static str {
129 match self {
130 Self::BouquetId => "Bouquet ID",
131 Self::OriginalNetworkId => "Original Network ID",
132 Self::NetworkId => "Network ID",
133 Self::DvbReserved(_) => "DVB Reserved",
134 Self::UserDefined(_) => "User Defined",
135 }
136 }
137}
138dvb_common::impl_spec_display!(ContextIdType, DvbReserved, UserDefined);
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143pub struct CridAuthority<'a> {
144 pub name: DvbText<'a>,
146 pub crid_authority_policy: CridAuthorityPolicy,
149 pub descriptors: DescriptorLoop<'a>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize))]
156pub struct ResolutionProvider<'a> {
157 pub name: DvbText<'a>,
159 pub descriptors: DescriptorLoop<'a>,
161 pub crid_authorities: Vec<CridAuthority<'a>>,
163}
164
165fn crid_authority_serialized_len(ca: &CridAuthority) -> usize {
166 CA_NAME_LEN_FIELD + ca.name.len() + CA_HEADER_LEN + ca.descriptors.len()
167}
168
169fn resolution_provider_serialized_len(rp: &ResolutionProvider) -> usize {
170 RP_NAME_LEN_FIELD
171 + rp.name.len()
172 + RP_DESC_LEN_FIELD
173 + rp.descriptors.len()
174 + rp.crid_authorities
175 .iter()
176 .map(crid_authority_serialized_len)
177 .sum::<usize>()
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
188pub struct RntSection<'a> {
189 pub context_id: u16,
191 pub version_number: u8,
193 pub current_next_indicator: bool,
195 pub section_number: u8,
197 pub last_section_number: u8,
199 pub context_id_type: ContextIdType,
201 pub common_descriptors: DescriptorLoop<'a>,
204 pub resolution_providers: Vec<ResolutionProvider<'a>>,
206}
207
208impl<'a> Parse<'a> for RntSection<'a> {
209 type Error = crate::error::Error;
210
211 fn parse(bytes: &'a [u8]) -> Result<Self> {
212 if bytes.len() < MIN_LEN {
213 return Err(Error::BufferTooShort {
214 need: MIN_LEN,
215 have: bytes.len(),
216 what: "RntSection",
217 });
218 }
219 if bytes[0] != TABLE_ID {
220 return Err(Error::UnexpectedTableId {
221 table_id: bytes[0],
222 what: "RntSection",
223 expected: &[TABLE_ID],
224 });
225 }
226
227 let section_length = ((bytes[1] & 0x0F) as u16) << 8 | bytes[2] as u16;
228 let total =
229 super::check_section_length(bytes.len(), HEADER_LEN, section_length as usize, MIN_LEN)?;
230
231 let context_id = u16::from_be_bytes([bytes[3], bytes[4]]);
232 let version_number = (bytes[5] >> 1) & 0x1F;
233 let current_next_indicator = (bytes[5] & 0x01) != 0;
234 let section_number = bytes[6];
235 let last_section_number = bytes[7];
236 let context_id_type = ContextIdType::from_u8(bytes[8]);
237
238 let common_desc_len_pos = HEADER_LEN + EXTENSION_HEADER_LEN;
239 let common_descriptors_length = (((bytes[common_desc_len_pos] & 0x0F) as usize) << 8)
240 | bytes[common_desc_len_pos + 1] as usize;
241 let common_desc_start = common_desc_len_pos + COMMON_DESC_LEN_FIELD;
242 let common_desc_end = common_desc_start + common_descriptors_length;
243 if common_desc_end > total - CRC_LEN {
244 return Err(Error::SectionLengthOverflow {
245 declared: common_descriptors_length,
246 available: (total - CRC_LEN).saturating_sub(common_desc_start),
247 });
248 }
249 let common_descriptors = DescriptorLoop::new(&bytes[common_desc_start..common_desc_end]);
250
251 let payload_end = total - CRC_LEN;
252 let mut pos = common_desc_end;
253 let mut resolution_providers = Vec::new();
254
255 while pos < payload_end {
256 if pos + RP_INFO_LEN_FIELD > payload_end {
257 return Err(Error::BufferTooShort {
258 need: pos + RP_INFO_LEN_FIELD,
259 have: payload_end,
260 what: "RntSection resolution_provider_info_length",
261 });
262 }
263 let rp_info_length = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
264 pos += RP_INFO_LEN_FIELD;
265 let rp_end = pos + rp_info_length;
266 if rp_end > payload_end {
267 return Err(Error::SectionLengthOverflow {
268 declared: rp_info_length,
269 available: payload_end.saturating_sub(pos),
270 });
271 }
272
273 if pos + RP_NAME_LEN_FIELD > rp_end {
274 return Err(Error::BufferTooShort {
275 need: pos + RP_NAME_LEN_FIELD,
276 have: rp_end,
277 what: "RntSection resolution_provider_name_length",
278 });
279 }
280 let name_len = bytes[pos] as usize;
281 pos += RP_NAME_LEN_FIELD;
282 if pos + name_len > rp_end {
283 return Err(Error::BufferTooShort {
284 need: pos + name_len,
285 have: rp_end,
286 what: "RntSection resolution_provider_name",
287 });
288 }
289 let name = DvbText::new(&bytes[pos..pos + name_len]);
290 pos += name_len;
291
292 if pos + RP_DESC_LEN_FIELD > rp_end {
293 return Err(Error::BufferTooShort {
294 need: pos + RP_DESC_LEN_FIELD,
295 have: rp_end,
296 what: "RntSection resolution_provider_descriptors_length",
297 });
298 }
299 let rp_desc_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
300 pos += RP_DESC_LEN_FIELD;
301 let rp_desc_start = pos;
302 let rp_desc_end = rp_desc_start + rp_desc_len;
303 if rp_desc_end > rp_end {
304 return Err(Error::SectionLengthOverflow {
305 declared: rp_desc_len,
306 available: rp_end.saturating_sub(rp_desc_start),
307 });
308 }
309 let descriptors = DescriptorLoop::new(&bytes[rp_desc_start..rp_desc_end]);
310 pos = rp_desc_end;
311
312 let mut crid_authorities = Vec::new();
313 while pos < rp_end {
314 if pos + CA_NAME_LEN_FIELD > rp_end {
315 return Err(Error::BufferTooShort {
316 need: pos + CA_NAME_LEN_FIELD,
317 have: rp_end,
318 what: "RntSection CRID_authority_name_length",
319 });
320 }
321 let ca_name_len = bytes[pos] as usize;
322 pos += CA_NAME_LEN_FIELD;
323 if pos + ca_name_len > rp_end {
324 return Err(Error::BufferTooShort {
325 need: pos + ca_name_len,
326 have: rp_end,
327 what: "RntSection CRID_authority_name",
328 });
329 }
330 let ca_name = DvbText::new(&bytes[pos..pos + ca_name_len]);
331 pos += ca_name_len;
332
333 if pos + CA_HEADER_LEN > rp_end {
334 return Err(Error::BufferTooShort {
335 need: pos + CA_HEADER_LEN,
336 have: rp_end,
337 what: "RntSection CRID_authority header",
338 });
339 }
340 let ca_packed = bytes[pos];
341 let crid_authority_policy = CridAuthorityPolicy::from_u8((ca_packed >> 4) & 0x03);
342 let ca_desc_len = (((ca_packed & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
343 pos += CA_HEADER_LEN;
344 let ca_desc_start = pos;
345 let ca_desc_end = ca_desc_start + ca_desc_len;
346 if ca_desc_end > rp_end {
347 return Err(Error::SectionLengthOverflow {
348 declared: ca_desc_len,
349 available: rp_end.saturating_sub(ca_desc_start),
350 });
351 }
352 let ca_descriptors = DescriptorLoop::new(&bytes[ca_desc_start..ca_desc_end]);
353 pos = ca_desc_end;
354
355 crid_authorities.push(CridAuthority {
356 name: ca_name,
357 crid_authority_policy,
358 descriptors: ca_descriptors,
359 });
360 }
361
362 resolution_providers.push(ResolutionProvider {
363 name,
364 descriptors,
365 crid_authorities,
366 });
367 pos = rp_end;
368 }
369
370 Ok(RntSection {
371 context_id,
372 version_number,
373 current_next_indicator,
374 section_number,
375 last_section_number,
376 context_id_type,
377 common_descriptors,
378 resolution_providers,
379 })
380 }
381}
382
383impl Serialize for RntSection<'_> {
384 type Error = crate::error::Error;
385
386 fn serialized_len(&self) -> usize {
387 HEADER_LEN
388 + EXTENSION_HEADER_LEN
389 + COMMON_DESC_LEN_FIELD
390 + self.common_descriptors.len()
391 + self
392 .resolution_providers
393 .iter()
394 .map(|rp| RP_INFO_LEN_FIELD + resolution_provider_serialized_len(rp))
395 .sum::<usize>()
396 + CRC_LEN
397 }
398
399 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
400 let len = self.serialized_len();
401 if buf.len() < len {
402 return Err(Error::OutputBufferTooSmall {
403 need: len,
404 have: buf.len(),
405 });
406 }
407
408 let section_length = (len - HEADER_LEN) as u16;
409 if section_length > 0x0FFF {
410 return Err(Error::SectionLengthOverflow {
411 declared: section_length as usize,
412 available: 0x0FFF,
413 });
414 }
415 buf[0] = TABLE_ID;
416 buf[1] = super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F);
417 buf[2] = (section_length & 0xFF) as u8;
418
419 buf[3..5].copy_from_slice(&self.context_id.to_be_bytes());
420 buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
421 buf[6] = self.section_number;
422 buf[7] = self.last_section_number;
423 buf[8] = self.context_id_type.to_u8();
424
425 let cdl = self.common_descriptors.len() as u16;
426 let cdl_pos = HEADER_LEN + EXTENSION_HEADER_LEN;
427 buf[cdl_pos] = RESERVED_NIBBLE | ((cdl >> 8) as u8 & 0x0F);
428 buf[cdl_pos + 1] = (cdl & 0xFF) as u8;
429
430 let cd_start = cdl_pos + COMMON_DESC_LEN_FIELD;
431 let cd_end = cd_start + self.common_descriptors.len();
432 buf[cd_start..cd_end].copy_from_slice(self.common_descriptors.raw());
433
434 let mut pos = cd_end;
435 for rp in &self.resolution_providers {
436 let rp_body_len = resolution_provider_serialized_len(rp);
437 let rp_info_length = rp_body_len as u16;
438 buf[pos] = RESERVED_NIBBLE | ((rp_info_length >> 8) as u8 & 0x0F);
439 buf[pos + 1] = (rp_info_length & 0xFF) as u8;
440 pos += RP_INFO_LEN_FIELD;
441
442 if rp.name.len() > u8::MAX as usize {
443 return Err(Error::ValueOutOfRange {
444 field: "resolution_provider_name_length",
445 reason: "exceeds 255 bytes",
446 });
447 }
448 buf[pos] = rp.name.len() as u8;
449 pos += RP_NAME_LEN_FIELD;
450 buf[pos..pos + rp.name.len()].copy_from_slice(rp.name.raw());
451 pos += rp.name.len();
452
453 let rdl = rp.descriptors.len() as u16;
454 buf[pos] = RESERVED_NIBBLE | ((rdl >> 8) as u8 & 0x0F);
455 buf[pos + 1] = (rdl & 0xFF) as u8;
456 pos += RP_DESC_LEN_FIELD;
457 buf[pos..pos + rp.descriptors.len()].copy_from_slice(rp.descriptors.raw());
458 pos += rp.descriptors.len();
459
460 for ca in &rp.crid_authorities {
461 if ca.name.len() > u8::MAX as usize {
462 return Err(Error::ValueOutOfRange {
463 field: "crid_authority_name_length",
464 reason: "exceeds 255 bytes",
465 });
466 }
467 buf[pos] = ca.name.len() as u8;
468 pos += CA_NAME_LEN_FIELD;
469 buf[pos..pos + ca.name.len()].copy_from_slice(ca.name.raw());
470 pos += ca.name.len();
471
472 let adl = ca.descriptors.len() as u16;
473 buf[pos] = 0xC0
474 | ((ca.crid_authority_policy.to_u8() & 0x03) << 4)
475 | ((adl >> 8) as u8 & 0x0F);
476 buf[pos + 1] = (adl & 0xFF) as u8;
477 pos += CA_HEADER_LEN;
478 buf[pos..pos + ca.descriptors.len()].copy_from_slice(ca.descriptors.raw());
479 pos += ca.descriptors.len();
480 }
481 }
482
483 let crc_pos = len - CRC_LEN;
484 let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
485 buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
486 Ok(len)
487 }
488}
489impl<'a> crate::traits::TableDef<'a> for RntSection<'a> {
490 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
491 const NAME: &'static str = "RESOLUTION_PROVIDER_NOTIFICATION";
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn parse_happy_path() {
500 let common_desc = [0x83u8, 0x02, 0xAB, 0xCD];
501 let rp = ResolutionProvider {
502 name: DvbText::new(b"bb"),
503 descriptors: DescriptorLoop::new(&[]),
504 crid_authorities: vec![CridAuthority {
505 name: DvbText::new(b"au"),
506 crid_authority_policy: CridAuthorityPolicy::Transient,
507 descriptors: DescriptorLoop::new(&[]),
508 }],
509 };
510 let rnt = RntSection {
511 context_id: 0x0042,
512 version_number: 3,
513 current_next_indicator: true,
514 section_number: 0,
515 last_section_number: 0,
516 context_id_type: ContextIdType::BouquetId,
517 common_descriptors: DescriptorLoop::new(&common_desc),
518 resolution_providers: vec![rp],
519 };
520 let mut buf = vec![0u8; rnt.serialized_len()];
521 rnt.serialize_into(&mut buf).unwrap();
522 let parsed = RntSection::parse(&buf).unwrap();
523 assert_eq!(parsed.context_id, 0x0042);
524 assert_eq!(parsed.version_number, 3);
525 assert!(parsed.current_next_indicator);
526 assert_eq!(parsed.context_id_type, ContextIdType::BouquetId);
527 assert_eq!(parsed.resolution_providers.len(), 1);
528 assert_eq!(parsed.resolution_providers[0].name.raw(), b"bb");
529 assert_eq!(parsed.resolution_providers[0].crid_authorities.len(), 1);
530 assert_eq!(
531 parsed.resolution_providers[0].crid_authorities[0].crid_authority_policy,
532 CridAuthorityPolicy::Transient
533 );
534 assert_eq!(
535 parsed.resolution_providers[0].crid_authorities[0]
536 .name
537 .raw(),
538 b"au"
539 );
540 }
541
542 #[test]
543 fn parse_no_descriptors_no_providers() {
544 let rnt = RntSection {
545 context_id: 0x0000,
546 version_number: 0,
547 current_next_indicator: false,
548 section_number: 0,
549 last_section_number: 0,
550 context_id_type: ContextIdType::BouquetId,
551 common_descriptors: DescriptorLoop::new(&[]),
552 resolution_providers: Vec::new(),
553 };
554 let mut buf = vec![0u8; rnt.serialized_len()];
555 rnt.serialize_into(&mut buf).unwrap();
556 let parsed = RntSection::parse(&buf).unwrap();
557 assert_eq!(parsed.common_descriptors.len(), 0);
558 assert!(parsed.resolution_providers.is_empty());
559 }
560
561 #[test]
562 fn byte_exact_round_trip() {
563 let rp = ResolutionProvider {
564 name: DvbText::new(b"provider"),
565 descriptors: DescriptorLoop::new(&[0x40, 0x03, b'R', b'N', b'T']),
566 crid_authorities: vec![
567 CridAuthority {
568 name: DvbText::new(b"auth1"),
569 crid_authority_policy: CridAuthorityPolicy::Permanent,
570 descriptors: DescriptorLoop::new(&[]),
571 },
572 CridAuthority {
573 name: DvbText::new(b"auth2"),
574 crid_authority_policy: CridAuthorityPolicy::Either,
575 descriptors: DescriptorLoop::new(&[0x42, 0x00]),
576 },
577 ],
578 };
579 let rnt = RntSection {
580 context_id: 0xABCD,
581 version_number: 15,
582 current_next_indicator: true,
583 section_number: 1,
584 last_section_number: 2,
585 context_id_type: ContextIdType::NetworkId,
586 common_descriptors: DescriptorLoop::new(&[0x40, 0x03, b'R', b'N', b'T']),
587 resolution_providers: vec![rp],
588 };
589 let mut buf = vec![0u8; rnt.serialized_len()];
590 rnt.serialize_into(&mut buf).unwrap();
591 let re = RntSection::parse(&buf).unwrap();
592 let mut buf2 = vec![0u8; re.serialized_len()];
593 re.serialize_into(&mut buf2).unwrap();
594 assert_eq!(buf, buf2, "byte-exact re-serialize");
595 let re = RntSection::parse(&buf).unwrap();
596 assert_eq!(re.resolution_providers.len(), 1);
597 assert_eq!(re.resolution_providers[0].name.raw(), b"provider");
598 assert_eq!(re.resolution_providers[0].crid_authorities.len(), 2);
599 assert_eq!(
600 re.resolution_providers[0].crid_authorities[1].crid_authority_policy,
601 CridAuthorityPolicy::Either
602 );
603 }
604
605 #[test]
606 fn parse_rejects_wrong_table_id() {
607 let rnt = RntSection {
608 context_id: 0x0001,
609 version_number: 0,
610 current_next_indicator: true,
611 section_number: 0,
612 last_section_number: 0,
613 context_id_type: ContextIdType::BouquetId,
614 common_descriptors: DescriptorLoop::new(&[]),
615 resolution_providers: Vec::new(),
616 };
617 let mut buf = vec![0u8; rnt.serialized_len()];
618 rnt.serialize_into(&mut buf).unwrap();
619 buf[0] = 0x70;
620 assert!(matches!(
621 RntSection::parse(&buf).unwrap_err(),
622 Error::UnexpectedTableId { table_id: 0x70, .. }
623 ));
624 }
625
626 #[test]
627 fn parse_rejects_short_buffer() {
628 assert!(matches!(
629 RntSection::parse(&[0x79, 0x00]).unwrap_err(),
630 Error::BufferTooShort { .. }
631 ));
632 }
633
634 #[test]
635 fn serialize_rejects_too_small_buffer() {
636 let rnt = RntSection {
637 context_id: 0x0001,
638 version_number: 0,
639 current_next_indicator: true,
640 section_number: 0,
641 last_section_number: 0,
642 context_id_type: ContextIdType::BouquetId,
643 common_descriptors: DescriptorLoop::new(&[]),
644 resolution_providers: Vec::new(),
645 };
646 let mut buf = vec![0u8; 2];
647 assert!(matches!(
648 rnt.serialize_into(&mut buf).unwrap_err(),
649 Error::OutputBufferTooSmall { .. }
650 ));
651 }
652
653 #[test]
654 fn parse_rejects_zero_section_length() {
655 let mut buf = vec![0u8; 64];
656 buf[0] = TABLE_ID;
657 buf[1] = 0xF0;
658 buf[2] = 0x00;
659 for b in &mut buf[3..] {
660 *b = 0xFF;
661 }
662 assert!(matches!(
663 RntSection::parse(&buf).unwrap_err(),
664 Error::SectionLengthOverflow { .. }
665 ));
666 }
667
668 #[test]
669 fn parse_handwritten_rnt_no_providers() {
670 let mut bytes: Vec<u8> = vec![
671 0x79, 0xF0, 0x0C, 0x00, 0x42, 0xC7, 0x00, 0x00, 0x01, 0xF0, 0x00,
672 ];
673 let crc = dvb_common::crc32_mpeg2::compute(&bytes);
674 bytes.extend_from_slice(&crc.to_be_bytes());
675 let rnt = RntSection::parse(&bytes).unwrap();
676 assert_eq!(rnt.context_id, 0x0042);
677 assert_eq!(rnt.version_number, 3);
678 assert!(rnt.current_next_indicator);
679 assert!(rnt.resolution_providers.is_empty());
680 }
681
682 #[test]
683 fn crid_authority_policy_round_trip() {
684 for v in 0u8..=3 {
685 let p = CridAuthorityPolicy::from_u8(v);
686 assert_eq!(p.to_u8(), v, "CridAuthorityPolicy round-trip for {v}");
687 }
688 }
689
690 #[test]
691 fn context_id_type_full_range_round_trip() {
692 for byte in 0u8..=0xFF {
693 let ct = ContextIdType::from_u8(byte);
694 assert_eq!(
695 ct.to_u8(),
696 byte,
697 "ContextIdType round-trip failed for {byte:#04x}"
698 );
699 }
700 }
701}