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<Error = dvb_ci::Error>>(s: &S) -> dvb_ci::Result<Vec<u8>> {
59 s.try_to_bytes()
60}
61
62pub(crate) fn peek_tag(apdu: &[u8]) -> Option<ApduTag> {
64 (apdu.len() >= 3).then(|| ApduTag::from_bytes(apdu[0], apdu[1], apdu[2]))
65}
66
67#[derive(Debug, Default, Clone, PartialEq, Eq)]
69pub struct ResourceOut {
70 pub apdus: Vec<Vec<u8>>,
72 pub notify: Vec<Notification>,
74 pub open: Vec<ResourceId>,
76}
77
78pub trait Resource {
80 fn id(&self) -> ResourceId;
82 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
84 Ok(ResourceOut::default())
85 }
86 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut>;
88 fn tick(&mut self, _elapsed: Duration) -> dvb_ci::Result<ResourceOut> {
90 Ok(ResourceOut::default())
91 }
92 fn reset(&mut self) {}
101}
102
103#[derive(Debug)]
107pub struct ResourceManager {
108 host_resources: Vec<ResourceId>,
109 module_resources: Vec<ResourceId>,
110 module_profiled: bool,
111 ready: bool,
112}
113
114impl ResourceManager {
115 #[must_use]
117 pub fn new(host_resources: Vec<ResourceId>) -> Self {
118 Self {
119 host_resources,
120 module_resources: Vec::new(),
121 module_profiled: false,
122 ready: false,
123 }
124 }
125
126 #[must_use]
128 pub fn module_resources(&self) -> &[ResourceId] {
129 &self.module_resources
130 }
131}
132
133impl Resource for ResourceManager {
134 fn id(&self) -> ResourceId {
135 RESOURCE_MANAGER
136 }
137
138 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
139 Ok(ResourceOut {
141 apdus: vec![ser(&ProfileEnq)?],
142 ..ResourceOut::default()
143 })
144 }
145
146 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
147 let mut out = ResourceOut::default();
148 match peek_tag(apdu) {
149 Some(t) if t == tag::PROFILE_ENQ => {
151 out.apdus.push(ser(&Profile {
152 resources: self.host_resources.clone(),
153 })?);
154 }
155 Some(t) if t == tag::PROFILE => {
157 let p = Profile::parse(apdu)?;
160 self.module_resources = p.resources;
161 self.module_profiled = true;
162 }
163 Some(t) if t == tag::PROFILE_CHANGE => {
165 out.apdus.push(ser(&ProfileEnq)?);
166 self.module_profiled = false;
167 self.ready = false;
168 }
169 _ => {}
170 }
171 if self.module_profiled && !self.ready {
185 self.ready = true;
186 out.apdus.push(ser(&ProfileChange)?);
187 out.notify.push(Notification::CamReady);
188 }
189 Ok(out)
190 }
191
192 fn reset(&mut self) {
198 self.module_resources.clear();
199 self.module_profiled = false;
200 self.ready = false;
201 }
202}
203
204#[derive(Debug, Default)]
207pub struct ApplicationInformation;
208
209impl Resource for ApplicationInformation {
210 fn id(&self) -> ResourceId {
211 APPLICATION_INFORMATION
212 }
213
214 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
215 Ok(ResourceOut {
216 apdus: vec![ser(&ApplicationInfoEnq)?],
217 ..ResourceOut::default()
218 })
219 }
220
221 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
222 let mut out = ResourceOut::default();
223 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
224 let ai = ApplicationInfo::parse(apdu)?;
225 out.notify.push(Notification::ApplicationInfo {
226 application_type: ai.application_type.to_u8(),
227 manufacturer: ai.application_manufacturer,
228 code: ai.manufacturer_code,
229 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
230 });
231 }
232 Ok(out)
233 }
234 }
236
237#[derive(Debug, Default)]
242pub struct ConditionalAccess;
243
244impl Resource for ConditionalAccess {
245 fn id(&self) -> ResourceId {
246 CONDITIONAL_ACCESS_SUPPORT
247 }
248
249 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
250 Ok(ResourceOut {
251 apdus: vec![ser(&CaInfoEnq)?],
252 ..ResourceOut::default()
253 })
254 }
255
256 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
257 let mut out = ResourceOut::default();
258 match peek_tag(apdu) {
259 Some(t) if t == tag::CA_INFO => {
260 let ci = CaInfo::parse(apdu)?;
263 out.notify.push(Notification::CaInfo {
264 ca_system_ids: ci.ca_system_ids,
265 });
266 }
267 Some(t) if t == tag::CA_PMT_REPLY => {
268 let r = CaPmtReply::parse(apdu)?;
271 let descrambling_ok = matches!(
279 r.ca_enable,
280 Some(
281 CaEnable::Possible
282 | CaEnable::PossiblePurchaseDialogue
283 | CaEnable::PossibleTechnicalDialogue
284 )
285 );
286 out.notify.push(Notification::CaPmtReply {
287 program_number: r.program_number,
288 ca_enable: r.ca_enable,
289 descrambling_ok,
290 });
291 }
292 _ => {}
293 }
294 Ok(out)
295 }
296 }
298
299const SECS_PER_DAY: u64 = 86_400;
300const MJD_UNIX_EPOCH: u64 = 40_587;
302
303fn bcd(v: u64) -> u8 {
304 (((v / 10) << 4) | (v % 10)) as u8
305}
306
307fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
310 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
311 let sod = unix_secs % SECS_PER_DAY;
312 [
313 (mjd >> 8) as u8,
314 mjd as u8,
315 bcd(sod / 3600),
316 bcd((sod % 3600) / 60),
317 bcd(sod % 60),
318 ]
319}
320
321fn system_utc() -> [u8; UTC_TIME_LEN] {
322 let secs = std::time::SystemTime::now()
323 .duration_since(std::time::UNIX_EPOCH)
324 .map(|d| d.as_secs())
325 .unwrap_or(0);
326 unix_to_mjd_bcd(secs)
327}
328
329pub struct DateTime {
333 clock: fn() -> [u8; UTC_TIME_LEN],
334 interval: u8,
335 since: Duration,
336}
337
338impl Default for DateTime {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344impl DateTime {
345 #[must_use]
347 pub fn new() -> Self {
348 Self {
349 clock: system_utc,
350 interval: 0,
351 since: Duration::ZERO,
352 }
353 }
354
355 #[must_use]
357 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
358 Self {
359 clock,
360 interval: 0,
361 since: Duration::ZERO,
362 }
363 }
364
365 fn reply(&self) -> dvb_ci::Result<Vec<u8>> {
366 ser(&CiDateTime {
367 utc_time: (self.clock)(),
368 local_offset: None,
369 })
370 }
371}
372
373impl Resource for DateTime {
374 fn id(&self) -> ResourceId {
375 DATE_TIME
376 }
377
378 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
379 let mut out = ResourceOut::default();
380 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
381 let enq = DateTimeEnq::parse(apdu)?;
382 self.interval = enq.response_interval;
383 self.since = Duration::ZERO;
384 out.apdus.push(self.reply()?);
385 }
386 Ok(out)
387 }
388
389 fn tick(&mut self, elapsed: Duration) -> dvb_ci::Result<ResourceOut> {
390 let mut out = ResourceOut::default();
391 if self.interval > 0 {
392 self.since += elapsed;
393 if self.since >= Duration::from_secs(u64::from(self.interval)) {
394 self.since = Duration::ZERO;
395 out.apdus.push(self.reply()?);
396 }
397 }
398 Ok(out)
399 }
400
401 fn reset(&mut self) {
407 self.interval = 0;
408 self.since = Duration::ZERO;
409 }
410}
411
412#[derive(Debug, Default)]
420pub struct Mmi;
421
422impl Resource for Mmi {
423 fn id(&self) -> ResourceId {
424 MMI
425 }
426
427 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
428 let mut out = ResourceOut::default();
429 match peek_tag(apdu) {
430 Some(t) if t == tag::ENQ => {
431 let e = Enq::parse(apdu)?;
434 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
435 prompt: text(e.text_chars),
436 blind: e.blind_answer,
437 answer_len: e.answer_text_length,
438 }));
439 }
440 Some(t) if t == tag::MENU_LAST => {
441 let m = Menu::parse(apdu)?;
444 out.notify
445 .push(Notification::Mmi(MmiEvent::Menu(to_menu(&m))));
446 }
447 Some(t) if t == tag::LIST_LAST => {
448 let l = List::parse(apdu)?;
451 out.notify
452 .push(Notification::Mmi(MmiEvent::List(to_menu(&l.0))));
453 }
454 Some(t) if t == tag::CLOSE_MMI => {
455 dvb_ci::objects::mmi_close::CloseMmi::parse(apdu)?;
456 out.notify.push(Notification::Mmi(MmiEvent::Close));
457 }
458 Some(t) if t == tag::DISPLAY_CONTROL => {
464 let dc = DisplayControl::parse(apdu)?;
467 let reply = match dc.cmd {
468 DisplayControlCmd::SetMmiMode => DisplayReply {
470 reply_id: DisplayReplyId::MmiModeAck,
471 body: DisplayReplyBody::MmiModeAck(
472 dc.mmi_mode.unwrap_or(MmiMode::HighLevel),
473 ),
474 },
475 _ => DisplayReply {
478 reply_id: DisplayReplyId::UnknownDisplayControlCmd,
479 body: DisplayReplyBody::None,
480 },
481 };
482 out.apdus.push(ser(&reply)?);
483 }
484 _ => {}
485 }
486 Ok(out)
487 }
488 }
490
491#[derive(Debug, Default)]
498pub struct HostControl;
499
500impl Resource for HostControl {
501 fn id(&self) -> ResourceId {
502 HOST_CONTROL
503 }
504
505 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
506 let mut out = ResourceOut::default();
507 let event = match peek_tag(apdu) {
511 Some(t) if t == tag::TUNE => {
512 let t = Tune::parse(apdu)?;
513 Some(HostControlEvent::Tune {
514 network_id: t.network_id,
515 original_network_id: t.original_network_id,
516 transport_stream_id: t.transport_stream_id,
517 service_id: t.service_id,
518 })
519 }
520 Some(t) if t == tag::REPLACE => {
521 let r = Replace::parse(apdu)?;
522 Some(HostControlEvent::Replace {
523 replacement_ref: r.replacement_ref,
524 replaced_pid: r.replaced_pid,
525 replacement_pid: r.replacement_pid,
526 })
527 }
528 Some(t) if t == tag::CLEAR_REPLACE => {
529 let c = ClearReplace::parse(apdu)?;
530 Some(HostControlEvent::ClearReplace {
531 replacement_ref: c.replacement_ref,
532 })
533 }
534 Some(t) if t == tag::ASK_RELEASE => {
535 AskRelease::parse(apdu)?;
536 Some(HostControlEvent::AskRelease)
537 }
538 _ => None,
539 };
540 if let Some(event) = event {
541 out.notify.push(Notification::HostControl(event));
542 }
543 Ok(out)
544 }
545 }
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use dvb_ci::objects::resource_manager::Profile;
552
553 fn s<S: Serialize<Error = dvb_ci::Error>>(v: &S) -> Vec<u8> {
556 ser(v).expect("test value must serialize")
557 }
558
559 #[test]
560 fn on_open_sends_profile_enq() {
561 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
562 let out = rm.on_open().unwrap();
563 assert_eq!(out.apdus, vec![s(&ProfileEnq)]);
564 }
565
566 #[test]
567 fn module_profile_triggers_profile_change_and_camready() {
568 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
574 rm.on_open().unwrap();
575 let empty_profile = s(&Profile { resources: vec![] });
576 let o = rm.on_apdu(&empty_profile).unwrap();
577 assert!(o.notify.contains(&Notification::CamReady));
578 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
579 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
580 assert!(o.open.is_empty(), "host opens no sessions itself");
581 }
582
583 #[test]
584 fn answers_a_module_profile_enquiry_without_re_readying() {
585 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
586 rm.on_open().unwrap();
587 rm.on_apdu(&s(&Profile {
588 resources: vec![APPLICATION_INFORMATION],
589 }))
590 .unwrap();
591 let o = rm.on_apdu(&s(&ProfileEnq)).unwrap();
593 assert_eq!(o.apdus.len(), 1);
594 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
595 assert!(!o.notify.contains(&Notification::CamReady));
596 }
597
598 #[test]
604 fn resource_manager_reset_clears_latched_handshake_state() {
605 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
606 rm.on_open().unwrap();
607 let o = rm
608 .on_apdu(&s(&Profile {
609 resources: vec![APPLICATION_INFORMATION],
610 }))
611 .unwrap();
612 assert!(o.notify.contains(&Notification::CamReady), "dirtied: ready");
613 assert!(
614 !rm.module_resources().is_empty(),
615 "dirtied: module_resources"
616 );
617
618 rm.reset();
619 assert!(
620 rm.module_resources().is_empty(),
621 "reset must clear module_resources"
622 );
623
624 rm.on_open().unwrap();
627 let o2 = rm.on_apdu(&s(&Profile { resources: vec![] })).unwrap();
628 assert!(
629 o2.notify.contains(&Notification::CamReady),
630 "a fresh handshake after reset() must fire CamReady again"
631 );
632 }
633
634 #[test]
635 fn mmi_surfaces_enquiry_and_close() {
636 let mut h = Mmi;
637 let enq = s(&Enq {
639 blind_answer: true,
640 answer_text_length: 4,
641 text_chars: b"PIN?",
642 });
643 assert_eq!(
644 h.on_apdu(&enq).unwrap().notify,
645 vec![Notification::Mmi(MmiEvent::Enquiry {
646 prompt: "PIN?".to_string(),
647 blind: true,
648 answer_len: 4,
649 })]
650 );
651 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
653 assert_eq!(
654 h.on_apdu(&close).unwrap().notify,
655 vec![Notification::Mmi(MmiEvent::Close)]
656 );
657 }
658
659 #[test]
660 fn mmi_surfaces_structured_menu_and_list() {
661 use dvb_ci::objects::mmi_high::{List, Menu, Text};
662 let txt = |s: &'static [u8]| Text {
663 more: false,
664 text_chars: s,
665 };
666 let mut h = Mmi;
667 let menu = s(&Menu {
669 more: false,
670 choice_nb: 2,
671 title: txt(b"AlphaCrypt"),
672 subtitle: txt(b"Module Mainmenu"),
673 bottom: txt(b"Select item and press OK"),
674 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
675 });
676 assert_eq!(
677 h.on_apdu(&menu).unwrap().notify,
678 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
679 title: "AlphaCrypt".to_string(),
680 subtitle: "Module Mainmenu".to_string(),
681 bottom: "Select item and press OK".to_string(),
682 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
683 }))]
684 );
685 let list = s(&List(Menu {
687 more: false,
688 choice_nb: 0xFF,
689 title: txt(b"Entitlements"),
690 subtitle: txt(b""),
691 bottom: txt(b""),
692 choices: vec![txt(b"ORF AUT")],
693 }));
694 assert_eq!(
695 h.on_apdu(&list).unwrap().notify,
696 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
697 title: "Entitlements".to_string(),
698 subtitle: String::new(),
699 bottom: String::new(),
700 choices: vec!["ORF AUT".to_string()],
701 }))]
702 );
703 }
704
705 #[test]
706 fn mmi_answers_display_control_set_mmi_mode() {
707 use dvb_ci::objects::mmi_display::{DisplayControl, DisplayControlCmd, MmiMode};
708 let mut h = Mmi;
709 let dc = s(&DisplayControl {
710 cmd: DisplayControlCmd::SetMmiMode,
711 mmi_mode: Some(MmiMode::HighLevel),
712 });
713 let out = h.on_apdu(&dc).unwrap();
714 assert_eq!(out.apdus, vec![vec![0x9F, 0x88, 0x02, 0x02, 0x01, 0x01]]);
716 assert!(out.notify.is_empty());
717 }
718
719 #[test]
720 fn profile_change_re_enquires() {
721 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
722 let out = rm
723 .on_apdu(&s(&dvb_ci::objects::resource_manager::ProfileChange))
724 .unwrap();
725 assert_eq!(out.apdus, vec![s(&ProfileEnq)]);
726 }
727
728 #[test]
729 fn application_information_surfaces_notification() {
730 use dvb_ci::objects::application_info::ApplicationType;
731 let mut h = ApplicationInformation;
732 assert_eq!(h.on_open().unwrap().apdus, vec![s(&ApplicationInfoEnq)]);
733 let ai = s(&ApplicationInfo {
734 application_type: ApplicationType::ConditionalAccess,
735 application_manufacturer: 0x1234,
736 manufacturer_code: 0x5678,
737 menu_string: b"Acme CAM",
738 });
739 let out = h.on_apdu(&ai).unwrap();
740 assert_eq!(
741 out.notify,
742 vec![Notification::ApplicationInfo {
743 application_type: 0x01,
744 manufacturer: 0x1234,
745 code: 0x5678,
746 menu: "Acme CAM".to_string(),
747 }]
748 );
749 }
750
751 #[test]
752 fn host_control_surfaces_tune_replace_clear_and_ask_release() {
753 let mut h = HostControl;
754 assert_eq!(h.id(), HOST_CONTROL);
755
756 let tune = s(&Tune {
758 network_id: 0x1122,
759 original_network_id: 0x3344,
760 transport_stream_id: 0x5566,
761 service_id: 0x7788,
762 });
763 assert_eq!(
764 h.on_apdu(&tune).unwrap().notify,
765 vec![Notification::HostControl(HostControlEvent::Tune {
766 network_id: 0x1122,
767 original_network_id: 0x3344,
768 transport_stream_id: 0x5566,
769 service_id: 0x7788,
770 })]
771 );
772
773 let replace = s(&Replace {
775 replacement_ref: 0x07,
776 replaced_pid: 0x0123,
777 replacement_pid: 0x01FF,
778 });
779 assert_eq!(
780 h.on_apdu(&replace).unwrap().notify,
781 vec![Notification::HostControl(HostControlEvent::Replace {
782 replacement_ref: 0x07,
783 replaced_pid: 0x0123,
784 replacement_pid: 0x01FF,
785 })]
786 );
787
788 let clear = s(&ClearReplace {
790 replacement_ref: 0x42,
791 });
792 assert_eq!(
793 h.on_apdu(&clear).unwrap().notify,
794 vec![Notification::HostControl(HostControlEvent::ClearReplace {
795 replacement_ref: 0x42,
796 })]
797 );
798
799 let ask = s(&AskRelease);
801 assert_eq!(
802 h.on_apdu(&ask).unwrap().notify,
803 vec![Notification::HostControl(HostControlEvent::AskRelease)]
804 );
805
806 assert!(h.on_apdu(&tune).unwrap().apdus.is_empty());
808 }
809
810 #[test]
811 fn mjd_bcd_encoding_is_correct() {
812 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
814 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
816 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
817 }
818
819 #[test]
820 fn date_time_replies_to_enq_and_resends_on_interval() {
821 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
822 let mut h = DateTime::with_clock(fixed);
823 let enq = s(&DateTimeEnq {
825 response_interval: 5,
826 });
827 let out = h.on_apdu(&enq).unwrap();
828 assert_eq!(out.apdus.len(), 1);
829 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
830 assert!(h.tick(Duration::from_secs(3)).unwrap().apdus.is_empty());
832 assert_eq!(h.tick(Duration::from_secs(3)).unwrap().apdus.len(), 1);
834 }
835
836 #[test]
837 fn date_time_interval_zero_does_not_resend() {
838 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
839 h.on_apdu(&s(&DateTimeEnq {
840 response_interval: 0,
841 }))
842 .unwrap();
843 assert!(h.tick(Duration::from_secs(60)).unwrap().apdus.is_empty());
844 }
845
846 #[test]
851 fn date_time_reset_clears_interval_and_resend_timer() {
852 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
853 h.on_apdu(&s(&DateTimeEnq {
854 response_interval: 5,
855 }))
856 .unwrap();
857 assert!(h.tick(Duration::from_secs(3)).unwrap().apdus.is_empty());
859
860 h.reset();
861 assert!(
865 h.tick(Duration::from_secs(60)).unwrap().apdus.is_empty(),
866 "reset must clear the resend cadence from the previous module"
867 );
868 }
869
870 #[test]
871 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
872 let mut h = ConditionalAccess;
873 assert_eq!(h.on_open().unwrap().apdus, vec![s(&CaInfoEnq)]);
874 let ci = s(&CaInfo {
876 ca_system_ids: vec![0x0B00, 0x1800],
877 });
878 assert_eq!(
879 h.on_apdu(&ci).unwrap().notify,
880 vec![Notification::CaInfo {
881 ca_system_ids: vec![0x0B00, 0x1800],
882 }]
883 );
884 let reply = s(&CaPmtReply {
886 program_number: 0x0042,
887 version_number: 0,
888 current_next_indicator: true,
889 ca_enable: Some(CaEnable::Possible),
890 streams: vec![],
891 });
892 assert_eq!(
893 h.on_apdu(&reply).unwrap().notify,
894 vec![Notification::CaPmtReply {
895 program_number: 0x0042,
896 ca_enable: Some(CaEnable::Possible),
897 descrambling_ok: true,
898 }]
899 );
900 }
901
902 #[test]
903 fn conditional_access_ca_pmt_reply_flag_clear_surfaces_none() {
904 let mut h = ConditionalAccess;
905 h.on_open().unwrap();
906 let reply = s(&CaPmtReply {
908 program_number: 0x0007,
909 version_number: 0,
910 current_next_indicator: true,
911 ca_enable: None,
912 streams: vec![],
913 });
914 assert_eq!(
915 h.on_apdu(&reply).unwrap().notify,
916 vec![Notification::CaPmtReply {
917 program_number: 0x0007,
918 ca_enable: None,
919 descrambling_ok: false,
920 }]
921 );
922 }
923}