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