use crate::event::{Action, Event, HostRequest, Notification};
use crate::resource::{
ApplicationInformation, ConditionalAccess, DateTime, Mmi, Resource, ResourceManager,
ResourceOut,
};
use crate::session::{SessionLayer, SessionOut};
use crate::transport::{Out as TransportOut, Transport};
use dvb_ci::builder::{build_ca_pmt, build_ca_pmt_for_caids};
use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
use dvb_ci::resource::{ResourceId, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI, RESOURCE_MANAGER};
use dvb_common::{Parse, Serialize};
use dvb_si::tables::pmt::PmtSection;
fn ser_apdu<S: Serialize>(s: &S) -> Vec<u8> {
let mut b = vec![0u8; s.serialized_len()];
match s.serialize_into(&mut b) {
Ok(n) => b.truncate(n),
Err(_) => b.clear(),
}
b
}
pub struct CiStack {
transport: Transport,
session: SessionLayer,
provided: Vec<ResourceId>,
resources: Vec<Box<dyn Resource>>,
cam_caids: Vec<u16>,
pending_descramble: Option<Vec<u8>>,
}
impl Default for CiStack {
fn default() -> Self {
Self::new()
}
}
impl CiStack {
#[must_use]
pub fn new() -> Self {
let provided = vec![RESOURCE_MANAGER, DATE_TIME];
Self {
transport: Transport::new(1),
session: SessionLayer::new(),
resources: vec![
Box::new(ResourceManager::new(provided.clone())),
Box::new(ApplicationInformation),
Box::new(ConditionalAccess),
Box::new(DateTime::new()),
Box::new(Mmi),
],
provided,
cam_caids: Vec::new(),
pending_descramble: None,
}
}
pub fn register(&mut self, resource: Box<dyn Resource>) -> &mut Self {
self.resources.push(resource);
self
}
fn handler_index(&self, resource: ResourceId) -> Option<usize> {
self.resources.iter().position(|r| r.id() == resource)
}
pub fn handle(&mut self, event: Event<'_>) -> Vec<Action> {
match event {
Event::Host(HostRequest::Init) => {
let mut actions = vec![Action::Reset, Action::QuerySlot];
let out = self.transport.init();
actions.extend(self.emit_transport(out));
actions
}
Event::Tick { elapsed } => {
let out = self.transport.tick(elapsed);
let mut actions = self.emit_transport(out);
for (session_nb, resource) in self.session.sessions() {
if let Some(i) = self.handler_index(resource) {
let out = self.resources[i].tick(elapsed);
actions.extend(self.process_resource_out(session_nb, out));
}
}
actions
}
Event::Readable(frame) => {
let out = self.transport.on_frame(frame);
self.emit_transport(out)
}
Event::Host(HostRequest::SendCaPmt(apdu)) => {
self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, apdu)
}
Event::Host(HostRequest::Descramble(pmt)) => self.descramble(pmt),
Event::Host(HostRequest::MmiMenuAnswer(choice_ref)) => {
let apdu = ser_apdu(&MenuAnsw { choice_ref });
self.send_to_resource(MMI, &apdu)
}
Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
let apdu = ser_apdu(&Answ {
answ_id: AnswId::Answer,
text_chars: text,
});
self.send_to_resource(MMI, &apdu)
}
Event::Host(HostRequest::MmiCancel) => {
let apdu = ser_apdu(&Answ {
answ_id: AnswId::Cancel,
text_chars: &[],
});
self.send_to_resource(MMI, &apdu)
}
Event::Host(HostRequest::Shutdown) => Vec::new(),
}
}
fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
match note {
Notification::CaInfo { ca_system_ids } => {
self.cam_caids = ca_system_ids.clone();
Vec::new()
}
Notification::CaPmtReply {
descrambling_ok, ..
} => match self.pending_descramble.take() {
Some(pmt) if *descrambling_ok => {
match self.build_ca_pmt_bytes(&pmt, CaPmtCmdId::OkDescrambling) {
Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
Err(detail) => vec![Action::Notify(Notification::Error { detail })],
}
}
_ => Vec::new(),
},
_ => Vec::new(),
}
}
fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
let bytes = match self.build_ca_pmt_bytes(pmt, CaPmtCmdId::Query) {
Ok(b) => b,
Err(detail) => return vec![Action::Notify(Notification::Error { detail })],
};
self.pending_descramble = Some(pmt.to_vec());
self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes)
}
fn build_ca_pmt_bytes(&self, pmt: &[u8], cmd_id: CaPmtCmdId) -> Result<Vec<u8>, String> {
let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
let lm = CaPmtListManagement::Only;
let built = if self.cam_caids.is_empty() {
build_ca_pmt(&parsed, lm, cmd_id)
} else {
build_ca_pmt_for_caids(&parsed, &self.cam_caids, lm, cmd_id)
};
Ok(built.to_bytes())
}
fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
let nb = (1u16..=u16::MAX).find(|&n| self.session.resource_of(n) == Some(resource));
match nb {
Some(nb) => {
let spdu = self.session.send_apdu(nb, apdu);
let out = self.transport.send_spdu(&spdu);
self.emit_transport(out)
}
None => vec![Action::Notify(Notification::Error {
detail: format!("no open session for resource {}", resource.name()),
})],
}
}
fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
let mut actions = Vec::new();
for w in out.writes {
actions.push(Action::Write(w));
}
if let Some(after) = out.timer {
actions.push(Action::SetTimer { after });
}
if let Some(err) = out.error {
actions.push(Action::Notify(Notification::Error {
detail: err.to_string(),
}));
}
for spdu in out.spdus {
actions.extend(self.drive_session(&spdu));
}
actions
}
fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
let provided = self.provided.clone();
let SessionOut {
spdus,
apdus,
opened,
closed,
} = self.session.on_spdu(spdu, |r| provided.contains(&r));
let mut actions = Vec::new();
for s in spdus {
actions.extend(self.send_spdu_actions(&s));
}
for (session_nb, resource) in opened {
actions.push(Action::Notify(Notification::SessionOpened { resource }));
if let Some(i) = self.handler_index(resource) {
let out = self.resources[i].on_open();
actions.extend(self.process_resource_out(session_nb, out));
}
}
for session_nb in closed {
actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
}
for (session_nb, apdu) in apdus {
if let Some(resource) = self.session.resource_of(session_nb) {
if let Some(i) = self.handler_index(resource) {
let out = self.resources[i].on_apdu(&apdu);
actions.extend(self.process_resource_out(session_nb, out));
}
}
}
actions
}
fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
let t = self.transport.send_spdu(spdu);
let mut actions = Vec::new();
for w in t.writes {
actions.push(Action::Write(w));
}
if let Some(after) = t.timer {
actions.push(Action::SetTimer { after });
}
actions
}
fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
let mut actions = Vec::new();
for apdu in out.apdus {
let spdu = self.session.send_apdu(session_nb, &apdu);
actions.extend(self.send_spdu_actions(&spdu));
}
for note in out.notify {
let follow = self.on_ca_notification(¬e);
actions.push(Action::Notify(note));
actions.extend(follow);
}
for resource in out.open {
let spdu = self.session.create_session(resource);
actions.extend(self.send_spdu_actions(&spdu));
}
actions
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::DEFAULT_POLL_INTERVAL;
use dvb_ci::resource::RESOURCE_MANAGER;
use dvb_ci::spdu::{tags as spdu_tags, OpenSessionRequest};
use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
use dvb_common::Serialize;
fn ser<S: Serialize>(s: &S) -> Vec<u8> {
let mut b = vec![0u8; s.serialized_len()];
match s.serialize_into(&mut b) {
Ok(n) => b.truncate(n),
Err(_) => b.clear(),
}
b
}
fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
v.extend_from_slice(spdu);
v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
v
}
#[test]
fn init_resets_and_opens_transport() {
let mut s = CiStack::new();
let a = s.handle(Event::Host(HostRequest::Init));
assert_eq!(a[0], Action::Reset);
assert_eq!(a[1], Action::QuerySlot);
assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
}
#[test]
fn full_pipeline_opens_a_session_for_a_provided_resource() {
let mut s = CiStack::new();
s.handle(Event::Host(HostRequest::Init));
s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
let osr = ser(&OpenSessionRequest {
resource: RESOURCE_MANAGER,
});
let actions = s.handle(Event::Readable(&r_data(1, &osr)));
assert!(actions.iter().any(|x| matches!(
x,
Action::Notify(Notification::SessionOpened {
resource
}) if *resource == RESOURCE_MANAGER
)));
let wrote_osr = actions.iter().any(|x| match x {
Action::Write(w) => w
.windows(1)
.any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
_ => false,
});
assert!(wrote_osr, "open_session_response must be sent down");
let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
assert!(nb.is_some());
}
#[test]
fn tick_drives_poll_when_active() {
let mut s = CiStack::new();
s.handle(Event::Host(HostRequest::Init));
s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
let a = s.handle(Event::Tick {
elapsed: DEFAULT_POLL_INTERVAL,
});
assert!(a
.iter()
.any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST))));
}
fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
let mut all = Vec::new();
for _ in 0..16 {
let a = s.handle(Event::Readable(&[
tpdu_tags::SB,
0x02,
0x01,
SbValue::new(false).0,
]));
let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
all.extend(a);
if !wrote {
break;
}
}
all
}
fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
use dvb_ci::spdu::SessionNumber;
let mut spdu = ser(&SessionNumber { session_nb });
spdu.extend_from_slice(apdu);
r_data(1, &spdu)
}
fn build_pmt() -> Vec<u8> {
let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
let mut program_info = Vec::new();
program_info.extend_from_slice(&prog_ca);
program_info.extend_from_slice(®);
let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
let mut body = Vec::new();
body.push(0x02); body.push(0);
body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
body.push(0x00);
body.push(0xE0 | 0x02); body.push(0x00);
let pil = program_info.len();
body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
body.push(pil as u8);
body.extend_from_slice(&program_info);
body.push(0x03);
body.push(0xE0 | 0x02);
body.push(0x01);
body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
body.push(lang.len() as u8);
body.extend_from_slice(&lang);
let section_length = body.len() - 3 + 4;
body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
body[2] = section_length as u8;
let crc = dvb_common::crc32_mpeg2::compute(&body);
body.extend_from_slice(&crc.to_be_bytes());
body
}
fn stack_with_ca_session() -> CiStack {
use dvb_ci::objects::ca_info::CaInfo;
use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
let mut s = CiStack::new();
s.handle(Event::Host(HostRequest::Init));
s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
s.handle(Event::Readable(&r_data(
1,
&ser(&OpenSessionRequest {
resource: RESOURCE_MANAGER,
}),
)));
s.handle(Event::Readable(&r_apdu(1, &ser(&ProfileEnq))));
s.handle(Event::Readable(&r_apdu(
1,
&ser(&Profile {
resources: vec![
RESOURCE_MANAGER,
APPLICATION_INFORMATION,
CONDITIONAL_ACCESS_SUPPORT,
MMI,
],
}),
)));
for (nb, res) in [
(2u16, APPLICATION_INFORMATION),
(3, CONDITIONAL_ACCESS_SUPPORT),
(4, MMI),
] {
s.handle(Event::Readable(&r_data(
1,
&ser(&CreateSessionResponse {
status: SessionStatus::Ok,
resource: res,
session_nb: nb,
}),
)));
}
let ca_nb = s
.session
.sessions()
.into_iter()
.find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
.map(|(n, _)| n)
.expect("CA session open");
s.handle(Event::Readable(&r_apdu(
ca_nb,
&ser(&CaInfo {
ca_system_ids: vec![0x0B00, 0x1800],
}),
)));
s
}
#[test]
fn descramble_filters_then_queries_then_oks() {
use dvb_ci::objects::ca_pmt::CaPmtCmdId;
use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
let mut s = stack_with_ca_session();
let ca_nb = s
.session
.sessions()
.into_iter()
.find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
.map(|(n, _)| n)
.unwrap();
let pmt = build_pmt();
let mut query_actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
query_actions.extend(pump_sbs(&mut s));
let q = first_ca_pmt(&query_actions).expect("ca_pmt query sent");
assert_eq!(q.cmd_id, CaPmtCmdId::Query);
assert_eq!(
q.program_ca_descriptors.as_slice(),
&[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
);
let mut ok_actions = s.handle(Event::Readable(&r_apdu(
ca_nb,
&ser(&CaPmtReply {
program_number: 1,
version_number: 1,
current_next_indicator: true,
ca_enable: Some(CaEnable::Possible),
streams: vec![],
}),
)));
assert!(ok_actions.iter().any(|a| matches!(
a,
Action::Notify(Notification::CaPmtReply {
descrambling_ok: true,
..
})
)));
ok_actions.extend(pump_sbs(&mut s));
assert!(
all_ca_pmts(&ok_actions)
.iter()
.any(|c| c.cmd_id == CaPmtCmdId::OkDescrambling),
"ca_pmt ok_descrambling sent after a positive reply"
);
}
#[test]
fn descramble_reply_not_possible_sends_no_ok() {
use dvb_ci::objects::ca_pmt::CaPmtCmdId;
use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
let mut s = stack_with_ca_session();
let ca_nb = s
.session
.sessions()
.into_iter()
.find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
.map(|(n, _)| n)
.unwrap();
let pmt = build_pmt();
let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
actions.extend(s.handle(Event::Readable(&r_apdu(
ca_nb,
&ser(&CaPmtReply {
program_number: 1,
version_number: 1,
current_next_indicator: true,
ca_enable: None,
streams: vec![],
}),
))));
actions.extend(pump_sbs(&mut s));
assert!(
all_ca_pmts(&actions)
.iter()
.all(|c| c.cmd_id != CaPmtCmdId::OkDescrambling),
"no ok_descrambling without a positive reply"
);
}
fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
actions
.iter()
.any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
}
#[test]
fn mmi_menu_answer_sends_menu_answ() {
let mut s = stack_with_ca_session();
let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
acts.extend(pump_sbs(&mut s));
assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
}
#[test]
fn mmi_enquiry_answer_sends_answ() {
let mut s = stack_with_ca_session();
let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
acts.extend(pump_sbs(&mut s));
assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
}
fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
use dvb_ci::objects::ca_pmt::CaPmt;
use dvb_common::Parse;
let tag = [0x9F, 0x80, 0x32];
let mut out = Vec::new();
for a in actions {
if let Action::Write(w) = a {
if let Some(pos) = w.windows(3).position(|x| x == tag) {
if let Ok(p) = CaPmt::parse(&w[pos..]) {
out.push(CaPmtSummary {
cmd_id: p.cmd_id.expect("programme cmd_id present"),
program_ca_descriptors: p.program_ca_descriptors.to_vec(),
});
}
}
}
}
out
}
fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
all_ca_pmts(actions).into_iter().next()
}
struct CaPmtSummary {
cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
program_ca_descriptors: Vec<u8>,
}
}