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
299impl<'a> crate::traits::TableDef<'a> for Int<'a> {
300    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
301    const NAME: &'static str = "IP_MAC_NOTIFICATION";
302}
303
304// ── Tests ─────────────────────────────────────────────────────────────────────
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    /// Build a minimal but valid INT section byte vector.
311    ///
312    /// `platform_desc` = raw bytes for the platform_descriptor_loop payload.
313    /// `loops`         = raw bytes for the combined target/operational loop region.
314    /// CRC is zeroed (the parser does not verify CRC for INT).
315    #[allow(clippy::too_many_arguments)]
316    fn build_int(
317        action_type: u8,
318        platform_id_hash: u8,
319        version_number: u8,
320        current_next_indicator: bool,
321        section_number: u8,
322        last_section_number: u8,
323        platform_id: u32,
324        processing_order: u8,
325        platform_desc: &[u8],
326        loops: &[u8],
327    ) -> Vec<u8> {
328        let int = Int {
329            action_type,
330            platform_id_hash,
331            version_number,
332            current_next_indicator,
333            section_number,
334            last_section_number,
335            platform_id,
336            processing_order,
337            platform_descriptors: platform_desc,
338            loops,
339        };
340        let mut buf = vec![0u8; int.serialized_len()];
341        int.serialize_into(&mut buf).unwrap();
342        buf
343    }
344
345    #[test]
346    fn parse_happy_path() {
347        // platform_id 0x001234, typical DVB IP/MAC announcement.
348        let bytes = build_int(
349            ACTION_TYPE_STREAM_ANNOUNCEMENT,
350            /* platform_id_hash = */
351            0x12 ^ 0x34, // XOR of low two bytes for illustration
352            /* version_number = */ 3,
353            /* current_next_indicator = */ true,
354            /* section_number = */ 0,
355            /* last_section_number = */ 0,
356            /* platform_id = */ 0x00_12_34,
357            /* processing_order = */ 0x00,
358            /* platform_desc = */ &[0x81, 0x02, 0xAB, 0xCD],
359            /* loops = */ &[],
360        );
361        let int = Int::parse(&bytes).unwrap();
362
363        assert_eq!(int.action_type, ACTION_TYPE_STREAM_ANNOUNCEMENT);
364        assert_eq!(int.platform_id_hash, 0x12 ^ 0x34);
365        assert_eq!(int.version_number, 3);
366        assert!(int.current_next_indicator);
367        assert_eq!(int.section_number, 0);
368        assert_eq!(int.last_section_number, 0);
369        assert_eq!(int.platform_id, 0x00_12_34);
370        assert_eq!(int.processing_order, 0x00);
371        assert_eq!(int.platform_descriptors, &[0x81, 0x02, 0xAB, 0xCD][..]);
372        assert_eq!(int.loops, &[] as &[u8]);
373    }
374
375    #[test]
376    fn parse_happy_path_with_loops() {
377        // Fake target + operational loop pair: two 2-byte length fields, each
378        // covering 0 bytes of descriptors.
379        let fake_loops: [u8; 4] = [
380            0xF0, 0x00, // target_descriptor_loop_length = 0
381            0xF0, 0x00, // operational_descriptor_loop_length = 0
382        ];
383        let bytes = build_int(
384            0x01,
385            0x56,
386            5,
387            false,
388            1,
389            1,
390            0x00_56_78,
391            0x01,
392            &[],
393            &fake_loops,
394        );
395        let int = Int::parse(&bytes).unwrap();
396        assert_eq!(int.platform_id, 0x00_56_78);
397        assert_eq!(int.version_number, 5);
398        assert!(!int.current_next_indicator);
399        assert_eq!(int.loops, &fake_loops[..]);
400    }
401
402    #[test]
403    fn parse_rejects_wrong_table_id() {
404        let mut bytes = build_int(0x01, 0x00, 0, true, 0, 0, 0x000001, 0x00, &[], &[]);
405        bytes[0] = 0x4B; // wrong table_id
406        let err = Int::parse(&bytes).unwrap_err();
407        assert!(matches!(
408            err,
409            Error::UnexpectedTableId { table_id: 0x4B, .. }
410        ));
411    }
412
413    #[test]
414    fn parse_rejects_buffer_too_short() {
415        let err = Int::parse(&[TABLE_ID, 0xF0]).unwrap_err();
416        assert!(matches!(err, Error::BufferTooShort { what: "Int", .. }));
417    }
418
419    #[test]
420    fn serialize_round_trip() {
421        let plat_desc = [0x7C, 0x04, 0x01, 0x02, 0x03, 0x04];
422        let fake_loops: [u8; 4] = [0xF0, 0x00, 0xF0, 0x00];
423        let bytes = build_int(
424            ACTION_TYPE_STREAM_ANNOUNCEMENT,
425            0xAB,
426            15,
427            true,
428            2,
429            3,
430            0x00_AB_CD,
431            0x00,
432            &plat_desc,
433            &fake_loops,
434        );
435
436        let int = Int::parse(&bytes).unwrap();
437
438        // Re-serialize.
439        let mut buf = vec![0u8; int.serialized_len()];
440        int.serialize_into(&mut buf).unwrap();
441
442        // Parse again.
443        let re = Int::parse(&buf).unwrap();
444
445        assert_eq!(int, re);
446        assert_eq!(re.action_type, ACTION_TYPE_STREAM_ANNOUNCEMENT);
447        assert_eq!(re.platform_id_hash, 0xAB);
448        assert_eq!(re.version_number, 15);
449        assert!(re.current_next_indicator);
450        assert_eq!(re.section_number, 2);
451        assert_eq!(re.last_section_number, 3);
452        assert_eq!(re.platform_id, 0x00_AB_CD);
453        assert_eq!(re.processing_order, 0x00);
454        assert_eq!(re.platform_descriptors, &plat_desc[..]);
455        assert_eq!(re.loops, &fake_loops[..]);
456    }
457
458    #[test]
459    fn serialize_rejects_too_small_output_buffer() {
460        let int = Int {
461            action_type: 0x01,
462            platform_id_hash: 0x00,
463            version_number: 0,
464            current_next_indicator: true,
465            section_number: 0,
466            last_section_number: 0,
467            platform_id: 0,
468            processing_order: 0,
469            platform_descriptors: &[],
470            loops: &[],
471        };
472        let mut buf = vec![0u8; 2]; // far too small
473        let err = int.serialize_into(&mut buf).unwrap_err();
474        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
475    }
476
477    #[test]
478    fn platform_id_24bit_boundary() {
479        // Verify max 24-bit value survives a round-trip without high-byte bleed.
480        let bytes = build_int(0x01, 0xFF, 0, true, 0, 0, 0x00FF_FFFF, 0x00, &[], &[]);
481        let int = Int::parse(&bytes).unwrap();
482        assert_eq!(int.platform_id, 0x00FF_FFFF);
483    }
484}