use std::time::Duration;
use dvb_ci::objects::application_info::{ApplicationInfo, ApplicationInfoEnq};
use dvb_ci::objects::ca_info::{CaInfo, CaInfoEnq};
use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
use dvb_ci::objects::date_time::{DateTime as CiDateTime, DateTimeEnq, UTC_TIME_LEN};
use dvb_ci::objects::mmi_high::{Enq, Menu};
use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
use dvb_ci::resource::{
ResourceId, APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI,
RESOURCE_MANAGER,
};
use dvb_ci::tag::{self, ApduTag};
use dvb_common::{Parse, Serialize};
use crate::event::{MmiEvent, Notification};
fn text(chars: &[u8]) -> String {
String::from_utf8_lossy(chars).into_owned()
}
pub(crate) 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
}
pub(crate) fn peek_tag(apdu: &[u8]) -> Option<ApduTag> {
(apdu.len() >= 3).then(|| ApduTag::from_bytes(apdu[0], apdu[1], apdu[2]))
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ResourceOut {
pub apdus: Vec<Vec<u8>>,
pub notify: Vec<Notification>,
pub open: Vec<ResourceId>,
}
pub trait Resource {
fn id(&self) -> ResourceId;
fn on_open(&mut self) -> ResourceOut {
ResourceOut::default()
}
fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut;
fn tick(&mut self, _elapsed: Duration) -> ResourceOut {
ResourceOut::default()
}
}
#[derive(Debug)]
pub struct ResourceManager {
host_resources: Vec<ResourceId>,
module_resources: Vec<ResourceId>,
host_profiled: bool,
module_profiled: bool,
ready: bool,
}
impl ResourceManager {
#[must_use]
pub fn new(host_resources: Vec<ResourceId>) -> Self {
Self {
host_resources,
module_resources: Vec::new(),
host_profiled: false,
module_profiled: false,
ready: false,
}
}
#[must_use]
pub fn module_resources(&self) -> &[ResourceId] {
&self.module_resources
}
}
impl Resource for ResourceManager {
fn id(&self) -> ResourceId {
RESOURCE_MANAGER
}
fn on_open(&mut self) -> ResourceOut {
ResourceOut {
apdus: vec![ser(&ProfileEnq)],
..ResourceOut::default()
}
}
fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
let mut out = ResourceOut::default();
match peek_tag(apdu) {
Some(t) if t == tag::PROFILE_ENQ => {
out.apdus.push(ser(&Profile {
resources: self.host_resources.clone(),
}));
self.host_profiled = true;
}
Some(t) if t == tag::PROFILE => {
if let Ok(p) = Profile::parse(apdu) {
self.module_resources = p.resources;
self.module_profiled = true;
}
}
Some(t) if t == tag::PROFILE_CHANGE => {
out.apdus.push(ser(&ProfileEnq));
self.module_profiled = false;
self.ready = false;
}
_ => {}
}
if self.module_profiled && self.host_profiled && !self.ready {
self.ready = true;
out.notify.push(Notification::CamReady);
for r in [APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI] {
if self.module_resources.contains(&r) {
out.open.push(r);
}
}
}
out
}
}
#[derive(Debug, Default)]
pub struct ApplicationInformation;
impl Resource for ApplicationInformation {
fn id(&self) -> ResourceId {
APPLICATION_INFORMATION
}
fn on_open(&mut self) -> ResourceOut {
ResourceOut {
apdus: vec![ser(&ApplicationInfoEnq)],
..ResourceOut::default()
}
}
fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
let mut out = ResourceOut::default();
if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
if let Ok(ai) = ApplicationInfo::parse(apdu) {
out.notify.push(Notification::ApplicationInfo {
application_type: ai.application_type.to_u8(),
manufacturer: ai.application_manufacturer,
code: ai.manufacturer_code,
menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
});
}
}
out
}
}
#[derive(Debug, Default)]
pub struct ConditionalAccess;
impl Resource for ConditionalAccess {
fn id(&self) -> ResourceId {
CONDITIONAL_ACCESS_SUPPORT
}
fn on_open(&mut self) -> ResourceOut {
ResourceOut {
apdus: vec![ser(&CaInfoEnq)],
..ResourceOut::default()
}
}
fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
let mut out = ResourceOut::default();
match peek_tag(apdu) {
Some(t) if t == tag::CA_INFO => {
if let Ok(ci) = CaInfo::parse(apdu) {
out.notify.push(Notification::CaInfo {
ca_system_ids: ci.ca_system_ids,
});
}
}
Some(t) if t == tag::CA_PMT_REPLY => {
if let Ok(r) = CaPmtReply::parse(apdu) {
let descrambling_ok = r.ca_enable.is_some_and(|e| {
matches!(
e,
CaEnable::Possible
| CaEnable::PossiblePurchaseDialogue
| CaEnable::PossibleTechnicalDialogue
)
});
out.notify.push(Notification::CaPmtReply {
program_number: r.program_number,
descrambling_ok,
});
}
}
_ => {}
}
out
}
}
const SECS_PER_DAY: u64 = 86_400;
const MJD_UNIX_EPOCH: u64 = 40_587;
fn bcd(v: u64) -> u8 {
(((v / 10) << 4) | (v % 10)) as u8
}
fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
let sod = unix_secs % SECS_PER_DAY;
[
(mjd >> 8) as u8,
mjd as u8,
bcd(sod / 3600),
bcd((sod % 3600) / 60),
bcd(sod % 60),
]
}
fn system_utc() -> [u8; UTC_TIME_LEN] {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
unix_to_mjd_bcd(secs)
}
pub struct DateTime {
clock: fn() -> [u8; UTC_TIME_LEN],
interval: u8,
since: Duration,
}
impl Default for DateTime {
fn default() -> Self {
Self::new()
}
}
impl DateTime {
#[must_use]
pub fn new() -> Self {
Self {
clock: system_utc,
interval: 0,
since: Duration::ZERO,
}
}
#[must_use]
pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
Self {
clock,
interval: 0,
since: Duration::ZERO,
}
}
fn reply(&self) -> Vec<u8> {
ser(&CiDateTime {
utc_time: (self.clock)(),
local_offset: None,
})
}
}
impl Resource for DateTime {
fn id(&self) -> ResourceId {
DATE_TIME
}
fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
let mut out = ResourceOut::default();
if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
if let Ok(enq) = DateTimeEnq::parse(apdu) {
self.interval = enq.response_interval;
self.since = Duration::ZERO;
out.apdus.push(self.reply());
}
}
out
}
fn tick(&mut self, elapsed: Duration) -> ResourceOut {
let mut out = ResourceOut::default();
if self.interval > 0 {
self.since += elapsed;
if self.since >= Duration::from_secs(u64::from(self.interval)) {
self.since = Duration::ZERO;
out.apdus.push(self.reply());
}
}
out
}
}
#[derive(Debug, Default)]
pub struct Mmi;
impl Resource for Mmi {
fn id(&self) -> ResourceId {
MMI
}
fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
let mut out = ResourceOut::default();
match peek_tag(apdu) {
Some(t) if t == tag::ENQ => {
if let Ok(e) = Enq::parse(apdu) {
out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
prompt: text(e.text_chars),
blind: e.blind_answer,
answer_len: e.answer_text_length,
}));
}
}
Some(t) if t == tag::MENU_LAST => {
if let Ok(m) = Menu::parse(apdu) {
out.notify.push(Notification::Mmi(MmiEvent::Menu {
title: text(m.title.text_chars),
items: m.choices.iter().map(|c| text(c.text_chars)).collect(),
}));
}
}
Some(t) if t == tag::CLOSE_MMI => {
out.notify.push(Notification::Mmi(MmiEvent::Close));
}
_ => {}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use dvb_ci::objects::resource_manager::Profile;
#[test]
fn on_open_sends_profile_enq() {
let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
let out = rm.on_open();
assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
}
#[test]
fn handshake_completes_and_opens_module_resources() {
let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
rm.on_open();
let module_profile = ser(&Profile {
resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT],
});
let o1 = rm.on_apdu(&module_profile);
assert!(
o1.notify.is_empty(),
"not ready until host profile sent too"
);
let o2 = rm.on_apdu(&ser(&ProfileEnq));
assert_eq!(o2.apdus.len(), 1);
assert_eq!(peek_tag(&o2.apdus[0]), Some(tag::PROFILE));
assert!(o2.notify.contains(&Notification::CamReady));
assert!(o2.open.contains(&APPLICATION_INFORMATION));
assert!(o2.open.contains(&CONDITIONAL_ACCESS_SUPPORT));
assert!(!o2.open.contains(&MMI), "module didn't advertise MMI");
}
#[test]
fn mmi_surfaces_enquiry_and_close() {
let mut h = Mmi;
let enq = ser(&Enq {
blind_answer: true,
answer_text_length: 4,
text_chars: b"PIN?",
});
assert_eq!(
h.on_apdu(&enq).notify,
vec![Notification::Mmi(MmiEvent::Enquiry {
prompt: "PIN?".to_string(),
blind: true,
answer_len: 4,
})]
);
let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
assert_eq!(
h.on_apdu(&close).notify,
vec![Notification::Mmi(MmiEvent::Close)]
);
}
#[test]
fn profile_change_re_enquires() {
let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
}
#[test]
fn application_information_surfaces_notification() {
use dvb_ci::objects::application_info::ApplicationType;
let mut h = ApplicationInformation;
assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
let ai = ser(&ApplicationInfo {
application_type: ApplicationType::ConditionalAccess,
application_manufacturer: 0x1234,
manufacturer_code: 0x5678,
menu_string: b"Acme CAM",
});
let out = h.on_apdu(&ai);
assert_eq!(
out.notify,
vec![Notification::ApplicationInfo {
application_type: 0x01,
manufacturer: 0x1234,
code: 0x5678,
menu: "Acme CAM".to_string(),
}]
);
}
#[test]
fn mjd_bcd_encoding_is_correct() {
assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
}
#[test]
fn date_time_replies_to_enq_and_resends_on_interval() {
let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
let mut h = DateTime::with_clock(fixed);
let enq = ser(&DateTimeEnq {
response_interval: 5,
});
let out = h.on_apdu(&enq);
assert_eq!(out.apdus.len(), 1);
assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
}
#[test]
fn date_time_interval_zero_does_not_resend() {
let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
h.on_apdu(&ser(&DateTimeEnq {
response_interval: 0,
}));
assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
}
#[test]
fn conditional_access_surfaces_ca_info_and_pmt_reply() {
let mut h = ConditionalAccess;
assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
let ci = ser(&CaInfo {
ca_system_ids: vec![0x0B00, 0x1800],
});
assert_eq!(
h.on_apdu(&ci).notify,
vec![Notification::CaInfo {
ca_system_ids: vec![0x0B00, 0x1800],
}]
);
let reply = ser(&CaPmtReply {
program_number: 0x0042,
version_number: 0,
current_next_indicator: true,
ca_enable: Some(CaEnable::Possible),
streams: vec![],
});
assert_eq!(
h.on_apdu(&reply).notify,
vec![Notification::CaPmtReply {
program_number: 0x0042,
descrambling_ok: true,
}]
);
}
}