1use crate::compatibility::CompatibilityDescriptor;
15use crate::descriptors::DescriptorLoop;
16use crate::error::{Error, Result};
17use dvb_common::{Parse, Serialize};
18
19pub const TABLE_ID: u8 = 0x4B;
21
22pub const PID: u16 = 0x0000;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize))]
28#[non_exhaustive]
29pub enum UntActionType {
30 Reserved,
32 SystemSoftwareUpdate,
34 DvbReserved(u8),
36 UserDefined(u8),
38}
39
40impl UntActionType {
41 #[must_use]
42 pub fn from_u8(v: u8) -> Self {
44 match v {
45 0x00 => Self::Reserved,
46 0x01 => Self::SystemSoftwareUpdate,
47 v if v < 0x80 => Self::DvbReserved(v),
48 _ => Self::UserDefined(v),
49 }
50 }
51
52 #[must_use]
53 pub fn to_u8(self) -> u8 {
55 match self {
56 Self::Reserved => 0x00,
57 Self::SystemSoftwareUpdate => 0x01,
58 Self::DvbReserved(v) | Self::UserDefined(v) => v,
59 }
60 }
61
62 #[must_use]
63 pub fn name(self) -> &'static str {
65 match self {
66 Self::Reserved => "Reserved",
67 Self::SystemSoftwareUpdate => "System Software Update",
68 Self::DvbReserved(_) => "DVB Reserved",
69 Self::UserDefined(_) => "User Defined",
70 }
71 }
72}
73dvb_common::impl_spec_display!(UntActionType, DvbReserved, UserDefined);
74
75const HEADER_LEN: usize = 3;
76const FIXED_BODY_LEN: usize = 9;
77const COMMON_DESC_LEN_FIELD: usize = 2;
78const CRC_LEN: usize = 4;
79const MIN_SECTION_LEN: usize = HEADER_LEN + FIXED_BODY_LEN + COMMON_DESC_LEN_FIELD + CRC_LEN;
80
81const OFFSET_ACTION_TYPE: usize = HEADER_LEN;
82const OFFSET_OUI_HASH: usize = HEADER_LEN + 1;
83const OFFSET_FLAGS: usize = HEADER_LEN + 2;
84const OFFSET_SECTION_NUMBER: usize = HEADER_LEN + 3;
85const OFFSET_LAST_SECTION_NUMBER: usize = HEADER_LEN + 4;
86const OFFSET_OUI: usize = HEADER_LEN + 5;
87const OFFSET_PROCESSING_ORDER: usize = HEADER_LEN + 8;
88const OFFSET_COMMON_DESC_LEN: usize = HEADER_LEN + FIXED_BODY_LEN;
89
90const VERSION_NUMBER_MASK: u8 = 0x3E;
91const VERSION_NUMBER_SHIFT: u8 = 1;
92const CURRENT_NEXT_MASK: u8 = 0x01;
93const LENGTH_HIGH_NIBBLE_MASK: u8 = 0x0F;
94const FLAGS_RESERVED_BITS: u8 = 0xC0;
95const RESERVED_NIBBLE: u8 = 0xF0;
96
97const PLATFORM_LOOP_LEN_FIELD: usize = 2;
98const DESC_LOOP_LEN_FIELD: usize = 2;
99
100#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize))]
109pub struct UntPlatform<'a> {
110 pub compatibility_descriptor: CompatibilityDescriptor<'a>,
112 pub target_operational_pairs: Vec<(DescriptorLoop<'a>, DescriptorLoop<'a>)>,
115}
116
117fn unt_platform_serialized_len(p: &UntPlatform) -> usize {
118 p.compatibility_descriptor.serialized_len()
119 + PLATFORM_LOOP_LEN_FIELD
120 + p.target_operational_pairs
121 .iter()
122 .map(|(t, o)| DESC_LOOP_LEN_FIELD + t.len() + DESC_LOOP_LEN_FIELD + o.len())
123 .sum::<usize>()
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize))]
134#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
135pub struct UntSection<'a> {
136 pub action_type: UntActionType,
138 pub oui_hash: u8,
140 pub version_number: u8,
142 pub current_next_indicator: bool,
144 pub section_number: u8,
146 pub last_section_number: u8,
148 pub oui: u32,
150 pub processing_order: u8,
152 pub common_descriptors: DescriptorLoop<'a>,
155 pub platforms: Vec<UntPlatform<'a>>,
157}
158
159impl<'a> Parse<'a> for UntSection<'a> {
160 type Error = crate::error::Error;
161
162 fn parse(bytes: &'a [u8]) -> Result<Self> {
163 if bytes.len() < MIN_SECTION_LEN {
164 return Err(Error::BufferTooShort {
165 need: MIN_SECTION_LEN,
166 have: bytes.len(),
167 what: "UntSection",
168 });
169 }
170 if bytes[0] != TABLE_ID {
171 return Err(Error::UnexpectedTableId {
172 table_id: bytes[0],
173 what: "UntSection",
174 expected: &[TABLE_ID],
175 });
176 }
177
178 let section_length =
179 (((bytes[1] & LENGTH_HIGH_NIBBLE_MASK) as usize) << 8) | bytes[2] as usize;
180 let total =
181 super::check_section_length(bytes.len(), HEADER_LEN, section_length, MIN_SECTION_LEN)?;
182
183 let action_type = UntActionType::from_u8(bytes[OFFSET_ACTION_TYPE]);
184 let oui_hash = bytes[OFFSET_OUI_HASH];
185 let flags_byte = bytes[OFFSET_FLAGS];
186 let version_number = (flags_byte & VERSION_NUMBER_MASK) >> VERSION_NUMBER_SHIFT;
187 let current_next_indicator = (flags_byte & CURRENT_NEXT_MASK) != 0;
188 let section_number = bytes[OFFSET_SECTION_NUMBER];
189 let last_section_number = bytes[OFFSET_LAST_SECTION_NUMBER];
190 let oui = ((bytes[OFFSET_OUI] as u32) << 16)
191 | ((bytes[OFFSET_OUI + 1] as u32) << 8)
192 | (bytes[OFFSET_OUI + 2] as u32);
193 let processing_order = bytes[OFFSET_PROCESSING_ORDER];
194
195 let cdl = (((bytes[OFFSET_COMMON_DESC_LEN] & LENGTH_HIGH_NIBBLE_MASK) as usize) << 8)
196 | bytes[OFFSET_COMMON_DESC_LEN + 1] as usize;
197 let common_desc_start = OFFSET_COMMON_DESC_LEN + COMMON_DESC_LEN_FIELD;
198 let common_desc_end = common_desc_start + cdl;
199 if common_desc_end > total - CRC_LEN {
200 return Err(Error::SectionLengthOverflow {
201 declared: cdl,
202 available: (total - CRC_LEN).saturating_sub(common_desc_start),
203 });
204 }
205 let common_descriptors = DescriptorLoop::new(&bytes[common_desc_start..common_desc_end]);
206
207 let payload_end = total - CRC_LEN;
208 let mut pos = common_desc_end;
209 let mut platforms = Vec::new();
210 while pos < payload_end {
211 if pos + crate::compatibility::COMPAT_DESC_LEN_FIELD > payload_end {
212 return Err(Error::BufferTooShort {
213 need: pos + crate::compatibility::COMPAT_DESC_LEN_FIELD,
214 have: payload_end,
215 what: "UntSection compatibilityDescriptorLength",
216 });
217 }
218 let compat_desc_len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
219 let compat_total = crate::compatibility::COMPAT_DESC_LEN_FIELD + compat_desc_len;
220 if pos + compat_total > payload_end {
221 return Err(Error::SectionLengthOverflow {
222 declared: compat_desc_len,
223 available: payload_end
224 .saturating_sub(pos + crate::compatibility::COMPAT_DESC_LEN_FIELD),
225 });
226 }
227 let compatibility_descriptor =
228 CompatibilityDescriptor::parse(&bytes[pos..pos + compat_total])?;
229 pos += compat_total;
230
231 if pos + PLATFORM_LOOP_LEN_FIELD > payload_end {
232 return Err(Error::BufferTooShort {
233 need: pos + PLATFORM_LOOP_LEN_FIELD,
234 have: payload_end,
235 what: "UntSection platform_loop_length",
236 });
237 }
238 let platform_loop_length = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
239 pos += PLATFORM_LOOP_LEN_FIELD;
240 let platform_end = pos + platform_loop_length;
241 if platform_end > payload_end {
242 return Err(Error::SectionLengthOverflow {
243 declared: platform_loop_length,
244 available: payload_end.saturating_sub(pos),
245 });
246 }
247
248 let mut target_operational_pairs = Vec::new();
249 while pos < platform_end {
250 if pos + DESC_LOOP_LEN_FIELD > platform_end {
251 return Err(Error::BufferTooShort {
252 need: pos + DESC_LOOP_LEN_FIELD,
253 have: platform_end,
254 what: "UntSection target_descriptor_loop length",
255 });
256 }
257 let target_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
258 let target_start = pos + DESC_LOOP_LEN_FIELD;
259 let target_end = target_start + target_len;
260 if target_end > platform_end {
261 return Err(Error::SectionLengthOverflow {
262 declared: target_len,
263 available: platform_end.saturating_sub(target_start),
264 });
265 }
266 let target_descriptors = DescriptorLoop::new(&bytes[target_start..target_end]);
267 pos = target_end;
268
269 if pos + DESC_LOOP_LEN_FIELD > platform_end {
270 return Err(Error::BufferTooShort {
271 need: pos + DESC_LOOP_LEN_FIELD,
272 have: platform_end,
273 what: "UntSection operational_descriptor_loop length",
274 });
275 }
276 let op_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
277 let op_start = pos + DESC_LOOP_LEN_FIELD;
278 let op_end = op_start + op_len;
279 if op_end > platform_end {
280 return Err(Error::SectionLengthOverflow {
281 declared: op_len,
282 available: platform_end.saturating_sub(op_start),
283 });
284 }
285 let operational_descriptors = DescriptorLoop::new(&bytes[op_start..op_end]);
286 pos = op_end;
287
288 target_operational_pairs.push((target_descriptors, operational_descriptors));
289 }
290 if pos != platform_end {
291 return Err(Error::SectionLengthOverflow {
292 declared: platform_loop_length,
293 available: pos.saturating_sub(platform_end - platform_loop_length),
294 });
295 }
296
297 platforms.push(UntPlatform {
298 compatibility_descriptor,
299 target_operational_pairs,
300 });
301 }
302
303 Ok(UntSection {
304 action_type,
305 oui_hash,
306 version_number,
307 current_next_indicator,
308 section_number,
309 last_section_number,
310 oui,
311 processing_order,
312 common_descriptors,
313 platforms,
314 })
315 }
316}
317
318impl Serialize for UntSection<'_> {
319 type Error = crate::error::Error;
320
321 fn serialized_len(&self) -> usize {
322 HEADER_LEN
323 + FIXED_BODY_LEN
324 + COMMON_DESC_LEN_FIELD
325 + self.common_descriptors.len()
326 + self
327 .platforms
328 .iter()
329 .map(unt_platform_serialized_len)
330 .sum::<usize>()
331 + CRC_LEN
332 }
333
334 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
335 let len = self.serialized_len();
336 if buf.len() < len {
337 return Err(Error::OutputBufferTooSmall {
338 need: len,
339 have: buf.len(),
340 });
341 }
342
343 let section_length = (len - HEADER_LEN) as u16;
344 if section_length > 0x0FFF {
345 return Err(Error::SectionLengthOverflow {
346 declared: section_length as usize,
347 available: 0x0FFF,
348 });
349 }
350 buf[0] = TABLE_ID;
351 buf[1] =
352 super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK);
353 buf[2] = (section_length & 0xFF) as u8;
354
355 buf[OFFSET_ACTION_TYPE] = self.action_type.to_u8();
356 buf[OFFSET_OUI_HASH] = self.oui_hash;
357 buf[OFFSET_FLAGS] = FLAGS_RESERVED_BITS
358 | ((self.version_number & 0x1F) << VERSION_NUMBER_SHIFT)
359 | u8::from(self.current_next_indicator);
360 buf[OFFSET_SECTION_NUMBER] = self.section_number;
361 buf[OFFSET_LAST_SECTION_NUMBER] = self.last_section_number;
362 buf[OFFSET_OUI] = ((self.oui >> 16) & 0xFF) as u8;
363 buf[OFFSET_OUI + 1] = ((self.oui >> 8) & 0xFF) as u8;
364 buf[OFFSET_OUI + 2] = (self.oui & 0xFF) as u8;
365 buf[OFFSET_PROCESSING_ORDER] = self.processing_order;
366
367 let cdl = self.common_descriptors.len() as u16;
368 buf[OFFSET_COMMON_DESC_LEN] =
369 RESERVED_NIBBLE | ((cdl >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK);
370 buf[OFFSET_COMMON_DESC_LEN + 1] = (cdl & 0xFF) as u8;
371
372 let common_start = OFFSET_COMMON_DESC_LEN + COMMON_DESC_LEN_FIELD;
373 let common_end = common_start + self.common_descriptors.len();
374 buf[common_start..common_end].copy_from_slice(self.common_descriptors.raw());
375
376 let mut pos = common_end;
377 for platform in &self.platforms {
378 let written = platform
379 .compatibility_descriptor
380 .serialize_into(&mut buf[pos..])?;
381 pos += written;
382
383 let inner_len: usize = platform
384 .target_operational_pairs
385 .iter()
386 .map(|(t, o)| DESC_LOOP_LEN_FIELD + t.len() + DESC_LOOP_LEN_FIELD + o.len())
387 .sum();
388 buf[pos..pos + PLATFORM_LOOP_LEN_FIELD]
389 .copy_from_slice(&(inner_len as u16).to_be_bytes());
390 pos += PLATFORM_LOOP_LEN_FIELD;
391
392 for (target_descriptors, operational_descriptors) in &platform.target_operational_pairs
393 {
394 let tl = target_descriptors.len() as u16;
395 buf[pos] = RESERVED_NIBBLE | ((tl >> 8) as u8 & 0x0F);
396 buf[pos + 1] = (tl & 0xFF) as u8;
397 pos += DESC_LOOP_LEN_FIELD;
398 buf[pos..pos + target_descriptors.len()].copy_from_slice(target_descriptors.raw());
399 pos += target_descriptors.len();
400
401 let ol = operational_descriptors.len() as u16;
402 buf[pos] = RESERVED_NIBBLE | ((ol >> 8) as u8 & 0x0F);
403 buf[pos + 1] = (ol & 0xFF) as u8;
404 pos += DESC_LOOP_LEN_FIELD;
405 buf[pos..pos + operational_descriptors.len()]
406 .copy_from_slice(operational_descriptors.raw());
407 pos += operational_descriptors.len();
408 }
409 }
410
411 let crc_pos = len - CRC_LEN;
412 let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
413 buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
414 Ok(len)
415 }
416}
417impl<'a> crate::traits::TableDef<'a> for UntSection<'a> {
418 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
419 const NAME: &'static str = "UPDATE_NOTIFICATION";
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn parse_happy_path() {
428 let oui: u32 = 0x00_01_5A;
429 let oui_hash: u8 = 0x01 ^ 0x5A;
430 let common_descs: &[u8] = &[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00];
431 let unt = UntSection {
432 action_type: UntActionType::SystemSoftwareUpdate,
433 oui_hash,
434 version_number: 7,
435 current_next_indicator: true,
436 section_number: 0,
437 last_section_number: 0,
438 oui,
439 processing_order: 0x00,
440 common_descriptors: DescriptorLoop::new(common_descs),
441 platforms: vec![UntPlatform {
442 compatibility_descriptor: CompatibilityDescriptor {
443 descriptors: vec![],
444 },
445 target_operational_pairs: vec![(
446 DescriptorLoop::new(&[]),
447 DescriptorLoop::new(&[]),
448 )],
449 }],
450 };
451 let sl = unt.serialized_len();
452 let mut buf = vec![0u8; sl];
453 unt.serialize_into(&mut buf).unwrap();
454 let parsed = UntSection::parse(&buf).unwrap();
455 assert_eq!(parsed.action_type, UntActionType::SystemSoftwareUpdate);
456 assert_eq!(parsed.oui_hash, oui_hash);
457 assert_eq!(parsed.version_number, 7);
458 assert!(parsed.current_next_indicator);
459 assert_eq!(parsed.oui, oui);
460 assert_eq!(parsed.common_descriptors.raw(), common_descs);
461 assert_eq!(parsed.platforms.len(), 1);
462 assert!(parsed.platforms[0]
463 .compatibility_descriptor
464 .descriptors
465 .is_empty());
466 }
467
468 #[test]
469 fn parse_empty_platforms() {
470 let unt = UntSection {
471 action_type: UntActionType::SystemSoftwareUpdate,
472 oui_hash: 0x5B,
473 version_number: 1,
474 current_next_indicator: false,
475 section_number: 1,
476 last_section_number: 2,
477 oui: 0x00015A,
478 processing_order: 0x01,
479 common_descriptors: DescriptorLoop::new(&[]),
480 platforms: Vec::new(),
481 };
482 let mut buf = vec![0u8; unt.serialized_len()];
483 unt.serialize_into(&mut buf).unwrap();
484 let parsed = UntSection::parse(&buf).unwrap();
485 assert!(!parsed.current_next_indicator);
486 assert!(parsed.platforms.is_empty());
487 }
488
489 #[test]
490 fn byte_exact_round_trip() {
491 let target_desc: &[u8] = &[0x09, 0x01, 0xAA];
492 let op_desc: &[u8] = &[0x0A, 0x01, 0xBB];
493 let unt = UntSection {
494 action_type: UntActionType::SystemSoftwareUpdate,
495 oui_hash: 0x5B,
496 version_number: 15,
497 current_next_indicator: true,
498 section_number: 2,
499 last_section_number: 5,
500 oui: 0x00015A,
501 processing_order: 0x02,
502 common_descriptors: DescriptorLoop::new(&[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00]),
503 platforms: vec![UntPlatform {
504 compatibility_descriptor: CompatibilityDescriptor {
505 descriptors: vec![],
506 },
507 target_operational_pairs: vec![(
508 DescriptorLoop::new(target_desc),
509 DescriptorLoop::new(op_desc),
510 )],
511 }],
512 };
513 let mut buf = vec![0u8; unt.serialized_len()];
514 unt.serialize_into(&mut buf).unwrap();
515 let re = UntSection::parse(&buf).unwrap();
516 let mut buf2 = vec![0u8; re.serialized_len()];
517 re.serialize_into(&mut buf2).unwrap();
518 assert_eq!(buf, buf2, "byte-exact re-serialize");
519 let re = UntSection::parse(&buf).unwrap();
520 assert_eq!(re.platforms.len(), 1);
521 assert!(re.platforms[0]
522 .compatibility_descriptor
523 .descriptors
524 .is_empty());
525 assert_eq!(re.platforms[0].target_operational_pairs.len(), 1);
526 assert_eq!(
527 re.platforms[0].target_operational_pairs[0].0.raw(),
528 target_desc
529 );
530 assert_eq!(re.platforms[0].target_operational_pairs[0].1.raw(), op_desc);
531 }
532
533 #[test]
534 fn round_trip_platform_with_multiple_pairs() {
535 let t0: &[u8] = &[0x09, 0x01, 0xAA];
536 let o0: &[u8] = &[0x0A, 0x01, 0xBB];
537 let t1: &[u8] = &[0x01, 0x02, 0xCC, 0xDD];
538 let o1: &[u8] = &[];
539 let unt = UntSection {
540 action_type: UntActionType::SystemSoftwareUpdate,
541 oui_hash: 0x5B,
542 version_number: 15,
543 current_next_indicator: true,
544 section_number: 0,
545 last_section_number: 0,
546 oui: 0x00015A,
547 processing_order: 0x02,
548 common_descriptors: DescriptorLoop::new(&[]),
549 platforms: vec![UntPlatform {
550 compatibility_descriptor: CompatibilityDescriptor {
551 descriptors: vec![],
552 },
553 target_operational_pairs: vec![
554 (DescriptorLoop::new(t0), DescriptorLoop::new(o0)),
555 (DescriptorLoop::new(t1), DescriptorLoop::new(o1)),
556 ],
557 }],
558 };
559 let mut buf = vec![0u8; unt.serialized_len()];
560 unt.serialize_into(&mut buf).unwrap();
561 let re = UntSection::parse(&buf).unwrap();
562 assert_eq!(re.platforms.len(), 1);
563 let pairs = &re.platforms[0].target_operational_pairs;
564 assert_eq!(pairs.len(), 2, "both pairs must survive the round-trip");
565 assert_eq!(pairs[0].0.raw(), t0);
566 assert_eq!(pairs[0].1.raw(), o0);
567 assert_eq!(pairs[1].0.raw(), t1);
568 assert_eq!(pairs[1].1.raw(), o1);
569 let mut buf2 = vec![0u8; unt.serialized_len()];
571 unt.serialize_into(&mut buf2).unwrap();
572 assert_eq!(buf, buf2, "byte-exact re-serialize");
573 }
574
575 #[test]
576 fn round_trip_platform_with_nonempty_compat() {
577 use crate::compatibility::{
581 CompatibilityDescriptorEntry, DescriptorType, SpecifierType, SubDescriptor,
582 SubDescriptorType,
583 };
584 let unt = UntSection {
585 action_type: UntActionType::SystemSoftwareUpdate,
586 oui_hash: 0x5B,
587 version_number: 3,
588 current_next_indicator: true,
589 section_number: 0,
590 last_section_number: 0,
591 oui: 0x00015A,
592 processing_order: 0x00,
593 common_descriptors: DescriptorLoop::new(&[]),
594 platforms: vec![UntPlatform {
595 compatibility_descriptor: CompatibilityDescriptor {
596 descriptors: vec![CompatibilityDescriptorEntry {
597 descriptor_type: DescriptorType::SystemHardware,
598 specifier_type: SpecifierType::IeeeOui,
599 specifier_data: [0x00, 0x15, 0x0A],
600 model: 0x1234,
601 version: 0x0001,
602 sub_descriptors: vec![SubDescriptor {
603 sub_descriptor_type: SubDescriptorType::Unallocated(0x05),
604 data: &[0xAA, 0xBB],
605 }],
606 }],
607 },
608 target_operational_pairs: vec![(
609 DescriptorLoop::new(&[]),
610 DescriptorLoop::new(&[]),
611 )],
612 }],
613 };
614 let mut buf = vec![0u8; unt.serialized_len()];
615 unt.serialize_into(&mut buf).unwrap();
616 let re = UntSection::parse(&buf).unwrap();
617 assert_eq!(re, unt);
618 let entry = &re.platforms[0].compatibility_descriptor.descriptors[0];
619 assert_eq!(entry.descriptor_type, DescriptorType::SystemHardware);
620 assert_eq!(entry.model, 0x1234);
621 assert_eq!(entry.sub_descriptors[0].data, &[0xAA, 0xBB]);
622 let mut buf2 = vec![0u8; unt.serialized_len()];
623 unt.serialize_into(&mut buf2).unwrap();
624 assert_eq!(buf, buf2, "byte-exact re-serialize");
625 }
626
627 #[test]
628 fn parse_rejects_wrong_table_id() {
629 let unt = UntSection {
630 action_type: UntActionType::SystemSoftwareUpdate,
631 oui_hash: 0x5B,
632 version_number: 0,
633 current_next_indicator: true,
634 section_number: 0,
635 last_section_number: 0,
636 oui: 0x00015A,
637 processing_order: 0x00,
638 common_descriptors: DescriptorLoop::new(&[]),
639 platforms: Vec::new(),
640 };
641 let mut buf = vec![0u8; unt.serialized_len()];
642 unt.serialize_into(&mut buf).unwrap();
643 buf[0] = 0x4A;
644 assert!(matches!(
645 UntSection::parse(&buf).unwrap_err(),
646 Error::UnexpectedTableId { table_id: 0x4A, .. }
647 ));
648 }
649
650 #[test]
651 fn parse_rejects_short_buffer() {
652 assert!(matches!(
653 UntSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
654 Error::BufferTooShort { .. }
655 ));
656 }
657
658 #[test]
659 fn serialize_rejects_small_output_buffer() {
660 let unt = UntSection {
661 action_type: UntActionType::SystemSoftwareUpdate,
662 oui_hash: 0x5B,
663 version_number: 0,
664 current_next_indicator: true,
665 section_number: 0,
666 last_section_number: 0,
667 oui: 0x00015A,
668 processing_order: 0x00,
669 common_descriptors: DescriptorLoop::new(&[]),
670 platforms: Vec::new(),
671 };
672 let mut buf = vec![0u8; unt.serialized_len() - 1];
673 assert!(matches!(
674 unt.serialize_into(&mut buf).unwrap_err(),
675 Error::OutputBufferTooSmall { .. }
676 ));
677 }
678
679 #[test]
680 fn parse_rejects_zero_section_length() {
681 let mut buf = vec![0u8; 64];
682 buf[0] = TABLE_ID;
683 buf[1] = 0xF0;
684 buf[2] = 0x00;
685 for b in &mut buf[3..] {
686 *b = 0xFF;
687 }
688 assert!(matches!(
689 UntSection::parse(&buf).unwrap_err(),
690 Error::SectionLengthOverflow { .. }
691 ));
692 }
693
694 #[test]
695 fn parse_handwritten_unt_no_platforms() {
696 let mut bytes: Vec<u8> = vec![
697 0x4B, 0xF0, 0x0F, 0x01, 0x5B, 0xC1, 0x00, 0x00, 0x00, 0x01, 0x5A, 0x00, 0xF0, 0x00,
698 ];
699 let crc = dvb_common::crc32_mpeg2::compute(&bytes);
700 bytes.extend_from_slice(&crc.to_be_bytes());
701 let unt = UntSection::parse(&bytes).unwrap();
702 assert_eq!(unt.action_type, UntActionType::SystemSoftwareUpdate);
703 assert_eq!(unt.oui, 0x00015A);
704 assert!(unt.current_next_indicator);
705 assert!(unt.platforms.is_empty());
706 }
707
708 #[test]
709 fn action_type_full_range_round_trip() {
710 for byte in 0u8..=0xFF {
711 let at = UntActionType::from_u8(byte);
712 assert_eq!(
713 at.to_u8(),
714 byte,
715 "UntActionType round-trip failed for {byte:#04x}"
716 );
717 }
718 }
719}