use std::collections::{
BTreeMap,
HashMap,
};
use crate::{
ca::{
ApduTag,
capmt::{
CaPmtCommand,
CaPmtListManagement,
Program,
},
resource::{
Resource,
ResourceContext,
ResourceId,
},
session::CaEvent,
transport::CiTransport,
},
error::{
Error,
Result,
},
};
struct ConditionalAccessSession {
slot_id: u8,
caids: Option<Vec<u16>>,
selected: BTreeMap<u16, Program>,
}
pub struct ConditionalAccessResource {
sessions: HashMap<u16, ConditionalAccessSession>,
programs: BTreeMap<u16, Program>,
}
impl ConditionalAccessResource {
pub fn new() -> Self {
ConditionalAccessResource {
sessions: HashMap::new(),
programs: BTreeMap::new(),
}
}
pub fn session_caids(&self, slot_id: u8, session_id: u16) -> Option<&[u16]> {
self.sessions
.get(&session_id)
.filter(|session| session.slot_id == slot_id)
.and_then(|session| session.caids.as_deref())
}
pub fn set_program(
&mut self,
transport: &mut CiTransport,
program: Program,
) -> Result<Vec<u8>> {
let program_number = program.program_number();
if self.programs.get(&program_number) == Some(&program) {
return Ok(Vec::new());
}
self.programs.insert(program_number, program.clone());
let mut session_ids: Vec<u16> = self.sessions.keys().copied().collect();
session_ids.sort_unstable();
let mut touched_slots = Vec::new();
for session_id in session_ids {
let session = self.sessions.get_mut(&session_id).expect("known session");
let Some(caids) = session.caids.as_deref() else {
continue;
};
let previous = session.selected.get(&program_number).cloned();
let list_management = if previous.is_some() {
CaPmtListManagement::Update
} else if session.selected.is_empty() {
CaPmtListManagement::Only
} else {
CaPmtListManagement::Add
};
if let Some(body) =
program.build_ca_pmt(caids, list_management, CaPmtCommand::OkDescrambling)?
{
transport.send_apdu(session.slot_id, session_id, ApduTag::CA_PMT, &body)?;
session.selected.insert(program_number, program.clone());
touched_slots.push(session.slot_id);
} else if let Some(previous) = previous {
let body = previous
.build_ca_pmt(
caids,
CaPmtListManagement::Update,
CaPmtCommand::NotSelected,
)?
.ok_or_else(|| {
Error::InvalidData(format!(
"ca slot {}: selected program {} no longer has a matching CA descriptor",
session.slot_id, program_number
))
})?;
transport.send_apdu(session.slot_id, session_id, ApduTag::CA_PMT, &body)?;
session.selected.remove(&program_number);
touched_slots.push(session.slot_id);
}
}
touched_slots.sort_unstable();
touched_slots.dedup();
Ok(touched_slots)
}
pub fn remove_program(
&mut self,
transport: &mut CiTransport,
program_number: u16,
) -> Result<Vec<u8>> {
if self.programs.remove(&program_number).is_none() {
return Ok(Vec::new());
}
let mut session_ids: Vec<u16> = self.sessions.keys().copied().collect();
session_ids.sort_unstable();
let mut touched_slots = Vec::new();
for session_id in session_ids {
let session = self.sessions.get_mut(&session_id).expect("known session");
let Some(program) = session.selected.get(&program_number).cloned() else {
continue;
};
let caids = session.caids.as_deref().ok_or_else(|| {
Error::InvalidData(format!(
"ca slot {}: selected program without confirmed CA_INFO",
session.slot_id
))
})?;
let body = program
.build_ca_pmt(
caids,
CaPmtListManagement::Update,
CaPmtCommand::NotSelected,
)?
.ok_or_else(|| {
Error::InvalidData(format!(
"ca slot {}: selected program {} has no matching CA descriptor",
session.slot_id, program_number
))
})?;
transport.send_apdu(session.slot_id, session_id, ApduTag::CA_PMT, &body)?;
session.selected.remove(&program_number);
touched_slots.push(session.slot_id);
}
touched_slots.sort_unstable();
touched_slots.dedup();
Ok(touched_slots)
}
fn synchronize_session(
programs: &BTreeMap<u16, Program>,
session: &mut ConditionalAccessSession,
ctx: &mut ResourceContext<'_>,
caids: Vec<u16>,
) -> Result<()> {
let old_caids = session.caids.replace(caids.clone());
let previous = std::mem::take(&mut session.selected);
let mut selected = BTreeMap::new();
for (&program_number, program) in programs {
let list_management = if selected.is_empty() {
CaPmtListManagement::Only
} else {
CaPmtListManagement::Add
};
if let Some(body) =
program.build_ca_pmt(&caids, list_management, CaPmtCommand::OkDescrambling)?
{
ctx.send_apdu(ApduTag::CA_PMT, &body)?;
selected.insert(program_number, program.clone());
}
}
if selected.is_empty()
&& let Some(old_caids) = old_caids
{
for (_, program) in previous {
if let Some(body) = program.build_ca_pmt(
&old_caids,
CaPmtListManagement::Update,
CaPmtCommand::NotSelected,
)? {
ctx.send_apdu(ApduTag::CA_PMT, &body)?;
}
}
}
session.selected = selected;
Ok(())
}
}
fn parse_ca_info(slot_id: u8, body: &[u8]) -> Result<Vec<u16>> {
if !body.len().is_multiple_of(2) {
return Err(Error::InvalidData(format!(
"ca slot {}: ca_info has an odd body length {}",
slot_id,
body.len()
)));
}
Ok(body
.chunks_exact(2)
.map(|bytes| (u16::from(bytes[0]) << 8) | u16::from(bytes[1]))
.collect())
}
impl Resource for ConditionalAccessResource {
fn resource_id(&self) -> ResourceId {
ResourceId::CONDITIONAL_ACCESS_SUPPORT
}
fn on_open(&mut self, ctx: &mut ResourceContext<'_>) -> Result<()> {
self.sessions.insert(
ctx.session_id,
ConditionalAccessSession {
slot_id: ctx.slot_id,
caids: None,
selected: BTreeMap::new(),
},
);
ctx.send_apdu(ApduTag::CA_INFO_ENQ, &[])
}
fn on_apdu(&mut self, ctx: &mut ResourceContext<'_>, tag: ApduTag, body: &[u8]) -> Result<()> {
match tag {
ApduTag::CA_INFO => {
let caids = parse_ca_info(ctx.slot_id, body)?;
let session = self.sessions.get_mut(&ctx.session_id).ok_or_else(|| {
Error::InvalidData(format!(
"ca slot {}: ca_info on unknown resource session {}",
ctx.slot_id, ctx.session_id
))
})?;
Self::synchronize_session(&self.programs, session, ctx, caids.clone())?;
ctx.event(CaEvent::CaInfo {
slot_id: ctx.slot_id,
session_id: ctx.session_id,
caids,
});
Ok(())
}
ApduTag::CA_PMT_REPLY | ApduTag::CA_UPDATE => Ok(()),
tag => Err(Error::InvalidData(format!(
"ca slot {}: unexpected conditional access apdu tag {:?}",
ctx.slot_id, tag
))),
}
}
fn on_close(&mut self, _slot_id: u8, session_id: u16) {
self.sessions.remove(&session_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ca_info() {
assert_eq!(
parse_ca_info(0, &[0x01, 0x00, 0x05, 0x00, 0x0B, 0x00]).unwrap(),
[0x0100, 0x0500, 0x0B00]
);
assert_eq!(parse_ca_info(0, &[]).unwrap(), []);
}
#[test]
fn test_parse_ca_info_rejects_odd_length() {
assert!(parse_ca_info(2, &[0x01]).is_err());
assert!(parse_ca_info(2, &[0x01, 0x00, 0xFF]).is_err());
}
}