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