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//! The target/operational descriptor-loop pairs in the body loop are unfolded
7//! into [`IntLoopEntry`] instances (Tables 13/17/18, §8.4.4.1).
8
9use crate::descriptors::DescriptorLoop;
10use crate::error::{Error, Result};
11use dvb_common::{Parse, Serialize};
12
13/// `table_id` for IP/MAC Notification Table.
14pub const TABLE_ID: u8 = 0x4C;
15
16/// PID on which INT is carried.
17///
18/// INT does not have a fixed PID.  It is discovered through a
19/// `data_broadcast_id_descriptor` (data_broadcast_id 0x000B) inside the PMT
20/// ES_info loop.  This constant is therefore 0x0000 (unknown/variable).
21pub const PID: u16 = 0x0000;
22
23/// Action type coding — ETSI EN 301 192 §8.4.4.1 Table 14.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26#[non_exhaustive]
27pub enum IntActionType {
28    /// 0x00 — reserved.
29    Reserved,
30    /// 0x01 — location of IP/MAC streams in DVB networks.
31    IpMacStreamLocation,
32    /// 0x02..=0xFF — reserved for future use.
33    DvbReserved(u8),
34}
35
36impl IntActionType {
37    #[must_use]
38    /// Decode from the wire value.  Every value maps (lossless).
39    pub fn from_u8(v: u8) -> Self {
40        match v {
41            0x00 => Self::Reserved,
42            0x01 => Self::IpMacStreamLocation,
43            _ => Self::DvbReserved(v),
44        }
45    }
46
47    #[must_use]
48    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
49    pub fn to_u8(self) -> u8 {
50        match self {
51            Self::Reserved => 0x00,
52            Self::IpMacStreamLocation => 0x01,
53            Self::DvbReserved(v) => v,
54        }
55    }
56
57    #[must_use]
58    /// Human-readable spec display name.
59    pub fn name(self) -> &'static str {
60        match self {
61            Self::Reserved => "Reserved",
62            Self::IpMacStreamLocation => "IP/MAC Stream Location",
63            Self::DvbReserved(_) => "DVB Reserved",
64        }
65    }
66}
67
68const OUTER_HEADER_LEN: usize = 3;
69const INT_FIXED_LEN: usize = 9;
70const LOOP_LEN_FIELD: usize = 2;
71const CRC_LEN: usize = 4;
72const MIN_SECTION_LEN: usize = OUTER_HEADER_LEN + INT_FIXED_LEN + LOOP_LEN_FIELD + CRC_LEN;
73
74const OFF_ACTION_TYPE: usize = 3;
75const OFF_PLATFORM_ID_HASH: usize = 4;
76const OFF_VERSION_BYTE: usize = 5;
77const OFF_SECTION_NUMBER: usize = 6;
78const OFF_LAST_SECTION_NUMBER: usize = 7;
79const OFF_PLATFORM_ID: usize = 8;
80const OFF_PROCESSING_ORDER: usize = 11;
81const OFF_PLATFORM_DESC_LEN: usize = 12;
82
83const RESERVED_NIBBLE: u8 = 0xF0;
84
85/// A target/operational descriptor-loop pair in the INT body loop
86/// (Tables 17/18, §8.4.4.1).
87#[derive(Debug, Clone, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89pub struct IntLoopEntry<'a> {
90    /// Target descriptor loop — raw descriptor bytes (after the 12-bit length
91    /// field).  Serializes as the typed descriptor sequence; `.raw()` yields the
92    /// wire bytes.
93    pub target_descriptors: DescriptorLoop<'a>,
94    /// Operational descriptor loop — raw descriptor bytes (after the 12-bit
95    /// length field).  Serializes as the typed descriptor sequence; `.raw()`
96    /// yields the wire bytes.
97    pub operational_descriptors: DescriptorLoop<'a>,
98}
99
100fn int_loop_entry_serialized_len(e: &IntLoopEntry) -> usize {
101    LOOP_LEN_FIELD + e.target_descriptors.len() + LOOP_LEN_FIELD + e.operational_descriptors.len()
102}
103
104/// IP/MAC Notification Table (INT), ETSI EN 301 192 v1.7.1 §8.4, Table 13.
105///
106/// The `loops` field is unfolded into typed [`IntLoopEntry`] instances
107/// (target + operational descriptor-loop pairs).
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize))]
110#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
111pub struct IntSection<'a> {
112    /// Semantics of this INT announcement — 0x01 = stream announcement/location.
113    pub action_type: IntActionType,
114    /// 8-bit XOR hash over the 24-bit platform_id.
115    pub platform_id_hash: u8,
116    /// 5-bit version_number.
117    pub version_number: u8,
118    /// `current_next_indicator` bit.
119    pub current_next_indicator: bool,
120    /// section_number within this sub-table.
121    pub section_number: u8,
122    /// last_section_number in this sub-table.
123    pub last_section_number: u8,
124    /// 24-bit platform identifier (TS 101 162) in the low 24 bits of a u32.
125    pub platform_id: u32,
126    /// Processing order relative to other INT sections.
127    pub processing_order: u8,
128    /// The `platform_descriptor_loop` (descriptors only, not the 2-byte length
129    /// field). Serializes as the typed descriptor sequence; `.raw()` yields the
130    /// wire bytes.
131    pub platform_descriptors: DescriptorLoop<'a>,
132    /// Target/operational descriptor-loop pairs — unfolded per Tables 17/18.
133    pub loops: Vec<IntLoopEntry<'a>>,
134}
135
136impl<'a> Parse<'a> for IntSection<'a> {
137    type Error = crate::error::Error;
138
139    fn parse(bytes: &'a [u8]) -> Result<Self> {
140        if bytes.len() < MIN_SECTION_LEN {
141            return Err(Error::BufferTooShort {
142                need: MIN_SECTION_LEN,
143                have: bytes.len(),
144                what: "IntSection",
145            });
146        }
147        if bytes[0] != TABLE_ID {
148            return Err(Error::UnexpectedTableId {
149                table_id: bytes[0],
150                what: "IntSection",
151                expected: &[TABLE_ID],
152            });
153        }
154
155        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
156        let total = super::check_section_length(
157            bytes.len(),
158            OUTER_HEADER_LEN,
159            section_length,
160            MIN_SECTION_LEN,
161        )?;
162
163        let action_type = IntActionType::from_u8(bytes[OFF_ACTION_TYPE]);
164        let platform_id_hash = bytes[OFF_PLATFORM_ID_HASH];
165        let version_byte = bytes[OFF_VERSION_BYTE];
166        let version_number = (version_byte >> 1) & 0x1F;
167        let current_next_indicator = (version_byte & 0x01) != 0;
168        let section_number = bytes[OFF_SECTION_NUMBER];
169        let last_section_number = bytes[OFF_LAST_SECTION_NUMBER];
170        let platform_id = ((bytes[OFF_PLATFORM_ID] as u32) << 16)
171            | ((bytes[OFF_PLATFORM_ID + 1] as u32) << 8)
172            | bytes[OFF_PLATFORM_ID + 2] as u32;
173        let processing_order = bytes[OFF_PROCESSING_ORDER];
174
175        let plat_desc_len = (((bytes[OFF_PLATFORM_DESC_LEN] & 0x0F) as usize) << 8)
176            | bytes[OFF_PLATFORM_DESC_LEN + 1] as usize;
177        let plat_desc_start = OFF_PLATFORM_DESC_LEN + LOOP_LEN_FIELD;
178        let plat_desc_end = plat_desc_start + plat_desc_len;
179        if plat_desc_end > total - CRC_LEN {
180            return Err(Error::SectionLengthOverflow {
181                declared: plat_desc_len,
182                available: (total - CRC_LEN).saturating_sub(plat_desc_start),
183            });
184        }
185        let platform_descriptors = DescriptorLoop::new(&bytes[plat_desc_start..plat_desc_end]);
186
187        let payload_end = total - CRC_LEN;
188        let mut pos = plat_desc_end;
189        let mut loops = Vec::new();
190        while pos < payload_end {
191            if pos + LOOP_LEN_FIELD > payload_end {
192                return Err(Error::BufferTooShort {
193                    need: pos + LOOP_LEN_FIELD,
194                    have: payload_end,
195                    what: "IntSection target_descriptor_loop length",
196                });
197            }
198            let target_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
199            let target_start = pos + LOOP_LEN_FIELD;
200            let target_end = target_start + target_len;
201            if target_end > payload_end {
202                return Err(Error::SectionLengthOverflow {
203                    declared: target_len,
204                    available: payload_end.saturating_sub(target_start),
205                });
206            }
207            let target_descriptors = DescriptorLoop::new(&bytes[target_start..target_end]);
208            pos = target_end;
209
210            if pos + LOOP_LEN_FIELD > payload_end {
211                return Err(Error::BufferTooShort {
212                    need: pos + LOOP_LEN_FIELD,
213                    have: payload_end,
214                    what: "IntSection operational_descriptor_loop length",
215                });
216            }
217            let op_len = (((bytes[pos] & 0x0F) as usize) << 8) | bytes[pos + 1] as usize;
218            let op_start = pos + LOOP_LEN_FIELD;
219            let op_end = op_start + op_len;
220            if op_end > payload_end {
221                return Err(Error::SectionLengthOverflow {
222                    declared: op_len,
223                    available: payload_end.saturating_sub(op_start),
224                });
225            }
226            let operational_descriptors = DescriptorLoop::new(&bytes[op_start..op_end]);
227            pos = op_end;
228
229            loops.push(IntLoopEntry {
230                target_descriptors,
231                operational_descriptors,
232            });
233        }
234
235        Ok(IntSection {
236            action_type,
237            platform_id_hash,
238            version_number,
239            current_next_indicator,
240            section_number,
241            last_section_number,
242            platform_id,
243            processing_order,
244            platform_descriptors,
245            loops,
246        })
247    }
248}
249
250impl Serialize for IntSection<'_> {
251    type Error = crate::error::Error;
252
253    fn serialized_len(&self) -> usize {
254        OUTER_HEADER_LEN
255            + INT_FIXED_LEN
256            + LOOP_LEN_FIELD
257            + self.platform_descriptors.len()
258            + self
259                .loops
260                .iter()
261                .map(int_loop_entry_serialized_len)
262                .sum::<usize>()
263            + CRC_LEN
264    }
265
266    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
267        let len = self.serialized_len();
268        if buf.len() < len {
269            return Err(Error::OutputBufferTooSmall {
270                need: len,
271                have: buf.len(),
272            });
273        }
274
275        let section_length = (len - OUTER_HEADER_LEN) as u16;
276        buf[0] = TABLE_ID;
277        buf[1] = super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F);
278        buf[2] = (section_length & 0xFF) as u8;
279
280        buf[OFF_ACTION_TYPE] = self.action_type.to_u8();
281        buf[OFF_PLATFORM_ID_HASH] = self.platform_id_hash;
282        buf[OFF_VERSION_BYTE] =
283            0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
284        buf[OFF_SECTION_NUMBER] = self.section_number;
285        buf[OFF_LAST_SECTION_NUMBER] = self.last_section_number;
286        buf[OFF_PLATFORM_ID] = ((self.platform_id >> 16) & 0xFF) as u8;
287        buf[OFF_PLATFORM_ID + 1] = ((self.platform_id >> 8) & 0xFF) as u8;
288        buf[OFF_PLATFORM_ID + 2] = (self.platform_id & 0xFF) as u8;
289        buf[OFF_PROCESSING_ORDER] = self.processing_order;
290
291        let pdl = self.platform_descriptors.len() as u16;
292        buf[OFF_PLATFORM_DESC_LEN] = RESERVED_NIBBLE | ((pdl >> 8) as u8 & 0x0F);
293        buf[OFF_PLATFORM_DESC_LEN + 1] = (pdl & 0xFF) as u8;
294
295        let plat_start = OFF_PLATFORM_DESC_LEN + LOOP_LEN_FIELD;
296        let plat_end = plat_start + self.platform_descriptors.len();
297        buf[plat_start..plat_end].copy_from_slice(self.platform_descriptors.raw());
298
299        let mut pos = plat_end;
300        for entry in &self.loops {
301            let tl = entry.target_descriptors.len() as u16;
302            buf[pos] = RESERVED_NIBBLE | ((tl >> 8) as u8 & 0x0F);
303            buf[pos + 1] = (tl & 0xFF) as u8;
304            pos += LOOP_LEN_FIELD;
305            buf[pos..pos + entry.target_descriptors.len()]
306                .copy_from_slice(entry.target_descriptors.raw());
307            pos += entry.target_descriptors.len();
308
309            let ol = entry.operational_descriptors.len() as u16;
310            buf[pos] = RESERVED_NIBBLE | ((ol >> 8) as u8 & 0x0F);
311            buf[pos + 1] = (ol & 0xFF) as u8;
312            pos += LOOP_LEN_FIELD;
313            buf[pos..pos + entry.operational_descriptors.len()]
314                .copy_from_slice(entry.operational_descriptors.raw());
315            pos += entry.operational_descriptors.len();
316        }
317
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        Ok(len)
322    }
323}
324impl<'a> crate::traits::TableDef<'a> for IntSection<'a> {
325    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
326    const NAME: &'static str = "IP_MAC_NOTIFICATION";
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn parse_happy_path_no_loops() {
335        let plat_desc: &[u8] = &[0x81, 0x02, 0xAB, 0xCD];
336        let int = IntSection {
337            action_type: IntActionType::IpMacStreamLocation,
338            platform_id_hash: 0x12 ^ 0x34,
339            version_number: 3,
340            current_next_indicator: true,
341            section_number: 0,
342            last_section_number: 0,
343            platform_id: 0x00_12_34,
344            processing_order: 0x00,
345            platform_descriptors: DescriptorLoop::new(plat_desc),
346            loops: Vec::new(),
347        };
348        let mut buf = vec![0u8; int.serialized_len()];
349        int.serialize_into(&mut buf).unwrap();
350        let parsed = IntSection::parse(&buf).unwrap();
351        assert_eq!(parsed.action_type, IntActionType::IpMacStreamLocation);
352        assert_eq!(parsed.platform_id, 0x00_12_34);
353        assert_eq!(parsed.platform_descriptors.raw(), plat_desc);
354        assert!(parsed.loops.is_empty());
355    }
356
357    #[test]
358    fn parse_happy_path_with_loops() {
359        let target_desc: &[u8] = &[0x09, 0x01, 0xAA];
360        let op_desc: &[u8] = &[0x0A, 0x01, 0xBB];
361        let int = IntSection {
362            action_type: IntActionType::IpMacStreamLocation,
363            platform_id_hash: 0x56,
364            version_number: 5,
365            current_next_indicator: false,
366            section_number: 1,
367            last_section_number: 1,
368            platform_id: 0x00_56_78,
369            processing_order: 0x01,
370            platform_descriptors: DescriptorLoop::new(&[]),
371            loops: vec![
372                IntLoopEntry {
373                    target_descriptors: DescriptorLoop::new(target_desc),
374                    operational_descriptors: DescriptorLoop::new(op_desc),
375                },
376                IntLoopEntry {
377                    target_descriptors: DescriptorLoop::new(&[]),
378                    operational_descriptors: DescriptorLoop::new(&[]),
379                },
380            ],
381        };
382        let mut buf = vec![0u8; int.serialized_len()];
383        int.serialize_into(&mut buf).unwrap();
384        let parsed = IntSection::parse(&buf).unwrap();
385        assert_eq!(parsed.loops.len(), 2);
386        assert_eq!(parsed.loops[0].target_descriptors.raw(), target_desc);
387        assert_eq!(parsed.loops[0].operational_descriptors.raw(), op_desc);
388        assert_eq!(parsed.loops[1].target_descriptors.len(), 0);
389        assert_eq!(parsed.loops[1].operational_descriptors.len(), 0);
390    }
391
392    #[test]
393    fn byte_exact_round_trip() {
394        let plat_desc: &[u8] = &[0x7C, 0x04, 0x01, 0x02, 0x03, 0x04];
395        let int = IntSection {
396            action_type: IntActionType::IpMacStreamLocation,
397            platform_id_hash: 0xAB,
398            version_number: 15,
399            current_next_indicator: true,
400            section_number: 2,
401            last_section_number: 3,
402            platform_id: 0x00_AB_CD,
403            processing_order: 0x00,
404            platform_descriptors: DescriptorLoop::new(plat_desc),
405            loops: vec![IntLoopEntry {
406                target_descriptors: DescriptorLoop::new(&[]),
407                operational_descriptors: DescriptorLoop::new(&[]),
408            }],
409        };
410        let mut buf = vec![0u8; int.serialized_len()];
411        int.serialize_into(&mut buf).unwrap();
412        let re = IntSection::parse(&buf).unwrap();
413        let mut buf2 = vec![0u8; re.serialized_len()];
414        re.serialize_into(&mut buf2).unwrap();
415        assert_eq!(buf, buf2, "byte-exact re-serialize");
416        assert_eq!(re, int);
417    }
418
419    #[test]
420    fn parse_rejects_wrong_table_id() {
421        let int = IntSection {
422            action_type: IntActionType::IpMacStreamLocation,
423            platform_id_hash: 0x00,
424            version_number: 0,
425            current_next_indicator: true,
426            section_number: 0,
427            last_section_number: 0,
428            platform_id: 0,
429            processing_order: 0,
430            platform_descriptors: DescriptorLoop::new(&[]),
431            loops: Vec::new(),
432        };
433        let mut buf = vec![0u8; int.serialized_len()];
434        int.serialize_into(&mut buf).unwrap();
435        buf[0] = 0x4B;
436        assert!(matches!(
437            IntSection::parse(&buf).unwrap_err(),
438            Error::UnexpectedTableId { table_id: 0x4B, .. }
439        ));
440    }
441
442    #[test]
443    fn parse_rejects_buffer_too_short() {
444        assert!(matches!(
445            IntSection::parse(&[TABLE_ID, 0xF0]).unwrap_err(),
446            Error::BufferTooShort {
447                what: "IntSection",
448                ..
449            }
450        ));
451    }
452
453    #[test]
454    fn serialize_rejects_too_small_output_buffer() {
455        let int = IntSection {
456            action_type: IntActionType::IpMacStreamLocation,
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: DescriptorLoop::new(&[]),
465            loops: Vec::new(),
466        };
467        let mut buf = vec![0u8; 2];
468        assert!(matches!(
469            int.serialize_into(&mut buf).unwrap_err(),
470            Error::OutputBufferTooSmall { .. }
471        ));
472    }
473
474    #[test]
475    fn parse_rejects_zero_section_length() {
476        let mut buf = vec![0u8; 64];
477        buf[0] = TABLE_ID;
478        buf[1] = 0xF0;
479        buf[2] = 0x00;
480        for b in &mut buf[3..] {
481            *b = 0xFF;
482        }
483        assert!(matches!(
484            IntSection::parse(&buf).unwrap_err(),
485            Error::SectionLengthOverflow { .. }
486        ));
487    }
488
489    #[test]
490    fn platform_id_24bit_boundary() {
491        let int = IntSection {
492            action_type: IntActionType::IpMacStreamLocation,
493            platform_id_hash: 0xFF,
494            version_number: 0,
495            current_next_indicator: true,
496            section_number: 0,
497            last_section_number: 0,
498            platform_id: 0x00FF_FFFF,
499            processing_order: 0x00,
500            platform_descriptors: DescriptorLoop::new(&[]),
501            loops: Vec::new(),
502        };
503        let mut buf = vec![0u8; int.serialized_len()];
504        int.serialize_into(&mut buf).unwrap();
505        let parsed = IntSection::parse(&buf).unwrap();
506        assert_eq!(parsed.platform_id, 0x00FF_FFFF);
507    }
508
509    #[test]
510    fn parse_handwritten_int_no_loops() {
511        let mut bytes: Vec<u8> = vec![
512            0x4C, 0xF0, 0x0F, 0x01, 0x00, 0xC7, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xF0, 0x00,
513        ];
514        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
515        bytes.extend_from_slice(&crc.to_be_bytes());
516        let int = IntSection::parse(&bytes).unwrap();
517        assert_eq!(int.action_type, IntActionType::IpMacStreamLocation);
518        assert_eq!(int.platform_id, 0x000001);
519        assert_eq!(int.version_number, 3);
520        assert!(int.current_next_indicator);
521        assert!(int.loops.is_empty());
522    }
523
524    #[test]
525    fn action_type_full_range_round_trip() {
526        for byte in 0u8..=0xFF {
527            let at = IntActionType::from_u8(byte);
528            assert_eq!(
529                at.to_u8(),
530                byte,
531                "IntActionType round-trip failed for {byte:#04x}"
532            );
533        }
534    }
535}