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