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