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