1use std::collections::BTreeSet;
7use std::io;
8use std::time::Duration;
9
10use broadcast_common::Serialize;
11use dvb_ci::builder::build_ca_pmt;
12use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
13use dvb_si::tables::cat::CatSection;
14use dvb_si::tables::pmt::PmtSection;
15
16use crate::device::{CaDevice, SlotInfo};
17use crate::event::{Action, Event, HostRequest, HotPlug, MmiEvent, Notification};
18use crate::managed::{self, CaError, ManagedCa};
19use crate::stack::CiStack;
20
21const MMI_CARD_ABSENT_KEYWORDS: &[&str] = &[
26 "no card",
27 "insert card",
28 "insert smart card",
29 "card removed",
30 "please insert",
31];
32
33const MMI_CARD_PRESENT_KEYWORDS: &[&str] = &["entitlement", "card valid", "subscription active"];
38
39pub struct Driver<D: CaDevice> {
41 device: D,
42 stack: CiStack,
43 notifications: Vec<Notification>,
44 next_timer: Option<Duration>,
46 buf: Vec<u8>,
48 last_slot: Option<SlotInfo>,
55 last_caids: Option<BTreeSet<u16>>,
58 last_descrambling_ok: Option<bool>,
61 managed: ManagedCa,
64}
65
66impl<D: CaDevice> Driver<D> {
67 #[must_use]
69 pub fn new(device: D) -> Self {
70 Self {
71 device,
72 stack: CiStack::new(),
73 notifications: Vec::new(),
74 next_timer: None,
75 buf: vec![0u8; 4096],
76 last_slot: None,
77 last_caids: None,
78 last_descrambling_ok: None,
79 managed: ManagedCa::new(),
80 }
81 }
82
83 pub fn managed_ca(&self) -> &ManagedCa {
86 &self.managed
87 }
88
89 pub fn device(&self) -> &D {
91 &self.device
92 }
93
94 pub fn device_mut(&mut self) -> &mut D {
97 &mut self.device
98 }
99
100 pub fn next_timer(&self) -> Option<Duration> {
102 self.next_timer
103 }
104
105 pub fn take_notifications(&mut self) -> Vec<Notification> {
107 core::mem::take(&mut self.notifications)
108 }
109
110 pub fn init(&mut self) -> io::Result<()> {
112 let actions = self.stack.handle(Event::Host(HostRequest::Init));
113 self.run(actions)
114 }
115
116 pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
119 let actions = self
120 .stack
121 .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
122 self.run(actions)
123 }
124
125 pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
131 let actions = self
132 .stack
133 .handle(Event::Host(HostRequest::Descramble(pmt_section)));
134 self.run(actions)
135 }
136
137 pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
140 let actions = self
141 .stack
142 .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
143 self.run(actions)
144 }
145
146 pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
149 let actions = self
150 .stack
151 .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
152 self.run(actions)
153 }
154
155 pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
158 let actions = self
159 .stack
160 .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
161 self.run(actions)
162 }
163
164 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
184 if !managed::pmt_has_ca(pmt) {
185 return Err(CaError::NoCaDescriptor {
186 program_number: pmt.program_number,
187 });
188 }
189 let list_management = if self.managed.is_empty() {
190 CaPmtListManagement::Only
191 } else {
192 CaPmtListManagement::Add
193 };
194 let cmd_id = CaPmtCmdId::OkDescrambling;
195 let built = build_ca_pmt(pmt, list_management, cmd_id);
196 let built_bytes = built.to_bytes();
197 let mut pmt_raw = vec![0u8; pmt.serialized_len()];
205 let n = pmt
206 .serialize_into(&mut pmt_raw)
207 .expect("PmtSection::serialize_into on a freshly-sized buffer cannot fail");
208 pmt_raw.truncate(n);
209 self.send_ca_pmt(&built_bytes)?;
210 self.managed.record(
211 pmt.program_number,
212 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
213 );
214 Ok(())
215 }
216
217 pub fn remove_service(&mut self, program_number: u16) -> Result<(), CaError> {
231 let raw = self
232 .managed
233 .services()
234 .get(&program_number)
235 .map(|s| s.pmt_raw.clone());
236 let Some(raw) = raw else {
237 return Ok(());
238 };
239 self.remove_program(&raw)?;
240 self.managed.remove(program_number);
241 Ok(())
242 }
243
244 pub fn set_requery_interval(&mut self, interval: Duration) {
254 self.managed.set_requery_interval(interval);
255 }
256
257 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
272 let entries = cat.ca_descriptors().map_err(CaError::Cat)?;
273 self.managed.set_cat(&entries);
274 Ok(())
275 }
276
277 #[must_use]
281 pub fn emm_pids(&self) -> &[u16] {
282 self.managed.emm_pids()
283 }
284
285 #[must_use]
288 pub fn descramble_pids(&self) -> &[u16] {
289 self.managed.descramble_pids()
290 }
291
292 #[must_use]
297 pub fn ca_pids(&self) -> &[u16] {
298 self.managed.ca_pids()
299 }
300
301 #[must_use]
309 pub fn required_pids(&self) -> Vec<u16> {
310 self.managed.required_pids()
311 }
312
313 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
315 let actions = self
316 .stack
317 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
318 self.run(actions)
319 }
320
321 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
323 let actions = self
324 .stack
325 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
326 self.run(actions)
327 }
328
329 pub fn mmi_cancel(&mut self) -> io::Result<()> {
331 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
332 self.run(actions)
333 }
334
335 pub fn enter_menu(&mut self) -> io::Result<()> {
338 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
339 self.run(actions)
340 }
341
342 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
351 self.run(vec![Action::QuerySlot])?;
352 if self.device.poll(timeout)? {
353 let n = self.device.read(&mut self.buf)?;
354 if n > 0 {
355 let frame = self.buf[..n].to_vec();
356 let actions = self.stack.handle(Event::Readable(&frame));
357 self.run(actions)?;
358 return Ok(true);
359 }
360 }
361 let actions = self.stack.handle(Event::Tick { elapsed: timeout });
362 self.run(actions)?;
363 self.requery_tick(timeout)?;
364 Ok(false)
365 }
366
367 fn requery_tick(&mut self, elapsed: Duration) -> io::Result<()> {
387 use broadcast_common::Parse;
388
389 if !self.managed.tick(elapsed) {
390 return Ok(());
391 }
392 let n = self.managed.services().len();
393 let ca_pmts: Vec<Vec<u8>> = self
394 .managed
395 .services()
396 .values()
397 .enumerate()
398 .map(|(i, s)| {
399 let list_management = if n == 1 {
400 CaPmtListManagement::Only
401 } else if i == 0 {
402 CaPmtListManagement::First
403 } else if i == n - 1 {
404 CaPmtListManagement::Last
405 } else {
406 CaPmtListManagement::More
407 };
408 let pmt = PmtSection::parse(&s.pmt_raw)
409 .expect("pmt_raw was produced by PmtSection::serialize_into at add_service time and must re-parse");
410 build_ca_pmt(&pmt, list_management, CaPmtCmdId::Query).to_bytes()
411 })
412 .collect();
413 for ca_pmt in ca_pmts {
414 self.send_ca_pmt(&ca_pmt)?;
415 }
416 Ok(())
417 }
418
419 pub fn pump_with<F: FnMut(&Notification)>(
428 &mut self,
429 timeout: Duration,
430 mut handler: F,
431 ) -> io::Result<bool> {
432 let progressed = self.pump(timeout)?;
433 for n in self.take_notifications() {
434 handler(&n);
435 }
436 Ok(progressed)
437 }
438
439 pub fn pump_hotplug<F: FnMut(HotPlug)>(
443 &mut self,
444 timeout: Duration,
445 mut handler: F,
446 ) -> io::Result<bool> {
447 self.pump_with(timeout, |n| {
448 if let Some(h) = n.hotplug() {
449 handler(h);
450 }
451 })
452 }
453
454 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
456 for action in actions {
457 match action {
458 Action::Write(bytes) => self.device.write(&bytes)?,
459 Action::Reset => self.device.reset()?,
460 Action::QuerySlot => {
461 let info = self.device.slot_info()?;
462 self.handle_slot_info(info)?;
463 }
464 Action::SetTimer { after } => self.next_timer = Some(after),
465 Action::Notify(n) => {
466 let inferred = self.infer_card(&n);
467 self.notifications.push(n);
468 self.notifications.extend(inferred);
469 }
470 }
471 }
472 Ok(())
473 }
474
475 fn handle_slot_info(&mut self, info: SlotInfo) -> io::Result<()> {
482 let prev = self.last_slot.replace(info);
483 match prev {
484 Some(prev) if !prev.module_present && info.module_present => {
485 self.notifications
486 .push(Notification::HotPlug(HotPlug::CamPresent));
487 self.reset_module_state();
488 let actions = self.stack.handle(Event::Host(HostRequest::Init));
492 self.run(actions)?;
493 }
494 Some(prev) if prev.module_present && !info.module_present => {
495 self.notifications
496 .push(Notification::HotPlug(HotPlug::CamRemoved));
497 self.reset_module_state();
498 }
499 _ => {}
500 }
501 Ok(())
502 }
503
504 fn reset_module_state(&mut self) {
514 self.stack = CiStack::new();
515 self.next_timer = None;
516 self.last_caids = None;
517 self.last_descrambling_ok = None;
518 self.managed.clear();
519 }
520
521 fn infer_card(&mut self, note: &Notification) -> Vec<Notification> {
528 match note {
529 Notification::CaInfo { ca_system_ids } => {
530 let new_set: BTreeSet<u16> = ca_system_ids.iter().copied().collect();
531 let mut out = Vec::new();
532 if let Some(prev) = &self.last_caids {
533 if prev.is_empty() && !new_set.is_empty() {
534 out.push(Notification::HotPlug(HotPlug::CardInserted));
535 } else if !prev.is_empty() && new_set.is_empty() {
536 out.push(Notification::HotPlug(HotPlug::CardRemoved));
537 } else if !prev.is_empty() && !new_set.is_empty() && *prev != new_set {
538 out.push(Notification::HotPlug(HotPlug::CardChanged));
539 }
540 }
541 self.managed.set_cam_caids(new_set.clone());
545 self.last_caids = Some(new_set);
546 out
547 }
548 Notification::CaPmtReply {
549 program_number,
550 ca_enable,
551 descrambling_ok,
552 } => {
553 let mut out = Vec::new();
554 if let Some(prev) = self.last_descrambling_ok {
555 if !prev && *descrambling_ok {
556 out.push(Notification::HotPlug(HotPlug::CardInserted));
557 } else if prev && !*descrambling_ok {
558 out.push(Notification::HotPlug(HotPlug::CardRemoved));
559 }
560 }
561 self.last_descrambling_ok = Some(*descrambling_ok);
562 if let Some((v, ok)) =
566 self.managed
567 .record_reply(*program_number, *ca_enable, *descrambling_ok)
568 {
569 out.push(Notification::Entitlement {
570 program_number: *program_number,
571 ca_enable: v,
572 descrambling_ok: ok,
573 });
574 }
575 out
576 }
577 Notification::Mmi(ev) => match Self::mmi_text(ev) {
578 Some(text) => {
579 let lower = text.to_lowercase();
580 if MMI_CARD_ABSENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
581 vec![Notification::HotPlug(HotPlug::CardRemoved)]
582 } else if MMI_CARD_PRESENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
583 vec![Notification::HotPlug(HotPlug::CardInserted)]
584 } else {
585 Vec::new()
586 }
587 }
588 None => Vec::new(),
589 },
590 _ => Vec::new(),
591 }
592 }
593
594 fn mmi_text(ev: &MmiEvent) -> Option<String> {
598 match ev {
599 MmiEvent::Menu(m) | MmiEvent::List(m) => {
600 let mut s = format!("{} {} {}", m.title, m.subtitle, m.bottom);
601 for choice in &m.choices {
602 s.push(' ');
603 s.push_str(choice);
604 }
605 Some(s)
606 }
607 MmiEvent::Enquiry { prompt, .. } => Some(prompt.clone()),
608 MmiEvent::Close => None,
609 }
610 }
611}
612
613#[cfg(test)]
614pub(crate) mod tests {
615 use super::*;
616 use crate::device::{DeviceOp, MockCaDevice};
617 use crate::event::{HostControlEvent, HotPlug, Notification};
618 use broadcast_common::Serialize;
619 use dvb_ci::tpdu::tags;
620
621 pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
622 let mut b = vec![0u8; s.serialized_len()];
623 match s.serialize_into(&mut b) {
624 Ok(n) => b.truncate(n),
625 Err(_) => b.clear(),
626 }
627 b
628 }
629
630 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
633 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
634 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
635 v.extend_from_slice(spdu);
636 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
637 v
638 }
639
640 pub(crate) fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
643 use dvb_ci::spdu::SessionNumber;
644 let mut spdu = ser(&SessionNumber { session_nb });
645 spdu.extend_from_slice(apdu);
646 r_data(1, &spdu)
647 }
648
649 pub(crate) fn sb() -> Vec<u8> {
652 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
653 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
654 }
655
656 pub(crate) fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
659 d.device_mut().inbound.push_back(frame);
660 d.pump(Duration::from_millis(10)).unwrap();
661 for _ in 0..8 {
662 d.device_mut().inbound.push_back(sb());
663 d.pump(Duration::from_millis(10)).unwrap();
664 }
665 }
666
667 pub(crate) fn driver_with_sessions() -> Driver<MockCaDevice> {
671 use dvb_ci::objects::resource_manager::Profile;
672 use dvb_ci::resource::{
673 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
674 RESOURCE_MANAGER,
675 };
676 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
677
678 let mut d = Driver::new(MockCaDevice::new([]));
679 d.init().unwrap();
680 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
682 feed(
684 &mut d,
685 r_data(
686 1,
687 &ser(&OpenSessionRequest {
688 resource: RESOURCE_MANAGER,
689 }),
690 ),
691 );
692 feed(
695 &mut d,
696 r_apdu(
697 1,
698 &ser(&Profile {
699 resources: vec![
700 APPLICATION_INFORMATION,
701 CONDITIONAL_ACCESS_SUPPORT,
702 MMI,
703 HOST_CONTROL,
704 ],
705 }),
706 ),
707 );
708 for (nb, res) in [
710 (2u16, APPLICATION_INFORMATION),
711 (3, CONDITIONAL_ACCESS_SUPPORT),
712 (4, MMI),
713 (5, HOST_CONTROL),
714 ] {
715 feed(
716 &mut d,
717 r_data(
718 1,
719 &ser(&CreateSessionResponse {
720 status: SessionStatus::Ok,
721 resource: res,
722 session_nb: nb,
723 }),
724 ),
725 );
726 }
727 d
728 }
729
730 const RM_SESSION: u16 = 1;
734 pub(crate) const CA_SESSION: u16 = 3;
735 const MMI_SESSION: u16 = 4;
736 const HOST_CONTROL_SESSION: u16 = 5;
737
738 #[test]
739 fn host_control_tune_apdu_surfaces_notification_via_driver() {
740 use dvb_ci::objects::host_control::Tune;
741
742 let mut d = driver_with_sessions();
743 let hc_nb = HOST_CONTROL_SESSION;
744 d.take_notifications(); let tune = Tune {
748 network_id: 0x1122,
749 original_network_id: 0x3344,
750 transport_stream_id: 0x5566,
751 service_id: 0x7788,
752 };
753 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
754
755 let notes = d.take_notifications();
757 assert!(
758 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
759 network_id: 0x1122,
760 original_network_id: 0x3344,
761 transport_stream_id: 0x5566,
762 service_id: 0x7788,
763 })),
764 "expected HostControl(Tune) notification, got {notes:?}"
765 );
766 }
767
768 #[test]
769 fn profile_reply_advertises_host_control() {
770 use broadcast_common::Parse;
771 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
772 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
773
774 let mut d = Driver::new(MockCaDevice::new([]));
775 d.init().unwrap();
776 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
777 feed(
779 &mut d,
780 r_data(
781 1,
782 &ser(&dvb_ci::spdu::OpenSessionRequest {
783 resource: RESOURCE_MANAGER,
784 }),
785 ),
786 );
787 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq)));
789
790 let want = dvb_ci::tag::PROFILE.to_bytes();
793 let found = d.device().ops.iter().any(|op| {
794 if let DeviceOp::Write(w) = op {
795 if let Some(pos) = w.windows(3).position(|x| x == want) {
796 if let Ok(p) = Profile::parse(&w[pos..]) {
797 return p.resources.contains(&HOST_CONTROL);
798 }
799 }
800 }
801 false
802 });
803 assert!(found, "profile reply must advertise HOST_CONTROL");
804 }
805
806 #[test]
807 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
808 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
809
810 let mut d = driver_with_sessions();
811 let mmi_nb = MMI_SESSION;
812
813 d.mmi_menu_answer(2).unwrap();
816 d.device_mut().inbound.push_back(sb());
817 d.pump(Duration::from_millis(10)).unwrap();
818 assert_apdu_on_session(&d, mmi_nb, &ser(&MenuAnsw { choice_ref: 2 }));
819
820 d.mmi_enquiry_answer(b"1234").unwrap();
822 d.device_mut().inbound.push_back(sb());
823 d.pump(Duration::from_millis(10)).unwrap();
824 assert_apdu_on_session(
825 &d,
826 mmi_nb,
827 &ser(&Answ {
828 answ_id: AnswId::Answer,
829 text_chars: b"1234",
830 }),
831 );
832 }
833
834 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
837 use dvb_ci::spdu::SessionNumber;
838 let mut want = ser(&SessionNumber { session_nb });
839 want.extend_from_slice(apdu);
840 let hit = d.device().ops.iter().any(|op| match op {
841 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
842 _ => false,
843 });
844 assert!(
845 hit,
846 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
847 );
848 }
849
850 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
854 use dvb_ci::spdu::SessionNumber;
855 let mut want = ser(&SessionNumber { session_nb });
856 want.extend_from_slice(apdu);
857 d.device()
858 .ops
859 .iter()
860 .filter(|op| match op {
861 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
862 _ => false,
863 })
864 .count()
865 }
866
867 #[test]
868 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
869 let mut d = Driver::new(MockCaDevice::new([]));
870 d.init().unwrap();
871 let ops = &d.device().ops;
872 assert_eq!(ops[0], DeviceOp::Reset);
873 assert_eq!(ops[1], DeviceOp::SlotInfo);
874 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
875 }
876
877 #[test]
878 fn reads_reply_then_polls_on_pump() {
879 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
881 let mut d = Driver::new(dev);
882 d.init().unwrap();
883 assert!(d.pump(Duration::from_millis(100)).unwrap());
885 assert!(!d.pump(Duration::from_millis(100)).unwrap());
887 let last = d.device().ops.last().unwrap();
888 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
889 }
890
891 #[test]
894 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
895 let mut dev = MockCaDevice::new([]);
896 dev.slot = SlotInfo {
897 num: 0,
898 module_ready: false,
899 module_present: false,
900 };
901 let mut d = Driver::new(dev);
902 d.init().unwrap();
903 let notes = d.take_notifications();
906 assert!(
907 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
908 "baseline observation must not fire CamPresent, got {notes:?}"
909 );
910 let resets_before = d
911 .device()
912 .ops
913 .iter()
914 .filter(|o| **o == DeviceOp::Reset)
915 .count();
916
917 d.device_mut().slot = SlotInfo {
919 num: 0,
920 module_ready: true,
921 module_present: true,
922 };
923 d.pump(Duration::from_millis(10)).unwrap();
924
925 let notes = d.take_notifications();
926 let cam_present_count = notes
927 .iter()
928 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
929 .count();
930 assert_eq!(
931 cam_present_count, 1,
932 "expected exactly one CamPresent, got {notes:?}"
933 );
934 let resets_after = d
936 .device()
937 .ops
938 .iter()
939 .filter(|o| **o == DeviceOp::Reset)
940 .count();
941 assert_eq!(
942 resets_after,
943 resets_before + 1,
944 "expected one fresh Reset on re-insert"
945 );
946 assert!(
947 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
948 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
949 d.device().ops.last()
950 );
951 }
952
953 #[test]
954 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
955 let mut d = driver_with_sessions();
956 d.take_notifications();
957
958 d.device_mut().slot.module_present = false;
960 d.pump(Duration::from_millis(10)).unwrap();
961 let notes = d.take_notifications();
962 assert!(
963 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
964 "expected CamRemoved, got {notes:?}"
965 );
966
967 d.mmi_menu_answer(0).unwrap();
971 let notes = d.take_notifications();
972 assert!(
973 notes
974 .iter()
975 .any(|n| matches!(n, Notification::Error { .. })),
976 "expected no open MMI session after teardown, got {notes:?}"
977 );
978
979 let resets_before = d
981 .device()
982 .ops
983 .iter()
984 .filter(|o| **o == DeviceOp::Reset)
985 .count();
986 d.device_mut().slot.module_present = true;
987 d.device_mut().slot.module_ready = true;
988 d.pump(Duration::from_millis(10)).unwrap();
989 let notes = d.take_notifications();
990 assert!(
991 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
992 "expected CamPresent on re-insert, got {notes:?}"
993 );
994 let resets_after = d
995 .device()
996 .ops
997 .iter()
998 .filter(|o| **o == DeviceOp::Reset)
999 .count();
1000 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1001 }
1002
1003 #[test]
1004 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1005 let mut d = Driver::new(MockCaDevice::new([]));
1006 d.init().unwrap();
1007 d.take_notifications();
1008
1009 for _ in 0..5 {
1010 d.pump(Duration::from_millis(10)).unwrap();
1011 }
1012 let notes = d.take_notifications();
1013 assert!(
1014 !notes.iter().any(|n| matches!(
1015 n,
1016 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1017 )),
1018 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1019 );
1020 }
1021
1022 #[test]
1023 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1024 use dvb_ci::objects::ca_info::CaInfo;
1025
1026 let mut d = driver_with_sessions();
1027 d.take_notifications();
1028
1029 feed(
1031 &mut d,
1032 r_apdu(
1033 CA_SESSION,
1034 &ser(&CaInfo {
1035 ca_system_ids: vec![],
1036 }),
1037 ),
1038 );
1039 let notes = d.take_notifications();
1040 assert!(
1041 !notes.iter().any(|n| matches!(
1042 n,
1043 Notification::HotPlug(
1044 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1045 )
1046 )),
1047 "first ca_info must only establish the baseline, got {notes:?}"
1048 );
1049
1050 feed(
1052 &mut d,
1053 r_apdu(
1054 CA_SESSION,
1055 &ser(&CaInfo {
1056 ca_system_ids: vec![0x0B00],
1057 }),
1058 ),
1059 );
1060 let notes = d.take_notifications();
1061 assert!(
1062 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1063 "expected CardInserted, got {notes:?}"
1064 );
1065
1066 feed(
1068 &mut d,
1069 r_apdu(
1070 CA_SESSION,
1071 &ser(&CaInfo {
1072 ca_system_ids: vec![0x1800],
1073 }),
1074 ),
1075 );
1076 let notes = d.take_notifications();
1077 assert!(
1078 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1079 "expected CardChanged, got {notes:?}"
1080 );
1081 }
1082
1083 #[test]
1084 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1085 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1086
1087 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1088 CaPmtReply {
1089 program_number: 1,
1090 version_number: 1,
1091 current_next_indicator: true,
1092 ca_enable,
1093 streams: vec![],
1094 }
1095 }
1096
1097 let mut d = driver_with_sessions();
1098 d.take_notifications();
1099
1100 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1102 let notes = d.take_notifications();
1103 assert!(
1104 !notes.iter().any(|n| matches!(
1105 n,
1106 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1107 )),
1108 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1109 );
1110
1111 feed(
1113 &mut d,
1114 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1115 );
1116 let notes = d.take_notifications();
1117 assert!(
1118 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1119 "expected CardInserted, got {notes:?}"
1120 );
1121
1122 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1124 let notes = d.take_notifications();
1125 assert!(
1126 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1127 "expected CardRemoved, got {notes:?}"
1128 );
1129 }
1130
1131 #[test]
1132 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1133 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1134
1135 let mut d = driver_with_sessions();
1136 d.take_notifications();
1137
1138 feed(
1141 &mut d,
1142 r_apdu(
1143 CA_SESSION,
1144 &ser(&CaPmtReply {
1145 program_number: 7,
1146 version_number: 1,
1147 current_next_indicator: true,
1148 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1149 streams: vec![],
1150 }),
1151 ),
1152 );
1153 let notes = d.take_notifications();
1154 assert!(
1155 notes.contains(&Notification::CaPmtReply {
1156 program_number: 7,
1157 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1158 descrambling_ok: true,
1159 }),
1160 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1161 );
1162 }
1163
1164 #[test]
1165 fn ca_pmt_reply_flag_clear_surfaces_none() {
1166 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1167
1168 let mut d = driver_with_sessions();
1169 d.take_notifications();
1170
1171 feed(
1174 &mut d,
1175 r_apdu(
1176 CA_SESSION,
1177 &ser(&CaPmtReply {
1178 program_number: 7,
1179 version_number: 1,
1180 current_next_indicator: true,
1181 ca_enable: None,
1182 streams: vec![],
1183 }),
1184 ),
1185 );
1186 let notes = d.take_notifications();
1187 assert!(
1188 notes.contains(&Notification::CaPmtReply {
1189 program_number: 7,
1190 ca_enable: None,
1191 descrambling_ok: false,
1192 }),
1193 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1194 );
1195 }
1196
1197 #[test]
1198 fn mmi_no_card_text_infers_card_removed() {
1199 use dvb_ci::objects::mmi_high::Enq;
1200
1201 let mut d = driver_with_sessions();
1202 d.take_notifications();
1203
1204 feed(
1205 &mut d,
1206 r_apdu(
1207 MMI_SESSION,
1208 &ser(&Enq {
1209 blind_answer: false,
1210 answer_text_length: 0,
1211 text_chars: b"NO CARD detected - please insert your smart card",
1212 }),
1213 ),
1214 );
1215
1216 let notes = d.take_notifications();
1217 assert!(
1218 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1219 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1220 );
1221 }
1222
1223 #[test]
1224 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1225 let mut dev = MockCaDevice::new([]);
1226 dev.slot = SlotInfo {
1227 num: 0,
1228 module_ready: false,
1229 module_present: false,
1230 };
1231 let mut d = Driver::new(dev);
1232 d.init().unwrap();
1233 d.take_notifications(); d.device_mut().slot = SlotInfo {
1237 num: 0,
1238 module_ready: true,
1239 module_present: true,
1240 };
1241
1242 let mut seen = Vec::new();
1243 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1244 .unwrap();
1245
1246 assert_eq!(
1247 seen,
1248 vec![HotPlug::CamPresent],
1249 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1250 );
1251 }
1252
1253 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
1258 [
1259 0x09,
1260 0x04,
1261 (ca_system_id >> 8) as u8,
1262 ca_system_id as u8,
1263 0xE0 | ((pid >> 8) as u8 & 0x1F),
1264 pid as u8,
1265 ]
1266 }
1267
1268 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
1285 const VIACCESS: u16 = 0x0500;
1286 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1287 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1288
1289 let mut body = Vec::new();
1290 body.push(0x02); body.push(0); body.push(0);
1293 body.extend_from_slice(&program_number.to_be_bytes());
1294 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
1299 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1300 body.push(prog_ca.len() as u8);
1301 body.extend_from_slice(&prog_ca);
1302 body.push(0x1B);
1304 body.push(0xE0 | 0x01);
1305 body.push(0x00);
1306 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1307 body.push(es0_ca.len() as u8);
1308 body.extend_from_slice(&es0_ca);
1309 body.push(0x0F);
1311 body.push(0xE0 | 0x01);
1312 body.push(0x01);
1313 body.push(0xF0);
1314 body.push(0x00);
1315
1316 let section_length = body.len() - 3 + 4;
1317 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1318 body[2] = section_length as u8;
1319 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1320 body.extend_from_slice(&crc.to_be_bytes());
1321 body
1322 }
1323
1324 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
1331 const VIACCESS: u16 = 0x0500;
1332 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1333 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1334
1335 let mut body = Vec::new();
1336 body.push(0x02); body.push(0); body.push(0);
1339 body.extend_from_slice(&program_number.to_be_bytes());
1340 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0); body.push(0xFF); body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1346 body.push(prog_ca.len() as u8);
1347 body.extend_from_slice(&prog_ca);
1348 body.push(0x1B);
1350 body.push(0xE0 | 0x01);
1351 body.push(0x00);
1352 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1353 body.push(es0_ca.len() as u8);
1354 body.extend_from_slice(&es0_ca);
1355 body.push(0x0F);
1357 body.push(0xE0 | 0x01);
1358 body.push(0x01);
1359 body.push(0xF0);
1360 body.push(0x00);
1361
1362 let section_length = body.len() - 3 + 4;
1363 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1364 body[2] = section_length as u8;
1365 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1366 body.extend_from_slice(&crc.to_be_bytes());
1367 body
1368 }
1369
1370 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
1374 let mut body = Vec::new();
1375 body.push(0x02);
1376 body.push(0);
1377 body.push(0);
1378 body.extend_from_slice(&program_number.to_be_bytes());
1379 body.push(0xC3);
1380 body.push(0x00);
1381 body.push(0x00);
1382 body.push(0xE0 | 0x01);
1383 body.push(0x00);
1384 body.push(0xF0); body.push(0x00);
1386 body.push(0x1B);
1388 body.push(0xE0 | 0x01);
1389 body.push(0x00);
1390 body.push(0xF0);
1391 body.push(0x00);
1392
1393 let section_length = body.len() - 3 + 4;
1394 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1395 body[2] = section_length as u8;
1396 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1397 body.extend_from_slice(&crc.to_be_bytes());
1398 body
1399 }
1400
1401 #[test]
1402 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
1403 use broadcast_common::Parse;
1404
1405 let mut d = driver_with_sessions();
1406 d.take_notifications();
1407
1408 let pmt_bytes = build_ca_pmt_fixture(1546);
1409 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1410
1411 d.add_service(&pmt).unwrap();
1412 d.device_mut().inbound.push_back(sb());
1413 d.pump(Duration::from_millis(10)).unwrap();
1414
1415 let expected =
1419 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
1420 assert_apdu_on_session(&d, CA_SESSION, &expected);
1421
1422 let svc = d
1424 .managed_ca()
1425 .services()
1426 .get(&1546)
1427 .expect("program_number 1546 must be tracked after add_service");
1428 assert_eq!(svc.es_pids, vec![0x0100, 0x0101]);
1429 assert_eq!(svc.ca_pids, vec![0x0064, 0x0065]);
1430 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
1431 assert_eq!(svc.last_ca_enable, None);
1432 }
1433
1434 #[test]
1435 fn add_service_rejects_pmt_without_ca_descriptor() {
1436 use broadcast_common::Parse;
1437
1438 let mut d = driver_with_sessions();
1439 let pmt_bytes = build_clear_pmt_fixture(999);
1440 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1441
1442 let err = d.add_service(&pmt).unwrap_err();
1443 assert!(
1444 matches!(
1445 err,
1446 CaError::NoCaDescriptor {
1447 program_number: 999
1448 }
1449 ),
1450 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
1451 );
1452 assert!(
1453 d.managed_ca().services().is_empty(),
1454 "a rejected PMT must not be recorded"
1455 );
1456 }
1457
1458 #[test]
1459 fn add_service_second_call_uses_add_list_management() {
1460 use broadcast_common::Parse;
1461
1462 let mut d = driver_with_sessions();
1463 d.take_notifications();
1464
1465 let pmt1_bytes = build_ca_pmt_fixture(1546);
1466 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1467 d.add_service(&pmt1).unwrap();
1468 d.device_mut().inbound.push_back(sb());
1469 d.pump(Duration::from_millis(10)).unwrap();
1470
1471 let pmt2_bytes = build_ca_pmt_fixture(1547);
1472 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1473 d.add_service(&pmt2).unwrap();
1474 d.device_mut().inbound.push_back(sb());
1475 d.pump(Duration::from_millis(10)).unwrap();
1476
1477 let expected2 =
1479 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
1480 assert_apdu_on_session(&d, CA_SESSION, &expected2);
1481
1482 assert_eq!(d.managed_ca().services().len(), 2);
1483 }
1484
1485 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
1495 const EXTENSION_HEADER_LEN: u16 = 5;
1496 const CRC_LEN: u16 = 4;
1497 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
1498 let mut v = Vec::new();
1499 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
1501 v.push((section_length & 0xFF) as u8);
1502 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
1507 let crc = broadcast_common::crc32_mpeg2::compute(&v);
1508 v.extend_from_slice(&crc.to_be_bytes());
1509 v
1510 }
1511
1512 #[test]
1513 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
1514 use broadcast_common::Parse;
1515 use dvb_ci::objects::ca_info::CaInfo;
1516 use dvb_si::tables::cat::CatSection;
1517
1518 let mut d = driver_with_sessions();
1519 d.take_notifications();
1520
1521 feed(
1523 &mut d,
1524 r_apdu(
1525 CA_SESSION,
1526 &ser(&CaInfo {
1527 ca_system_ids: vec![0x0648, 0x0100],
1528 }),
1529 ),
1530 );
1531 d.take_notifications();
1532
1533 let mut descriptors = Vec::new();
1536 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
1537 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
1538 let cat_bytes = build_cat_fixture(&descriptors);
1539 let cat = CatSection::parse(&cat_bytes).unwrap();
1540
1541 d.set_cat(&cat).unwrap();
1542
1543 assert_eq!(
1544 d.emm_pids(),
1545 &[0x1FF0],
1546 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
1547 );
1548 }
1549
1550 #[test]
1551 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
1552 use broadcast_common::Parse;
1553 use dvb_ci::objects::ca_info::CaInfo;
1554 use dvb_si::tables::cat::CatSection;
1555
1556 let mut d = driver_with_sessions();
1557 d.take_notifications();
1558
1559 let mut descriptors = Vec::new();
1560 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
1561 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
1562 let cat_bytes = build_cat_fixture(&descriptors);
1563 let cat = CatSection::parse(&cat_bytes).unwrap();
1564
1565 d.set_cat(&cat).unwrap();
1568 assert!(
1569 d.emm_pids().is_empty(),
1570 "emm_pids must be empty before any ca_info arrives, got {:?}",
1571 d.emm_pids()
1572 );
1573
1574 feed(
1577 &mut d,
1578 r_apdu(
1579 CA_SESSION,
1580 &ser(&CaInfo {
1581 ca_system_ids: vec![0x0648, 0x0100],
1582 }),
1583 ),
1584 );
1585 d.take_notifications();
1586
1587 assert_eq!(
1588 d.emm_pids(),
1589 &[0x1FF0],
1590 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
1591 );
1592 }
1593
1594 #[test]
1600 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
1601 use broadcast_common::Parse;
1602 use dvb_ci::objects::ca_info::CaInfo;
1603 use dvb_si::tables::cat::CatSection;
1604
1605 let mut d = driver_with_sessions();
1606 d.take_notifications();
1607
1608 feed(
1610 &mut d,
1611 r_apdu(
1612 CA_SESSION,
1613 &ser(&CaInfo {
1614 ca_system_ids: vec![0x0648, 0x0100],
1615 }),
1616 ),
1617 );
1618 d.take_notifications();
1619
1620 let mut descriptors = Vec::new();
1622 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
1623 descriptors.extend_from_slice(&ca_descriptor(0x0100, 0x1FF0));
1624 let cat_bytes = build_cat_fixture(&descriptors);
1625 let cat = CatSection::parse(&cat_bytes).unwrap();
1626
1627 d.set_cat(&cat).unwrap();
1628
1629 assert_eq!(
1630 d.emm_pids(),
1631 &[0x1FF0],
1632 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
1633 d.emm_pids()
1634 );
1635 }
1636
1637 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
1641 const VIACCESS: u16 = 0x0500;
1642 let prog_ca = ca_descriptor(VIACCESS, 0x0074);
1643 let es0_ca = ca_descriptor(VIACCESS, 0x0075);
1644
1645 let mut body = Vec::new();
1646 body.push(0x02); body.push(0);
1648 body.push(0);
1649 body.extend_from_slice(&program_number.to_be_bytes());
1650 body.push(0xC3);
1651 body.push(0x00);
1652 body.push(0x00);
1653 body.push(0xE0 | 0x02); body.push(0x00);
1655 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1656 body.push(prog_ca.len() as u8);
1657 body.extend_from_slice(&prog_ca);
1658 body.push(0x1B);
1660 body.push(0xE0 | 0x02);
1661 body.push(0x00);
1662 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1663 body.push(es0_ca.len() as u8);
1664 body.extend_from_slice(&es0_ca);
1665 body.push(0x0F);
1667 body.push(0xE0 | 0x02);
1668 body.push(0x01);
1669 body.push(0xF0);
1670 body.push(0x00);
1671
1672 let section_length = body.len() - 3 + 4;
1673 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1674 body[2] = section_length as u8;
1675 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1676 body.extend_from_slice(&crc.to_be_bytes());
1677 body
1678 }
1679
1680 #[test]
1681 fn descramble_pids_is_the_union_of_active_services_es_pids() {
1682 use broadcast_common::Parse;
1683
1684 let mut d = driver_with_sessions();
1685 d.take_notifications();
1686
1687 assert!(
1688 d.descramble_pids().is_empty(),
1689 "no service added yet: descramble_pids must be empty"
1690 );
1691
1692 let pmt1_bytes = build_ca_pmt_fixture(1546);
1693 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1694 d.add_service(&pmt1).unwrap();
1695 d.device_mut().inbound.push_back(sb());
1696 d.pump(Duration::from_millis(10)).unwrap();
1697
1698 assert_eq!(d.descramble_pids(), &[0x0100, 0x0101]);
1699
1700 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
1701 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1702 d.add_service(&pmt2).unwrap();
1703 d.device_mut().inbound.push_back(sb());
1704 d.pump(Duration::from_millis(10)).unwrap();
1705
1706 assert_eq!(
1708 d.descramble_pids(),
1709 &[0x0100, 0x0101, 0x0200, 0x0201],
1710 "descramble_pids must be the union across both added services"
1711 );
1712 }
1713
1714 pub(crate) fn ca_pmt_reply_for(
1719 program_number: u16,
1720 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
1721 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
1722 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
1723 program_number,
1724 version_number: 1,
1725 current_next_indicator: true,
1726 ca_enable,
1727 streams: vec![],
1728 }
1729 }
1730
1731 #[test]
1732 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
1733 use broadcast_common::Parse;
1734 use dvb_ci::objects::ca_pmt_reply::CaEnable;
1735
1736 let mut d = driver_with_sessions();
1737 d.take_notifications();
1738
1739 let pmt_bytes = build_ca_pmt_fixture(1546);
1740 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1741 d.add_service(&pmt).unwrap();
1742 d.device_mut().inbound.push_back(sb());
1743 d.pump(Duration::from_millis(10)).unwrap();
1744 d.take_notifications();
1745
1746 let expected_initial_ca_pmt =
1750 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
1751 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
1752
1753 let expected_ca_pmt =
1757 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::Query).to_bytes();
1758 let sends_before_requery = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1759
1760 let mut all_notes = Vec::new();
1761
1762 feed(
1766 &mut d,
1767 r_apdu(
1768 CA_SESSION,
1769 &ser(&ca_pmt_reply_for(
1770 1546,
1771 Some(CaEnable::NotPossibleNoEntitlement),
1772 )),
1773 ),
1774 );
1775 all_notes.extend(d.take_notifications());
1776
1777 d.pump(Duration::from_secs(11)).unwrap();
1786 all_notes.extend(d.take_notifications());
1787 d.device_mut().inbound.push_back(sb());
1788 d.pump(Duration::from_millis(10)).unwrap();
1789
1790 let sends_after_requery = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1791 assert_eq!(
1792 sends_after_requery,
1793 sends_before_requery + 1,
1794 "expected the re-query timer to resend the exact ca_pmt exactly once"
1795 );
1796
1797 feed(
1800 &mut d,
1801 r_apdu(
1802 CA_SESSION,
1803 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
1804 ),
1805 );
1806 all_notes.extend(d.take_notifications());
1807
1808 let hits = all_notes
1809 .iter()
1810 .filter(|n| {
1811 matches!(
1812 n,
1813 Notification::Entitlement {
1814 program_number: 1546,
1815 ca_enable: CaEnable::Possible,
1816 descrambling_ok: true,
1817 }
1818 )
1819 })
1820 .count();
1821 assert_eq!(
1822 hits, 1,
1823 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
1824 );
1825 }
1826
1827 #[test]
1828 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
1829 use broadcast_common::Parse;
1830 use dvb_ci::objects::ca_pmt_reply::CaEnable;
1831
1832 let mut d = driver_with_sessions();
1833 d.take_notifications();
1834
1835 let pmt_bytes = build_ca_pmt_fixture(1547);
1836 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1837 d.add_service(&pmt).unwrap();
1838 d.device_mut().inbound.push_back(sb());
1839 d.pump(Duration::from_millis(10)).unwrap();
1840 d.take_notifications();
1841
1842 feed(
1846 &mut d,
1847 r_apdu(
1848 CA_SESSION,
1849 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
1850 ),
1851 );
1852 d.take_notifications();
1853
1854 for _ in 0..2 {
1857 d.pump(Duration::from_secs(11)).unwrap();
1858 d.take_notifications();
1859 feed(
1860 &mut d,
1861 r_apdu(
1862 CA_SESSION,
1863 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
1864 ),
1865 );
1866 let notes = d.take_notifications();
1867 assert!(
1868 !notes
1869 .iter()
1870 .any(|n| matches!(n, Notification::Entitlement { .. })),
1871 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
1872 );
1873 }
1874 }
1875
1876 #[test]
1877 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
1878 use broadcast_common::Parse;
1879 use dvb_ci::objects::ca_pmt_reply::CaEnable;
1880
1881 let mut d = driver_with_sessions();
1882 d.take_notifications();
1883
1884 let pmt_bytes = build_ca_pmt_fixture(1548);
1885 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1886 d.add_service(&pmt).unwrap();
1887 d.device_mut().inbound.push_back(sb());
1888 d.pump(Duration::from_millis(10)).unwrap();
1889 d.take_notifications();
1890
1891 feed(
1893 &mut d,
1894 r_apdu(
1895 CA_SESSION,
1896 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
1897 ),
1898 );
1899 d.take_notifications();
1900
1901 feed(
1905 &mut d,
1906 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
1907 );
1908 let notes = d.take_notifications();
1909 assert!(
1910 !notes
1911 .iter()
1912 .any(|n| matches!(n, Notification::Entitlement { .. })),
1913 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
1914 );
1915 }
1916
1917 #[test]
1918 fn set_requery_interval_zero_disables_resend() {
1919 use broadcast_common::Parse;
1920
1921 let mut d = driver_with_sessions();
1922 d.set_requery_interval(Duration::ZERO);
1923 d.take_notifications();
1924
1925 let pmt_bytes = build_ca_pmt_fixture(1549);
1926 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1927 d.add_service(&pmt).unwrap();
1928 d.device_mut().inbound.push_back(sb());
1929 d.pump(Duration::from_millis(10)).unwrap();
1930
1931 let expected_ca_pmt =
1934 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::Query).to_bytes();
1935 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1936
1937 d.pump(Duration::from_secs(1000)).unwrap();
1939
1940 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1941 assert_eq!(
1942 sends_after, sends_before,
1943 "Duration::ZERO must disable the re-query resend"
1944 );
1945 }
1946
1947 #[test]
1948 fn requery_timer_resends_every_active_service_not_just_one() {
1949 use broadcast_common::Parse;
1950
1951 let mut d = driver_with_sessions();
1952 d.take_notifications();
1953
1954 let pmt1_bytes = build_ca_pmt_fixture(1546);
1957 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1958 d.add_service(&pmt1).unwrap();
1959 d.device_mut().inbound.push_back(sb());
1960 d.pump(Duration::from_millis(10)).unwrap();
1961
1962 let pmt2_bytes = build_ca_pmt_fixture(1547);
1963 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1964 d.add_service(&pmt2).unwrap();
1965 d.device_mut().inbound.push_back(sb());
1966 d.pump(Duration::from_millis(10)).unwrap();
1967 d.take_notifications();
1968
1969 let expected1 =
1974 build_ca_pmt(&pmt1, CaPmtListManagement::First, CaPmtCmdId::Query).to_bytes();
1975 let expected2 =
1976 build_ca_pmt(&pmt2, CaPmtListManagement::Last, CaPmtCmdId::Query).to_bytes();
1977 let sends_before1 = count_apdu_on_session(&d, CA_SESSION, &expected1);
1978 let sends_before2 = count_apdu_on_session(&d, CA_SESSION, &expected2);
1979
1980 d.pump(Duration::from_secs(11)).unwrap();
1987 feed(&mut d, sb());
1988
1989 let sends_after1 = count_apdu_on_session(&d, CA_SESSION, &expected1);
1990 let sends_after2 = count_apdu_on_session(&d, CA_SESSION, &expected2);
1991 assert_eq!(
1992 sends_after1,
1993 sends_before1 + 1,
1994 "expected service 1546's query ca_pmt resent exactly once on the shared tick"
1995 );
1996 assert_eq!(
1997 sends_after2,
1998 sends_before2 + 1,
1999 "expected service 1547's query ca_pmt resent exactly once on the shared tick"
2000 );
2001 }
2002
2003 #[test]
2007 fn requery_after_remove_uses_only_for_sole_survivor() {
2008 use broadcast_common::Parse;
2009
2010 let mut d = driver_with_sessions();
2011 d.take_notifications();
2012
2013 let pmt1_bytes = build_ca_pmt_fixture(1546);
2016 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2017 d.add_service(&pmt1).unwrap();
2018 d.device_mut().inbound.push_back(sb());
2019 d.pump(Duration::from_millis(10)).unwrap();
2020
2021 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2022 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2023 d.add_service(&pmt2).unwrap();
2024 d.device_mut().inbound.push_back(sb());
2025 d.pump(Duration::from_millis(10)).unwrap();
2026
2027 d.remove_service(1546).unwrap();
2029 d.device_mut().inbound.push_back(sb());
2030 d.pump(Duration::from_millis(10)).unwrap();
2031 d.take_notifications();
2032
2033 let expected_only =
2039 build_ca_pmt(&pmt2, CaPmtListManagement::Only, CaPmtCmdId::Query).to_bytes();
2040 let expected_stale_add =
2041 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::Query).to_bytes();
2042 let sends_only_before = count_apdu_on_session(&d, CA_SESSION, &expected_only);
2043 let sends_add_before = count_apdu_on_session(&d, CA_SESSION, &expected_stale_add);
2044
2045 d.pump(Duration::from_secs(11)).unwrap();
2046 feed(&mut d, sb());
2047
2048 assert_eq!(
2049 count_apdu_on_session(&d, CA_SESSION, &expected_only),
2050 sends_only_before + 1,
2051 "sole-survivor re-query must resend with list_management = Only"
2052 );
2053 assert_eq!(
2054 count_apdu_on_session(&d, CA_SESSION, &expected_stale_add),
2055 sends_add_before,
2056 "sole-survivor re-query must NOT resend the stale Add list_management"
2057 );
2058 }
2059
2060 #[test]
2061 fn requery_two_services_uses_first_then_last() {
2062 use broadcast_common::Parse;
2063
2064 let mut d = driver_with_sessions();
2065 d.take_notifications();
2066
2067 let pmt1_bytes = build_ca_pmt_fixture(1546);
2070 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2071 d.add_service(&pmt1).unwrap();
2072 d.device_mut().inbound.push_back(sb());
2073 d.pump(Duration::from_millis(10)).unwrap();
2074
2075 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2076 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2077 d.add_service(&pmt2).unwrap();
2078 d.device_mut().inbound.push_back(sb());
2079 d.pump(Duration::from_millis(10)).unwrap();
2080 d.take_notifications();
2081
2082 let expected_first =
2086 build_ca_pmt(&pmt1, CaPmtListManagement::First, CaPmtCmdId::Query).to_bytes();
2087 let expected_last =
2088 build_ca_pmt(&pmt2, CaPmtListManagement::Last, CaPmtCmdId::Query).to_bytes();
2089 let sends_first_before = count_apdu_on_session(&d, CA_SESSION, &expected_first);
2090 let sends_last_before = count_apdu_on_session(&d, CA_SESSION, &expected_last);
2091
2092 d.pump(Duration::from_secs(11)).unwrap();
2093 feed(&mut d, sb());
2094
2095 assert_eq!(
2096 count_apdu_on_session(&d, CA_SESSION, &expected_first),
2097 sends_first_before + 1,
2098 "the lowest-program_number active service must re-query with First"
2099 );
2100 assert_eq!(
2101 count_apdu_on_session(&d, CA_SESSION, &expected_last),
2102 sends_last_before + 1,
2103 "the highest-program_number active service must re-query with Last"
2104 );
2105 }
2106
2107 #[test]
2110 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
2111 use broadcast_common::Parse;
2112
2113 let mut d = driver_with_sessions();
2114 d.take_notifications();
2115
2116 let pmt1_bytes = build_ca_pmt_fixture(1546);
2120 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2121 d.add_service(&pmt1).unwrap();
2122 d.device_mut().inbound.push_back(sb());
2123 d.pump(Duration::from_millis(10)).unwrap();
2124
2125 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2126 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2127 d.add_service(&pmt2).unwrap();
2128 d.device_mut().inbound.push_back(sb());
2129 d.pump(Duration::from_millis(10)).unwrap();
2130
2131 d.remove_service(1546).unwrap();
2132 d.device_mut().inbound.push_back(sb());
2133 d.pump(Duration::from_millis(10)).unwrap();
2134
2135 let expected =
2139 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
2140 assert_apdu_on_session(&d, CA_SESSION, &expected);
2141
2142 assert_eq!(
2143 d.descramble_pids(),
2144 &[0x0200, 0x0201],
2145 "1546's ES PIDs must be gone; 1547's must remain"
2146 );
2147 assert!(
2148 d.managed_ca().services().get(&1546).is_none(),
2149 "1546 must no longer be tracked"
2150 );
2151 assert!(
2152 d.managed_ca().services().get(&1547).is_some(),
2153 "1547 must remain tracked"
2154 );
2155 }
2156
2157 #[test]
2158 fn remove_service_of_untracked_program_is_a_no_op() {
2159 let mut d = driver_with_sessions();
2160 d.take_notifications();
2161
2162 let ops_before = d.device().ops.len();
2163 d.remove_service(0xFFFF).unwrap();
2164 assert_eq!(
2165 d.device().ops.len(),
2166 ops_before,
2167 "removing an untracked program must not send anything to the device"
2168 );
2169 assert!(
2170 d.managed_ca().services().is_empty(),
2171 "removing an untracked program must not disturb the (empty) managed set"
2172 );
2173 }
2174
2175 #[test]
2176 fn cam_removed_edge_clears_managed_state() {
2177 use broadcast_common::Parse;
2178 use dvb_ci::objects::ca_info::CaInfo;
2179 use dvb_si::tables::cat::CatSection;
2180
2181 let mut d = driver_with_sessions();
2182 d.take_notifications();
2183
2184 let pmt_bytes = build_ca_pmt_fixture(1546);
2185 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2186 d.add_service(&pmt).unwrap();
2187 d.device_mut().inbound.push_back(sb());
2188 d.pump(Duration::from_millis(10)).unwrap();
2189
2190 feed(
2193 &mut d,
2194 r_apdu(
2195 CA_SESSION,
2196 &ser(&CaInfo {
2197 ca_system_ids: vec![0x0648],
2198 }),
2199 ),
2200 );
2201 d.take_notifications();
2202 let mut descriptors = Vec::new();
2203 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2204 let cat_bytes = build_cat_fixture(&descriptors);
2205 let cat = CatSection::parse(&cat_bytes).unwrap();
2206 d.set_cat(&cat).unwrap();
2207
2208 assert!(
2209 !d.managed_ca().services().is_empty(),
2210 "precondition: a service is tracked"
2211 );
2212 assert!(
2213 !d.descramble_pids().is_empty(),
2214 "precondition: descramble_pids populated"
2215 );
2216 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
2217
2218 d.device_mut().slot.module_present = false;
2220 d.pump(Duration::from_millis(10)).unwrap();
2221 let notes = d.take_notifications();
2222 assert!(
2223 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
2224 "expected CamRemoved, got {notes:?}"
2225 );
2226
2227 assert!(
2228 d.managed_ca().services().is_empty(),
2229 "services must be cleared on CamRemoved"
2230 );
2231 assert!(
2232 d.descramble_pids().is_empty(),
2233 "descramble_pids must be cleared on CamRemoved"
2234 );
2235 assert!(
2236 d.emm_pids().is_empty(),
2237 "emm_pids must be cleared on CamRemoved"
2238 );
2239 }
2240}