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 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, HOST_CONTROL, MMI,
25 RESOURCE_MANAGER, ResourceId,
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 = matches!(
251 r.ca_enable,
252 Some(
253 CaEnable::Possible
254 | CaEnable::PossiblePurchaseDialogue
255 | CaEnable::PossibleTechnicalDialogue
256 )
257 );
258 out.notify.push(Notification::CaPmtReply {
259 program_number: r.program_number,
260 ca_enable: r.ca_enable,
261 descrambling_ok,
262 });
263 }
264 }
265 _ => {}
266 }
267 out
268 }
269}
270
271const SECS_PER_DAY: u64 = 86_400;
272const MJD_UNIX_EPOCH: u64 = 40_587;
274
275fn bcd(v: u64) -> u8 {
276 (((v / 10) << 4) | (v % 10)) as u8
277}
278
279fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
282 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
283 let sod = unix_secs % SECS_PER_DAY;
284 [
285 (mjd >> 8) as u8,
286 mjd as u8,
287 bcd(sod / 3600),
288 bcd((sod % 3600) / 60),
289 bcd(sod % 60),
290 ]
291}
292
293fn system_utc() -> [u8; UTC_TIME_LEN] {
294 let secs = std::time::SystemTime::now()
295 .duration_since(std::time::UNIX_EPOCH)
296 .map(|d| d.as_secs())
297 .unwrap_or(0);
298 unix_to_mjd_bcd(secs)
299}
300
301pub struct DateTime {
305 clock: fn() -> [u8; UTC_TIME_LEN],
306 interval: u8,
307 since: Duration,
308}
309
310impl Default for DateTime {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316impl DateTime {
317 #[must_use]
319 pub fn new() -> Self {
320 Self {
321 clock: system_utc,
322 interval: 0,
323 since: Duration::ZERO,
324 }
325 }
326
327 #[must_use]
329 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
330 Self {
331 clock,
332 interval: 0,
333 since: Duration::ZERO,
334 }
335 }
336
337 fn reply(&self) -> Vec<u8> {
338 ser(&CiDateTime {
339 utc_time: (self.clock)(),
340 local_offset: None,
341 })
342 }
343}
344
345impl Resource for DateTime {
346 fn id(&self) -> ResourceId {
347 DATE_TIME
348 }
349
350 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
351 let mut out = ResourceOut::default();
352 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
353 if let Ok(enq) = DateTimeEnq::parse(apdu) {
354 self.interval = enq.response_interval;
355 self.since = Duration::ZERO;
356 out.apdus.push(self.reply());
357 }
358 }
359 out
360 }
361
362 fn tick(&mut self, elapsed: Duration) -> ResourceOut {
363 let mut out = ResourceOut::default();
364 if self.interval > 0 {
365 self.since += elapsed;
366 if self.since >= Duration::from_secs(u64::from(self.interval)) {
367 self.since = Duration::ZERO;
368 out.apdus.push(self.reply());
369 }
370 }
371 out
372 }
373}
374
375#[derive(Debug, Default)]
383pub struct Mmi;
384
385impl Resource for Mmi {
386 fn id(&self) -> ResourceId {
387 MMI
388 }
389
390 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
391 let mut out = ResourceOut::default();
392 match peek_tag(apdu) {
393 Some(t) if t == tag::ENQ => {
394 if let Ok(e) = Enq::parse(apdu) {
395 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
396 prompt: text(e.text_chars),
397 blind: e.blind_answer,
398 answer_len: e.answer_text_length,
399 }));
400 }
401 }
402 Some(t) if t == tag::MENU_LAST => {
403 if let Ok(m) = Menu::parse(apdu) {
404 out.notify
405 .push(Notification::Mmi(MmiEvent::Menu(to_menu(&m))));
406 }
407 }
408 Some(t) if t == tag::LIST_LAST => {
409 if let Ok(l) = List::parse(apdu) {
410 out.notify
411 .push(Notification::Mmi(MmiEvent::List(to_menu(&l.0))));
412 }
413 }
414 Some(t) if t == tag::CLOSE_MMI => {
415 out.notify.push(Notification::Mmi(MmiEvent::Close));
416 }
417 Some(t) if t == tag::DISPLAY_CONTROL => {
423 if let Ok(dc) = DisplayControl::parse(apdu) {
424 let reply = match dc.cmd {
425 DisplayControlCmd::SetMmiMode => DisplayReply {
427 reply_id: DisplayReplyId::MmiModeAck,
428 body: DisplayReplyBody::MmiModeAck(
429 dc.mmi_mode.unwrap_or(MmiMode::HighLevel),
430 ),
431 },
432 _ => DisplayReply {
435 reply_id: DisplayReplyId::UnknownDisplayControlCmd,
436 body: DisplayReplyBody::None,
437 },
438 };
439 out.apdus.push(ser(&reply));
440 }
441 }
442 _ => {}
443 }
444 out
445 }
446}
447
448#[derive(Debug, Default)]
455pub struct HostControl;
456
457impl Resource for HostControl {
458 fn id(&self) -> ResourceId {
459 HOST_CONTROL
460 }
461
462 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
463 let mut out = ResourceOut::default();
464 let event = match peek_tag(apdu) {
465 Some(t) if t == tag::TUNE => Tune::parse(apdu).ok().map(|t| HostControlEvent::Tune {
466 network_id: t.network_id,
467 original_network_id: t.original_network_id,
468 transport_stream_id: t.transport_stream_id,
469 service_id: t.service_id,
470 }),
471 Some(t) if t == tag::REPLACE => {
472 Replace::parse(apdu)
473 .ok()
474 .map(|r| HostControlEvent::Replace {
475 replacement_ref: r.replacement_ref,
476 replaced_pid: r.replaced_pid,
477 replacement_pid: r.replacement_pid,
478 })
479 }
480 Some(t) if t == tag::CLEAR_REPLACE => {
481 ClearReplace::parse(apdu)
482 .ok()
483 .map(|c| HostControlEvent::ClearReplace {
484 replacement_ref: c.replacement_ref,
485 })
486 }
487 Some(t) if t == tag::ASK_RELEASE => AskRelease::parse(apdu)
488 .ok()
489 .map(|_| HostControlEvent::AskRelease),
490 _ => None,
491 };
492 if let Some(event) = event {
493 out.notify.push(Notification::HostControl(event));
494 }
495 out
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use dvb_ci::objects::resource_manager::Profile;
503
504 #[test]
505 fn on_open_sends_profile_enq() {
506 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
507 let out = rm.on_open();
508 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
509 }
510
511 #[test]
512 fn module_profile_triggers_profile_change_and_camready() {
513 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
519 rm.on_open();
520 let empty_profile = ser(&Profile { resources: vec![] });
521 let o = rm.on_apdu(&empty_profile);
522 assert!(o.notify.contains(&Notification::CamReady));
523 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
524 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
525 assert!(o.open.is_empty(), "host opens no sessions itself");
526 }
527
528 #[test]
529 fn answers_a_module_profile_enquiry_without_re_readying() {
530 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
531 rm.on_open();
532 rm.on_apdu(&ser(&Profile {
533 resources: vec![APPLICATION_INFORMATION],
534 }));
535 let o = rm.on_apdu(&ser(&ProfileEnq));
537 assert_eq!(o.apdus.len(), 1);
538 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
539 assert!(!o.notify.contains(&Notification::CamReady));
540 }
541
542 #[test]
543 fn mmi_surfaces_enquiry_and_close() {
544 let mut h = Mmi;
545 let enq = ser(&Enq {
547 blind_answer: true,
548 answer_text_length: 4,
549 text_chars: b"PIN?",
550 });
551 assert_eq!(
552 h.on_apdu(&enq).notify,
553 vec![Notification::Mmi(MmiEvent::Enquiry {
554 prompt: "PIN?".to_string(),
555 blind: true,
556 answer_len: 4,
557 })]
558 );
559 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
561 assert_eq!(
562 h.on_apdu(&close).notify,
563 vec![Notification::Mmi(MmiEvent::Close)]
564 );
565 }
566
567 #[test]
568 fn mmi_surfaces_structured_menu_and_list() {
569 use dvb_ci::objects::mmi_high::{List, Menu, Text};
570 let txt = |s: &'static [u8]| Text {
571 more: false,
572 text_chars: s,
573 };
574 let mut h = Mmi;
575 let menu = ser(&Menu {
577 more: false,
578 choice_nb: 2,
579 title: txt(b"AlphaCrypt"),
580 subtitle: txt(b"Module Mainmenu"),
581 bottom: txt(b"Select item and press OK"),
582 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
583 });
584 assert_eq!(
585 h.on_apdu(&menu).notify,
586 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
587 title: "AlphaCrypt".to_string(),
588 subtitle: "Module Mainmenu".to_string(),
589 bottom: "Select item and press OK".to_string(),
590 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
591 }))]
592 );
593 let list = ser(&List(Menu {
595 more: false,
596 choice_nb: 0xFF,
597 title: txt(b"Entitlements"),
598 subtitle: txt(b""),
599 bottom: txt(b""),
600 choices: vec![txt(b"ORF AUT")],
601 }));
602 assert_eq!(
603 h.on_apdu(&list).notify,
604 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
605 title: "Entitlements".to_string(),
606 subtitle: String::new(),
607 bottom: String::new(),
608 choices: vec!["ORF AUT".to_string()],
609 }))]
610 );
611 }
612
613 #[test]
614 fn mmi_answers_display_control_set_mmi_mode() {
615 use dvb_ci::objects::mmi_display::{DisplayControl, DisplayControlCmd, MmiMode};
616 let mut h = Mmi;
617 let dc = ser(&DisplayControl {
618 cmd: DisplayControlCmd::SetMmiMode,
619 mmi_mode: Some(MmiMode::HighLevel),
620 });
621 let out = h.on_apdu(&dc);
622 assert_eq!(out.apdus, vec![vec![0x9F, 0x88, 0x02, 0x02, 0x01, 0x01]]);
624 assert!(out.notify.is_empty());
625 }
626
627 #[test]
628 fn profile_change_re_enquires() {
629 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
630 let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
631 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
632 }
633
634 #[test]
635 fn application_information_surfaces_notification() {
636 use dvb_ci::objects::application_info::ApplicationType;
637 let mut h = ApplicationInformation;
638 assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
639 let ai = ser(&ApplicationInfo {
640 application_type: ApplicationType::ConditionalAccess,
641 application_manufacturer: 0x1234,
642 manufacturer_code: 0x5678,
643 menu_string: b"Acme CAM",
644 });
645 let out = h.on_apdu(&ai);
646 assert_eq!(
647 out.notify,
648 vec![Notification::ApplicationInfo {
649 application_type: 0x01,
650 manufacturer: 0x1234,
651 code: 0x5678,
652 menu: "Acme CAM".to_string(),
653 }]
654 );
655 }
656
657 #[test]
658 fn host_control_surfaces_tune_replace_clear_and_ask_release() {
659 let mut h = HostControl;
660 assert_eq!(h.id(), HOST_CONTROL);
661
662 let tune = ser(&Tune {
664 network_id: 0x1122,
665 original_network_id: 0x3344,
666 transport_stream_id: 0x5566,
667 service_id: 0x7788,
668 });
669 assert_eq!(
670 h.on_apdu(&tune).notify,
671 vec![Notification::HostControl(HostControlEvent::Tune {
672 network_id: 0x1122,
673 original_network_id: 0x3344,
674 transport_stream_id: 0x5566,
675 service_id: 0x7788,
676 })]
677 );
678
679 let replace = ser(&Replace {
681 replacement_ref: 0x07,
682 replaced_pid: 0x0123,
683 replacement_pid: 0x01FF,
684 });
685 assert_eq!(
686 h.on_apdu(&replace).notify,
687 vec![Notification::HostControl(HostControlEvent::Replace {
688 replacement_ref: 0x07,
689 replaced_pid: 0x0123,
690 replacement_pid: 0x01FF,
691 })]
692 );
693
694 let clear = ser(&ClearReplace {
696 replacement_ref: 0x42,
697 });
698 assert_eq!(
699 h.on_apdu(&clear).notify,
700 vec![Notification::HostControl(HostControlEvent::ClearReplace {
701 replacement_ref: 0x42,
702 })]
703 );
704
705 let ask = ser(&AskRelease);
707 assert_eq!(
708 h.on_apdu(&ask).notify,
709 vec![Notification::HostControl(HostControlEvent::AskRelease)]
710 );
711
712 assert!(h.on_apdu(&tune).apdus.is_empty());
714 }
715
716 #[test]
717 fn mjd_bcd_encoding_is_correct() {
718 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
720 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
722 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
723 }
724
725 #[test]
726 fn date_time_replies_to_enq_and_resends_on_interval() {
727 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
728 let mut h = DateTime::with_clock(fixed);
729 let enq = ser(&DateTimeEnq {
731 response_interval: 5,
732 });
733 let out = h.on_apdu(&enq);
734 assert_eq!(out.apdus.len(), 1);
735 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
736 assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
738 assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
740 }
741
742 #[test]
743 fn date_time_interval_zero_does_not_resend() {
744 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
745 h.on_apdu(&ser(&DateTimeEnq {
746 response_interval: 0,
747 }));
748 assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
749 }
750
751 #[test]
752 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
753 let mut h = ConditionalAccess;
754 assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
755 let ci = ser(&CaInfo {
757 ca_system_ids: vec![0x0B00, 0x1800],
758 });
759 assert_eq!(
760 h.on_apdu(&ci).notify,
761 vec![Notification::CaInfo {
762 ca_system_ids: vec![0x0B00, 0x1800],
763 }]
764 );
765 let reply = ser(&CaPmtReply {
767 program_number: 0x0042,
768 version_number: 0,
769 current_next_indicator: true,
770 ca_enable: Some(CaEnable::Possible),
771 streams: vec![],
772 });
773 assert_eq!(
774 h.on_apdu(&reply).notify,
775 vec![Notification::CaPmtReply {
776 program_number: 0x0042,
777 ca_enable: Some(CaEnable::Possible),
778 descrambling_ok: true,
779 }]
780 );
781 }
782
783 #[test]
784 fn conditional_access_ca_pmt_reply_flag_clear_surfaces_none() {
785 let mut h = ConditionalAccess;
786 h.on_open();
787 let reply = ser(&CaPmtReply {
789 program_number: 0x0007,
790 version_number: 0,
791 current_next_indicator: true,
792 ca_enable: None,
793 streams: vec![],
794 });
795 assert_eq!(
796 h.on_apdu(&reply).notify,
797 vec![Notification::CaPmtReply {
798 program_number: 0x0007,
799 ca_enable: None,
800 descrambling_ok: false,
801 }]
802 );
803 }
804}