1use std::time::Duration;
11
12use broadcast_common::{Parse, Serialize};
13use dvb_ci::objects::application_info::{ApplicationInfo, ApplicationInfoEnq};
14use dvb_ci::objects::ca_info::{CaInfo, CaInfoEnq};
15use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
16use dvb_ci::objects::date_time::{DateTime as CiDateTime, DateTimeEnq, UTC_TIME_LEN};
17use dvb_ci::objects::host_control::{AskRelease, ClearReplace, Replace, Tune};
18use dvb_ci::objects::mmi_display::{
19 DisplayControl, DisplayControlCmd, DisplayReply, DisplayReplyBody, DisplayReplyId, MmiMode,
20};
21use dvb_ci::objects::mmi_high::{Enq, List, Menu};
22use dvb_ci::objects::resource_manager::{Profile, ProfileChange, ProfileEnq};
23use dvb_ci::resource::{
24 ResourceId, APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, HOST_CONTROL, MMI,
25 RESOURCE_MANAGER,
26};
27use dvb_ci::tag::{self, ApduTag};
28
29use crate::event::{HostControlEvent, MmiEvent, MmiMenu, Notification};
30
31fn text(chars: &[u8]) -> String {
34 String::from_utf8_lossy(chars).into_owned()
35}
36
37fn to_menu(m: &Menu<'_>) -> MmiMenu {
41 MmiMenu {
42 title: text(m.title.text_chars),
43 subtitle: text(m.subtitle.text_chars),
44 bottom: text(m.bottom.text_chars),
45 choices: m.choices.iter().map(|c| text(c.text_chars)).collect(),
46 }
47}
48
49pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
50 let mut b = vec![0u8; s.serialized_len()];
51 match s.serialize_into(&mut b) {
52 Ok(n) => b.truncate(n),
53 Err(_) => b.clear(),
54 }
55 b
56}
57
58pub(crate) fn peek_tag(apdu: &[u8]) -> Option<ApduTag> {
60 (apdu.len() >= 3).then(|| ApduTag::from_bytes(apdu[0], apdu[1], apdu[2]))
61}
62
63#[derive(Debug, Default, Clone, PartialEq, Eq)]
65pub struct ResourceOut {
66 pub apdus: Vec<Vec<u8>>,
68 pub notify: Vec<Notification>,
70 pub open: Vec<ResourceId>,
72}
73
74pub trait Resource {
76 fn id(&self) -> ResourceId;
78 fn on_open(&mut self) -> ResourceOut {
80 ResourceOut::default()
81 }
82 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut;
84 fn tick(&mut self, _elapsed: Duration) -> ResourceOut {
86 ResourceOut::default()
87 }
88}
89
90#[derive(Debug)]
94pub struct ResourceManager {
95 host_resources: Vec<ResourceId>,
96 module_resources: Vec<ResourceId>,
97 module_profiled: bool,
98 ready: bool,
99}
100
101impl ResourceManager {
102 #[must_use]
104 pub fn new(host_resources: Vec<ResourceId>) -> Self {
105 Self {
106 host_resources,
107 module_resources: Vec::new(),
108 module_profiled: false,
109 ready: false,
110 }
111 }
112
113 #[must_use]
115 pub fn module_resources(&self) -> &[ResourceId] {
116 &self.module_resources
117 }
118}
119
120impl Resource for ResourceManager {
121 fn id(&self) -> ResourceId {
122 RESOURCE_MANAGER
123 }
124
125 fn on_open(&mut self) -> ResourceOut {
126 ResourceOut {
128 apdus: vec![ser(&ProfileEnq)],
129 ..ResourceOut::default()
130 }
131 }
132
133 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
134 let mut out = ResourceOut::default();
135 match peek_tag(apdu) {
136 Some(t) if t == tag::PROFILE_ENQ => {
138 out.apdus.push(ser(&Profile {
139 resources: self.host_resources.clone(),
140 }));
141 }
142 Some(t) if t == tag::PROFILE => {
144 if let Ok(p) = Profile::parse(apdu) {
145 self.module_resources = p.resources;
146 self.module_profiled = true;
147 }
148 }
149 Some(t) if t == tag::PROFILE_CHANGE => {
151 out.apdus.push(ser(&ProfileEnq));
152 self.module_profiled = false;
153 self.ready = false;
154 }
155 _ => {}
156 }
157 if self.module_profiled && !self.ready {
171 self.ready = true;
172 out.apdus.push(ser(&ProfileChange));
173 out.notify.push(Notification::CamReady);
174 }
175 out
176 }
177}
178
179#[derive(Debug, Default)]
182pub struct ApplicationInformation;
183
184impl Resource for ApplicationInformation {
185 fn id(&self) -> ResourceId {
186 APPLICATION_INFORMATION
187 }
188
189 fn on_open(&mut self) -> ResourceOut {
190 ResourceOut {
191 apdus: vec![ser(&ApplicationInfoEnq)],
192 ..ResourceOut::default()
193 }
194 }
195
196 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
197 let mut out = ResourceOut::default();
198 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
199 if let Ok(ai) = ApplicationInfo::parse(apdu) {
200 out.notify.push(Notification::ApplicationInfo {
201 application_type: ai.application_type.to_u8(),
202 manufacturer: ai.application_manufacturer,
203 code: ai.manufacturer_code,
204 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
205 });
206 }
207 }
208 out
209 }
210}
211
212#[derive(Debug, Default)]
217pub struct ConditionalAccess;
218
219impl Resource for ConditionalAccess {
220 fn id(&self) -> ResourceId {
221 CONDITIONAL_ACCESS_SUPPORT
222 }
223
224 fn on_open(&mut self) -> ResourceOut {
225 ResourceOut {
226 apdus: vec![ser(&CaInfoEnq)],
227 ..ResourceOut::default()
228 }
229 }
230
231 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
232 let mut out = ResourceOut::default();
233 match peek_tag(apdu) {
234 Some(t) if t == tag::CA_INFO => {
235 if let Ok(ci) = CaInfo::parse(apdu) {
236 out.notify.push(Notification::CaInfo {
237 ca_system_ids: ci.ca_system_ids,
238 });
239 }
240 }
241 Some(t) if t == tag::CA_PMT_REPLY => {
242 if let Ok(r) = CaPmtReply::parse(apdu) {
243 let descrambling_ok = r.ca_enable.is_some_and(|e| {
244 matches!(
245 e,
246 CaEnable::Possible
247 | CaEnable::PossiblePurchaseDialogue
248 | CaEnable::PossibleTechnicalDialogue
249 )
250 });
251 out.notify.push(Notification::CaPmtReply {
252 program_number: r.program_number,
253 descrambling_ok,
254 });
255 }
256 }
257 _ => {}
258 }
259 out
260 }
261}
262
263const SECS_PER_DAY: u64 = 86_400;
264const MJD_UNIX_EPOCH: u64 = 40_587;
266
267fn bcd(v: u64) -> u8 {
268 (((v / 10) << 4) | (v % 10)) as u8
269}
270
271fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
274 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
275 let sod = unix_secs % SECS_PER_DAY;
276 [
277 (mjd >> 8) as u8,
278 mjd as u8,
279 bcd(sod / 3600),
280 bcd((sod % 3600) / 60),
281 bcd(sod % 60),
282 ]
283}
284
285fn system_utc() -> [u8; UTC_TIME_LEN] {
286 let secs = std::time::SystemTime::now()
287 .duration_since(std::time::UNIX_EPOCH)
288 .map(|d| d.as_secs())
289 .unwrap_or(0);
290 unix_to_mjd_bcd(secs)
291}
292
293pub struct DateTime {
297 clock: fn() -> [u8; UTC_TIME_LEN],
298 interval: u8,
299 since: Duration,
300}
301
302impl Default for DateTime {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308impl DateTime {
309 #[must_use]
311 pub fn new() -> Self {
312 Self {
313 clock: system_utc,
314 interval: 0,
315 since: Duration::ZERO,
316 }
317 }
318
319 #[must_use]
321 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
322 Self {
323 clock,
324 interval: 0,
325 since: Duration::ZERO,
326 }
327 }
328
329 fn reply(&self) -> Vec<u8> {
330 ser(&CiDateTime {
331 utc_time: (self.clock)(),
332 local_offset: None,
333 })
334 }
335}
336
337impl Resource for DateTime {
338 fn id(&self) -> ResourceId {
339 DATE_TIME
340 }
341
342 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
343 let mut out = ResourceOut::default();
344 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
345 if let Ok(enq) = DateTimeEnq::parse(apdu) {
346 self.interval = enq.response_interval;
347 self.since = Duration::ZERO;
348 out.apdus.push(self.reply());
349 }
350 }
351 out
352 }
353
354 fn tick(&mut self, elapsed: Duration) -> ResourceOut {
355 let mut out = ResourceOut::default();
356 if self.interval > 0 {
357 self.since += elapsed;
358 if self.since >= Duration::from_secs(u64::from(self.interval)) {
359 self.since = Duration::ZERO;
360 out.apdus.push(self.reply());
361 }
362 }
363 out
364 }
365}
366
367#[derive(Debug, Default)]
375pub struct Mmi;
376
377impl Resource for Mmi {
378 fn id(&self) -> ResourceId {
379 MMI
380 }
381
382 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
383 let mut out = ResourceOut::default();
384 match peek_tag(apdu) {
385 Some(t) if t == tag::ENQ => {
386 if let Ok(e) = Enq::parse(apdu) {
387 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
388 prompt: text(e.text_chars),
389 blind: e.blind_answer,
390 answer_len: e.answer_text_length,
391 }));
392 }
393 }
394 Some(t) if t == tag::MENU_LAST => {
395 if let Ok(m) = Menu::parse(apdu) {
396 out.notify
397 .push(Notification::Mmi(MmiEvent::Menu(to_menu(&m))));
398 }
399 }
400 Some(t) if t == tag::LIST_LAST => {
401 if let Ok(l) = List::parse(apdu) {
402 out.notify
403 .push(Notification::Mmi(MmiEvent::List(to_menu(&l.0))));
404 }
405 }
406 Some(t) if t == tag::CLOSE_MMI => {
407 out.notify.push(Notification::Mmi(MmiEvent::Close));
408 }
409 Some(t) if t == tag::DISPLAY_CONTROL => {
415 if let Ok(dc) = DisplayControl::parse(apdu) {
416 let reply = match dc.cmd {
417 DisplayControlCmd::SetMmiMode => DisplayReply {
419 reply_id: DisplayReplyId::MmiModeAck,
420 body: DisplayReplyBody::MmiModeAck(
421 dc.mmi_mode.unwrap_or(MmiMode::HighLevel),
422 ),
423 },
424 _ => DisplayReply {
427 reply_id: DisplayReplyId::UnknownDisplayControlCmd,
428 body: DisplayReplyBody::None,
429 },
430 };
431 out.apdus.push(ser(&reply));
432 }
433 }
434 _ => {}
435 }
436 out
437 }
438}
439
440#[derive(Debug, Default)]
447pub struct HostControl;
448
449impl Resource for HostControl {
450 fn id(&self) -> ResourceId {
451 HOST_CONTROL
452 }
453
454 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
455 let mut out = ResourceOut::default();
456 let event = match peek_tag(apdu) {
457 Some(t) if t == tag::TUNE => Tune::parse(apdu).ok().map(|t| HostControlEvent::Tune {
458 network_id: t.network_id,
459 original_network_id: t.original_network_id,
460 transport_stream_id: t.transport_stream_id,
461 service_id: t.service_id,
462 }),
463 Some(t) if t == tag::REPLACE => {
464 Replace::parse(apdu)
465 .ok()
466 .map(|r| HostControlEvent::Replace {
467 replacement_ref: r.replacement_ref,
468 replaced_pid: r.replaced_pid,
469 replacement_pid: r.replacement_pid,
470 })
471 }
472 Some(t) if t == tag::CLEAR_REPLACE => {
473 ClearReplace::parse(apdu)
474 .ok()
475 .map(|c| HostControlEvent::ClearReplace {
476 replacement_ref: c.replacement_ref,
477 })
478 }
479 Some(t) if t == tag::ASK_RELEASE => AskRelease::parse(apdu)
480 .ok()
481 .map(|_| HostControlEvent::AskRelease),
482 _ => None,
483 };
484 if let Some(event) = event {
485 out.notify.push(Notification::HostControl(event));
486 }
487 out
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use dvb_ci::objects::resource_manager::Profile;
495
496 #[test]
497 fn on_open_sends_profile_enq() {
498 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
499 let out = rm.on_open();
500 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
501 }
502
503 #[test]
504 fn module_profile_triggers_profile_change_and_camready() {
505 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
511 rm.on_open();
512 let empty_profile = ser(&Profile { resources: vec![] });
513 let o = rm.on_apdu(&empty_profile);
514 assert!(o.notify.contains(&Notification::CamReady));
515 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
516 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
517 assert!(o.open.is_empty(), "host opens no sessions itself");
518 }
519
520 #[test]
521 fn answers_a_module_profile_enquiry_without_re_readying() {
522 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
523 rm.on_open();
524 rm.on_apdu(&ser(&Profile {
525 resources: vec![APPLICATION_INFORMATION],
526 }));
527 let o = rm.on_apdu(&ser(&ProfileEnq));
529 assert_eq!(o.apdus.len(), 1);
530 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
531 assert!(!o.notify.contains(&Notification::CamReady));
532 }
533
534 #[test]
535 fn mmi_surfaces_enquiry_and_close() {
536 let mut h = Mmi;
537 let enq = ser(&Enq {
539 blind_answer: true,
540 answer_text_length: 4,
541 text_chars: b"PIN?",
542 });
543 assert_eq!(
544 h.on_apdu(&enq).notify,
545 vec![Notification::Mmi(MmiEvent::Enquiry {
546 prompt: "PIN?".to_string(),
547 blind: true,
548 answer_len: 4,
549 })]
550 );
551 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
553 assert_eq!(
554 h.on_apdu(&close).notify,
555 vec![Notification::Mmi(MmiEvent::Close)]
556 );
557 }
558
559 #[test]
560 fn mmi_surfaces_structured_menu_and_list() {
561 use dvb_ci::objects::mmi_high::{List, Menu, Text};
562 let txt = |s: &'static [u8]| Text {
563 more: false,
564 text_chars: s,
565 };
566 let mut h = Mmi;
567 let menu = ser(&Menu {
569 more: false,
570 choice_nb: 2,
571 title: txt(b"AlphaCrypt"),
572 subtitle: txt(b"Module Mainmenu"),
573 bottom: txt(b"Select item and press OK"),
574 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
575 });
576 assert_eq!(
577 h.on_apdu(&menu).notify,
578 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
579 title: "AlphaCrypt".to_string(),
580 subtitle: "Module Mainmenu".to_string(),
581 bottom: "Select item and press OK".to_string(),
582 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
583 }))]
584 );
585 let list = ser(&List(Menu {
587 more: false,
588 choice_nb: 0xFF,
589 title: txt(b"Entitlements"),
590 subtitle: txt(b""),
591 bottom: txt(b""),
592 choices: vec![txt(b"ORF AUT")],
593 }));
594 assert_eq!(
595 h.on_apdu(&list).notify,
596 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
597 title: "Entitlements".to_string(),
598 subtitle: String::new(),
599 bottom: String::new(),
600 choices: vec!["ORF AUT".to_string()],
601 }))]
602 );
603 }
604
605 #[test]
606 fn mmi_answers_display_control_set_mmi_mode() {
607 use dvb_ci::objects::mmi_display::{DisplayControl, DisplayControlCmd, MmiMode};
608 let mut h = Mmi;
609 let dc = ser(&DisplayControl {
610 cmd: DisplayControlCmd::SetMmiMode,
611 mmi_mode: Some(MmiMode::HighLevel),
612 });
613 let out = h.on_apdu(&dc);
614 assert_eq!(out.apdus, vec![vec![0x9F, 0x88, 0x02, 0x02, 0x01, 0x01]]);
616 assert!(out.notify.is_empty());
617 }
618
619 #[test]
620 fn profile_change_re_enquires() {
621 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
622 let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
623 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
624 }
625
626 #[test]
627 fn application_information_surfaces_notification() {
628 use dvb_ci::objects::application_info::ApplicationType;
629 let mut h = ApplicationInformation;
630 assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
631 let ai = ser(&ApplicationInfo {
632 application_type: ApplicationType::ConditionalAccess,
633 application_manufacturer: 0x1234,
634 manufacturer_code: 0x5678,
635 menu_string: b"Acme CAM",
636 });
637 let out = h.on_apdu(&ai);
638 assert_eq!(
639 out.notify,
640 vec![Notification::ApplicationInfo {
641 application_type: 0x01,
642 manufacturer: 0x1234,
643 code: 0x5678,
644 menu: "Acme CAM".to_string(),
645 }]
646 );
647 }
648
649 #[test]
650 fn host_control_surfaces_tune_replace_clear_and_ask_release() {
651 let mut h = HostControl;
652 assert_eq!(h.id(), HOST_CONTROL);
653
654 let tune = ser(&Tune {
656 network_id: 0x1122,
657 original_network_id: 0x3344,
658 transport_stream_id: 0x5566,
659 service_id: 0x7788,
660 });
661 assert_eq!(
662 h.on_apdu(&tune).notify,
663 vec![Notification::HostControl(HostControlEvent::Tune {
664 network_id: 0x1122,
665 original_network_id: 0x3344,
666 transport_stream_id: 0x5566,
667 service_id: 0x7788,
668 })]
669 );
670
671 let replace = ser(&Replace {
673 replacement_ref: 0x07,
674 replaced_pid: 0x0123,
675 replacement_pid: 0x01FF,
676 });
677 assert_eq!(
678 h.on_apdu(&replace).notify,
679 vec![Notification::HostControl(HostControlEvent::Replace {
680 replacement_ref: 0x07,
681 replaced_pid: 0x0123,
682 replacement_pid: 0x01FF,
683 })]
684 );
685
686 let clear = ser(&ClearReplace {
688 replacement_ref: 0x42,
689 });
690 assert_eq!(
691 h.on_apdu(&clear).notify,
692 vec![Notification::HostControl(HostControlEvent::ClearReplace {
693 replacement_ref: 0x42,
694 })]
695 );
696
697 let ask = ser(&AskRelease);
699 assert_eq!(
700 h.on_apdu(&ask).notify,
701 vec![Notification::HostControl(HostControlEvent::AskRelease)]
702 );
703
704 assert!(h.on_apdu(&tune).apdus.is_empty());
706 }
707
708 #[test]
709 fn mjd_bcd_encoding_is_correct() {
710 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
712 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
714 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
715 }
716
717 #[test]
718 fn date_time_replies_to_enq_and_resends_on_interval() {
719 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
720 let mut h = DateTime::with_clock(fixed);
721 let enq = ser(&DateTimeEnq {
723 response_interval: 5,
724 });
725 let out = h.on_apdu(&enq);
726 assert_eq!(out.apdus.len(), 1);
727 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
728 assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
730 assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
732 }
733
734 #[test]
735 fn date_time_interval_zero_does_not_resend() {
736 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
737 h.on_apdu(&ser(&DateTimeEnq {
738 response_interval: 0,
739 }));
740 assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
741 }
742
743 #[test]
744 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
745 let mut h = ConditionalAccess;
746 assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
747 let ci = ser(&CaInfo {
749 ca_system_ids: vec![0x0B00, 0x1800],
750 });
751 assert_eq!(
752 h.on_apdu(&ci).notify,
753 vec![Notification::CaInfo {
754 ca_system_ids: vec![0x0B00, 0x1800],
755 }]
756 );
757 let reply = ser(&CaPmtReply {
759 program_number: 0x0042,
760 version_number: 0,
761 current_next_indicator: true,
762 ca_enable: Some(CaEnable::Possible),
763 streams: vec![],
764 });
765 assert_eq!(
766 h.on_apdu(&reply).notify,
767 vec![Notification::CaPmtReply {
768 program_number: 0x0042,
769 descrambling_ok: true,
770 }]
771 );
772 }
773}