Skip to main content

dvb_si/descriptors/
content_identifier.rs

1//! Content Identifier Descriptor — ETSI TS 102 323 §12.1 (tag 0x76).
2//!
3//! Carries one or more Content Reference Identifier (CRID) entries that
4//! uniquely identify TV/radio programmes, series, or recommendations.
5//! Each entry specifies a type and a location indicator that determines
6//! whether the CRID is carried inline or as a reference.
7
8use crate::error::{Error, Result};
9use crate::traits::Descriptor;
10use dvb_common::{Parse, Serialize};
11
12/// Descriptor tag for content_identifier_descriptor.
13pub const TAG: u8 = 0x76;
14const HEADER_LEN: usize = 2;
15const CRID_TYPE_MASK: u8 = 0xFC;
16const CRID_LOCATION_MASK: u8 = 0x03;
17
18/// CRID location per TS 102 323 Table 10.
19///
20/// Only the two defined locations are representable. Locations `0b10`/`0b11`
21/// are reserved with no defined payload length, so the parser rejects them
22/// (it cannot know how many bytes the entry occupies) rather than producing
23/// an un-round-trippable value.
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26pub enum CridLocation<'a> {
27    /// Location 0b00 — CRID carried inline as raw ASCII bytes.
28    Inline(&'a [u8]),
29    /// Location 0b01 — CRID reference (CIT index).
30    Reference(u16),
31}
32
33/// One CRID entry within a Content Identifier Descriptor.
34#[derive(Debug, Clone, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
37pub struct CridEntry<'a> {
38    /// crid_type byte — identifies what the entry references.
39    /// Per TS 102 323 Table 8: 0x01 = episode, 0x02 = series,
40    /// 0x03 = recommendation, 0x31..=0x3F = user-defined.
41    pub crid_type: u8,
42    /// crid_location and its payload.
43    pub location: CridLocation<'a>,
44}
45
46/// Content Identifier Descriptor.
47///
48/// Holds a sequence of CRID entries that identify programme content
49/// for recording, scheduling, or recommendation purposes.
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize))]
52#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
53pub struct ContentIdentifierDescriptor<'a> {
54    /// Entries in wire order.
55    pub entries: Vec<CridEntry<'a>>,
56}
57
58impl<'a> Parse<'a> for ContentIdentifierDescriptor<'a> {
59    type Error = crate::error::Error;
60    fn parse(bytes: &'a [u8]) -> Result<Self> {
61        if bytes.len() < HEADER_LEN {
62            return Err(Error::BufferTooShort {
63                need: HEADER_LEN,
64                have: bytes.len(),
65                what: "ContentIdentifierDescriptor header",
66            });
67        }
68        if bytes[0] != TAG {
69            return Err(Error::InvalidDescriptor {
70                tag: bytes[0],
71                reason: "unexpected tag for ContentIdentifierDescriptor",
72            });
73        }
74        let length = bytes[1] as usize;
75        let end = HEADER_LEN + length;
76        if bytes.len() < end {
77            return Err(Error::BufferTooShort {
78                need: end,
79                have: bytes.len(),
80                what: "ContentIdentifierDescriptor body",
81            });
82        }
83        if length == 0 {
84            return Ok(Self {
85                entries: Vec::new(),
86            });
87        }
88        let body = &bytes[HEADER_LEN..end];
89        let mut entries = Vec::new();
90        let mut pos = 0;
91        while pos < body.len() {
92            let header_byte = body[pos];
93            pos += 1;
94            let crid_type = (header_byte & CRID_TYPE_MASK) >> 2;
95            let crid_location = header_byte & CRID_LOCATION_MASK;
96            let location = match crid_location {
97                0x00 => {
98                    if pos >= body.len() {
99                        return Err(Error::InvalidDescriptor {
100                            tag: TAG,
101                            reason: "inline CRID length byte missing",
102                        });
103                    }
104                    let crid_length = body[pos] as usize;
105                    pos += 1;
106                    if pos + crid_length > body.len() {
107                        return Err(Error::InvalidDescriptor {
108                            tag: TAG,
109                            reason: "inline CRID length exceeds descriptor body",
110                        });
111                    }
112                    let crid_bytes = &body[pos..pos + crid_length];
113                    pos += crid_length;
114                    CridLocation::Inline(crid_bytes)
115                }
116                0x01 => {
117                    if pos + 2 > body.len() {
118                        return Err(Error::InvalidDescriptor {
119                            tag: TAG,
120                            reason: "CRID reference truncated",
121                        });
122                    }
123                    // big-endian u16 per spec
124                    let crid_ref = u16::from_be_bytes([body[pos], body[pos + 1]]);
125                    pos += 2;
126                    CridLocation::Reference(crid_ref)
127                }
128                _ => {
129                    return Err(Error::InvalidDescriptor {
130                        tag: TAG,
131                        reason: "reserved crid_location value",
132                    });
133                }
134            };
135            entries.push(CridEntry {
136                crid_type,
137                location,
138            });
139        }
140        Ok(Self { entries })
141    }
142}
143
144impl Serialize for ContentIdentifierDescriptor<'_> {
145    type Error = crate::error::Error;
146    fn serialized_len(&self) -> usize {
147        let body_len: usize = self
148            .entries
149            .iter()
150            .map(|e| match &e.location {
151                CridLocation::Inline(data) => 2 + data.len(),
152                CridLocation::Reference(_) => 3,
153            })
154            .sum();
155        HEADER_LEN + body_len
156    }
157
158    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
159        let len = self.serialized_len();
160        if buf.len() < len {
161            return Err(Error::OutputBufferTooSmall {
162                need: len,
163                have: buf.len(),
164            });
165        }
166        buf[0] = TAG;
167        buf[1] = (len - HEADER_LEN) as u8;
168        let mut pos = HEADER_LEN;
169        for entry in &self.entries {
170            let header = (entry.crid_type << 2) & CRID_TYPE_MASK;
171            match &entry.location {
172                CridLocation::Inline(data) => {
173                    buf[pos] = header;
174                    buf[pos + 1] = data.len() as u8;
175                    buf[pos + 2..pos + 2 + data.len()].copy_from_slice(data);
176                    pos += 2 + data.len();
177                }
178                CridLocation::Reference(val) => {
179                    buf[pos] = header | 0x01;
180                    let bytes = val.to_be_bytes();
181                    buf[pos + 1] = bytes[0];
182                    buf[pos + 2] = bytes[1];
183                    pos += 3;
184                }
185            }
186        }
187        // fill any remaining buffer that might be passed in
188        Ok(len)
189    }
190}
191
192impl<'a> Descriptor<'a> for ContentIdentifierDescriptor<'a> {
193    const TAG: u8 = TAG;
194    fn descriptor_length(&self) -> u8 {
195        (self.serialized_len() - HEADER_LEN) as u8
196    }
197}
198
199impl<'a> crate::traits::DescriptorDef<'a> for ContentIdentifierDescriptor<'a> {
200    const TAG: u8 = TAG;
201    const NAME: &'static str = "CONTENT_IDENTIFIER";
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn parse_single_inline_crid() {
210        let data = b"DVB/CRID/EPG123";
211        let mut buf = vec![TAG, (data.len() + 2) as u8, 0x01 << 2, data.len() as u8];
212        buf.extend_from_slice(data);
213        let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
214        assert_eq!(d.entries.len(), 1);
215        assert_eq!(d.entries[0].crid_type, 0x01);
216        match &d.entries[0].location {
217            CridLocation::Inline(bytes) => assert_eq!(*bytes, data.as_slice()),
218            _ => panic!("expected Inline"),
219        }
220    }
221
222    #[test]
223    fn parse_single_reference_crid() {
224        let buf = [TAG, 0x03, (0x02 << 2) | 0x01, 0x00, 0x42];
225        let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
226        assert_eq!(d.entries.len(), 1);
227        assert_eq!(d.entries[0].crid_type, 0x02);
228        match d.entries[0].location {
229            CridLocation::Reference(val) => assert_eq!(val, 0x0042),
230            _ => panic!("expected Reference"),
231        }
232    }
233
234    #[test]
235    fn parse_multiple_entries() {
236        let inline_data = b"EPG/EPG123";
237        let ref_val: u16 = 0x0100;
238        // First entry: inline CRID (type=0x01, location=0x00)
239        let mut buf = vec![
240            TAG,
241            0x00, // placeholder for length
242            0x01 << 2,
243            inline_data.len() as u8,
244        ];
245        buf.extend_from_slice(inline_data);
246        // Second entry: reference CRID (type=0x03, location=0x01)
247        buf.push((0x03 << 2) | 0x01);
248        buf.extend_from_slice(&ref_val.to_be_bytes());
249        // Patch descriptor_length
250        let body_len = buf.len() - HEADER_LEN;
251        buf[1] = body_len as u8;
252
253        let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
254        assert_eq!(d.entries.len(), 2);
255        // Verify first entry
256        assert_eq!(d.entries[0].crid_type, 0x01);
257        match &d.entries[0].location {
258            CridLocation::Inline(bytes) => assert_eq!(*bytes, inline_data.as_slice()),
259            _ => panic!("expected Inline for first entry"),
260        }
261        // Verify second entry
262        assert_eq!(d.entries[1].crid_type, 0x03);
263        match d.entries[1].location {
264            CridLocation::Reference(val) => assert_eq!(val, ref_val),
265            _ => panic!("expected Reference for second entry"),
266        }
267    }
268
269    #[test]
270    fn parse_rejects_wrong_tag() {
271        let buf = [0x7A, 0x03, 0x04, 0x00, 0x42];
272        assert!(matches!(
273            ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
274            Error::InvalidDescriptor { tag: 0x7A, .. }
275        ));
276    }
277
278    #[test]
279    fn parse_rejects_inline_length_overrun() {
280        // Full descriptor: length=4 body = [header, crid_len=10, 0xAA, 0xBB].
281        // Inline crid_length=10 exceeds the 2 remaining body bytes.
282        let buf = [TAG, 4, 0x01 << 2, 10, 0xAA, 0xBB];
283        assert!(matches!(
284            ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
285            Error::InvalidDescriptor { tag: TAG, .. }
286        ));
287    }
288
289    #[test]
290    fn parse_rejects_reference_truncated() {
291        // Full descriptor: length=2 body = [header, 0xAA]. Reference location
292        // needs 2 bytes but only 1 remains.
293        let buf = [TAG, 2, (0x02 << 2) | 0x01, 0xAA];
294        assert!(matches!(
295            ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
296            Error::InvalidDescriptor { tag: TAG, .. }
297        ));
298    }
299
300    #[test]
301    fn parse_rejects_reserved_location() {
302        // location=0x02 is reserved
303        let buf = [TAG, 0x01, (0x01 << 2) | 0x02];
304        assert!(matches!(
305            ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
306            Error::InvalidDescriptor { tag: TAG, .. }
307        ));
308        // location=0x03 is also reserved
309        let buf = [TAG, 0x01, (0x01 << 2) | 0x03];
310        assert!(matches!(
311            ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
312            Error::InvalidDescriptor { tag: TAG, .. }
313        ));
314    }
315
316    #[test]
317    fn empty_descriptor_valid() {
318        let buf = [TAG, 0x00];
319        let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
320        assert_eq!(d.entries.len(), 0);
321    }
322
323    #[test]
324    fn serialize_round_trip_inline_and_reference() {
325        let inline_data = b"DVB/CRID/TEST456";
326        let ref_val: u16 = 789;
327        let desc = ContentIdentifierDescriptor {
328            entries: vec![
329                CridEntry {
330                    crid_type: 0x01,
331                    location: CridLocation::Inline(inline_data.as_slice()),
332                },
333                CridEntry {
334                    crid_type: 0x03,
335                    location: CridLocation::Reference(ref_val),
336                },
337            ],
338        };
339        let mut buf = vec![0u8; desc.serialized_len()];
340        desc.serialize_into(&mut buf).unwrap();
341        let parsed = ContentIdentifierDescriptor::parse(&buf).unwrap();
342        assert_eq!(parsed.entries.len(), desc.entries.len());
343        // Check inline entry
344        match &parsed.entries[0].location {
345            CridLocation::Inline(bytes) => assert_eq!(*bytes, inline_data.as_slice()),
346            _ => panic!("expected Inline"),
347        }
348        assert_eq!(parsed.entries[0].crid_type, 0x01);
349        // Check reference entry
350        match parsed.entries[1].location {
351            CridLocation::Reference(val) => assert_eq!(val, ref_val),
352            _ => panic!("expected Reference"),
353        }
354        assert_eq!(parsed.entries[1].crid_type, 0x03);
355    }
356}