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[pos..]
174 .split_first_chunk::<2>()
175 .ok_or(Error::BufferTooShort {
176 need: pos + CRID_ENTRY_FIXED_LEN,
177 have: payload_end,
178 what: "CitSection crid_entry",
179 })?;
180 let crid_ref = u16::from_be_bytes(*b2);
181 let prepend_string_index = bytes[pos + 2];
182 let unique_string_length = bytes[pos + 3] as usize;
183 pos += CRID_ENTRY_FIXED_LEN;
184 if pos + unique_string_length > payload_end {
185 return Err(Error::BufferTooShort {
186 need: pos + unique_string_length,
187 have: payload_end,
188 what: "CitSection unique_string",
189 });
190 }
191 let unique_string = DvbText::new(&bytes[pos..pos + unique_string_length]);
192 pos += unique_string_length;
193 crid_entries.push(CridEntry {
194 crid_ref,
195 prepend_string_index,
196 unique_string,
197 });
198 }
199
200 Ok(CitSection {
201 private_indicator,
202 service_id,
203 version_number,
204 current_next_indicator,
205 section_number,
206 last_section_number,
207 transport_stream_id,
208 original_network_id,
209 prepend_strings,
210 crid_entries,
211 })
212 }
213}
214
215impl Serialize for CitSection<'_> {
216 type Error = crate::error::Error;
217
218 fn serialized_len(&self) -> usize {
219 HEADER_LEN
220 + EXTENSION_LEN
221 + self.prepend_strings.len()
222 + self
223 .crid_entries
224 .iter()
225 .map(crid_entry_serialized_len)
226 .sum::<usize>()
227 + CRC_LEN
228 }
229
230 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
231 let len = self.serialized_len();
232 if buf.len() < len {
233 return Err(Error::OutputBufferTooSmall {
234 need: len,
235 have: buf.len(),
236 });
237 }
238 if self.prepend_strings.len() > u8::MAX as usize {
239 return Err(Error::SectionLengthOverflow {
240 declared: self.prepend_strings.len(),
241 available: u8::MAX as usize,
242 });
243 }
244
245 let section_length = (len - HEADER_LEN) as u16;
246 if section_length > 0x0FFF {
247 return Err(Error::SectionLengthOverflow {
248 declared: section_length as usize,
249 available: 0x0FFF,
250 });
251 }
252 buf[0] = TABLE_ID;
253 buf[1] = super::SECTION_B1_SSI
254 | (u8::from(self.private_indicator) << 6)
255 | super::SECTION_B1_RESERVED_HI
256 | ((section_length >> 8) as u8 & 0x0F);
257 buf[2] = (section_length & 0xFF) as u8;
258
259 buf[3..5].copy_from_slice(&self.service_id.to_be_bytes());
260 buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
261 buf[6] = self.section_number;
262 buf[7] = self.last_section_number;
263 buf[8..10].copy_from_slice(&self.transport_stream_id.to_be_bytes());
264 buf[10..12].copy_from_slice(&self.original_network_id.to_be_bytes());
265 buf[12] = self.prepend_strings.len() as u8;
266
267 let ps_start = HEADER_LEN + EXTENSION_LEN;
268 let ps_end = ps_start + self.prepend_strings.len();
269 buf[ps_start..ps_end].copy_from_slice(&self.prepend_strings);
270
271 let mut pos = ps_end;
272 for entry in &self.crid_entries {
273 buf[pos..pos + 2].copy_from_slice(&entry.crid_ref.to_be_bytes());
274 buf[pos + 2] = entry.prepend_string_index;
275 buf[pos + 3] = entry.unique_string.len() as u8;
276 pos += CRID_ENTRY_FIXED_LEN;
277 buf[pos..pos + entry.unique_string.len()].copy_from_slice(&entry.unique_string);
278 pos += entry.unique_string.len();
279 }
280
281 let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
282 buf[pos..pos + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
283 Ok(len)
284 }
285}
286impl<'a> crate::traits::TableDef<'a> for CitSection<'a> {
287 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
288 const NAME: &'static str = "CONTENT_IDENTIFIER";
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn parse_happy_path_no_crid_entries() {
297 let prepend = DvbText::new(b"CRID://example.com\x00");
298 let cit = CitSection {
299 private_indicator: false,
300 service_id: 0x1234,
301 version_number: 3,
302 current_next_indicator: true,
303 section_number: 0,
304 last_section_number: 0,
305 transport_stream_id: 0x0064,
306 original_network_id: 0x0002,
307 prepend_strings: prepend,
308 crid_entries: Vec::new(),
309 };
310 let mut buf = vec![0u8; cit.serialized_len()];
311 cit.serialize_into(&mut buf).unwrap();
312 let parsed = CitSection::parse(&buf).unwrap();
313 assert_eq!(parsed.service_id, 0x1234);
314 assert_eq!(parsed.version_number, 3);
315 assert!(parsed.current_next_indicator);
316 assert_eq!(parsed.prepend_strings, prepend);
317 assert!(parsed.crid_entries.is_empty());
318 }
319
320 #[test]
321 fn parse_happy_path_with_crid_entries() {
322 let prepend = DvbText::new(b"crid://bbc.co.uk/\x00");
323 let entries = vec![
324 CridEntry {
325 crid_ref: 0x0001,
326 prepend_string_index: 0x00,
327 unique_string: DvbText::new(b"ep1"),
328 },
329 CridEntry {
330 crid_ref: 0x0002,
331 prepend_string_index: 0xFF,
332 unique_string: DvbText::new(b"crid://bbc.co.uk/EV-1"),
333 },
334 ];
335 let cit = CitSection {
336 private_indicator: false,
337 service_id: 0xABCD,
338 version_number: 7,
339 current_next_indicator: true,
340 section_number: 1,
341 last_section_number: 3,
342 transport_stream_id: 0x01F4,
343 original_network_id: 0x0028,
344 prepend_strings: prepend,
345 crid_entries: entries,
346 };
347 let mut buf = vec![0u8; cit.serialized_len()];
348 cit.serialize_into(&mut buf).unwrap();
349 let parsed = CitSection::parse(&buf).unwrap();
350 assert_eq!(parsed.service_id, 0xABCD);
351 assert_eq!(parsed.crid_entries.len(), 2);
352 assert_eq!(parsed.crid_entries[0].crid_ref, 0x0001);
353 assert_eq!(parsed.crid_entries[0].prepend_string_index, 0x00);
354 assert_eq!(parsed.crid_entries[0].unique_string, DvbText::new(b"ep1"));
355 assert_eq!(parsed.crid_entries[1].crid_ref, 0x0002);
356 assert_eq!(parsed.crid_entries[1].prepend_string_index, 0xFF);
357 assert_eq!(
358 parsed.crid_entries[1].unique_string,
359 DvbText::new(b"crid://bbc.co.uk/EV-1")
360 );
361 }
362
363 #[test]
364 fn byte_exact_round_trip() {
365 let prepend = DvbText::new(b"crid://example.com/\x00");
366 let entries = vec![CridEntry {
367 crid_ref: 0x0042,
368 prepend_string_index: 0x00,
369 unique_string: DvbText::new(b"episode42"),
370 }];
371 let original = CitSection {
372 private_indicator: true,
373 service_id: 0x4321,
374 version_number: 15,
375 current_next_indicator: false,
376 section_number: 2,
377 last_section_number: 4,
378 transport_stream_id: 0x03E8,
379 original_network_id: 0x0050,
380 prepend_strings: prepend,
381 crid_entries: entries,
382 };
383 let mut buf = vec![0u8; original.serialized_len()];
384 original.serialize_into(&mut buf).unwrap();
385 let parsed = CitSection::parse(&buf).unwrap();
386 let mut buf2 = vec![0u8; parsed.serialized_len()];
387 parsed.serialize_into(&mut buf2).unwrap();
388 assert_eq!(buf, buf2, "byte-exact re-serialize");
389 assert_eq!(parsed.crid_entries.len(), 1);
390 assert_eq!(parsed.crid_entries[0].crid_ref, 0x0042);
391 assert_eq!(
392 parsed.crid_entries[0].unique_string,
393 DvbText::new(b"episode42")
394 );
395 }
396
397 #[test]
398 fn parse_rejects_wrong_table_id() {
399 let cit = CitSection {
400 private_indicator: false,
401 service_id: 0x0001,
402 version_number: 0,
403 current_next_indicator: true,
404 section_number: 0,
405 last_section_number: 0,
406 transport_stream_id: 0x0001,
407 original_network_id: 0x0001,
408 prepend_strings: DvbText::new(&[]),
409 crid_entries: Vec::new(),
410 };
411 let mut buf = vec![0u8; cit.serialized_len()];
412 cit.serialize_into(&mut buf).unwrap();
413 buf[0] = 0x40;
414 assert!(matches!(
415 CitSection::parse(&buf).unwrap_err(),
416 Error::UnexpectedTableId { table_id: 0x40, .. }
417 ));
418 }
419
420 #[test]
421 fn parse_rejects_buffer_too_short() {
422 assert!(matches!(
423 CitSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
424 Error::BufferTooShort { .. }
425 ));
426 }
427
428 #[test]
429 fn parse_rejects_truncated_crid_entry() {
430 let prepend = DvbText::new(&[]);
431 let cit = CitSection {
432 private_indicator: false,
433 service_id: 0x0001,
434 version_number: 0,
435 current_next_indicator: true,
436 section_number: 0,
437 last_section_number: 0,
438 transport_stream_id: 0x0001,
439 original_network_id: 0x0001,
440 prepend_strings: prepend,
441 crid_entries: Vec::new(),
442 };
443 let mut buf = vec![0u8; cit.serialized_len()];
444 cit.serialize_into(&mut buf).unwrap();
445 let mut truncated = buf.clone();
446 truncated.truncate(buf.len() - 2);
447 let sl = (truncated.len() - HEADER_LEN) as u16;
448 truncated[1] = (truncated[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
449 truncated[2] = (sl & 0xFF) as u8;
450 assert!(CitSection::parse(&truncated).is_err());
451 }
452
453 #[test]
454 fn serialize_rejects_output_buffer_too_small() {
455 let cit = CitSection {
456 private_indicator: false,
457 service_id: 0x0001,
458 version_number: 0,
459 current_next_indicator: true,
460 section_number: 0,
461 last_section_number: 0,
462 transport_stream_id: 0x0001,
463 original_network_id: 0x0001,
464 prepend_strings: DvbText::new(&[]),
465 crid_entries: Vec::new(),
466 };
467 let mut buf = vec![0u8; 2];
468 assert!(matches!(
469 cit.serialize_into(&mut buf).unwrap_err(),
470 Error::OutputBufferTooSmall { .. }
471 ));
472 }
473
474 #[test]
475 fn parse_rejects_zero_section_length() {
476 let mut buf = vec![0u8; 64];
477 buf[0] = TABLE_ID;
478 buf[1] = 0xF0;
479 buf[2] = 0x00;
480 for b in &mut buf[3..] {
481 *b = 0xFF;
482 }
483 assert!(matches!(
484 CitSection::parse(&buf).unwrap_err(),
485 Error::SectionLengthOverflow { .. }
486 ));
487 }
488
489 #[test]
490 fn parse_handwritten_cit_no_entries() {
491 let mut bytes: Vec<u8> = vec![
492 0x77, 0xF0, 0x0E, 0x12, 0x34, 0xC7, 0x00, 0x00, 0x00, 0x64, 0x00, 0x02, 0x00,
493 ];
494 let crc = dvb_common::crc32_mpeg2::compute(&bytes);
495 bytes.extend_from_slice(&crc.to_be_bytes());
496 let cit = CitSection::parse(&bytes).unwrap();
497 assert_eq!(cit.service_id, 0x1234);
498 assert_eq!(cit.transport_stream_id, 0x0064);
499 assert!(cit.crid_entries.is_empty());
500 }
501
502 #[test]
503 fn prepend_string_resolver() {
504 let cit = CitSection {
505 private_indicator: false,
506 service_id: 0x0001,
507 version_number: 0,
508 current_next_indicator: true,
509 section_number: 0,
510 last_section_number: 0,
511 transport_stream_id: 0x0001,
512 original_network_id: 0x0001,
513 prepend_strings: DvbText::new(b"crid://example.com/\x00crid://other.com/\x00"),
514 crid_entries: Vec::new(),
515 };
516 assert_eq!(cit.prepend_string(0), Some(&b"crid://example.com/"[..]));
517 assert_eq!(cit.prepend_string(1), Some(&b"crid://other.com/"[..]));
518 assert_eq!(cit.prepend_string(2), None);
519 }
520}