use crate::descriptor::{iter_descriptors, DescriptorIter};
use crate::TsError;
pub const PAT_TABLE_ID: u8 = 0x00;
pub const CAT_TABLE_ID: u8 = 0x01;
pub const PMT_TABLE_ID: u8 = 0x02;
pub const TSDT_TABLE_ID: u8 = 0x03;
pub const SDT_ACTUAL_TABLE_ID: u8 = 0x42;
pub const SDT_OTHER_TABLE_ID: u8 = 0x46;
pub const EIT_ACTUAL_PF_TABLE_ID: u8 = 0x4E;
pub const EIT_OTHER_PF_TABLE_ID: u8 = 0x4F;
pub const EIT_ACTUAL_SCHEDULE_FIRST: u8 = 0x50;
pub const EIT_ACTUAL_SCHEDULE_LAST: u8 = 0x5F;
pub const EIT_OTHER_SCHEDULE_FIRST: u8 = 0x60;
pub const EIT_OTHER_SCHEDULE_LAST: u8 = 0x6F;
pub const PAT_PID: u16 = 0x0000;
pub const CAT_PID: u16 = 0x0001;
pub const TSDT_PID: u16 = 0x0002;
pub const SDT_PID: u16 = 0x0011;
pub const EIT_PID: u16 = 0x0012;
const SECTION_HEADER_LEN: usize = 8;
const SECTION_CRC_LEN: usize = 4;
pub const MAX_PSI_SECTION_LEN: usize = 3 + 0x3FD;
#[derive(Debug, Default, Clone)]
pub struct ProgramAssociationTable {
pub transport_stream_id: u16,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub programs: Vec<(u16, u16)>,
}
impl ProgramAssociationTable {
pub fn parse(section: &[u8]) -> Result<Self, TsError> {
let (hdr, body) = parse_section_header(section, PAT_TABLE_ID)?;
let mut programs = Vec::new();
let mut i = 0;
while i + 4 <= body.len() {
let program_number = u16::from_be_bytes([body[i], body[i + 1]]);
let pid = ((((body[i + 2] & 0b0001_1111) as u16) << 8) | (body[i + 3] as u16)) & 0x1FFF;
programs.push((program_number, pid));
i += 4;
}
Ok(Self {
transport_stream_id: hdr.table_id_extension,
version_number: hdr.version_number,
current_next_indicator: hdr.current_next_indicator,
section_number: hdr.section_number,
last_section_number: hdr.last_section_number,
programs,
})
}
}
#[derive(Debug, Clone)]
pub struct PmtStream {
pub stream_type: u8,
pub elementary_pid: u16,
pub descriptors: Vec<u8>,
}
impl PmtStream {
pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
iter_descriptors(&self.descriptors)
}
}
#[derive(Debug, Default, Clone)]
pub struct ProgramMapTable {
pub program_number: u16,
pub version_number: u8,
pub current_next_indicator: bool,
pub pcr_pid: u16,
pub program_info: Vec<u8>,
pub streams: Vec<PmtStream>,
}
impl ProgramMapTable {
pub fn iter_program_descriptors(&self) -> DescriptorIter<'_> {
iter_descriptors(&self.program_info)
}
pub fn parse(section: &[u8]) -> Result<Self, TsError> {
let (hdr, body) = parse_section_header(section, PMT_TABLE_ID)?;
if body.len() < 4 {
return Err(TsError::Truncated {
what: "PMT body",
have: body.len(),
need: 4,
});
}
let pcr_pid = u16::from_be_bytes([body[0] & 0b0001_1111, body[1]]);
let program_info_length = (u16::from_be_bytes([body[2] & 0b0000_1111, body[3]])) as usize;
let after_pcr: usize = 4;
let pi_end =
after_pcr
.checked_add(program_info_length)
.ok_or(TsError::SectionLengthOverrun {
claimed: program_info_length,
have: body.len() - after_pcr,
})?;
if pi_end > body.len() {
return Err(TsError::SectionLengthOverrun {
claimed: program_info_length,
have: body.len() - after_pcr,
});
}
let program_info = body[after_pcr..pi_end].to_vec();
let mut streams = Vec::new();
let mut i = pi_end;
while i + 5 <= body.len() {
let stream_type = body[i];
let elementary_pid = u16::from_be_bytes([body[i + 1] & 0b0001_1111, body[i + 2]]);
let es_info_length =
(u16::from_be_bytes([body[i + 3] & 0b0000_1111, body[i + 4]])) as usize;
let descr_start = i + 5;
let descr_end =
descr_start
.checked_add(es_info_length)
.ok_or(TsError::SectionLengthOverrun {
claimed: es_info_length,
have: body.len() - descr_start,
})?;
if descr_end > body.len() {
return Err(TsError::SectionLengthOverrun {
claimed: es_info_length,
have: body.len() - descr_start,
});
}
let descriptors = body[descr_start..descr_end].to_vec();
streams.push(PmtStream {
stream_type,
elementary_pid,
descriptors,
});
i = descr_end;
}
Ok(Self {
program_number: hdr.table_id_extension,
version_number: hdr.version_number,
current_next_indicator: hdr.current_next_indicator,
pcr_pid,
program_info,
streams,
})
}
}
#[derive(Debug, Default, Clone)]
pub struct ConditionalAccessTable {
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub descriptors: Vec<u8>,
}
impl ConditionalAccessTable {
pub fn parse(section: &[u8]) -> Result<Self, TsError> {
let (hdr, body) = parse_section_header(section, CAT_TABLE_ID)?;
Ok(Self {
version_number: hdr.version_number,
current_next_indicator: hdr.current_next_indicator,
section_number: hdr.section_number,
last_section_number: hdr.last_section_number,
descriptors: body.to_vec(),
})
}
pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
iter_descriptors(&self.descriptors)
}
}
#[derive(Debug, Default, Clone)]
pub struct TransportStreamDescriptionTable {
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub descriptors: Vec<u8>,
}
impl TransportStreamDescriptionTable {
pub fn parse(section: &[u8]) -> Result<Self, TsError> {
let (hdr, body) = parse_section_header(section, TSDT_TABLE_ID)?;
Ok(Self {
version_number: hdr.version_number,
current_next_indicator: hdr.current_next_indicator,
section_number: hdr.section_number,
last_section_number: hdr.last_section_number,
descriptors: body.to_vec(),
})
}
pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
iter_descriptors(&self.descriptors)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunningStatus {
Undefined,
NotRunning,
StartsSoon,
Pausing,
Running,
OffAir,
Reserved(u8),
}
impl RunningStatus {
pub fn from_bits(value: u8) -> Self {
match value & 0b0000_0111 {
0 => RunningStatus::Undefined,
1 => RunningStatus::NotRunning,
2 => RunningStatus::StartsSoon,
3 => RunningStatus::Pausing,
4 => RunningStatus::Running,
5 => RunningStatus::OffAir,
other => RunningStatus::Reserved(other),
}
}
}
#[derive(Debug, Clone)]
pub struct SdtService {
pub service_id: u16,
pub eit_schedule_flag: bool,
pub eit_present_following_flag: bool,
pub running_status: RunningStatus,
pub free_ca_mode: bool,
pub descriptors: Vec<u8>,
}
impl SdtService {
pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
iter_descriptors(&self.descriptors)
}
}
#[derive(Debug, Default, Clone)]
pub struct ServiceDescriptionTable {
pub transport_stream_id: u16,
pub other_transport_stream: bool,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub original_network_id: u16,
pub services: Vec<SdtService>,
}
impl ServiceDescriptionTable {
pub fn parse(section: &[u8]) -> Result<Self, TsError> {
let table_id = section.first().copied().unwrap_or(0);
let other_transport_stream = match table_id {
SDT_ACTUAL_TABLE_ID => false,
SDT_OTHER_TABLE_ID => true,
_ => {
return Err(TsError::Unsupported(
"PSI table_id does not match expected value",
))
}
};
let (hdr, body) = parse_section_header(section, table_id)?;
if body.len() < 3 {
return Err(TsError::Truncated {
what: "SDT body",
have: body.len(),
need: 3,
});
}
let original_network_id = u16::from_be_bytes([body[0], body[1]]);
let mut services = Vec::new();
let mut i = 3;
while i + 5 <= body.len() {
let service_id = u16::from_be_bytes([body[i], body[i + 1]]);
let b = body[i + 2];
let eit_schedule_flag = (b & 0b0000_0010) != 0;
let eit_present_following_flag = (b & 0b0000_0001) != 0;
let b3 = body[i + 3];
let running_status = RunningStatus::from_bits(b3 >> 5);
let free_ca_mode = (b3 & 0b0001_0000) != 0;
let descriptors_length = (u16::from_be_bytes([b3 & 0b0000_1111, body[i + 4]])) as usize;
let descr_start = i + 5;
let descr_end = descr_start.checked_add(descriptors_length).ok_or(
TsError::SectionLengthOverrun {
claimed: descriptors_length,
have: body.len() - descr_start,
},
)?;
if descr_end > body.len() {
return Err(TsError::SectionLengthOverrun {
claimed: descriptors_length,
have: body.len() - descr_start,
});
}
services.push(SdtService {
service_id,
eit_schedule_flag,
eit_present_following_flag,
running_status,
free_ca_mode,
descriptors: body[descr_start..descr_end].to_vec(),
});
i = descr_end;
}
Ok(Self {
transport_stream_id: hdr.table_id_extension,
other_transport_stream,
version_number: hdr.version_number,
current_next_indicator: hdr.current_next_indicator,
section_number: hdr.section_number,
last_section_number: hdr.last_section_number,
original_network_id,
services,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EitDateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub mjd: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EitDuration {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
}
impl EitDuration {
pub fn as_seconds(&self) -> u32 {
(self.hours as u32) * 3600 + (self.minutes as u32) * 60 + (self.seconds as u32)
}
}
fn bcd_byte(b: u8) -> u8 {
(b >> 4) * 10 + (b & 0x0F)
}
fn decode_eit_start_time(bytes: [u8; 5]) -> Option<EitDateTime> {
if bytes == [0xFF, 0xFF, 0xFF, 0xFF, 0xFF] {
return None;
}
let mjd = u16::from_be_bytes([bytes[0], bytes[1]]);
let mjd_i = mjd as i64;
let yp = ((mjd_i - 15078) * 100 - 20) / 36525; let yp_days = (yp * 36525) / 100; let mp = ((mjd_i - 14956 - yp_days) * 10000 - 1) / 306001;
let mp_days = (mp * 306001) / 10000; let d = mjd_i - 14956 - yp_days - mp_days;
let k: i64 = if mp == 14 || mp == 15 { 1 } else { 0 };
let y = yp + k;
let m = mp - 1 - k * 12;
let year = (1900 + y) as u16;
let month = m as u8;
let day = d as u8;
let hour = bcd_byte(bytes[2]);
let minute = bcd_byte(bytes[3]);
let second = bcd_byte(bytes[4]);
Some(EitDateTime {
year,
month,
day,
hour,
minute,
second,
mjd,
})
}
#[derive(Debug, Clone)]
pub struct EitEvent {
pub event_id: u16,
pub start_time: Option<EitDateTime>,
pub duration: EitDuration,
pub running_status: RunningStatus,
pub free_ca_mode: bool,
pub descriptors: Vec<u8>,
}
impl EitEvent {
pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
iter_descriptors(&self.descriptors)
}
}
#[derive(Debug, Default, Clone)]
pub struct EventInformationTable {
pub service_id: u16,
pub other_transport_stream: bool,
pub schedule: bool,
pub table_id: u8,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub transport_stream_id: u16,
pub original_network_id: u16,
pub segment_last_section_number: u8,
pub last_table_id: u8,
pub events: Vec<EitEvent>,
}
impl EventInformationTable {
pub fn is_eit_table_id(table_id: u8) -> bool {
matches!(table_id, EIT_ACTUAL_PF_TABLE_ID | EIT_OTHER_PF_TABLE_ID)
|| (EIT_ACTUAL_SCHEDULE_FIRST..=EIT_ACTUAL_SCHEDULE_LAST).contains(&table_id)
|| (EIT_OTHER_SCHEDULE_FIRST..=EIT_OTHER_SCHEDULE_LAST).contains(&table_id)
}
pub fn parse(section: &[u8]) -> Result<Self, TsError> {
let table_id = section.first().copied().unwrap_or(0);
if !Self::is_eit_table_id(table_id) {
return Err(TsError::Unsupported(
"PSI table_id does not match expected value",
));
}
let other_transport_stream = table_id == EIT_OTHER_PF_TABLE_ID
|| (EIT_OTHER_SCHEDULE_FIRST..=EIT_OTHER_SCHEDULE_LAST).contains(&table_id);
let schedule = (EIT_ACTUAL_SCHEDULE_FIRST..=EIT_OTHER_SCHEDULE_LAST).contains(&table_id);
let (hdr, body) = parse_section_header(section, table_id)?;
if body.len() < 6 {
return Err(TsError::Truncated {
what: "EIT body",
have: body.len(),
need: 6,
});
}
let transport_stream_id = u16::from_be_bytes([body[0], body[1]]);
let original_network_id = u16::from_be_bytes([body[2], body[3]]);
let segment_last_section_number = body[4];
let last_table_id = body[5];
let mut events = Vec::new();
let mut i = 6;
while i + 12 <= body.len() {
let event_id = u16::from_be_bytes([body[i], body[i + 1]]);
let start_time = decode_eit_start_time([
body[i + 2],
body[i + 3],
body[i + 4],
body[i + 5],
body[i + 6],
]);
let duration = EitDuration {
hours: bcd_byte(body[i + 7]),
minutes: bcd_byte(body[i + 8]),
seconds: bcd_byte(body[i + 9]),
};
let b10 = body[i + 10];
let running_status = RunningStatus::from_bits(b10 >> 5);
let free_ca_mode = (b10 & 0b0001_0000) != 0;
let descriptors_length =
(u16::from_be_bytes([b10 & 0b0000_1111, body[i + 11]])) as usize;
let descr_start = i + 12;
let descr_end = descr_start.checked_add(descriptors_length).ok_or(
TsError::SectionLengthOverrun {
claimed: descriptors_length,
have: body.len() - descr_start,
},
)?;
if descr_end > body.len() {
return Err(TsError::SectionLengthOverrun {
claimed: descriptors_length,
have: body.len() - descr_start,
});
}
events.push(EitEvent {
event_id,
start_time,
duration,
running_status,
free_ca_mode,
descriptors: body[descr_start..descr_end].to_vec(),
});
i = descr_end;
}
Ok(Self {
service_id: hdr.table_id_extension,
other_transport_stream,
schedule,
table_id,
version_number: hdr.version_number,
current_next_indicator: hdr.current_next_indicator,
section_number: hdr.section_number,
last_section_number: hdr.last_section_number,
transport_stream_id,
original_network_id,
segment_last_section_number,
last_table_id,
events,
})
}
}
#[derive(Debug, Default)]
pub struct PsiSectionAssembler {
in_flight: Vec<u8>,
target_len: Option<usize>,
last_cc: Option<u8>,
}
impl PsiSectionAssembler {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.in_flight.clear();
self.target_len = None;
self.last_cc = None;
}
pub fn feed(
&mut self,
payload: &[u8],
pusi: bool,
continuity_counter: u8,
) -> Result<Vec<Vec<u8>>, TsError> {
let cc = continuity_counter & 0x0F;
if let Some(prev) = self.last_cc {
let expected = (prev + 1) & 0x0F;
if cc != expected {
self.in_flight.clear();
self.target_len = None;
}
}
self.last_cc = Some(cc);
let mut out = Vec::new();
let mut rest: &[u8] = payload;
if pusi {
if rest.is_empty() {
return Ok(out);
}
let ptr = rest[0] as usize;
rest = &rest[1..];
if ptr > rest.len() {
self.in_flight.clear();
self.target_len = None;
return Ok(out);
}
let (tail_of_prev, after_ptr) = rest.split_at(ptr);
if !self.in_flight.is_empty() || self.target_len.is_some() {
if let Some(section) = self.extend_in_flight(tail_of_prev)? {
out.push(section);
}
self.in_flight.clear();
self.target_len = None;
}
rest = after_ptr;
} else if self.target_len.is_none() {
return Ok(out);
} else {
if let Some(section) = self.extend_in_flight(rest)? {
out.push(section);
}
return Ok(out);
}
while !rest.is_empty() {
if rest[0] == 0xFF {
break;
}
if rest.len() < 3 {
self.in_flight.extend_from_slice(rest);
self.target_len = None;
break;
}
let section_length =
((((rest[1] & 0b0000_1111) as usize) << 8) | (rest[2] as usize)) & 0x0FFF;
let total = 3 + section_length;
if total > MAX_PSI_SECTION_LEN {
self.in_flight.clear();
self.target_len = None;
return Err(TsError::SectionLengthOverrun {
claimed: section_length,
have: rest.len() - 3,
});
}
if rest.len() >= total {
out.push(rest[..total].to_vec());
rest = &rest[total..];
} else {
self.in_flight.clear();
self.in_flight.extend_from_slice(rest);
self.target_len = Some(total);
break;
}
}
Ok(out)
}
fn extend_in_flight(&mut self, bytes: &[u8]) -> Result<Option<Vec<u8>>, TsError> {
if bytes.is_empty() {
return Ok(None);
}
if self.target_len.is_none() {
let want = 3usize.saturating_sub(self.in_flight.len());
let take = want.min(bytes.len());
self.in_flight.extend_from_slice(&bytes[..take]);
if self.in_flight.len() < 3 {
return Ok(None);
}
let section_length = ((((self.in_flight[1] & 0b0000_1111) as usize) << 8)
| (self.in_flight[2] as usize))
& 0x0FFF;
let total = 3 + section_length;
if total > MAX_PSI_SECTION_LEN {
self.in_flight.clear();
self.target_len = None;
return Err(TsError::SectionLengthOverrun {
claimed: section_length,
have: bytes.len() - take,
});
}
self.target_len = Some(total);
return self.extend_in_flight(&bytes[take..]);
}
let target = self.target_len.expect("checked above");
let want = target.saturating_sub(self.in_flight.len());
let take = want.min(bytes.len());
self.in_flight.extend_from_slice(&bytes[..take]);
if self.in_flight.len() < target {
return Ok(None);
}
let done = std::mem::take(&mut self.in_flight);
self.target_len = None;
Ok(Some(done))
}
}
struct SectionHeader {
table_id_extension: u16,
version_number: u8,
current_next_indicator: bool,
section_number: u8,
last_section_number: u8,
}
fn parse_section_header(
section: &[u8],
expected_table_id: u8,
) -> Result<(SectionHeader, &[u8]), TsError> {
if section.len() < SECTION_HEADER_LEN + SECTION_CRC_LEN {
return Err(TsError::Truncated {
what: "PSI section header",
have: section.len(),
need: SECTION_HEADER_LEN + SECTION_CRC_LEN,
});
}
let table_id = section[0];
if table_id != expected_table_id {
return Err(TsError::Unsupported(
"PSI table_id does not match expected value",
));
}
let b1 = section[1];
let b2 = section[2];
let section_length = ((((b1 & 0b0000_1111) as usize) << 8) | (b2 as usize)) & 0x0FFF;
let total = 3 + section_length;
if total > section.len() {
return Err(TsError::SectionLengthOverrun {
claimed: section_length,
have: section.len() - 3,
});
}
let section = §ion[..total];
let crc_pos = total - SECTION_CRC_LEN;
let computed = mpeg2_crc32(§ion[..crc_pos]);
let header_crc = u32::from_be_bytes([
section[crc_pos],
section[crc_pos + 1],
section[crc_pos + 2],
section[crc_pos + 3],
]);
if computed != header_crc {
return Err(TsError::PsiCrcMismatch {
header: header_crc,
computed,
});
}
let table_id_extension = u16::from_be_bytes([section[3], section[4]]);
let b5 = section[5];
let version_number = (b5 >> 1) & 0b0001_1111;
let current_next_indicator = (b5 & 0b0000_0001) != 0;
let section_number = section[6];
let last_section_number = section[7];
let body = §ion[SECTION_HEADER_LEN..crc_pos];
Ok((
SectionHeader {
table_id_extension,
version_number,
current_next_indicator,
section_number,
last_section_number,
},
body,
))
}
pub fn mpeg2_crc32(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &b in bytes {
crc ^= (b as u32) << 24;
for _ in 0..8 {
if (crc & 0x8000_0000) != 0 {
crc = (crc << 1) ^ 0x04C1_1DB7;
} else {
crc <<= 1;
}
}
}
crc
}
pub fn iter_sections(ts_payload: &[u8]) -> SectionIter<'_> {
if ts_payload.is_empty() {
return SectionIter { rest: &[][..] };
}
let ptr = ts_payload[0] as usize;
let start = 1 + ptr;
if start > ts_payload.len() {
return SectionIter { rest: &[][..] };
}
SectionIter {
rest: &ts_payload[start..],
}
}
#[derive(Debug)]
pub struct SectionIter<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for SectionIter<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
if self.rest.len() < 3 {
return None;
}
if self.rest[0] == 0xFF {
return None;
}
let section_length =
((((self.rest[1] & 0b0000_1111) as usize) << 8) | (self.rest[2] as usize)) & 0x0FFF;
let total = 3 + section_length;
if total > self.rest.len() {
return None;
}
let (head, tail) = self.rest.split_at(total);
self.rest = tail;
Some(head)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::DescriptorBody;
fn build_pat_section(tsid: u16, version: u8, programs: &[(u16, u16)]) -> Vec<u8> {
let body_len = programs.len() * 4;
let section_length = 5 + body_len + 4;
let mut s = Vec::with_capacity(3 + section_length);
s.push(PAT_TABLE_ID);
let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
s.push(len_hi);
s.push((section_length & 0xFF) as u8);
s.extend_from_slice(&tsid.to_be_bytes());
s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
s.push(0); s.push(0); for (prog, pid) in programs {
s.extend_from_slice(&prog.to_be_bytes());
s.push(0b1110_0000 | ((pid >> 8) & 0x1F) as u8);
s.push((pid & 0xFF) as u8);
}
let crc = mpeg2_crc32(&s);
s.extend_from_slice(&crc.to_be_bytes());
s
}
fn build_pmt_section(
program_number: u16,
version: u8,
pcr_pid: u16,
program_info: &[u8],
streams: &[(u8, u16, &[u8])],
) -> Vec<u8> {
let body_len: usize =
4 + program_info.len() + streams.iter().map(|(_, _, d)| 5 + d.len()).sum::<usize>();
let section_length = 5 + body_len + 4;
let mut s = Vec::with_capacity(3 + section_length);
s.push(PMT_TABLE_ID);
let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
s.push(len_hi);
s.push((section_length & 0xFF) as u8);
s.extend_from_slice(&program_number.to_be_bytes());
s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
s.push(0);
s.push(0);
s.push(0b1110_0000 | ((pcr_pid >> 8) & 0x1F) as u8);
s.push((pcr_pid & 0xFF) as u8);
let pil = program_info.len() as u16;
s.push(0b1111_0000 | ((pil >> 8) & 0x0F) as u8);
s.push((pil & 0xFF) as u8);
s.extend_from_slice(program_info);
for (stype, epid, descr) in streams {
s.push(*stype);
s.push(0b1110_0000 | ((*epid >> 8) & 0x1F) as u8);
s.push((*epid & 0xFF) as u8);
let el = descr.len() as u16;
s.push(0b1111_0000 | ((el >> 8) & 0x0F) as u8);
s.push((el & 0xFF) as u8);
s.extend_from_slice(descr);
}
let crc = mpeg2_crc32(&s);
s.extend_from_slice(&crc.to_be_bytes());
s
}
#[test]
fn mpeg2_crc32_known_vector() {
let crc = mpeg2_crc32(b"123456789");
assert_eq!(crc, 0x0376_E6E7);
}
#[test]
fn pat_one_program_round_trip() {
let section = build_pat_section(1, 3, &[(1, 0x100)]);
let pat = ProgramAssociationTable::parse(§ion).unwrap();
assert_eq!(pat.transport_stream_id, 1);
assert_eq!(pat.version_number, 3);
assert!(pat.current_next_indicator);
assert_eq!(pat.programs, vec![(1, 0x100)]);
}
#[test]
fn pmt_avc_ac3_pgs_round_trip() {
let avc_descr: &[u8] = &[0x52, 0x01, 0x00]; let ac3_descr: &[u8] = &[0x6A, 0x01, 0x80];
let pgs_descr: &[u8] = &[];
let section = build_pmt_section(
1,
5,
0x100,
&[],
&[
(0x1B, 0x1011, avc_descr),
(0x81, 0x1100, ac3_descr),
(0x90, 0x1200, pgs_descr),
],
);
let pmt = ProgramMapTable::parse(§ion).unwrap();
assert_eq!(pmt.program_number, 1);
assert_eq!(pmt.version_number, 5);
assert!(pmt.current_next_indicator);
assert_eq!(pmt.pcr_pid, 0x100);
assert!(pmt.program_info.is_empty());
assert_eq!(pmt.streams.len(), 3);
assert_eq!(pmt.streams[0].stream_type, 0x1B);
assert_eq!(pmt.streams[0].elementary_pid, 0x1011);
assert_eq!(pmt.streams[0].descriptors, avc_descr);
assert_eq!(pmt.streams[1].stream_type, 0x81);
assert_eq!(pmt.streams[1].elementary_pid, 0x1100);
assert_eq!(pmt.streams[1].descriptors, ac3_descr);
assert_eq!(pmt.streams[2].stream_type, 0x90);
assert_eq!(pmt.streams[2].elementary_pid, 0x1200);
assert!(pmt.streams[2].descriptors.is_empty());
}
#[test]
fn psi_crc_corruption_is_rejected() {
let mut section = build_pat_section(7, 0, &[(1, 0x100)]);
section[3] ^= 0x01;
let err = ProgramAssociationTable::parse(§ion).unwrap_err();
match err {
TsError::PsiCrcMismatch { .. } => {}
other => panic!("expected PsiCrcMismatch, got {other:?}"),
}
}
#[test]
fn iter_sections_skips_pointer_field_and_stuffing() {
let section = build_pat_section(1, 0, &[(1, 0x100)]);
let mut payload = Vec::new();
payload.push(0u8); payload.extend_from_slice(§ion);
payload.extend(std::iter::repeat(0xFF).take(10));
let mut it = iter_sections(&payload);
let s = it.next().expect("section");
assert_eq!(s, section);
assert!(it.next().is_none());
}
#[test]
fn iter_sections_with_nonzero_pointer_field() {
let section = build_pat_section(1, 0, &[(1, 0x100)]);
let mut payload = Vec::new();
payload.push(3u8); payload.extend_from_slice(&[0xAA, 0xBB, 0xCC]); payload.extend_from_slice(§ion);
let s = iter_sections(&payload).next().expect("section");
assert_eq!(s, section);
}
#[test]
fn pmt_per_stream_descriptors_decode_iso639() {
let es_descr: &[u8] = &[0x0A, 0x08, b'e', b'n', b'g', 0x00, b'j', b'p', b'n', 0x02];
let section = build_pmt_section(1, 0, 0x100, &[], &[(0x81, 0x1100, es_descr)]);
let pmt = ProgramMapTable::parse(§ion).unwrap();
assert_eq!(pmt.streams.len(), 1);
let descriptors: Vec<_> = pmt.streams[0]
.iter_descriptors()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(descriptors.len(), 1);
assert_eq!(descriptors[0].tag, 0x0A);
match &descriptors[0].body {
crate::descriptor::DescriptorBody::Iso639Language(langs) => {
assert_eq!(langs.len(), 2);
assert_eq!(&langs[0].language, b"eng");
assert_eq!(&langs[1].language, b"jpn");
assert_eq!(langs[1].audio_type, 2);
}
other => panic!("expected Iso639Language, got {other:?}"),
}
}
#[test]
fn pmt_program_info_descriptors_decode_registration() {
let program_info: &[u8] = &[0x05, 0x04, b'H', b'D', b'M', b'V'];
let section = build_pmt_section(1, 0, 0x100, program_info, &[(0x1B, 0x1011, &[])]);
let pmt = ProgramMapTable::parse(§ion).unwrap();
let descriptors: Vec<_> = pmt
.iter_program_descriptors()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(descriptors.len(), 1);
match &descriptors[0].body {
crate::descriptor::DescriptorBody::Registration {
format_identifier, ..
} => {
assert_eq!(format_identifier, b"HDMV");
}
other => panic!("expected Registration, got {other:?}"),
}
}
#[test]
fn pat_network_pid_program_zero() {
let section = build_pat_section(2, 0, &[(0, 0x10), (1, 0x100)]);
let pat = ProgramAssociationTable::parse(§ion).unwrap();
assert_eq!(pat.programs[0], (0, 0x10));
assert_eq!(pat.programs[1], (1, 0x100));
}
fn build_cat_section(version: u8, descriptors: &[u8]) -> Vec<u8> {
let section_length = 5 + descriptors.len() + 4;
let mut s = Vec::with_capacity(3 + section_length);
s.push(CAT_TABLE_ID);
let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
s.push(len_hi);
s.push((section_length & 0xFF) as u8);
s.push(0xFF);
s.push(0xFF);
s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
s.push(0);
s.push(0);
s.extend_from_slice(descriptors);
let crc = mpeg2_crc32(&s);
s.extend_from_slice(&crc.to_be_bytes());
s
}
#[test]
fn cat_round_trip_single_ca_descriptor() {
let ca_descr: &[u8] = &[0x09, 0x06, 0x05, 0x00, 0xE1, 0x23, 0xCA, 0xFE];
let section = build_cat_section(3, ca_descr);
let cat = ConditionalAccessTable::parse(§ion).unwrap();
assert_eq!(cat.version_number, 3);
assert!(cat.current_next_indicator);
let descrs: Vec<_> = cat.iter_descriptors().collect::<Result<_, _>>().unwrap();
assert_eq!(descrs.len(), 1);
match &descrs[0].body {
crate::descriptor::DescriptorBody::Ca(ca) => {
assert_eq!(ca.ca_system_id, 0x0500);
assert_eq!(ca.ca_pid, 0x0123);
assert_eq!(ca.private_data, &[0xCA, 0xFE]);
}
other => panic!("expected CA, got {other:?}"),
}
}
#[test]
fn cat_rejects_wrong_table_id() {
let section = build_pat_section(1, 0, &[(1, 0x100)]);
let err = ConditionalAccessTable::parse(§ion).unwrap_err();
match err {
TsError::Unsupported(_) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
fn build_tsdt_section(version: u8, section_number: u8, descriptors: &[u8]) -> Vec<u8> {
let section_length = 5 + descriptors.len() + 4;
let mut s = Vec::with_capacity(3 + section_length);
s.push(TSDT_TABLE_ID);
let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
s.push(len_hi);
s.push((section_length & 0xFF) as u8);
s.push(0xFF);
s.push(0xFF);
s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
s.push(section_number);
s.push(section_number); s.extend_from_slice(descriptors);
let crc = mpeg2_crc32(&s);
s.extend_from_slice(&crc.to_be_bytes());
s
}
#[test]
fn tsdt_round_trip_registration_descriptor() {
let reg_descr: &[u8] = &[0x05, 0x05, b'H', b'D', b'M', b'V', 0xAB];
let section = build_tsdt_section(9, 0, reg_descr);
let tsdt = TransportStreamDescriptionTable::parse(§ion).unwrap();
assert_eq!(tsdt.version_number, 9);
assert!(tsdt.current_next_indicator);
assert_eq!(tsdt.section_number, 0);
assert_eq!(tsdt.last_section_number, 0);
let descrs: Vec<_> = tsdt.iter_descriptors().collect::<Result<_, _>>().unwrap();
assert_eq!(descrs.len(), 1);
match &descrs[0].body {
crate::descriptor::DescriptorBody::Registration {
format_identifier, ..
} => assert_eq!(format_identifier, b"HDMV"),
other => panic!("expected Registration, got {other:?}"),
}
}
#[test]
fn tsdt_empty_descriptor_loop() {
let section = build_tsdt_section(0, 0, &[]);
let tsdt = TransportStreamDescriptionTable::parse(§ion).unwrap();
assert_eq!(tsdt.version_number, 0);
assert!(tsdt.descriptors.is_empty());
assert_eq!(tsdt.iter_descriptors().count(), 0);
}
#[test]
fn tsdt_rejects_wrong_table_id() {
let section = build_cat_section(1, &[0x09, 0x04, 0x05, 0x00, 0xE1, 0x23]);
let err = TransportStreamDescriptionTable::parse(§ion).unwrap_err();
match err {
TsError::Unsupported(_) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn tsdt_reassembles_via_psi_assembler() {
let reg_descr: &[u8] = &[0x05, 0x04, b'A', b'V', b'0', b'1'];
let section = build_tsdt_section(2, 0, reg_descr);
let mut payload = vec![0u8]; payload.extend_from_slice(§ion);
payload.resize(184, 0xFF);
let mut asm = PsiSectionAssembler::new();
let out = asm.feed(&payload, true, 0).unwrap();
assert_eq!(out.len(), 1);
let tsdt = TransportStreamDescriptionTable::parse(&out[0]).unwrap();
assert_eq!(tsdt.version_number, 2);
assert_eq!(tsdt.iter_descriptors().count(), 1);
}
fn fake_ts_payload(payload: &[u8], _pusi: bool, _cc: u8) -> Vec<u8> {
payload.to_vec()
}
#[test]
fn assembler_single_packet_section() {
let section = build_pat_section(7, 0, &[(1, 0x100)]);
let mut payload = vec![0u8]; payload.extend_from_slice(§ion);
payload.resize(184, 0xFF);
let mut asm = PsiSectionAssembler::new();
let out = asm
.feed(&fake_ts_payload(&payload, true, 0), true, 0)
.unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], section);
let pat = ProgramAssociationTable::parse(&out[0]).unwrap();
assert_eq!(pat.programs, vec![(1, 0x100)]);
}
#[test]
fn assembler_section_spans_two_ts_packets() {
let stuff_descr = vec![0u8; 100]; let mut descr_block = vec![];
descr_block.push(0xC0); descr_block.push(stuff_descr.len() as u8);
descr_block.extend_from_slice(&stuff_descr);
let section = build_pmt_section(
1,
0,
0x100,
&[],
&[(0x1B, 0x1011, &descr_block), (0x81, 0x1100, &descr_block)],
);
assert!(
section.len() > 184,
"section ({} bytes) must exceed a single TS payload to exercise the assembler",
section.len()
);
let mut p0 = vec![0u8]; p0.extend_from_slice(§ion[..183]);
let mut p1 = Vec::new();
p1.extend_from_slice(§ion[183..]);
p1.resize(184, 0xFF);
let mut asm = PsiSectionAssembler::new();
let out0 = asm.feed(&p0, true, 5).unwrap();
assert!(
out0.is_empty(),
"section straddles two TS packets — first feed must not yield"
);
let out1 = asm.feed(&p1, false, 6).unwrap();
assert_eq!(out1.len(), 1, "second feed must complete the section");
assert_eq!(out1[0], section);
let pmt = ProgramMapTable::parse(&out1[0]).unwrap();
assert_eq!(pmt.streams.len(), 2);
}
#[test]
fn assembler_section_spans_three_ts_packets() {
let payload_chunk = vec![0xABu8; 250];
let mut descr_block: Vec<u8> = Vec::new();
for tag in [0xC0u8, 0xC1] {
descr_block.push(tag);
descr_block.push(payload_chunk.len() as u8);
descr_block.extend_from_slice(&payload_chunk);
}
let section = build_pmt_section(1, 0, 0x100, &descr_block, &[(0x1B, 0x1011, &[])]);
assert!(
section.len() > 2 * 184,
"need >2 TS payloads worth, got {} bytes",
section.len()
);
let mut p0 = vec![0u8];
p0.extend_from_slice(§ion[..183]);
let p1 = section[183..183 + 184].to_vec();
let mut p2 = section[183 + 184..].to_vec();
p2.resize(184, 0xFF);
let mut asm = PsiSectionAssembler::new();
assert!(asm.feed(&p0, true, 0).unwrap().is_empty());
assert!(asm.feed(&p1, false, 1).unwrap().is_empty());
let out = asm.feed(&p2, false, 2).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], section);
}
#[test]
fn assembler_cc_skip_discards_in_flight_section() {
let section = build_pat_section(1, 0, &[(1, 0x100), (2, 0x200), (3, 0x300)]);
let mut padded = Vec::new();
for _ in 0..200 {
padded.extend_from_slice(§ion);
}
let big_section = build_pmt_section(
1,
0,
0x100,
&[0u8; 200],
&[(0x1B, 0x1011, &[]), (0x81, 0x1100, &[])],
);
let mut p0 = vec![0u8];
p0.extend_from_slice(&big_section[..183]);
let mut asm = PsiSectionAssembler::new();
assert!(asm.feed(&p0, true, 0).unwrap().is_empty());
let p1 = vec![0xFFu8; 184];
let out = asm.feed(&p1, false, 2).unwrap();
assert!(out.is_empty());
let mut p2 = big_section[183..].to_vec();
p2.resize(184, 0xFF);
let out2 = asm.feed(&p2, false, 3).unwrap();
assert!(out2.is_empty(), "CC-skip must have dropped in-flight bytes");
let _ = padded;
}
#[test]
fn assembler_stuffing_terminates_payload() {
let section = build_pat_section(9, 0, &[(1, 0x100)]);
let mut payload = vec![0u8]; payload.extend_from_slice(§ion);
payload.extend_from_slice(&[0xFFu8; 64]);
let mut asm = PsiSectionAssembler::new();
let out = asm.feed(&payload, true, 0).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], section);
}
#[test]
fn assembler_two_sections_same_payload() {
let s0 = build_pat_section(1, 0, &[(1, 0x100)]);
let s1 = build_pat_section(2, 0, &[(2, 0x200)]);
let mut payload = vec![0u8];
payload.extend_from_slice(&s0);
payload.extend_from_slice(&s1);
payload.resize(184, 0xFF);
let mut asm = PsiSectionAssembler::new();
let out = asm.feed(&payload, true, 0).unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0], s0);
assert_eq!(out[1], s1);
}
#[test]
fn assembler_pointer_field_finishes_prev_section() {
let big_descr = vec![0u8; 200];
let s0 = build_pmt_section(1, 0, 0x100, &big_descr, &[(0x1B, 0x1011, &[])]);
let s1 = build_pat_section(7, 0, &[(3, 0x300)]);
assert!(s0.len() > 184, "s0 must straddle a TS packet boundary");
let mut p0 = vec![0u8]; p0.extend_from_slice(&s0[..183]);
let remaining = s0.len() - 183;
let mut p1 = vec![remaining as u8];
p1.extend_from_slice(&s0[183..]);
p1.extend_from_slice(&s1);
p1.resize(184, 0xFF);
let mut asm = PsiSectionAssembler::new();
let out0 = asm.feed(&p0, true, 0).unwrap();
assert!(out0.is_empty());
let out1 = asm.feed(&p1, true, 1).unwrap();
assert_eq!(out1.len(), 2);
assert_eq!(out1[0], s0);
assert_eq!(out1[1], s1);
}
#[test]
fn assembler_reset_drops_buffer() {
let section = build_pmt_section(1, 0, 0x100, &[0u8; 200], &[(0x1B, 0x1011, &[])]);
let mut p0 = vec![0u8];
p0.extend_from_slice(§ion[..183]);
let mut asm = PsiSectionAssembler::new();
assert!(asm.feed(&p0, true, 0).unwrap().is_empty());
asm.reset();
let mut p1 = section[183..].to_vec();
p1.resize(184, 0xFF);
let out = asm.feed(&p1, false, 1).unwrap();
assert!(out.is_empty());
}
fn build_sdt_service(
service_id: u16,
eit_sched: bool,
eit_pf: bool,
running_status: u8,
free_ca: bool,
descriptors: &[u8],
) -> Vec<u8> {
let mut s = Vec::new();
s.extend_from_slice(&service_id.to_be_bytes());
let mut b = 0b1111_1100u8;
if eit_sched {
b |= 0b10;
}
if eit_pf {
b |= 0b01;
}
s.push(b);
let dlen = descriptors.len() as u16;
let b3 = ((running_status & 0b111) << 5)
| (if free_ca { 0b0001_0000 } else { 0 })
| ((dlen >> 8) & 0x0F) as u8;
s.push(b3);
s.push((dlen & 0xFF) as u8);
s.extend_from_slice(descriptors);
s
}
fn build_sdt_section(
table_id: u8,
tsid: u16,
version: u8,
original_network_id: u16,
services: &[Vec<u8>],
) -> Vec<u8> {
let body_len: usize = 3 + services.iter().map(|s| s.len()).sum::<usize>();
let section_length = 5 + body_len + 4;
let mut s = Vec::with_capacity(3 + section_length);
s.push(table_id);
let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
s.push(len_hi);
s.push((section_length & 0xFF) as u8);
s.extend_from_slice(&tsid.to_be_bytes());
s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
s.push(0); s.push(0); s.extend_from_slice(&original_network_id.to_be_bytes());
s.push(0xFF); for svc in services {
s.extend_from_slice(svc);
}
let crc = mpeg2_crc32(&s);
s.extend_from_slice(&crc.to_be_bytes());
s
}
fn build_service_descriptor(service_type: u8, provider: &[u8], name: &[u8]) -> Vec<u8> {
let mut body = vec![service_type, provider.len() as u8];
body.extend_from_slice(provider);
body.push(name.len() as u8);
body.extend_from_slice(name);
let mut v = vec![0x48u8, body.len() as u8];
v.extend_from_slice(&body);
v
}
#[test]
fn sdt_actual_single_service_round_trip() {
let sd = build_service_descriptor(0x01, b"Provider", b"Channel One");
let svc = build_sdt_service(0x0064, true, true, 4, false, &sd);
let section = build_sdt_section(SDT_ACTUAL_TABLE_ID, 0x0001, 7, 0x2024, &[svc]);
let sdt = ServiceDescriptionTable::parse(§ion).unwrap();
assert!(!sdt.other_transport_stream);
assert_eq!(sdt.transport_stream_id, 0x0001);
assert_eq!(sdt.version_number, 7);
assert!(sdt.current_next_indicator);
assert_eq!(sdt.original_network_id, 0x2024);
assert_eq!(sdt.services.len(), 1);
let s = &sdt.services[0];
assert_eq!(s.service_id, 0x0064);
assert!(s.eit_schedule_flag);
assert!(s.eit_present_following_flag);
assert_eq!(s.running_status, RunningStatus::Running);
assert!(!s.free_ca_mode);
let d = s.iter_descriptors().next().unwrap().unwrap();
match d.body {
crate::descriptor::DescriptorBody::Service(svc) => {
assert_eq!(svc.service_type, 0x01);
assert_eq!(svc.service_provider_name, b"Provider");
assert_eq!(svc.service_name, b"Channel One");
}
other => panic!("expected Service, got {other:?}"),
}
}
#[test]
fn sdt_other_table_id_sets_flag() {
let svc = build_sdt_service(0x0001, false, false, 1, true, &[]);
let section = build_sdt_section(SDT_OTHER_TABLE_ID, 0x0009, 0, 0x0001, &[svc]);
let sdt = ServiceDescriptionTable::parse(§ion).unwrap();
assert!(sdt.other_transport_stream);
assert_eq!(sdt.services.len(), 1);
let s = &sdt.services[0];
assert_eq!(s.running_status, RunningStatus::NotRunning);
assert!(s.free_ca_mode);
assert!(!s.eit_schedule_flag);
assert!(!s.eit_present_following_flag);
}
#[test]
fn sdt_multiple_services() {
let svc0 = build_sdt_service(0x0064, true, true, 4, false, &[]);
let svc1 = build_sdt_service(0x0065, false, true, 5, true, &[]);
let section = build_sdt_section(SDT_ACTUAL_TABLE_ID, 0x0002, 1, 0x1000, &[svc0, svc1]);
let sdt = ServiceDescriptionTable::parse(§ion).unwrap();
assert_eq!(sdt.services.len(), 2);
assert_eq!(sdt.services[0].service_id, 0x0064);
assert_eq!(sdt.services[1].service_id, 0x0065);
assert_eq!(sdt.services[1].running_status, RunningStatus::OffAir);
}
#[test]
fn sdt_rejects_wrong_table_id() {
let section = build_pmt_section(1, 0, 0x100, &[], &[(0x1B, 0x1011, &[])]);
assert!(ServiceDescriptionTable::parse(§ion).is_err());
}
#[test]
fn sdt_crc_mismatch_rejected() {
let svc = build_sdt_service(0x0064, true, true, 4, false, &[]);
let mut section = build_sdt_section(SDT_ACTUAL_TABLE_ID, 0x0001, 7, 0x2024, &[svc]);
let last = section.len() - 1;
section[last] ^= 0xFF;
assert!(matches!(
ServiceDescriptionTable::parse(§ion),
Err(TsError::PsiCrcMismatch { .. })
));
}
#[test]
fn running_status_reserved_values() {
assert_eq!(RunningStatus::from_bits(6), RunningStatus::Reserved(6));
assert_eq!(RunningStatus::from_bits(7), RunningStatus::Reserved(7));
assert_eq!(RunningStatus::from_bits(0), RunningStatus::Undefined);
assert_eq!(RunningStatus::from_bits(2), RunningStatus::StartsSoon);
assert_eq!(RunningStatus::from_bits(3), RunningStatus::Pausing);
}
fn build_short_event_descriptor(lang: &[u8; 3], name: &[u8], text: &[u8]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(lang);
body.push(name.len() as u8);
body.extend_from_slice(name);
body.push(text.len() as u8);
body.extend_from_slice(text);
let mut v = vec![0x4Du8, body.len() as u8];
v.extend_from_slice(&body);
v
}
#[allow(clippy::too_many_arguments)]
fn build_eit_event(
event_id: u16,
start_time: [u8; 5],
duration_bcd: [u8; 3],
running_status: u8,
free_ca: bool,
descriptors: &[u8],
) -> Vec<u8> {
let mut s = Vec::new();
s.extend_from_slice(&event_id.to_be_bytes());
s.extend_from_slice(&start_time);
s.extend_from_slice(&duration_bcd);
let dlen = descriptors.len() as u16;
let b = ((running_status & 0b111) << 5)
| (if free_ca { 0b0001_0000 } else { 0 })
| ((dlen >> 8) & 0x0F) as u8;
s.push(b);
s.push((dlen & 0xFF) as u8);
s.extend_from_slice(descriptors);
s
}
#[allow(clippy::too_many_arguments)]
fn build_eit_section(
table_id: u8,
service_id: u16,
version: u8,
tsid: u16,
onid: u16,
segment_last: u8,
last_table_id: u8,
events: &[Vec<u8>],
) -> Vec<u8> {
let body_len: usize = 6 + events.iter().map(|e| e.len()).sum::<usize>();
let section_length = 5 + body_len + 4;
let mut s = Vec::with_capacity(3 + section_length);
s.push(table_id);
let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
s.push(len_hi);
s.push((section_length & 0xFF) as u8);
s.extend_from_slice(&service_id.to_be_bytes());
s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
s.push(0); s.push(0); s.extend_from_slice(&tsid.to_be_bytes());
s.extend_from_slice(&onid.to_be_bytes());
s.push(segment_last);
s.push(last_table_id);
for e in events {
s.extend_from_slice(e);
}
let crc = mpeg2_crc32(&s);
s.extend_from_slice(&crc.to_be_bytes());
s
}
#[test]
fn eit_start_time_spec_example() {
let dt = decode_eit_start_time([0xC0, 0x79, 0x12, 0x45, 0x00]).unwrap();
assert_eq!(dt.mjd, 0xC079);
assert_eq!(dt.year, 1993);
assert_eq!(dt.month, 10);
assert_eq!(dt.day, 13);
assert_eq!(dt.hour, 12);
assert_eq!(dt.minute, 45);
assert_eq!(dt.second, 0);
}
#[test]
fn eit_start_time_undefined_sentinel() {
assert!(decode_eit_start_time([0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
}
#[test]
fn eit_duration_spec_example() {
let d = EitDuration {
hours: bcd_byte(0x01),
minutes: bcd_byte(0x45),
seconds: bcd_byte(0x30),
};
assert_eq!(d.hours, 1);
assert_eq!(d.minutes, 45);
assert_eq!(d.seconds, 30);
assert_eq!(d.as_seconds(), 3600 + 45 * 60 + 30);
}
#[test]
fn eit_actual_pf_single_event_round_trip() {
let sed = build_short_event_descriptor(b"eng", b"The Event", b"A short description");
let ev = build_eit_event(
0x1234,
[0xC0, 0x79, 0x12, 0x45, 0x00],
[0x01, 0x45, 0x30],
4, false,
&sed,
);
let section = build_eit_section(
EIT_ACTUAL_PF_TABLE_ID,
0x0064, 5,
0x0001, 0x2024, 0,
EIT_ACTUAL_PF_TABLE_ID,
&[ev],
);
let eit = EventInformationTable::parse(§ion).unwrap();
assert!(!eit.other_transport_stream);
assert!(!eit.schedule);
assert_eq!(eit.table_id, EIT_ACTUAL_PF_TABLE_ID);
assert_eq!(eit.service_id, 0x0064);
assert_eq!(eit.version_number, 5);
assert!(eit.current_next_indicator);
assert_eq!(eit.transport_stream_id, 0x0001);
assert_eq!(eit.original_network_id, 0x2024);
assert_eq!(eit.last_table_id, EIT_ACTUAL_PF_TABLE_ID);
assert_eq!(eit.events.len(), 1);
let e = &eit.events[0];
assert_eq!(e.event_id, 0x1234);
let st = e.start_time.unwrap();
assert_eq!((st.year, st.month, st.day), (1993, 10, 13));
assert_eq!((st.hour, st.minute, st.second), (12, 45, 0));
assert_eq!(e.duration.as_seconds(), 6330);
assert_eq!(e.running_status, RunningStatus::Running);
assert!(!e.free_ca_mode);
let d = e.iter_descriptors().next().unwrap().unwrap();
match d.body {
DescriptorBody::ShortEvent(se) => {
assert_eq!(&se.language_code, b"eng");
assert_eq!(se.event_name, b"The Event");
assert_eq!(se.text, b"A short description");
}
other => panic!("expected ShortEvent, got {other:?}"),
}
}
#[test]
fn eit_schedule_and_other_flags() {
let ev = build_eit_event(1, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [0, 0, 0], 0, true, &[]);
let section = build_eit_section(0x60, 0x0001, 0, 0x0001, 0x0001, 0, 0x62, &[ev]);
let eit = EventInformationTable::parse(§ion).unwrap();
assert!(eit.other_transport_stream);
assert!(eit.schedule);
assert_eq!(eit.last_table_id, 0x62);
assert!(eit.events[0].start_time.is_none());
assert!(eit.events[0].free_ca_mode);
}
#[test]
fn eit_multiple_events() {
let e0 = build_eit_event(
10,
[0xC0, 0x79, 0x12, 0x00, 0x00],
[0, 0x30, 0],
4,
false,
&[],
);
let e1 = build_eit_event(
11,
[0xC0, 0x79, 0x12, 0x30, 0x00],
[0x01, 0, 0],
1,
false,
&[],
);
let section = build_eit_section(
EIT_ACTUAL_PF_TABLE_ID,
0x0064,
0,
0x0001,
0x0001,
0,
0x4E,
&[e0, e1],
);
let eit = EventInformationTable::parse(§ion).unwrap();
assert_eq!(eit.events.len(), 2);
assert_eq!(eit.events[0].event_id, 10);
assert_eq!(eit.events[1].event_id, 11);
assert_eq!(eit.events[0].duration.minutes, 30);
assert_eq!(eit.events[1].duration.hours, 1);
}
#[test]
fn eit_table_id_classification() {
assert!(EventInformationTable::is_eit_table_id(0x4E));
assert!(EventInformationTable::is_eit_table_id(0x4F));
assert!(EventInformationTable::is_eit_table_id(0x50));
assert!(EventInformationTable::is_eit_table_id(0x5F));
assert!(EventInformationTable::is_eit_table_id(0x60));
assert!(EventInformationTable::is_eit_table_id(0x6F));
assert!(!EventInformationTable::is_eit_table_id(0x4D));
assert!(!EventInformationTable::is_eit_table_id(0x70));
assert!(!EventInformationTable::is_eit_table_id(0x42));
}
#[test]
fn eit_rejects_wrong_table_id() {
let ev = build_eit_event(1, [0xFF; 5], [0, 0, 0], 0, false, &[]);
let mut section = build_eit_section(0x70, 0, 0, 0, 0, 0, 0, &[ev]);
section[0] = 0x70;
assert!(matches!(
EventInformationTable::parse(§ion),
Err(TsError::Unsupported(_))
));
}
#[test]
fn eit_crc_mismatch_rejected() {
let ev = build_eit_event(1, [0xC0, 0x79, 0x12, 0x45, 0x00], [0, 0, 0], 4, false, &[]);
let mut section = build_eit_section(
EIT_ACTUAL_PF_TABLE_ID,
0x0064,
0,
0x0001,
0x0001,
0,
0x4E,
&[ev],
);
let last = section.len() - 1;
section[last] ^= 0xFF;
assert!(matches!(
EventInformationTable::parse(§ion),
Err(TsError::PsiCrcMismatch { .. })
));
}
}