1use crate::error::{Error, Result};
13use crate::text::DvbText;
14use alloc::vec::Vec;
15use dvb_common::{Parse, Serialize};
16
17pub const TABLE_ID: u8 = 0x77;
19
20pub const PID: u16 = 0x0012;
25
26const HEADER_LEN: usize = 3;
27const EXTENSION_LEN: usize = 10;
28const CRC_LEN: usize = 4;
29const MIN_SECTION_LEN: usize = HEADER_LEN + EXTENSION_LEN + CRC_LEN;
30
31const CRID_REF_LEN: usize = 2;
32const CRID_ENTRY_FIXED_LEN: usize = CRID_REF_LEN + 1 + 1;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40pub struct CridEntry<'a> {
41 pub crid_ref: u16,
43 pub prepend_string_index: u8,
46 pub unique_string: DvbText<'a>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize))]
57#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
58pub struct CitSection<'a> {
59 pub private_indicator: bool,
61 pub service_id: u16,
64 pub version_number: u8,
66 pub current_next_indicator: bool,
68 pub section_number: u8,
70 pub last_section_number: u8,
72 pub transport_stream_id: u16,
74 pub original_network_id: u16,
76 pub prepend_strings: DvbText<'a>,
79 pub crid_entries: Vec<CridEntry<'a>>,
81}
82
83impl<'a> CitSection<'a> {
84 pub fn prepend_string(&self, index: u8) -> Option<&[u8]> {
92 let raw: &[u8] = &self.prepend_strings;
93 let mut remaining: &[u8] = raw;
94 let mut current: u8 = 0;
95 while !remaining.is_empty() {
96 let nul_pos = remaining
97 .iter()
98 .position(|&b| b == 0)
99 .unwrap_or(remaining.len());
100 let fragment = &remaining[..nul_pos];
101 if current == index {
102 return Some(fragment);
103 }
104 current += 1;
105 remaining = if nul_pos < remaining.len() {
106 &remaining[nul_pos + 1..]
107 } else {
108 &[]
109 };
110 }
111 None
112 }
113}
114
115fn crid_entry_serialized_len(e: &CridEntry) -> usize {
116 CRID_ENTRY_FIXED_LEN + e.unique_string.len()
117}
118
119impl<'a> Parse<'a> for CitSection<'a> {
120 type Error = crate::error::Error;
121
122 fn parse(bytes: &'a [u8]) -> Result<Self> {
123 if bytes.len() < MIN_SECTION_LEN {
124 return Err(Error::BufferTooShort {
125 need: MIN_SECTION_LEN,
126 have: bytes.len(),
127 what: "CitSection",
128 });
129 }
130 if bytes[0] != TABLE_ID {
131 return Err(Error::UnexpectedTableId {
132 table_id: bytes[0],
133 what: "CitSection",
134 expected: &[TABLE_ID],
135 });
136 }
137
138 let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
139 let total =
140 super::check_section_length(bytes.len(), HEADER_LEN, section_length, MIN_SECTION_LEN)?;
141
142 let private_indicator = (bytes[1] & 0x40) != 0;
143 let service_id = u16::from_be_bytes(*bytes[3..].first_chunk::<2>().unwrap());
144 let version_number = (bytes[5] >> 1) & 0x1F;
145 let current_next_indicator = (bytes[5] & 0x01) != 0;
146 let section_number = bytes[6];
147 let last_section_number = bytes[7];
148 let transport_stream_id = u16::from_be_bytes(*bytes[8..].first_chunk::<2>().unwrap());
149 let original_network_id = u16::from_be_bytes(*bytes[10..].first_chunk::<2>().unwrap());
150 let prepend_strings_length = bytes[12];
151
152 let ps_start = HEADER_LEN + EXTENSION_LEN;
153 let ps_end = ps_start + prepend_strings_length as usize;
154 let payload_end = total - CRC_LEN;
155 if ps_end > payload_end {
156 return Err(Error::SectionLengthOverflow {
157 declared: prepend_strings_length as usize,
158 available: payload_end.saturating_sub(ps_start),
159 });
160 }
161 let prepend_strings = DvbText::new(&bytes[ps_start..ps_end]);
162
163 let mut pos = ps_end;
164 let mut crid_entries = Vec::new();
165 while pos < payload_end {
166 if pos + CRID_ENTRY_FIXED_LEN > payload_end {
167 return Err(Error::BufferTooShort {
168 need: pos + CRID_ENTRY_FIXED_LEN,
169 have: payload_end,
170 what: "CitSection crid_entry",
171 });
172 }
173 let (b2, _) = bytes
174 .get(pos..)
175 .and_then(|s| s.split_first_chunk::<2>())
176 .ok_or(Error::BufferTooShort {
177 need: pos + CRID_ENTRY_FIXED_LEN,
178 have: payload_end,
179 what: "CitSection crid_entry",
180 })?;
181 let crid_ref = u16::from_be_bytes(*b2);
182 let prepend_string_index = bytes[pos + 2];
183 let unique_string_length = bytes[pos + 3] as usize;
184 pos += CRID_ENTRY_FIXED_LEN;
185 if pos + unique_string_length > payload_end {
186 return Err(Error::BufferTooShort {
187 need: pos + unique_string_length,
188 have: payload_end,
189 what: "CitSection unique_string",
190 });
191 }
192 let unique_string = DvbText::new(&bytes[pos..pos + unique_string_length]);
193 pos += unique_string_length;
194 crid_entries.push(CridEntry {
195 crid_ref,
196 prepend_string_index,
197 unique_string,
198 });
199 }
200
201 Ok(CitSection {
202 private_indicator,
203 service_id,
204 version_number,
205 current_next_indicator,
206 section_number,
207 last_section_number,
208 transport_stream_id,
209 original_network_id,
210 prepend_strings,
211 crid_entries,
212 })
213 }
214}
215
216impl Serialize for CitSection<'_> {
217 type Error = crate::error::Error;
218
219 fn serialized_len(&self) -> usize {
220 HEADER_LEN
221 + EXTENSION_LEN
222 + self.prepend_strings.len()
223 + self
224 .crid_entries
225 .iter()
226 .map(crid_entry_serialized_len)
227 .sum::<usize>()
228 + CRC_LEN
229 }
230
231 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
232 let len = self.serialized_len();
233 if buf.len() < len {
234 return Err(Error::OutputBufferTooSmall {
235 need: len,
236 have: buf.len(),
237 });
238 }
239 if self.prepend_strings.len() > u8::MAX as usize {
240 return Err(Error::SectionLengthOverflow {
241 declared: self.prepend_strings.len(),
242 available: u8::MAX as usize,
243 });
244 }
245
246 let section_length = (len - HEADER_LEN) as u16;
247 if section_length > 0x0FFF {
248 return Err(Error::SectionLengthOverflow {
249 declared: section_length as usize,
250 available: 0x0FFF,
251 });
252 }
253 buf[0] = TABLE_ID;
254 buf[1] = super::SECTION_B1_SSI
255 | (u8::from(self.private_indicator) << 6)
256 | super::SECTION_B1_RESERVED_HI
257 | ((section_length >> 8) as u8 & 0x0F);
258 buf[2] = (section_length & 0xFF) as u8;
259
260 buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
261 buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
262 buf[6] = self.section_number;
263 buf[7] = self.last_section_number;
264 buf[8..10].copy_from_slice(&self.transport_stream_id.to_be_bytes());
265 buf[10..12].copy_from_slice(&self.original_network_id.to_be_bytes());
266 buf[12] = self.prepend_strings.len() as u8;
267
268 let ps_start = HEADER_LEN + EXTENSION_LEN;
269 let ps_end = ps_start + self.prepend_strings.len();
270 buf[ps_start..ps_end].copy_from_slice(&self.prepend_strings);
271
272 let mut pos = ps_end;
273 for entry in &self.crid_entries {
274 buf[pos..pos + 2].copy_from_slice(&entry.crid_ref.to_be_bytes());
275 buf[pos + 2] = entry.prepend_string_index;
276 buf[pos + 3] = entry.unique_string.len() as u8;
277 pos += CRID_ENTRY_FIXED_LEN;
278 buf[pos..pos + entry.unique_string.len()].copy_from_slice(&entry.unique_string);
279 pos += entry.unique_string.len();
280 }
281
282 let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
283 buf[pos..pos + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
284 Ok(len)
285 }
286}
287impl<'a> crate::traits::TableDef<'a> for CitSection<'a> {
288 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
289 const NAME: &'static str = "CONTENT_IDENTIFIER";
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn parse_happy_path_no_crid_entries() {
298 let prepend = DvbText::new(b"CRID://example.com\x00");
299 let cit = CitSection {
300 private_indicator: false,
301 service_id: 0x1234,
302 version_number: 3,
303 current_next_indicator: true,
304 section_number: 0,
305 last_section_number: 0,
306 transport_stream_id: 0x0064,
307 original_network_id: 0x0002,
308 prepend_strings: prepend,
309 crid_entries: Vec::new(),
310 };
311 let mut buf = vec![0u8; cit.serialized_len()];
312 cit.serialize_into(&mut buf).unwrap();
313 let parsed = CitSection::parse(&buf).unwrap();
314 assert_eq!(parsed.service_id, 0x1234);
315 assert_eq!(parsed.version_number, 3);
316 assert!(parsed.current_next_indicator);
317 assert_eq!(parsed.prepend_strings, prepend);
318 assert!(parsed.crid_entries.is_empty());
319 }
320
321 #[test]
322 fn parse_happy_path_with_crid_entries() {
323 let prepend = DvbText::new(b"crid://bbc.co.uk/\x00");
324 let entries = vec![
325 CridEntry {
326 crid_ref: 0x0001,
327 prepend_string_index: 0x00,
328 unique_string: DvbText::new(b"ep1"),
329 },
330 CridEntry {
331 crid_ref: 0x0002,
332 prepend_string_index: 0xFF,
333 unique_string: DvbText::new(b"crid://bbc.co.uk/EV-1"),
334 },
335 ];
336 let cit = CitSection {
337 private_indicator: false,
338 service_id: 0xABCD,
339 version_number: 7,
340 current_next_indicator: true,
341 section_number: 1,
342 last_section_number: 3,
343 transport_stream_id: 0x01F4,
344 original_network_id: 0x0028,
345 prepend_strings: prepend,
346 crid_entries: entries,
347 };
348 let mut buf = vec![0u8; cit.serialized_len()];
349 cit.serialize_into(&mut buf).unwrap();
350 let parsed = CitSection::parse(&buf).unwrap();
351 assert_eq!(parsed.service_id, 0xABCD);
352 assert_eq!(parsed.crid_entries.len(), 2);
353 assert_eq!(parsed.crid_entries[0].crid_ref, 0x0001);
354 assert_eq!(parsed.crid_entries[0].prepend_string_index, 0x00);
355 assert_eq!(parsed.crid_entries[0].unique_string, DvbText::new(b"ep1"));
356 assert_eq!(parsed.crid_entries[1].crid_ref, 0x0002);
357 assert_eq!(parsed.crid_entries[1].prepend_string_index, 0xFF);
358 assert_eq!(
359 parsed.crid_entries[1].unique_string,
360 DvbText::new(b"crid://bbc.co.uk/EV-1")
361 );
362 }
363
364 #[test]
365 fn byte_exact_round_trip() {
366 let prepend = DvbText::new(b"crid://example.com/\x00");
367 let entries = vec![CridEntry {
368 crid_ref: 0x0042,
369 prepend_string_index: 0x00,
370 unique_string: DvbText::new(b"episode42"),
371 }];
372 let original = CitSection {
373 private_indicator: true,
374 service_id: 0x4321,
375 version_number: 15,
376 current_next_indicator: false,
377 section_number: 2,
378 last_section_number: 4,
379 transport_stream_id: 0x03E8,
380 original_network_id: 0x0050,
381 prepend_strings: prepend,
382 crid_entries: entries,
383 };
384 let mut buf = vec![0u8; original.serialized_len()];
385 original.serialize_into(&mut buf).unwrap();
386 let parsed = CitSection::parse(&buf).unwrap();
387 let mut buf2 = vec![0u8; parsed.serialized_len()];
388 parsed.serialize_into(&mut buf2).unwrap();
389 assert_eq!(buf, buf2, "byte-exact re-serialize");
390 assert_eq!(parsed.crid_entries.len(), 1);
391 assert_eq!(parsed.crid_entries[0].crid_ref, 0x0042);
392 assert_eq!(
393 parsed.crid_entries[0].unique_string,
394 DvbText::new(b"episode42")
395 );
396 }
397
398 #[test]
399 fn parse_rejects_wrong_table_id() {
400 let cit = CitSection {
401 private_indicator: false,
402 service_id: 0x0001,
403 version_number: 0,
404 current_next_indicator: true,
405 section_number: 0,
406 last_section_number: 0,
407 transport_stream_id: 0x0001,
408 original_network_id: 0x0001,
409 prepend_strings: DvbText::new(&[]),
410 crid_entries: Vec::new(),
411 };
412 let mut buf = vec![0u8; cit.serialized_len()];
413 cit.serialize_into(&mut buf).unwrap();
414 buf[0] = 0x40;
415 assert!(matches!(
416 CitSection::parse(&buf).unwrap_err(),
417 Error::UnexpectedTableId { table_id: 0x40, .. }
418 ));
419 }
420
421 #[test]
422 fn parse_rejects_buffer_too_short() {
423 assert!(matches!(
424 CitSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
425 Error::BufferTooShort { .. }
426 ));
427 }
428
429 #[test]
430 fn parse_rejects_truncated_crid_entry() {
431 let prepend = DvbText::new(&[]);
432 let cit = CitSection {
433 private_indicator: false,
434 service_id: 0x0001,
435 version_number: 0,
436 current_next_indicator: true,
437 section_number: 0,
438 last_section_number: 0,
439 transport_stream_id: 0x0001,
440 original_network_id: 0x0001,
441 prepend_strings: prepend,
442 crid_entries: Vec::new(),
443 };
444 let mut buf = vec![0u8; cit.serialized_len()];
445 cit.serialize_into(&mut buf).unwrap();
446 let mut truncated = buf.clone();
447 truncated.truncate(buf.len() - 2);
448 let sl = (truncated.len() - HEADER_LEN) as u16;
449 truncated[1] = (truncated[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
450 truncated[2] = (sl & 0xFF) as u8;
451 assert!(CitSection::parse(&truncated).is_err());
452 }
453
454 #[test]
455 fn serialize_rejects_output_buffer_too_small() {
456 let cit = CitSection {
457 private_indicator: false,
458 service_id: 0x0001,
459 version_number: 0,
460 current_next_indicator: true,
461 section_number: 0,
462 last_section_number: 0,
463 transport_stream_id: 0x0001,
464 original_network_id: 0x0001,
465 prepend_strings: DvbText::new(&[]),
466 crid_entries: Vec::new(),
467 };
468 let mut buf = vec![0u8; 2];
469 assert!(matches!(
470 cit.serialize_into(&mut buf).unwrap_err(),
471 Error::OutputBufferTooSmall { .. }
472 ));
473 }
474
475 #[test]
476 fn parse_rejects_zero_section_length() {
477 let mut buf = vec![0u8; 64];
478 buf[0] = TABLE_ID;
479 buf[1] = 0xF0;
480 buf[2] = 0x00;
481 for b in &mut buf[3..] {
482 *b = 0xFF;
483 }
484 assert!(matches!(
485 CitSection::parse(&buf).unwrap_err(),
486 Error::SectionLengthOverflow { .. }
487 ));
488 }
489
490 #[test]
491 fn parse_handwritten_cit_no_entries() {
492 let mut bytes: Vec<u8> = vec![
493 0x77, 0xF0, 0x0E, 0x12, 0x34, 0xC7, 0x00, 0x00, 0x00, 0x64, 0x00, 0x02, 0x00,
494 ];
495 let crc = dvb_common::crc32_mpeg2::compute(&bytes);
496 bytes.extend_from_slice(&crc.to_be_bytes());
497 let cit = CitSection::parse(&bytes).unwrap();
498 assert_eq!(cit.service_id, 0x1234);
499 assert_eq!(cit.transport_stream_id, 0x0064);
500 assert!(cit.crid_entries.is_empty());
501 }
502
503 #[test]
504 fn prepend_string_resolver() {
505 let cit = CitSection {
506 private_indicator: false,
507 service_id: 0x0001,
508 version_number: 0,
509 current_next_indicator: true,
510 section_number: 0,
511 last_section_number: 0,
512 transport_stream_id: 0x0001,
513 original_network_id: 0x0001,
514 prepend_strings: DvbText::new(b"crid://example.com/\x00crid://other.com/\x00"),
515 crid_entries: Vec::new(),
516 };
517 assert_eq!(cit.prepend_string(0), Some(&b"crid://example.com/"[..]));
518 assert_eq!(cit.prepend_string(1), Some(&b"crid://other.com/"[..]));
519 assert_eq!(cit.prepend_string(2), None);
520 }
521}