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