Skip to main content

dvb_si/tables/
rct.rs

1//! Related Content Table — ETSI TS 102 323 v1.4.1 §10.4.
2//!
3//! Signals links to related material for a service. Carried in the ES whose
4//! PID is named by a `related_content_descriptor` in that service's PMT
5//! (stream_type 0x05, private sections). There is no fixed PID.
6//!
7//! The link_info loop is unfolded into [`LinkInfo`] entries (Table 110, §10.4.3)
8//! with the conditional `dvb_binary_locator` sub-structure typed as
9//! [`DvbBinaryLocator`] (Table 31, §7.3.2.3.3).
10
11use crate::descriptors::DescriptorLoop;
12
13/// Identifier type — ETSI TS 102 323 v1.4.1 §7.3.2.3.3 Table 32
14/// (`docs/ts_102_323_tva.md`, Table 32 — Identifier type).
15///
16/// 2-bit field in the DVB binary locator. Selects what kind of event identifier
17/// field (if any) is present.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20#[non_exhaustive]
21pub enum IdentifierType {
22    /// `0b00` — no event identifier field is present.
23    None,
24    /// `0b01` — event identifier is an `event_id`.
25    EventId,
26    /// `0b10` — event identifier is a `TVA_id` carried in EIT.
27    TvaIdEit,
28    /// `0b11` — event identifier is a `TVA_id` carried in PES.
29    TvaIdPes,
30}
31
32impl IdentifierType {
33    #[must_use]
34    /// Creates a value from a 2-bit wire nibble (upper bits masked off).
35    pub fn from_u8(v: u8) -> Self {
36        match v & 0x03 {
37            0x00 => Self::None,
38            0x01 => Self::EventId,
39            0x02 => Self::TvaIdEit,
40            0x03 => Self::TvaIdPes,
41            // unreachable after masking to 2 bits, but the compiler requires it
42            _ => unreachable!(),
43        }
44    }
45
46    #[must_use]
47    /// Returns the 2-bit wire nibble for this value.
48    pub fn to_u8(self) -> u8 {
49        match self {
50            Self::None => 0x00,
51            Self::EventId => 0x01,
52            Self::TvaIdEit => 0x02,
53            Self::TvaIdPes => 0x03,
54        }
55    }
56
57    #[must_use]
58    /// Returns the spec token for this value.
59    pub fn name(self) -> &'static str {
60        match self {
61            Self::None => "none",
62            Self::EventId => "event_id",
63            Self::TvaIdEit => "TVA_id (EIT)",
64            Self::TvaIdPes => "TVA_id (PES)",
65        }
66    }
67}
68dvb_common::impl_spec_display!(IdentifierType);
69use crate::error::{Error, Result};
70use crate::text::{DvbText, LangCode};
71use alloc::vec::Vec;
72use dvb_common::{Parse, Serialize};
73
74/// `table_id` for Related Content Table.
75pub const TABLE_ID: u8 = 0x76;
76
77/// Well-known PID on which RCT is carried: none (signalled via PMT).
78pub const PID: u16 = 0x0000;
79
80const MIN_HEADER_LEN: usize = 3;
81const EXTENSION_HEADER_LEN: usize = 5;
82const POST_EXT_FIXED_LEN: usize = 3;
83const LINK_ENTRY_HEADER_LEN: usize = 2;
84const DESC_LOOP_LEN_FIELD: usize = 2;
85const CRC_LEN: usize = 4;
86const MIN_SECTION_LEN: usize =
87    MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN + DESC_LOOP_LEN_FIELD + CRC_LEN;
88
89const LINK_TYPE_MASK: u8 = 0xF0;
90const LINK_TYPE_SHIFT: u8 = 4;
91const HOW_RELATED_HI_MASK: u8 = 0x03;
92const TERM_ID_HI_MASK: u8 = 0x0F;
93const TERM_ID_HI_SHIFT: u8 = 8;
94const GROUP_ID_MASK: u8 = 0xF0;
95const GROUP_ID_SHIFT: u8 = 4;
96const PRECEDENCE_MASK: u8 = 0x0F;
97
98const LOCATOR_ID_TYPE_MASK: u8 = 0xC0;
99const LOCATOR_ID_TYPE_SHIFT: u8 = 6;
100const LOCATOR_RELIABILITY_MASK: u8 = 0x20;
101const LOCATOR_INLINE_MASK: u8 = 0x10;
102const LOCATOR_START_DATE_HI_MASK: u8 = 0x07;
103const LOCATOR_START_DATE_HI_SHIFT: usize = 6;
104const LOCATOR_RESERVED_BITS: u8 = 0x08;
105
106const ITEM_RFU_MASK: u8 = 0xC0;
107const ITEM_COUNT_MASK: u8 = 0x3F;
108
109const LINK_INFO_HEADER_RFU: u8 = 0x0C;
110
111const ICON_FLAG_MASK: u8 = 0x80;
112const ICON_ID_MASK: u8 = 0x70;
113const ICON_ID_SHIFT: u8 = 4;
114const ICON_DESC_LEN_HI_MASK: u8 = 0x0F;
115
116/// Link type — ETSI TS 102 323 §10.4.3 Table 111.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize))]
119#[non_exhaustive]
120pub enum LinkType {
121    /// 0x0 — URI string only.
122    UriString,
123    /// 0x1 — Binary locator only.
124    BinaryLocator,
125    /// 0x2 — Both binary locator and URI.
126    Both,
127    /// 0x3 — Through means of a descriptor.
128    Descriptor,
129    /// 0x4..=0xF — DVB reserved.
130    DvbReserved(u8),
131}
132
133impl LinkType {
134    #[must_use]
135    /// Decode from the wire value.  Every value maps (lossless).
136    pub fn from_u8(v: u8) -> Self {
137        match v & 0x0F {
138            0x0 => Self::UriString,
139            0x1 => Self::BinaryLocator,
140            0x2 => Self::Both,
141            0x3 => Self::Descriptor,
142            v => Self::DvbReserved(v),
143        }
144    }
145
146    #[must_use]
147    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
148    pub fn to_u8(self) -> u8 {
149        match self {
150            Self::UriString => 0x0,
151            Self::BinaryLocator => 0x1,
152            Self::Both => 0x2,
153            Self::Descriptor => 0x3,
154            Self::DvbReserved(v) => v,
155        }
156    }
157
158    #[must_use]
159    /// Human-readable spec display name.
160    pub fn name(self) -> &'static str {
161        match self {
162            Self::UriString => "URI String",
163            Self::BinaryLocator => "Binary Locator",
164            Self::Both => "Both",
165            Self::Descriptor => "Descriptor",
166            Self::DvbReserved(_) => "DVB Reserved",
167        }
168    }
169}
170dvb_common::impl_spec_display!(LinkType, DvbReserved);
171
172/// How-related classification scheme ID — ETSI TS 102 323 §10.4.3 Table 112.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize))]
175#[non_exhaustive]
176pub enum HowRelated {
177    /// 0x00 — HowRelatedCS:2004.
178    Cs2004,
179    /// 0x01 — HowRelatedCS:2005.
180    Cs2005,
181    /// 0x02 — HowRelatedCS:2007.
182    Cs2007,
183    /// 0x03..=0x2F — DVB reserved.
184    DvbReserved(u8),
185    /// 0x30..=0x3F — User private.
186    UserPrivate(u8),
187}
188
189impl HowRelated {
190    #[must_use]
191    /// Decode from the wire value.  Every value maps (lossless).
192    pub fn from_u8(v: u8) -> Self {
193        match v {
194            0x00 => Self::Cs2004,
195            0x01 => Self::Cs2005,
196            0x02 => Self::Cs2007,
197            v if v < 0x30 => Self::DvbReserved(v),
198            _ => Self::UserPrivate(v),
199        }
200    }
201
202    #[must_use]
203    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
204    pub fn to_u8(self) -> u8 {
205        match self {
206            Self::Cs2004 => 0x00,
207            Self::Cs2005 => 0x01,
208            Self::Cs2007 => 0x02,
209            Self::DvbReserved(v) | Self::UserPrivate(v) => v,
210        }
211    }
212
213    #[must_use]
214    /// Human-readable spec display name.
215    pub fn name(self) -> &'static str {
216        match self {
217            Self::Cs2004 => "HowRelatedCS:2004",
218            Self::Cs2005 => "HowRelatedCS:2005",
219            Self::Cs2007 => "HowRelatedCS:2007",
220            Self::DvbReserved(_) => "DVB Reserved",
221            Self::UserPrivate(_) => "User Private",
222        }
223    }
224}
225dvb_common::impl_spec_display!(HowRelated, DvbReserved, UserPrivate);
226
227/// A promotional text item within a link_info entry (Table 110, §10.4.3).
228#[derive(Debug, Clone, PartialEq, Eq)]
229#[cfg_attr(feature = "serde", derive(serde::Serialize))]
230pub struct LinkItem<'a> {
231    /// ISO 639-2 language code.
232    pub language_code: LangCode,
233    /// Promotional text (EN 300 468 Annex A).
234    pub promotional_text: DvbText<'a>,
235}
236
237fn link_item_serialized_len(item: &LinkItem) -> usize {
238    3 + 1 + item.promotional_text.len()
239}
240
241/// Service identification within a DVB binary locator (Table 31, §7.3.2.3.3).
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize))]
244#[non_exhaustive]
245pub enum DvbLocatorService {
246    /// `inline_service == 0`: 10-bit `DVB_service_triplet_ID`.
247    Triplet {
248        /// 10-bit DVB service triplet ID.
249        dvb_service_triplet_id: u16,
250    },
251    /// `inline_service == 1`: full triplet.
252    Full {
253        /// `transport_stream_id`.
254        transport_stream_id: u16,
255        /// `original_network_id`.
256        original_network_id: u16,
257        /// `service_id`.
258        service_id: u16,
259    },
260}
261
262/// Event identifier within a DVB binary locator (Table 31).
263#[derive(Debug, Clone, PartialEq, Eq)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize))]
265#[non_exhaustive]
266pub enum DvbLocatorIdentifier {
267    /// `identifier_type == 0x00`: no identifier field.
268    None,
269    /// `identifier_type == 0x01`: `event_id`.
270    EventId {
271        /// 16-bit event_id.
272        event_id: u16,
273    },
274    /// `identifier_type == 0x02`: `TVA_id` carried in EIT.
275    TvaIdEit {
276        /// 16-bit TVA_id.
277        tva_id: u16,
278    },
279    /// `identifier_type == 0x03`: `TVA_id` carried in PES + `component`.
280    TvaIdPes {
281        /// 16-bit TVA_id.
282        tva_id: u16,
283        /// 8-bit component.
284        component: u8,
285    },
286}
287
288/// Time windows within a DVB binary locator (Table 31).
289#[derive(Debug, Clone, PartialEq, Eq)]
290#[cfg_attr(feature = "serde", derive(serde::Serialize))]
291pub struct DvbLocatorWindows {
292    /// `early_start_window` (3 bits).
293    pub early_start_window: u8,
294    /// `late_end_window` (5 bits).
295    pub late_end_window: u8,
296}
297
298/// DVB binary locator (Table 31, §7.3.2.3.3).
299#[derive(Debug, Clone, PartialEq, Eq)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize))]
301pub struct DvbBinaryLocator {
302    /// `identifier_type` (2 bits, Table 32) — [`IdentifierType`].
303    pub identifier_type: IdentifierType,
304    /// `scheduled_time_reliability`.
305    pub scheduled_time_reliability: bool,
306    /// `inline_service`.
307    pub inline_service: bool,
308    /// `start_date` (9 bits).
309    pub start_date: u16,
310    /// Service identification (conditional on `inline_service`).
311    pub service: DvbLocatorService,
312    /// `start_time` (16 bits).
313    pub start_time: u16,
314    /// `duration` (16 bits).
315    pub duration: u16,
316    /// Event identifier (conditional on `identifier_type`).
317    pub identifier: DvbLocatorIdentifier,
318    /// Time windows (present iff `identifier_type == 0 && scheduled_time_reliability == 1`).
319    pub windows: Option<DvbLocatorWindows>,
320}
321
322fn locator_serialized_len(loc: &DvbBinaryLocator) -> usize {
323    let mut len = 2;
324    len += match &loc.service {
325        DvbLocatorService::Triplet { .. } => 1,
326        DvbLocatorService::Full { .. } => 6,
327    };
328    len += 4;
329    len += match &loc.identifier {
330        DvbLocatorIdentifier::None => 0,
331        DvbLocatorIdentifier::EventId { .. } => 2,
332        DvbLocatorIdentifier::TvaIdEit { .. } => 2,
333        DvbLocatorIdentifier::TvaIdPes { .. } => 3,
334    };
335    if loc.windows.is_some() {
336        len += 1;
337    }
338    len
339}
340
341/// A single link_info entry (Table 110, §10.4.3).
342#[derive(Debug, Clone, PartialEq, Eq)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize))]
344pub struct LinkInfo<'a> {
345    /// `link_type` (4 bits, Table 111).
346    pub link_type: LinkType,
347    /// `how_related_classification_scheme_id` (6 bits).
348    pub how_related: HowRelated,
349    /// `term_id` (12 bits).
350    pub term_id: u16,
351    /// `group_id` (4 bits).
352    pub group_id: u8,
353    /// `precedence` (4 bits).
354    pub precedence: u8,
355    /// Media URI bytes (present iff `link_type == 0x00 || link_type == 0x02`).
356    pub media_uri: Option<&'a [u8]>,
357    /// DVB binary locator (present iff `link_type == 0x01 || link_type == 0x02`).
358    pub dvb_binary_locator: Option<DvbBinaryLocator>,
359    /// Promotional text items.
360    pub items: Vec<LinkItem<'a>>,
361    /// `default_icon_flag`.
362    pub default_icon_flag: bool,
363    /// `icon_id` (3 bits).
364    pub icon_id: u8,
365    /// Per-link descriptor loop.
366    pub descriptors: DescriptorLoop<'a>,
367}
368
369fn link_info_serialized_len(li: &LinkInfo) -> usize {
370    let mut len = 4;
371    len += li.media_uri.map_or(0, |u| 1 + u.len());
372    len += li
373        .dvb_binary_locator
374        .as_ref()
375        .map_or(0, locator_serialized_len);
376    len += 1;
377    len += li.items.iter().map(link_item_serialized_len).sum::<usize>();
378    len += 2;
379    len += li.descriptors.len();
380    len
381}
382
383/// Related Content Table (ETSI TS 102 323 v1.4.1 §10.4.2, Table 109).
384///
385/// The link_info loop is unfolded into typed [`LinkInfo`] entries.
386#[derive(Debug, Clone, PartialEq, Eq)]
387#[cfg_attr(feature = "serde", derive(serde::Serialize))]
388#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
389pub struct RctSection<'a> {
390    /// `table_id_extension_flag` (bit 6 of byte 1).
391    pub table_id_extension_flag: bool,
392    /// `service_id` — table_id_extension field.
393    pub service_id: u16,
394    /// 5-bit `version_number`.
395    pub version_number: u8,
396    /// `current_next_indicator`.
397    pub current_next_indicator: bool,
398    /// `section_number`.
399    pub section_number: u8,
400    /// `last_section_number`.
401    pub last_section_number: u8,
402    /// `year_offset` — reference year.
403    pub year_offset: u16,
404    /// Link info entries — unfolded per Table 110.
405    pub links: Vec<LinkInfo<'a>>,
406    /// Trailing descriptor loop.
407    pub descriptors: DescriptorLoop<'a>,
408}
409
410fn parse_locator(data: &[u8]) -> Result<(DvbBinaryLocator, usize)> {
411    if data.len() < 2 {
412        return Err(Error::BufferTooShort {
413            need: 2,
414            have: data.len(),
415            what: "RctSection dvb_binary_locator header",
416        });
417    }
418    let b0 = data[0];
419    let b1 = data[1];
420    let identifier_type =
421        IdentifierType::from_u8((b0 & LOCATOR_ID_TYPE_MASK) >> LOCATOR_ID_TYPE_SHIFT);
422    let scheduled_time_reliability = (b0 & LOCATOR_RELIABILITY_MASK) != 0;
423    let inline_service = (b0 & LOCATOR_INLINE_MASK) != 0;
424    let start_date = ((b0 & LOCATOR_START_DATE_HI_MASK) as u16) << LOCATOR_START_DATE_HI_SHIFT
425        | ((b1 >> 2) as u16 & 0x3F);
426
427    let mut pos = 2;
428    let service = if inline_service {
429        if pos + 6 > data.len() {
430            return Err(Error::BufferTooShort {
431                need: pos + 6,
432                have: data.len(),
433                what: "RctSection dvb_binary_locator full triplet",
434            });
435        }
436        let tsid = u16::from_be_bytes([data[pos], data[pos + 1]]);
437        let onid = u16::from_be_bytes([data[pos + 2], data[pos + 3]]);
438        let sid = u16::from_be_bytes([data[pos + 4], data[pos + 5]]);
439        pos += 6;
440        DvbLocatorService::Full {
441            transport_stream_id: tsid,
442            original_network_id: onid,
443            service_id: sid,
444        }
445    } else {
446        if data.len() < 3 {
447            return Err(Error::BufferTooShort {
448                need: 3,
449                have: data.len(),
450                what: "RctSection dvb_binary_locator triplet",
451            });
452        }
453        let triplet_id = ((b1 as u16 & 0x03) << 8) | data[2] as u16;
454        pos = 3;
455        DvbLocatorService::Triplet {
456            dvb_service_triplet_id: triplet_id,
457        }
458    };
459
460    if pos + 4 > data.len() {
461        return Err(Error::BufferTooShort {
462            need: pos + 4,
463            have: data.len(),
464            what: "RctSection dvb_binary_locator start_time/duration",
465        });
466    }
467    let start_time = u16::from_be_bytes([data[pos], data[pos + 1]]);
468    let duration = u16::from_be_bytes([data[pos + 2], data[pos + 3]]);
469    pos += 4;
470
471    let identifier = match identifier_type {
472        IdentifierType::None => DvbLocatorIdentifier::None,
473        IdentifierType::EventId => {
474            if pos + 2 > data.len() {
475                return Err(Error::BufferTooShort {
476                    need: pos + 2,
477                    have: data.len(),
478                    what: "RctSection dvb_binary_locator event_id",
479                });
480            }
481            let event_id = u16::from_be_bytes([data[pos], data[pos + 1]]);
482            pos += 2;
483            DvbLocatorIdentifier::EventId { event_id }
484        }
485        IdentifierType::TvaIdEit => {
486            if pos + 2 > data.len() {
487                return Err(Error::BufferTooShort {
488                    need: pos + 2,
489                    have: data.len(),
490                    what: "RctSection dvb_binary_locator TVA_id (EIT)",
491                });
492            }
493            let tva_id = u16::from_be_bytes([data[pos], data[pos + 1]]);
494            pos += 2;
495            DvbLocatorIdentifier::TvaIdEit { tva_id }
496        }
497        IdentifierType::TvaIdPes => {
498            if pos + 3 > data.len() {
499                return Err(Error::BufferTooShort {
500                    need: pos + 3,
501                    have: data.len(),
502                    what: "RctSection dvb_binary_locator TVA_id (PES)",
503                });
504            }
505            let tva_id = u16::from_be_bytes([data[pos], data[pos + 1]]);
506            let component = data[pos + 2];
507            pos += 3;
508            DvbLocatorIdentifier::TvaIdPes { tva_id, component }
509        }
510    };
511
512    let windows = if identifier_type == IdentifierType::None && scheduled_time_reliability {
513        if pos >= data.len() {
514            return Err(Error::BufferTooShort {
515                need: pos + 1,
516                have: data.len(),
517                what: "RctSection dvb_binary_locator windows",
518            });
519        }
520        let wb = data[pos];
521        pos += 1;
522        Some(DvbLocatorWindows {
523            early_start_window: (wb >> 5) & 0x07,
524            late_end_window: wb & 0x1F,
525        })
526    } else {
527        None
528    };
529
530    Ok((
531        DvbBinaryLocator {
532            identifier_type,
533            scheduled_time_reliability,
534            inline_service,
535            start_date,
536            service,
537            start_time,
538            duration,
539            identifier,
540            windows,
541        },
542        pos,
543    ))
544}
545
546fn serialize_locator(loc: &DvbBinaryLocator, buf: &mut [u8]) -> usize {
547    let b0 = (loc.identifier_type.to_u8() << LOCATOR_ID_TYPE_SHIFT)
548        | (u8::from(loc.scheduled_time_reliability) << 5)
549        | (u8::from(loc.inline_service) << 4)
550        | LOCATOR_RESERVED_BITS
551        | ((loc.start_date >> LOCATOR_START_DATE_HI_SHIFT) as u8 & LOCATOR_START_DATE_HI_MASK);
552    buf[0] = b0;
553
554    let mut pos: usize;
555    match &loc.service {
556        DvbLocatorService::Triplet {
557            dvb_service_triplet_id,
558        } => {
559            let sd_lo = (loc.start_date & 0x3F) as u8;
560            buf[1] = (sd_lo << 2) | ((dvb_service_triplet_id >> 8) as u8 & 0x03);
561            buf[2] = (dvb_service_triplet_id & 0xFF) as u8;
562            pos = 3;
563        }
564        DvbLocatorService::Full {
565            transport_stream_id,
566            original_network_id,
567            service_id,
568        } => {
569            let sd_lo = (loc.start_date & 0x3F) as u8;
570            buf[1] = (sd_lo << 2) | 0x03;
571            buf[2..4].copy_from_slice(&transport_stream_id.to_be_bytes());
572            buf[4..6].copy_from_slice(&original_network_id.to_be_bytes());
573            buf[6..8].copy_from_slice(&service_id.to_be_bytes());
574            pos = 8;
575        }
576    }
577    buf[pos..pos + 2].copy_from_slice(&loc.start_time.to_be_bytes());
578    buf[pos + 2..pos + 4].copy_from_slice(&loc.duration.to_be_bytes());
579    pos += 4;
580
581    match &loc.identifier {
582        DvbLocatorIdentifier::None => {}
583        DvbLocatorIdentifier::EventId { event_id } => {
584            buf[pos..pos + 2].copy_from_slice(&event_id.to_be_bytes());
585            pos += 2;
586        }
587        DvbLocatorIdentifier::TvaIdEit { tva_id } => {
588            buf[pos..pos + 2].copy_from_slice(&tva_id.to_be_bytes());
589            pos += 2;
590        }
591        DvbLocatorIdentifier::TvaIdPes { tva_id, component } => {
592            buf[pos..pos + 2].copy_from_slice(&tva_id.to_be_bytes());
593            buf[pos + 2] = *component;
594            pos += 3;
595        }
596    }
597    if let Some(w) = &loc.windows {
598        buf[pos] = ((w.early_start_window & 0x07) << 5) | (w.late_end_window & 0x1F);
599        pos += 1;
600    }
601    pos
602}
603
604fn parse_link_info(data: &[u8], link_info_length: usize) -> Result<LinkInfo<'_>> {
605    if data.len() < 4 {
606        return Err(Error::BufferTooShort {
607            need: 4,
608            have: data.len(),
609            what: "RctSection link_info header",
610        });
611    }
612    let link_type = LinkType::from_u8((data[0] & LINK_TYPE_MASK) >> LINK_TYPE_SHIFT);
613    let how_related_raw = (data[0] & HOW_RELATED_HI_MASK) << 4 | (data[1] >> 4) & 0x0F;
614    let how_related = HowRelated::from_u8(how_related_raw);
615    let term_id = ((data[1] as u16 & TERM_ID_HI_MASK as u16) << TERM_ID_HI_SHIFT) | data[2] as u16;
616    let group_id = (data[3] & GROUP_ID_MASK) >> GROUP_ID_SHIFT;
617    let precedence = data[3] & PRECEDENCE_MASK;
618
619    let mut pos = 4;
620    let end = link_info_length;
621
622    let media_uri = if link_type == LinkType::UriString || link_type == LinkType::Both {
623        if pos >= end {
624            return Err(Error::BufferTooShort {
625                need: pos + 1,
626                have: end,
627                what: "RctSection media_uri_length",
628            });
629        }
630        let uri_len = data[pos] as usize;
631        pos += 1;
632        if pos + uri_len > end {
633            return Err(Error::BufferTooShort {
634                need: pos + uri_len,
635                have: end,
636                what: "RctSection media_uri",
637            });
638        }
639        let uri = &data[pos..pos + uri_len];
640        pos += uri_len;
641        Some(uri)
642    } else {
643        None
644    };
645
646    let dvb_binary_locator = if link_type == LinkType::BinaryLocator || link_type == LinkType::Both
647    {
648        if pos >= end {
649            return Err(Error::BufferTooShort {
650                need: pos + 1,
651                have: end,
652                what: "RctSection dvb_binary_locator",
653            });
654        }
655        let (loc, consumed) = parse_locator(&data[pos..end])?;
656        pos += consumed;
657        Some(loc)
658    } else {
659        None
660    };
661
662    if pos >= end {
663        return Err(Error::BufferTooShort {
664            need: pos + 1,
665            have: end,
666            what: "RctSection number_items",
667        });
668    }
669    let ni_byte = data[pos];
670    let number_items = (ni_byte & ITEM_COUNT_MASK) as usize;
671    pos += 1;
672
673    let mut items = Vec::with_capacity(number_items);
674    for _ in 0..number_items {
675        if pos + 4 > end {
676            return Err(Error::BufferTooShort {
677                need: pos + 4,
678                have: end,
679                what: "RctSection link_item",
680            });
681        }
682        let language_code = LangCode([data[pos], data[pos + 1], data[pos + 2]]);
683        let text_len = data[pos + 3] as usize;
684        pos += 4;
685        if pos + text_len > end {
686            return Err(Error::BufferTooShort {
687                need: pos + text_len,
688                have: end,
689                what: "RctSection promotional_text",
690            });
691        }
692        let promotional_text = DvbText::new(&data[pos..pos + text_len]);
693        pos += text_len;
694        items.push(LinkItem {
695            language_code,
696            promotional_text,
697        });
698    }
699
700    if pos + 2 > end {
701        return Err(Error::BufferTooShort {
702            need: pos + 2,
703            have: end,
704            what: "RctSection icon/desc",
705        });
706    }
707    let default_icon_flag = (data[pos] & ICON_FLAG_MASK) != 0;
708    let icon_id = (data[pos] & ICON_ID_MASK) >> ICON_ID_SHIFT;
709    let desc_len = (((data[pos] & ICON_DESC_LEN_HI_MASK) as usize) << 8) | data[pos + 1] as usize;
710    pos += 2;
711    let desc_start = pos;
712    let desc_end = desc_start + desc_len;
713    if desc_end > end {
714        return Err(Error::SectionLengthOverflow {
715            declared: desc_len,
716            available: end.saturating_sub(desc_start),
717        });
718    }
719    let descriptors = DescriptorLoop::new(&data[desc_start..desc_end]);
720
721    Ok(LinkInfo {
722        link_type,
723        how_related,
724        term_id,
725        group_id,
726        precedence,
727        media_uri,
728        dvb_binary_locator,
729        items,
730        default_icon_flag,
731        icon_id,
732        descriptors,
733    })
734}
735
736fn serialize_link_info(li: &LinkInfo, buf: &mut [u8]) -> usize {
737    let lt = li.link_type.to_u8();
738    let hr = li.how_related.to_u8();
739    buf[0] =
740        ((lt & 0x0F) << LINK_TYPE_SHIFT) | LINK_INFO_HEADER_RFU | ((hr >> 4) & HOW_RELATED_HI_MASK);
741    buf[1] = ((hr & 0x0F) << 4) | ((li.term_id >> TERM_ID_HI_SHIFT) as u8 & TERM_ID_HI_MASK);
742    buf[2] = li.term_id as u8;
743    buf[3] = ((li.group_id & 0x0F) << GROUP_ID_SHIFT) | (li.precedence & PRECEDENCE_MASK);
744
745    let mut pos = 4;
746    if let Some(uri) = li.media_uri {
747        buf[pos] = uri.len() as u8;
748        pos += 1;
749        buf[pos..pos + uri.len()].copy_from_slice(uri);
750        pos += uri.len();
751    }
752    if let Some(ref loc) = li.dvb_binary_locator {
753        let n = serialize_locator(loc, &mut buf[pos..]);
754        pos += n;
755    }
756    buf[pos] = ITEM_RFU_MASK | (li.items.len() as u8 & ITEM_COUNT_MASK);
757    pos += 1;
758    for item in &li.items {
759        buf[pos..pos + 3].copy_from_slice(&item.language_code.0);
760        buf[pos + 3] = item.promotional_text.len() as u8;
761        pos += 4;
762        buf[pos..pos + item.promotional_text.len()].copy_from_slice(item.promotional_text.raw());
763        pos += item.promotional_text.len();
764    }
765    let dll = li.descriptors.len() as u16;
766    buf[pos] = u8::from(li.default_icon_flag) << 7
767        | ((li.icon_id & 0x07) << ICON_ID_SHIFT)
768        | ((dll >> 8) as u8 & ICON_DESC_LEN_HI_MASK);
769    buf[pos + 1] = (dll & 0xFF) as u8;
770    pos += 2;
771    buf[pos..pos + li.descriptors.len()].copy_from_slice(li.descriptors.raw());
772    pos += li.descriptors.len();
773    pos
774}
775
776impl<'a> Parse<'a> for RctSection<'a> {
777    type Error = crate::error::Error;
778
779    fn parse(bytes: &'a [u8]) -> Result<Self> {
780        if bytes.len() < MIN_SECTION_LEN {
781            return Err(Error::BufferTooShort {
782                need: MIN_SECTION_LEN,
783                have: bytes.len(),
784                what: "RctSection",
785            });
786        }
787        if bytes[0] != TABLE_ID {
788            return Err(Error::UnexpectedTableId {
789                table_id: bytes[0],
790                what: "RctSection",
791                expected: &[TABLE_ID],
792            });
793        }
794
795        let table_id_extension_flag = (bytes[1] & 0x40) != 0;
796        let section_length = (((bytes[1] & 0x0F) as u16) << 8) | bytes[2] as u16;
797        let total = super::check_section_length(
798            bytes.len(),
799            MIN_HEADER_LEN,
800            section_length as usize,
801            MIN_SECTION_LEN,
802        )?;
803
804        let service_id = u16::from_be_bytes([bytes[3], bytes[4]]);
805        let version_number = (bytes[5] >> 1) & 0x1F;
806        let current_next_indicator = (bytes[5] & 0x01) != 0;
807        let section_number = bytes[6];
808        let last_section_number = bytes[7];
809        let year_offset = u16::from_be_bytes([bytes[8], bytes[9]]);
810        let link_count = bytes[10];
811
812        let payload_end = total - CRC_LEN;
813        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN;
814
815        let mut links = Vec::with_capacity(link_count as usize);
816        for _ in 0..link_count {
817            if pos + LINK_ENTRY_HEADER_LEN > payload_end {
818                return Err(Error::BufferTooShort {
819                    need: pos + LINK_ENTRY_HEADER_LEN,
820                    have: payload_end,
821                    what: "RctSection link_entry header",
822                });
823            }
824            let link_info_length = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
825            let link_data_start = pos + LINK_ENTRY_HEADER_LEN;
826            let link_data_end = link_data_start + link_info_length;
827            if link_data_end > payload_end {
828                return Err(Error::SectionLengthOverflow {
829                    declared: link_info_length,
830                    available: payload_end.saturating_sub(link_data_start),
831                });
832            }
833            let link_info =
834                parse_link_info(&bytes[link_data_start..link_data_end], link_info_length)?;
835            links.push(link_info);
836            pos = link_data_end;
837        }
838
839        if pos + DESC_LOOP_LEN_FIELD > payload_end {
840            return Err(Error::BufferTooShort {
841                need: pos + DESC_LOOP_LEN_FIELD,
842                have: payload_end,
843                what: "RctSection descriptor_loop_length field",
844            });
845        }
846        let descriptor_loop_length =
847            (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
848        let desc_start = pos + DESC_LOOP_LEN_FIELD;
849        let desc_end = desc_start + descriptor_loop_length;
850        if desc_end > payload_end {
851            return Err(Error::SectionLengthOverflow {
852                declared: descriptor_loop_length,
853                available: payload_end.saturating_sub(desc_start),
854            });
855        }
856        let descriptors = DescriptorLoop::new(&bytes[desc_start..desc_end]);
857
858        Ok(RctSection {
859            table_id_extension_flag,
860            service_id,
861            version_number,
862            current_next_indicator,
863            section_number,
864            last_section_number,
865            year_offset,
866            links,
867            descriptors,
868        })
869    }
870}
871
872impl Serialize for RctSection<'_> {
873    type Error = crate::error::Error;
874
875    fn serialized_len(&self) -> usize {
876        MIN_HEADER_LEN
877            + EXTENSION_HEADER_LEN
878            + POST_EXT_FIXED_LEN
879            + self
880                .links
881                .iter()
882                .map(|li| LINK_ENTRY_HEADER_LEN + link_info_serialized_len(li))
883                .sum::<usize>()
884            + DESC_LOOP_LEN_FIELD
885            + self.descriptors.len()
886            + CRC_LEN
887    }
888
889    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
890        let len = self.serialized_len();
891        if buf.len() < len {
892            return Err(Error::OutputBufferTooSmall {
893                need: len,
894                have: buf.len(),
895            });
896        }
897
898        let section_length_usize = len - MIN_HEADER_LEN;
899        if section_length_usize > 0x0FFF {
900            return Err(Error::SectionLengthOverflow {
901                declared: section_length_usize,
902                available: 0x0FFF,
903            });
904        }
905        let section_length = section_length_usize as u16;
906        buf[0] = TABLE_ID;
907        let tief_bit: u8 = if self.table_id_extension_flag {
908            0x40
909        } else {
910            0x00
911        };
912        buf[1] = super::SECTION_B1_SSI
913            | tief_bit
914            | super::SECTION_B1_RESERVED_HI
915            | ((section_length >> 8) as u8 & 0x0F);
916        buf[2] = (section_length & 0xFF) as u8;
917
918        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
919        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
920        buf[6] = self.section_number;
921        buf[7] = self.last_section_number;
922        buf[8..10].copy_from_slice(&self.year_offset.to_be_bytes());
923        if self.links.len() > u8::MAX as usize {
924            return Err(Error::SectionLengthOverflow {
925                declared: self.links.len(),
926                available: u8::MAX as usize,
927            });
928        }
929        buf[10] = self.links.len() as u8;
930
931        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN;
932        for li in &self.links {
933            let li_body_len = link_info_serialized_len(li) as u16;
934            buf[pos] = 0xF0 | ((li_body_len >> 8) as u8 & 0x0F);
935            buf[pos + 1] = (li_body_len & 0xFF) as u8;
936            pos += LINK_ENTRY_HEADER_LEN;
937            pos += serialize_link_info(li, &mut buf[pos..]);
938        }
939
940        let dll = self.descriptors.len() as u16;
941        buf[pos] = 0xF0 | ((dll >> 8) as u8 & 0x0F);
942        buf[pos + 1] = (dll & 0xFF) as u8;
943        pos += DESC_LOOP_LEN_FIELD;
944        buf[pos..pos + self.descriptors.len()].copy_from_slice(self.descriptors.raw());
945        pos += self.descriptors.len();
946
947        let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
948        buf[pos..pos + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
949        Ok(len)
950    }
951}
952impl<'a> crate::traits::TableDef<'a> for RctSection<'a> {
953    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
954    const NAME: &'static str = "RELATED_CONTENT";
955}
956
957#[cfg(test)]
958mod tests {
959    use super::*;
960
961    #[test]
962    fn parse_no_links_no_descriptors() {
963        let rct = RctSection {
964            table_id_extension_flag: false,
965            service_id: 0x0064,
966            version_number: 3,
967            current_next_indicator: true,
968            section_number: 0,
969            last_section_number: 0,
970            year_offset: 0x07D3,
971            links: Vec::new(),
972            descriptors: DescriptorLoop::new(&[]),
973        };
974        let mut buf = vec![0u8; rct.serialized_len()];
975        rct.serialize_into(&mut buf).unwrap();
976        let p = RctSection::parse(&buf).unwrap();
977        assert!(!p.table_id_extension_flag);
978        assert_eq!(p.service_id, 0x0064);
979        assert_eq!(p.year_offset, 0x07D3);
980        assert!(p.links.is_empty());
981    }
982
983    #[test]
984    fn parse_one_link_uri_only() {
985        let li = LinkInfo {
986            link_type: LinkType::UriString,
987            how_related: HowRelated::Cs2005,
988            term_id: 0x123,
989            group_id: 0x5,
990            precedence: 0x9,
991            media_uri: Some(b"http://example.com"),
992            dvb_binary_locator: None,
993            items: Vec::new(),
994            default_icon_flag: false,
995            icon_id: 0,
996            descriptors: DescriptorLoop::new(&[]),
997        };
998        let rct = RctSection {
999            table_id_extension_flag: false,
1000            service_id: 0x1234,
1001            version_number: 7,
1002            current_next_indicator: true,
1003            section_number: 1,
1004            last_section_number: 3,
1005            year_offset: 2003,
1006            links: vec![li],
1007            descriptors: DescriptorLoop::new(&[]),
1008        };
1009        let mut buf = vec![0u8; rct.serialized_len()];
1010        rct.serialize_into(&mut buf).unwrap();
1011        let p = RctSection::parse(&buf).unwrap();
1012        assert_eq!(p.links.len(), 1);
1013        assert_eq!(p.links[0].link_type, LinkType::UriString);
1014        assert_eq!(p.links[0].how_related, HowRelated::Cs2005);
1015        assert_eq!(p.links[0].term_id, 0x123);
1016        assert_eq!(p.links[0].group_id, 0x5);
1017        assert_eq!(p.links[0].precedence, 0x9);
1018        assert_eq!(p.links[0].media_uri.unwrap(), b"http://example.com");
1019    }
1020
1021    #[test]
1022    fn parse_one_link_with_locator_and_items() {
1023        let loc = DvbBinaryLocator {
1024            identifier_type: IdentifierType::EventId,
1025            scheduled_time_reliability: false,
1026            inline_service: true,
1027            start_date: 0x0FF,
1028            service: DvbLocatorService::Full {
1029                transport_stream_id: 0x1000,
1030                original_network_id: 0x2000,
1031                service_id: 0x3000,
1032            },
1033            start_time: 0x8000,
1034            duration: 0x4000,
1035            identifier: DvbLocatorIdentifier::EventId { event_id: 0xBEEF },
1036            windows: None,
1037        };
1038        let li = LinkInfo {
1039            link_type: LinkType::BinaryLocator,
1040            how_related: HowRelated::Cs2007,
1041            term_id: 0x456,
1042            group_id: 0x1,
1043            precedence: 0x2,
1044            media_uri: None,
1045            dvb_binary_locator: Some(loc),
1046            items: vec![LinkItem {
1047                language_code: LangCode(*b"eng"),
1048                promotional_text: DvbText::new(b"Promo"),
1049            }],
1050            default_icon_flag: true,
1051            icon_id: 3,
1052            descriptors: DescriptorLoop::new(&[0x80, 0x00]),
1053        };
1054        let rct = RctSection {
1055            table_id_extension_flag: true,
1056            service_id: 0xABCD,
1057            version_number: 15,
1058            current_next_indicator: false,
1059            section_number: 2,
1060            last_section_number: 5,
1061            year_offset: 2024,
1062            links: vec![li],
1063            descriptors: DescriptorLoop::new(&[]),
1064        };
1065        let mut buf = vec![0u8; rct.serialized_len()];
1066        rct.serialize_into(&mut buf).unwrap();
1067        let mut buf2 = vec![0u8; rct.serialized_len()];
1068        rct.serialize_into(&mut buf2).unwrap();
1069        assert_eq!(buf, buf2, "byte-exact re-serialize");
1070        let p = RctSection::parse(&buf).unwrap();
1071        assert_eq!(p.links.len(), 1);
1072        assert_eq!(p.links[0].link_type, LinkType::BinaryLocator);
1073        let l = p.links[0].dvb_binary_locator.as_ref().unwrap();
1074        assert_eq!(l.identifier_type, IdentifierType::EventId);
1075        assert!(l.inline_service);
1076        assert_eq!(l.start_date, 0x0FF);
1077        assert_eq!(p.links[0].items.len(), 1);
1078        assert_eq!(p.links[0].items[0].language_code, LangCode(*b"eng"));
1079        assert!(p.links[0].default_icon_flag);
1080        assert_eq!(p.links[0].icon_id, 3);
1081    }
1082
1083    #[test]
1084    fn byte_exact_round_trip_simple() {
1085        let li = LinkInfo {
1086            link_type: LinkType::UriString,
1087            how_related: HowRelated::Cs2004,
1088            term_id: 0,
1089            group_id: 0,
1090            precedence: 0,
1091            media_uri: Some(b"uri"),
1092            dvb_binary_locator: None,
1093            items: Vec::new(),
1094            default_icon_flag: false,
1095            icon_id: 0,
1096            descriptors: DescriptorLoop::new(&[]),
1097        };
1098        let rct = RctSection {
1099            table_id_extension_flag: false,
1100            service_id: 0x0001,
1101            version_number: 0,
1102            current_next_indicator: true,
1103            section_number: 0,
1104            last_section_number: 0,
1105            year_offset: 0x07D3,
1106            links: vec![li],
1107            descriptors: DescriptorLoop::new(&[]),
1108        };
1109        let mut buf = vec![0u8; rct.serialized_len()];
1110        rct.serialize_into(&mut buf).unwrap();
1111        let parsed = RctSection::parse(&buf).unwrap();
1112        let mut buf2 = vec![0u8; parsed.serialized_len()];
1113        parsed.serialize_into(&mut buf2).unwrap();
1114        assert_eq!(buf, buf2);
1115    }
1116
1117    #[test]
1118    fn parse_rejects_wrong_table_id() {
1119        let rct = RctSection {
1120            table_id_extension_flag: false,
1121            service_id: 0x0001,
1122            version_number: 0,
1123            current_next_indicator: true,
1124            section_number: 0,
1125            last_section_number: 0,
1126            year_offset: 2024,
1127            links: Vec::new(),
1128            descriptors: DescriptorLoop::new(&[]),
1129        };
1130        let mut buf = vec![0u8; rct.serialized_len()];
1131        rct.serialize_into(&mut buf).unwrap();
1132        buf[0] = 0x4A;
1133        assert!(matches!(
1134            RctSection::parse(&buf).unwrap_err(),
1135            Error::UnexpectedTableId { table_id: 0x4A, .. }
1136        ));
1137    }
1138
1139    #[test]
1140    fn parse_rejects_buffer_too_short() {
1141        assert!(matches!(
1142            RctSection::parse(&[0x76, 0x80, 0x00]).unwrap_err(),
1143            Error::BufferTooShort { .. }
1144        ));
1145    }
1146
1147    #[test]
1148    fn serialize_rejects_output_buffer_too_small() {
1149        let rct = RctSection {
1150            table_id_extension_flag: false,
1151            service_id: 0x0001,
1152            version_number: 0,
1153            current_next_indicator: true,
1154            section_number: 0,
1155            last_section_number: 0,
1156            year_offset: 0,
1157            links: Vec::new(),
1158            descriptors: DescriptorLoop::new(&[]),
1159        };
1160        let mut buf = vec![0u8; 2];
1161        assert!(matches!(
1162            rct.serialize_into(&mut buf).unwrap_err(),
1163            Error::OutputBufferTooSmall { .. }
1164        ));
1165    }
1166
1167    #[test]
1168    fn parse_rejects_zero_section_length() {
1169        let mut buf = vec![0u8; 64];
1170        buf[0] = TABLE_ID;
1171        buf[1] = 0xF0;
1172        buf[2] = 0x00;
1173        for b in &mut buf[3..] {
1174            *b = 0xFF;
1175        }
1176        assert!(matches!(
1177            RctSection::parse(&buf).unwrap_err(),
1178            Error::SectionLengthOverflow { .. }
1179        ));
1180    }
1181
1182    #[test]
1183    fn parse_handwritten_rct_no_links() {
1184        let mut bytes: Vec<u8> = vec![
1185            0x76, 0x80, 0x0E, 0x00, 0x64, 0xC7, 0x00, 0x00, 0x07, 0xD3, 0x00, 0xF0, 0x00,
1186        ];
1187        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
1188        bytes.extend_from_slice(&crc.to_be_bytes());
1189        let rct = RctSection::parse(&bytes).unwrap();
1190        assert_eq!(rct.service_id, 0x0064);
1191        assert_eq!(rct.year_offset, 0x07D3);
1192        assert!(rct.links.is_empty());
1193    }
1194
1195    #[test]
1196    fn link_type_full_range_round_trip() {
1197        for v in 0u8..=0x0F {
1198            let lt = LinkType::from_u8(v);
1199            assert_eq!(lt.to_u8(), v, "LinkType round-trip failed for {v:#04x}");
1200        }
1201    }
1202
1203    #[test]
1204    fn how_related_full_range_round_trip() {
1205        for v in 0u8..=0x3F {
1206            let hr = HowRelated::from_u8(v);
1207            assert_eq!(hr.to_u8(), v, "HowRelated round-trip failed for {v:#04x}");
1208        }
1209    }
1210
1211    #[test]
1212    fn identifier_type_full_range_round_trip() {
1213        for v in 0u8..=0x03 {
1214            let it = IdentifierType::from_u8(v);
1215            assert_eq!(
1216                it.to_u8(),
1217                v,
1218                "IdentifierType round-trip failed for 0x{v:02X}"
1219            );
1220        }
1221    }
1222
1223    #[test]
1224    fn identifier_type_known_values() {
1225        assert_eq!(IdentifierType::from_u8(0), IdentifierType::None);
1226        assert_eq!(IdentifierType::from_u8(1), IdentifierType::EventId);
1227        assert_eq!(IdentifierType::from_u8(2), IdentifierType::TvaIdEit);
1228        assert_eq!(IdentifierType::from_u8(3), IdentifierType::TvaIdPes);
1229        assert_eq!(IdentifierType::None.name(), "none");
1230        assert_eq!(IdentifierType::EventId.name(), "event_id");
1231        assert_eq!(IdentifierType::TvaIdEit.name(), "TVA_id (EIT)");
1232    }
1233}