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 dvb_common::{Parse, Serialize};
18
19/// `table_id` for the Update Notification Table.
20pub const TABLE_ID: u8 = 0x4B;
21
22/// Well-known PID for UNT: **none** — the UNT has no fixed PID.
23pub const PID: u16 = 0x0000;
24
25/// Action type coding — ETSI TS 102 006 §9.4.2 Table 12.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize))]
28#[non_exhaustive]
29pub enum UntActionType {
30    /// 0x00 — reserved.
31    Reserved,
32    /// 0x01 — System Software Update.
33    SystemSoftwareUpdate,
34    /// 0x02..=0x7F — DVB reserved for future use.
35    DvbReserved(u8),
36    /// 0x80..=0xFF — user defined.
37    UserDefined(u8),
38}
39
40impl UntActionType {
41    #[must_use]
42    /// Decode from the wire value.  Every value maps (lossless).
43    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    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
54    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    /// Human-readable spec display name.
64    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/// A single platform entry in the UNT platform loop
101/// (Tables 11/15/17/18, §9.4.2.2–9.4.2.4).
102///
103/// Each entry consists of a `compatibilityDescriptor()` block (typed as
104/// [`CompatibilityDescriptor`] — ISO/IEC 13818-6 groupInfo structure, not a
105/// standard SI descriptor), followed by a `platform_loop_length` field and
106/// target/operational descriptor-loop pairs.
107#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize))]
109pub struct UntPlatform<'a> {
110    /// `compatibilityDescriptor()` — TS 102 006 Table 15 / ISO/IEC 13818-6.
111    pub compatibility_descriptor: CompatibilityDescriptor<'a>,
112    /// N pairs of (target_descriptor_loop, operational_descriptor_loop) per
113    /// TS 102 006 Table 11.
114    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/// Update Notification Table (UNT), ETSI TS 102 006 v1.4.1 §9.4, Table 11.
127///
128/// The platform loop is unfolded into typed [`UntPlatform`] entries.
129/// The `compatibilityDescriptor()` within each entry is typed as
130/// [`CompatibilityDescriptor`] (ISO/IEC 13818-6 groupInfo form — not a
131/// standard SI tag/length descriptor).
132#[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    /// Action type (Table 12): 0x01 = System Software Update, 0x80–0xFF user defined.
137    pub action_type: UntActionType,
138    /// OUI hash: XOR of the three OUI bytes.
139    pub oui_hash: u8,
140    /// 5-bit version_number of this sub-table.
141    pub version_number: u8,
142    /// `current_next_indicator`: `true` means currently applicable.
143    pub current_next_indicator: bool,
144    /// Index of this section within the sub-table.
145    pub section_number: u8,
146    /// Index of the last section in the sub-table.
147    pub last_section_number: u8,
148    /// 24-bit IEEE OUI (low 24 bits of u32).
149    pub oui: u32,
150    /// Processing order (Table 13).
151    pub processing_order: u8,
152    /// Body of `common_descriptor_loop()` — the bytes AFTER the 12-bit length
153    /// field.
154    pub common_descriptors: DescriptorLoop<'a>,
155    /// Platform entries — unfolded per §9.4.2.2–9.4.2.4.
156    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        // serialize is deterministic.
570        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        // A platform carrying a non-empty compatibilityDescriptor() (one entry
578        // with a sub-descriptor) — the other UNT tests only exercise the empty
579        // form, so this pins the full compat block through UntSection framing.
580        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}