1use std::time::Duration;
11
12use dvb_ci::objects::application_info::{ApplicationInfo, ApplicationInfoEnq};
13use dvb_ci::objects::ca_info::{CaInfo, CaInfoEnq};
14use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
15use dvb_ci::objects::date_time::{DateTime as CiDateTime, DateTimeEnq, UTC_TIME_LEN};
16use dvb_ci::objects::mmi_high::{Enq, Menu};
17use dvb_ci::objects::resource_manager::{Profile, ProfileChange, ProfileEnq};
18use dvb_ci::resource::{
19 ResourceId, APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI,
20 RESOURCE_MANAGER,
21};
22use dvb_ci::tag::{self, ApduTag};
23use dvb_common::{Parse, Serialize};
24
25use crate::event::{MmiEvent, Notification};
26
27fn text(chars: &[u8]) -> String {
30 String::from_utf8_lossy(chars).into_owned()
31}
32
33pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
34 let mut b = vec![0u8; s.serialized_len()];
35 match s.serialize_into(&mut b) {
36 Ok(n) => b.truncate(n),
37 Err(_) => b.clear(),
38 }
39 b
40}
41
42pub(crate) fn peek_tag(apdu: &[u8]) -> Option<ApduTag> {
44 (apdu.len() >= 3).then(|| ApduTag::from_bytes(apdu[0], apdu[1], apdu[2]))
45}
46
47#[derive(Debug, Default, Clone, PartialEq, Eq)]
49pub struct ResourceOut {
50 pub apdus: Vec<Vec<u8>>,
52 pub notify: Vec<Notification>,
54 pub open: Vec<ResourceId>,
56}
57
58pub trait Resource {
60 fn id(&self) -> ResourceId;
62 fn on_open(&mut self) -> ResourceOut {
64 ResourceOut::default()
65 }
66 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut;
68 fn tick(&mut self, _elapsed: Duration) -> ResourceOut {
70 ResourceOut::default()
71 }
72}
73
74#[derive(Debug)]
78pub struct ResourceManager {
79 host_resources: Vec<ResourceId>,
80 module_resources: Vec<ResourceId>,
81 module_profiled: bool,
82 ready: bool,
83}
84
85impl ResourceManager {
86 #[must_use]
88 pub fn new(host_resources: Vec<ResourceId>) -> Self {
89 Self {
90 host_resources,
91 module_resources: Vec::new(),
92 module_profiled: false,
93 ready: false,
94 }
95 }
96
97 #[must_use]
99 pub fn module_resources(&self) -> &[ResourceId] {
100 &self.module_resources
101 }
102}
103
104impl Resource for ResourceManager {
105 fn id(&self) -> ResourceId {
106 RESOURCE_MANAGER
107 }
108
109 fn on_open(&mut self) -> ResourceOut {
110 ResourceOut {
112 apdus: vec![ser(&ProfileEnq)],
113 ..ResourceOut::default()
114 }
115 }
116
117 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
118 let mut out = ResourceOut::default();
119 match peek_tag(apdu) {
120 Some(t) if t == tag::PROFILE_ENQ => {
122 out.apdus.push(ser(&Profile {
123 resources: self.host_resources.clone(),
124 }));
125 }
126 Some(t) if t == tag::PROFILE => {
128 if let Ok(p) = Profile::parse(apdu) {
129 self.module_resources = p.resources;
130 self.module_profiled = true;
131 }
132 }
133 Some(t) if t == tag::PROFILE_CHANGE => {
135 out.apdus.push(ser(&ProfileEnq));
136 self.module_profiled = false;
137 self.ready = false;
138 }
139 _ => {}
140 }
141 if self.module_profiled && !self.ready {
149 self.ready = true;
150 out.apdus.push(ser(&ProfileChange));
151 out.notify.push(Notification::CamReady);
152 }
153 out
154 }
155}
156
157#[derive(Debug, Default)]
160pub struct ApplicationInformation;
161
162impl Resource for ApplicationInformation {
163 fn id(&self) -> ResourceId {
164 APPLICATION_INFORMATION
165 }
166
167 fn on_open(&mut self) -> ResourceOut {
168 ResourceOut {
169 apdus: vec![ser(&ApplicationInfoEnq)],
170 ..ResourceOut::default()
171 }
172 }
173
174 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
175 let mut out = ResourceOut::default();
176 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
177 if let Ok(ai) = ApplicationInfo::parse(apdu) {
178 out.notify.push(Notification::ApplicationInfo {
179 application_type: ai.application_type.to_u8(),
180 manufacturer: ai.application_manufacturer,
181 code: ai.manufacturer_code,
182 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
183 });
184 }
185 }
186 out
187 }
188}
189
190#[derive(Debug, Default)]
195pub struct ConditionalAccess;
196
197impl Resource for ConditionalAccess {
198 fn id(&self) -> ResourceId {
199 CONDITIONAL_ACCESS_SUPPORT
200 }
201
202 fn on_open(&mut self) -> ResourceOut {
203 ResourceOut {
204 apdus: vec![ser(&CaInfoEnq)],
205 ..ResourceOut::default()
206 }
207 }
208
209 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
210 let mut out = ResourceOut::default();
211 match peek_tag(apdu) {
212 Some(t) if t == tag::CA_INFO => {
213 if let Ok(ci) = CaInfo::parse(apdu) {
214 out.notify.push(Notification::CaInfo {
215 ca_system_ids: ci.ca_system_ids,
216 });
217 }
218 }
219 Some(t) if t == tag::CA_PMT_REPLY => {
220 if let Ok(r) = CaPmtReply::parse(apdu) {
221 let descrambling_ok = r.ca_enable.is_some_and(|e| {
222 matches!(
223 e,
224 CaEnable::Possible
225 | CaEnable::PossiblePurchaseDialogue
226 | CaEnable::PossibleTechnicalDialogue
227 )
228 });
229 out.notify.push(Notification::CaPmtReply {
230 program_number: r.program_number,
231 descrambling_ok,
232 });
233 }
234 }
235 _ => {}
236 }
237 out
238 }
239}
240
241const SECS_PER_DAY: u64 = 86_400;
242const MJD_UNIX_EPOCH: u64 = 40_587;
244
245fn bcd(v: u64) -> u8 {
246 (((v / 10) << 4) | (v % 10)) as u8
247}
248
249fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
252 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
253 let sod = unix_secs % SECS_PER_DAY;
254 [
255 (mjd >> 8) as u8,
256 mjd as u8,
257 bcd(sod / 3600),
258 bcd((sod % 3600) / 60),
259 bcd(sod % 60),
260 ]
261}
262
263fn system_utc() -> [u8; UTC_TIME_LEN] {
264 let secs = std::time::SystemTime::now()
265 .duration_since(std::time::UNIX_EPOCH)
266 .map(|d| d.as_secs())
267 .unwrap_or(0);
268 unix_to_mjd_bcd(secs)
269}
270
271pub struct DateTime {
275 clock: fn() -> [u8; UTC_TIME_LEN],
276 interval: u8,
277 since: Duration,
278}
279
280impl Default for DateTime {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286impl DateTime {
287 #[must_use]
289 pub fn new() -> Self {
290 Self {
291 clock: system_utc,
292 interval: 0,
293 since: Duration::ZERO,
294 }
295 }
296
297 #[must_use]
299 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
300 Self {
301 clock,
302 interval: 0,
303 since: Duration::ZERO,
304 }
305 }
306
307 fn reply(&self) -> Vec<u8> {
308 ser(&CiDateTime {
309 utc_time: (self.clock)(),
310 local_offset: None,
311 })
312 }
313}
314
315impl Resource for DateTime {
316 fn id(&self) -> ResourceId {
317 DATE_TIME
318 }
319
320 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
321 let mut out = ResourceOut::default();
322 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
323 if let Ok(enq) = DateTimeEnq::parse(apdu) {
324 self.interval = enq.response_interval;
325 self.since = Duration::ZERO;
326 out.apdus.push(self.reply());
327 }
328 }
329 out
330 }
331
332 fn tick(&mut self, elapsed: Duration) -> ResourceOut {
333 let mut out = ResourceOut::default();
334 if self.interval > 0 {
335 self.since += elapsed;
336 if self.since >= Duration::from_secs(u64::from(self.interval)) {
337 self.since = Duration::ZERO;
338 out.apdus.push(self.reply());
339 }
340 }
341 out
342 }
343}
344
345#[derive(Debug, Default)]
350pub struct Mmi;
351
352impl Resource for Mmi {
353 fn id(&self) -> ResourceId {
354 MMI
355 }
356
357 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
358 let mut out = ResourceOut::default();
359 match peek_tag(apdu) {
360 Some(t) if t == tag::ENQ => {
361 if let Ok(e) = Enq::parse(apdu) {
362 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
363 prompt: text(e.text_chars),
364 blind: e.blind_answer,
365 answer_len: e.answer_text_length,
366 }));
367 }
368 }
369 Some(t) if t == tag::MENU_LAST => {
370 if let Ok(m) = Menu::parse(apdu) {
371 out.notify.push(Notification::Mmi(MmiEvent::Menu {
372 title: text(m.title.text_chars),
373 items: m.choices.iter().map(|c| text(c.text_chars)).collect(),
374 }));
375 }
376 }
377 Some(t) if t == tag::CLOSE_MMI => {
378 out.notify.push(Notification::Mmi(MmiEvent::Close));
379 }
380 _ => {}
381 }
382 out
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use dvb_ci::objects::resource_manager::Profile;
390
391 #[test]
392 fn on_open_sends_profile_enq() {
393 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
394 let out = rm.on_open();
395 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
396 }
397
398 #[test]
399 fn module_profile_triggers_profile_change_and_camready() {
400 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
404 rm.on_open();
405 let module_profile = ser(&Profile {
406 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT],
407 });
408 let o = rm.on_apdu(&module_profile);
409 assert!(o.notify.contains(&Notification::CamReady));
410 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
411 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
412 assert!(
413 o.open.is_empty(),
414 "the module opens its own sessions, not the host"
415 );
416 }
417
418 #[test]
419 fn answers_a_module_profile_enquiry_without_re_readying() {
420 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
421 rm.on_open();
422 rm.on_apdu(&ser(&Profile {
423 resources: vec![APPLICATION_INFORMATION],
424 }));
425 let o = rm.on_apdu(&ser(&ProfileEnq));
427 assert_eq!(o.apdus.len(), 1);
428 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
429 assert!(!o.notify.contains(&Notification::CamReady));
430 }
431
432 #[test]
433 fn mmi_surfaces_enquiry_and_close() {
434 let mut h = Mmi;
435 let enq = ser(&Enq {
437 blind_answer: true,
438 answer_text_length: 4,
439 text_chars: b"PIN?",
440 });
441 assert_eq!(
442 h.on_apdu(&enq).notify,
443 vec![Notification::Mmi(MmiEvent::Enquiry {
444 prompt: "PIN?".to_string(),
445 blind: true,
446 answer_len: 4,
447 })]
448 );
449 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
451 assert_eq!(
452 h.on_apdu(&close).notify,
453 vec![Notification::Mmi(MmiEvent::Close)]
454 );
455 }
456
457 #[test]
458 fn profile_change_re_enquires() {
459 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
460 let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
461 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
462 }
463
464 #[test]
465 fn application_information_surfaces_notification() {
466 use dvb_ci::objects::application_info::ApplicationType;
467 let mut h = ApplicationInformation;
468 assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
469 let ai = ser(&ApplicationInfo {
470 application_type: ApplicationType::ConditionalAccess,
471 application_manufacturer: 0x1234,
472 manufacturer_code: 0x5678,
473 menu_string: b"Acme CAM",
474 });
475 let out = h.on_apdu(&ai);
476 assert_eq!(
477 out.notify,
478 vec![Notification::ApplicationInfo {
479 application_type: 0x01,
480 manufacturer: 0x1234,
481 code: 0x5678,
482 menu: "Acme CAM".to_string(),
483 }]
484 );
485 }
486
487 #[test]
488 fn mjd_bcd_encoding_is_correct() {
489 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
491 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
493 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
494 }
495
496 #[test]
497 fn date_time_replies_to_enq_and_resends_on_interval() {
498 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
499 let mut h = DateTime::with_clock(fixed);
500 let enq = ser(&DateTimeEnq {
502 response_interval: 5,
503 });
504 let out = h.on_apdu(&enq);
505 assert_eq!(out.apdus.len(), 1);
506 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
507 assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
509 assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
511 }
512
513 #[test]
514 fn date_time_interval_zero_does_not_resend() {
515 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
516 h.on_apdu(&ser(&DateTimeEnq {
517 response_interval: 0,
518 }));
519 assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
520 }
521
522 #[test]
523 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
524 let mut h = ConditionalAccess;
525 assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
526 let ci = ser(&CaInfo {
528 ca_system_ids: vec![0x0B00, 0x1800],
529 });
530 assert_eq!(
531 h.on_apdu(&ci).notify,
532 vec![Notification::CaInfo {
533 ca_system_ids: vec![0x0B00, 0x1800],
534 }]
535 );
536 let reply = ser(&CaPmtReply {
538 program_number: 0x0042,
539 version_number: 0,
540 current_next_indicator: true,
541 ca_enable: Some(CaEnable::Possible),
542 streams: vec![],
543 });
544 assert_eq!(
545 h.on_apdu(&reply).notify,
546 vec![Notification::CaPmtReply {
547 program_number: 0x0042,
548 descrambling_ok: true,
549 }]
550 );
551 }
552}