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}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize))]
83#[non_exhaustive]
84pub enum CridLocation<'a> {
85 Inline(&'a [u8]),
87 Reference(u16),
89}
90
91#[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 pub crid_type: CridType,
98 pub location: CridLocation<'a>,
100}
101
102#[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 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}