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