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
25const HEADER_LEN: usize = 3;
26const FIXED_BODY_LEN: usize = 9;
27const COMMON_DESC_LEN_FIELD: usize = 2;
28const CRC_LEN: usize = 4;
29const MIN_SECTION_LEN: usize = HEADER_LEN + FIXED_BODY_LEN + COMMON_DESC_LEN_FIELD + CRC_LEN;
30
31const OFFSET_ACTION_TYPE: usize = HEADER_LEN;
32const OFFSET_OUI_HASH: usize = HEADER_LEN + 1;
33const OFFSET_FLAGS: usize = HEADER_LEN + 2;
34const OFFSET_SECTION_NUMBER: usize = HEADER_LEN + 3;
35const OFFSET_LAST_SECTION_NUMBER: usize = HEADER_LEN + 4;
36const OFFSET_OUI: usize = HEADER_LEN + 5;
37const OFFSET_PROCESSING_ORDER: usize = HEADER_LEN + 8;
38const OFFSET_COMMON_DESC_LEN: usize = HEADER_LEN + FIXED_BODY_LEN;
39
40const VERSION_NUMBER_MASK: u8 = 0x3E;
41const VERSION_NUMBER_SHIFT: u8 = 1;
42const CURRENT_NEXT_MASK: u8 = 0x01;
43const LENGTH_HIGH_NIBBLE_MASK: u8 = 0x0F;
44const FLAGS_RESERVED_BITS: u8 = 0xC0;
45const RESERVED_NIBBLE: u8 = 0xF0;
46
47const PLATFORM_LOOP_LEN_FIELD: usize = 2;
48const DESC_LOOP_LEN_FIELD: usize = 2;
49
50/// A single platform entry in the UNT platform loop
51/// (Tables 11/15/17/18, §9.4.2.2–9.4.2.4).
52///
53/// Each entry consists of a `compatibilityDescriptor()` block (typed as
54/// [`CompatibilityDescriptor`] — ISO/IEC 13818-6 groupInfo structure, not a
55/// standard SI descriptor), followed by a `platform_loop_length` field and
56/// target/operational descriptor-loop pairs.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize))]
59pub struct UntPlatform<'a> {
60    /// `compatibilityDescriptor()` — TS 102 006 Table 15 / ISO/IEC 13818-6.
61    pub compatibility_descriptor: CompatibilityDescriptor<'a>,
62    /// N pairs of (target_descriptor_loop, operational_descriptor_loop) per
63    /// TS 102 006 Table 11.
64    pub target_operational_pairs: Vec<(DescriptorLoop<'a>, DescriptorLoop<'a>)>,
65}
66
67fn unt_platform_serialized_len(p: &UntPlatform) -> usize {
68    p.compatibility_descriptor.serialized_len()
69        + PLATFORM_LOOP_LEN_FIELD
70        + p.target_operational_pairs
71            .iter()
72            .map(|(t, o)| DESC_LOOP_LEN_FIELD + t.len() + DESC_LOOP_LEN_FIELD + o.len())
73            .sum::<usize>()
74}
75
76/// Update Notification Table (UNT), ETSI TS 102 006 v1.4.1 §9.4, Table 11.
77///
78/// The platform loop is unfolded into typed [`UntPlatform`] entries.
79/// The `compatibilityDescriptor()` within each entry is typed as
80/// [`CompatibilityDescriptor`] (ISO/IEC 13818-6 groupInfo form — not a
81/// standard SI tag/length descriptor).
82#[derive(Debug, Clone, PartialEq, Eq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize))]
84#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
85pub struct UntSection<'a> {
86    /// Action type (Table 12): 0x01 = System Software Update, 0x80–0xFF user defined.
87    pub action_type: u8,
88    /// OUI hash: XOR of the three OUI bytes.
89    pub oui_hash: u8,
90    /// 5-bit version_number of this sub-table.
91    pub version_number: u8,
92    /// `current_next_indicator`: `true` means currently applicable.
93    pub current_next_indicator: bool,
94    /// Index of this section within the sub-table.
95    pub section_number: u8,
96    /// Index of the last section in the sub-table.
97    pub last_section_number: u8,
98    /// 24-bit IEEE OUI (low 24 bits of u32).
99    pub oui: u32,
100    /// Processing order (Table 13).
101    pub processing_order: u8,
102    /// Body of `common_descriptor_loop()` — the bytes AFTER the 12-bit length
103    /// field.
104    pub common_descriptors: DescriptorLoop<'a>,
105    /// Platform entries — unfolded per §9.4.2.2–9.4.2.4.
106    pub platforms: Vec<UntPlatform<'a>>,
107}
108
109impl<'a> Parse<'a> for UntSection<'a> {
110    type Error = crate::error::Error;
111
112    fn parse(bytes: &'a [u8]) -> Result<Self> {
113        if bytes.len() < MIN_SECTION_LEN {
114            return Err(Error::BufferTooShort {
115                need: MIN_SECTION_LEN,
116                have: bytes.len(),
117                what: "UntSection",
118            });
119        }
120        if bytes[0] != TABLE_ID {
121            return Err(Error::UnexpectedTableId {
122                table_id: bytes[0],
123                what: "UntSection",
124                expected: &[TABLE_ID],
125            });
126        }
127
128        let section_length =
129            (((bytes[1] & LENGTH_HIGH_NIBBLE_MASK) as usize) << 8) | bytes[2] as usize;
130        let total =
131            super::check_section_length(bytes.len(), HEADER_LEN, section_length, MIN_SECTION_LEN)?;
132
133        let action_type = bytes[OFFSET_ACTION_TYPE];
134        let oui_hash = bytes[OFFSET_OUI_HASH];
135        let flags_byte = bytes[OFFSET_FLAGS];
136        let version_number = (flags_byte & VERSION_NUMBER_MASK) >> VERSION_NUMBER_SHIFT;
137        let current_next_indicator = (flags_byte & CURRENT_NEXT_MASK) != 0;
138        let section_number = bytes[OFFSET_SECTION_NUMBER];
139        let last_section_number = bytes[OFFSET_LAST_SECTION_NUMBER];
140        let oui = ((bytes[OFFSET_OUI] as u32) << 16)
141            | ((bytes[OFFSET_OUI + 1] as u32) << 8)
142            | (bytes[OFFSET_OUI + 2] as u32);
143        let processing_order = bytes[OFFSET_PROCESSING_ORDER];
144
145        let cdl = (((bytes[OFFSET_COMMON_DESC_LEN] & LENGTH_HIGH_NIBBLE_MASK) as usize) << 8)
146            | bytes[OFFSET_COMMON_DESC_LEN + 1] as usize;
147        let common_desc_start = OFFSET_COMMON_DESC_LEN + COMMON_DESC_LEN_FIELD;
148        let common_desc_end = common_desc_start + cdl;
149        if common_desc_end > total - CRC_LEN {
150            return Err(Error::SectionLengthOverflow {
151                declared: cdl,
152                available: (total - CRC_LEN).saturating_sub(common_desc_start),
153            });
154        }
155        let common_descriptors = DescriptorLoop::new(&bytes[common_desc_start..common_desc_end]);
156
157        let payload_end = total - CRC_LEN;
158        let mut pos = common_desc_end;
159        let mut platforms = Vec::new();
160        while pos < payload_end {
161            if pos + crate::compatibility::COMPAT_DESC_LEN_FIELD > payload_end {
162                return Err(Error::BufferTooShort {
163                    need: pos + crate::compatibility::COMPAT_DESC_LEN_FIELD,
164                    have: payload_end,
165                    what: "UntSection compatibilityDescriptorLength",
166                });
167            }
168            let compat_desc_len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
169            let compat_total = crate::compatibility::COMPAT_DESC_LEN_FIELD + compat_desc_len;
170            if pos + compat_total > payload_end {
171                return Err(Error::SectionLengthOverflow {
172                    declared: compat_desc_len,
173                    available: payload_end
174                        .saturating_sub(pos + crate::compatibility::COMPAT_DESC_LEN_FIELD),
175                });
176            }
177            let compatibility_descriptor =
178                CompatibilityDescriptor::parse(&bytes[pos..pos + compat_total])?;
179            pos += compat_total;
180
181            if pos + PLATFORM_LOOP_LEN_FIELD > payload_end {
182                return Err(Error::BufferTooShort {
183                    need: pos + PLATFORM_LOOP_LEN_FIELD,
184                    have: payload_end,
185                    what: "UntSection platform_loop_length",
186                });
187            }
188            let platform_loop_length = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
189            pos += PLATFORM_LOOP_LEN_FIELD;
190            let platform_end = pos + platform_loop_length;
191            if platform_end > payload_end {
192                return Err(Error::SectionLengthOverflow {
193                    declared: platform_loop_length,
194                    available: payload_end.saturating_sub(pos),
195                });
196            }
197
198            let mut target_operational_pairs = Vec::new();
199            while pos < platform_end {
200                if pos + DESC_LOOP_LEN_FIELD > platform_end {
201                    return Err(Error::BufferTooShort {
202                        need: pos + DESC_LOOP_LEN_FIELD,
203                        have: platform_end,
204                        what: "UntSection target_descriptor_loop length",
205                    });
206                }
207                let target_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
208                let target_start = pos + DESC_LOOP_LEN_FIELD;
209                let target_end = target_start + target_len;
210                if target_end > platform_end {
211                    return Err(Error::SectionLengthOverflow {
212                        declared: target_len,
213                        available: platform_end.saturating_sub(target_start),
214                    });
215                }
216                let target_descriptors = DescriptorLoop::new(&bytes[target_start..target_end]);
217                pos = target_end;
218
219                if pos + DESC_LOOP_LEN_FIELD > platform_end {
220                    return Err(Error::BufferTooShort {
221                        need: pos + DESC_LOOP_LEN_FIELD,
222                        have: platform_end,
223                        what: "UntSection operational_descriptor_loop length",
224                    });
225                }
226                let op_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
227                let op_start = pos + DESC_LOOP_LEN_FIELD;
228                let op_end = op_start + op_len;
229                if op_end > platform_end {
230                    return Err(Error::SectionLengthOverflow {
231                        declared: op_len,
232                        available: platform_end.saturating_sub(op_start),
233                    });
234                }
235                let operational_descriptors = DescriptorLoop::new(&bytes[op_start..op_end]);
236                pos = op_end;
237
238                target_operational_pairs.push((target_descriptors, operational_descriptors));
239            }
240            if pos != platform_end {
241                return Err(Error::SectionLengthOverflow {
242                    declared: platform_loop_length,
243                    available: pos.saturating_sub(platform_end - platform_loop_length),
244                });
245            }
246
247            platforms.push(UntPlatform {
248                compatibility_descriptor,
249                target_operational_pairs,
250            });
251        }
252
253        Ok(UntSection {
254            action_type,
255            oui_hash,
256            version_number,
257            current_next_indicator,
258            section_number,
259            last_section_number,
260            oui,
261            processing_order,
262            common_descriptors,
263            platforms,
264        })
265    }
266}
267
268impl Serialize for UntSection<'_> {
269    type Error = crate::error::Error;
270
271    fn serialized_len(&self) -> usize {
272        HEADER_LEN
273            + FIXED_BODY_LEN
274            + COMMON_DESC_LEN_FIELD
275            + self.common_descriptors.len()
276            + self
277                .platforms
278                .iter()
279                .map(unt_platform_serialized_len)
280                .sum::<usize>()
281            + CRC_LEN
282    }
283
284    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
285        let len = self.serialized_len();
286        if buf.len() < len {
287            return Err(Error::OutputBufferTooSmall {
288                need: len,
289                have: buf.len(),
290            });
291        }
292
293        let section_length = (len - HEADER_LEN) as u16;
294        if section_length > 0x0FFF {
295            return Err(Error::SectionLengthOverflow {
296                declared: section_length as usize,
297                available: 0x0FFF,
298            });
299        }
300        buf[0] = TABLE_ID;
301        buf[1] =
302            super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK);
303        buf[2] = (section_length & 0xFF) as u8;
304
305        buf[OFFSET_ACTION_TYPE] = self.action_type;
306        buf[OFFSET_OUI_HASH] = self.oui_hash;
307        buf[OFFSET_FLAGS] = FLAGS_RESERVED_BITS
308            | ((self.version_number & 0x1F) << VERSION_NUMBER_SHIFT)
309            | u8::from(self.current_next_indicator);
310        buf[OFFSET_SECTION_NUMBER] = self.section_number;
311        buf[OFFSET_LAST_SECTION_NUMBER] = self.last_section_number;
312        buf[OFFSET_OUI] = ((self.oui >> 16) & 0xFF) as u8;
313        buf[OFFSET_OUI + 1] = ((self.oui >> 8) & 0xFF) as u8;
314        buf[OFFSET_OUI + 2] = (self.oui & 0xFF) as u8;
315        buf[OFFSET_PROCESSING_ORDER] = self.processing_order;
316
317        let cdl = self.common_descriptors.len() as u16;
318        buf[OFFSET_COMMON_DESC_LEN] =
319            RESERVED_NIBBLE | ((cdl >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK);
320        buf[OFFSET_COMMON_DESC_LEN + 1] = (cdl & 0xFF) as u8;
321
322        let common_start = OFFSET_COMMON_DESC_LEN + COMMON_DESC_LEN_FIELD;
323        let common_end = common_start + self.common_descriptors.len();
324        buf[common_start..common_end].copy_from_slice(self.common_descriptors.raw());
325
326        let mut pos = common_end;
327        for platform in &self.platforms {
328            let written = platform
329                .compatibility_descriptor
330                .serialize_into(&mut buf[pos..])?;
331            pos += written;
332
333            let inner_len: usize = platform
334                .target_operational_pairs
335                .iter()
336                .map(|(t, o)| DESC_LOOP_LEN_FIELD + t.len() + DESC_LOOP_LEN_FIELD + o.len())
337                .sum();
338            buf[pos..pos + PLATFORM_LOOP_LEN_FIELD]
339                .copy_from_slice(&(inner_len as u16).to_be_bytes());
340            pos += PLATFORM_LOOP_LEN_FIELD;
341
342            for (target_descriptors, operational_descriptors) in &platform.target_operational_pairs
343            {
344                let tl = target_descriptors.len() as u16;
345                buf[pos] = RESERVED_NIBBLE | ((tl >> 8) as u8 & 0x0F);
346                buf[pos + 1] = (tl & 0xFF) as u8;
347                pos += DESC_LOOP_LEN_FIELD;
348                buf[pos..pos + target_descriptors.len()].copy_from_slice(target_descriptors.raw());
349                pos += target_descriptors.len();
350
351                let ol = operational_descriptors.len() as u16;
352                buf[pos] = RESERVED_NIBBLE | ((ol >> 8) as u8 & 0x0F);
353                buf[pos + 1] = (ol & 0xFF) as u8;
354                pos += DESC_LOOP_LEN_FIELD;
355                buf[pos..pos + operational_descriptors.len()]
356                    .copy_from_slice(operational_descriptors.raw());
357                pos += operational_descriptors.len();
358            }
359        }
360
361        let crc_pos = len - CRC_LEN;
362        let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
363        buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
364        Ok(len)
365    }
366}
367impl<'a> crate::traits::TableDef<'a> for UntSection<'a> {
368    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
369    const NAME: &'static str = "UPDATE_NOTIFICATION";
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn parse_happy_path() {
378        let oui: u32 = 0x00_01_5A;
379        let oui_hash: u8 = 0x01 ^ 0x5A;
380        let common_descs: &[u8] = &[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00];
381        let unt = UntSection {
382            action_type: 0x01,
383            oui_hash,
384            version_number: 7,
385            current_next_indicator: true,
386            section_number: 0,
387            last_section_number: 0,
388            oui,
389            processing_order: 0x00,
390            common_descriptors: DescriptorLoop::new(common_descs),
391            platforms: vec![UntPlatform {
392                compatibility_descriptor: CompatibilityDescriptor {
393                    descriptors: vec![],
394                },
395                target_operational_pairs: vec![(
396                    DescriptorLoop::new(&[]),
397                    DescriptorLoop::new(&[]),
398                )],
399            }],
400        };
401        let sl = unt.serialized_len();
402        let mut buf = vec![0u8; sl];
403        unt.serialize_into(&mut buf).unwrap();
404        let parsed = UntSection::parse(&buf).unwrap();
405        assert_eq!(parsed.action_type, 0x01);
406        assert_eq!(parsed.oui_hash, oui_hash);
407        assert_eq!(parsed.version_number, 7);
408        assert!(parsed.current_next_indicator);
409        assert_eq!(parsed.oui, oui);
410        assert_eq!(parsed.common_descriptors.raw(), common_descs);
411        assert_eq!(parsed.platforms.len(), 1);
412        assert!(parsed.platforms[0]
413            .compatibility_descriptor
414            .descriptors
415            .is_empty());
416    }
417
418    #[test]
419    fn parse_empty_platforms() {
420        let unt = UntSection {
421            action_type: 0x01,
422            oui_hash: 0x5B,
423            version_number: 1,
424            current_next_indicator: false,
425            section_number: 1,
426            last_section_number: 2,
427            oui: 0x00015A,
428            processing_order: 0x01,
429            common_descriptors: DescriptorLoop::new(&[]),
430            platforms: Vec::new(),
431        };
432        let mut buf = vec![0u8; unt.serialized_len()];
433        unt.serialize_into(&mut buf).unwrap();
434        let parsed = UntSection::parse(&buf).unwrap();
435        assert!(!parsed.current_next_indicator);
436        assert!(parsed.platforms.is_empty());
437    }
438
439    #[test]
440    fn byte_exact_round_trip() {
441        let target_desc: &[u8] = &[0x09, 0x01, 0xAA];
442        let op_desc: &[u8] = &[0x0A, 0x01, 0xBB];
443        let unt = UntSection {
444            action_type: 0x01,
445            oui_hash: 0x5B,
446            version_number: 15,
447            current_next_indicator: true,
448            section_number: 2,
449            last_section_number: 5,
450            oui: 0x00015A,
451            processing_order: 0x02,
452            common_descriptors: DescriptorLoop::new(&[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00]),
453            platforms: vec![UntPlatform {
454                compatibility_descriptor: CompatibilityDescriptor {
455                    descriptors: vec![],
456                },
457                target_operational_pairs: vec![(
458                    DescriptorLoop::new(target_desc),
459                    DescriptorLoop::new(op_desc),
460                )],
461            }],
462        };
463        let mut buf = vec![0u8; unt.serialized_len()];
464        unt.serialize_into(&mut buf).unwrap();
465        let re = UntSection::parse(&buf).unwrap();
466        let mut buf2 = vec![0u8; re.serialized_len()];
467        re.serialize_into(&mut buf2).unwrap();
468        assert_eq!(buf, buf2, "byte-exact re-serialize");
469        let re = UntSection::parse(&buf).unwrap();
470        assert_eq!(re.platforms.len(), 1);
471        assert!(re.platforms[0]
472            .compatibility_descriptor
473            .descriptors
474            .is_empty());
475        assert_eq!(re.platforms[0].target_operational_pairs.len(), 1);
476        assert_eq!(
477            re.platforms[0].target_operational_pairs[0].0.raw(),
478            target_desc
479        );
480        assert_eq!(re.platforms[0].target_operational_pairs[0].1.raw(), op_desc);
481    }
482
483    #[test]
484    fn round_trip_platform_with_multiple_pairs() {
485        // TS 102 006 Table 11: platform_loop_length wraps N (target, operational)
486        // descriptor-loop pairs. This bites the multi-pair parse loop — the old
487        // code stopped after the first pair.
488        let t0: &[u8] = &[0x09, 0x01, 0xAA];
489        let o0: &[u8] = &[0x0A, 0x01, 0xBB];
490        let t1: &[u8] = &[0x01, 0x02, 0xCC, 0xDD];
491        let o1: &[u8] = &[];
492        let unt = UntSection {
493            action_type: 0x01,
494            oui_hash: 0x5B,
495            version_number: 15,
496            current_next_indicator: true,
497            section_number: 0,
498            last_section_number: 0,
499            oui: 0x00015A,
500            processing_order: 0x02,
501            common_descriptors: DescriptorLoop::new(&[]),
502            platforms: vec![UntPlatform {
503                compatibility_descriptor: CompatibilityDescriptor {
504                    descriptors: vec![],
505                },
506                target_operational_pairs: vec![
507                    (DescriptorLoop::new(t0), DescriptorLoop::new(o0)),
508                    (DescriptorLoop::new(t1), DescriptorLoop::new(o1)),
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        assert_eq!(re.platforms.len(), 1);
516        let pairs = &re.platforms[0].target_operational_pairs;
517        assert_eq!(pairs.len(), 2, "both pairs must survive the round-trip");
518        assert_eq!(pairs[0].0.raw(), t0);
519        assert_eq!(pairs[0].1.raw(), o0);
520        assert_eq!(pairs[1].0.raw(), t1);
521        assert_eq!(pairs[1].1.raw(), o1);
522        // serialize is deterministic.
523        let mut buf2 = vec![0u8; unt.serialized_len()];
524        unt.serialize_into(&mut buf2).unwrap();
525        assert_eq!(buf, buf2, "byte-exact re-serialize");
526    }
527
528    #[test]
529    fn round_trip_platform_with_nonempty_compat() {
530        // A platform carrying a non-empty compatibilityDescriptor() (one entry
531        // with a sub-descriptor) — the other UNT tests only exercise the empty
532        // form, so this pins the full compat block through UntSection framing.
533        use crate::compatibility::{CompatibilityDescriptorEntry, SubDescriptor};
534        let unt = UntSection {
535            action_type: 0x01,
536            oui_hash: 0x5B,
537            version_number: 3,
538            current_next_indicator: true,
539            section_number: 0,
540            last_section_number: 0,
541            oui: 0x00015A,
542            processing_order: 0x00,
543            common_descriptors: DescriptorLoop::new(&[]),
544            platforms: vec![UntPlatform {
545                compatibility_descriptor: CompatibilityDescriptor {
546                    descriptors: vec![CompatibilityDescriptorEntry {
547                        descriptor_type: 0x01,
548                        specifier_type: 0x01,
549                        specifier_data: [0x00, 0x15, 0x0A],
550                        model: 0x1234,
551                        version: 0x0001,
552                        sub_descriptors: vec![SubDescriptor {
553                            sub_descriptor_type: 0x05,
554                            data: &[0xAA, 0xBB],
555                        }],
556                    }],
557                },
558                target_operational_pairs: vec![(
559                    DescriptorLoop::new(&[]),
560                    DescriptorLoop::new(&[]),
561                )],
562            }],
563        };
564        let mut buf = vec![0u8; unt.serialized_len()];
565        unt.serialize_into(&mut buf).unwrap();
566        let re = UntSection::parse(&buf).unwrap();
567        assert_eq!(re, unt);
568        let entry = &re.platforms[0].compatibility_descriptor.descriptors[0];
569        assert_eq!(entry.descriptor_type, 0x01);
570        assert_eq!(entry.model, 0x1234);
571        assert_eq!(entry.sub_descriptors[0].data, &[0xAA, 0xBB]);
572        let mut buf2 = vec![0u8; unt.serialized_len()];
573        unt.serialize_into(&mut buf2).unwrap();
574        assert_eq!(buf, buf2, "byte-exact re-serialize");
575    }
576
577    #[test]
578    fn parse_rejects_wrong_table_id() {
579        let unt = UntSection {
580            action_type: 0x01,
581            oui_hash: 0x5B,
582            version_number: 0,
583            current_next_indicator: true,
584            section_number: 0,
585            last_section_number: 0,
586            oui: 0x00015A,
587            processing_order: 0x00,
588            common_descriptors: DescriptorLoop::new(&[]),
589            platforms: Vec::new(),
590        };
591        let mut buf = vec![0u8; unt.serialized_len()];
592        unt.serialize_into(&mut buf).unwrap();
593        buf[0] = 0x4A;
594        assert!(matches!(
595            UntSection::parse(&buf).unwrap_err(),
596            Error::UnexpectedTableId { table_id: 0x4A, .. }
597        ));
598    }
599
600    #[test]
601    fn parse_rejects_short_buffer() {
602        assert!(matches!(
603            UntSection::parse(&[TABLE_ID, 0x00]).unwrap_err(),
604            Error::BufferTooShort { .. }
605        ));
606    }
607
608    #[test]
609    fn serialize_rejects_small_output_buffer() {
610        let unt = UntSection {
611            action_type: 0x01,
612            oui_hash: 0x5B,
613            version_number: 0,
614            current_next_indicator: true,
615            section_number: 0,
616            last_section_number: 0,
617            oui: 0x00015A,
618            processing_order: 0x00,
619            common_descriptors: DescriptorLoop::new(&[]),
620            platforms: Vec::new(),
621        };
622        let mut buf = vec![0u8; unt.serialized_len() - 1];
623        assert!(matches!(
624            unt.serialize_into(&mut buf).unwrap_err(),
625            Error::OutputBufferTooSmall { .. }
626        ));
627    }
628
629    #[test]
630    fn parse_rejects_zero_section_length() {
631        let mut buf = vec![0u8; 64];
632        buf[0] = TABLE_ID;
633        buf[1] = 0xF0;
634        buf[2] = 0x00;
635        for b in &mut buf[3..] {
636            *b = 0xFF;
637        }
638        assert!(matches!(
639            UntSection::parse(&buf).unwrap_err(),
640            Error::SectionLengthOverflow { .. }
641        ));
642    }
643
644    #[test]
645    fn parse_handwritten_unt_no_platforms() {
646        let mut bytes: Vec<u8> = vec![
647            0x4B, 0xF0, 0x0F, 0x01, 0x5B, 0xC1, 0x00, 0x00, 0x00, 0x01, 0x5A, 0x00, 0xF0, 0x00,
648        ];
649        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
650        bytes.extend_from_slice(&crc.to_be_bytes());
651        let unt = UntSection::parse(&bytes).unwrap();
652        assert_eq!(unt.action_type, 0x01);
653        assert_eq!(unt.oui, 0x00015A);
654        assert!(unt.current_next_indicator);
655        assert!(unt.platforms.is_empty());
656    }
657}