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_display::{
17 DisplayControl, DisplayControlCmd, DisplayReply, DisplayReplyBody, DisplayReplyId, MmiMode,
18};
19use dvb_ci::objects::mmi_high::{Enq, List, Menu};
20use dvb_ci::objects::resource_manager::{Profile, ProfileChange, ProfileEnq};
21use dvb_ci::resource::{
22 ResourceId, APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI,
23 RESOURCE_MANAGER,
24};
25use dvb_ci::tag::{self, ApduTag};
26use dvb_common::{Parse, Serialize};
27
28use crate::event::{MmiEvent, MmiMenu, Notification};
29
30fn text(chars: &[u8]) -> String {
33 String::from_utf8_lossy(chars).into_owned()
34}
35
36fn to_menu(m: &Menu<'_>) -> MmiMenu {
40 MmiMenu {
41 title: text(m.title.text_chars),
42 subtitle: text(m.subtitle.text_chars),
43 bottom: text(m.bottom.text_chars),
44 choices: m.choices.iter().map(|c| text(c.text_chars)).collect(),
45 }
46}
47
48pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
49 let mut b = vec![0u8; s.serialized_len()];
50 match s.serialize_into(&mut b) {
51 Ok(n) => b.truncate(n),
52 Err(_) => b.clear(),
53 }
54 b
55}
56
57pub(crate) fn peek_tag(apdu: &[u8]) -> Option<ApduTag> {
59 (apdu.len() >= 3).then(|| ApduTag::from_bytes(apdu[0], apdu[1], apdu[2]))
60}
61
62#[derive(Debug, Default, Clone, PartialEq, Eq)]
64pub struct ResourceOut {
65 pub apdus: Vec<Vec<u8>>,
67 pub notify: Vec<Notification>,
69 pub open: Vec<ResourceId>,
71}
72
73pub trait Resource {
75 fn id(&self) -> ResourceId;
77 fn on_open(&mut self) -> ResourceOut {
79 ResourceOut::default()
80 }
81 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut;
83 fn tick(&mut self, _elapsed: Duration) -> ResourceOut {
85 ResourceOut::default()
86 }
87}
88
89#[derive(Debug)]
93pub struct ResourceManager {
94 host_resources: Vec<ResourceId>,
95 module_resources: Vec<ResourceId>,
96 module_profiled: bool,
97 ready: bool,
98}
99
100impl ResourceManager {
101 #[must_use]
103 pub fn new(host_resources: Vec<ResourceId>) -> Self {
104 Self {
105 host_resources,
106 module_resources: Vec::new(),
107 module_profiled: false,
108 ready: false,
109 }
110 }
111
112 #[must_use]
114 pub fn module_resources(&self) -> &[ResourceId] {
115 &self.module_resources
116 }
117}
118
119impl Resource for ResourceManager {
120 fn id(&self) -> ResourceId {
121 RESOURCE_MANAGER
122 }
123
124 fn on_open(&mut self) -> ResourceOut {
125 ResourceOut {
127 apdus: vec![ser(&ProfileEnq)],
128 ..ResourceOut::default()
129 }
130 }
131
132 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
133 let mut out = ResourceOut::default();
134 match peek_tag(apdu) {
135 Some(t) if t == tag::PROFILE_ENQ => {
137 out.apdus.push(ser(&Profile {
138 resources: self.host_resources.clone(),
139 }));
140 }
141 Some(t) if t == tag::PROFILE => {
143 if let Ok(p) = Profile::parse(apdu) {
144 self.module_resources = p.resources;
145 self.module_profiled = true;
146 }
147 }
148 Some(t) if t == tag::PROFILE_CHANGE => {
150 out.apdus.push(ser(&ProfileEnq));
151 self.module_profiled = false;
152 self.ready = false;
153 }
154 _ => {}
155 }
156 if self.module_profiled && !self.ready {
170 self.ready = true;
171 out.apdus.push(ser(&ProfileChange));
172 out.notify.push(Notification::CamReady);
173 }
174 out
175 }
176}
177
178#[derive(Debug, Default)]
181pub struct ApplicationInformation;
182
183impl Resource for ApplicationInformation {
184 fn id(&self) -> ResourceId {
185 APPLICATION_INFORMATION
186 }
187
188 fn on_open(&mut self) -> ResourceOut {
189 ResourceOut {
190 apdus: vec![ser(&ApplicationInfoEnq)],
191 ..ResourceOut::default()
192 }
193 }
194
195 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
196 let mut out = ResourceOut::default();
197 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
198 if let Ok(ai) = ApplicationInfo::parse(apdu) {
199 out.notify.push(Notification::ApplicationInfo {
200 application_type: ai.application_type.to_u8(),
201 manufacturer: ai.application_manufacturer,
202 code: ai.manufacturer_code,
203 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
204 });
205 }
206 }
207 out
208 }
209}
210
211#[derive(Debug, Default)]
216pub struct ConditionalAccess;
217
218impl Resource for ConditionalAccess {
219 fn id(&self) -> ResourceId {
220 CONDITIONAL_ACCESS_SUPPORT
221 }
222
223 fn on_open(&mut self) -> ResourceOut {
224 ResourceOut {
225 apdus: vec![ser(&CaInfoEnq)],
226 ..ResourceOut::default()
227 }
228 }
229
230 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
231 let mut out = ResourceOut::default();
232 match peek_tag(apdu) {
233 Some(t) if t == tag::CA_INFO => {
234 if let Ok(ci) = CaInfo::parse(apdu) {
235 out.notify.push(Notification::CaInfo {
236 ca_system_ids: ci.ca_system_ids,
237 });
238 }
239 }
240 Some(t) if t == tag::CA_PMT_REPLY => {
241 if let Ok(r) = CaPmtReply::parse(apdu) {
242 let descrambling_ok = r.ca_enable.is_some_and(|e| {
243 matches!(
244 e,
245 CaEnable::Possible
246 | CaEnable::PossiblePurchaseDialogue
247 | CaEnable::PossibleTechnicalDialogue
248 )
249 });
250 out.notify.push(Notification::CaPmtReply {
251 program_number: r.program_number,
252 descrambling_ok,
253 });
254 }
255 }
256 _ => {}
257 }
258 out
259 }
260}
261
262const SECS_PER_DAY: u64 = 86_400;
263const MJD_UNIX_EPOCH: u64 = 40_587;
265
266fn bcd(v: u64) -> u8 {
267 (((v / 10) << 4) | (v % 10)) as u8
268}
269
270fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
273 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
274 let sod = unix_secs % SECS_PER_DAY;
275 [
276 (mjd >> 8) as u8,
277 mjd as u8,
278 bcd(sod / 3600),
279 bcd((sod % 3600) / 60),
280 bcd(sod % 60),
281 ]
282}
283
284fn system_utc() -> [u8; UTC_TIME_LEN] {
285 let secs = std::time::SystemTime::now()
286 .duration_since(std::time::UNIX_EPOCH)
287 .map(|d| d.as_secs())
288 .unwrap_or(0);
289 unix_to_mjd_bcd(secs)
290}
291
292pub struct DateTime {
296 clock: fn() -> [u8; UTC_TIME_LEN],
297 interval: u8,
298 since: Duration,
299}
300
301impl Default for DateTime {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307impl DateTime {
308 #[must_use]
310 pub fn new() -> Self {
311 Self {
312 clock: system_utc,
313 interval: 0,
314 since: Duration::ZERO,
315 }
316 }
317
318 #[must_use]
320 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
321 Self {
322 clock,
323 interval: 0,
324 since: Duration::ZERO,
325 }
326 }
327
328 fn reply(&self) -> Vec<u8> {
329 ser(&CiDateTime {
330 utc_time: (self.clock)(),
331 local_offset: None,
332 })
333 }
334}
335
336impl Resource for DateTime {
337 fn id(&self) -> ResourceId {
338 DATE_TIME
339 }
340
341 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
342 let mut out = ResourceOut::default();
343 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
344 if let Ok(enq) = DateTimeEnq::parse(apdu) {
345 self.interval = enq.response_interval;
346 self.since = Duration::ZERO;
347 out.apdus.push(self.reply());
348 }
349 }
350 out
351 }
352
353 fn tick(&mut self, elapsed: Duration) -> ResourceOut {
354 let mut out = ResourceOut::default();
355 if self.interval > 0 {
356 self.since += elapsed;
357 if self.since >= Duration::from_secs(u64::from(self.interval)) {
358 self.since = Duration::ZERO;
359 out.apdus.push(self.reply());
360 }
361 }
362 out
363 }
364}
365
366#[derive(Debug, Default)]
374pub struct Mmi;
375
376impl Resource for Mmi {
377 fn id(&self) -> ResourceId {
378 MMI
379 }
380
381 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
382 let mut out = ResourceOut::default();
383 match peek_tag(apdu) {
384 Some(t) if t == tag::ENQ => {
385 if let Ok(e) = Enq::parse(apdu) {
386 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
387 prompt: text(e.text_chars),
388 blind: e.blind_answer,
389 answer_len: e.answer_text_length,
390 }));
391 }
392 }
393 Some(t) if t == tag::MENU_LAST => {
394 if let Ok(m) = Menu::parse(apdu) {
395 out.notify
396 .push(Notification::Mmi(MmiEvent::Menu(to_menu(&m))));
397 }
398 }
399 Some(t) if t == tag::LIST_LAST => {
400 if let Ok(l) = List::parse(apdu) {
401 out.notify
402 .push(Notification::Mmi(MmiEvent::List(to_menu(&l.0))));
403 }
404 }
405 Some(t) if t == tag::CLOSE_MMI => {
406 out.notify.push(Notification::Mmi(MmiEvent::Close));
407 }
408 Some(t) if t == tag::DISPLAY_CONTROL => {
414 if let Ok(dc) = DisplayControl::parse(apdu) {
415 let reply = match dc.cmd {
416 DisplayControlCmd::SetMmiMode => DisplayReply {
418 reply_id: DisplayReplyId::MmiModeAck,
419 body: DisplayReplyBody::MmiModeAck(
420 dc.mmi_mode.unwrap_or(MmiMode::HighLevel),
421 ),
422 },
423 _ => DisplayReply {
426 reply_id: DisplayReplyId::UnknownDisplayControlCmd,
427 body: DisplayReplyBody::None,
428 },
429 };
430 out.apdus.push(ser(&reply));
431 }
432 }
433 _ => {}
434 }
435 out
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use dvb_ci::objects::resource_manager::Profile;
443
444 #[test]
445 fn on_open_sends_profile_enq() {
446 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
447 let out = rm.on_open();
448 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
449 }
450
451 #[test]
452 fn module_profile_triggers_profile_change_and_camready() {
453 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
459 rm.on_open();
460 let empty_profile = ser(&Profile { resources: vec![] });
461 let o = rm.on_apdu(&empty_profile);
462 assert!(o.notify.contains(&Notification::CamReady));
463 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
464 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
465 assert!(o.open.is_empty(), "host opens no sessions itself");
466 }
467
468 #[test]
469 fn answers_a_module_profile_enquiry_without_re_readying() {
470 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
471 rm.on_open();
472 rm.on_apdu(&ser(&Profile {
473 resources: vec![APPLICATION_INFORMATION],
474 }));
475 let o = rm.on_apdu(&ser(&ProfileEnq));
477 assert_eq!(o.apdus.len(), 1);
478 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
479 assert!(!o.notify.contains(&Notification::CamReady));
480 }
481
482 #[test]
483 fn mmi_surfaces_enquiry_and_close() {
484 let mut h = Mmi;
485 let enq = ser(&Enq {
487 blind_answer: true,
488 answer_text_length: 4,
489 text_chars: b"PIN?",
490 });
491 assert_eq!(
492 h.on_apdu(&enq).notify,
493 vec![Notification::Mmi(MmiEvent::Enquiry {
494 prompt: "PIN?".to_string(),
495 blind: true,
496 answer_len: 4,
497 })]
498 );
499 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
501 assert_eq!(
502 h.on_apdu(&close).notify,
503 vec![Notification::Mmi(MmiEvent::Close)]
504 );
505 }
506
507 #[test]
508 fn mmi_surfaces_structured_menu_and_list() {
509 use dvb_ci::objects::mmi_high::{List, Menu, Text};
510 let txt = |s: &'static [u8]| Text {
511 more: false,
512 text_chars: s,
513 };
514 let mut h = Mmi;
515 let menu = ser(&Menu {
517 more: false,
518 choice_nb: 2,
519 title: txt(b"AlphaCrypt"),
520 subtitle: txt(b"Module Mainmenu"),
521 bottom: txt(b"Select item and press OK"),
522 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
523 });
524 assert_eq!(
525 h.on_apdu(&menu).notify,
526 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
527 title: "AlphaCrypt".to_string(),
528 subtitle: "Module Mainmenu".to_string(),
529 bottom: "Select item and press OK".to_string(),
530 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
531 }))]
532 );
533 let list = ser(&List(Menu {
535 more: false,
536 choice_nb: 0xFF,
537 title: txt(b"Entitlements"),
538 subtitle: txt(b""),
539 bottom: txt(b""),
540 choices: vec![txt(b"ORF AUT")],
541 }));
542 assert_eq!(
543 h.on_apdu(&list).notify,
544 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
545 title: "Entitlements".to_string(),
546 subtitle: String::new(),
547 bottom: String::new(),
548 choices: vec!["ORF AUT".to_string()],
549 }))]
550 );
551 }
552
553 #[test]
554 fn mmi_answers_display_control_set_mmi_mode() {
555 use dvb_ci::objects::mmi_display::{DisplayControl, DisplayControlCmd, MmiMode};
556 let mut h = Mmi;
557 let dc = ser(&DisplayControl {
558 cmd: DisplayControlCmd::SetMmiMode,
559 mmi_mode: Some(MmiMode::HighLevel),
560 });
561 let out = h.on_apdu(&dc);
562 assert_eq!(out.apdus, vec![vec![0x9F, 0x88, 0x02, 0x02, 0x01, 0x01]]);
564 assert!(out.notify.is_empty());
565 }
566
567 #[test]
568 fn profile_change_re_enquires() {
569 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
570 let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
571 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
572 }
573
574 #[test]
575 fn application_information_surfaces_notification() {
576 use dvb_ci::objects::application_info::ApplicationType;
577 let mut h = ApplicationInformation;
578 assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
579 let ai = ser(&ApplicationInfo {
580 application_type: ApplicationType::ConditionalAccess,
581 application_manufacturer: 0x1234,
582 manufacturer_code: 0x5678,
583 menu_string: b"Acme CAM",
584 });
585 let out = h.on_apdu(&ai);
586 assert_eq!(
587 out.notify,
588 vec![Notification::ApplicationInfo {
589 application_type: 0x01,
590 manufacturer: 0x1234,
591 code: 0x5678,
592 menu: "Acme CAM".to_string(),
593 }]
594 );
595 }
596
597 #[test]
598 fn mjd_bcd_encoding_is_correct() {
599 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
601 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
603 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
604 }
605
606 #[test]
607 fn date_time_replies_to_enq_and_resends_on_interval() {
608 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
609 let mut h = DateTime::with_clock(fixed);
610 let enq = ser(&DateTimeEnq {
612 response_interval: 5,
613 });
614 let out = h.on_apdu(&enq);
615 assert_eq!(out.apdus.len(), 1);
616 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
617 assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
619 assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
621 }
622
623 #[test]
624 fn date_time_interval_zero_does_not_resend() {
625 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
626 h.on_apdu(&ser(&DateTimeEnq {
627 response_interval: 0,
628 }));
629 assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
630 }
631
632 #[test]
633 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
634 let mut h = ConditionalAccess;
635 assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
636 let ci = ser(&CaInfo {
638 ca_system_ids: vec![0x0B00, 0x1800],
639 });
640 assert_eq!(
641 h.on_apdu(&ci).notify,
642 vec![Notification::CaInfo {
643 ca_system_ids: vec![0x0B00, 0x1800],
644 }]
645 );
646 let reply = ser(&CaPmtReply {
648 program_number: 0x0042,
649 version_number: 0,
650 current_next_indicator: true,
651 ca_enable: Some(CaEnable::Possible),
652 streams: vec![],
653 });
654 assert_eq!(
655 h.on_apdu(&reply).notify,
656 vec![Notification::CaPmtReply {
657 program_number: 0x0042,
658 descrambling_ok: true,
659 }]
660 );
661 }
662}