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