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..].first_chunk::<2>().unwrap());
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..].first_chunk::<2>().unwrap());
149        let original_network_id = u16::from_be_bytes(*bytes[10..].first_chunk::<2>().unwrap());
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 (b2, _) = bytes[pos..]
174                .split_first_chunk::<2>()
175                .ok_or(Error::BufferTooShort {
176                    need: pos + CRID_ENTRY_FIXED_LEN,
177                    have: payload_end,
178                    what: "CitSection crid_entry",
179                })?;
180            let crid_ref = u16::from_be_bytes(*b2);
181            let prepend_string_index = bytes[pos + 2];
182            let unique_string_length = bytes[pos + 3] as usize;
183            pos += CRID_ENTRY_FIXED_LEN;
184            if pos + unique_string_length > payload_end {
185                return Err(Error::BufferTooShort {
186                    need: pos + unique_string_length,
187                    have: payload_end,
188                    what: "CitSection unique_string",
189                });
190            }
191            let unique_string = DvbText::new(&bytes[pos..pos + unique_string_length]);
192            pos += unique_string_length;
193            crid_entries.push(CridEntry {
194                crid_ref,
195                prepend_string_index,
196                unique_string,
197            });
198        }
199
200        Ok(CitSection {
201            private_indicator,
202            service_id,
203            version_number,
204            current_next_indicator,
205            section_number,
206            last_section_number,
207            transport_stream_id,
208            original_network_id,
209            prepend_strings,
210            crid_entries,
211        })
212    }
213}
214
215impl Serialize for CitSection<'_> {
216    type Error = crate::error::Error;
217
218    fn serialized_len(&self) -> usize {
219        HEADER_LEN
220            + EXTENSION_LEN
221            + self.prepend_strings.len()
222            + self
223                .crid_entries
224                .iter()
225                .map(crid_entry_serialized_len)
226                .sum::<usize>()
227            + CRC_LEN
228    }
229
230    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
231        let len = self.serialized_len();
232        if buf.len() < len {
233            return Err(Error::OutputBufferTooSmall {
234                need: len,
235                have: buf.len(),
236            });
237        }
238        if self.prepend_strings.len() > u8::MAX as usize {
239            return Err(Error::SectionLengthOverflow {
240                declared: self.prepend_strings.len(),
241                available: u8::MAX as usize,
242            });
243        }
244
245        let section_length = (len - HEADER_LEN) as u16;
246        if section_length > 0x0FFF {
247            return Err(Error::SectionLengthOverflow {
248                declared: section_length as usize,
249                available: 0x0FFF,
250            });
251        }
252        buf[0] = TABLE_ID;
253        buf[1] = super::SECTION_B1_SSI
254            | (u8::from(self.private_indicator) << 6)
255            | super::SECTION_B1_RESERVED_HI
256            | ((section_length >> 8) as u8 & 0x0F);
257        buf[2] = (section_length & 0xFF) as u8;
258
259        buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
260        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
261        buf[6] = self.section_number;
262        buf[7] = self.last_section_number;
263        buf[8..10].copy_from_slice(&self.transport_stream_id.to_be_bytes());
264        buf[10..12].copy_from_slice(&self.original_network_id.to_be_bytes());
265        buf[12] = self.prepend_strings.len() as u8;
266
267        let ps_start = HEADER_LEN + EXTENSION_LEN;
268        let ps_end = ps_start + self.prepend_strings.len();
269        buf[ps_start..ps_end].copy_from_slice(&self.prepend_strings);
270
271        let mut pos = ps_end;
272        for entry in &self.crid_entries {
273            buf[pos..pos + 2].copy_from_slice(&entry.crid_ref.to_be_bytes());
274            buf[pos + 2] = entry.prepend_string_index;
275            buf[pos + 3] = entry.unique_string.len() as u8;
276            pos += CRID_ENTRY_FIXED_LEN;
277            buf[pos..pos + entry.unique_string.len()].copy_from_slice(&entry.unique_string);
278            pos += entry.unique_string.len();
279        }
280
281        let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
282        buf[pos..pos + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
283        Ok(len)
284    }
285}
286impl<'a> crate::traits::TableDef<'a> for CitSection<'a> {
287    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
288    const NAME: &'static str = "CONTENT_IDENTIFIER";
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn parse_happy_path_no_crid_entries() {
297        let prepend = DvbText::new(b"CRID://example.com\x00");
298        let cit = CitSection {
299            private_indicator: false,
300            service_id: 0x1234,
301            version_number: 3,
302            current_next_indicator: true,
303            section_number: 0,
304            last_section_number: 0,
305            transport_stream_id: 0x0064,
306            original_network_id: 0x0002,
307            prepend_strings: prepend,
308            crid_entries: Vec::new(),
309        };
310        let mut buf = vec![0u8; cit.serialized_len()];
311        cit.serialize_into(&mut buf).unwrap();
312        let parsed = CitSection::parse(&buf).unwrap();
313        assert_eq!(parsed.service_id, 0x1234);
314        assert_eq!(parsed.version_number, 3);
315        assert!(parsed.current_next_indicator);
316        assert_eq!(parsed.prepend_strings, prepend);
317        assert!(parsed.crid_entries.is_empty());
318    }
319
320    #[test]
321    fn parse_happy_path_with_crid_entries() {
322        let prepend = DvbText::new(b"crid://bbc.co.uk/\x00");
323        let entries = vec![
324            CridEntry {
325                crid_ref: 0x0001,
326                prepend_string_index: 0x00,
327                unique_string: DvbText::new(b"ep1"),
328            },
329            CridEntry {
330                crid_ref: 0x0002,
331                prepend_string_index: 0xFF,
332                unique_string: DvbText::new(b"crid://bbc.co.uk/EV-1"),
333            },
334        ];
335        let cit = CitSection {
336            private_indicator: false,
337            service_id: 0xABCD,
338            version_number: 7,
339            current_next_indicator: true,
340            section_number: 1,
341            last_section_number: 3,
342            transport_stream_id: 0x01F4,
343            original_network_id: 0x0028,
344            prepend_strings: prepend,
345            crid_entries: entries,
346        };
347        let mut buf = vec![0u8; cit.serialized_len()];
348        cit.serialize_into(&mut buf).unwrap();
349        let parsed = CitSection::parse(&buf).unwrap();
350        assert_eq!(parsed.service_id, 0xABCD);
351        assert_eq!(parsed.crid_entries.len(), 2);
352        assert_eq!(parsed.crid_entries[0].crid_ref, 0x0001);
353        assert_eq!(parsed.crid_entries[0].prepend_string_index, 0x00);
354        assert_eq!(parsed.crid_entries[0].unique_string, DvbText::new(b"ep1"));
355        assert_eq!(parsed.crid_entries[1].crid_ref, 0x0002);
356        assert_eq!(parsed.crid_entries[1].prepend_string_index, 0xFF);
357        assert_eq!(
358            parsed.crid_entries[1].unique_string,
359            DvbText::new(b"crid://bbc.co.uk/EV-1")
360        );
361    }
362
363    #[test]
364    fn byte_exact_round_trip() {
365        let prepend = DvbText::new(b"crid://example.com/\x00");
366        let entries = vec![CridEntry {
367            crid_ref: 0x0042,
368            prepend_string_index: 0x00,
369            unique_string: DvbText::new(b"episode42"),
370        }];
371        let original = CitSection {
372            private_indicator: true,
373            service_id: 0x4321,
374            version_number: 15,
375            current_next_indicator: false,
376            section_number: 2,
377            last_section_number: 4,
378            transport_stream_id: 0x03E8,
379            original_network_id: 0x0050,
380            prepend_strings: prepend,
381            crid_entries: entries,
382        };
383        let mut buf = vec![0u8; original.serialized_len()];
384        original.serialize_into(&mut buf).unwrap();
385        let parsed = CitSection::parse(&buf).unwrap();
386        let mut buf2 = vec![0u8; parsed.serialized_len()];
387        parsed.serialize_into(&mut buf2).unwrap();
388        assert_eq!(buf, buf2, "byte-exact re-serialize");
389        assert_eq!(parsed.crid_entries.len(), 1);
390        assert_eq!(parsed.crid_entries[0].crid_ref, 0x0042);
391        assert_eq!(
392            parsed.crid_entries[0].unique_string,
393            DvbText::new(b"episode42")
394        );
395    }
396
397    #[test]
398    fn parse_rejects_wrong_table_id() {
399        let cit = CitSection {
400            private_indicator: false,
401            service_id: 0x0001,
402            version_number: 0,
403            current_next_indicator: true,
404            section_number: 0,
405            last_section_number: 0,
406            transport_stream_id: 0x0001,
407            original_network_id: 0x0001,
408            prepend_strings: DvbText::new(&[]),
409            crid_entries: Vec::new(),
410        };
411        let mut buf = vec![0u8; cit.serialized_len()];
412        cit.serialize_into(&mut buf).unwrap();
413        buf[0] = 0x40;
414        assert!(matches!(
415            CitSection::parse(&buf).unwrap_err(),
416            Error::UnexpectedTableId { table_id: 0x40, .. }
417        ));
418    }
419
420    #[test]
421    fn parse_rejects_buffer_too_short() {
422        assert!(matches!(
423            CitSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
424            Error::BufferTooShort { .. }
425        ));
426    }
427
428    #[test]
429    fn parse_rejects_truncated_crid_entry() {
430        let prepend = DvbText::new(&[]);
431        let cit = CitSection {
432            private_indicator: false,
433            service_id: 0x0001,
434            version_number: 0,
435            current_next_indicator: true,
436            section_number: 0,
437            last_section_number: 0,
438            transport_stream_id: 0x0001,
439            original_network_id: 0x0001,
440            prepend_strings: prepend,
441            crid_entries: Vec::new(),
442        };
443        let mut buf = vec![0u8; cit.serialized_len()];
444        cit.serialize_into(&mut buf).unwrap();
445        let mut truncated = buf.clone();
446        truncated.truncate(buf.len() - 2);
447        let sl = (truncated.len() - HEADER_LEN) as u16;
448        truncated[1] = (truncated[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
449        truncated[2] = (sl & 0xFF) as u8;
450        assert!(CitSection::parse(&truncated).is_err());
451    }
452
453    #[test]
454    fn serialize_rejects_output_buffer_too_small() {
455        let cit = CitSection {
456            private_indicator: false,
457            service_id: 0x0001,
458            version_number: 0,
459            current_next_indicator: true,
460            section_number: 0,
461            last_section_number: 0,
462            transport_stream_id: 0x0001,
463            original_network_id: 0x0001,
464            prepend_strings: DvbText::new(&[]),
465            crid_entries: Vec::new(),
466        };
467        let mut buf = vec![0u8; 2];
468        assert!(matches!(
469            cit.serialize_into(&mut buf).unwrap_err(),
470            Error::OutputBufferTooSmall { .. }
471        ));
472    }
473
474    #[test]
475    fn parse_rejects_zero_section_length() {
476        let mut buf = vec![0u8; 64];
477        buf[0] = TABLE_ID;
478        buf[1] = 0xF0;
479        buf[2] = 0x00;
480        for b in &mut buf[3..] {
481            *b = 0xFF;
482        }
483        assert!(matches!(
484            CitSection::parse(&buf).unwrap_err(),
485            Error::SectionLengthOverflow { .. }
486        ));
487    }
488
489    #[test]
490    fn parse_handwritten_cit_no_entries() {
491        let mut bytes: Vec<u8> = vec![
492            0x77, 0xF0, 0x0E, 0x12, 0x34, 0xC7, 0x00, 0x00, 0x00, 0x64, 0x00, 0x02, 0x00,
493        ];
494        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
495        bytes.extend_from_slice(&crc.to_be_bytes());
496        let cit = CitSection::parse(&bytes).unwrap();
497        assert_eq!(cit.service_id, 0x1234);
498        assert_eq!(cit.transport_stream_id, 0x0064);
499        assert!(cit.crid_entries.is_empty());
500    }
501
502    #[test]
503    fn prepend_string_resolver() {
504        let cit = CitSection {
505            private_indicator: false,
506            service_id: 0x0001,
507            version_number: 0,
508            current_next_indicator: true,
509            section_number: 0,
510            last_section_number: 0,
511            transport_stream_id: 0x0001,
512            original_network_id: 0x0001,
513            prepend_strings: DvbText::new(b"crid://example.com/\x00crid://other.com/\x00"),
514            crid_entries: Vec::new(),
515        };
516        assert_eq!(cit.prepend_string(0), Some(&b"crid://example.com/"[..]));
517        assert_eq!(cit.prepend_string(1), Some(&b"crid://other.com/"[..]));
518        assert_eq!(cit.prepend_string(2), None);
519    }
520}