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//! Structure (§10.4.2 Table 109):
8//!   table_id(8) | section_syntax_indicator(1) | table_id_extension_flag(1) |
9//!   reserved(2) | section_length(12) | service_id(16) | reserved(2) |
10//!   version_number(5) | current_next_indicator(1) | section_number(8) |
11//!   last_section_number(8) | year_offset(16) | link_count(8) |
12//!   for j<link_count { reserved(4) | link_info_length(12) | link_info() } |
13//!   reserved_future_use(4) | descriptor_loop_length(12) | descriptors |
14//!   CRC_32(32)
15
16use crate::error::{Error, Result};
17use crate::traits::Table;
18use dvb_common::{Parse, Serialize};
19
20/// table_id for Related Content Table.
21pub const TABLE_ID: u8 = 0x76;
22
23/// Well-known PID on which RCT is carried.
24///
25/// RCT is signalled — its ES PID is named by a `related_content_descriptor`
26/// in the service PMT. There is no fixed broadcast PID. This constant is
27/// `0x0000` per the `Table` trait contract for tables without a fixed PID.
28pub const PID: u16 = 0x0000;
29
30// ── Length constants ────────────────────────────────────────────────────────
31
32/// Bytes 0-2: table_id + the section_syntax/flags/section_length word.
33const MIN_HEADER_LEN: usize = 3;
34
35/// Bytes 3-7: service_id(2) + version/cni byte(1) + section_number(1) +
36/// last_section_number(1).
37const EXTENSION_HEADER_LEN: usize = 5;
38
39/// Bytes after extension header: year_offset(2) + link_count(1) = 3 bytes.
40const POST_EXT_FIXED_LEN: usize = 3;
41
42/// Per-link header: reserved(4) + link_info_length(12) = 2 bytes.
43const LINK_ENTRY_HEADER_LEN: usize = 2;
44
45/// Descriptor loop length field: reserved_future_use(4) + descriptor_loop_length(12) = 2 bytes.
46const DESC_LOOP_LEN_FIELD: usize = 2;
47
48/// CRC-32 trailer.
49const CRC_LEN: usize = 4;
50
51/// Minimum parseable section length (no links, no descriptors).
52const MIN_SECTION_LEN: usize =
53    MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN + DESC_LOOP_LEN_FIELD + CRC_LEN;
54
55// ── Structs ─────────────────────────────────────────────────────────────────
56
57/// Related Content Table (ETSI TS 102 323 v1.4.1 §10.4.2).
58///
59/// The `link_info_loop` field holds the raw bytes of the entire
60/// `for (j=0; j<link_count; j++)` block (all link entries concatenated).
61/// The `descriptors` field holds the raw bytes of the trailing descriptor loop.
62/// Neither is parsed further; the integrator walks them using the spec tables.
63#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct Rct<'a> {
66    /// `table_id_extension_flag` (bit 6 of byte 1).
67    ///
68    /// `false`: `service_id` identifies the service this sub-table belongs to.
69    /// `true`: all sections relate to a single service; `service_id` is ignored.
70    pub table_id_extension_flag: bool,
71
72    /// `service_id` — table_id_extension field (bytes 3-4).
73    pub service_id: u16,
74
75    /// 5-bit `version_number` (bits 5-1 of byte 5).
76    pub version_number: u8,
77
78    /// `current_next_indicator` (bit 0 of byte 5).
79    pub current_next_indicator: bool,
80
81    /// `section_number` (byte 6).
82    pub section_number: u8,
83
84    /// `last_section_number` (byte 7).
85    pub last_section_number: u8,
86
87    /// `year_offset` — reference year (bytes 8-9).
88    ///
89    /// Binary encoding, e.g. `0x07D3` = 2003. Date values inside the section
90    /// are relative to this year.
91    pub year_offset: u16,
92
93    /// Number of link entries (`link_count`, byte 10).
94    pub link_count: u8,
95
96    /// Raw bytes of the entire link_info loop
97    /// (`for j=0; j<link_count` block from §10.4.2 Table 109).
98    ///
99    /// Each entry begins with a 2-byte header: `reserved(4) | link_info_length(12)`.
100    /// The following `link_info_length` bytes contain the link_info() payload
101    /// (§10.4.3 Table 110). The integrator is responsible for further parsing.
102    #[cfg_attr(feature = "serde", serde(borrow))]
103    pub link_info_loop: &'a [u8],
104
105    /// Raw bytes of the trailing descriptor loop
106    /// (`for k=0; k<descriptor_loop_length` from §10.4.2 Table 109).
107    #[cfg_attr(feature = "serde", serde(borrow))]
108    pub descriptors: &'a [u8],
109}
110
111// ── Parse ────────────────────────────────────────────────────────────────────
112
113impl<'a> Parse<'a> for Rct<'a> {
114    type Error = crate::error::Error;
115
116    fn parse(bytes: &'a [u8]) -> Result<Self> {
117        if bytes.len() < MIN_SECTION_LEN {
118            return Err(Error::BufferTooShort {
119                need: MIN_SECTION_LEN,
120                have: bytes.len(),
121                what: "Rct",
122            });
123        }
124
125        if bytes[0] != TABLE_ID {
126            return Err(Error::UnexpectedTableId {
127                table_id: bytes[0],
128                what: "Rct",
129                expected: &[TABLE_ID],
130            });
131        }
132
133        // Byte 1: section_syntax_indicator(1) | table_id_extension_flag(1) | reserved(2) |
134        //         section_length_hi(4)
135        // Byte 2: section_length_lo(8)
136        let table_id_extension_flag = (bytes[1] & 0x40) != 0;
137        let section_length = (((bytes[1] & 0x0F) as u16) << 8) | bytes[2] as u16;
138        let total = MIN_HEADER_LEN + section_length as usize;
139
140        if bytes.len() < total {
141            return Err(Error::SectionLengthOverflow {
142                declared: section_length as usize,
143                available: bytes.len() - MIN_HEADER_LEN,
144            });
145        }
146
147        // Byte 3-4: service_id
148        let service_id = u16::from_be_bytes([bytes[3], bytes[4]]);
149
150        // Byte 5: reserved(2) | version_number(5) | current_next_indicator(1)
151        let version_number = (bytes[5] >> 1) & 0x1F;
152        let current_next_indicator = (bytes[5] & 0x01) != 0;
153
154        // Byte 6: section_number
155        // Byte 7: last_section_number
156        let section_number = bytes[6];
157        let last_section_number = bytes[7];
158
159        // Byte 8-9: year_offset
160        let year_offset = u16::from_be_bytes([bytes[8], bytes[9]]);
161
162        // Byte 10: link_count
163        let link_count = bytes[10];
164
165        // Walk the link_info loop to find its total byte span.
166        // Each entry: 2-byte header (reserved(4)|link_info_length(12)) + link_info_length bytes.
167        let payload_end = total - CRC_LEN; // byte index just past last payload byte
168        let mut pos = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN; // byte 11
169        let link_loop_start = pos;
170
171        for j in 0..link_count {
172            if pos + LINK_ENTRY_HEADER_LEN > payload_end {
173                return Err(Error::BufferTooShort {
174                    need: pos + LINK_ENTRY_HEADER_LEN,
175                    have: payload_end,
176                    what: "Rct link_entry header",
177                });
178            }
179            let link_info_length = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
180            let entry_end = pos + LINK_ENTRY_HEADER_LEN + link_info_length;
181            if entry_end > payload_end {
182                return Err(Error::SectionLengthOverflow {
183                    declared: link_info_length,
184                    available: payload_end.saturating_sub(pos + LINK_ENTRY_HEADER_LEN),
185                });
186            }
187            let _ = j; // used only for the error message above
188            pos = entry_end;
189        }
190
191        let link_info_loop = &bytes[link_loop_start..pos];
192
193        // Descriptor loop length field: reserved_future_use(4) | descriptor_loop_length(12)
194        if pos + DESC_LOOP_LEN_FIELD > payload_end {
195            return Err(Error::BufferTooShort {
196                need: pos + DESC_LOOP_LEN_FIELD,
197                have: payload_end,
198                what: "Rct descriptor_loop_length field",
199            });
200        }
201        let descriptor_loop_length =
202            (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
203        let desc_start = pos + DESC_LOOP_LEN_FIELD;
204        let desc_end = desc_start + descriptor_loop_length;
205
206        if desc_end > payload_end {
207            return Err(Error::SectionLengthOverflow {
208                declared: descriptor_loop_length,
209                available: payload_end.saturating_sub(desc_start),
210            });
211        }
212
213        let descriptors = &bytes[desc_start..desc_end];
214
215        Ok(Rct {
216            table_id_extension_flag,
217            service_id,
218            version_number,
219            current_next_indicator,
220            section_number,
221            last_section_number,
222            year_offset,
223            link_count,
224            link_info_loop,
225            descriptors,
226        })
227    }
228}
229
230// ── Serialize ────────────────────────────────────────────────────────────────
231
232impl Serialize for Rct<'_> {
233    type Error = crate::error::Error;
234
235    fn serialized_len(&self) -> usize {
236        MIN_HEADER_LEN
237            + EXTENSION_HEADER_LEN
238            + POST_EXT_FIXED_LEN
239            + self.link_info_loop.len()
240            + DESC_LOOP_LEN_FIELD
241            + self.descriptors.len()
242            + CRC_LEN
243    }
244
245    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
246        let len = self.serialized_len();
247        if buf.len() < len {
248            return Err(Error::OutputBufferTooSmall {
249                need: len,
250                have: buf.len(),
251            });
252        }
253
254        let section_length = (len - MIN_HEADER_LEN) as u16;
255
256        // Byte 0: table_id
257        buf[0] = TABLE_ID;
258
259        // Byte 1: section_syntax_indicator(1)=1 | table_id_extension_flag(1) |
260        //         reserved(2)=11 | section_length_hi(4)
261        let tief_bit: u8 = if self.table_id_extension_flag {
262            0x40
263        } else {
264            0x00
265        };
266        buf[1] = 0x80 | tief_bit | 0x30 | ((section_length >> 8) as u8 & 0x0F);
267        buf[2] = (section_length & 0xFF) as u8;
268
269        // Bytes 3-4: service_id
270        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
271
272        // Byte 5: reserved(2)=11 | version_number(5) | current_next_indicator(1)
273        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
274
275        // Bytes 6-7: section_number, last_section_number
276        buf[6] = self.section_number;
277        buf[7] = self.last_section_number;
278
279        // Bytes 8-9: year_offset
280        buf[8..10].copy_from_slice(&self.year_offset.to_be_bytes());
281
282        // Byte 10: link_count
283        buf[10] = self.link_count;
284
285        // Link info loop (raw bytes, already framed with per-entry headers)
286        let loop_start = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + POST_EXT_FIXED_LEN;
287        let loop_end = loop_start + self.link_info_loop.len();
288        buf[loop_start..loop_end].copy_from_slice(self.link_info_loop);
289
290        // Descriptor loop length field: reserved_future_use(4)=1111 | descriptor_loop_length(12)
291        let dll = self.descriptors.len() as u16;
292        buf[loop_end] = 0xF0 | ((dll >> 8) as u8 & 0x0F);
293        buf[loop_end + 1] = (dll & 0xFF) as u8;
294
295        let desc_start = loop_end + DESC_LOOP_LEN_FIELD;
296        let desc_end = desc_start + self.descriptors.len();
297        buf[desc_start..desc_end].copy_from_slice(self.descriptors);
298
299        // CRC-32: compute over everything up to (but not including) the CRC slot.
300        let crc = dvb_common::crc32_mpeg2::compute(&buf[..desc_end]);
301        buf[desc_end..desc_end + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
302
303        Ok(len)
304    }
305}
306
307// ── Table impl ────────────────────────────────────────────────────────────────
308
309impl<'a> Table<'a> for Rct<'a> {
310    const TABLE_ID: u8 = TABLE_ID;
311    const PID: u16 = PID;
312}
313
314impl<'a> crate::traits::TableDef<'a> for Rct<'a> {
315    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
316    const NAME: &'static str = "RELATED_CONTENT";
317}
318
319// ── Tests ─────────────────────────────────────────────────────────────────────
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    /// Build a minimal RCT section byte vector suitable for parsing.
326    ///
327    /// `link_info_loop_bytes` must already be formatted as the raw wire bytes
328    /// for the entire link loop (i.e. `link_count` entries each preceded by
329    /// their 2-byte `reserved|link_info_length` header).
330    #[allow(clippy::too_many_arguments)]
331    fn build_rct(
332        service_id: u16,
333        version: u8,
334        current_next: bool,
335        section_number: u8,
336        last_section_number: u8,
337        year_offset: u16,
338        link_count: u8,
339        link_info_loop_bytes: &[u8],
340        descriptors: &[u8],
341    ) -> Vec<u8> {
342        let rct = Rct {
343            table_id_extension_flag: false,
344            service_id,
345            version_number: version,
346            current_next_indicator: current_next,
347            section_number,
348            last_section_number,
349            year_offset,
350            link_count,
351            link_info_loop: link_info_loop_bytes,
352            descriptors,
353        };
354        let mut buf = vec![0u8; rct.serialized_len()];
355        rct.serialize_into(&mut buf).unwrap();
356        buf
357    }
358
359    #[test]
360    fn parse_happy_path_no_links_no_descriptors() {
361        // service_id=0x0064 (100), version=3, current=true, sec 0/0,
362        // year_offset=2003 (0x07D3), link_count=0
363        let bytes = build_rct(0x0064, 3, true, 0, 0, 0x07D3, 0, &[], &[]);
364        let rct = Rct::parse(&bytes).unwrap();
365
366        assert!(!rct.table_id_extension_flag);
367        assert_eq!(rct.service_id, 0x0064);
368        assert_eq!(rct.version_number, 3);
369        assert!(rct.current_next_indicator);
370        assert_eq!(rct.section_number, 0);
371        assert_eq!(rct.last_section_number, 0);
372        assert_eq!(rct.year_offset, 0x07D3);
373        assert_eq!(rct.link_count, 0);
374        assert_eq!(rct.link_info_loop, &[] as &[u8]);
375        assert_eq!(rct.descriptors, &[] as &[u8]);
376    }
377
378    #[test]
379    fn parse_happy_path_with_one_link_and_descriptor() {
380        // Construct one link_info entry of 4 bytes of payload.
381        // Header: reserved(4)=0xF | link_info_length=4 → bytes [0xF0, 0x04]
382        // Payload: arbitrary 4 bytes.
383        let link_payload: &[u8] = &[0xF0, 0x04, 0xAB, 0xCD, 0xEF, 0x01];
384        // One private descriptor (tag=0x80, length=2, data=[0x01, 0x02]).
385        let desc: &[u8] = &[0x80, 0x02, 0x01, 0x02];
386
387        let bytes = build_rct(0x1234, 7, true, 1, 3, 2003, 1, link_payload, desc);
388        let rct = Rct::parse(&bytes).unwrap();
389
390        assert_eq!(rct.service_id, 0x1234);
391        assert_eq!(rct.version_number, 7);
392        assert_eq!(rct.link_count, 1);
393        assert_eq!(rct.link_info_loop, link_payload);
394        assert_eq!(rct.descriptors, desc);
395        assert_eq!(rct.section_number, 1);
396        assert_eq!(rct.last_section_number, 3);
397        assert_eq!(rct.year_offset, 2003);
398    }
399
400    #[test]
401    fn parse_rejects_wrong_table_id() {
402        let mut bytes = build_rct(0x0001, 0, true, 0, 0, 2024, 0, &[], &[]);
403        bytes[0] = 0x4A; // BAT table_id
404        let err = Rct::parse(&bytes).unwrap_err();
405        assert!(matches!(
406            err,
407            Error::UnexpectedTableId { table_id: 0x4A, .. }
408        ));
409    }
410
411    #[test]
412    fn parse_rejects_buffer_too_short() {
413        // Less than MIN_SECTION_LEN bytes.
414        let err = Rct::parse(&[0x76, 0x80, 0x00]).unwrap_err();
415        assert!(matches!(err, Error::BufferTooShort { .. }));
416    }
417
418    #[test]
419    fn serialize_round_trip() {
420        let link_loop: &[u8] = &[0xF0, 0x03, 0x11, 0x22, 0x33];
421        let desc: &[u8] = &[0x58, 0x00]; // local_time_offset_descriptor, length 0
422
423        let rct = Rct {
424            table_id_extension_flag: true,
425            service_id: 0xABCD,
426            version_number: 15,
427            current_next_indicator: false,
428            section_number: 2,
429            last_section_number: 5,
430            year_offset: 2024,
431            link_count: 1,
432            link_info_loop: link_loop,
433            descriptors: desc,
434        };
435
436        let mut buf = vec![0u8; rct.serialized_len()];
437        rct.serialize_into(&mut buf).unwrap();
438        let parsed = Rct::parse(&buf).unwrap();
439
440        assert_eq!(parsed.table_id_extension_flag, rct.table_id_extension_flag);
441        assert_eq!(parsed.service_id, rct.service_id);
442        assert_eq!(parsed.version_number, rct.version_number);
443        assert_eq!(parsed.current_next_indicator, rct.current_next_indicator);
444        assert_eq!(parsed.section_number, rct.section_number);
445        assert_eq!(parsed.last_section_number, rct.last_section_number);
446        assert_eq!(parsed.year_offset, rct.year_offset);
447        assert_eq!(parsed.link_count, rct.link_count);
448        assert_eq!(parsed.link_info_loop, rct.link_info_loop);
449        assert_eq!(parsed.descriptors, rct.descriptors);
450    }
451}