use crate::error::CodecError;
use bytes::Bytes;
pub const TABLE_ID: u8 = 0xFC;
pub const CMD_SPLICE_NULL: u8 = 0x00;
pub const CMD_SPLICE_SCHEDULE: u8 = 0x04;
pub const CMD_SPLICE_INSERT: u8 = 0x05;
pub const CMD_TIME_SIGNAL: u8 = 0x06;
pub const CMD_BANDWIDTH_RESERVATION: u8 = 0x07;
pub const CMD_PRIVATE_COMMAND: u8 = 0xFF;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpliceInfo {
pub command_type: u8,
pub pts_adjustment: u64,
pub pts: Option<u64>,
pub duration: Option<u64>,
pub event_id: Option<u32>,
pub cancel: bool,
pub out_of_network: bool,
pub raw: Bytes,
}
impl SpliceInfo {
pub fn absolute_pts(&self) -> Option<u64> {
self.pts.map(|p| (p + self.pts_adjustment) & ((1u64 << 33) - 1))
}
}
pub fn parse_splice_info_section(bytes: &[u8]) -> Result<SpliceInfo, CodecError> {
if bytes.len() < 17 {
return Err(CodecError::EndOfStream {
needed: 17,
remaining: bytes.len(),
});
}
if bytes[0] != TABLE_ID {
return Err(CodecError::Scte35Malformed("table_id != 0xFC"));
}
let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
let total_len = 3 + section_length;
if total_len > bytes.len() {
return Err(CodecError::EndOfStream {
needed: total_len,
remaining: bytes.len(),
});
}
if section_length < 15 {
return Err(CodecError::Scte35Malformed("section_length too short"));
}
let crc_offset = total_len - 4;
let computed = crc32_mpeg2(&bytes[..crc_offset]);
let wire = ((bytes[crc_offset] as u32) << 24)
| ((bytes[crc_offset + 1] as u32) << 16)
| ((bytes[crc_offset + 2] as u32) << 8)
| (bytes[crc_offset + 3] as u32);
if computed != wire {
return Err(CodecError::Scte35BadCrc { computed, wire });
}
let encrypted = bytes[4] & 0x80 != 0;
let pts_adjustment = (((bytes[4] & 0x01) as u64) << 32)
| ((bytes[5] as u64) << 24)
| ((bytes[6] as u64) << 16)
| ((bytes[7] as u64) << 8)
| (bytes[8] as u64);
let splice_command_length = (((bytes[11] & 0x0F) as usize) << 8) | bytes[12] as usize;
let splice_command_type = bytes[13];
let cmd_start = 14;
let cmd_end = if splice_command_length == 0xFFF {
cmd_start
} else {
cmd_start + splice_command_length
};
if cmd_end > crc_offset {
return Err(CodecError::Scte35Malformed("splice_command extends past section"));
}
let mut event_id = None;
let mut cancel = false;
let mut out_of_network = false;
let mut pts = None;
let mut duration = None;
if !encrypted {
match splice_command_type {
CMD_SPLICE_INSERT => {
let parsed = parse_splice_insert(&bytes[cmd_start..crc_offset])?;
event_id = Some(parsed.event_id);
cancel = parsed.cancel;
out_of_network = parsed.out_of_network;
pts = parsed.pts;
duration = parsed.duration;
}
CMD_TIME_SIGNAL => {
pts = parse_splice_time(&bytes[cmd_start..crc_offset])?;
}
_ => {}
}
}
let raw = Bytes::copy_from_slice(&bytes[..total_len]);
Ok(SpliceInfo {
command_type: splice_command_type,
pts_adjustment,
pts,
duration,
event_id,
cancel,
out_of_network,
raw,
})
}
struct ParsedSpliceInsert {
event_id: u32,
cancel: bool,
out_of_network: bool,
pts: Option<u64>,
duration: Option<u64>,
}
fn parse_splice_insert(body: &[u8]) -> Result<ParsedSpliceInsert, CodecError> {
if body.len() < 5 {
return Err(CodecError::EndOfStream {
needed: 5,
remaining: body.len(),
});
}
let event_id = ((body[0] as u32) << 24) | ((body[1] as u32) << 16) | ((body[2] as u32) << 8) | (body[3] as u32);
let cancel = body[4] & 0x80 != 0;
let mut pts = None;
let mut duration = None;
let mut out_of_network = false;
if !cancel {
if body.len() < 6 {
return Err(CodecError::EndOfStream {
needed: 6,
remaining: body.len(),
});
}
let flags = body[5];
out_of_network = flags & 0x80 != 0;
let program_splice = flags & 0x40 != 0;
let duration_flag = flags & 0x20 != 0;
let splice_immediate = flags & 0x10 != 0;
let mut cursor = 6;
if program_splice && !splice_immediate {
let (parsed_pts, consumed) = parse_splice_time_inline(&body[cursor..])?;
pts = parsed_pts;
cursor += consumed;
}
if !program_splice {
if cursor >= body.len() {
return Err(CodecError::EndOfStream {
needed: cursor + 1,
remaining: body.len(),
});
}
let component_count = body[cursor] as usize;
cursor += 1;
for _ in 0..component_count {
if cursor >= body.len() {
return Err(CodecError::EndOfStream {
needed: cursor + 1,
remaining: body.len(),
});
}
cursor += 1; if !splice_immediate {
let (_pts, consumed) = parse_splice_time_inline(&body[cursor..])?;
cursor += consumed;
}
}
}
if duration_flag {
if body.len() < cursor + 5 {
return Err(CodecError::EndOfStream {
needed: cursor + 5,
remaining: body.len(),
});
}
let dur = (((body[cursor] & 0x01) as u64) << 32)
| ((body[cursor + 1] as u64) << 24)
| ((body[cursor + 2] as u64) << 16)
| ((body[cursor + 3] as u64) << 8)
| (body[cursor + 4] as u64);
duration = Some(dur);
}
}
Ok(ParsedSpliceInsert {
event_id,
cancel,
out_of_network,
pts,
duration,
})
}
fn parse_splice_time(body: &[u8]) -> Result<Option<u64>, CodecError> {
Ok(parse_splice_time_inline(body)?.0)
}
fn parse_splice_time_inline(body: &[u8]) -> Result<(Option<u64>, usize), CodecError> {
if body.is_empty() {
return Err(CodecError::EndOfStream {
needed: 1,
remaining: 0,
});
}
let time_specified = body[0] & 0x80 != 0;
if !time_specified {
return Ok((None, 1));
}
if body.len() < 5 {
return Err(CodecError::EndOfStream {
needed: 5,
remaining: body.len(),
});
}
let pts = (((body[0] & 0x01) as u64) << 32)
| ((body[1] as u64) << 24)
| ((body[2] as u64) << 16)
| ((body[3] as u64) << 8)
| (body[4] as u64);
Ok((Some(pts), 5))
}
fn crc32_mpeg2(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= (byte as u32) << 24;
for _ in 0..8 {
crc = if crc & 0x8000_0000 != 0 {
(crc << 1) ^ 0x04C1_1DB7
} else {
crc << 1
};
}
}
crc
}
#[cfg(test)]
mod tests {
use super::*;
fn build_section(prefix: &[u8], command_body: &[u8], descriptors: &[u8]) -> Vec<u8> {
let total_minus_crc = prefix.len() + command_body.len() + 2 + descriptors.len();
let total = total_minus_crc + 4;
let section_length = total - 3;
let mut out = Vec::with_capacity(total);
out.push(TABLE_ID);
out.push(0x30 | ((section_length >> 8) as u8 & 0x0F));
out.push(section_length as u8);
out.extend_from_slice(&prefix[3..]);
let cmd_len = command_body.len();
out[11] = (out[11] & 0xF0) | ((cmd_len >> 8) as u8 & 0x0F);
out[12] = cmd_len as u8;
out.extend_from_slice(command_body);
out.push((descriptors.len() >> 8) as u8);
out.push(descriptors.len() as u8);
out.extend_from_slice(descriptors);
let crc = crc32_mpeg2(&out);
out.push((crc >> 24) as u8);
out.push((crc >> 16) as u8);
out.push((crc >> 8) as u8);
out.push(crc as u8);
out
}
fn default_prefix(command_type: u8) -> Vec<u8> {
vec![
TABLE_ID,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
0x00,
0x00, 0x00, 0xFF, 0xF0, 0x00, command_type,
]
}
#[test]
fn parses_splice_null() {
let prefix = default_prefix(CMD_SPLICE_NULL);
let bytes = build_section(&prefix, &[], &[]);
let info = parse_splice_info_section(&bytes).expect("splice_null parses");
assert_eq!(info.command_type, CMD_SPLICE_NULL);
assert!(info.pts.is_none());
assert!(info.duration.is_none());
assert!(info.event_id.is_none());
assert!(!info.cancel);
assert_eq!(&info.raw[..], &bytes[..]);
}
#[test]
fn parses_time_signal_with_pts() {
let prefix = default_prefix(CMD_TIME_SIGNAL);
let pts: u64 = 0x1_2345_6789;
let command_body = vec![
0xFE | ((pts >> 32) as u8 & 0x01),
(pts >> 24) as u8,
(pts >> 16) as u8,
(pts >> 8) as u8,
pts as u8,
];
let bytes = build_section(&prefix, &command_body, &[]);
let info = parse_splice_info_section(&bytes).expect("time_signal parses");
assert_eq!(info.command_type, CMD_TIME_SIGNAL);
assert_eq!(info.pts, Some(pts));
}
#[test]
fn parses_time_signal_immediate() {
let prefix = default_prefix(CMD_TIME_SIGNAL);
let command_body = vec![0x7F];
let bytes = build_section(&prefix, &command_body, &[]);
let info = parse_splice_info_section(&bytes).expect("time_signal immediate parses");
assert!(info.pts.is_none());
}
#[test]
fn parses_splice_insert_with_duration_and_pts() {
let prefix = default_prefix(CMD_SPLICE_INSERT);
let event_id: u32 = 0xDEAD_BEEF;
let pts: u64 = 0x0_FFFF_FFFF;
let duration: u64 = 0x1_0000_0000;
let command_body = vec![
(event_id >> 24) as u8,
(event_id >> 16) as u8,
(event_id >> 8) as u8,
event_id as u8,
0x7F, 0xEF, 0xFE | ((pts >> 32) as u8 & 0x01),
(pts >> 24) as u8,
(pts >> 16) as u8,
(pts >> 8) as u8,
pts as u8,
0xFE | ((duration >> 32) as u8 & 0x01),
(duration >> 24) as u8,
(duration >> 16) as u8,
(duration >> 8) as u8,
duration as u8,
0x00,
0x01, 0x00, 0x00, ];
let bytes = build_section(&prefix, &command_body, &[]);
let info = parse_splice_info_section(&bytes).expect("splice_insert parses");
assert_eq!(info.command_type, CMD_SPLICE_INSERT);
assert_eq!(info.event_id, Some(event_id));
assert!(!info.cancel);
assert!(info.out_of_network);
assert_eq!(info.pts, Some(pts));
assert_eq!(info.duration, Some(duration));
}
#[test]
fn parses_splice_insert_cancel_no_body() {
let prefix = default_prefix(CMD_SPLICE_INSERT);
let event_id: u32 = 0x1234_5678;
let command_body = vec![
(event_id >> 24) as u8,
(event_id >> 16) as u8,
(event_id >> 8) as u8,
event_id as u8,
0xFF, ];
let bytes = build_section(&prefix, &command_body, &[]);
let info = parse_splice_info_section(&bytes).expect("splice_insert cancel parses");
assert_eq!(info.event_id, Some(event_id));
assert!(info.cancel);
assert!(info.pts.is_none());
assert!(info.duration.is_none());
}
#[test]
fn rejects_bad_crc() {
let prefix = default_prefix(CMD_SPLICE_NULL);
let mut bytes = build_section(&prefix, &[], &[]);
bytes[3] ^= 0x01;
let err = parse_splice_info_section(&bytes).expect_err("bad CRC must reject");
assert!(matches!(err, CodecError::Scte35BadCrc { .. }), "{err:?}");
}
#[test]
fn rejects_truncated() {
let prefix = default_prefix(CMD_SPLICE_NULL);
let bytes = build_section(&prefix, &[], &[]);
let err = parse_splice_info_section(&bytes[..10]).expect_err("truncated must reject");
assert!(matches!(err, CodecError::EndOfStream { .. }), "{err:?}");
}
#[test]
fn rejects_wrong_table_id() {
let prefix = default_prefix(CMD_SPLICE_NULL);
let mut bytes = build_section(&prefix, &[], &[]);
bytes[0] = 0x00;
let err = parse_splice_info_section(&bytes).expect_err("wrong table_id");
assert!(matches!(err, CodecError::Scte35Malformed(_)), "{err:?}");
}
#[test]
fn pts_adjustment_round_trips() {
let mut prefix = default_prefix(CMD_SPLICE_NULL);
let pts_adj: u64 = 0x1_FFFF_FFFE;
prefix[4] = (prefix[4] & 0xFE) | ((pts_adj >> 32) as u8 & 0x01);
prefix[5] = (pts_adj >> 24) as u8;
prefix[6] = (pts_adj >> 16) as u8;
prefix[7] = (pts_adj >> 8) as u8;
prefix[8] = pts_adj as u8;
let bytes = build_section(&prefix, &[], &[]);
let info = parse_splice_info_section(&bytes).expect("parses");
assert_eq!(info.pts_adjustment, pts_adj);
}
#[test]
fn absolute_pts_wraps_at_33_bits() {
let info = SpliceInfo {
command_type: CMD_TIME_SIGNAL,
pts_adjustment: 1,
pts: Some((1u64 << 33) - 1),
duration: None,
event_id: None,
cancel: false,
out_of_network: false,
raw: Bytes::new(),
};
assert_eq!(info.absolute_pts(), Some(0));
}
#[test]
fn crc32_mpeg2_known_vector() {
assert_eq!(crc32_mpeg2(b"123456789"), 0x0376E6E7);
}
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
..ProptestConfig::default()
})]
#[test]
fn parse_handles_arbitrary_byte_mutations_without_panic(
byte_index in 0usize..64,
xor_mask in 1u8..=0xFFu8,
) {
let prefix = default_prefix(CMD_SPLICE_NULL);
let original = build_section(&prefix, &[], &[]);
assert!(
parse_splice_info_section(&original).is_ok(),
"harness baseline: unmutated section must parse",
);
let idx = byte_index % original.len();
let mut mutated = original.clone();
mutated[idx] ^= xor_mask;
match parse_splice_info_section(&mutated) {
Ok(_info) => {
let body = &mutated[..mutated.len() - 4];
let computed = crc32_mpeg2(body);
let wire = u32::from_be_bytes([
mutated[mutated.len() - 4],
mutated[mutated.len() - 3],
mutated[mutated.len() - 2],
mutated[mutated.len() - 1],
]);
assert_eq!(
computed, wire,
"if parse accepted, CRC must match: idx={idx} mask={xor_mask:#x}",
);
}
Err(CodecError::Scte35BadCrc { .. })
| Err(CodecError::Scte35Malformed(_))
| Err(CodecError::EndOfStream { .. }) => {
}
Err(other) => panic!(
"unexpected error variant for mutated section: {other:?} \
(idx={idx} mask={xor_mask:#x})",
),
}
}
}
}