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))]
94pub struct Int<'a> {
95    /// Semantics of this INT announcement — 0x01 = stream announcement/location.
96    pub action_type: u8,
97
98    /// 8-bit XOR hash over the 24-bit platform_id.
99    /// Used for fast section filtering; not unique — always verify against
100    /// the full `platform_id`.
101    pub platform_id_hash: u8,
102
103    /// 5-bit version_number.
104    pub version_number: u8,
105
106    /// current_next_indicator bit.  `true` means this section is currently
107    /// applicable; `false` means it will become applicable at the next update.
108    pub current_next_indicator: bool,
109
110    /// section_number within this sub-table.
111    pub section_number: u8,
112
113    /// last_section_number in this sub-table.
114    pub last_section_number: u8,
115
116    /// 24-bit platform identifier (TS 101 162) stored in the low 24 bits of
117    /// a `u32`.  The high byte is always zero on the wire.
118    pub platform_id: u32,
119
120    /// Processing order relative to other INT sections for the same platform_id.
121    /// 0x00 means no ordering constraint.
122    pub processing_order: u8,
123
124    /// The `platform_descriptor_loop` (descriptors only, not the 2-byte length
125    /// field). Serializes as the typed descriptor sequence; `.raw()` yields the
126    /// wire bytes.
127    pub platform_descriptors: DescriptorLoop<'a>,
128
129    /// Raw bytes of all `N × (target_descriptor_loop | operational_descriptor_loop)`
130    /// entries that follow the platform_descriptor_loop and precede the CRC.
131    ///
132    /// Each iteration starts with a target_descriptor_loop length field (4-bit
133    /// reserved + 12-bit length) followed by target descriptors, then an
134    /// operational_descriptor_loop length field followed by operational
135    /// descriptors.  Callers iterate this by walking the 2-byte length headers
136    /// in sequence.
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 = DescriptorLoop::new(&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.raw());
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: DescriptorLoop::new(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!(
372            int.platform_descriptors.raw(),
373            &[0x81, 0x02, 0xAB, 0xCD][..]
374        );
375        assert_eq!(int.loops, &[] as &[u8]);
376    }
377
378    #[test]
379    fn parse_happy_path_with_loops() {
380        // Fake target + operational loop pair: two 2-byte length fields, each
381        // covering 0 bytes of descriptors.
382        let fake_loops: [u8; 4] = [
383            0xF0, 0x00, // target_descriptor_loop_length = 0
384            0xF0, 0x00, // operational_descriptor_loop_length = 0
385        ];
386        let bytes = build_int(
387            0x01,
388            0x56,
389            5,
390            false,
391            1,
392            1,
393            0x00_56_78,
394            0x01,
395            &[],
396            &fake_loops,
397        );
398        let int = Int::parse(&bytes).unwrap();
399        assert_eq!(int.platform_id, 0x00_56_78);
400        assert_eq!(int.version_number, 5);
401        assert!(!int.current_next_indicator);
402        assert_eq!(int.loops, &fake_loops[..]);
403    }
404
405    #[test]
406    fn parse_rejects_wrong_table_id() {
407        let mut bytes = build_int(0x01, 0x00, 0, true, 0, 0, 0x000001, 0x00, &[], &[]);
408        bytes[0] = 0x4B; // wrong table_id
409        let err = Int::parse(&bytes).unwrap_err();
410        assert!(matches!(
411            err,
412            Error::UnexpectedTableId { table_id: 0x4B, .. }
413        ));
414    }
415
416    #[test]
417    fn parse_rejects_buffer_too_short() {
418        let err = Int::parse(&[TABLE_ID, 0xF0]).unwrap_err();
419        assert!(matches!(err, Error::BufferTooShort { what: "Int", .. }));
420    }
421
422    #[test]
423    fn serialize_round_trip() {
424        let plat_desc = [0x7C, 0x04, 0x01, 0x02, 0x03, 0x04];
425        let fake_loops: [u8; 4] = [0xF0, 0x00, 0xF0, 0x00];
426        let bytes = build_int(
427            ACTION_TYPE_STREAM_ANNOUNCEMENT,
428            0xAB,
429            15,
430            true,
431            2,
432            3,
433            0x00_AB_CD,
434            0x00,
435            &plat_desc,
436            &fake_loops,
437        );
438
439        let int = Int::parse(&bytes).unwrap();
440
441        // Re-serialize.
442        let mut buf = vec![0u8; int.serialized_len()];
443        int.serialize_into(&mut buf).unwrap();
444
445        // Parse again.
446        let re = Int::parse(&buf).unwrap();
447
448        assert_eq!(int, re);
449        assert_eq!(re.action_type, ACTION_TYPE_STREAM_ANNOUNCEMENT);
450        assert_eq!(re.platform_id_hash, 0xAB);
451        assert_eq!(re.version_number, 15);
452        assert!(re.current_next_indicator);
453        assert_eq!(re.section_number, 2);
454        assert_eq!(re.last_section_number, 3);
455        assert_eq!(re.platform_id, 0x00_AB_CD);
456        assert_eq!(re.processing_order, 0x00);
457        assert_eq!(re.platform_descriptors.raw(), &plat_desc[..]);
458        assert_eq!(re.loops, &fake_loops[..]);
459    }
460
461    #[test]
462    fn serialize_rejects_too_small_output_buffer() {
463        let int = Int {
464            action_type: 0x01,
465            platform_id_hash: 0x00,
466            version_number: 0,
467            current_next_indicator: true,
468            section_number: 0,
469            last_section_number: 0,
470            platform_id: 0,
471            processing_order: 0,
472            platform_descriptors: DescriptorLoop::new(&[]),
473            loops: &[],
474        };
475        let mut buf = vec![0u8; 2]; // far too small
476        let err = int.serialize_into(&mut buf).unwrap_err();
477        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
478    }
479
480    #[test]
481    fn platform_id_24bit_boundary() {
482        // Verify max 24-bit value survives a round-trip without high-byte bleed.
483        let bytes = build_int(0x01, 0xFF, 0, true, 0, 0, 0x00FF_FFFF, 0x00, &[], &[]);
484        let int = Int::parse(&bytes).unwrap();
485        assert_eq!(int.platform_id, 0x00FF_FFFF);
486    }
487}