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