1use super::descriptor_body;
9use crate::error::{Error, Result};
10use alloc::vec::Vec;
11use dvb_common::{Parse, Serialize};
12
13pub const TAG: u8 = 0x76;
15const HEADER_LEN: usize = 2;
16const CRID_TYPE_MASK: u8 = 0xFC;
17const CRID_LOCATION_MASK: u8 = 0x03;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22#[non_exhaustive]
23pub enum CridType {
24 NoTypeDefined,
26 ItemOfContent,
29 Series,
31 Recommendation,
33 Reserved(u8),
35}
36
37impl CridType {
38 #[must_use]
39 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85#[non_exhaustive]
86pub enum CridLocation<'a> {
87 Inline(&'a [u8]),
89 Reference(u16),
91}
92
93#[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 pub crid_type: CridType,
100 pub location: CridLocation<'a>,
102}
103
104#[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 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 (ref_bytes, _) = body
165 .get(pos..)
166 .and_then(|s| s.split_first_chunk::<2>())
167 .ok_or(Error::InvalidDescriptor {
168 tag: TAG,
169 reason: "CRID reference truncated",
170 })?;
171 let crid_ref = u16::from_be_bytes(*ref_bytes);
172 pos += 2;
173 CridLocation::Reference(crid_ref)
174 }
175 _ => {
176 return Err(Error::InvalidDescriptor {
177 tag: TAG,
178 reason: "reserved crid_location value",
179 });
180 }
181 };
182 entries.push(CridEntry {
183 crid_type,
184 location,
185 });
186 }
187 Ok(Self { entries })
188 }
189}
190
191impl Serialize for ContentIdentifierDescriptor<'_> {
192 type Error = crate::error::Error;
193 fn serialized_len(&self) -> usize {
194 let body_len: usize = self
195 .entries
196 .iter()
197 .map(|e| match &e.location {
198 CridLocation::Inline(data) => 2 + data.len(),
199 CridLocation::Reference(_) => 3,
200 })
201 .sum();
202 HEADER_LEN + body_len
203 }
204
205 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
206 let len = self.serialized_len();
207 if buf.len() < len {
208 return Err(Error::OutputBufferTooSmall {
209 need: len,
210 have: buf.len(),
211 });
212 }
213 buf[0] = TAG;
214 buf[1] = (len - HEADER_LEN) as u8;
215 let mut pos = HEADER_LEN;
216 for entry in &self.entries {
217 let header = (entry.crid_type.to_u8() << 2) & CRID_TYPE_MASK;
218 match &entry.location {
219 CridLocation::Inline(data) => {
220 buf[pos] = header;
221 buf[pos + 1] = data.len() as u8;
222 buf[pos + 2..pos + 2 + data.len()].copy_from_slice(data);
223 pos += 2 + data.len();
224 }
225 CridLocation::Reference(val) => {
226 buf[pos] = header | 0x01;
227 let bytes = val.to_be_bytes();
228 buf[pos + 1] = bytes[0];
229 buf[pos + 2] = bytes[1];
230 pos += 3;
231 }
232 }
233 }
234 Ok(len)
235 }
236}
237impl<'a> crate::traits::DescriptorDef<'a> for ContentIdentifierDescriptor<'a> {
238 const TAG: u8 = TAG;
239 const NAME: &'static str = "CONTENT_IDENTIFIER";
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn parse_single_inline_crid() {
248 let data = b"DVB/CRID/EPG123";
249 let mut buf = vec![TAG, (data.len() + 2) as u8, 0x01 << 2, data.len() as u8];
250 buf.extend_from_slice(data);
251 let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
252 assert_eq!(d.entries.len(), 1);
253 assert_eq!(d.entries[0].crid_type, CridType::ItemOfContent);
254 match &d.entries[0].location {
255 CridLocation::Inline(bytes) => assert_eq!(*bytes, data.as_slice()),
256 _ => panic!("expected Inline"),
257 }
258 }
259
260 #[test]
261 fn parse_single_reference_crid() {
262 let buf = [TAG, 0x03, (0x02 << 2) | 0x01, 0x00, 0x42];
263 let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
264 assert_eq!(d.entries.len(), 1);
265 assert_eq!(d.entries[0].crid_type, CridType::Series);
266 match d.entries[0].location {
267 CridLocation::Reference(val) => assert_eq!(val, 0x0042),
268 _ => panic!("expected Reference"),
269 }
270 }
271
272 #[test]
273 fn parse_multiple_entries() {
274 let inline_data = b"EPG/EPG123";
275 let ref_val: u16 = 0x0100;
276 let mut buf = vec![TAG, 0x00, 0x01 << 2, inline_data.len() as u8];
277 buf.extend_from_slice(inline_data);
278 buf.push((0x03 << 2) | 0x01);
279 buf.extend_from_slice(&ref_val.to_be_bytes());
280 let body_len = buf.len() - HEADER_LEN;
281 buf[1] = body_len as u8;
282
283 let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
284 assert_eq!(d.entries.len(), 2);
285 assert_eq!(d.entries[0].crid_type, CridType::ItemOfContent);
286 match &d.entries[0].location {
287 CridLocation::Inline(bytes) => assert_eq!(*bytes, inline_data.as_slice()),
288 _ => panic!("expected Inline for first entry"),
289 }
290 assert_eq!(d.entries[1].crid_type, CridType::Recommendation);
291 match d.entries[1].location {
292 CridLocation::Reference(val) => assert_eq!(val, ref_val),
293 _ => panic!("expected Reference for second entry"),
294 }
295 }
296
297 #[test]
298 fn parse_rejects_wrong_tag() {
299 let buf = [0x7A, 0x03, 0x04, 0x00, 0x42];
300 assert!(matches!(
301 ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
302 Error::InvalidDescriptor { tag: 0x7A, .. }
303 ));
304 }
305
306 #[test]
307 fn parse_rejects_inline_length_overrun() {
308 let buf = [TAG, 4, 0x01 << 2, 10, 0xAA, 0xBB];
309 assert!(matches!(
310 ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
311 Error::InvalidDescriptor { tag: TAG, .. }
312 ));
313 }
314
315 #[test]
316 fn parse_rejects_reference_truncated() {
317 let buf = [TAG, 2, (0x02 << 2) | 0x01, 0xAA];
318 assert!(matches!(
319 ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
320 Error::InvalidDescriptor { tag: TAG, .. }
321 ));
322 }
323
324 #[test]
325 fn parse_rejects_reserved_location() {
326 let buf = [TAG, 0x01, (0x01 << 2) | 0x02];
327 assert!(matches!(
328 ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
329 Error::InvalidDescriptor { tag: TAG, .. }
330 ));
331 let buf = [TAG, 0x01, (0x01 << 2) | 0x03];
332 assert!(matches!(
333 ContentIdentifierDescriptor::parse(&buf).unwrap_err(),
334 Error::InvalidDescriptor { tag: TAG, .. }
335 ));
336 }
337
338 #[test]
339 fn empty_descriptor_valid() {
340 let buf = [TAG, 0x00];
341 let d = ContentIdentifierDescriptor::parse(&buf).unwrap();
342 assert_eq!(d.entries.len(), 0);
343 }
344
345 #[test]
346 fn serialize_round_trip_inline_and_reference() {
347 let inline_data = b"DVB/CRID/TEST456";
348 let ref_val: u16 = 789;
349 let desc = ContentIdentifierDescriptor {
350 entries: vec![
351 CridEntry {
352 crid_type: CridType::ItemOfContent,
353 location: CridLocation::Inline(inline_data.as_slice()),
354 },
355 CridEntry {
356 crid_type: CridType::Recommendation,
357 location: CridLocation::Reference(ref_val),
358 },
359 ],
360 };
361 let mut buf = vec![0u8; desc.serialized_len()];
362 desc.serialize_into(&mut buf).unwrap();
363 let parsed = ContentIdentifierDescriptor::parse(&buf).unwrap();
364 assert_eq!(parsed.entries.len(), desc.entries.len());
365 match &parsed.entries[0].location {
366 CridLocation::Inline(bytes) => assert_eq!(*bytes, inline_data.as_slice()),
367 _ => panic!("expected Inline"),
368 }
369 assert_eq!(parsed.entries[0].crid_type, CridType::ItemOfContent);
370 match parsed.entries[1].location {
371 CridLocation::Reference(val) => assert_eq!(val, ref_val),
372 _ => panic!("expected Reference"),
373 }
374 assert_eq!(parsed.entries[1].crid_type, CridType::Recommendation);
375 }
376
377 #[test]
378 fn crid_type_full_range_round_trip() {
379 for b in 0..=0xFF_u8 {
380 let ct = CridType::from_u8(b);
381 assert_eq!(ct.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
382 }
383 }
384
385 #[test]
386 fn crid_type_name_for_known() {
387 assert_eq!(CridType::NoTypeDefined.name(), "no type defined");
388 assert_eq!(CridType::ItemOfContent.name(), "item of content");
389 assert_eq!(CridType::Series.name(), "series");
390 assert_eq!(CridType::Recommendation.name(), "recommendation");
391 assert_eq!(CridType::Reserved(0x55).name(), "reserved");
392 }
393}