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::default())?],
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 length_form: dvb_ci::length::LengthForm::Minimal,
153 resources: self.host_resources.clone(),
154 })?);
155 }
156 Some(t) if t == tag::PROFILE => {
158 let p = Profile::parse(apdu)?;
161 self.module_resources = p.resources;
162 self.module_profiled = true;
163 }
164 Some(t) if t == tag::PROFILE_CHANGE => {
166 out.apdus.push(ser(&ProfileEnq::default())?);
167 self.module_profiled = false;
168 self.ready = false;
169 }
170 _ => {}
171 }
172 if self.module_profiled && !self.ready {
186 self.ready = true;
187 out.apdus.push(ser(&ProfileChange::default())?);
188 out.notify.push(Notification::CamReady);
189 }
190 Ok(out)
191 }
192
193 fn reset(&mut self) {
199 self.module_resources.clear();
200 self.module_profiled = false;
201 self.ready = false;
202 }
203}
204
205#[derive(Debug, Default)]
208pub struct ApplicationInformation;
209
210impl Resource for ApplicationInformation {
211 fn id(&self) -> ResourceId {
212 APPLICATION_INFORMATION
213 }
214
215 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
216 Ok(ResourceOut {
217 apdus: vec![ser(&ApplicationInfoEnq::default())?],
218 ..ResourceOut::default()
219 })
220 }
221
222 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
223 let mut out = ResourceOut::default();
224 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
225 let ai = ApplicationInfo::parse(apdu)?;
226 out.notify.push(Notification::ApplicationInfo {
227 application_type: ai.application_type.to_u8(),
228 manufacturer: ai.application_manufacturer,
229 code: ai.manufacturer_code,
230 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
231 });
232 }
233 Ok(out)
234 }
235 }
237
238#[derive(Debug, Default)]
243pub struct ConditionalAccess;
244
245impl Resource for ConditionalAccess {
246 fn id(&self) -> ResourceId {
247 CONDITIONAL_ACCESS_SUPPORT
248 }
249
250 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
251 Ok(ResourceOut {
252 apdus: vec![ser(&CaInfoEnq::default())?],
253 ..ResourceOut::default()
254 })
255 }
256
257 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
258 let mut out = ResourceOut::default();
259 match peek_tag(apdu) {
260 Some(t) if t == tag::CA_INFO => {
261 let ci = CaInfo::parse(apdu)?;
264 out.notify.push(Notification::CaInfo {
265 ca_system_ids: ci.ca_system_ids,
266 });
267 }
268 Some(t) if t == tag::CA_PMT_REPLY => {
269 let r = CaPmtReply::parse(apdu)?;
272 let descrambling_ok = matches!(
280 r.ca_enable,
281 Some(
282 CaEnable::Possible
283 | CaEnable::PossiblePurchaseDialogue
284 | CaEnable::PossibleTechnicalDialogue
285 )
286 );
287 out.notify.push(Notification::CaPmtReply {
288 program_number: r.program_number,
289 ca_enable: r.ca_enable,
290 descrambling_ok,
291 });
292 }
293 _ => {}
294 }
295 Ok(out)
296 }
297 }
299
300const SECS_PER_DAY: u64 = 86_400;
301const MJD_UNIX_EPOCH: u64 = 40_587;
303
304fn bcd(v: u64) -> u8 {
305 (((v / 10) << 4) | (v % 10)) as u8
306}
307
308fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
311 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
312 let sod = unix_secs % SECS_PER_DAY;
313 [
314 (mjd >> 8) as u8,
315 mjd as u8,
316 bcd(sod / 3600),
317 bcd((sod % 3600) / 60),
318 bcd(sod % 60),
319 ]
320}
321
322fn system_utc() -> [u8; UTC_TIME_LEN] {
323 let secs = std::time::SystemTime::now()
324 .duration_since(std::time::UNIX_EPOCH)
325 .map(|d| d.as_secs())
326 .unwrap_or(0);
327 unix_to_mjd_bcd(secs)
328}
329
330pub struct DateTime {
334 clock: fn() -> [u8; UTC_TIME_LEN],
335 interval: u8,
336 since: Duration,
337}
338
339impl Default for DateTime {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl DateTime {
346 #[must_use]
348 pub fn new() -> Self {
349 Self {
350 clock: system_utc,
351 interval: 0,
352 since: Duration::ZERO,
353 }
354 }
355
356 #[must_use]
358 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
359 Self {
360 clock,
361 interval: 0,
362 since: Duration::ZERO,
363 }
364 }
365
366 fn reply(&self) -> dvb_ci::Result<Vec<u8>> {
367 ser(&CiDateTime {
368 length_form: dvb_ci::length::LengthForm::Minimal,
369 utc_time: (self.clock)(),
370 local_offset: None,
371 })
372 }
373}
374
375impl Resource for DateTime {
376 fn id(&self) -> ResourceId {
377 DATE_TIME
378 }
379
380 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
381 let mut out = ResourceOut::default();
382 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
383 let enq = DateTimeEnq::parse(apdu)?;
384 self.interval = enq.response_interval;
385 self.since = Duration::ZERO;
386 out.apdus.push(self.reply()?);
387 }
388 Ok(out)
389 }
390
391 fn tick(&mut self, elapsed: Duration) -> dvb_ci::Result<ResourceOut> {
392 let mut out = ResourceOut::default();
393 if self.interval > 0 {
394 self.since += elapsed;
395 if self.since >= Duration::from_secs(u64::from(self.interval)) {
396 self.since = Duration::ZERO;
397 out.apdus.push(self.reply()?);
398 }
399 }
400 Ok(out)
401 }
402
403 fn reset(&mut self) {
409 self.interval = 0;
410 self.since = Duration::ZERO;
411 }
412}
413
414#[derive(Debug, Default)]
422pub struct Mmi;
423
424impl Resource for Mmi {
425 fn id(&self) -> ResourceId {
426 MMI
427 }
428
429 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
430 let mut out = ResourceOut::default();
431 match peek_tag(apdu) {
432 Some(t) if t == tag::ENQ => {
433 let e = Enq::parse(apdu)?;
436 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
437 prompt: text(e.text_chars),
438 blind: e.blind_answer,
439 answer_len: e.answer_text_length,
440 }));
441 }
442 Some(t) if t == tag::MENU_LAST => {
443 let m = Menu::parse(apdu)?;
446 out.notify
447 .push(Notification::Mmi(MmiEvent::Menu(to_menu(&m))));
448 }
449 Some(t) if t == tag::LIST_LAST => {
450 let l = List::parse(apdu)?;
453 out.notify
454 .push(Notification::Mmi(MmiEvent::List(to_menu(&l.0))));
455 }
456 Some(t) if t == tag::CLOSE_MMI => {
457 dvb_ci::objects::mmi_close::CloseMmi::parse(apdu)?;
458 out.notify.push(Notification::Mmi(MmiEvent::Close));
459 }
460 Some(t) if t == tag::DISPLAY_CONTROL => {
466 let dc = DisplayControl::parse(apdu)?;
469 let reply = match dc.cmd {
470 DisplayControlCmd::SetMmiMode => DisplayReply {
472 length_form: dvb_ci::length::LengthForm::Minimal,
473 reply_id: DisplayReplyId::MmiModeAck,
474 body: DisplayReplyBody::MmiModeAck(
475 dc.mmi_mode.unwrap_or(MmiMode::HighLevel),
476 ),
477 },
478 _ => DisplayReply {
481 length_form: dvb_ci::length::LengthForm::Minimal,
482 reply_id: DisplayReplyId::UnknownDisplayControlCmd,
483 body: DisplayReplyBody::None,
484 },
485 };
486 out.apdus.push(ser(&reply)?);
487 }
488 _ => {}
489 }
490 Ok(out)
491 }
492 }
494
495#[derive(Debug, Default)]
502pub struct HostControl;
503
504impl Resource for HostControl {
505 fn id(&self) -> ResourceId {
506 HOST_CONTROL
507 }
508
509 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
510 let mut out = ResourceOut::default();
511 let event = match peek_tag(apdu) {
515 Some(t) if t == tag::TUNE => {
516 let t = Tune::parse(apdu)?;
517 Some(HostControlEvent::Tune {
518 network_id: t.network_id,
519 original_network_id: t.original_network_id,
520 transport_stream_id: t.transport_stream_id,
521 service_id: t.service_id,
522 })
523 }
524 Some(t) if t == tag::REPLACE => {
525 let r = Replace::parse(apdu)?;
526 Some(HostControlEvent::Replace {
527 replacement_ref: r.replacement_ref,
528 replaced_pid: r.replaced_pid,
529 replacement_pid: r.replacement_pid,
530 })
531 }
532 Some(t) if t == tag::CLEAR_REPLACE => {
533 let c = ClearReplace::parse(apdu)?;
534 Some(HostControlEvent::ClearReplace {
535 replacement_ref: c.replacement_ref,
536 })
537 }
538 Some(t) if t == tag::ASK_RELEASE => {
539 AskRelease::parse(apdu)?;
540 Some(HostControlEvent::AskRelease)
541 }
542 _ => None,
543 };
544 if let Some(event) = event {
545 out.notify.push(Notification::HostControl(event));
546 }
547 Ok(out)
548 }
549 }
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use dvb_ci::objects::resource_manager::Profile;
556 use mpeg_ts::pid::Pid;
557
558 fn s<S: Serialize<Error = dvb_ci::Error>>(v: &S) -> Vec<u8> {
561 ser(v).expect("test value must serialize")
562 }
563
564 #[test]
565 fn on_open_sends_profile_enq() {
566 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
567 let out = rm.on_open().unwrap();
568 assert_eq!(out.apdus, vec![s(&ProfileEnq::default())]);
569 }
570
571 #[test]
572 fn module_profile_triggers_profile_change_and_camready() {
573 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
579 rm.on_open().unwrap();
580 let empty_profile = s(&Profile {
581 length_form: dvb_ci::length::LengthForm::Minimal,
582 resources: vec![],
583 });
584 let o = rm.on_apdu(&empty_profile).unwrap();
585 assert!(o.notify.contains(&Notification::CamReady));
586 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
587 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
588 assert!(o.open.is_empty(), "host opens no sessions itself");
589 }
590
591 #[test]
592 fn answers_a_module_profile_enquiry_without_re_readying() {
593 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
594 rm.on_open().unwrap();
595 rm.on_apdu(&s(&Profile {
596 length_form: dvb_ci::length::LengthForm::Minimal,
597 resources: vec![APPLICATION_INFORMATION],
598 }))
599 .unwrap();
600 let o = rm.on_apdu(&s(&ProfileEnq::default())).unwrap();
602 assert_eq!(o.apdus.len(), 1);
603 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
604 assert!(!o.notify.contains(&Notification::CamReady));
605 }
606
607 #[test]
613 fn resource_manager_reset_clears_latched_handshake_state() {
614 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
615 rm.on_open().unwrap();
616 let o = rm
617 .on_apdu(&s(&Profile {
618 length_form: dvb_ci::length::LengthForm::Minimal,
619 resources: vec![APPLICATION_INFORMATION],
620 }))
621 .unwrap();
622 assert!(o.notify.contains(&Notification::CamReady), "dirtied: ready");
623 assert!(
624 !rm.module_resources().is_empty(),
625 "dirtied: module_resources"
626 );
627
628 rm.reset();
629 assert!(
630 rm.module_resources().is_empty(),
631 "reset must clear module_resources"
632 );
633
634 rm.on_open().unwrap();
637 let o2 = rm
638 .on_apdu(&s(&Profile {
639 length_form: dvb_ci::length::LengthForm::Minimal,
640 resources: vec![],
641 }))
642 .unwrap();
643 assert!(
644 o2.notify.contains(&Notification::CamReady),
645 "a fresh handshake after reset() must fire CamReady again"
646 );
647 }
648
649 #[test]
650 fn mmi_surfaces_enquiry_and_close() {
651 let mut h = Mmi;
652 let enq = s(&Enq {
654 reserved: Default::default(),
655 length_form: dvb_ci::length::LengthForm::Minimal,
656 blind_answer: true,
657 answer_text_length: 4,
658 text_chars: b"PIN?",
659 });
660 assert_eq!(
661 h.on_apdu(&enq).unwrap().notify,
662 vec![Notification::Mmi(MmiEvent::Enquiry {
663 prompt: "PIN?".to_string(),
664 blind: true,
665 answer_len: 4,
666 })]
667 );
668 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
670 assert_eq!(
671 h.on_apdu(&close).unwrap().notify,
672 vec![Notification::Mmi(MmiEvent::Close)]
673 );
674 }
675
676 #[test]
677 fn mmi_surfaces_structured_menu_and_list() {
678 use dvb_ci::objects::mmi_high::{List, Menu, Text};
679 let txt = |s: &'static [u8]| Text {
680 length_form: dvb_ci::length::LengthForm::Minimal,
681 more: false,
682 text_chars: s,
683 };
684 let mut h = Mmi;
685 let menu = s(&Menu {
687 length_form: dvb_ci::length::LengthForm::Minimal,
688 more: false,
689 choice_nb: 2,
690 title: txt(b"AlphaCrypt"),
691 subtitle: txt(b"Module Mainmenu"),
692 bottom: txt(b"Select item and press OK"),
693 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
694 });
695 assert_eq!(
696 h.on_apdu(&menu).unwrap().notify,
697 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
698 title: "AlphaCrypt".to_string(),
699 subtitle: "Module Mainmenu".to_string(),
700 bottom: "Select item and press OK".to_string(),
701 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
702 }))]
703 );
704 let list = s(&List(Menu {
706 length_form: dvb_ci::length::LengthForm::Minimal,
707 more: false,
708 choice_nb: 0xFF,
709 title: txt(b"Entitlements"),
710 subtitle: txt(b""),
711 bottom: txt(b""),
712 choices: vec![txt(b"ORF AUT")],
713 }));
714 assert_eq!(
715 h.on_apdu(&list).unwrap().notify,
716 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
717 title: "Entitlements".to_string(),
718 subtitle: String::new(),
719 bottom: String::new(),
720 choices: vec!["ORF AUT".to_string()],
721 }))]
722 );
723 }
724
725 #[test]
726 fn mmi_answers_display_control_set_mmi_mode() {
727 use dvb_ci::objects::mmi_display::{DisplayControl, DisplayControlCmd, MmiMode};
728 let mut h = Mmi;
729 let dc = s(&DisplayControl {
730 length_form: dvb_ci::length::LengthForm::Minimal,
731 cmd: DisplayControlCmd::SetMmiMode,
732 mmi_mode: Some(MmiMode::HighLevel),
733 });
734 let out = h.on_apdu(&dc).unwrap();
735 assert_eq!(out.apdus, vec![vec![0x9F, 0x88, 0x02, 0x02, 0x01, 0x01]]);
737 assert!(out.notify.is_empty());
738 }
739
740 #[test]
741 fn profile_change_re_enquires() {
742 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
743 let out = rm
744 .on_apdu(&s(
745 &dvb_ci::objects::resource_manager::ProfileChange::default(),
746 ))
747 .unwrap();
748 assert_eq!(out.apdus, vec![s(&ProfileEnq::default())]);
749 }
750
751 #[test]
752 fn application_information_surfaces_notification() {
753 use dvb_ci::objects::application_info::ApplicationType;
754 let mut h = ApplicationInformation;
755 assert_eq!(
756 h.on_open().unwrap().apdus,
757 vec![s(&ApplicationInfoEnq::default())]
758 );
759 let ai = s(&ApplicationInfo {
760 length_form: dvb_ci::length::LengthForm::Minimal,
761 application_type: ApplicationType::ConditionalAccess,
762 application_manufacturer: 0x1234,
763 manufacturer_code: 0x5678,
764 menu_string: b"Acme CAM",
765 });
766 let out = h.on_apdu(&ai).unwrap();
767 assert_eq!(
768 out.notify,
769 vec![Notification::ApplicationInfo {
770 application_type: 0x01,
771 manufacturer: 0x1234,
772 code: 0x5678,
773 menu: "Acme CAM".to_string(),
774 }]
775 );
776 }
777
778 #[test]
779 fn host_control_surfaces_tune_replace_clear_and_ask_release() {
780 let mut h = HostControl;
781 assert_eq!(h.id(), HOST_CONTROL);
782
783 let tune = s(&Tune {
785 length_form: dvb_ci::length::LengthForm::Minimal,
786 network_id: 0x1122,
787 original_network_id: 0x3344,
788 transport_stream_id: 0x5566,
789 service_id: 0x7788,
790 });
791 assert_eq!(
792 h.on_apdu(&tune).unwrap().notify,
793 vec![Notification::HostControl(HostControlEvent::Tune {
794 network_id: 0x1122,
795 original_network_id: 0x3344,
796 transport_stream_id: 0x5566,
797 service_id: 0x7788,
798 })]
799 );
800
801 let replace = s(&Replace {
803 length_form: dvb_ci::length::LengthForm::Minimal,
804 replacement_ref: 0x07,
805 replaced_pid: Pid::new(0x0123),
806 replacement_pid: Pid::new(0x01FF),
807 replaced_reserved: Default::default(),
808 replacement_reserved: Default::default(),
809 });
810 assert_eq!(
811 h.on_apdu(&replace).unwrap().notify,
812 vec![Notification::HostControl(HostControlEvent::Replace {
813 replacement_ref: 0x07,
814 replaced_pid: Pid::new(0x0123),
815 replacement_pid: Pid::new(0x01FF),
816 })]
817 );
818
819 let clear = s(&ClearReplace {
821 length_form: dvb_ci::length::LengthForm::Minimal,
822 replacement_ref: 0x42,
823 });
824 assert_eq!(
825 h.on_apdu(&clear).unwrap().notify,
826 vec![Notification::HostControl(HostControlEvent::ClearReplace {
827 replacement_ref: 0x42,
828 })]
829 );
830
831 let ask = s(&AskRelease::default());
833 assert_eq!(
834 h.on_apdu(&ask).unwrap().notify,
835 vec![Notification::HostControl(HostControlEvent::AskRelease)]
836 );
837
838 assert!(h.on_apdu(&tune).unwrap().apdus.is_empty());
840 }
841
842 #[test]
843 fn mjd_bcd_encoding_is_correct() {
844 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
846 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
848 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
849 }
850
851 #[test]
852 fn date_time_replies_to_enq_and_resends_on_interval() {
853 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
854 let mut h = DateTime::with_clock(fixed);
855 let enq = s(&DateTimeEnq {
857 length_form: dvb_ci::length::LengthForm::Minimal,
858 response_interval: 5,
859 });
860 let out = h.on_apdu(&enq).unwrap();
861 assert_eq!(out.apdus.len(), 1);
862 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
863 assert!(h.tick(Duration::from_secs(3)).unwrap().apdus.is_empty());
865 assert_eq!(h.tick(Duration::from_secs(3)).unwrap().apdus.len(), 1);
867 }
868
869 #[test]
870 fn date_time_interval_zero_does_not_resend() {
871 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
872 h.on_apdu(&s(&DateTimeEnq {
873 length_form: dvb_ci::length::LengthForm::Minimal,
874 response_interval: 0,
875 }))
876 .unwrap();
877 assert!(h.tick(Duration::from_secs(60)).unwrap().apdus.is_empty());
878 }
879
880 #[test]
885 fn date_time_reset_clears_interval_and_resend_timer() {
886 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
887 h.on_apdu(&s(&DateTimeEnq {
888 length_form: dvb_ci::length::LengthForm::Minimal,
889 response_interval: 5,
890 }))
891 .unwrap();
892 assert!(h.tick(Duration::from_secs(3)).unwrap().apdus.is_empty());
894
895 h.reset();
896 assert!(
900 h.tick(Duration::from_secs(60)).unwrap().apdus.is_empty(),
901 "reset must clear the resend cadence from the previous module"
902 );
903 }
904
905 #[test]
906 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
907 let mut h = ConditionalAccess;
908 assert_eq!(h.on_open().unwrap().apdus, vec![s(&CaInfoEnq::default())]);
909 let ci = s(&CaInfo {
911 length_form: dvb_ci::length::LengthForm::Minimal,
912 ca_system_ids: vec![0x0B00, 0x1800],
913 });
914 assert_eq!(
915 h.on_apdu(&ci).unwrap().notify,
916 vec![Notification::CaInfo {
917 ca_system_ids: vec![0x0B00, 0x1800],
918 }]
919 );
920 let reply = s(&CaPmtReply {
922 reserved_ca_enable: Default::default(),
923 reserved_version: Default::default(),
924 length_form: dvb_ci::length::LengthForm::Minimal,
925 program_number: 0x0042,
926 version_number: 0,
927 current_next_indicator: true,
928 ca_enable: Some(CaEnable::Possible),
929 streams: vec![],
930 });
931 assert_eq!(
932 h.on_apdu(&reply).unwrap().notify,
933 vec![Notification::CaPmtReply {
934 program_number: 0x0042,
935 ca_enable: Some(CaEnable::Possible),
936 descrambling_ok: true,
937 }]
938 );
939 }
940
941 #[test]
942 fn conditional_access_ca_pmt_reply_flag_clear_surfaces_none() {
943 let mut h = ConditionalAccess;
944 h.on_open().unwrap();
945 let reply = s(&CaPmtReply {
947 reserved_ca_enable: Default::default(),
948 reserved_version: Default::default(),
949 length_form: dvb_ci::length::LengthForm::Minimal,
950 program_number: 0x0007,
951 version_number: 0,
952 current_next_indicator: true,
953 ca_enable: None,
954 streams: vec![],
955 });
956 assert_eq!(
957 h.on_apdu(&reply).unwrap().notify,
958 vec![Notification::CaPmtReply {
959 program_number: 0x0007,
960 ca_enable: None,
961 descrambling_ok: false,
962 }]
963 );
964 }
965}