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