1use super::descriptor_body;
9use crate::error::{Error, Result};
10use dvb_common::{Parse, Serialize};
11
12pub const TAG: u8 = 0x76;
14const HEADER_LEN: usize = 2;
15const CRID_TYPE_MASK: u8 = 0xFC;
16const CRID_LOCATION_MASK: u8 = 0x03;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21#[non_exhaustive]
22pub enum CridType {
23 NoTypeDefined,
25 ItemOfContent,
28 Series,
30 Recommendation,
32 Reserved(u8),
34}
35
36impl CridType {
37 #[must_use]
38 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize))]
84#[non_exhaustive]
85pub enum CridLocation<'a> {
86 Inline(&'a [u8]),
88 Reference(u16),
90}
91
92#[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 pub crid_type: CridType,
99 pub location: CridLocation<'a>,
101}
102
103#[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 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}