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 if apdu.len() < APDU_TAG_LEN {
148 return Err(dvb_ci::Error::InvalidLength("short APDU"));
149 }
150 let mut out = ResourceOut::default();
151 match peek_tag(apdu) {
152 Some(t) if t == tag::PROFILE_ENQ => {
154 out.apdus.push(ser(&Profile {
155 length_form: dvb_ci::length::LengthForm::Minimal,
156 resources: self.host_resources.clone(),
157 })?);
158 }
159 Some(t) if t == tag::PROFILE => {
161 let p = Profile::parse(apdu)?;
164 self.module_resources = p.resources;
165 self.module_profiled = true;
166 }
167 Some(t) if t == tag::PROFILE_CHANGE => {
169 out.apdus.push(ser(&ProfileEnq::default())?);
170 self.module_profiled = false;
171 self.ready = false;
172 }
173 _ => {}
174 }
175 if self.module_profiled && !self.ready {
189 self.ready = true;
190 out.apdus.push(ser(&ProfileChange::default())?);
191 out.notify.push(Notification::CamReady);
192 }
193 Ok(out)
194 }
195
196 fn reset(&mut self) {
202 self.module_resources.clear();
203 self.module_profiled = false;
204 self.ready = false;
205 }
206}
207
208#[derive(Debug, Default)]
211pub struct ApplicationInformation;
212
213impl Resource for ApplicationInformation {
214 fn id(&self) -> ResourceId {
215 APPLICATION_INFORMATION
216 }
217
218 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
219 Ok(ResourceOut {
220 apdus: vec![ser(&ApplicationInfoEnq::default())?],
221 ..ResourceOut::default()
222 })
223 }
224
225 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
226 if apdu.len() < APDU_TAG_LEN {
227 return Err(dvb_ci::Error::InvalidLength("short APDU"));
228 }
229 let mut out = ResourceOut::default();
230 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
231 let ai = ApplicationInfo::parse(apdu)?;
232 out.notify.push(Notification::ApplicationInfo {
233 application_type: ai.application_type.to_u8(),
234 manufacturer: ai.application_manufacturer,
235 code: ai.manufacturer_code,
236 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
237 });
238 }
239 Ok(out)
240 }
241 }
243
244#[derive(Debug, Default)]
249pub struct ConditionalAccess;
250
251impl Resource for ConditionalAccess {
252 fn id(&self) -> ResourceId {
253 CONDITIONAL_ACCESS_SUPPORT
254 }
255
256 fn on_open(&mut self) -> dvb_ci::Result<ResourceOut> {
257 Ok(ResourceOut {
258 apdus: vec![ser(&CaInfoEnq::default())?],
259 ..ResourceOut::default()
260 })
261 }
262
263 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
264 if apdu.len() < APDU_TAG_LEN {
265 return Err(dvb_ci::Error::InvalidLength("short APDU"));
266 }
267 let mut out = ResourceOut::default();
268 match peek_tag(apdu) {
269 Some(t) if t == tag::CA_INFO => {
270 let ci = CaInfo::parse(apdu)?;
273 out.notify.push(Notification::CaInfo {
274 ca_system_ids: ci.ca_system_ids,
275 });
276 }
277 Some(t) if t == tag::CA_PMT_REPLY => {
278 let r = CaPmtReply::parse(apdu)?;
281 let descrambling_ok = matches!(
289 r.ca_enable,
290 Some(
291 CaEnable::Possible
292 | CaEnable::PossiblePurchaseDialogue
293 | CaEnable::PossibleTechnicalDialogue
294 )
295 );
296 out.notify.push(Notification::CaPmtReply {
297 program_number: r.program_number,
298 ca_enable: r.ca_enable,
299 descrambling_ok,
300 });
301 }
302 _ => {}
303 }
304 Ok(out)
305 }
306 }
308
309const SECS_PER_DAY: u64 = 86_400;
310const MJD_UNIX_EPOCH: u64 = 40_587;
312
313fn bcd(v: u64) -> u8 {
314 (((v / 10) << 4) | (v % 10)) as u8
315}
316
317fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
320 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
321 let sod = unix_secs % SECS_PER_DAY;
322 [
323 (mjd >> 8) as u8,
324 mjd as u8,
325 bcd(sod / 3600),
326 bcd((sod % 3600) / 60),
327 bcd(sod % 60),
328 ]
329}
330
331fn system_utc() -> [u8; UTC_TIME_LEN] {
332 let secs = std::time::SystemTime::now()
333 .duration_since(std::time::UNIX_EPOCH)
334 .map(|d| d.as_secs())
335 .unwrap_or(0);
336 unix_to_mjd_bcd(secs)
337}
338
339pub struct DateTime {
343 clock: fn() -> [u8; UTC_TIME_LEN],
344 interval: u8,
345 since: Duration,
346}
347
348impl Default for DateTime {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354impl DateTime {
355 #[must_use]
357 pub fn new() -> Self {
358 Self {
359 clock: system_utc,
360 interval: 0,
361 since: Duration::ZERO,
362 }
363 }
364
365 #[must_use]
367 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
368 Self {
369 clock,
370 interval: 0,
371 since: Duration::ZERO,
372 }
373 }
374
375 fn reply(&self) -> dvb_ci::Result<Vec<u8>> {
376 ser(&CiDateTime {
377 length_form: dvb_ci::length::LengthForm::Minimal,
378 utc_time: (self.clock)(),
379 local_offset: None,
380 })
381 }
382}
383
384impl Resource for DateTime {
385 fn id(&self) -> ResourceId {
386 DATE_TIME
387 }
388
389 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
390 if apdu.len() < APDU_TAG_LEN {
391 return Err(dvb_ci::Error::InvalidLength("short APDU"));
392 }
393 let mut out = ResourceOut::default();
394 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
395 let enq = DateTimeEnq::parse(apdu)?;
396 self.interval = enq.response_interval;
397 self.since = Duration::ZERO;
398 out.apdus.push(self.reply()?);
399 }
400 Ok(out)
401 }
402
403 fn tick(&mut self, elapsed: Duration) -> dvb_ci::Result<ResourceOut> {
404 let mut out = ResourceOut::default();
405 if self.interval > 0 {
406 self.since += elapsed;
407 if self.since >= Duration::from_secs(u64::from(self.interval)) {
408 self.since = Duration::ZERO;
409 out.apdus.push(self.reply()?);
410 }
411 }
412 Ok(out)
413 }
414
415 fn reset(&mut self) {
421 self.interval = 0;
422 self.since = Duration::ZERO;
423 }
424}
425
426#[derive(Debug, Default)]
434pub struct Mmi {
435 chain: Option<MmiChain>,
436}
437
438pub const MAX_MMI_CHAIN_LEN: usize = 0xFFFF;
441
442const APDU_TAG_LEN: usize = 3;
443
444#[derive(Debug)]
445struct MmiChain {
446 last: ApduTag,
447 body: Vec<u8>,
448}
449
450impl Mmi {
451 fn chain_more(&mut self, apdu: &[u8], last: ApduTag) -> dvb_ci::Result<()> {
452 let tail = apdu
453 .get(APDU_TAG_LEN..)
454 .ok_or(dvb_ci::Error::InvalidLength("short APDU"))?;
455 let (len, hdr) = dvb_ci::length::decode(tail)?;
456 let start = APDU_TAG_LEN + hdr;
457 let data = apdu
458 .get(start..start + len)
459 .ok_or(dvb_ci::Error::LengthTooLarge(len))?;
460 let mut chain = match self.chain.take() {
461 Some(c) if c.last == last => c,
462 _ => MmiChain {
463 last,
464 body: Vec::new(),
465 },
466 };
467 if chain.body.len() + data.len() > MAX_MMI_CHAIN_LEN {
468 return Err(dvb_ci::Error::LengthTooLarge(chain.body.len() + data.len()));
469 }
470 chain.body.extend_from_slice(data);
471 self.chain = Some(chain);
472 Ok(())
473 }
474
475 fn complete(&mut self, apdu: &[u8], last: ApduTag) -> dvb_ci::Result<Vec<u8>> {
479 let Some(chain) = self.chain.take().filter(|c| c.last == last) else {
480 return Ok(apdu.to_vec());
481 };
482 let tail = apdu
483 .get(APDU_TAG_LEN..)
484 .ok_or(dvb_ci::Error::InvalidLength("short APDU"))?;
485 let (len, hdr) = dvb_ci::length::decode(tail)?;
486 let start = APDU_TAG_LEN + hdr;
487 let data = apdu
488 .get(start..start + len)
489 .ok_or(dvb_ci::Error::LengthTooLarge(len))?;
490 let total = chain.body.len() + data.len();
491 let mut buf = vec![0u8; APDU_TAG_LEN + dvb_ci::length::encoded_len(total) + total];
492 buf[..APDU_TAG_LEN].copy_from_slice(&last.to_bytes());
493 let n = dvb_ci::length::encode_into(total, &mut buf[APDU_TAG_LEN..])?;
494 let body_at = APDU_TAG_LEN + n;
495 buf[body_at..body_at + chain.body.len()].copy_from_slice(&chain.body);
496 buf[body_at + chain.body.len()..].copy_from_slice(data);
497 Ok(buf)
498 }
499}
500
501impl Resource for Mmi {
502 fn id(&self) -> ResourceId {
503 MMI
504 }
505
506 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
507 if apdu.len() < APDU_TAG_LEN {
508 return Err(dvb_ci::Error::InvalidLength("short APDU"));
509 }
510 let mut out = ResourceOut::default();
511 match peek_tag(apdu) {
512 Some(t) if t == tag::ENQ => {
513 let e = Enq::parse(apdu)?;
516 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
517 prompt: text(e.text_chars),
518 blind: e.blind_answer,
519 answer_len: e.answer_text_length,
520 }));
521 }
522 Some(t) if t == tag::MENU_MORE => self.chain_more(apdu, tag::MENU_LAST)?,
523 Some(t) if t == tag::LIST_MORE => self.chain_more(apdu, tag::LIST_LAST)?,
524 Some(t) if t == tag::MENU_LAST => {
525 let full = self.complete(apdu, tag::MENU_LAST)?;
528 let m = Menu::parse(&full)?;
529 out.notify
530 .push(Notification::Mmi(MmiEvent::Menu(to_menu(&m))));
531 }
532 Some(t) if t == tag::LIST_LAST => {
533 let full = self.complete(apdu, tag::LIST_LAST)?;
536 let l = List::parse(&full)?;
537 out.notify
538 .push(Notification::Mmi(MmiEvent::List(to_menu(&l.0))));
539 }
540 Some(t) if t == tag::CLOSE_MMI => {
541 dvb_ci::objects::mmi_close::CloseMmi::parse(apdu)?;
542 out.notify.push(Notification::Mmi(MmiEvent::Close));
543 }
544 Some(t) if t == tag::DISPLAY_CONTROL => {
550 let dc = DisplayControl::parse(apdu)?;
553 let reply = match dc.cmd {
554 DisplayControlCmd::SetMmiMode => DisplayReply {
556 length_form: dvb_ci::length::LengthForm::Minimal,
557 reply_id: DisplayReplyId::MmiModeAck,
558 body: DisplayReplyBody::MmiModeAck(
559 dc.mmi_mode.unwrap_or(MmiMode::HighLevel),
560 ),
561 },
562 _ => DisplayReply {
565 length_form: dvb_ci::length::LengthForm::Minimal,
566 reply_id: DisplayReplyId::UnknownDisplayControlCmd,
567 body: DisplayReplyBody::None,
568 },
569 };
570 out.apdus.push(ser(&reply)?);
571 }
572 _ => {}
573 }
574 Ok(out)
575 }
576 fn reset(&mut self) {
577 self.chain = None;
578 }
579}
580
581#[derive(Debug, Default)]
588pub struct HostControl;
589
590impl Resource for HostControl {
591 fn id(&self) -> ResourceId {
592 HOST_CONTROL
593 }
594
595 fn on_apdu(&mut self, apdu: &[u8]) -> dvb_ci::Result<ResourceOut> {
596 if apdu.len() < APDU_TAG_LEN {
597 return Err(dvb_ci::Error::InvalidLength("short APDU"));
598 }
599 let mut out = ResourceOut::default();
600 let event = match peek_tag(apdu) {
604 Some(t) if t == tag::TUNE => {
605 let t = Tune::parse(apdu)?;
606 Some(HostControlEvent::Tune {
607 network_id: t.network_id,
608 original_network_id: t.original_network_id,
609 transport_stream_id: t.transport_stream_id,
610 service_id: t.service_id,
611 })
612 }
613 Some(t) if t == tag::REPLACE => {
614 let r = Replace::parse(apdu)?;
615 Some(HostControlEvent::Replace {
616 replacement_ref: r.replacement_ref,
617 replaced_pid: r.replaced_pid,
618 replacement_pid: r.replacement_pid,
619 })
620 }
621 Some(t) if t == tag::CLEAR_REPLACE => {
622 let c = ClearReplace::parse(apdu)?;
623 Some(HostControlEvent::ClearReplace {
624 replacement_ref: c.replacement_ref,
625 })
626 }
627 Some(t) if t == tag::ASK_RELEASE => {
628 AskRelease::parse(apdu)?;
629 Some(HostControlEvent::AskRelease)
630 }
631 _ => None,
632 };
633 if let Some(event) = event {
634 out.notify.push(Notification::HostControl(event));
635 }
636 Ok(out)
637 }
638 }
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use dvb_ci::objects::resource_manager::Profile;
645 use mpeg_ts::pid::Pid;
646
647 fn s<S: Serialize<Error = dvb_ci::Error>>(v: &S) -> Vec<u8> {
650 ser(v).expect("test value must serialize")
651 }
652
653 #[test]
654 fn on_open_sends_profile_enq() {
655 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
656 let out = rm.on_open().unwrap();
657 assert_eq!(out.apdus, vec![s(&ProfileEnq::default())]);
658 }
659
660 #[test]
661 fn module_profile_triggers_profile_change_and_camready() {
662 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
668 rm.on_open().unwrap();
669 let empty_profile = s(&Profile {
670 length_form: dvb_ci::length::LengthForm::Minimal,
671 resources: vec![],
672 });
673 let o = rm.on_apdu(&empty_profile).unwrap();
674 assert!(o.notify.contains(&Notification::CamReady));
675 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
676 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
677 assert!(o.open.is_empty(), "host opens no sessions itself");
678 }
679
680 #[test]
681 fn answers_a_module_profile_enquiry_without_re_readying() {
682 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
683 rm.on_open().unwrap();
684 rm.on_apdu(&s(&Profile {
685 length_form: dvb_ci::length::LengthForm::Minimal,
686 resources: vec![APPLICATION_INFORMATION],
687 }))
688 .unwrap();
689 let o = rm.on_apdu(&s(&ProfileEnq::default())).unwrap();
691 assert_eq!(o.apdus.len(), 1);
692 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
693 assert!(!o.notify.contains(&Notification::CamReady));
694 }
695
696 #[test]
702 fn resource_manager_reset_clears_latched_handshake_state() {
703 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
704 rm.on_open().unwrap();
705 let o = rm
706 .on_apdu(&s(&Profile {
707 length_form: dvb_ci::length::LengthForm::Minimal,
708 resources: vec![APPLICATION_INFORMATION],
709 }))
710 .unwrap();
711 assert!(o.notify.contains(&Notification::CamReady), "dirtied: ready");
712 assert!(
713 !rm.module_resources().is_empty(),
714 "dirtied: module_resources"
715 );
716
717 rm.reset();
718 assert!(
719 rm.module_resources().is_empty(),
720 "reset must clear module_resources"
721 );
722
723 rm.on_open().unwrap();
726 let o2 = rm
727 .on_apdu(&s(&Profile {
728 length_form: dvb_ci::length::LengthForm::Minimal,
729 resources: vec![],
730 }))
731 .unwrap();
732 assert!(
733 o2.notify.contains(&Notification::CamReady),
734 "a fresh handshake after reset() must fire CamReady again"
735 );
736 }
737
738 #[test]
739 fn mmi_surfaces_enquiry_and_close() {
740 let mut h = Mmi::default();
741 let enq = s(&Enq {
743 reserved: Default::default(),
744 length_form: dvb_ci::length::LengthForm::Minimal,
745 blind_answer: true,
746 answer_text_length: 4,
747 text_chars: b"PIN?",
748 });
749 assert_eq!(
750 h.on_apdu(&enq).unwrap().notify,
751 vec![Notification::Mmi(MmiEvent::Enquiry {
752 prompt: "PIN?".to_string(),
753 blind: true,
754 answer_len: 4,
755 })]
756 );
757 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
759 assert_eq!(
760 h.on_apdu(&close).unwrap().notify,
761 vec![Notification::Mmi(MmiEvent::Close)]
762 );
763 }
764
765 #[test]
766 fn mmi_surfaces_structured_menu_and_list() {
767 use dvb_ci::objects::mmi_high::{List, Menu, Text};
768 let txt = |s: &'static [u8]| Text {
769 length_form: dvb_ci::length::LengthForm::Minimal,
770 more: false,
771 text_chars: s,
772 };
773 let mut h = Mmi::default();
774 let menu = s(&Menu {
776 length_form: dvb_ci::length::LengthForm::Minimal,
777 more: false,
778 choice_nb: 2,
779 title: txt(b"AlphaCrypt"),
780 subtitle: txt(b"Module Mainmenu"),
781 bottom: txt(b"Select item and press OK"),
782 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
783 });
784 assert_eq!(
785 h.on_apdu(&menu).unwrap().notify,
786 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
787 title: "AlphaCrypt".to_string(),
788 subtitle: "Module Mainmenu".to_string(),
789 bottom: "Select item and press OK".to_string(),
790 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
791 }))]
792 );
793 let list = s(&List(Menu {
795 length_form: dvb_ci::length::LengthForm::Minimal,
796 more: false,
797 choice_nb: 0xFF,
798 title: txt(b"Entitlements"),
799 subtitle: txt(b""),
800 bottom: txt(b""),
801 choices: vec![txt(b"ORF AUT")],
802 }));
803 assert_eq!(
804 h.on_apdu(&list).unwrap().notify,
805 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
806 title: "Entitlements".to_string(),
807 subtitle: String::new(),
808 bottom: String::new(),
809 choices: vec!["ORF AUT".to_string()],
810 }))]
811 );
812 }
813
814 fn chained(more: ApduTag, last: ApduTag, whole_last_apdu: &[u8], split: usize) -> [Vec<u8>; 2] {
815 let (len, hdr) = dvb_ci::length::decode(&whole_last_apdu[3..]).unwrap();
816 let body = &whole_last_apdu[3 + hdr..3 + hdr + len];
817 let part = |t: ApduTag, b: &[u8]| {
818 let mut v = t.to_bytes().to_vec();
819 let mut l = [0u8; 3];
820 let n = dvb_ci::length::encode_into(b.len(), &mut l).unwrap();
821 v.extend_from_slice(&l[..n]);
822 v.extend_from_slice(b);
823 v
824 };
825 [part(more, &body[..split]), part(last, &body[split..])]
826 }
827
828 #[test]
829 fn mmi_reassembles_a_menu_chained_over_menu_more_and_menu_last() {
830 use dvb_ci::objects::mmi_high::{Menu, Text};
831 let txt = |s: &'static [u8]| Text {
832 length_form: dvb_ci::length::LengthForm::Minimal,
833 more: false,
834 text_chars: s,
835 };
836 let whole = s(&Menu {
837 length_form: dvb_ci::length::LengthForm::Minimal,
838 more: false,
839 choice_nb: 2,
840 title: txt(b"AlphaCrypt"),
841 subtitle: txt(b"Module Mainmenu"),
842 bottom: txt(b"Select item and press OK"),
843 choices: vec![txt(b"Smartcard"), txt(b"Quit")],
844 });
845 let [first, second] = chained(tag::MENU_MORE, tag::MENU_LAST, &whole, 20);
846 let mut h = Mmi::default();
847 assert!(h.on_apdu(&first).unwrap().notify.is_empty());
848 assert_eq!(
849 h.on_apdu(&second).unwrap().notify,
850 vec![Notification::Mmi(MmiEvent::Menu(MmiMenu {
851 title: "AlphaCrypt".to_string(),
852 subtitle: "Module Mainmenu".to_string(),
853 bottom: "Select item and press OK".to_string(),
854 choices: vec!["Smartcard".to_string(), "Quit".to_string()],
855 }))]
856 );
857 }
858
859 #[test]
860 fn mmi_reassembles_a_list_chained_over_list_more_and_list_last() {
861 use dvb_ci::objects::mmi_high::{List, Menu, Text};
862 let txt = |s: &'static [u8]| Text {
863 length_form: dvb_ci::length::LengthForm::Minimal,
864 more: false,
865 text_chars: s,
866 };
867 let whole = s(&List(Menu {
868 length_form: dvb_ci::length::LengthForm::Minimal,
869 more: false,
870 choice_nb: 2,
871 title: txt(b"Entitlements"),
872 subtitle: txt(b""),
873 bottom: txt(b""),
874 choices: vec![txt(b"ORF AUT"), txt(b"ZDF")],
875 }));
876 let [first, second] = chained(tag::LIST_MORE, tag::LIST_LAST, &whole, 9);
877 let mut h = Mmi::default();
878 assert!(h.on_apdu(&first).unwrap().notify.is_empty());
879 assert_eq!(
880 h.on_apdu(&second).unwrap().notify,
881 vec![Notification::Mmi(MmiEvent::List(MmiMenu {
882 title: "Entitlements".to_string(),
883 subtitle: String::new(),
884 bottom: String::new(),
885 choices: vec!["ORF AUT".to_string(), "ZDF".to_string()],
886 }))]
887 );
888 }
889
890 #[test]
891 fn mmi_discards_a_partial_chain_on_reset() {
892 let mut h = Mmi::default();
893 let first = [0x9F, 0x88, 0x0A, 0x01, 0x02];
894 assert!(h.on_apdu(&first).unwrap().notify.is_empty());
895 h.reset();
896 assert!(h.chain.is_none());
897 }
898
899 #[test]
900 fn mmi_answers_display_control_set_mmi_mode() {
901 use dvb_ci::objects::mmi_display::{DisplayControl, DisplayControlCmd, MmiMode};
902 let mut h = Mmi::default();
903 let dc = s(&DisplayControl {
904 length_form: dvb_ci::length::LengthForm::Minimal,
905 cmd: DisplayControlCmd::SetMmiMode,
906 mmi_mode: Some(MmiMode::HighLevel),
907 });
908 let out = h.on_apdu(&dc).unwrap();
909 assert_eq!(out.apdus, vec![vec![0x9F, 0x88, 0x02, 0x02, 0x01, 0x01]]);
911 assert!(out.notify.is_empty());
912 }
913
914 #[test]
915 fn profile_change_re_enquires() {
916 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
917 let out = rm
918 .on_apdu(&s(
919 &dvb_ci::objects::resource_manager::ProfileChange::default(),
920 ))
921 .unwrap();
922 assert_eq!(out.apdus, vec![s(&ProfileEnq::default())]);
923 }
924
925 #[test]
926 fn application_information_surfaces_notification() {
927 use dvb_ci::objects::application_info::ApplicationType;
928 let mut h = ApplicationInformation;
929 assert_eq!(
930 h.on_open().unwrap().apdus,
931 vec![s(&ApplicationInfoEnq::default())]
932 );
933 let ai = s(&ApplicationInfo {
934 length_form: dvb_ci::length::LengthForm::Minimal,
935 application_type: ApplicationType::ConditionalAccess,
936 application_manufacturer: 0x1234,
937 manufacturer_code: 0x5678,
938 menu_string: b"Acme CAM",
939 });
940 let out = h.on_apdu(&ai).unwrap();
941 assert_eq!(
942 out.notify,
943 vec![Notification::ApplicationInfo {
944 application_type: 0x01,
945 manufacturer: 0x1234,
946 code: 0x5678,
947 menu: "Acme CAM".to_string(),
948 }]
949 );
950 }
951
952 #[test]
953 fn host_control_surfaces_tune_replace_clear_and_ask_release() {
954 let mut h = HostControl;
955 assert_eq!(h.id(), HOST_CONTROL);
956
957 let tune = s(&Tune {
959 length_form: dvb_ci::length::LengthForm::Minimal,
960 network_id: 0x1122,
961 original_network_id: 0x3344,
962 transport_stream_id: 0x5566,
963 service_id: 0x7788,
964 });
965 assert_eq!(
966 h.on_apdu(&tune).unwrap().notify,
967 vec![Notification::HostControl(HostControlEvent::Tune {
968 network_id: 0x1122,
969 original_network_id: 0x3344,
970 transport_stream_id: 0x5566,
971 service_id: 0x7788,
972 })]
973 );
974
975 let replace = s(&Replace {
977 length_form: dvb_ci::length::LengthForm::Minimal,
978 replacement_ref: 0x07,
979 replaced_pid: Pid::new(0x0123),
980 replacement_pid: Pid::new(0x01FF),
981 replaced_reserved: Default::default(),
982 replacement_reserved: Default::default(),
983 });
984 assert_eq!(
985 h.on_apdu(&replace).unwrap().notify,
986 vec![Notification::HostControl(HostControlEvent::Replace {
987 replacement_ref: 0x07,
988 replaced_pid: Pid::new(0x0123),
989 replacement_pid: Pid::new(0x01FF),
990 })]
991 );
992
993 let clear = s(&ClearReplace {
995 length_form: dvb_ci::length::LengthForm::Minimal,
996 replacement_ref: 0x42,
997 });
998 assert_eq!(
999 h.on_apdu(&clear).unwrap().notify,
1000 vec![Notification::HostControl(HostControlEvent::ClearReplace {
1001 replacement_ref: 0x42,
1002 })]
1003 );
1004
1005 let ask = s(&AskRelease::default());
1007 assert_eq!(
1008 h.on_apdu(&ask).unwrap().notify,
1009 vec![Notification::HostControl(HostControlEvent::AskRelease)]
1010 );
1011
1012 assert!(h.on_apdu(&tune).unwrap().apdus.is_empty());
1014 }
1015
1016 #[test]
1017 fn mjd_bcd_encoding_is_correct() {
1018 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
1020 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
1022 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
1023 }
1024
1025 #[test]
1026 fn date_time_replies_to_enq_and_resends_on_interval() {
1027 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
1028 let mut h = DateTime::with_clock(fixed);
1029 let enq = s(&DateTimeEnq {
1031 length_form: dvb_ci::length::LengthForm::Minimal,
1032 response_interval: 5,
1033 });
1034 let out = h.on_apdu(&enq).unwrap();
1035 assert_eq!(out.apdus.len(), 1);
1036 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
1037 assert!(h.tick(Duration::from_secs(3)).unwrap().apdus.is_empty());
1039 assert_eq!(h.tick(Duration::from_secs(3)).unwrap().apdus.len(), 1);
1041 }
1042
1043 #[test]
1044 fn date_time_interval_zero_does_not_resend() {
1045 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
1046 h.on_apdu(&s(&DateTimeEnq {
1047 length_form: dvb_ci::length::LengthForm::Minimal,
1048 response_interval: 0,
1049 }))
1050 .unwrap();
1051 assert!(h.tick(Duration::from_secs(60)).unwrap().apdus.is_empty());
1052 }
1053
1054 #[test]
1059 fn date_time_reset_clears_interval_and_resend_timer() {
1060 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
1061 h.on_apdu(&s(&DateTimeEnq {
1062 length_form: dvb_ci::length::LengthForm::Minimal,
1063 response_interval: 5,
1064 }))
1065 .unwrap();
1066 assert!(h.tick(Duration::from_secs(3)).unwrap().apdus.is_empty());
1068
1069 h.reset();
1070 assert!(
1074 h.tick(Duration::from_secs(60)).unwrap().apdus.is_empty(),
1075 "reset must clear the resend cadence from the previous module"
1076 );
1077 }
1078
1079 #[test]
1080 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
1081 let mut h = ConditionalAccess;
1082 assert_eq!(h.on_open().unwrap().apdus, vec![s(&CaInfoEnq::default())]);
1083 let ci = s(&CaInfo {
1085 length_form: dvb_ci::length::LengthForm::Minimal,
1086 ca_system_ids: vec![0x0B00, 0x1800],
1087 });
1088 assert_eq!(
1089 h.on_apdu(&ci).unwrap().notify,
1090 vec![Notification::CaInfo {
1091 ca_system_ids: vec![0x0B00, 0x1800],
1092 }]
1093 );
1094 let reply = s(&CaPmtReply {
1096 reserved_ca_enable: Default::default(),
1097 reserved_version: Default::default(),
1098 length_form: dvb_ci::length::LengthForm::Minimal,
1099 program_number: 0x0042,
1100 version_number: 0,
1101 current_next_indicator: true,
1102 ca_enable: Some(CaEnable::Possible),
1103 streams: vec![],
1104 });
1105 assert_eq!(
1106 h.on_apdu(&reply).unwrap().notify,
1107 vec![Notification::CaPmtReply {
1108 program_number: 0x0042,
1109 ca_enable: Some(CaEnable::Possible),
1110 descrambling_ok: true,
1111 }]
1112 );
1113 }
1114
1115 #[test]
1116 fn conditional_access_ca_pmt_reply_flag_clear_surfaces_none() {
1117 let mut h = ConditionalAccess;
1118 h.on_open().unwrap();
1119 let reply = s(&CaPmtReply {
1121 reserved_ca_enable: Default::default(),
1122 reserved_version: Default::default(),
1123 length_form: dvb_ci::length::LengthForm::Minimal,
1124 program_number: 0x0007,
1125 version_number: 0,
1126 current_next_indicator: true,
1127 ca_enable: None,
1128 streams: vec![],
1129 });
1130 assert_eq!(
1131 h.on_apdu(&reply).unwrap().notify,
1132 vec![Notification::CaPmtReply {
1133 program_number: 0x0007,
1134 ca_enable: None,
1135 descrambling_ok: false,
1136 }]
1137 );
1138 }
1139
1140 #[test]
1141 fn short_apdu_returns_err() {
1142 for len in 0..3usize {
1143 let buf = vec![0u8; len];
1144 assert!(Mmi::default().on_apdu(&buf).is_err(), "Mmi len {len}");
1145 assert!(
1146 ResourceManager::new(vec![]).on_apdu(&buf).is_err(),
1147 "RM len {len}"
1148 );
1149 assert!(
1150 ApplicationInformation.on_apdu(&buf).is_err(),
1151 "AI len {len}"
1152 );
1153 assert!(ConditionalAccess.on_apdu(&buf).is_err(), "CA len {len}");
1154 assert!(DateTime::default().on_apdu(&buf).is_err(), "DT len {len}");
1155 assert!(HostControl.on_apdu(&buf).is_err(), "HC len {len}");
1156 }
1157 }
1158}