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