use libmpegts::psi::{
CaDescriptorRef,
DescriptorsRef,
PmtSectionRef,
};
use crate::error::{
Error,
Result,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub(super) enum CaPmtListManagement {
Only = 0x03,
Add = 0x04,
Update = 0x05,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub(super) enum CaPmtCommand {
OkDescrambling = 0x01,
NotSelected = 0x04,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CaDescriptor {
caid: u16,
bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ProgramStream {
stream_type: u8,
elementary_pid: u16,
descriptors: Vec<CaDescriptor>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Program {
program_number: u16,
version: u8,
descriptors: Vec<CaDescriptor>,
streams: Vec<ProgramStream>,
}
impl Program {
pub fn parse(section: &[u8]) -> Result<Self> {
let pmt = PmtSectionRef::try_from(section)
.map_err(|error| Error::InvalidData(format!("invalid PMT section: {error}")))?;
let program_number = pmt.program_number();
if program_number == 0 {
return Err(Error::InvalidData(
"invalid PMT section: program number is zero".to_owned(),
));
}
let descriptors = parse_ca_descriptors(program_number, pmt.program_descriptors())?;
let mut streams = Vec::new();
for stream in pmt.streams() {
let stream = stream.map_err(|error| {
Error::InvalidData(format!(
"invalid PMT section for program {program_number}: {error}"
))
})?;
streams.push(ProgramStream {
stream_type: stream.stream_type(),
elementary_pid: stream.elementary_pid(),
descriptors: parse_ca_descriptors(program_number, stream.stream_descriptors())?,
});
}
Ok(Program {
program_number,
version: pmt.version(),
descriptors,
streams,
})
}
pub fn program_number(&self) -> u16 {
self.program_number
}
pub fn build_ca_pmt(
&self,
caids: &[u16],
list_management: CaPmtListManagement,
command: CaPmtCommand,
) -> Result<Option<Vec<u8>>> {
let mut body = Vec::new();
body.push(list_management as u8);
body.extend_from_slice(&self.program_number.to_be_bytes());
body.push(0xC1 | ((self.version & 0x1F) << 1));
let mut matched = append_info(&mut body, &self.descriptors, caids, command)?;
for stream in &self.streams {
body.push(stream.stream_type);
body.extend_from_slice(&(0xE000 | stream.elementary_pid).to_be_bytes());
matched |= append_info(&mut body, &stream.descriptors, caids, command)?;
}
if matched { Ok(Some(body)) } else { Ok(None) }
}
}
fn parse_ca_descriptors(
program_number: u16,
descriptors: Option<DescriptorsRef<'_>>,
) -> Result<Vec<CaDescriptor>> {
let mut result = Vec::new();
let Some(descriptors) = descriptors else {
return Ok(result);
};
for descriptor in descriptors {
let descriptor = descriptor.map_err(|error| {
Error::InvalidData(format!(
"invalid descriptor in PMT program {program_number}: {error}"
))
})?;
if descriptor.tag() != CaDescriptorRef::TAG {
continue;
}
let ca = CaDescriptorRef::try_from(descriptor).map_err(|error| {
Error::InvalidData(format!(
"invalid CA descriptor in PMT program {program_number}: {error}"
))
})?;
result.push(CaDescriptor {
caid: ca.ca_system_id(),
bytes: descriptor.bytes().to_vec(),
});
}
Ok(result)
}
fn append_info(
out: &mut Vec<u8>,
descriptors: &[CaDescriptor],
caids: &[u16],
command: CaPmtCommand,
) -> Result<bool> {
let matching: Vec<&CaDescriptor> = descriptors
.iter()
.filter(|descriptor| caids.contains(&descriptor.caid))
.collect();
if matching.is_empty() {
out.extend_from_slice(&[0xF0, 0x00]);
return Ok(false);
}
let descriptors_len: usize = matching
.iter()
.map(|descriptor| descriptor.bytes.len())
.sum();
let info_len = 1usize
.checked_add(descriptors_len)
.ok_or_else(|| Error::InvalidData("CA_PMT info length overflow".to_owned()))?;
if info_len > 0x0FFF {
return Err(Error::InvalidData(format!(
"CA_PMT info is too large: {info_len} bytes"
)));
}
let info_len = info_len as u16;
out.extend_from_slice(&(0xF000 | info_len).to_be_bytes());
out.push(command as u8);
for descriptor in matching {
out.extend_from_slice(&descriptor.bytes);
}
Ok(true)
}
#[cfg(test)]
mod tests {
use libmpegts::psi::{
PmtBuilder,
PmtConfig,
PmtStream,
};
use super::*;
fn ca_descriptor(caid: u16, pid: u16, private: &[u8]) -> Vec<u8> {
let mut descriptor = vec![0x09, (4 + private.len()) as u8];
descriptor.extend_from_slice(&caid.to_be_bytes());
descriptor.extend_from_slice(&(0xE000 | pid).to_be_bytes());
descriptor.extend_from_slice(private);
descriptor
}
fn pmt_section() -> Vec<u8> {
let mut program_descriptors = vec![0x52, 0x01, 0xAA];
program_descriptors.extend(ca_descriptor(0x0100, 0x1ABC, &[0x55]));
let mut video_descriptors = vec![0x52, 0x01, 0x01];
video_descriptors.extend(ca_descriptor(0x0500, 0x0123, &[]));
let sections = PmtBuilder::build(PmtConfig {
program_number: 0x1234,
pcr_pid: 0x0101,
version: 3,
program_descriptors,
streams: vec![
PmtStream {
stream_type: 0x1B,
elementary_pid: 0x0101,
stream_descriptors: video_descriptors,
},
PmtStream {
stream_type: 0x04,
elementary_pid: 0x0102,
stream_descriptors: Vec::new(),
},
],
});
sections[0].to_vec()
}
#[test]
fn builds_program_level_ca_pmt_and_keeps_all_streams() {
let program = Program::parse(&pmt_section()).unwrap();
let program_ca = ca_descriptor(0x0100, 0x1ABC, &[0x55]);
let mut expected = vec![0x03, 0x12, 0x34, 0xC7, 0xF0, 0x08, 0x01];
expected.extend_from_slice(&program_ca);
expected.extend_from_slice(&[0x1B, 0xE1, 0x01, 0xF0, 0x00, 0x04, 0xE1, 0x02, 0xF0, 0x00]);
assert_eq!(
program
.build_ca_pmt(
&[0x0100],
CaPmtListManagement::Only,
CaPmtCommand::OkDescrambling,
)
.unwrap(),
Some(expected)
);
}
#[test]
fn filters_ca_descriptors_for_each_session() {
let program = Program::parse(&pmt_section()).unwrap();
let video_ca = ca_descriptor(0x0500, 0x0123, &[]);
let mut expected = vec![
0x04, 0x12, 0x34, 0xC7, 0xF0, 0x00, 0x1B, 0xE1, 0x01, 0xF0, 0x07, 0x01,
];
expected.extend_from_slice(&video_ca);
expected.extend_from_slice(&[0x04, 0xE1, 0x02, 0xF0, 0x00]);
assert_eq!(
program
.build_ca_pmt(
&[0x0500],
CaPmtListManagement::Add,
CaPmtCommand::OkDescrambling,
)
.unwrap(),
Some(expected)
);
assert_eq!(
program
.build_ca_pmt(
&[0x0600],
CaPmtListManagement::Only,
CaPmtCommand::OkDescrambling,
)
.unwrap(),
None
);
}
#[test]
fn builds_not_selected_command() {
let program = Program::parse(&pmt_section()).unwrap();
let body = program
.build_ca_pmt(
&[0x0100],
CaPmtListManagement::Update,
CaPmtCommand::NotSelected,
)
.unwrap()
.unwrap();
assert_eq!(body[0], CaPmtListManagement::Update as u8);
assert_eq!(body[6], CaPmtCommand::NotSelected as u8);
}
#[test]
fn rejects_invalid_section_and_ca_descriptor() {
let mut bad_crc = pmt_section();
bad_crc[3] ^= 1;
assert!(Program::parse(&bad_crc).is_err());
let sections = PmtBuilder::build(PmtConfig {
program_number: 1,
pcr_pid: 0x0100,
version: 0,
program_descriptors: vec![0x09, 0x03, 0x01, 0x00, 0xE1],
streams: Vec::new(),
});
assert!(Program::parse(§ions[0]).is_err());
}
}