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