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