Skip to main content

dvb_si/tables/
cit.rs

1//! Content Identifier Table — ETSI TS 102 323 v1.4.1 §12.2.
2//!
3//! The CIT maps content reference identifiers (CRIDs) to events for a given
4//! service. Carried on PID 0x0012 (shared with the EIT) with table_id 0x77.
5//! Structure: fixed header + prepend-string block + typed CRID entry loop + CRC-32.
6//!
7//! The CRID entry loop is unfolded into [`CridEntry`] instances (Table 119,
8//! §12.2). The prepend-string block is a flat byte array of null-terminated
9//! fragments addressed by index; it is kept raw (`&[u8]`) since each entry is
10//! just a single byte — there is no per-entry sub-structure to unfold.
11
12use crate::error::{Error, Result};
13use crate::text::DvbText;
14use alloc::vec::Vec;
15use dvb_common::{Parse, Serialize};
16
17/// `table_id` for Content Identifier Table.
18pub const TABLE_ID: u8 = 0x77;
19
20/// PID on which CIT sections are carried.
21///
22/// Note: PID 0x0012 is shared with the EIT family; demultiplexers must filter
23/// by table_id in addition to PID to isolate CIT sections.
24pub const PID: u16 = 0x0012;
25
26const HEADER_LEN: usize = 3;
27const EXTENSION_LEN: usize = 10;
28const CRC_LEN: usize = 4;
29const MIN_SECTION_LEN: usize = HEADER_LEN + EXTENSION_LEN + CRC_LEN;
30
31const CRID_REF_LEN: usize = 2;
32const CRID_ENTRY_FIXED_LEN: usize = CRID_REF_LEN + 1 + 1;
33
34/// A single CRID entry in the CIT loop (Table 119, §12.2).
35///
36/// Wire layout: `crid_ref(16) | prepend_string_index(8) | unique_string_length(8)
37/// | unique_string_byte[8]×unique_string_length`.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40pub struct CridEntry<'a> {
41    /// `crid_ref` — 16-bit reference into the CRID resolution system.
42    pub crid_ref: u16,
43    /// `prepend_string_index` — index into the prepend-string block.
44    /// `0xFF` means no prepend string (the unique string is the full CRID).
45    pub prepend_string_index: u8,
46    /// `unique_string` — the unique portion of the CRID, decoded as DVB text.
47    pub unique_string: DvbText<'a>,
48}
49
50/// Content Identifier Table (ETSI TS 102 323 v1.4.1 §12.2, Table 119).
51///
52/// The `crid_entries` loop is unfolded into typed [`CridEntry`] instances.
53/// The `prepend_strings` block is kept as a raw byte slice (flat array of
54/// null-terminated fragments with no per-entry sub-structure to type).
55#[derive(Debug, Clone, PartialEq, Eq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize))]
57#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
58pub struct CitSection<'a> {
59    /// `private_indicator` bit from byte 1.
60    pub private_indicator: bool,
61    /// `service_id` — identifies the container this section belongs to
62    /// (table_id_extension, bytes 3-4).
63    pub service_id: u16,
64    /// 5-bit `version_number`.
65    pub version_number: u8,
66    /// `current_next_indicator` bit.
67    pub current_next_indicator: bool,
68    /// Section counter within the sub-table.
69    pub section_number: u8,
70    /// Final section number in the sub-table.
71    pub last_section_number: u8,
72    /// `transport_stream_id` of the carrying TS.
73    pub transport_stream_id: u16,
74    /// `original_network_id` of the originating network.
75    pub original_network_id: u16,
76    /// Prepend-string block (null-terminated fragments addressed by index),
77    /// decoded as DVB text.
78    pub prepend_strings: DvbText<'a>,
79    /// CRID entry loop — unfolded per Table 119.
80    pub crid_entries: Vec<CridEntry<'a>>,
81}
82
83impl<'a> CitSection<'a> {
84    /// Resolve a prepend string by its `prepend_string_index`.
85    ///
86    /// Returns `None` if `index` is out of range. The block is a sequence of
87    /// null-terminated fragments; index 0 is the first fragment, index 1 the
88    /// second, etc. The returned slice includes everything up to (but not
89    /// including) the terminating NUL byte; an empty slice means the index
90    /// points to an empty or immediately-terminated fragment.
91    pub fn prepend_string(&self, index: u8) -> Option<&[u8]> {
92        let raw: &[u8] = &self.prepend_strings;
93        let mut remaining: &[u8] = raw;
94        let mut current: u8 = 0;
95        while !remaining.is_empty() {
96            let nul_pos = remaining
97                .iter()
98                .position(|&b| b == 0)
99                .unwrap_or(remaining.len());
100            let fragment = &remaining[..nul_pos];
101            if current == index {
102                return Some(fragment);
103            }
104            current += 1;
105            remaining = if nul_pos < remaining.len() {
106                &remaining[nul_pos + 1..]
107            } else {
108                &[]
109            };
110        }
111        None
112    }
113}
114
115fn crid_entry_serialized_len(e: &CridEntry) -> usize {
116    CRID_ENTRY_FIXED_LEN + e.unique_string.len()
117}
118
119impl<'a> Parse<'a> for CitSection<'a> {
120    type Error = crate::error::Error;
121
122    fn parse(bytes: &'a [u8]) -> Result<Self> {
123        if bytes.len() < MIN_SECTION_LEN {
124            return Err(Error::BufferTooShort {
125                need: MIN_SECTION_LEN,
126                have: bytes.len(),
127                what: "CitSection",
128            });
129        }
130        if bytes[0] != TABLE_ID {
131            return Err(Error::UnexpectedTableId {
132                table_id: bytes[0],
133                what: "CitSection",
134                expected: &[TABLE_ID],
135            });
136        }
137
138        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
139        let total =
140            super::check_section_length(bytes.len(), HEADER_LEN, section_length, MIN_SECTION_LEN)?;
141
142        let private_indicator = (bytes[1] & 0x40) != 0;
143        let service_id = u16::from_be_bytes([bytes[3], bytes[4]]);
144        let version_number = (bytes[5] >> 1) & 0x1F;
145        let current_next_indicator = (bytes[5] & 0x01) != 0;
146        let section_number = bytes[6];
147        let last_section_number = bytes[7];
148        let transport_stream_id = u16::from_be_bytes([bytes[8], bytes[9]]);
149        let original_network_id = u16::from_be_bytes([bytes[10], bytes[11]]);
150        let prepend_strings_length = bytes[12];
151
152        let ps_start = HEADER_LEN + EXTENSION_LEN;
153        let ps_end = ps_start + prepend_strings_length as usize;
154        let payload_end = total - CRC_LEN;
155        if ps_end > payload_end {
156            return Err(Error::SectionLengthOverflow {
157                declared: prepend_strings_length as usize,
158                available: payload_end.saturating_sub(ps_start),
159            });
160        }
161        let prepend_strings = DvbText::new(&bytes[ps_start..ps_end]);
162
163        let mut pos = ps_end;
164        let mut crid_entries = Vec::new();
165        while pos < payload_end {
166            if pos + CRID_ENTRY_FIXED_LEN > payload_end {
167                return Err(Error::BufferTooShort {
168                    need: pos + CRID_ENTRY_FIXED_LEN,
169                    have: payload_end,
170                    what: "CitSection crid_entry",
171                });
172            }
173            let crid_ref = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]);
174            let prepend_string_index = bytes[pos + 2];
175            let unique_string_length = bytes[pos + 3] as usize;
176            pos += CRID_ENTRY_FIXED_LEN;
177            if pos + unique_string_length > payload_end {
178                return Err(Error::BufferTooShort {
179                    need: pos + unique_string_length,
180                    have: payload_end,
181                    what: "CitSection unique_string",
182                });
183            }
184            let unique_string = DvbText::new(&bytes[pos..pos + unique_string_length]);
185            pos += unique_string_length;
186            crid_entries.push(CridEntry {
187                crid_ref,
188                prepend_string_index,
189                unique_string,
190            });
191        }
192
193        Ok(CitSection {
194            private_indicator,
195            service_id,
196            version_number,
197            current_next_indicator,
198            section_number,
199            last_section_number,
200            transport_stream_id,
201            original_network_id,
202            prepend_strings,
203            crid_entries,
204        })
205    }
206}
207
208impl Serialize for CitSection<'_> {
209    type Error = crate::error::Error;
210
211    fn serialized_len(&self) -> usize {
212        HEADER_LEN
213            + EXTENSION_LEN
214            + self.prepend_strings.len()
215            + self
216                .crid_entries
217                .iter()
218                .map(crid_entry_serialized_len)
219                .sum::<usize>()
220            + CRC_LEN
221    }
222
223    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
224        let len = self.serialized_len();
225        if buf.len() < len {
226            return Err(Error::OutputBufferTooSmall {
227                need: len,
228                have: buf.len(),
229            });
230        }
231        if self.prepend_strings.len() > u8::MAX as usize {
232            return Err(Error::SectionLengthOverflow {
233                declared: self.prepend_strings.len(),
234                available: u8::MAX as usize,
235            });
236        }
237
238        let section_length = (len - HEADER_LEN) as u16;
239        if section_length > 0x0FFF {
240            return Err(Error::SectionLengthOverflow {
241                declared: section_length as usize,
242                available: 0x0FFF,
243            });
244        }
245        buf[0] = TABLE_ID;
246        buf[1] = super::SECTION_B1_SSI
247            | (u8::from(self.private_indicator) << 6)
248            | super::SECTION_B1_RESERVED_HI
249            | ((section_length >> 8) as u8 & 0x0F);
250        buf[2] = (section_length & 0xFF) as u8;
251
252        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
253        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
254        buf[6] = self.section_number;
255        buf[7] = self.last_section_number;
256        buf[8..10].copy_from_slice(&self.transport_stream_id.to_be_bytes());
257        buf[10..12].copy_from_slice(&self.original_network_id.to_be_bytes());
258        buf[12] = self.prepend_strings.len() as u8;
259
260        let ps_start = HEADER_LEN + EXTENSION_LEN;
261        let ps_end = ps_start + self.prepend_strings.len();
262        buf[ps_start..ps_end].copy_from_slice(&self.prepend_strings);
263
264        let mut pos = ps_end;
265        for entry in &self.crid_entries {
266            buf[pos..pos + 2].copy_from_slice(&entry.crid_ref.to_be_bytes());
267            buf[pos + 2] = entry.prepend_string_index;
268            buf[pos + 3] = entry.unique_string.len() as u8;
269            pos += CRID_ENTRY_FIXED_LEN;
270            buf[pos..pos + entry.unique_string.len()].copy_from_slice(&entry.unique_string);
271            pos += entry.unique_string.len();
272        }
273
274        let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
275        buf[pos..pos + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
276        Ok(len)
277    }
278}
279impl<'a> crate::traits::TableDef<'a> for CitSection<'a> {
280    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
281    const NAME: &'static str = "CONTENT_IDENTIFIER";
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn parse_happy_path_no_crid_entries() {
290        let prepend = DvbText::new(b"CRID://example.com\x00");
291        let cit = CitSection {
292            private_indicator: false,
293            service_id: 0x1234,
294            version_number: 3,
295            current_next_indicator: true,
296            section_number: 0,
297            last_section_number: 0,
298            transport_stream_id: 0x0064,
299            original_network_id: 0x0002,
300            prepend_strings: prepend,
301            crid_entries: Vec::new(),
302        };
303        let mut buf = vec![0u8; cit.serialized_len()];
304        cit.serialize_into(&mut buf).unwrap();
305        let parsed = CitSection::parse(&buf).unwrap();
306        assert_eq!(parsed.service_id, 0x1234);
307        assert_eq!(parsed.version_number, 3);
308        assert!(parsed.current_next_indicator);
309        assert_eq!(parsed.prepend_strings, prepend);
310        assert!(parsed.crid_entries.is_empty());
311    }
312
313    #[test]
314    fn parse_happy_path_with_crid_entries() {
315        let prepend = DvbText::new(b"crid://bbc.co.uk/\x00");
316        let entries = vec![
317            CridEntry {
318                crid_ref: 0x0001,
319                prepend_string_index: 0x00,
320                unique_string: DvbText::new(b"ep1"),
321            },
322            CridEntry {
323                crid_ref: 0x0002,
324                prepend_string_index: 0xFF,
325                unique_string: DvbText::new(b"crid://bbc.co.uk/EV-1"),
326            },
327        ];
328        let cit = CitSection {
329            private_indicator: false,
330            service_id: 0xABCD,
331            version_number: 7,
332            current_next_indicator: true,
333            section_number: 1,
334            last_section_number: 3,
335            transport_stream_id: 0x01F4,
336            original_network_id: 0x0028,
337            prepend_strings: prepend,
338            crid_entries: entries,
339        };
340        let mut buf = vec![0u8; cit.serialized_len()];
341        cit.serialize_into(&mut buf).unwrap();
342        let parsed = CitSection::parse(&buf).unwrap();
343        assert_eq!(parsed.service_id, 0xABCD);
344        assert_eq!(parsed.crid_entries.len(), 2);
345        assert_eq!(parsed.crid_entries[0].crid_ref, 0x0001);
346        assert_eq!(parsed.crid_entries[0].prepend_string_index, 0x00);
347        assert_eq!(parsed.crid_entries[0].unique_string, DvbText::new(b"ep1"));
348        assert_eq!(parsed.crid_entries[1].crid_ref, 0x0002);
349        assert_eq!(parsed.crid_entries[1].prepend_string_index, 0xFF);
350        assert_eq!(
351            parsed.crid_entries[1].unique_string,
352            DvbText::new(b"crid://bbc.co.uk/EV-1")
353        );
354    }
355
356    #[test]
357    fn byte_exact_round_trip() {
358        let prepend = DvbText::new(b"crid://example.com/\x00");
359        let entries = vec![CridEntry {
360            crid_ref: 0x0042,
361            prepend_string_index: 0x00,
362            unique_string: DvbText::new(b"episode42"),
363        }];
364        let original = CitSection {
365            private_indicator: true,
366            service_id: 0x4321,
367            version_number: 15,
368            current_next_indicator: false,
369            section_number: 2,
370            last_section_number: 4,
371            transport_stream_id: 0x03E8,
372            original_network_id: 0x0050,
373            prepend_strings: prepend,
374            crid_entries: entries,
375        };
376        let mut buf = vec![0u8; original.serialized_len()];
377        original.serialize_into(&mut buf).unwrap();
378        let parsed = CitSection::parse(&buf).unwrap();
379        let mut buf2 = vec![0u8; parsed.serialized_len()];
380        parsed.serialize_into(&mut buf2).unwrap();
381        assert_eq!(buf, buf2, "byte-exact re-serialize");
382        assert_eq!(parsed.crid_entries.len(), 1);
383        assert_eq!(parsed.crid_entries[0].crid_ref, 0x0042);
384        assert_eq!(
385            parsed.crid_entries[0].unique_string,
386            DvbText::new(b"episode42")
387        );
388    }
389
390    #[test]
391    fn parse_rejects_wrong_table_id() {
392        let cit = CitSection {
393            private_indicator: false,
394            service_id: 0x0001,
395            version_number: 0,
396            current_next_indicator: true,
397            section_number: 0,
398            last_section_number: 0,
399            transport_stream_id: 0x0001,
400            original_network_id: 0x0001,
401            prepend_strings: DvbText::new(&[]),
402            crid_entries: Vec::new(),
403        };
404        let mut buf = vec![0u8; cit.serialized_len()];
405        cit.serialize_into(&mut buf).unwrap();
406        buf[0] = 0x40;
407        assert!(matches!(
408            CitSection::parse(&buf).unwrap_err(),
409            Error::UnexpectedTableId { table_id: 0x40, .. }
410        ));
411    }
412
413    #[test]
414    fn parse_rejects_buffer_too_short() {
415        assert!(matches!(
416            CitSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
417            Error::BufferTooShort { .. }
418        ));
419    }
420
421    #[test]
422    fn parse_rejects_truncated_crid_entry() {
423        let prepend = DvbText::new(&[]);
424        let cit = CitSection {
425            private_indicator: false,
426            service_id: 0x0001,
427            version_number: 0,
428            current_next_indicator: true,
429            section_number: 0,
430            last_section_number: 0,
431            transport_stream_id: 0x0001,
432            original_network_id: 0x0001,
433            prepend_strings: prepend,
434            crid_entries: Vec::new(),
435        };
436        let mut buf = vec![0u8; cit.serialized_len()];
437        cit.serialize_into(&mut buf).unwrap();
438        let mut truncated = buf.clone();
439        truncated.truncate(buf.len() - 2);
440        let sl = (truncated.len() - HEADER_LEN) as u16;
441        truncated[1] = (truncated[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
442        truncated[2] = (sl & 0xFF) as u8;
443        assert!(CitSection::parse(&truncated).is_err());
444    }
445
446    #[test]
447    fn serialize_rejects_output_buffer_too_small() {
448        let cit = CitSection {
449            private_indicator: false,
450            service_id: 0x0001,
451            version_number: 0,
452            current_next_indicator: true,
453            section_number: 0,
454            last_section_number: 0,
455            transport_stream_id: 0x0001,
456            original_network_id: 0x0001,
457            prepend_strings: DvbText::new(&[]),
458            crid_entries: Vec::new(),
459        };
460        let mut buf = vec![0u8; 2];
461        assert!(matches!(
462            cit.serialize_into(&mut buf).unwrap_err(),
463            Error::OutputBufferTooSmall { .. }
464        ));
465    }
466
467    #[test]
468    fn parse_rejects_zero_section_length() {
469        let mut buf = vec![0u8; 64];
470        buf[0] = TABLE_ID;
471        buf[1] = 0xF0;
472        buf[2] = 0x00;
473        for b in &mut buf[3..] {
474            *b = 0xFF;
475        }
476        assert!(matches!(
477            CitSection::parse(&buf).unwrap_err(),
478            Error::SectionLengthOverflow { .. }
479        ));
480    }
481
482    #[test]
483    fn parse_handwritten_cit_no_entries() {
484        let mut bytes: Vec<u8> = vec![
485            0x77, 0xF0, 0x0E, 0x12, 0x34, 0xC7, 0x00, 0x00, 0x00, 0x64, 0x00, 0x02, 0x00,
486        ];
487        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
488        bytes.extend_from_slice(&crc.to_be_bytes());
489        let cit = CitSection::parse(&bytes).unwrap();
490        assert_eq!(cit.service_id, 0x1234);
491        assert_eq!(cit.transport_stream_id, 0x0064);
492        assert!(cit.crid_entries.is_empty());
493    }
494
495    #[test]
496    fn prepend_string_resolver() {
497        let cit = CitSection {
498            private_indicator: false,
499            service_id: 0x0001,
500            version_number: 0,
501            current_next_indicator: true,
502            section_number: 0,
503            last_section_number: 0,
504            transport_stream_id: 0x0001,
505            original_network_id: 0x0001,
506            prepend_strings: DvbText::new(b"crid://example.com/\x00crid://other.com/\x00"),
507            crid_entries: Vec::new(),
508        };
509        assert_eq!(cit.prepend_string(0), Some(&b"crid://example.com/"[..]));
510        assert_eq!(cit.prepend_string(1), Some(&b"crid://other.com/"[..]));
511        assert_eq!(cit.prepend_string(2), None);
512    }
513}