Skip to main content

dvb_si/tables/
rnt.rs

1//! Resolution provider Notification Table — ETSI TS 102 323 v1.4.1 §5.2.2.
2//!
3//! Carries the locations of CRI (Content Referencing Information) and metadata
4//! for CRID authorities. Carried on PID 0x0016 with table_id 0x79.
5//!
6//! The resolution-provider loop is unfolded into [`ResolutionProvider`] and
7//! [`CridAuthority`] entries (Table 1, §5.2.2).
8
9use crate::descriptors::DescriptorLoop;
10use crate::error::{Error, Result};
11use crate::text::DvbText;
12use alloc::vec::Vec;
13use dvb_common::{Parse, Serialize};
14
15/// `table_id` for the Resolution provider Notification Table.
16pub const TABLE_ID: u8 = 0x79;
17/// Well-known PID on which RNT sections are carried.
18pub 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/// CRID authority policy — ETSI TS 102 323 §5.2.2 Table 3.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37#[non_exhaustive]
38pub enum CridAuthorityPolicy {
39    /// '00' — Permanent (CRIDs are never re-used).
40    Permanent,
41    /// '01' — Transient (CRIDs may be re-used over time).
42    Transient,
43    /// '10' — Either (each CRID may be transient or permanent).
44    Either,
45    /// '11' — Reserved.
46    Reserved,
47}
48
49impl CridAuthorityPolicy {
50    #[must_use]
51    /// Decode from the wire value.  Every value maps (lossless).
52    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    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
63    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    /// Human-readable spec display name.
74    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/// Context ID type — ETSI TS 102 323 §5.2.2 Table 2.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize))]
88#[non_exhaustive]
89pub enum ContextIdType {
90    /// 0x00 — context_id is a value of bouquet_id.
91    BouquetId,
92    /// 0x01 — context_id is a value of original_network_id.
93    OriginalNetworkId,
94    /// 0x02 — context_id is a value of network_id.
95    NetworkId,
96    /// 0x03..=0x7F — DVB reserved.
97    DvbReserved(u8),
98    /// 0x80..=0xFF — User defined.
99    UserDefined(u8),
100}
101
102impl ContextIdType {
103    #[must_use]
104    /// Decode from the wire value.  Every value maps (lossless).
105    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    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
117    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    /// Human-readable spec display name.
128    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/// A CRID authority entry within a resolution provider (Table 1, §5.2.2).
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143pub struct CridAuthority<'a> {
144    /// CRID authority name (EN 300 468 Annex A text).
145    pub name: DvbText<'a>,
146    /// `CRID_authority_policy` — 2-bit value (Table 3):
147    /// 0 = permanent, 1 = transient, 2 = either, 3 = reserved.
148    pub crid_authority_policy: CridAuthorityPolicy,
149    /// CRID authority descriptor loop.
150    pub descriptors: DescriptorLoop<'a>,
151}
152
153/// A resolution-provider entry (Table 1, §5.2.2).
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize))]
156pub struct ResolutionProvider<'a> {
157    /// Resolution provider name (EN 300 468 Annex A text).
158    pub name: DvbText<'a>,
159    /// Per-provider descriptor loop.
160    pub descriptors: DescriptorLoop<'a>,
161    /// CRID authority sub-entries.
162    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/// Resolution provider Notification Table (ETSI TS 102 323 v1.4.1 §5.2.2,
181/// Table 1).
182///
183/// The resolution-provider loop is unfolded into typed
184/// [`ResolutionProvider`] entries.
185#[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    /// 16-bit context identifier (table_id_extension).
190    pub context_id: u16,
191    /// 5-bit version_number.
192    pub version_number: u8,
193    /// `current_next_indicator` bit.
194    pub current_next_indicator: bool,
195    /// section_number in the sub-table sequence.
196    pub section_number: u8,
197    /// last_section_number in the sub-table sequence.
198    pub last_section_number: u8,
199    /// `context_id_type` byte (Table 2).
200    pub context_id_type: ContextIdType,
201    /// Common descriptor loop. Serializes as the typed descriptor sequence;
202    /// `.raw()` yields the wire bytes.
203    pub common_descriptors: DescriptorLoop<'a>,
204    /// Resolution-provider entries — unfolded per Table 1.
205    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}