Skip to main content

scte35_splice/
section.rs

1//! splice_info_section() — ANSI/SCTE 35 2023r1 §9.6, Table 5 (table_id 0xFC).
2//!
3//! The top-level SCTE 35 message: a short-form MPEG section carrying one splice
4//! command and a splice descriptor loop, trailed by an MPEG CRC-32 (the same
5//! `crc32_mpeg2` every PSI/SI section uses; verified §9.6.1 to be the MPEG
6//! Systems decoder CRC).
7//!
8//! ## Encryption (§9.6.1, §11.3)
9//!
10//! When `encrypted_packet == 1` the region from `splice_command_type` through
11//! `E_CRC_32` is encrypted; this parser does **not** decrypt. Such a section is
12//! kept as [`SpliceInfoSection::encrypted_payload`] (the raw encrypted bytes,
13//! verbatim) and the command / descriptor accessors return nothing — but the
14//! section still round-trips byte-for-byte and its CRC is recomputed on
15//! serialize. Clear sections (`encrypted_packet == 0`, the overwhelming common
16//! case) expose the typed command and descriptor loop.
17
18use crate::commands::AnyCommand;
19use crate::descriptors::{parse_loop, SpliceDescriptorIter};
20use crate::error::{Error, Result};
21use crate::time::PTS_MAX;
22use broadcast_common::{Parse, Serialize};
23
24/// `table_id` of a splice_info_section (§9.6.1).
25pub const TABLE_ID: u8 = 0xFC;
26
27/// Bytes of the fixed header from `table_id` through `splice_command_type`.
28const FIXED_HEADER_LEN: usize = 14;
29
30/// Bytes of the trailing CRC_32.
31const CRC_LEN: usize = 4;
32
33/// `tier` value (0xFFF) that downstream equipment shall ignore (§9.6.1).
34pub const TIER_IGNORE: u16 = 0x0FFF;
35
36/// The clear (decrypted) splice command and its splice descriptor loop, present
37/// only when `encrypted_packet == 0`.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40pub struct ClearPayload<'a> {
41    /// The typed splice command (§9.7).
42    pub command: AnyCommand<'a>,
43    /// Raw splice descriptor loop bytes; walk typed via
44    /// [`SpliceInfoSection::descriptors`].
45    pub descriptor_loop: &'a [u8],
46}
47
48/// splice_info_section() — §9.6, Table 5.
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize))]
51pub struct SpliceInfoSection<'a> {
52    /// 2-bit `sap_type` (§9.6.1, Table 6); `0x3` = SAP type not specified.
53    pub sap_type: u8,
54    /// 8-bit `protocol_version` (0 is the only valid value at present).
55    pub protocol_version: u8,
56    /// `encrypted_packet` flag (§9.6.1).
57    pub encrypted_packet: bool,
58    /// 6-bit `encryption_algorithm` (§11.3, Table 29). Undefined when not
59    /// encrypted.
60    pub encryption_algorithm: u8,
61    /// 33-bit `pts_adjustment` (90 kHz ticks), added (wrapping) to every
62    /// `pts_time` in the message.
63    pub pts_adjustment: u64,
64    /// 8-bit `cw_index`. Undefined when not encrypted.
65    pub cw_index: u8,
66    /// 12-bit `tier` (§9.6.1); 0xFFF shall be ignored downstream.
67    pub tier: u16,
68    /// The clear command + descriptor loop, present when `encrypted_packet`
69    /// is `false`.
70    pub clear: Option<ClearPayload<'a>>,
71    /// The raw encrypted region (`splice_command_type` through `E_CRC_32`),
72    /// present when `encrypted_packet` is `true`. Not decrypted by this crate.
73    pub encrypted_payload: Option<&'a [u8]>,
74}
75
76impl<'a> SpliceInfoSection<'a> {
77    /// Build a clear (unencrypted) section from a command and a raw descriptor
78    /// loop, with `pts_adjustment`/`tier` defaulted to zero / `0x3` sap_type.
79    #[must_use]
80    pub fn new_clear(command: AnyCommand<'a>, descriptor_loop: &'a [u8]) -> Self {
81        Self {
82            sap_type: 0x3,
83            protocol_version: 0,
84            encrypted_packet: false,
85            encryption_algorithm: 0,
86            pts_adjustment: 0,
87            cw_index: 0,
88            tier: 0,
89            clear: Some(ClearPayload {
90                command,
91                descriptor_loop,
92            }),
93            encrypted_payload: None,
94        }
95    }
96
97    /// Walk the splice descriptor loop, yielding typed
98    /// [`AnySpliceDescriptor`](crate::descriptors::AnySpliceDescriptor)s. Empty
99    /// when the section is encrypted.
100    #[must_use]
101    pub fn descriptors(&self) -> SpliceDescriptorIter<'a> {
102        match &self.clear {
103            Some(c) => parse_loop(c.descriptor_loop),
104            None => parse_loop(&[]),
105        }
106    }
107
108    /// The `pts_adjustment` decoded to a [`Duration`](core::time::Duration).
109    #[must_use]
110    pub fn pts_adjustment_duration(&self) -> core::time::Duration {
111        crate::time::ticks_to_duration(self.pts_adjustment)
112    }
113
114    /// Set `pts_adjustment` from a [`Duration`](core::time::Duration)
115    /// (truncating to 90 kHz ticks). Errors if it exceeds the 33-bit range.
116    pub fn set_pts_adjustment_duration(&mut self, d: core::time::Duration) -> Result<()> {
117        self.pts_adjustment =
118            crate::time::duration_to_ticks(d, PTS_MAX).ok_or(Error::InvalidValue {
119                field: "splice_info_section.pts_adjustment",
120                reason: "duration exceeds 33-bit 90 kHz range",
121            })?;
122        Ok(())
123    }
124
125    /// True if `tier == 0xFFF`, which downstream equipment shall ignore.
126    #[must_use]
127    pub fn tier_is_ignored(&self) -> bool {
128        self.tier == TIER_IGNORE
129    }
130
131    /// Bytes of the section body from `splice_command_type` through (but not
132    /// including) the trailing CRC_32 — the splice_command_length region plus
133    /// the descriptor loop and its 2-byte length, or the raw encrypted region.
134    fn payload_len(&self) -> usize {
135        match (&self.clear, self.encrypted_payload) {
136            (Some(c), _) => 1 + c.command.body_len() + 2 + c.descriptor_loop.len(),
137            (None, Some(enc)) => enc.len(),
138            (None, None) => 0,
139        }
140    }
141}
142
143impl<'a> Parse<'a> for SpliceInfoSection<'a> {
144    type Error = Error;
145    fn parse(bytes: &'a [u8]) -> Result<Self> {
146        if bytes.len() < FIXED_HEADER_LEN + CRC_LEN {
147            return Err(Error::BufferTooShort {
148                need: FIXED_HEADER_LEN + CRC_LEN,
149                have: bytes.len(),
150                what: "splice_info_section header",
151            });
152        }
153        if bytes[0] != TABLE_ID {
154            return Err(Error::UnexpectedTableId { table_id: bytes[0] });
155        }
156        let sap_type = (bytes[1] >> 4) & 0x03;
157        let section_length = (u16::from(bytes[1] & 0x0F) << 8) | u16::from(bytes[2]);
158        let total = 3 + section_length as usize;
159        if bytes.len() < total {
160            return Err(Error::LengthOverflow {
161                declared: section_length as usize,
162                available: bytes.len().saturating_sub(3),
163                what: "splice_info_section section_length",
164            });
165        }
166        // Verify CRC over the whole section (table_id..=CRC_32), prior to any
167        // decryption (§9.6.1). Guard total >= FIXED_HEADER_LEN + CRC_LEN before
168        // computing crc_pos so that section_length == 0 doesn't underflow (#216).
169        if total < FIXED_HEADER_LEN + CRC_LEN {
170            return Err(Error::BufferTooShort {
171                need: FIXED_HEADER_LEN + CRC_LEN,
172                have: total,
173                what: "splice_info_section body + CRC",
174            });
175        }
176        let crc_pos = total - CRC_LEN;
177        let (crc_region, crc_bytes) =
178            bytes[..total]
179                .split_last_chunk::<CRC_LEN>()
180                .ok_or(Error::BufferTooShort {
181                    need: FIXED_HEADER_LEN + CRC_LEN,
182                    have: bytes.len(),
183                    what: "splice_info_section header",
184                })?;
185        let expected = u32::from_be_bytes(*crc_bytes);
186        let computed = broadcast_common::crc32_mpeg2::compute(crc_region);
187        if computed != expected {
188            return Err(Error::CrcMismatch { computed, expected });
189        }
190
191        let protocol_version = bytes[3];
192        let encrypted_packet = bytes[4] & 0x80 != 0;
193        let encryption_algorithm = (bytes[4] >> 1) & 0x3F;
194        let pts_adjustment = (u64::from(bytes[4] & 0x01) << 32)
195            | (u64::from(bytes[5]) << 24)
196            | (u64::from(bytes[6]) << 16)
197            | (u64::from(bytes[7]) << 8)
198            | u64::from(bytes[8]);
199        let cw_index = bytes[9];
200        let tier = (u16::from(bytes[10]) << 4) | (u16::from(bytes[11]) >> 4);
201        let splice_command_length = (u16::from(bytes[11] & 0x0F) << 8) | u16::from(bytes[12]);
202        let splice_command_type = bytes[13];
203
204        let mut section = SpliceInfoSection {
205            sap_type,
206            protocol_version,
207            encrypted_packet,
208            encryption_algorithm,
209            pts_adjustment,
210            cw_index,
211            tier,
212            clear: None,
213            encrypted_payload: None,
214        };
215
216        // Region from splice_command_type (byte 13) through E_CRC_32/CRC_32.
217        let payload = &bytes[13..crc_pos];
218        if encrypted_packet {
219            // Keep the encrypted region (command_type + body + descriptors +
220            // E_CRC_32) verbatim; do not attempt to interpret it.
221            section.encrypted_payload = Some(payload);
222            return Ok(section);
223        }
224
225        // Clear: splice_command_type (1) is payload[0]; the command body is
226        // splice_command_length bytes; then descriptor_loop_length (2) + loop.
227        let scl = splice_command_length as usize;
228        // splice_command_length may be 0xFFF ("ignore"); when so, derive the
229        // command body length from the structure by parsing greedily up to the
230        // descriptor_loop_length. We require the actual value here; reject the
231        // backwards-compat sentinel as unparseable rather than guess.
232        if splice_command_length == 0x0FFF {
233            return Err(Error::InvalidValue {
234                field: "splice_info_section.splice_command_length",
235                reason:
236                    "0xFFF backwards-compat sentinel is not supported; provide the actual length",
237            });
238        }
239        // payload = [command_type][command_body(scl)][dll(2)][loop]
240        if payload.len() < 1 + scl + 2 {
241            return Err(Error::BufferTooShort {
242                need: 1 + scl + 2,
243                have: payload.len(),
244                what: "splice_info_section command + descriptor_loop_length",
245            });
246        }
247        let command_body = &payload[1..1 + scl];
248        let command = AnyCommand::dispatch(splice_command_type, command_body)?;
249
250        let dll_pos = 1 + scl;
251        let (dll_bytes, _) = payload
252            .get(dll_pos..)
253            .and_then(|s| s.split_first_chunk::<2>())
254            .ok_or(Error::BufferTooShort {
255                need: 1 + scl + 2,
256                have: payload.len(),
257                what: "splice_info_section command + descriptor_loop_length",
258            })?;
259        let descriptor_loop_length = usize::from(u16::from_be_bytes(*dll_bytes));
260        let loop_start = dll_pos + 2;
261        if payload.len() < loop_start + descriptor_loop_length {
262            return Err(Error::LengthOverflow {
263                declared: descriptor_loop_length,
264                available: payload.len().saturating_sub(loop_start),
265                what: "splice_info_section descriptor_loop_length",
266            });
267        }
268        let descriptor_loop = &payload[loop_start..loop_start + descriptor_loop_length];
269        // Any bytes between the descriptor loop and the CRC are alignment
270        // stuffing (§9.6.1); for a clear section there should be none, but we
271        // tolerate by ignoring them (they are re-derived on serialize).
272        section.clear = Some(ClearPayload {
273            command,
274            descriptor_loop,
275        });
276        Ok(section)
277    }
278}
279
280impl Serialize for SpliceInfoSection<'_> {
281    type Error = Error;
282    fn serialized_len(&self) -> usize {
283        // table_id(1) + flags/length(2) + protocol_version(1) + 5 + cw_index(1)
284        // + tier/scl(3) = 13 bytes before splice_command_type, then the payload
285        // (command_type..E_CRC_32 / descriptors), then CRC_32(4).
286        13 + self.payload_len() + CRC_LEN
287    }
288
289    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
290        let need = self.serialized_len();
291        if buf.len() < need {
292            return Err(Error::OutputBufferTooSmall {
293                need,
294                have: buf.len(),
295            });
296        }
297        if self.pts_adjustment > PTS_MAX {
298            return Err(Error::InvalidValue {
299                field: "splice_info_section.pts_adjustment",
300                reason: "exceeds 33-bit range",
301            });
302        }
303        if self.tier > 0x0FFF {
304            return Err(Error::InvalidValue {
305                field: "splice_info_section.tier",
306                reason: "exceeds 12-bit range",
307            });
308        }
309
310        // section_length counts the bytes after byte 2 up to and including CRC.
311        let section_length = (need - 3) as u16;
312        if section_length > 4093 {
313            return Err(Error::InvalidValue {
314                field: "splice_info_section.section_length",
315                reason: "exceeds 4093",
316            });
317        }
318
319        buf[0] = TABLE_ID;
320        // section_syntax_indicator=0, private_indicator=0, sap_type(2),
321        // section_length(12).
322        buf[1] = ((self.sap_type & 0x03) << 4) | ((section_length >> 8) as u8 & 0x0F);
323        buf[2] = (section_length & 0xFF) as u8;
324        buf[3] = self.protocol_version;
325        buf[4] = (u8::from(self.encrypted_packet) << 7)
326            | ((self.encryption_algorithm & 0x3F) << 1)
327            | ((self.pts_adjustment >> 32) as u8 & 0x01);
328        buf[5] = (self.pts_adjustment >> 24) as u8;
329        buf[6] = (self.pts_adjustment >> 16) as u8;
330        buf[7] = (self.pts_adjustment >> 8) as u8;
331        buf[8] = self.pts_adjustment as u8;
332        buf[9] = self.cw_index;
333        buf[10] = (self.tier >> 4) as u8;
334
335        // payload region (after splice_command_length byte at index 12).
336        match (&self.clear, self.encrypted_payload) {
337            (Some(c), _) => {
338                let scl = c.command.body_len() as u16;
339                buf[11] = (((self.tier & 0x0F) as u8) << 4) | ((scl >> 8) as u8 & 0x0F);
340                buf[12] = (scl & 0xFF) as u8;
341                buf[13] = c.command.command_type();
342                let mut pos = 14;
343                pos += c.command.serialize_body_into(&mut buf[pos..])?;
344                let dll = c.descriptor_loop.len() as u16;
345                buf[pos] = (dll >> 8) as u8;
346                buf[pos + 1] = (dll & 0xFF) as u8;
347                pos += 2;
348                buf[pos..pos + c.descriptor_loop.len()].copy_from_slice(c.descriptor_loop);
349                pos += c.descriptor_loop.len();
350                debug_assert_eq!(pos, need - CRC_LEN);
351            }
352            (None, Some(enc)) => {
353                // For an encrypted section, splice_command_length is not known
354                // to us (it is inside the encrypted region); write the
355                // backwards-compat 0xFFF sentinel and emit the region verbatim.
356                buf[11] = (((self.tier & 0x0F) as u8) << 4) | 0x0F;
357                buf[12] = 0xFF;
358                buf[13..13 + enc.len()].copy_from_slice(enc);
359            }
360            (None, None) => {
361                buf[11] = ((self.tier & 0x0F) as u8) << 4;
362                buf[12] = 0;
363                // No command/payload: splice_command_type would be missing.
364                return Err(Error::InvalidValue {
365                    field: "splice_info_section",
366                    reason: "neither a clear command nor an encrypted payload is present",
367                });
368            }
369        }
370
371        let crc_pos = need - CRC_LEN;
372        let crc = broadcast_common::crc32_mpeg2::compute(&buf[..crc_pos]);
373        buf[crc_pos..need].copy_from_slice(&crc.to_be_bytes());
374        Ok(need)
375    }
376}