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