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