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))]
126pub struct Unt<'a> {
127 /// Action type (Table 12 of ETSI TS 102 006):
128 /// 0x01 = System Software Update, 0x80–0xFF = user defined.
129 pub action_type: u8,
130
131 /// OUI hash: `OUI[23:16] ^ OUI[15:8] ^ OUI[7:0]` (XOR of the three OUI
132 /// bytes, used as a quick equality check before comparing the full OUI).
133 pub oui_hash: u8,
134
135 /// 5-bit version_number of this sub-table.
136 pub version_number: u8,
137
138 /// `current_next_indicator`: `true` means this section is currently
139 /// applicable; `false` means it applies starting from the next version.
140 pub current_next_indicator: bool,
141
142 /// Index of this section within the sub-table.
143 pub section_number: u8,
144
145 /// Index of the last section in the sub-table.
146 pub last_section_number: u8,
147
148 /// 24-bit IEEE Organizationally Unique Identifier.
149 ///
150 /// Stored in the low 24 bits of a `u32` (high byte is always zero).
151 /// The DVB-reserved generic OUI `0x00015A` means the receiver should
152 /// analyse the UNT payload to determine applicability.
153 pub oui: u32,
154
155 /// Processing order (Table 13): 0x00 = first action, 0x01–0xFE =
156 /// subsequent (ascending), 0xFF = no ordering implied.
157 pub processing_order: u8,
158
159 /// Body of `common_descriptor_loop()` — the bytes AFTER the 12-bit length
160 /// field. Contains zero or more standard SI descriptors (tag + length +
161 /// payload), as defined in §9.4.2.1. Serializes as the typed descriptor
162 /// sequence; `.raw()` yields the wire bytes.
163 pub common_descriptors: DescriptorLoop<'a>,
164
165 /// Raw bytes of the entire platform loop region — everything after
166 /// `common_descriptor_loop()` up to (but not including) the CRC_32.
167 ///
168 /// Each platform entry starts with a `compatibilityDescriptor()` block
169 /// (ISO/IEC 13818-6 §11 groupInfo form — a 2-byte length prefix +
170 /// descriptor list, **not** a standard SI tag/length descriptor), followed
171 /// by a 16-bit `platform_loop_length` then zero or more platform entries
172 /// each containing target and operational descriptor loops.
173 ///
174 /// To walk platform entries, parse this field according to
175 /// ETSI TS 102 006 §9.4.2.2–9.4.2.4.
176 #[cfg_attr(feature = "serde", serde(borrow))]
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}