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