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 && let Some(pos) = w.windows(3).position(|x| x == want)
796 && let Ok(p) = Profile::parse(&w[pos..])
797 {
798 return p.resources.contains(&HOST_CONTROL);
799 }
800 false
801 });
802 assert!(found, "profile reply must advertise HOST_CONTROL");
803 }
804
805 #[test]
806 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
807 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
808
809 let mut d = driver_with_sessions();
810 let mmi_nb = MMI_SESSION;
811
812 d.mmi_menu_answer(2).unwrap();
815 d.device_mut().inbound.push_back(sb());
816 d.pump(Duration::from_millis(10)).unwrap();
817 assert_apdu_on_session(&d, mmi_nb, &ser(&MenuAnsw { choice_ref: 2 }));
818
819 d.mmi_enquiry_answer(b"1234").unwrap();
821 d.device_mut().inbound.push_back(sb());
822 d.pump(Duration::from_millis(10)).unwrap();
823 assert_apdu_on_session(
824 &d,
825 mmi_nb,
826 &ser(&Answ {
827 answ_id: AnswId::Answer,
828 text_chars: b"1234",
829 }),
830 );
831 }
832
833 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
836 use dvb_ci::spdu::SessionNumber;
837 let mut want = ser(&SessionNumber { session_nb });
838 want.extend_from_slice(apdu);
839 let hit = d.device().ops.iter().any(|op| match op {
840 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
841 _ => false,
842 });
843 assert!(
844 hit,
845 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
846 );
847 }
848
849 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
853 use dvb_ci::spdu::SessionNumber;
854 let mut want = ser(&SessionNumber { session_nb });
855 want.extend_from_slice(apdu);
856 d.device()
857 .ops
858 .iter()
859 .filter(|op| match op {
860 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
861 _ => false,
862 })
863 .count()
864 }
865
866 #[test]
867 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
868 let mut d = Driver::new(MockCaDevice::new([]));
869 d.init().unwrap();
870 let ops = &d.device().ops;
871 assert_eq!(ops[0], DeviceOp::Reset);
872 assert_eq!(ops[1], DeviceOp::SlotInfo);
873 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
874 }
875
876 #[test]
877 fn reads_reply_then_polls_on_pump() {
878 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
880 let mut d = Driver::new(dev);
881 d.init().unwrap();
882 assert!(d.pump(Duration::from_millis(100)).unwrap());
884 assert!(!d.pump(Duration::from_millis(100)).unwrap());
886 let last = d.device().ops.last().unwrap();
887 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
888 }
889
890 #[test]
893 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
894 let mut dev = MockCaDevice::new([]);
895 dev.slot = SlotInfo {
896 num: 0,
897 module_ready: false,
898 module_present: false,
899 };
900 let mut d = Driver::new(dev);
901 d.init().unwrap();
902 let notes = d.take_notifications();
905 assert!(
906 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
907 "baseline observation must not fire CamPresent, got {notes:?}"
908 );
909 let resets_before = d
910 .device()
911 .ops
912 .iter()
913 .filter(|o| **o == DeviceOp::Reset)
914 .count();
915
916 d.device_mut().slot = SlotInfo {
918 num: 0,
919 module_ready: true,
920 module_present: true,
921 };
922 d.pump(Duration::from_millis(10)).unwrap();
923
924 let notes = d.take_notifications();
925 let cam_present_count = notes
926 .iter()
927 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
928 .count();
929 assert_eq!(
930 cam_present_count, 1,
931 "expected exactly one CamPresent, got {notes:?}"
932 );
933 let resets_after = d
935 .device()
936 .ops
937 .iter()
938 .filter(|o| **o == DeviceOp::Reset)
939 .count();
940 assert_eq!(
941 resets_after,
942 resets_before + 1,
943 "expected one fresh Reset on re-insert"
944 );
945 assert!(
946 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
947 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
948 d.device().ops.last()
949 );
950 }
951
952 #[test]
953 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
954 let mut d = driver_with_sessions();
955 d.take_notifications();
956
957 d.device_mut().slot.module_present = false;
959 d.pump(Duration::from_millis(10)).unwrap();
960 let notes = d.take_notifications();
961 assert!(
962 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
963 "expected CamRemoved, got {notes:?}"
964 );
965
966 d.mmi_menu_answer(0).unwrap();
970 let notes = d.take_notifications();
971 assert!(
972 notes
973 .iter()
974 .any(|n| matches!(n, Notification::Error { .. })),
975 "expected no open MMI session after teardown, got {notes:?}"
976 );
977
978 let resets_before = d
980 .device()
981 .ops
982 .iter()
983 .filter(|o| **o == DeviceOp::Reset)
984 .count();
985 d.device_mut().slot.module_present = true;
986 d.device_mut().slot.module_ready = true;
987 d.pump(Duration::from_millis(10)).unwrap();
988 let notes = d.take_notifications();
989 assert!(
990 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
991 "expected CamPresent on re-insert, got {notes:?}"
992 );
993 let resets_after = d
994 .device()
995 .ops
996 .iter()
997 .filter(|o| **o == DeviceOp::Reset)
998 .count();
999 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1000 }
1001
1002 #[test]
1003 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1004 let mut d = Driver::new(MockCaDevice::new([]));
1005 d.init().unwrap();
1006 d.take_notifications();
1007
1008 for _ in 0..5 {
1009 d.pump(Duration::from_millis(10)).unwrap();
1010 }
1011 let notes = d.take_notifications();
1012 assert!(
1013 !notes.iter().any(|n| matches!(
1014 n,
1015 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1016 )),
1017 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1018 );
1019 }
1020
1021 #[test]
1022 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1023 use dvb_ci::objects::ca_info::CaInfo;
1024
1025 let mut d = driver_with_sessions();
1026 d.take_notifications();
1027
1028 feed(
1030 &mut d,
1031 r_apdu(
1032 CA_SESSION,
1033 &ser(&CaInfo {
1034 ca_system_ids: vec![],
1035 }),
1036 ),
1037 );
1038 let notes = d.take_notifications();
1039 assert!(
1040 !notes.iter().any(|n| matches!(
1041 n,
1042 Notification::HotPlug(
1043 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1044 )
1045 )),
1046 "first ca_info must only establish the baseline, got {notes:?}"
1047 );
1048
1049 feed(
1051 &mut d,
1052 r_apdu(
1053 CA_SESSION,
1054 &ser(&CaInfo {
1055 ca_system_ids: vec![0x0B00],
1056 }),
1057 ),
1058 );
1059 let notes = d.take_notifications();
1060 assert!(
1061 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1062 "expected CardInserted, got {notes:?}"
1063 );
1064
1065 feed(
1067 &mut d,
1068 r_apdu(
1069 CA_SESSION,
1070 &ser(&CaInfo {
1071 ca_system_ids: vec![0x1800],
1072 }),
1073 ),
1074 );
1075 let notes = d.take_notifications();
1076 assert!(
1077 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1078 "expected CardChanged, got {notes:?}"
1079 );
1080 }
1081
1082 #[test]
1083 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1084 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1085
1086 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1087 CaPmtReply {
1088 program_number: 1,
1089 version_number: 1,
1090 current_next_indicator: true,
1091 ca_enable,
1092 streams: vec![],
1093 }
1094 }
1095
1096 let mut d = driver_with_sessions();
1097 d.take_notifications();
1098
1099 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1101 let notes = d.take_notifications();
1102 assert!(
1103 !notes.iter().any(|n| matches!(
1104 n,
1105 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1106 )),
1107 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1108 );
1109
1110 feed(
1112 &mut d,
1113 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1114 );
1115 let notes = d.take_notifications();
1116 assert!(
1117 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1118 "expected CardInserted, got {notes:?}"
1119 );
1120
1121 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1123 let notes = d.take_notifications();
1124 assert!(
1125 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1126 "expected CardRemoved, got {notes:?}"
1127 );
1128 }
1129
1130 #[test]
1131 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1132 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1133
1134 let mut d = driver_with_sessions();
1135 d.take_notifications();
1136
1137 feed(
1140 &mut d,
1141 r_apdu(
1142 CA_SESSION,
1143 &ser(&CaPmtReply {
1144 program_number: 7,
1145 version_number: 1,
1146 current_next_indicator: true,
1147 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1148 streams: vec![],
1149 }),
1150 ),
1151 );
1152 let notes = d.take_notifications();
1153 assert!(
1154 notes.contains(&Notification::CaPmtReply {
1155 program_number: 7,
1156 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1157 descrambling_ok: true,
1158 }),
1159 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1160 );
1161 }
1162
1163 #[test]
1164 fn ca_pmt_reply_flag_clear_surfaces_none() {
1165 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1166
1167 let mut d = driver_with_sessions();
1168 d.take_notifications();
1169
1170 feed(
1173 &mut d,
1174 r_apdu(
1175 CA_SESSION,
1176 &ser(&CaPmtReply {
1177 program_number: 7,
1178 version_number: 1,
1179 current_next_indicator: true,
1180 ca_enable: None,
1181 streams: vec![],
1182 }),
1183 ),
1184 );
1185 let notes = d.take_notifications();
1186 assert!(
1187 notes.contains(&Notification::CaPmtReply {
1188 program_number: 7,
1189 ca_enable: None,
1190 descrambling_ok: false,
1191 }),
1192 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1193 );
1194 }
1195
1196 #[test]
1197 fn mmi_no_card_text_infers_card_removed() {
1198 use dvb_ci::objects::mmi_high::Enq;
1199
1200 let mut d = driver_with_sessions();
1201 d.take_notifications();
1202
1203 feed(
1204 &mut d,
1205 r_apdu(
1206 MMI_SESSION,
1207 &ser(&Enq {
1208 blind_answer: false,
1209 answer_text_length: 0,
1210 text_chars: b"NO CARD detected - please insert your smart card",
1211 }),
1212 ),
1213 );
1214
1215 let notes = d.take_notifications();
1216 assert!(
1217 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1218 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1219 );
1220 }
1221
1222 #[test]
1223 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1224 let mut dev = MockCaDevice::new([]);
1225 dev.slot = SlotInfo {
1226 num: 0,
1227 module_ready: false,
1228 module_present: false,
1229 };
1230 let mut d = Driver::new(dev);
1231 d.init().unwrap();
1232 d.take_notifications(); d.device_mut().slot = SlotInfo {
1236 num: 0,
1237 module_ready: true,
1238 module_present: true,
1239 };
1240
1241 let mut seen = Vec::new();
1242 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1243 .unwrap();
1244
1245 assert_eq!(
1246 seen,
1247 vec![HotPlug::CamPresent],
1248 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1249 );
1250 }
1251
1252 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
1257 [
1258 0x09,
1259 0x04,
1260 (ca_system_id >> 8) as u8,
1261 ca_system_id as u8,
1262 0xE0 | ((pid >> 8) as u8 & 0x1F),
1263 pid as u8,
1264 ]
1265 }
1266
1267 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
1284 const VIACCESS: u16 = 0x0500;
1285 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1286 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1287
1288 let mut body = Vec::new();
1289 body.push(0x02); body.push(0); body.push(0);
1292 body.extend_from_slice(&program_number.to_be_bytes());
1293 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
1298 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1299 body.push(prog_ca.len() as u8);
1300 body.extend_from_slice(&prog_ca);
1301 body.push(0x1B);
1303 body.push(0xE0 | 0x01);
1304 body.push(0x00);
1305 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1306 body.push(es0_ca.len() as u8);
1307 body.extend_from_slice(&es0_ca);
1308 body.push(0x0F);
1310 body.push(0xE0 | 0x01);
1311 body.push(0x01);
1312 body.push(0xF0);
1313 body.push(0x00);
1314
1315 let section_length = body.len() - 3 + 4;
1316 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1317 body[2] = section_length as u8;
1318 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1319 body.extend_from_slice(&crc.to_be_bytes());
1320 body
1321 }
1322
1323 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
1330 const VIACCESS: u16 = 0x0500;
1331 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1332 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1333
1334 let mut body = Vec::new();
1335 body.push(0x02); body.push(0); body.push(0);
1338 body.extend_from_slice(&program_number.to_be_bytes());
1339 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));
1345 body.push(prog_ca.len() as u8);
1346 body.extend_from_slice(&prog_ca);
1347 body.push(0x1B);
1349 body.push(0xE0 | 0x01);
1350 body.push(0x00);
1351 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1352 body.push(es0_ca.len() as u8);
1353 body.extend_from_slice(&es0_ca);
1354 body.push(0x0F);
1356 body.push(0xE0 | 0x01);
1357 body.push(0x01);
1358 body.push(0xF0);
1359 body.push(0x00);
1360
1361 let section_length = body.len() - 3 + 4;
1362 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1363 body[2] = section_length as u8;
1364 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1365 body.extend_from_slice(&crc.to_be_bytes());
1366 body
1367 }
1368
1369 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
1373 let mut body = Vec::new();
1374 body.push(0x02);
1375 body.push(0);
1376 body.push(0);
1377 body.extend_from_slice(&program_number.to_be_bytes());
1378 body.push(0xC3);
1379 body.push(0x00);
1380 body.push(0x00);
1381 body.push(0xE0 | 0x01);
1382 body.push(0x00);
1383 body.push(0xF0); body.push(0x00);
1385 body.push(0x1B);
1387 body.push(0xE0 | 0x01);
1388 body.push(0x00);
1389 body.push(0xF0);
1390 body.push(0x00);
1391
1392 let section_length = body.len() - 3 + 4;
1393 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1394 body[2] = section_length as u8;
1395 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1396 body.extend_from_slice(&crc.to_be_bytes());
1397 body
1398 }
1399
1400 #[test]
1401 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
1402 use broadcast_common::Parse;
1403
1404 let mut d = driver_with_sessions();
1405 d.take_notifications();
1406
1407 let pmt_bytes = build_ca_pmt_fixture(1546);
1408 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1409
1410 d.add_service(&pmt).unwrap();
1411 d.device_mut().inbound.push_back(sb());
1412 d.pump(Duration::from_millis(10)).unwrap();
1413
1414 let expected =
1418 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
1419 assert_apdu_on_session(&d, CA_SESSION, &expected);
1420
1421 let svc = d
1423 .managed_ca()
1424 .services()
1425 .get(&1546)
1426 .expect("program_number 1546 must be tracked after add_service");
1427 assert_eq!(svc.es_pids, vec![0x0100, 0x0101]);
1428 assert_eq!(svc.ca_pids, vec![0x0064, 0x0065]);
1429 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
1430 assert_eq!(svc.last_ca_enable, None);
1431 }
1432
1433 #[test]
1434 fn add_service_rejects_pmt_without_ca_descriptor() {
1435 use broadcast_common::Parse;
1436
1437 let mut d = driver_with_sessions();
1438 let pmt_bytes = build_clear_pmt_fixture(999);
1439 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1440
1441 let err = d.add_service(&pmt).unwrap_err();
1442 assert!(
1443 matches!(
1444 err,
1445 CaError::NoCaDescriptor {
1446 program_number: 999
1447 }
1448 ),
1449 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
1450 );
1451 assert!(
1452 d.managed_ca().services().is_empty(),
1453 "a rejected PMT must not be recorded"
1454 );
1455 }
1456
1457 #[test]
1458 fn add_service_second_call_uses_add_list_management() {
1459 use broadcast_common::Parse;
1460
1461 let mut d = driver_with_sessions();
1462 d.take_notifications();
1463
1464 let pmt1_bytes = build_ca_pmt_fixture(1546);
1465 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1466 d.add_service(&pmt1).unwrap();
1467 d.device_mut().inbound.push_back(sb());
1468 d.pump(Duration::from_millis(10)).unwrap();
1469
1470 let pmt2_bytes = build_ca_pmt_fixture(1547);
1471 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1472 d.add_service(&pmt2).unwrap();
1473 d.device_mut().inbound.push_back(sb());
1474 d.pump(Duration::from_millis(10)).unwrap();
1475
1476 let expected2 =
1478 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
1479 assert_apdu_on_session(&d, CA_SESSION, &expected2);
1480
1481 assert_eq!(d.managed_ca().services().len(), 2);
1482 }
1483
1484 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
1494 const EXTENSION_HEADER_LEN: u16 = 5;
1495 const CRC_LEN: u16 = 4;
1496 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
1497 let mut v = Vec::new();
1498 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
1500 v.push((section_length & 0xFF) as u8);
1501 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
1506 let crc = broadcast_common::crc32_mpeg2::compute(&v);
1507 v.extend_from_slice(&crc.to_be_bytes());
1508 v
1509 }
1510
1511 #[test]
1512 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
1513 use broadcast_common::Parse;
1514 use dvb_ci::objects::ca_info::CaInfo;
1515 use dvb_si::tables::cat::CatSection;
1516
1517 let mut d = driver_with_sessions();
1518 d.take_notifications();
1519
1520 feed(
1522 &mut d,
1523 r_apdu(
1524 CA_SESSION,
1525 &ser(&CaInfo {
1526 ca_system_ids: vec![0x0648, 0x0100],
1527 }),
1528 ),
1529 );
1530 d.take_notifications();
1531
1532 let mut descriptors = Vec::new();
1535 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
1536 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
1537 let cat_bytes = build_cat_fixture(&descriptors);
1538 let cat = CatSection::parse(&cat_bytes).unwrap();
1539
1540 d.set_cat(&cat).unwrap();
1541
1542 assert_eq!(
1543 d.emm_pids(),
1544 &[0x1FF0],
1545 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
1546 );
1547 }
1548
1549 #[test]
1550 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
1551 use broadcast_common::Parse;
1552 use dvb_ci::objects::ca_info::CaInfo;
1553 use dvb_si::tables::cat::CatSection;
1554
1555 let mut d = driver_with_sessions();
1556 d.take_notifications();
1557
1558 let mut descriptors = Vec::new();
1559 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
1560 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
1561 let cat_bytes = build_cat_fixture(&descriptors);
1562 let cat = CatSection::parse(&cat_bytes).unwrap();
1563
1564 d.set_cat(&cat).unwrap();
1567 assert!(
1568 d.emm_pids().is_empty(),
1569 "emm_pids must be empty before any ca_info arrives, got {:?}",
1570 d.emm_pids()
1571 );
1572
1573 feed(
1576 &mut d,
1577 r_apdu(
1578 CA_SESSION,
1579 &ser(&CaInfo {
1580 ca_system_ids: vec![0x0648, 0x0100],
1581 }),
1582 ),
1583 );
1584 d.take_notifications();
1585
1586 assert_eq!(
1587 d.emm_pids(),
1588 &[0x1FF0],
1589 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
1590 );
1591 }
1592
1593 #[test]
1599 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
1600 use broadcast_common::Parse;
1601 use dvb_ci::objects::ca_info::CaInfo;
1602 use dvb_si::tables::cat::CatSection;
1603
1604 let mut d = driver_with_sessions();
1605 d.take_notifications();
1606
1607 feed(
1609 &mut d,
1610 r_apdu(
1611 CA_SESSION,
1612 &ser(&CaInfo {
1613 ca_system_ids: vec![0x0648, 0x0100],
1614 }),
1615 ),
1616 );
1617 d.take_notifications();
1618
1619 let mut descriptors = Vec::new();
1621 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
1622 descriptors.extend_from_slice(&ca_descriptor(0x0100, 0x1FF0));
1623 let cat_bytes = build_cat_fixture(&descriptors);
1624 let cat = CatSection::parse(&cat_bytes).unwrap();
1625
1626 d.set_cat(&cat).unwrap();
1627
1628 assert_eq!(
1629 d.emm_pids(),
1630 &[0x1FF0],
1631 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
1632 d.emm_pids()
1633 );
1634 }
1635
1636 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
1640 const VIACCESS: u16 = 0x0500;
1641 let prog_ca = ca_descriptor(VIACCESS, 0x0074);
1642 let es0_ca = ca_descriptor(VIACCESS, 0x0075);
1643
1644 let mut body = Vec::new();
1645 body.push(0x02); body.push(0);
1647 body.push(0);
1648 body.extend_from_slice(&program_number.to_be_bytes());
1649 body.push(0xC3);
1650 body.push(0x00);
1651 body.push(0x00);
1652 body.push(0xE0 | 0x02); body.push(0x00);
1654 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1655 body.push(prog_ca.len() as u8);
1656 body.extend_from_slice(&prog_ca);
1657 body.push(0x1B);
1659 body.push(0xE0 | 0x02);
1660 body.push(0x00);
1661 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1662 body.push(es0_ca.len() as u8);
1663 body.extend_from_slice(&es0_ca);
1664 body.push(0x0F);
1666 body.push(0xE0 | 0x02);
1667 body.push(0x01);
1668 body.push(0xF0);
1669 body.push(0x00);
1670
1671 let section_length = body.len() - 3 + 4;
1672 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1673 body[2] = section_length as u8;
1674 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1675 body.extend_from_slice(&crc.to_be_bytes());
1676 body
1677 }
1678
1679 #[test]
1680 fn descramble_pids_is_the_union_of_active_services_es_pids() {
1681 use broadcast_common::Parse;
1682
1683 let mut d = driver_with_sessions();
1684 d.take_notifications();
1685
1686 assert!(
1687 d.descramble_pids().is_empty(),
1688 "no service added yet: descramble_pids must be empty"
1689 );
1690
1691 let pmt1_bytes = build_ca_pmt_fixture(1546);
1692 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1693 d.add_service(&pmt1).unwrap();
1694 d.device_mut().inbound.push_back(sb());
1695 d.pump(Duration::from_millis(10)).unwrap();
1696
1697 assert_eq!(d.descramble_pids(), &[0x0100, 0x0101]);
1698
1699 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
1700 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1701 d.add_service(&pmt2).unwrap();
1702 d.device_mut().inbound.push_back(sb());
1703 d.pump(Duration::from_millis(10)).unwrap();
1704
1705 assert_eq!(
1707 d.descramble_pids(),
1708 &[0x0100, 0x0101, 0x0200, 0x0201],
1709 "descramble_pids must be the union across both added services"
1710 );
1711 }
1712
1713 pub(crate) fn ca_pmt_reply_for(
1718 program_number: u16,
1719 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
1720 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
1721 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
1722 program_number,
1723 version_number: 1,
1724 current_next_indicator: true,
1725 ca_enable,
1726 streams: vec![],
1727 }
1728 }
1729
1730 #[test]
1731 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
1732 use broadcast_common::Parse;
1733 use dvb_ci::objects::ca_pmt_reply::CaEnable;
1734
1735 let mut d = driver_with_sessions();
1736 d.take_notifications();
1737
1738 let pmt_bytes = build_ca_pmt_fixture(1546);
1739 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1740 d.add_service(&pmt).unwrap();
1741 d.device_mut().inbound.push_back(sb());
1742 d.pump(Duration::from_millis(10)).unwrap();
1743 d.take_notifications();
1744
1745 let expected_initial_ca_pmt =
1749 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
1750 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
1751
1752 let expected_ca_pmt =
1756 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::Query).to_bytes();
1757 let sends_before_requery = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1758
1759 let mut all_notes = Vec::new();
1760
1761 feed(
1765 &mut d,
1766 r_apdu(
1767 CA_SESSION,
1768 &ser(&ca_pmt_reply_for(
1769 1546,
1770 Some(CaEnable::NotPossibleNoEntitlement),
1771 )),
1772 ),
1773 );
1774 all_notes.extend(d.take_notifications());
1775
1776 d.pump(Duration::from_secs(11)).unwrap();
1785 all_notes.extend(d.take_notifications());
1786 d.device_mut().inbound.push_back(sb());
1787 d.pump(Duration::from_millis(10)).unwrap();
1788
1789 let sends_after_requery = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1790 assert_eq!(
1791 sends_after_requery,
1792 sends_before_requery + 1,
1793 "expected the re-query timer to resend the exact ca_pmt exactly once"
1794 );
1795
1796 feed(
1799 &mut d,
1800 r_apdu(
1801 CA_SESSION,
1802 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
1803 ),
1804 );
1805 all_notes.extend(d.take_notifications());
1806
1807 let hits = all_notes
1808 .iter()
1809 .filter(|n| {
1810 matches!(
1811 n,
1812 Notification::Entitlement {
1813 program_number: 1546,
1814 ca_enable: CaEnable::Possible,
1815 descrambling_ok: true,
1816 }
1817 )
1818 })
1819 .count();
1820 assert_eq!(
1821 hits, 1,
1822 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
1823 );
1824 }
1825
1826 #[test]
1827 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
1828 use broadcast_common::Parse;
1829 use dvb_ci::objects::ca_pmt_reply::CaEnable;
1830
1831 let mut d = driver_with_sessions();
1832 d.take_notifications();
1833
1834 let pmt_bytes = build_ca_pmt_fixture(1547);
1835 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1836 d.add_service(&pmt).unwrap();
1837 d.device_mut().inbound.push_back(sb());
1838 d.pump(Duration::from_millis(10)).unwrap();
1839 d.take_notifications();
1840
1841 feed(
1845 &mut d,
1846 r_apdu(
1847 CA_SESSION,
1848 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
1849 ),
1850 );
1851 d.take_notifications();
1852
1853 for _ in 0..2 {
1856 d.pump(Duration::from_secs(11)).unwrap();
1857 d.take_notifications();
1858 feed(
1859 &mut d,
1860 r_apdu(
1861 CA_SESSION,
1862 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
1863 ),
1864 );
1865 let notes = d.take_notifications();
1866 assert!(
1867 !notes
1868 .iter()
1869 .any(|n| matches!(n, Notification::Entitlement { .. })),
1870 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
1871 );
1872 }
1873 }
1874
1875 #[test]
1876 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
1877 use broadcast_common::Parse;
1878 use dvb_ci::objects::ca_pmt_reply::CaEnable;
1879
1880 let mut d = driver_with_sessions();
1881 d.take_notifications();
1882
1883 let pmt_bytes = build_ca_pmt_fixture(1548);
1884 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1885 d.add_service(&pmt).unwrap();
1886 d.device_mut().inbound.push_back(sb());
1887 d.pump(Duration::from_millis(10)).unwrap();
1888 d.take_notifications();
1889
1890 feed(
1892 &mut d,
1893 r_apdu(
1894 CA_SESSION,
1895 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
1896 ),
1897 );
1898 d.take_notifications();
1899
1900 feed(
1904 &mut d,
1905 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
1906 );
1907 let notes = d.take_notifications();
1908 assert!(
1909 !notes
1910 .iter()
1911 .any(|n| matches!(n, Notification::Entitlement { .. })),
1912 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
1913 );
1914 }
1915
1916 #[test]
1917 fn set_requery_interval_zero_disables_resend() {
1918 use broadcast_common::Parse;
1919
1920 let mut d = driver_with_sessions();
1921 d.set_requery_interval(Duration::ZERO);
1922 d.take_notifications();
1923
1924 let pmt_bytes = build_ca_pmt_fixture(1549);
1925 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1926 d.add_service(&pmt).unwrap();
1927 d.device_mut().inbound.push_back(sb());
1928 d.pump(Duration::from_millis(10)).unwrap();
1929
1930 let expected_ca_pmt =
1933 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::Query).to_bytes();
1934 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1935
1936 d.pump(Duration::from_secs(1000)).unwrap();
1938
1939 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
1940 assert_eq!(
1941 sends_after, sends_before,
1942 "Duration::ZERO must disable the re-query resend"
1943 );
1944 }
1945
1946 #[test]
1947 fn requery_timer_resends_every_active_service_not_just_one() {
1948 use broadcast_common::Parse;
1949
1950 let mut d = driver_with_sessions();
1951 d.take_notifications();
1952
1953 let pmt1_bytes = build_ca_pmt_fixture(1546);
1956 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1957 d.add_service(&pmt1).unwrap();
1958 d.device_mut().inbound.push_back(sb());
1959 d.pump(Duration::from_millis(10)).unwrap();
1960
1961 let pmt2_bytes = build_ca_pmt_fixture(1547);
1962 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1963 d.add_service(&pmt2).unwrap();
1964 d.device_mut().inbound.push_back(sb());
1965 d.pump(Duration::from_millis(10)).unwrap();
1966 d.take_notifications();
1967
1968 let expected1 =
1973 build_ca_pmt(&pmt1, CaPmtListManagement::First, CaPmtCmdId::Query).to_bytes();
1974 let expected2 =
1975 build_ca_pmt(&pmt2, CaPmtListManagement::Last, CaPmtCmdId::Query).to_bytes();
1976 let sends_before1 = count_apdu_on_session(&d, CA_SESSION, &expected1);
1977 let sends_before2 = count_apdu_on_session(&d, CA_SESSION, &expected2);
1978
1979 d.pump(Duration::from_secs(11)).unwrap();
1986 feed(&mut d, sb());
1987
1988 let sends_after1 = count_apdu_on_session(&d, CA_SESSION, &expected1);
1989 let sends_after2 = count_apdu_on_session(&d, CA_SESSION, &expected2);
1990 assert_eq!(
1991 sends_after1,
1992 sends_before1 + 1,
1993 "expected service 1546's query ca_pmt resent exactly once on the shared tick"
1994 );
1995 assert_eq!(
1996 sends_after2,
1997 sends_before2 + 1,
1998 "expected service 1547's query ca_pmt resent exactly once on the shared tick"
1999 );
2000 }
2001
2002 #[test]
2006 fn requery_after_remove_uses_only_for_sole_survivor() {
2007 use broadcast_common::Parse;
2008
2009 let mut d = driver_with_sessions();
2010 d.take_notifications();
2011
2012 let pmt1_bytes = build_ca_pmt_fixture(1546);
2015 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2016 d.add_service(&pmt1).unwrap();
2017 d.device_mut().inbound.push_back(sb());
2018 d.pump(Duration::from_millis(10)).unwrap();
2019
2020 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2021 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2022 d.add_service(&pmt2).unwrap();
2023 d.device_mut().inbound.push_back(sb());
2024 d.pump(Duration::from_millis(10)).unwrap();
2025
2026 d.remove_service(1546).unwrap();
2028 d.device_mut().inbound.push_back(sb());
2029 d.pump(Duration::from_millis(10)).unwrap();
2030 d.take_notifications();
2031
2032 let expected_only =
2038 build_ca_pmt(&pmt2, CaPmtListManagement::Only, CaPmtCmdId::Query).to_bytes();
2039 let expected_stale_add =
2040 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::Query).to_bytes();
2041 let sends_only_before = count_apdu_on_session(&d, CA_SESSION, &expected_only);
2042 let sends_add_before = count_apdu_on_session(&d, CA_SESSION, &expected_stale_add);
2043
2044 d.pump(Duration::from_secs(11)).unwrap();
2045 feed(&mut d, sb());
2046
2047 assert_eq!(
2048 count_apdu_on_session(&d, CA_SESSION, &expected_only),
2049 sends_only_before + 1,
2050 "sole-survivor re-query must resend with list_management = Only"
2051 );
2052 assert_eq!(
2053 count_apdu_on_session(&d, CA_SESSION, &expected_stale_add),
2054 sends_add_before,
2055 "sole-survivor re-query must NOT resend the stale Add list_management"
2056 );
2057 }
2058
2059 #[test]
2060 fn requery_two_services_uses_first_then_last() {
2061 use broadcast_common::Parse;
2062
2063 let mut d = driver_with_sessions();
2064 d.take_notifications();
2065
2066 let pmt1_bytes = build_ca_pmt_fixture(1546);
2069 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2070 d.add_service(&pmt1).unwrap();
2071 d.device_mut().inbound.push_back(sb());
2072 d.pump(Duration::from_millis(10)).unwrap();
2073
2074 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2075 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2076 d.add_service(&pmt2).unwrap();
2077 d.device_mut().inbound.push_back(sb());
2078 d.pump(Duration::from_millis(10)).unwrap();
2079 d.take_notifications();
2080
2081 let expected_first =
2085 build_ca_pmt(&pmt1, CaPmtListManagement::First, CaPmtCmdId::Query).to_bytes();
2086 let expected_last =
2087 build_ca_pmt(&pmt2, CaPmtListManagement::Last, CaPmtCmdId::Query).to_bytes();
2088 let sends_first_before = count_apdu_on_session(&d, CA_SESSION, &expected_first);
2089 let sends_last_before = count_apdu_on_session(&d, CA_SESSION, &expected_last);
2090
2091 d.pump(Duration::from_secs(11)).unwrap();
2092 feed(&mut d, sb());
2093
2094 assert_eq!(
2095 count_apdu_on_session(&d, CA_SESSION, &expected_first),
2096 sends_first_before + 1,
2097 "the lowest-program_number active service must re-query with First"
2098 );
2099 assert_eq!(
2100 count_apdu_on_session(&d, CA_SESSION, &expected_last),
2101 sends_last_before + 1,
2102 "the highest-program_number active service must re-query with Last"
2103 );
2104 }
2105
2106 #[test]
2109 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
2110 use broadcast_common::Parse;
2111
2112 let mut d = driver_with_sessions();
2113 d.take_notifications();
2114
2115 let pmt1_bytes = build_ca_pmt_fixture(1546);
2119 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2120 d.add_service(&pmt1).unwrap();
2121 d.device_mut().inbound.push_back(sb());
2122 d.pump(Duration::from_millis(10)).unwrap();
2123
2124 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2125 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2126 d.add_service(&pmt2).unwrap();
2127 d.device_mut().inbound.push_back(sb());
2128 d.pump(Duration::from_millis(10)).unwrap();
2129
2130 d.remove_service(1546).unwrap();
2131 d.device_mut().inbound.push_back(sb());
2132 d.pump(Duration::from_millis(10)).unwrap();
2133
2134 let expected =
2138 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
2139 assert_apdu_on_session(&d, CA_SESSION, &expected);
2140
2141 assert_eq!(
2142 d.descramble_pids(),
2143 &[0x0200, 0x0201],
2144 "1546's ES PIDs must be gone; 1547's must remain"
2145 );
2146 assert!(
2147 d.managed_ca().services().get(&1546).is_none(),
2148 "1546 must no longer be tracked"
2149 );
2150 assert!(
2151 d.managed_ca().services().get(&1547).is_some(),
2152 "1547 must remain tracked"
2153 );
2154 }
2155
2156 #[test]
2157 fn remove_service_of_untracked_program_is_a_no_op() {
2158 let mut d = driver_with_sessions();
2159 d.take_notifications();
2160
2161 let ops_before = d.device().ops.len();
2162 d.remove_service(0xFFFF).unwrap();
2163 assert_eq!(
2164 d.device().ops.len(),
2165 ops_before,
2166 "removing an untracked program must not send anything to the device"
2167 );
2168 assert!(
2169 d.managed_ca().services().is_empty(),
2170 "removing an untracked program must not disturb the (empty) managed set"
2171 );
2172 }
2173
2174 #[test]
2175 fn cam_removed_edge_clears_managed_state() {
2176 use broadcast_common::Parse;
2177 use dvb_ci::objects::ca_info::CaInfo;
2178 use dvb_si::tables::cat::CatSection;
2179
2180 let mut d = driver_with_sessions();
2181 d.take_notifications();
2182
2183 let pmt_bytes = build_ca_pmt_fixture(1546);
2184 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2185 d.add_service(&pmt).unwrap();
2186 d.device_mut().inbound.push_back(sb());
2187 d.pump(Duration::from_millis(10)).unwrap();
2188
2189 feed(
2192 &mut d,
2193 r_apdu(
2194 CA_SESSION,
2195 &ser(&CaInfo {
2196 ca_system_ids: vec![0x0648],
2197 }),
2198 ),
2199 );
2200 d.take_notifications();
2201 let mut descriptors = Vec::new();
2202 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2203 let cat_bytes = build_cat_fixture(&descriptors);
2204 let cat = CatSection::parse(&cat_bytes).unwrap();
2205 d.set_cat(&cat).unwrap();
2206
2207 assert!(
2208 !d.managed_ca().services().is_empty(),
2209 "precondition: a service is tracked"
2210 );
2211 assert!(
2212 !d.descramble_pids().is_empty(),
2213 "precondition: descramble_pids populated"
2214 );
2215 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
2216
2217 d.device_mut().slot.module_present = false;
2219 d.pump(Duration::from_millis(10)).unwrap();
2220 let notes = d.take_notifications();
2221 assert!(
2222 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
2223 "expected CamRemoved, got {notes:?}"
2224 );
2225
2226 assert!(
2227 d.managed_ca().services().is_empty(),
2228 "services must be cleared on CamRemoved"
2229 );
2230 assert!(
2231 d.descramble_pids().is_empty(),
2232 "descramble_pids must be cleared on CamRemoved"
2233 );
2234 assert!(
2235 d.emm_pids().is_empty(),
2236 "emm_pids must be cleared on CamRemoved"
2237 );
2238 }
2239}