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