Skip to main content

dvb_si/tables/
unt.rs

1//! Update Notification Table — ETSI TS 102 006 v1.4.1 §9.4.
2//!
3//! The UNT delivers software-update instructions for DVB receivers. It is
4//! carried on a PID that is **signalled** — there is no fixed PID. The PMT
5//! ES_info loop for the update data carousel contains a
6//! `data_broadcast_id_descriptor` (tag 0x66) with `data_broadcast_id = 0x000A`;
7//! the associated elementary PID is the one carrying UNT sections.
8//!
9//! The platform loop is unfolded into [`UntPlatform`] entries (Tables 11/15/17/18,
10//! §9.4.2.2–9.4.2.4). The `compatibilityDescriptor()` block is typed as
11//! [`CompatibilityDescriptor`] (ISO/IEC 13818-6 groupInfo form — NOT a standard
12//! SI tag/length descriptor).
13
14use crate::compatibility::CompatibilityDescriptor;
15use crate::descriptors::DescriptorLoop;
16use crate::error::{Error, Result};
17use alloc::vec::Vec;
18use dvb_common::{Parse, Serialize};
19
20/// `table_id` for the Update Notification Table.
21pub const TABLE_ID: u8 = 0x4B;
22
23/// Well-known PID for UNT: **none** — the UNT has no fixed PID.
24pub const PID: u16 = 0x0000;
25
26/// Action type coding — ETSI TS 102 006 §9.4.2 Table 12.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29#[non_exhaustive]
30pub enum UntActionType {
31    /// 0x00 — reserved.
32    Reserved,
33    /// 0x01 — System Software Update.
34    SystemSoftwareUpdate,
35    /// 0x02..=0x7F — DVB reserved for future use.
36    DvbReserved(u8),
37    /// 0x80..=0xFF — user defined.
38    UserDefined(u8),
39}
40
41impl UntActionType {
42    #[must_use]
43    /// Decode from the wire value.  Every value maps (lossless).
44    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    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
55    pub 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    /// Human-readable spec display name.
65    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}
74dvb_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/// A single platform entry in the UNT platform loop
102/// (Tables 11/15/17/18, §9.4.2.2–9.4.2.4).
103///
104/// Each entry consists of a `compatibilityDescriptor()` block (typed as
105/// [`CompatibilityDescriptor`] — ISO/IEC 13818-6 groupInfo structure, not a
106/// standard SI descriptor), followed by a `platform_loop_length` field and
107/// target/operational descriptor-loop pairs.
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize))]
110pub struct UntPlatform<'a> {
111    /// `compatibilityDescriptor()` — TS 102 006 Table 15 / ISO/IEC 13818-6.
112    pub compatibility_descriptor: CompatibilityDescriptor<'a>,
113    /// N pairs of (target_descriptor_loop, operational_descriptor_loop) per
114    /// TS 102 006 Table 11.
115    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/// Update Notification Table (UNT), ETSI TS 102 006 v1.4.1 §9.4, Table 11.
128///
129/// The platform loop is unfolded into typed [`UntPlatform`] entries.
130/// The `compatibilityDescriptor()` within each entry is typed as
131/// [`CompatibilityDescriptor`] (ISO/IEC 13818-6 groupInfo form — not a
132/// standard SI tag/length descriptor).
133#[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    /// Action type (Table 12): 0x01 = System Software Update, 0x80–0xFF user defined.
138    pub action_type: UntActionType,
139    /// OUI hash: XOR of the three OUI bytes.
140    pub oui_hash: u8,
141    /// 5-bit version_number of this sub-table.
142    pub version_number: u8,
143    /// `current_next_indicator`: `true` means currently applicable.
144    pub current_next_indicator: bool,
145    /// Index of this section within the sub-table.
146    pub section_number: u8,
147    /// Index of the last section in the sub-table.
148    pub last_section_number: u8,
149    /// 24-bit IEEE OUI (low 24 bits of u32).
150    pub oui: u32,
151    /// Processing order (Table 13).
152    pub processing_order: u8,
153    /// Body of `common_descriptor_loop()` — the bytes AFTER the 12-bit length
154    /// field.
155    pub common_descriptors: DescriptorLoop<'a>,
156    /// Platform entries — unfolded per §9.4.2.2–9.4.2.4.
157    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 = dvb_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!(parsed.platforms[0]
480            .compatibility_descriptor
481            .descriptors
482            .is_empty());
483    }
484
485    #[test]
486    fn parse_empty_platforms() {
487        let unt = UntSection {
488            action_type: UntActionType::SystemSoftwareUpdate,
489            oui_hash: 0x5B,
490            version_number: 1,
491            current_next_indicator: false,
492            section_number: 1,
493            last_section_number: 2,
494            oui: 0x00015A,
495            processing_order: 0x01,
496            common_descriptors: DescriptorLoop::new(&[]),
497            platforms: Vec::new(),
498        };
499        let mut buf = vec![0u8; unt.serialized_len()];
500        unt.serialize_into(&mut buf).unwrap();
501        let parsed = UntSection::parse(&buf).unwrap();
502        assert!(!parsed.current_next_indicator);
503        assert!(parsed.platforms.is_empty());
504    }
505
506    #[test]
507    fn byte_exact_round_trip() {
508        let target_desc: &[u8] = &[0x09, 0x01, 0xAA];
509        let op_desc: &[u8] = &[0x0A, 0x01, 0xBB];
510        let unt = UntSection {
511            action_type: UntActionType::SystemSoftwareUpdate,
512            oui_hash: 0x5B,
513            version_number: 15,
514            current_next_indicator: true,
515            section_number: 2,
516            last_section_number: 5,
517            oui: 0x00015A,
518            processing_order: 0x02,
519            common_descriptors: DescriptorLoop::new(&[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00]),
520            platforms: vec![UntPlatform {
521                compatibility_descriptor: CompatibilityDescriptor {
522                    descriptors: vec![],
523                },
524                target_operational_pairs: vec![(
525                    DescriptorLoop::new(target_desc),
526                    DescriptorLoop::new(op_desc),
527                )],
528            }],
529        };
530        let mut buf = vec![0u8; unt.serialized_len()];
531        unt.serialize_into(&mut buf).unwrap();
532        let re = UntSection::parse(&buf).unwrap();
533        let mut buf2 = vec![0u8; re.serialized_len()];
534        re.serialize_into(&mut buf2).unwrap();
535        assert_eq!(buf, buf2, "byte-exact re-serialize");
536        let re = UntSection::parse(&buf).unwrap();
537        assert_eq!(re.platforms.len(), 1);
538        assert!(re.platforms[0]
539            .compatibility_descriptor
540            .descriptors
541            .is_empty());
542        assert_eq!(re.platforms[0].target_operational_pairs.len(), 1);
543        assert_eq!(
544            re.platforms[0].target_operational_pairs[0].0.raw(),
545            target_desc
546        );
547        assert_eq!(re.platforms[0].target_operational_pairs[0].1.raw(), op_desc);
548    }
549
550    #[test]
551    fn round_trip_platform_with_multiple_pairs() {
552        let t0: &[u8] = &[0x09, 0x01, 0xAA];
553        let o0: &[u8] = &[0x0A, 0x01, 0xBB];
554        let t1: &[u8] = &[0x01, 0x02, 0xCC, 0xDD];
555        let o1: &[u8] = &[];
556        let unt = UntSection {
557            action_type: UntActionType::SystemSoftwareUpdate,
558            oui_hash: 0x5B,
559            version_number: 15,
560            current_next_indicator: true,
561            section_number: 0,
562            last_section_number: 0,
563            oui: 0x00015A,
564            processing_order: 0x02,
565            common_descriptors: DescriptorLoop::new(&[]),
566            platforms: vec![UntPlatform {
567                compatibility_descriptor: CompatibilityDescriptor {
568                    descriptors: vec![],
569                },
570                target_operational_pairs: vec![
571                    (DescriptorLoop::new(t0), DescriptorLoop::new(o0)),
572                    (DescriptorLoop::new(t1), DescriptorLoop::new(o1)),
573                ],
574            }],
575        };
576        let mut buf = vec![0u8; unt.serialized_len()];
577        unt.serialize_into(&mut buf).unwrap();
578        let re = UntSection::parse(&buf).unwrap();
579        assert_eq!(re.platforms.len(), 1);
580        let pairs = &re.platforms[0].target_operational_pairs;
581        assert_eq!(pairs.len(), 2, "both pairs must survive the round-trip");
582        assert_eq!(pairs[0].0.raw(), t0);
583        assert_eq!(pairs[0].1.raw(), o0);
584        assert_eq!(pairs[1].0.raw(), t1);
585        assert_eq!(pairs[1].1.raw(), o1);
586        // serialize is deterministic.
587        let mut buf2 = vec![0u8; unt.serialized_len()];
588        unt.serialize_into(&mut buf2).unwrap();
589        assert_eq!(buf, buf2, "byte-exact re-serialize");
590    }
591
592    #[test]
593    fn round_trip_platform_with_nonempty_compat() {
594        // A platform carrying a non-empty compatibilityDescriptor() (one entry
595        // with a sub-descriptor) — the other UNT tests only exercise the empty
596        // form, so this pins the full compat block through UntSection framing.
597        use crate::compatibility::{
598            CompatibilityDescriptorEntry, DescriptorType, SpecifierType, SubDescriptor,
599            SubDescriptorType,
600        };
601        let unt = UntSection {
602            action_type: UntActionType::SystemSoftwareUpdate,
603            oui_hash: 0x5B,
604            version_number: 3,
605            current_next_indicator: true,
606            section_number: 0,
607            last_section_number: 0,
608            oui: 0x00015A,
609            processing_order: 0x00,
610            common_descriptors: DescriptorLoop::new(&[]),
611            platforms: vec![UntPlatform {
612                compatibility_descriptor: CompatibilityDescriptor {
613                    descriptors: vec![CompatibilityDescriptorEntry {
614                        descriptor_type: DescriptorType::SystemHardware,
615                        specifier_type: SpecifierType::IeeeOui,
616                        specifier_data: [0x00, 0x15, 0x0A],
617                        model: 0x1234,
618                        version: 0x0001,
619                        sub_descriptors: vec![SubDescriptor {
620                            sub_descriptor_type: SubDescriptorType::Unallocated(0x05),
621                            data: &[0xAA, 0xBB],
622                        }],
623                    }],
624                },
625                target_operational_pairs: vec![(
626                    DescriptorLoop::new(&[]),
627                    DescriptorLoop::new(&[]),
628                )],
629            }],
630        };
631        let mut buf = vec![0u8; unt.serialized_len()];
632        unt.serialize_into(&mut buf).unwrap();
633        let re = UntSection::parse(&buf).unwrap();
634        assert_eq!(re, unt);
635        let entry = &re.platforms[0].compatibility_descriptor.descriptors[0];
636        assert_eq!(entry.descriptor_type, DescriptorType::SystemHardware);
637        assert_eq!(entry.model, 0x1234);
638        assert_eq!(entry.sub_descriptors[0].data, &[0xAA, 0xBB]);
639        let mut buf2 = vec![0u8; unt.serialized_len()];
640        unt.serialize_into(&mut buf2).unwrap();
641        assert_eq!(buf, buf2, "byte-exact re-serialize");
642    }
643
644    #[test]
645    fn parse_rejects_wrong_table_id() {
646        let unt = UntSection {
647            action_type: UntActionType::SystemSoftwareUpdate,
648            oui_hash: 0x5B,
649            version_number: 0,
650            current_next_indicator: true,
651            section_number: 0,
652            last_section_number: 0,
653            oui: 0x00015A,
654            processing_order: 0x00,
655            common_descriptors: DescriptorLoop::new(&[]),
656            platforms: Vec::new(),
657        };
658        let mut buf = vec![0u8; unt.serialized_len()];
659        unt.serialize_into(&mut buf).unwrap();
660        buf[0] = 0x4A;
661        assert!(matches!(
662            UntSection::parse(&buf).unwrap_err(),
663            Error::UnexpectedTableId { table_id: 0x4A, .. }
664        ));
665    }
666
667    #[test]
668    fn parse_rejects_short_buffer() {
669        assert!(matches!(
670            UntSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
671            Error::BufferTooShort { .. }
672        ));
673    }
674
675    #[test]
676    fn serialize_rejects_small_output_buffer() {
677        let unt = UntSection {
678            action_type: UntActionType::SystemSoftwareUpdate,
679            oui_hash: 0x5B,
680            version_number: 0,
681            current_next_indicator: true,
682            section_number: 0,
683            last_section_number: 0,
684            oui: 0x00015A,
685            processing_order: 0x00,
686            common_descriptors: DescriptorLoop::new(&[]),
687            platforms: Vec::new(),
688        };
689        let mut buf = vec![0u8; unt.serialized_len() - 1];
690        assert!(matches!(
691            unt.serialize_into(&mut buf).unwrap_err(),
692            Error::OutputBufferTooSmall { .. }
693        ));
694    }
695
696    #[test]
697    fn parse_rejects_zero_section_length() {
698        let mut buf = vec![0u8; 64];
699        buf[0] = TABLE_ID;
700        buf[1] = 0xF0;
701        buf[2] = 0x00;
702        for b in &mut buf[3..] {
703            *b = 0xFF;
704        }
705        assert!(matches!(
706            UntSection::parse(&buf).unwrap_err(),
707            Error::SectionLengthOverflow { .. }
708        ));
709    }
710
711    #[test]
712    fn parse_handwritten_unt_no_platforms() {
713        let mut bytes: Vec<u8> = vec![
714            0x4B, 0xF0, 0x0F, 0x01, 0x5B, 0xC1, 0x00, 0x00, 0x00, 0x01, 0x5A, 0x00, 0xF0, 0x00,
715        ];
716        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
717        bytes.extend_from_slice(&crc.to_be_bytes());
718        let unt = UntSection::parse(&bytes).unwrap();
719        assert_eq!(unt.action_type, UntActionType::SystemSoftwareUpdate);
720        assert_eq!(unt.oui, 0x00015A);
721        assert!(unt.current_next_indicator);
722        assert!(unt.platforms.is_empty());
723    }
724
725    #[test]
726    fn action_type_full_range_round_trip() {
727        for byte in 0u8..=0xFF {
728            let at = UntActionType::from_u8(byte);
729            assert_eq!(
730                at.to_u8(),
731                byte,
732                "UntActionType round-trip failed for {byte:#04x}"
733            );
734        }
735    }
736}