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