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