Skip to main content

dvb_si/tables/
int.rs

1//! IP/MAC Notification Table — ETSI EN 301 192 v1.7.1 §8.4.
2//!
3//! INT is referenced by a `data_broadcast_id_descriptor` (data_broadcast_id 0x000B)
4//! in the PMT ES_info loop; there is no fixed PID.  table_id is 0x4C.
5//!
6//! Section structure:
7//!
8//! ```text
9//!   [0]      table_id (0x4C)
10//!   [1..2]   section_syntax_indicator(1) | reserved_future_use(1) | reserved(2) | section_length(12)
11//!   [3]      action_type
12//!   [4]      platform_id_hash
13//!   [5]      reserved(2) | version_number(5) | current_next_indicator(1)
14//!   [6]      section_number
15//!   [7]      last_section_number
16//!   [8..10]  platform_id (24-bit, big-endian, stored in u32)
17//!   [11]     processing_order
18//!   [12..]   platform_descriptor_loop  (4-bit reserved | 12-bit length | descriptors)
19//!            then N × (target_descriptor_loop | operational_descriptor_loop)
20//!   [...-4]  CRC_32
21//! ```
22
23use crate::error::{Error, Result};
24use crate::traits::Table;
25use dvb_common::{Parse, Serialize};
26
27/// table_id for IP/MAC Notification Table.
28pub const TABLE_ID: u8 = 0x4C;
29
30/// PID on which INT is carried.
31///
32/// INT does not have a fixed PID.  It is discovered through a
33/// `data_broadcast_id_descriptor` (data_broadcast_id 0x000B) inside the PMT
34/// ES_info loop.  This constant is therefore 0x0000 (unknown/variable), matching
35/// the convention used by other tables with no fixed PID in this crate.
36pub const PID: u16 = 0x0000;
37
38/// action_type value: IP/MAC stream announcement or location (§8.4.4.1 Table 14).
39pub const ACTION_TYPE_STREAM_ANNOUNCEMENT: u8 = 0x01;
40
41// ── layout constants ────────────────────────────────────────────────────────
42
43/// Bytes 0-2: table_id + section_length field.
44const OUTER_HEADER_LEN: usize = 3;
45
46/// Bytes 3-11 (after the outer header): fixed INT-specific fields.
47///
48/// action_type(1) + platform_id_hash(1) + version_byte(1) +
49/// section_number(1) + last_section_number(1) + platform_id(3) + processing_order(1)
50const INT_FIXED_LEN: usize = 9;
51
52/// Minimum length of a descriptor-loop length header (4-bit reserved + 12-bit length).
53const LOOP_LEN_FIELD: usize = 2;
54
55/// CRC_32 at end of section.
56const CRC_LEN: usize = 4;
57
58/// Minimum complete section size:
59/// outer_header + INT_fixed + platform_descriptor_loop_len_field + CRC.
60const MIN_SECTION_LEN: usize = OUTER_HEADER_LEN + INT_FIXED_LEN + LOOP_LEN_FIELD + CRC_LEN;
61
62// ── platform_descriptor_loop byte-offset inside the section ─────────────────
63
64/// Byte offset of the first INT-fixed field (action_type).
65const OFF_ACTION_TYPE: usize = 3;
66/// Byte offset of platform_id_hash.
67const OFF_PLATFORM_ID_HASH: usize = 4;
68/// Byte offset of the version_number / current_next_indicator byte.
69const OFF_VERSION_BYTE: usize = 5;
70/// Byte offset of section_number.
71const OFF_SECTION_NUMBER: usize = 6;
72/// Byte offset of last_section_number.
73const OFF_LAST_SECTION_NUMBER: usize = 7;
74/// First byte of the 24-bit platform_id.
75const OFF_PLATFORM_ID: usize = 8;
76/// Byte offset of processing_order.
77const OFF_PROCESSING_ORDER: usize = 11;
78/// Start of the platform_descriptor_loop length field.
79const OFF_PLATFORM_DESC_LEN: usize = 12;
80
81// ── public types ─────────────────────────────────────────────────────────────
82
83/// IP/MAC Notification Table (INT), ETSI EN 301 192 v1.7.1 §8.4.
84///
85/// All variable-length regions are borrowed as raw bytes from the source
86/// slice.  The per-target-and-operational-descriptor loops are exposed as a
87/// single `loops` slice covering everything after the `platform_descriptor_loop`
88/// and before the CRC; callers that need to iterate individual entries must
89/// walk the raw bytes using the same 4-bit-reserved + 12-bit-length framing
90/// described by the spec.
91#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93pub struct Int<'a> {
94    /// Semantics of this INT announcement — 0x01 = stream announcement/location.
95    pub action_type: u8,
96
97    /// 8-bit XOR hash over the 24-bit platform_id.
98    /// Used for fast section filtering; not unique — always verify against
99    /// the full `platform_id`.
100    pub platform_id_hash: u8,
101
102    /// 5-bit version_number.
103    pub version_number: u8,
104
105    /// current_next_indicator bit.  `true` means this section is currently
106    /// applicable; `false` means it will become applicable at the next update.
107    pub current_next_indicator: bool,
108
109    /// section_number within this sub-table.
110    pub section_number: u8,
111
112    /// last_section_number in this sub-table.
113    pub last_section_number: u8,
114
115    /// 24-bit platform identifier (TS 101 162) stored in the low 24 bits of
116    /// a `u32`.  The high byte is always zero on the wire.
117    pub platform_id: u32,
118
119    /// Processing order relative to other INT sections for the same platform_id.
120    /// 0x00 means no ordering constraint.
121    pub processing_order: u8,
122
123    /// Raw bytes of the `platform_descriptor_loop` (descriptors only, not the
124    /// 2-byte length field).
125    #[cfg_attr(feature = "serde", serde(borrow))]
126    pub platform_descriptors: &'a [u8],
127
128    /// Raw bytes of all `N × (target_descriptor_loop | operational_descriptor_loop)`
129    /// entries that follow the platform_descriptor_loop and precede the CRC.
130    ///
131    /// Each iteration starts with a target_descriptor_loop length field (4-bit
132    /// reserved + 12-bit length) followed by target descriptors, then an
133    /// operational_descriptor_loop length field followed by operational
134    /// descriptors.  Callers iterate this by walking the 2-byte length headers
135    /// in sequence.
136    #[cfg_attr(feature = "serde", serde(borrow))]
137    pub loops: &'a [u8],
138}
139
140// ── Parse ────────────────────────────────────────────────────────────────────
141
142impl<'a> Parse<'a> for Int<'a> {
143    type Error = crate::error::Error;
144
145    fn parse(bytes: &'a [u8]) -> Result<Self> {
146        // 1. Absolute minimum length check.
147        if bytes.len() < MIN_SECTION_LEN {
148            return Err(Error::BufferTooShort {
149                need: MIN_SECTION_LEN,
150                have: bytes.len(),
151                what: "Int",
152            });
153        }
154
155        // 2. table_id guard.
156        if bytes[0] != TABLE_ID {
157            return Err(Error::UnexpectedTableId {
158                table_id: bytes[0],
159                what: "Int",
160                expected: &[TABLE_ID],
161            });
162        }
163
164        // 3. section_length: bits [1] 3-0 (high) and [2] (low).
165        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
166        let total = OUTER_HEADER_LEN + section_length;
167        if bytes.len() < total {
168            return Err(Error::SectionLengthOverflow {
169                declared: section_length,
170                available: bytes.len() - OUTER_HEADER_LEN,
171            });
172        }
173
174        // 4. Fixed INT fields.
175        let action_type = bytes[OFF_ACTION_TYPE];
176        let platform_id_hash = bytes[OFF_PLATFORM_ID_HASH];
177        let version_byte = bytes[OFF_VERSION_BYTE];
178        let version_number = (version_byte >> 1) & 0x1F;
179        let current_next_indicator = (version_byte & 0x01) != 0;
180        let section_number = bytes[OFF_SECTION_NUMBER];
181        let last_section_number = bytes[OFF_LAST_SECTION_NUMBER];
182        let platform_id = ((bytes[OFF_PLATFORM_ID] as u32) << 16)
183            | ((bytes[OFF_PLATFORM_ID + 1] as u32) << 8)
184            | bytes[OFF_PLATFORM_ID + 2] as u32;
185        let processing_order = bytes[OFF_PROCESSING_ORDER];
186
187        // 5. platform_descriptor_loop length field (4-bit reserved | 12-bit length).
188        let plat_desc_len = (((bytes[OFF_PLATFORM_DESC_LEN] & 0x0F) as usize) << 8)
189            | bytes[OFF_PLATFORM_DESC_LEN + 1] as usize;
190
191        let plat_desc_start = OFF_PLATFORM_DESC_LEN + LOOP_LEN_FIELD;
192        let plat_desc_end = plat_desc_start + plat_desc_len;
193
194        // Ensure platform_descriptors fit within the section (before CRC).
195        if plat_desc_end > total - CRC_LEN {
196            return Err(Error::SectionLengthOverflow {
197                declared: plat_desc_len,
198                available: (total - CRC_LEN).saturating_sub(plat_desc_start),
199            });
200        }
201
202        let platform_descriptors = &bytes[plat_desc_start..plat_desc_end];
203
204        // 6. Remainder (target/operational loops) — everything between end of
205        //    platform_descriptors and CRC.
206        let loops_start = plat_desc_end;
207        let loops_end = total - CRC_LEN;
208        let loops = &bytes[loops_start..loops_end];
209
210        Ok(Int {
211            action_type,
212            platform_id_hash,
213            version_number,
214            current_next_indicator,
215            section_number,
216            last_section_number,
217            platform_id,
218            processing_order,
219            platform_descriptors,
220            loops,
221        })
222    }
223}
224
225// ── Serialize ─────────────────────────────────────────────────────────────────
226
227impl Serialize for Int<'_> {
228    type Error = crate::error::Error;
229
230    fn serialized_len(&self) -> usize {
231        OUTER_HEADER_LEN
232            + INT_FIXED_LEN
233            + LOOP_LEN_FIELD           // platform_descriptor_loop length field
234            + self.platform_descriptors.len()
235            + self.loops.len()
236            + CRC_LEN
237    }
238
239    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
240        let len = self.serialized_len();
241        if buf.len() < len {
242            return Err(Error::OutputBufferTooSmall {
243                need: len,
244                have: buf.len(),
245            });
246        }
247
248        // section_length = everything after the 3-byte outer header.
249        let section_length = (len - OUTER_HEADER_LEN) as u16;
250        buf[0] = TABLE_ID;
251        // section_syntax_indicator=1, reserved_future_use=1, reserved=11 → top nibble 0xF.
252        buf[1] = 0xF0 | ((section_length >> 8) as u8 & 0x0F);
253        buf[2] = (section_length & 0xFF) as u8;
254
255        // INT fixed fields.
256        buf[OFF_ACTION_TYPE] = self.action_type;
257        buf[OFF_PLATFORM_ID_HASH] = self.platform_id_hash;
258        buf[OFF_VERSION_BYTE] =
259            0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
260        buf[OFF_SECTION_NUMBER] = self.section_number;
261        buf[OFF_LAST_SECTION_NUMBER] = self.last_section_number;
262        // platform_id: 24 bits big-endian.
263        buf[OFF_PLATFORM_ID] = ((self.platform_id >> 16) & 0xFF) as u8;
264        buf[OFF_PLATFORM_ID + 1] = ((self.platform_id >> 8) & 0xFF) as u8;
265        buf[OFF_PLATFORM_ID + 2] = (self.platform_id & 0xFF) as u8;
266        buf[OFF_PROCESSING_ORDER] = self.processing_order;
267
268        // platform_descriptor_loop length field (top nibble reserved = 1111).
269        let pdl = self.platform_descriptors.len() as u16;
270        buf[OFF_PLATFORM_DESC_LEN] = 0xF0 | ((pdl >> 8) as u8 & 0x0F);
271        buf[OFF_PLATFORM_DESC_LEN + 1] = (pdl & 0xFF) as u8;
272
273        // platform_descriptors.
274        let plat_start = OFF_PLATFORM_DESC_LEN + LOOP_LEN_FIELD;
275        let plat_end = plat_start + self.platform_descriptors.len();
276        buf[plat_start..plat_end].copy_from_slice(self.platform_descriptors);
277
278        // loops (raw target/operational iterations).
279        let loops_start = plat_end;
280        let loops_end = loops_start + self.loops.len();
281        buf[loops_start..loops_end].copy_from_slice(self.loops);
282
283        // CRC: compute over everything up to (but not including) the CRC slot.
284        let crc_pos = len - CRC_LEN;
285        let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_pos]);
286        buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
287
288        Ok(len)
289    }
290}
291
292// ── Table trait ──────────────────────────────────────────────────────────────
293
294impl<'a> Table<'a> for Int<'a> {
295    const TABLE_ID: u8 = TABLE_ID;
296    const PID: u16 = PID;
297}
298
299// ── Tests ─────────────────────────────────────────────────────────────────────
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    /// Build a minimal but valid INT section byte vector.
306    ///
307    /// `platform_desc` = raw bytes for the platform_descriptor_loop payload.
308    /// `loops`         = raw bytes for the combined target/operational loop region.
309    /// CRC is zeroed (the parser does not verify CRC for INT).
310    #[allow(clippy::too_many_arguments)]
311    fn build_int(
312        action_type: u8,
313        platform_id_hash: u8,
314        version_number: u8,
315        current_next_indicator: bool,
316        section_number: u8,
317        last_section_number: u8,
318        platform_id: u32,
319        processing_order: u8,
320        platform_desc: &[u8],
321        loops: &[u8],
322    ) -> Vec<u8> {
323        let int = Int {
324            action_type,
325            platform_id_hash,
326            version_number,
327            current_next_indicator,
328            section_number,
329            last_section_number,
330            platform_id,
331            processing_order,
332            platform_descriptors: platform_desc,
333            loops,
334        };
335        let mut buf = vec![0u8; int.serialized_len()];
336        int.serialize_into(&mut buf).unwrap();
337        buf
338    }
339
340    #[test]
341    fn parse_happy_path() {
342        // platform_id 0x001234, typical DVB IP/MAC announcement.
343        let bytes = build_int(
344            ACTION_TYPE_STREAM_ANNOUNCEMENT,
345            /* platform_id_hash = */
346            0x12 ^ 0x34, // XOR of low two bytes for illustration
347            /* version_number = */ 3,
348            /* current_next_indicator = */ true,
349            /* section_number = */ 0,
350            /* last_section_number = */ 0,
351            /* platform_id = */ 0x00_12_34,
352            /* processing_order = */ 0x00,
353            /* platform_desc = */ &[0x81, 0x02, 0xAB, 0xCD],
354            /* loops = */ &[],
355        );
356        let int = Int::parse(&bytes).unwrap();
357
358        assert_eq!(int.action_type, ACTION_TYPE_STREAM_ANNOUNCEMENT);
359        assert_eq!(int.platform_id_hash, 0x12 ^ 0x34);
360        assert_eq!(int.version_number, 3);
361        assert!(int.current_next_indicator);
362        assert_eq!(int.section_number, 0);
363        assert_eq!(int.last_section_number, 0);
364        assert_eq!(int.platform_id, 0x00_12_34);
365        assert_eq!(int.processing_order, 0x00);
366        assert_eq!(int.platform_descriptors, &[0x81, 0x02, 0xAB, 0xCD][..]);
367        assert_eq!(int.loops, &[] as &[u8]);
368    }
369
370    #[test]
371    fn parse_happy_path_with_loops() {
372        // Fake target + operational loop pair: two 2-byte length fields, each
373        // covering 0 bytes of descriptors.
374        let fake_loops: [u8; 4] = [
375            0xF0, 0x00, // target_descriptor_loop_length = 0
376            0xF0, 0x00, // operational_descriptor_loop_length = 0
377        ];
378        let bytes = build_int(
379            0x01,
380            0x56,
381            5,
382            false,
383            1,
384            1,
385            0x00_56_78,
386            0x01,
387            &[],
388            &fake_loops,
389        );
390        let int = Int::parse(&bytes).unwrap();
391        assert_eq!(int.platform_id, 0x00_56_78);
392        assert_eq!(int.version_number, 5);
393        assert!(!int.current_next_indicator);
394        assert_eq!(int.loops, &fake_loops[..]);
395    }
396
397    #[test]
398    fn parse_rejects_wrong_table_id() {
399        let mut bytes = build_int(0x01, 0x00, 0, true, 0, 0, 0x000001, 0x00, &[], &[]);
400        bytes[0] = 0x4B; // wrong table_id
401        let err = Int::parse(&bytes).unwrap_err();
402        assert!(matches!(
403            err,
404            Error::UnexpectedTableId { table_id: 0x4B, .. }
405        ));
406    }
407
408    #[test]
409    fn parse_rejects_buffer_too_short() {
410        let err = Int::parse(&[TABLE_ID, 0xF0]).unwrap_err();
411        assert!(matches!(err, Error::BufferTooShort { what: "Int", .. }));
412    }
413
414    #[test]
415    fn serialize_round_trip() {
416        let plat_desc = [0x7C, 0x04, 0x01, 0x02, 0x03, 0x04];
417        let fake_loops: [u8; 4] = [0xF0, 0x00, 0xF0, 0x00];
418        let bytes = build_int(
419            ACTION_TYPE_STREAM_ANNOUNCEMENT,
420            0xAB,
421            15,
422            true,
423            2,
424            3,
425            0x00_AB_CD,
426            0x00,
427            &plat_desc,
428            &fake_loops,
429        );
430
431        let int = Int::parse(&bytes).unwrap();
432
433        // Re-serialize.
434        let mut buf = vec![0u8; int.serialized_len()];
435        int.serialize_into(&mut buf).unwrap();
436
437        // Parse again.
438        let re = Int::parse(&buf).unwrap();
439
440        assert_eq!(int, re);
441        assert_eq!(re.action_type, ACTION_TYPE_STREAM_ANNOUNCEMENT);
442        assert_eq!(re.platform_id_hash, 0xAB);
443        assert_eq!(re.version_number, 15);
444        assert!(re.current_next_indicator);
445        assert_eq!(re.section_number, 2);
446        assert_eq!(re.last_section_number, 3);
447        assert_eq!(re.platform_id, 0x00_AB_CD);
448        assert_eq!(re.processing_order, 0x00);
449        assert_eq!(re.platform_descriptors, &plat_desc[..]);
450        assert_eq!(re.loops, &fake_loops[..]);
451    }
452
453    #[test]
454    fn serialize_rejects_too_small_output_buffer() {
455        let int = Int {
456            action_type: 0x01,
457            platform_id_hash: 0x00,
458            version_number: 0,
459            current_next_indicator: true,
460            section_number: 0,
461            last_section_number: 0,
462            platform_id: 0,
463            processing_order: 0,
464            platform_descriptors: &[],
465            loops: &[],
466        };
467        let mut buf = vec![0u8; 2]; // far too small
468        let err = int.serialize_into(&mut buf).unwrap_err();
469        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
470    }
471
472    #[test]
473    fn platform_id_24bit_boundary() {
474        // Verify max 24-bit value survives a round-trip without high-byte bleed.
475        let bytes = build_int(0x01, 0xFF, 0, true, 0, 0, 0x00FF_FFFF, 0x00, &[], &[]);
476        let int = Int::parse(&bytes).unwrap();
477        assert_eq!(int.platform_id, 0x00FF_FFFF);
478    }
479}