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//! Structure (long-form section):
10//! - 3-byte section header (table_id + section_length)
11//! - action_type (8 bit)
12//! - OUI_hash   (8 bit)
13//! - reserved(2) | version_number(5) | current_next_indicator(1)
14//! - section_number (8 bit)
15//! - last_section_number (8 bit)
16//! - OUI (24 bit, big-endian)
17//! - processing_order (8 bit)
18//! - common_descriptor_loop() — reserved(4) + length(12) + raw descriptors
19//! - platform_loop — zero or more platform entries, each containing a
20//!   `compatibilityDescriptor()` (ISO/IEC 13818-6 groupInfo form, NOT a
21//!   standard tag/length SI descriptor) followed by
22//!   `platform_loop_length(16)` then target and operational descriptor loops
23//! - CRC_32 (32 bit)
24
25use crate::descriptors::DescriptorLoop;
26use crate::error::{Error, Result};
27use crate::traits::Table;
28use dvb_common::{Parse, Serialize};
29
30/// `table_id` for the Update Notification Table.
31pub const TABLE_ID: u8 = 0x4B;
32
33/// Well-known PID for UNT: **none** — the UNT has no fixed PID.
34///
35/// The carrying PID is signalled via a `data_broadcast_id_descriptor`
36/// (`data_broadcast_id = 0x000A`) in the PMT ES_info loop. This constant is
37/// set to `0x0000` (the value `Table::PID` returns for tables with no fixed
38/// PID) so that callers can detect the special case.
39pub const PID: u16 = 0x0000;
40
41/// Minimum byte length of a valid UNT section (3-byte header + 9-byte
42/// fixed body + 2-byte common_descriptor_loop_length field + 4-byte CRC).
43const MIN_SECTION_LEN: usize = HEADER_LEN + FIXED_BODY_LEN + COMMON_DESC_LEN_FIELD + CRC_LEN;
44
45/// 3-byte outer header: table_id(8) + section_syntax_indicator(1) +
46/// reserved_future_use(1) + reserved(2) + section_length(12).
47const HEADER_LEN: usize = 3;
48
49/// Fixed portion after the header and before the common_descriptor_loop:
50/// action_type(8) + OUI_hash(8) + flags_byte(8) + section_number(8) +
51/// last_section_number(8) + OUI(24) + processing_order(8) = 9 bytes.
52const FIXED_BODY_LEN: usize = 9;
53
54/// Width of the `reserved(4) | common_descriptor_loop_length(12)` length
55/// field, in bytes.
56const COMMON_DESC_LEN_FIELD: usize = 2;
57
58/// CRC_32 trailer, 4 bytes.
59const CRC_LEN: usize = 4;
60
61/// Byte offset of `action_type` inside the raw section buffer.
62const OFFSET_ACTION_TYPE: usize = HEADER_LEN;
63
64/// Byte offset of `OUI_hash` inside the raw section buffer.
65const OFFSET_OUI_HASH: usize = HEADER_LEN + 1;
66
67/// Byte offset of the flags byte (reserved(2) | version_number(5) |
68/// current_next_indicator(1)) inside the raw section buffer.
69const OFFSET_FLAGS: usize = HEADER_LEN + 2;
70
71/// Byte offset of `section_number`.
72const OFFSET_SECTION_NUMBER: usize = HEADER_LEN + 3;
73
74/// Byte offset of `last_section_number`.
75const OFFSET_LAST_SECTION_NUMBER: usize = HEADER_LEN + 4;
76
77/// Byte offset of the first byte of the 3-byte OUI.
78const OFFSET_OUI: usize = HEADER_LEN + 5;
79
80/// Byte offset of `processing_order`.
81const OFFSET_PROCESSING_ORDER: usize = HEADER_LEN + 8;
82
83/// Byte offset of the `reserved(4) | common_descriptor_loop_length(12)` field.
84const OFFSET_COMMON_DESC_LEN: usize = HEADER_LEN + FIXED_BODY_LEN;
85
86/// Mask to extract the 5-bit version_number from the flags byte.
87const VERSION_NUMBER_MASK: u8 = 0x3E;
88
89/// Bit shift for version_number inside the flags byte.
90const VERSION_NUMBER_SHIFT: u8 = 1;
91
92/// Mask for current_next_indicator in the flags byte.
93const CURRENT_NEXT_MASK: u8 = 0x01;
94
95/// Mask for the high-4 of a 12-bit length field in its first byte.
96const LENGTH_HIGH_NIBBLE_MASK: u8 = 0x0F;
97
98/// Serialize flag byte: reserved(2) = 0b11, rest provided by caller.
99const FLAGS_RESERVED_BITS: u8 = 0xC0;
100
101/// Syntax indicator + reserved in the section_length byte: long-form
102/// (section_syntax_indicator=1, reserved_future_use=1, reserved=11).
103const SECTION_LEN_BYTE1_FLAGS: u8 = 0xB0;
104
105/// Reserved nibble for `common_descriptor_loop_length` and
106/// `platform_loop_length` high-nibble: 0xF0 (4 reserved bits set to 1).
107const RESERVED_NIBBLE: u8 = 0xF0;
108
109/// Update Notification Table (UNT).
110///
111/// Typed fields cover the fixed header (action_type through processing_order).
112/// Variable-length regions are kept as raw `&[u8]` borrows to avoid pulling in
113/// the full ISO/IEC 13818-6 `compatibilityDescriptor` parser:
114///
115/// - `common_descriptors` — the body of the `common_descriptor_loop()`, i.e.
116///   the bytes AFTER the 12-bit length field (standard SI descriptor format).
117/// - `platform_loop` — the entire remaining payload between the
118///   `common_descriptor_loop` and the CRC.  This region contains zero or more
119///   platform entries, each starting with a `compatibilityDescriptor()` (an
120///   ISO/IEC 13818-6 groupInfo block — **not** a standard tag/length SI
121///   descriptor) followed by a 16-bit `platform_loop_length` and the
122///   corresponding target / operational descriptor loops. Callers that need to
123///   walk individual platform entries must parse this field manually.
124#[derive(Debug, Clone, PartialEq, Eq)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize))]
126#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
127pub struct Unt<'a> {
128    /// Action type (Table 12 of ETSI TS 102 006):
129    /// 0x01 = System Software Update, 0x80–0xFF = user defined.
130    pub action_type: u8,
131
132    /// OUI hash: `OUI[23:16] ^ OUI[15:8] ^ OUI[7:0]` (XOR of the three OUI
133    /// bytes, used as a quick equality check before comparing the full OUI).
134    pub oui_hash: u8,
135
136    /// 5-bit version_number of this sub-table.
137    pub version_number: u8,
138
139    /// `current_next_indicator`: `true` means this section is currently
140    /// applicable; `false` means it applies starting from the next version.
141    pub current_next_indicator: bool,
142
143    /// Index of this section within the sub-table.
144    pub section_number: u8,
145
146    /// Index of the last section in the sub-table.
147    pub last_section_number: u8,
148
149    /// 24-bit IEEE Organizationally Unique Identifier.
150    ///
151    /// Stored in the low 24 bits of a `u32` (high byte is always zero).
152    /// The DVB-reserved generic OUI `0x00015A` means the receiver should
153    /// analyse the UNT payload to determine applicability.
154    pub oui: u32,
155
156    /// Processing order (Table 13): 0x00 = first action, 0x01–0xFE =
157    /// subsequent (ascending), 0xFF = no ordering implied.
158    pub processing_order: u8,
159
160    /// Body of `common_descriptor_loop()` — the bytes AFTER the 12-bit length
161    /// field.  Contains zero or more standard SI descriptors (tag + length +
162    /// payload), as defined in §9.4.2.1. Serializes as the typed descriptor
163    /// sequence; `.raw()` yields the wire bytes.
164    pub common_descriptors: DescriptorLoop<'a>,
165
166    /// Raw bytes of the entire platform loop region — everything after
167    /// `common_descriptor_loop()` up to (but not including) the CRC_32.
168    ///
169    /// Each platform entry starts with a `compatibilityDescriptor()` block
170    /// (ISO/IEC 13818-6 §11 groupInfo form — a 2-byte length prefix +
171    /// descriptor list, **not** a standard SI tag/length descriptor), followed
172    /// by a 16-bit `platform_loop_length` then zero or more platform entries
173    /// each containing target and operational descriptor loops.
174    ///
175    /// To walk platform entries, parse this field according to
176    /// ETSI TS 102 006 §9.4.2.2–9.4.2.4.
177    pub platform_loop: &'a [u8],
178}
179
180impl<'a> Parse<'a> for Unt<'a> {
181    type Error = crate::error::Error;
182
183    fn parse(bytes: &'a [u8]) -> Result<Self> {
184        // ── 1. Minimum-length guard ──────────────────────────────────────────
185        if bytes.len() < MIN_SECTION_LEN {
186            return Err(Error::BufferTooShort {
187                need: MIN_SECTION_LEN,
188                have: bytes.len(),
189                what: "Unt",
190            });
191        }
192
193        // ── 2. table_id check ────────────────────────────────────────────────
194        if bytes[0] != TABLE_ID {
195            return Err(Error::UnexpectedTableId {
196                table_id: bytes[0],
197                what: "Unt",
198                expected: &[TABLE_ID],
199            });
200        }
201
202        // ── 3. section_length → total byte count ─────────────────────────────
203        let section_length =
204            (((bytes[1] & LENGTH_HIGH_NIBBLE_MASK) as usize) << 8) | bytes[2] as usize;
205        let total = HEADER_LEN + section_length;
206        if bytes.len() < total {
207            return Err(Error::SectionLengthOverflow {
208                declared: section_length,
209                available: bytes.len() - HEADER_LEN,
210            });
211        }
212
213        // ── 4. Fixed header fields ────────────────────────────────────────────
214        let action_type = bytes[OFFSET_ACTION_TYPE];
215        let oui_hash = bytes[OFFSET_OUI_HASH];
216        let flags_byte = bytes[OFFSET_FLAGS];
217        let version_number = (flags_byte & VERSION_NUMBER_MASK) >> VERSION_NUMBER_SHIFT;
218        let current_next_indicator = (flags_byte & CURRENT_NEXT_MASK) != 0;
219        let section_number = bytes[OFFSET_SECTION_NUMBER];
220        let last_section_number = bytes[OFFSET_LAST_SECTION_NUMBER];
221        // OUI is a 24-bit big-endian value packed into bytes [OFFSET_OUI..OFFSET_OUI+3].
222        let oui = ((bytes[OFFSET_OUI] as u32) << 16)
223            | ((bytes[OFFSET_OUI + 1] as u32) << 8)
224            | (bytes[OFFSET_OUI + 2] as u32);
225        let processing_order = bytes[OFFSET_PROCESSING_ORDER];
226
227        // ── 5. common_descriptor_loop ────────────────────────────────────────
228        // reserved(4) | common_descriptor_loop_length(12)
229        let cdl = (((bytes[OFFSET_COMMON_DESC_LEN] & LENGTH_HIGH_NIBBLE_MASK) as usize) << 8)
230            | bytes[OFFSET_COMMON_DESC_LEN + 1] as usize;
231        let common_desc_start = OFFSET_COMMON_DESC_LEN + COMMON_DESC_LEN_FIELD;
232        let common_desc_end = common_desc_start + cdl;
233        if common_desc_end > total - CRC_LEN {
234            return Err(Error::SectionLengthOverflow {
235                declared: cdl,
236                available: (total - CRC_LEN).saturating_sub(common_desc_start),
237            });
238        }
239        let common_descriptors = DescriptorLoop::new(&bytes[common_desc_start..common_desc_end]);
240
241        // ── 6. platform_loop ─────────────────────────────────────────────────
242        let platform_loop_start = common_desc_end;
243        let platform_loop_end = total - CRC_LEN;
244        let platform_loop = &bytes[platform_loop_start..platform_loop_end];
245
246        Ok(Unt {
247            action_type,
248            oui_hash,
249            version_number,
250            current_next_indicator,
251            section_number,
252            last_section_number,
253            oui,
254            processing_order,
255            common_descriptors,
256            platform_loop,
257        })
258    }
259}
260
261impl Serialize for Unt<'_> {
262    type Error = crate::error::Error;
263
264    fn serialized_len(&self) -> usize {
265        HEADER_LEN
266            + FIXED_BODY_LEN
267            + COMMON_DESC_LEN_FIELD
268            + self.common_descriptors.len()
269            + self.platform_loop.len()
270            + CRC_LEN
271    }
272
273    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
274        let len = self.serialized_len();
275        if buf.len() < len {
276            return Err(Error::OutputBufferTooSmall {
277                need: len,
278                have: buf.len(),
279            });
280        }
281
282        // ── Header ───────────────────────────────────────────────────────────
283        let section_length = (len - HEADER_LEN) as u16;
284        buf[0] = TABLE_ID;
285        buf[1] = SECTION_LEN_BYTE1_FLAGS | ((section_length >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK);
286        buf[2] = (section_length & 0xFF) as u8;
287
288        // ── Fixed body ───────────────────────────────────────────────────────
289        buf[OFFSET_ACTION_TYPE] = self.action_type;
290        buf[OFFSET_OUI_HASH] = self.oui_hash;
291        buf[OFFSET_FLAGS] = FLAGS_RESERVED_BITS
292            | ((self.version_number & 0x1F) << VERSION_NUMBER_SHIFT)
293            | u8::from(self.current_next_indicator);
294        buf[OFFSET_SECTION_NUMBER] = self.section_number;
295        buf[OFFSET_LAST_SECTION_NUMBER] = self.last_section_number;
296        // OUI — 24 bits, big-endian.
297        buf[OFFSET_OUI] = ((self.oui >> 16) & 0xFF) as u8;
298        buf[OFFSET_OUI + 1] = ((self.oui >> 8) & 0xFF) as u8;
299        buf[OFFSET_OUI + 2] = (self.oui & 0xFF) as u8;
300        buf[OFFSET_PROCESSING_ORDER] = self.processing_order;
301
302        // ── common_descriptor_loop length field ──────────────────────────────
303        let cdl = self.common_descriptors.len() as u16;
304        buf[OFFSET_COMMON_DESC_LEN] =
305            RESERVED_NIBBLE | ((cdl >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK);
306        buf[OFFSET_COMMON_DESC_LEN + 1] = (cdl & 0xFF) as u8;
307
308        // ── common_descriptors body ──────────────────────────────────────────
309        let common_start = OFFSET_COMMON_DESC_LEN + COMMON_DESC_LEN_FIELD;
310        let common_end = common_start + self.common_descriptors.len();
311        buf[common_start..common_end].copy_from_slice(self.common_descriptors.raw());
312
313        // ── platform_loop ────────────────────────────────────────────────────
314        let plat_end = common_end + self.platform_loop.len();
315        buf[common_end..plat_end].copy_from_slice(self.platform_loop);
316
317        // ── CRC_32 — compute over everything up to (but not including) the CRC slot.
318        let crc_pos = len - CRC_LEN;
319        let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
320        buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
321
322        Ok(len)
323    }
324}
325
326impl<'a> Table<'a> for Unt<'a> {
327    const TABLE_ID: u8 = TABLE_ID;
328    const PID: u16 = PID;
329}
330
331impl<'a> crate::traits::TableDef<'a> for Unt<'a> {
332    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
333    const NAME: &'static str = "UPDATE_NOTIFICATION";
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    /// Build a minimal but syntactically valid UNT section byte buffer.
341    ///
342    /// `common_descs`  — raw bytes to place inside the common_descriptor_loop body.
343    /// `platform_loop` — raw bytes for the entire platform_loop region.
344    #[allow(clippy::too_many_arguments)]
345    fn build_unt(
346        action_type: u8,
347        oui_hash: u8,
348        version_number: u8,
349        current_next_indicator: bool,
350        section_number: u8,
351        last_section_number: u8,
352        oui: u32,
353        processing_order: u8,
354        common_descs: &[u8],
355        platform_loop: &[u8],
356    ) -> Vec<u8> {
357        // section_length covers everything after the 3-byte outer header up to
358        // and including the CRC_32.
359        let section_length = FIXED_BODY_LEN
360            + COMMON_DESC_LEN_FIELD
361            + common_descs.len()
362            + platform_loop.len()
363            + CRC_LEN;
364
365        let mut v: Vec<u8> = Vec::with_capacity(HEADER_LEN + section_length);
366
367        // Header.
368        v.push(TABLE_ID);
369        v.push(SECTION_LEN_BYTE1_FLAGS | ((section_length >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK));
370        v.push((section_length & 0xFF) as u8);
371
372        // Fixed body.
373        v.push(action_type);
374        v.push(oui_hash);
375        let flags = FLAGS_RESERVED_BITS
376            | ((version_number & 0x1F) << VERSION_NUMBER_SHIFT)
377            | u8::from(current_next_indicator);
378        v.push(flags);
379        v.push(section_number);
380        v.push(last_section_number);
381        v.push(((oui >> 16) & 0xFF) as u8);
382        v.push(((oui >> 8) & 0xFF) as u8);
383        v.push((oui & 0xFF) as u8);
384        v.push(processing_order);
385
386        // common_descriptor_loop length + body.
387        let cdl = common_descs.len() as u16;
388        v.push(RESERVED_NIBBLE | ((cdl >> 8) as u8 & LENGTH_HIGH_NIBBLE_MASK));
389        v.push((cdl & 0xFF) as u8);
390        v.extend_from_slice(common_descs);
391
392        // Platform loop.
393        v.extend_from_slice(platform_loop);
394
395        // CRC_32 placeholder.
396        v.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
397
398        v
399    }
400
401    /// Verify all typed fields are parsed correctly on a happy-path input.
402    #[test]
403    fn parse_happy_path() {
404        // OUI = 0x00015A (DVB generic), hash = 0x00 ^ 0x01 ^ 0x5A = 0x5B.
405        let oui: u32 = 0x00_01_5A;
406        let oui_hash: u8 = 0x01 ^ 0x5A;
407
408        // A minimal SSU-compatible descriptor: data_broadcast_id_descriptor
409        // tag 0x66, length 4, data_broadcast_id 0x000A, selector_len 0x00.
410        let common_descs: &[u8] = &[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00];
411
412        let bytes = build_unt(
413            0x01, // action_type: System Software Update
414            oui_hash,
415            7,    // version_number (5-bit)
416            true, // current_next_indicator
417            0,    // section_number
418            0,    // last_section_number
419            oui,
420            0x00, // processing_order: first
421            common_descs,
422            &[], // empty platform loop
423        );
424
425        let unt = Unt::parse(&bytes).expect("parse must succeed");
426
427        assert_eq!(unt.action_type, 0x01);
428        assert_eq!(unt.oui_hash, oui_hash);
429        assert_eq!(unt.version_number, 7);
430        assert!(unt.current_next_indicator);
431        assert_eq!(unt.section_number, 0);
432        assert_eq!(unt.last_section_number, 0);
433        assert_eq!(unt.oui, oui);
434        assert_eq!(unt.processing_order, 0x00);
435        assert_eq!(unt.common_descriptors.raw(), common_descs);
436        assert_eq!(unt.platform_loop, &[] as &[u8]);
437    }
438
439    /// current_next_indicator = false must parse correctly.
440    #[test]
441    fn parse_current_next_false() {
442        let bytes = build_unt(0x01, 0x5B, 1, false, 1, 2, 0x00015A, 0x01, &[], &[]);
443        let unt = Unt::parse(&bytes).unwrap();
444        assert!(!unt.current_next_indicator);
445        assert_eq!(unt.section_number, 1);
446        assert_eq!(unt.last_section_number, 2);
447    }
448
449    /// Platform loop bytes are preserved verbatim.
450    #[test]
451    fn parse_preserves_platform_loop() {
452        // Minimal compatibilityDescriptor: length=0x0004, descriptorCount=0x0000,
453        // then platform_loop_length=0x0000.
454        let plat: &[u8] = &[0x00, 0x04, 0x00, 0x00, 0x00, 0x00];
455        let bytes = build_unt(0x01, 0x5B, 3, true, 0, 0, 0x00015A, 0xFF, &[], plat);
456        let unt = Unt::parse(&bytes).unwrap();
457        assert_eq!(unt.platform_loop, plat);
458        assert_eq!(unt.processing_order, 0xFF);
459    }
460
461    /// Wrong table_id must produce `Error::UnexpectedTableId`.
462    #[test]
463    fn parse_rejects_wrong_table_id() {
464        let mut bytes = build_unt(0x01, 0x5B, 0, true, 0, 0, 0x00015A, 0x00, &[], &[]);
465        bytes[0] = 0x4A; // BAT table_id — not 0x4B
466        let err = Unt::parse(&bytes).unwrap_err();
467        assert!(
468            matches!(err, Error::UnexpectedTableId { table_id: 0x4A, .. }),
469            "expected UnexpectedTableId(0x4A), got {err:?}"
470        );
471    }
472
473    /// Buffer shorter than the minimum section size must produce
474    /// `Error::BufferTooShort`.
475    #[test]
476    fn parse_rejects_short_buffer() {
477        let err = Unt::parse(&[TABLE_ID, 0x00]).unwrap_err();
478        assert!(
479            matches!(err, Error::BufferTooShort { .. }),
480            "expected BufferTooShort, got {err:?}"
481        );
482    }
483
484    /// `serialize_into` on a buffer that is one byte too small must return
485    /// `Error::OutputBufferTooSmall`.
486    #[test]
487    fn serialize_rejects_small_output_buffer() {
488        let unt = Unt {
489            action_type: 0x01,
490            oui_hash: 0x5B,
491            version_number: 0,
492            current_next_indicator: true,
493            section_number: 0,
494            last_section_number: 0,
495            oui: 0x00015A,
496            processing_order: 0x00,
497            common_descriptors: DescriptorLoop::new(&[]),
498            platform_loop: &[],
499        };
500        let mut buf = vec![0u8; unt.serialized_len() - 1];
501        let err = unt.serialize_into(&mut buf).unwrap_err();
502        assert!(
503            matches!(err, Error::OutputBufferTooSmall { .. }),
504            "expected OutputBufferTooSmall, got {err:?}"
505        );
506    }
507
508    /// Serialize a `Unt` → parse → assert structural equality (round-trip).
509    #[test]
510    fn serialize_round_trip() {
511        let common_descs: &[u8] = &[0x66, 0x04, 0x00, 0x0A, 0x00, 0x00];
512        // Minimal compatibilityDescriptor + empty platform_loop_length.
513        let plat: &[u8] = &[0x00, 0x04, 0x00, 0x00, 0x00, 0x00];
514
515        let original = Unt {
516            action_type: 0x01,
517            oui_hash: 0x5B,
518            version_number: 15,
519            current_next_indicator: true,
520            section_number: 2,
521            last_section_number: 5,
522            oui: 0x00015A,
523            processing_order: 0x02,
524            common_descriptors: DescriptorLoop::new(common_descs),
525            platform_loop: plat,
526        };
527
528        let mut buf = vec![0u8; original.serialized_len()];
529        original
530            .serialize_into(&mut buf)
531            .expect("serialize must succeed");
532
533        let reparsed = Unt::parse(&buf).expect("reparse must succeed");
534        assert_eq!(original, reparsed);
535    }
536}