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