use std::collections::BTreeSet;
use std::io;
use std::time::Duration;
use dvb_si::tables::cat::CatSection;
use dvb_si::tables::pmt::PmtSection;
use crate::dataplane::{CiDataDevice, TS_PACKET_LEN};
use crate::device::CaDevice;
use crate::driver::Driver;
use crate::event::Notification;
use crate::managed::CaError;
const TS_SYNC_BYTE: u8 = 0x47;
const TS_PID_HIGH_MASK: u8 = 0x1F;
const READ_BATCH_PACKETS: usize = 32;
fn packet_pid(packet: &[u8]) -> u16 {
(u16::from(packet[1] & TS_PID_HIGH_MASK) << 8) | u16::from(packet[2])
}
fn filter_ts(scrambled: &[u8], allow: &BTreeSet<u16>) -> io::Result<Vec<u8>> {
if scrambled.len() % TS_PACKET_LEN != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"scrambled TS is not a whole number of 188-byte packets",
));
}
let mut out = Vec::new();
for packet in scrambled.chunks_exact(TS_PACKET_LEN) {
if packet[0] != TS_SYNC_BYTE {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"TS packet sync byte != 0x47 (misaligned input)",
));
}
if allow.contains(&packet_pid(packet)) {
out.extend_from_slice(packet);
}
}
Ok(out)
}
pub struct CaDescrambler<D: CaDevice, C: CiDataDevice> {
driver: Driver<D>,
ci: C,
}
impl<D: CaDevice, C: CiDataDevice> CaDescrambler<D, C> {
#[must_use]
pub fn new(driver: Driver<D>, ci: C) -> Self {
Self { driver, ci }
}
pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
self.driver.add_service(pmt)
}
pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
self.driver.set_cat(cat)
}
pub fn feed_ts(&mut self, scrambled: &[u8]) -> io::Result<Vec<u8>> {
let allow: BTreeSet<u16> = self.required_pids().into_iter().collect();
let kept = filter_ts(scrambled, &allow)?;
if !kept.is_empty() {
self.ci.write(&kept)?;
}
let mut out = Vec::new();
let mut buf = [0u8; READ_BATCH_PACKETS * TS_PACKET_LEN];
loop {
let n = self.ci.read(&mut buf)?;
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
}
Ok(out)
}
#[must_use]
pub fn required_pids(&self) -> Vec<u16> {
self.driver.required_pids()
}
pub fn take_notifications(&mut self) -> Vec<Notification> {
self.driver.take_notifications()
}
pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
self.driver.pump(timeout)
}
#[must_use]
pub fn driver(&self) -> &Driver<D> {
&self.driver
}
pub fn driver_mut(&mut self) -> &mut Driver<D> {
&mut self.driver
}
#[must_use]
pub fn ci(&self) -> &C {
&self.ci
}
pub fn ci_mut(&mut self) -> &mut C {
&mut self.ci
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dataplane::MockCiDataDevice;
use crate::device::MockCaDevice;
use crate::driver::tests::{
CA_SESSION, build_ca_pmt_fixture, build_ca_pmt_fixture_dedicated_pcr, build_cat_fixture,
build_clear_pmt_fixture, ca_descriptor, ca_pmt_reply_for, driver_with_sessions, feed,
r_apdu, ser,
};
use crate::managed::CaError;
use broadcast_common::Parse;
fn packet(pid: u16, fill: u8) -> Vec<u8> {
let mut p = vec![fill; TS_PACKET_LEN];
p[0] = TS_SYNC_BYTE;
p[1] = 0x40 | ((pid >> 8) as u8);
p[2] = pid as u8;
p
}
#[test]
fn filter_ts_keeps_only_allowed_pids() {
let p_100 = packet(0x100, 0xAA);
let p_64 = packet(0x64, 0xBB);
let p_200 = packet(0x200, 0xCC);
let mut scrambled = Vec::new();
scrambled.extend_from_slice(&p_100);
scrambled.extend_from_slice(&p_64);
scrambled.extend_from_slice(&p_200);
let allow: BTreeSet<u16> = [0x100, 0x64].into_iter().collect();
let kept = filter_ts(&scrambled, &allow).unwrap();
let mut expected = Vec::new();
expected.extend_from_slice(&p_100);
expected.extend_from_slice(&p_64);
assert_eq!(
kept, expected,
"0x200 must be dropped, the two allowed packets kept byte-exact and in order"
);
let empty: BTreeSet<u16> = BTreeSet::new();
assert!(filter_ts(&scrambled, &empty).unwrap().is_empty());
assert_eq!(
filter_ts(&scrambled[..scrambled.len() - 1], &allow)
.unwrap_err()
.kind(),
io::ErrorKind::InvalidInput
);
let mut bad = p_100.clone();
bad[0] = 0x00;
assert_eq!(
filter_ts(&bad, &allow).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
fn descrambler_with(
driver: Driver<MockCaDevice>,
descrambled: impl IntoIterator<Item = Vec<u8>>,
) -> CaDescrambler<MockCaDevice, MockCiDataDevice> {
CaDescrambler::new(driver, MockCiDataDevice::new(descrambled))
}
#[test]
fn feed_ts_filters_to_required_pids_and_returns_descrambled() {
use dvb_ci::objects::ca_info::CaInfo;
let mut d = driver_with_sessions();
d.take_notifications();
let pmt_bytes = build_ca_pmt_fixture(1546);
let pmt = PmtSection::parse(&pmt_bytes).unwrap();
d.add_service(&pmt).unwrap();
feed(
&mut d,
r_apdu(
CA_SESSION,
&ser(&CaInfo {
ca_system_ids: vec![0x0648],
}),
),
);
d.take_notifications();
let mut descriptors = Vec::new();
descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
let cat_bytes = build_cat_fixture(&descriptors);
let cat = CatSection::parse(&cat_bytes).unwrap();
d.set_cat(&cat).unwrap();
assert_eq!(
d.required_pids(),
vec![0x0064, 0x0065, 0x0100, 0x0101, 0x1FF0],
"precondition: required_pids = descramble_pids ∪ ca_pids ∪ emm_pids"
);
let descrambled_script = packet(0x100, 0xEE);
let mut descrambler = descrambler_with(d, [descrambled_script.clone()]);
assert_eq!(
descrambler.required_pids(),
vec![0x0064, 0x0065, 0x0100, 0x0101, 0x1FF0],
"required_pids must delegate through the wrapper"
);
let required_pkt = packet(0x100, 0x11);
let junk_pkt = packet(0x999, 0x22);
let mut scrambled = Vec::new();
scrambled.extend_from_slice(&required_pkt);
scrambled.extend_from_slice(&junk_pkt);
let out = descrambler.feed_ts(&scrambled).unwrap();
assert_eq!(
descrambler.ci().written_ts(),
required_pkt,
"ci0 must receive ONLY the required-PID packet; the junk packet on 0x999 must be dropped"
);
assert_eq!(
out, descrambled_script,
"feed_ts must return the scripted descrambled TS read back from ci0"
);
}
#[test]
fn feed_ts_keeps_a_dedicated_pcr_pid_packet() {
let mut d = driver_with_sessions();
d.take_notifications();
let pmt_bytes = build_ca_pmt_fixture_dedicated_pcr(1550);
let pmt = PmtSection::parse(&pmt_bytes).unwrap();
d.add_service(&pmt).unwrap();
assert!(
d.required_pids().contains(&0x00FF),
"precondition: required_pids must include the dedicated PCR PID, got {:?}",
d.required_pids()
);
let mut descrambler = descrambler_with(d, []);
let pcr_pkt = packet(0x00FF, 0x33);
let junk_pkt = packet(0x0AAA, 0x44);
let mut scrambled = Vec::new();
scrambled.extend_from_slice(&pcr_pkt);
scrambled.extend_from_slice(&junk_pkt);
descrambler.feed_ts(&scrambled).unwrap();
assert_eq!(
descrambler.ci().written_ts(),
pcr_pkt,
"ci0 must receive the dedicated PCR PID packet (clock reference); \
the junk packet must be dropped"
);
}
#[test]
fn take_notifications_delegates_entitlement() {
use dvb_ci::objects::ca_pmt_reply::CaEnable;
let mut d = driver_with_sessions();
d.take_notifications();
let pmt_bytes = build_ca_pmt_fixture(1546);
let pmt = PmtSection::parse(&pmt_bytes).unwrap();
d.add_service(&pmt).unwrap();
d.take_notifications();
let mut descrambler = descrambler_with(d, []);
feed(
descrambler.driver_mut(),
r_apdu(
CA_SESSION,
&ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
),
);
let notes = descrambler.take_notifications();
let hits = notes
.iter()
.filter(|n| {
matches!(
n,
Notification::Entitlement {
program_number: 1546,
ca_enable: CaEnable::Possible,
descrambling_ok: true,
}
)
})
.count();
assert_eq!(
hits, 1,
"expected exactly one Entitlement notification to surface via CaDescrambler::take_notifications(), got {notes:?}"
);
}
#[test]
fn add_service_delegates() {
let d = Driver::new(MockCaDevice::new([]));
let mut descrambler = descrambler_with(d, []);
let pmt_bytes = build_clear_pmt_fixture(999);
let pmt = PmtSection::parse(&pmt_bytes).unwrap();
let err = descrambler.add_service(&pmt).unwrap_err();
assert!(
matches!(
err,
CaError::NoCaDescriptor {
program_number: 999
}
),
"expected CaError::NoCaDescriptor via delegation, got {err:?}"
);
}
}