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