1use std::collections::BTreeSet;
7use std::io;
8use std::time::{Duration, Instant};
9
10use broadcast_common::Serialize;
11use dvb_ci::builder::{build_ca_pmt, build_ca_pmt_for_caids};
12use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
13use dvb_si::tables::cat::CatSection;
14use dvb_si::tables::pmt::PmtSection;
15
16use broadcast_common::Parse;
17
18use crate::device::{CaDevice, SlotInfo};
19use crate::event::{Action, Event, HostRequest, HotPlug, MmiEvent, Notification};
20use crate::managed::{self, CaError, ManagedCa};
21use crate::stack::CiStack;
22
23const MMI_CARD_ABSENT_KEYWORDS: &[&str] = &[
28 "no card",
29 "insert card",
30 "insert smart card",
31 "card removed",
32 "please insert",
33];
34
35const MMI_CARD_PRESENT_KEYWORDS: &[&str] = &["entitlement", "card valid", "subscription active"];
40
41type Clock = Box<dyn Fn() -> Instant>;
46
47fn default_clock() -> Clock {
48 Box::new(Instant::now)
49}
50
51pub struct Driver<D: CaDevice> {
53 device: D,
54 stack: CiStack,
55 notifications: Vec<Notification>,
56 next_timer: Option<Duration>,
58 buf: Vec<u8>,
60 last_slot: Option<SlotInfo>,
67 last_caids: Option<BTreeSet<u16>>,
70 last_descrambling_ok: Option<bool>,
73 last_mmi_card_present: Option<bool>,
79 managed: ManagedCa,
82 last_pump: Option<Instant>,
87 clock: Clock,
89 requery_pending: Option<CaError>,
93}
94
95impl<D: CaDevice> Driver<D> {
96 #[must_use]
98 pub fn new(device: D) -> Self {
99 Self {
100 device,
101 stack: CiStack::new(),
102 notifications: Vec::new(),
103 next_timer: None,
104 buf: vec![0u8; 4096],
105 last_slot: None,
106 last_caids: None,
107 last_descrambling_ok: None,
108 last_mmi_card_present: None,
109 managed: ManagedCa::new(),
110 last_pump: None,
111 clock: default_clock(),
112 requery_pending: None,
113 }
114 }
115
116 #[must_use]
120 pub fn with_clock(mut self, clock: impl Fn() -> Instant + 'static) -> Self {
121 self.clock = Box::new(clock);
122 self
123 }
124
125 pub fn managed_ca(&self) -> &ManagedCa {
128 &self.managed
129 }
130
131 pub fn device(&self) -> &D {
133 &self.device
134 }
135
136 pub fn device_mut(&mut self) -> &mut D {
139 &mut self.device
140 }
141
142 pub fn next_timer(&self) -> Option<Duration> {
144 self.next_timer
145 }
146
147 pub fn take_notifications(&mut self) -> Vec<Notification> {
149 core::mem::take(&mut self.notifications)
150 }
151
152 pub fn init(&mut self) -> io::Result<()> {
154 let actions = self.stack.handle(Event::Host(HostRequest::Init));
155 self.run(actions)
156 }
157
158 pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
161 let actions = self
162 .stack
163 .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
164 self.run(actions)
165 }
166
167 pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
173 let actions = self
174 .stack
175 .handle(Event::Host(HostRequest::Descramble(pmt_section)));
176 self.run(actions)
177 }
178
179 pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
182 let actions = self
183 .stack
184 .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
185 self.run(actions)
186 }
187
188 pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
191 let actions = self
192 .stack
193 .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
194 self.run(actions)
195 }
196
197 pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
200 let actions = self
201 .stack
202 .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
203 self.run(actions)
204 }
205
206 fn build_ca_pmt_filtered(
218 &self,
219 pmt: &PmtSection<'_>,
220 list_management: CaPmtListManagement,
221 cmd_id: CaPmtCmdId,
222 ) -> dvb_ci::builder::CaPmtBuilt {
223 let cam_caids = self.managed.cam_caids();
224 if cam_caids.is_empty() {
225 build_ca_pmt(pmt, list_management, cmd_id)
226 } else {
227 let allowed: Vec<u16> = cam_caids.iter().copied().collect();
228 build_ca_pmt_for_caids(pmt, &allowed, list_management, cmd_id)
229 }
230 }
231
232 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
252 if !managed::pmt_has_ca(pmt) {
253 return Err(CaError::NoCaDescriptor {
254 program_number: pmt.program_number,
255 });
256 }
257 let list_management = if self.managed.is_empty() {
258 CaPmtListManagement::Only
259 } else {
260 CaPmtListManagement::Add
261 };
262 let cmd_id = CaPmtCmdId::OkDescrambling;
263 let built_bytes = self
270 .build_ca_pmt_filtered(pmt, list_management, cmd_id)
271 .try_to_bytes()?;
272 let mut pmt_raw = vec![0u8; pmt.serialized_len()];
280 let n = pmt.serialize_into(&mut pmt_raw)?;
281 pmt_raw.truncate(n);
282 self.send_ca_pmt(&built_bytes)?;
283 self.managed.record(
284 pmt.program_number,
285 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
286 );
287 Ok(())
288 }
289
290 pub fn remove_service(&mut self, program_number: u16) -> Result<(), CaError> {
304 let raw = self
305 .managed
306 .services()
307 .get(&program_number)
308 .map(|s| s.pmt_raw.clone());
309 let Some(raw) = raw else {
310 return Ok(());
311 };
312 self.remove_program(&raw)?;
313 self.managed.remove(program_number);
314 Ok(())
315 }
316
317 pub fn set_requery_interval(&mut self, interval: Duration) {
326 self.managed.set_requery_interval(interval);
327 }
328
329 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
344 let entries = cat.ca_descriptors().map_err(CaError::Cat)?;
345 self.managed.set_cat(&entries);
346 Ok(())
347 }
348
349 #[must_use]
353 pub fn emm_pids(&self) -> &[u16] {
354 self.managed.emm_pids()
355 }
356
357 #[must_use]
360 pub fn descramble_pids(&self) -> &[u16] {
361 self.managed.descramble_pids()
362 }
363
364 #[must_use]
369 pub fn ca_pids(&self) -> &[u16] {
370 self.managed.ca_pids()
371 }
372
373 #[must_use]
381 pub fn required_pids(&self) -> Vec<u16> {
382 self.managed.required_pids()
383 }
384
385 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
387 let actions = self
388 .stack
389 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
390 self.run(actions)
391 }
392
393 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
395 let actions = self
396 .stack
397 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
398 self.run(actions)
399 }
400
401 pub fn mmi_cancel(&mut self) -> io::Result<()> {
403 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
404 self.run(actions)
405 }
406
407 pub fn enter_menu(&mut self) -> io::Result<()> {
410 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
411 self.run(actions)
412 }
413
414 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
423 let now = (self.clock)();
434 let elapsed = self
435 .last_pump
436 .map_or(timeout, |prev| now.saturating_duration_since(prev));
437 self.last_pump = Some(now);
438
439 self.run(vec![Action::QuerySlot])?;
440 if self.device.poll(timeout)? {
441 let n = self.device.read(&mut self.buf)?;
442 if n > 0 {
443 let actions = self.stack.handle(Event::Readable(&self.buf[..n]));
447 self.run(actions)?;
448 return Ok(true);
449 }
450 }
451 let actions = self.stack.handle(Event::Tick { elapsed });
452 self.run(actions)?;
453 self.requery_tick(elapsed)?;
454 Ok(false)
455 }
456
457 fn requery_tick(&mut self, elapsed: Duration) -> io::Result<()> {
501 if let Some(err) = self.requery_pending.take() {
505 return Err(io::Error::other(err));
506 }
507 if !self.managed.tick(elapsed) {
508 return Ok(());
509 }
510 let mut ca_pmts: Vec<(Vec<u8>, Vec<u8>)> =
511 Vec::with_capacity(self.managed.services().len());
512 for s in self.managed.services().values() {
513 let outcome: Result<(Vec<u8>, Vec<u8>), CaError> = (|| {
518 let pmt = PmtSection::parse(&s.pmt_raw).map_err(CaError::PmtParse)?;
519 let query = self
520 .build_ca_pmt_filtered(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query)
521 .try_to_bytes()?;
522 let ok_descrambling = self
523 .build_ca_pmt_filtered(
524 &pmt,
525 CaPmtListManagement::Update,
526 CaPmtCmdId::OkDescrambling,
527 )
528 .try_to_bytes()?;
529 Ok((query, ok_descrambling))
530 })();
531 match outcome {
532 Ok(pair) => ca_pmts.push(pair),
533 Err(err) => {
534 self.requery_pending = Some(err);
538 }
539 }
540 }
541 for (query, ok_descrambling) in ca_pmts {
542 self.send_ca_pmt(&query)?;
543 self.send_ca_pmt(&ok_descrambling)?;
544 }
545 if let Some(err) = self.requery_pending.take() {
546 return Err(io::Error::other(err));
547 }
548 Ok(())
549 }
550
551 pub fn pump_with<F: FnMut(&Notification)>(
560 &mut self,
561 timeout: Duration,
562 mut handler: F,
563 ) -> io::Result<bool> {
564 let progressed = self.pump(timeout)?;
565 for n in self.take_notifications() {
566 handler(&n);
567 }
568 Ok(progressed)
569 }
570
571 pub fn pump_hotplug<F: FnMut(HotPlug)>(
575 &mut self,
576 timeout: Duration,
577 mut handler: F,
578 ) -> io::Result<bool> {
579 self.pump_with(timeout, |n| {
580 if let Some(h) = n.hotplug() {
581 handler(h);
582 }
583 })
584 }
585
586 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
588 for action in actions {
589 match action {
590 Action::Write(bytes) => self.device.write(&bytes)?,
591 Action::Reset => self.device.reset()?,
592 Action::QuerySlot => {
593 let info = self.device.slot_info()?;
594 self.handle_slot_info(info)?;
595 }
596 Action::SetTimer { after } => self.next_timer = Some(after),
597 Action::Notify(n) => {
598 let inferred = self.infer_card(&n);
599 self.notifications.push(n);
600 self.notifications.extend(inferred);
601 }
602 }
603 }
604 Ok(())
605 }
606
607 fn handle_slot_info(&mut self, info: SlotInfo) -> io::Result<()> {
614 let prev = self.last_slot.replace(info);
615 match prev {
616 Some(prev) if !prev.module_present && info.module_present => {
617 self.notifications
618 .push(Notification::HotPlug(HotPlug::CamPresent));
619 self.reset_module_state();
620 let actions = self.stack.handle(Event::Host(HostRequest::Init));
624 self.run(actions)?;
625 }
626 Some(prev) if prev.module_present && !info.module_present => {
627 self.notifications
628 .push(Notification::HotPlug(HotPlug::CamRemoved));
629 self.reset_module_state();
630 }
631 _ => {}
632 }
633 Ok(())
634 }
635
636 fn reset_module_state(&mut self) {
646 self.stack = CiStack::new();
647 self.next_timer = None;
648 self.last_caids = None;
649 self.last_descrambling_ok = None;
650 self.last_mmi_card_present = None;
651 self.managed.clear();
652 }
653
654 fn infer_card(&mut self, note: &Notification) -> Vec<Notification> {
661 match note {
662 Notification::CaInfo { ca_system_ids } => {
663 let new_set: BTreeSet<u16> = ca_system_ids.iter().copied().collect();
664 let mut out = Vec::new();
665 if let Some(prev) = &self.last_caids {
666 if prev.is_empty() && !new_set.is_empty() {
667 out.push(Notification::HotPlug(HotPlug::CardInserted));
668 } else if !prev.is_empty() && new_set.is_empty() {
669 out.push(Notification::HotPlug(HotPlug::CardRemoved));
670 } else if !prev.is_empty() && !new_set.is_empty() && *prev != new_set {
671 out.push(Notification::HotPlug(HotPlug::CardChanged));
672 }
673 }
674 self.managed.set_cam_caids(new_set.clone());
678 self.last_caids = Some(new_set);
679 out
680 }
681 Notification::CaPmtReply {
682 program_number,
683 ca_enable,
684 descrambling_ok,
685 } => {
686 let mut out = Vec::new();
687 if let Some(prev) = self.last_descrambling_ok {
688 if !prev && *descrambling_ok {
689 out.push(Notification::HotPlug(HotPlug::CardInserted));
690 } else if prev && !*descrambling_ok {
691 out.push(Notification::HotPlug(HotPlug::CardRemoved));
692 }
693 }
694 self.last_descrambling_ok = Some(*descrambling_ok);
695 if let Some((v, ok)) =
699 self.managed
700 .record_reply(*program_number, *ca_enable, *descrambling_ok)
701 {
702 out.push(Notification::Entitlement {
703 program_number: *program_number,
704 ca_enable: v,
705 descrambling_ok: ok,
706 });
707 }
708 out
709 }
710 Notification::Mmi(ev) => {
711 let present = Self::mmi_text(ev).and_then(|text| {
712 let lower = text.to_lowercase();
713 if MMI_CARD_ABSENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
714 Some(false)
715 } else if MMI_CARD_PRESENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
716 Some(true)
717 } else {
718 None
719 }
720 });
721 match present {
722 Some(p) if self.last_mmi_card_present != Some(p) => {
724 self.last_mmi_card_present = Some(p);
725 vec![Notification::HotPlug(if p {
726 HotPlug::CardInserted
727 } else {
728 HotPlug::CardRemoved
729 })]
730 }
731 _ => Vec::new(),
732 }
733 }
734 _ => Vec::new(),
735 }
736 }
737
738 fn mmi_text(ev: &MmiEvent) -> Option<String> {
742 match ev {
743 MmiEvent::Menu(m) | MmiEvent::List(m) => {
744 let mut s = format!("{} {} {}", m.title, m.subtitle, m.bottom);
745 for choice in &m.choices {
746 s.push(' ');
747 s.push_str(choice);
748 }
749 Some(s)
750 }
751 MmiEvent::Enquiry { prompt, .. } => Some(prompt.clone()),
752 MmiEvent::Close => None,
753 }
754 }
755}
756
757#[cfg(test)]
758pub(crate) mod tests {
759 use super::*;
760 use crate::device::{DeviceOp, MockCaDevice};
761 use crate::event::{HostControlEvent, HotPlug, Notification};
762 use broadcast_common::Serialize;
763 use dvb_ci::tpdu::tags;
764
765 pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
766 let mut b = vec![0u8; s.serialized_len()];
767 match s.serialize_into(&mut b) {
768 Ok(n) => b.truncate(n),
769 Err(_) => b.clear(),
770 }
771 b
772 }
773
774 #[derive(Clone)]
780 pub(crate) struct TestClock(std::rc::Rc<std::cell::Cell<Instant>>);
781
782 impl TestClock {
783 pub(crate) fn new() -> Self {
784 Self(std::rc::Rc::new(std::cell::Cell::new(Instant::now())))
785 }
786
787 pub(crate) fn advance(&self, d: Duration) {
789 self.0.set(self.0.get() + d);
790 }
791
792 pub(crate) fn as_fn(&self) -> impl Fn() -> Instant + 'static {
794 let cell = self.0.clone();
795 move || cell.get()
796 }
797 }
798
799 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
802 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
803 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
804 v.extend_from_slice(spdu);
805 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
806 v
807 }
808
809 pub(crate) fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
812 use dvb_ci::spdu::SessionNumber;
813 let mut spdu = ser(&SessionNumber { session_nb });
814 spdu.extend_from_slice(apdu);
815 r_data(1, &spdu)
816 }
817
818 pub(crate) fn sb() -> Vec<u8> {
821 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
822 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
823 }
824
825 pub(crate) fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
828 d.device_mut().inbound.push_back(frame);
829 d.pump(Duration::from_millis(10)).unwrap();
830 for _ in 0..8 {
831 d.device_mut().inbound.push_back(sb());
832 d.pump(Duration::from_millis(10)).unwrap();
833 }
834 }
835
836 pub(crate) fn driver_with_sessions() -> Driver<MockCaDevice> {
840 use dvb_ci::objects::resource_manager::Profile;
841 use dvb_ci::resource::{
842 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
843 RESOURCE_MANAGER,
844 };
845 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
846
847 let mut d = Driver::new(MockCaDevice::new([]));
848 d.init().unwrap();
849 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
851 feed(
853 &mut d,
854 r_data(
855 1,
856 &ser(&OpenSessionRequest {
857 resource: RESOURCE_MANAGER,
858 }),
859 ),
860 );
861 feed(
864 &mut d,
865 r_apdu(
866 1,
867 &ser(&Profile {
868 resources: vec![
869 APPLICATION_INFORMATION,
870 CONDITIONAL_ACCESS_SUPPORT,
871 MMI,
872 HOST_CONTROL,
873 ],
874 }),
875 ),
876 );
877 for (nb, res) in [
879 (2u16, APPLICATION_INFORMATION),
880 (3, CONDITIONAL_ACCESS_SUPPORT),
881 (4, MMI),
882 (5, HOST_CONTROL),
883 ] {
884 feed(
885 &mut d,
886 r_data(
887 1,
888 &ser(&CreateSessionResponse {
889 status: SessionStatus::Ok,
890 resource: res,
891 session_nb: nb,
892 }),
893 ),
894 );
895 }
896 d
897 }
898
899 const RM_SESSION: u16 = 1;
903 pub(crate) const CA_SESSION: u16 = 3;
904 const MMI_SESSION: u16 = 4;
905 const HOST_CONTROL_SESSION: u16 = 5;
906
907 #[test]
908 fn host_control_tune_apdu_surfaces_notification_via_driver() {
909 use dvb_ci::objects::host_control::Tune;
910
911 let mut d = driver_with_sessions();
912 let hc_nb = HOST_CONTROL_SESSION;
913 d.take_notifications(); let tune = Tune {
917 network_id: 0x1122,
918 original_network_id: 0x3344,
919 transport_stream_id: 0x5566,
920 service_id: 0x7788,
921 };
922 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
923
924 let notes = d.take_notifications();
926 assert!(
927 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
928 network_id: 0x1122,
929 original_network_id: 0x3344,
930 transport_stream_id: 0x5566,
931 service_id: 0x7788,
932 })),
933 "expected HostControl(Tune) notification, got {notes:?}"
934 );
935 }
936
937 #[test]
940 fn padded_tune_from_the_cam_surfaces_an_error_notification() {
941 let mut d = driver_with_sessions();
942 d.take_notifications();
943 let padded = [
945 0x9F, 0x84, 0x00, 0x09, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xEE,
946 ];
947 feed(&mut d, r_apdu(HOST_CONTROL_SESSION, &padded));
948 let notes = d.take_notifications();
949 assert!(
950 notes.iter().any(
951 |n| matches!(n, Notification::Error { detail } if detail.contains("trailing bytes"))
952 ),
953 "expected an Error notification naming the trailing bytes, got {notes:?}"
954 );
955 assert!(
956 !notes
957 .iter()
958 .any(|n| matches!(n, Notification::HostControl(_))),
959 "a refused tune must not also surface as a HostControl event: {notes:?}"
960 );
961 }
962
963 #[test]
966 fn malformed_mmi_apdu_from_the_cam_surfaces_an_error_notification() {
967 let mut d = driver_with_sessions();
968 d.take_notifications();
969 feed(&mut d, r_apdu(MMI_SESSION, &[0x9F, 0x88, 0x07, 0x01, 0x00]));
971 let notes = d.take_notifications();
972 assert!(
973 notes
974 .iter()
975 .any(|n| matches!(n, Notification::Error { .. })),
976 "expected an Error notification, got {notes:?}"
977 );
978 }
979
980 #[test]
981 fn profile_reply_advertises_host_control() {
982 use broadcast_common::Parse;
983 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
984 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
985
986 let mut d = Driver::new(MockCaDevice::new([]));
987 d.init().unwrap();
988 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
989 feed(
991 &mut d,
992 r_data(
993 1,
994 &ser(&dvb_ci::spdu::OpenSessionRequest {
995 resource: RESOURCE_MANAGER,
996 }),
997 ),
998 );
999 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq)));
1001
1002 let want = dvb_ci::tag::PROFILE.to_bytes();
1005 let found = d.device().ops.iter().any(|op| {
1006 if let DeviceOp::Write(w) = op
1007 && let Some(pos) = w.windows(3).position(|x| x == want)
1008 && let Ok(p) = Profile::parse(&w[pos..])
1009 {
1010 return p.resources.contains(&HOST_CONTROL);
1011 }
1012 false
1013 });
1014 assert!(found, "profile reply must advertise HOST_CONTROL");
1015 }
1016
1017 #[test]
1018 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
1019 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
1020
1021 let mut d = driver_with_sessions();
1022 let mmi_nb = MMI_SESSION;
1023
1024 d.mmi_menu_answer(2).unwrap();
1027 d.device_mut().inbound.push_back(sb());
1028 d.pump(Duration::from_millis(10)).unwrap();
1029 assert_apdu_on_session(&d, mmi_nb, &ser(&MenuAnsw { choice_ref: 2 }));
1030
1031 d.mmi_enquiry_answer(b"1234").unwrap();
1033 d.device_mut().inbound.push_back(sb());
1034 d.pump(Duration::from_millis(10)).unwrap();
1035 assert_apdu_on_session(
1036 &d,
1037 mmi_nb,
1038 &ser(&Answ {
1039 answ_id: AnswId::Answer,
1040 text_chars: b"1234",
1041 }),
1042 );
1043 }
1044
1045 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
1048 use dvb_ci::spdu::SessionNumber;
1049 let mut want = ser(&SessionNumber { session_nb });
1050 want.extend_from_slice(apdu);
1051 let hit = d.device().ops.iter().any(|op| match op {
1052 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1053 _ => false,
1054 });
1055 assert!(
1056 hit,
1057 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
1058 );
1059 }
1060
1061 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
1065 use dvb_ci::spdu::SessionNumber;
1066 let mut want = ser(&SessionNumber { session_nb });
1067 want.extend_from_slice(apdu);
1068 d.device()
1069 .ops
1070 .iter()
1071 .filter(|op| match op {
1072 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1073 _ => false,
1074 })
1075 .count()
1076 }
1077
1078 #[test]
1079 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
1080 let mut d = Driver::new(MockCaDevice::new([]));
1081 d.init().unwrap();
1082 let ops = &d.device().ops;
1083 assert_eq!(ops[0], DeviceOp::Reset);
1084 assert_eq!(ops[1], DeviceOp::SlotInfo);
1085 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
1086 }
1087
1088 #[test]
1089 fn reads_reply_then_polls_on_pump() {
1090 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1092 let clock = TestClock::new();
1093 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1094 d.init().unwrap();
1095 assert!(d.pump(Duration::from_millis(100)).unwrap());
1097 clock.advance(Duration::from_millis(100));
1102 assert!(!d.pump(Duration::from_millis(100)).unwrap());
1104 let last = d.device().ops.last().unwrap();
1105 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1106 }
1107
1108 #[test]
1114 fn pump_advances_the_stack_clock_by_real_elapsed_time_not_by_timeout() {
1115 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1116 let clock = TestClock::new();
1117 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1118 d.init().unwrap();
1119 assert!(d.pump(Duration::from_secs(1)).unwrap());
1120
1121 clock.advance(Duration::from_millis(1));
1125 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1126 assert!(
1127 d.device().ops.last().is_none_or(|op| !matches!(
1128 op,
1129 DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)
1130 )),
1131 "must not poll after only 1ms of real elapsed time, regardless of the 1s `timeout` argument"
1132 );
1133
1134 clock.advance(Duration::from_millis(100));
1136 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1137 let last = d.device().ops.last().unwrap();
1138 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1139 }
1140
1141 #[test]
1144 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
1145 let mut dev = MockCaDevice::new([]);
1146 dev.slot = SlotInfo {
1147 num: 0,
1148 module_ready: false,
1149 module_present: false,
1150 };
1151 let mut d = Driver::new(dev);
1152 d.init().unwrap();
1153 let notes = d.take_notifications();
1156 assert!(
1157 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1158 "baseline observation must not fire CamPresent, got {notes:?}"
1159 );
1160 let resets_before = d
1161 .device()
1162 .ops
1163 .iter()
1164 .filter(|o| **o == DeviceOp::Reset)
1165 .count();
1166
1167 d.device_mut().slot = SlotInfo {
1169 num: 0,
1170 module_ready: true,
1171 module_present: true,
1172 };
1173 d.pump(Duration::from_millis(10)).unwrap();
1174
1175 let notes = d.take_notifications();
1176 let cam_present_count = notes
1177 .iter()
1178 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
1179 .count();
1180 assert_eq!(
1181 cam_present_count, 1,
1182 "expected exactly one CamPresent, got {notes:?}"
1183 );
1184 let resets_after = d
1186 .device()
1187 .ops
1188 .iter()
1189 .filter(|o| **o == DeviceOp::Reset)
1190 .count();
1191 assert_eq!(
1192 resets_after,
1193 resets_before + 1,
1194 "expected one fresh Reset on re-insert"
1195 );
1196 assert!(
1197 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
1198 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
1199 d.device().ops.last()
1200 );
1201 }
1202
1203 #[test]
1204 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
1205 let mut d = driver_with_sessions();
1206 d.take_notifications();
1207
1208 d.device_mut().slot.module_present = false;
1210 d.pump(Duration::from_millis(10)).unwrap();
1211 let notes = d.take_notifications();
1212 assert!(
1213 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
1214 "expected CamRemoved, got {notes:?}"
1215 );
1216
1217 d.mmi_menu_answer(0).unwrap();
1221 let notes = d.take_notifications();
1222 assert!(
1223 notes
1224 .iter()
1225 .any(|n| matches!(n, Notification::Error { .. })),
1226 "expected no open MMI session after teardown, got {notes:?}"
1227 );
1228
1229 let resets_before = d
1231 .device()
1232 .ops
1233 .iter()
1234 .filter(|o| **o == DeviceOp::Reset)
1235 .count();
1236 d.device_mut().slot.module_present = true;
1237 d.device_mut().slot.module_ready = true;
1238 d.pump(Duration::from_millis(10)).unwrap();
1239 let notes = d.take_notifications();
1240 assert!(
1241 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1242 "expected CamPresent on re-insert, got {notes:?}"
1243 );
1244 let resets_after = d
1245 .device()
1246 .ops
1247 .iter()
1248 .filter(|o| **o == DeviceOp::Reset)
1249 .count();
1250 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1251 }
1252
1253 #[test]
1254 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1255 let mut d = Driver::new(MockCaDevice::new([]));
1256 d.init().unwrap();
1257 d.take_notifications();
1258
1259 for _ in 0..5 {
1260 d.pump(Duration::from_millis(10)).unwrap();
1261 }
1262 let notes = d.take_notifications();
1263 assert!(
1264 !notes.iter().any(|n| matches!(
1265 n,
1266 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1267 )),
1268 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1269 );
1270 }
1271
1272 #[test]
1273 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1274 use dvb_ci::objects::ca_info::CaInfo;
1275
1276 let mut d = driver_with_sessions();
1277 d.take_notifications();
1278
1279 feed(
1281 &mut d,
1282 r_apdu(
1283 CA_SESSION,
1284 &ser(&CaInfo {
1285 ca_system_ids: vec![],
1286 }),
1287 ),
1288 );
1289 let notes = d.take_notifications();
1290 assert!(
1291 !notes.iter().any(|n| matches!(
1292 n,
1293 Notification::HotPlug(
1294 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1295 )
1296 )),
1297 "first ca_info must only establish the baseline, got {notes:?}"
1298 );
1299
1300 feed(
1302 &mut d,
1303 r_apdu(
1304 CA_SESSION,
1305 &ser(&CaInfo {
1306 ca_system_ids: vec![0x0B00],
1307 }),
1308 ),
1309 );
1310 let notes = d.take_notifications();
1311 assert!(
1312 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1313 "expected CardInserted, got {notes:?}"
1314 );
1315
1316 feed(
1318 &mut d,
1319 r_apdu(
1320 CA_SESSION,
1321 &ser(&CaInfo {
1322 ca_system_ids: vec![0x1800],
1323 }),
1324 ),
1325 );
1326 let notes = d.take_notifications();
1327 assert!(
1328 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1329 "expected CardChanged, got {notes:?}"
1330 );
1331 }
1332
1333 #[test]
1334 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1335 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1336
1337 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1338 CaPmtReply {
1339 program_number: 1,
1340 version_number: 1,
1341 current_next_indicator: true,
1342 ca_enable,
1343 streams: vec![],
1344 }
1345 }
1346
1347 let mut d = driver_with_sessions();
1348 d.take_notifications();
1349
1350 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1352 let notes = d.take_notifications();
1353 assert!(
1354 !notes.iter().any(|n| matches!(
1355 n,
1356 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1357 )),
1358 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1359 );
1360
1361 feed(
1363 &mut d,
1364 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1365 );
1366 let notes = d.take_notifications();
1367 assert!(
1368 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1369 "expected CardInserted, got {notes:?}"
1370 );
1371
1372 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1374 let notes = d.take_notifications();
1375 assert!(
1376 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1377 "expected CardRemoved, got {notes:?}"
1378 );
1379 }
1380
1381 #[test]
1382 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1383 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1384
1385 let mut d = driver_with_sessions();
1386 d.take_notifications();
1387
1388 feed(
1391 &mut d,
1392 r_apdu(
1393 CA_SESSION,
1394 &ser(&CaPmtReply {
1395 program_number: 7,
1396 version_number: 1,
1397 current_next_indicator: true,
1398 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1399 streams: vec![],
1400 }),
1401 ),
1402 );
1403 let notes = d.take_notifications();
1404 assert!(
1405 notes.contains(&Notification::CaPmtReply {
1406 program_number: 7,
1407 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1408 descrambling_ok: true,
1409 }),
1410 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1411 );
1412 }
1413
1414 #[test]
1415 fn ca_pmt_reply_flag_clear_surfaces_none() {
1416 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1417
1418 let mut d = driver_with_sessions();
1419 d.take_notifications();
1420
1421 feed(
1424 &mut d,
1425 r_apdu(
1426 CA_SESSION,
1427 &ser(&CaPmtReply {
1428 program_number: 7,
1429 version_number: 1,
1430 current_next_indicator: true,
1431 ca_enable: None,
1432 streams: vec![],
1433 }),
1434 ),
1435 );
1436 let notes = d.take_notifications();
1437 assert!(
1438 notes.contains(&Notification::CaPmtReply {
1439 program_number: 7,
1440 ca_enable: None,
1441 descrambling_ok: false,
1442 }),
1443 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1444 );
1445 }
1446
1447 #[test]
1448 fn mmi_no_card_text_infers_card_removed() {
1449 use dvb_ci::objects::mmi_high::Enq;
1450
1451 let mut d = driver_with_sessions();
1452 d.take_notifications();
1453
1454 feed(
1455 &mut d,
1456 r_apdu(
1457 MMI_SESSION,
1458 &ser(&Enq {
1459 blind_answer: false,
1460 answer_text_length: 0,
1461 text_chars: b"NO CARD detected - please insert your smart card",
1462 }),
1463 ),
1464 );
1465
1466 let notes = d.take_notifications();
1467 assert!(
1468 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1469 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1470 );
1471 }
1472
1473 #[test]
1476 fn mmi_card_keyword_heuristic_is_edge_triggered() {
1477 use dvb_ci::objects::mmi_high::Enq;
1478
1479 let enq = |text: &'static [u8]| {
1480 r_apdu(
1481 MMI_SESSION,
1482 &ser(&Enq {
1483 blind_answer: false,
1484 answer_text_length: 0,
1485 text_chars: text,
1486 }),
1487 )
1488 };
1489 let mut d = driver_with_sessions();
1490 d.take_notifications();
1491 let count = |notes: &[Notification], hp: HotPlug| {
1492 notes
1493 .iter()
1494 .filter(|n| **n == Notification::HotPlug(hp))
1495 .count()
1496 };
1497
1498 for _ in 0..3 {
1500 feed(&mut d, enq(b"No card - please insert your smart card"));
1501 }
1502 let notes = d.take_notifications();
1503 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1504 assert_eq!(count(¬es, HotPlug::CardInserted), 0);
1505
1506 for _ in 0..2 {
1508 feed(&mut d, enq(b"Your entitlement is valid"));
1509 }
1510 let notes = d.take_notifications();
1511 assert_eq!(count(¬es, HotPlug::CardInserted), 1, "{notes:?}");
1512 assert_eq!(count(¬es, HotPlug::CardRemoved), 0);
1513
1514 feed(&mut d, enq(b"Card removed"));
1516 feed(&mut d, enq(b"Card removed"));
1517 let notes = d.take_notifications();
1518 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1519 }
1520
1521 #[test]
1522 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1523 let mut dev = MockCaDevice::new([]);
1524 dev.slot = SlotInfo {
1525 num: 0,
1526 module_ready: false,
1527 module_present: false,
1528 };
1529 let mut d = Driver::new(dev);
1530 d.init().unwrap();
1531 d.take_notifications(); d.device_mut().slot = SlotInfo {
1535 num: 0,
1536 module_ready: true,
1537 module_present: true,
1538 };
1539
1540 let mut seen = Vec::new();
1541 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1542 .unwrap();
1543
1544 assert_eq!(
1545 seen,
1546 vec![HotPlug::CamPresent],
1547 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1548 );
1549 }
1550
1551 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
1556 [
1557 0x09,
1558 0x04,
1559 (ca_system_id >> 8) as u8,
1560 ca_system_id as u8,
1561 0xE0 | ((pid >> 8) as u8 & 0x1F),
1562 pid as u8,
1563 ]
1564 }
1565
1566 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
1583 const VIACCESS: u16 = 0x0500;
1584 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1585 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1586
1587 let mut body = Vec::new();
1588 body.push(0x02); body.push(0); body.push(0);
1591 body.extend_from_slice(&program_number.to_be_bytes());
1592 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
1597 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1598 body.push(prog_ca.len() as u8);
1599 body.extend_from_slice(&prog_ca);
1600 body.push(0x1B);
1602 body.push(0xE0 | 0x01);
1603 body.push(0x00);
1604 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1605 body.push(es0_ca.len() as u8);
1606 body.extend_from_slice(&es0_ca);
1607 body.push(0x0F);
1609 body.push(0xE0 | 0x01);
1610 body.push(0x01);
1611 body.push(0xF0);
1612 body.push(0x00);
1613
1614 let section_length = body.len() - 3 + 4;
1615 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1616 body[2] = section_length as u8;
1617 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1618 body.extend_from_slice(&crc.to_be_bytes());
1619 body
1620 }
1621
1622 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
1629 const VIACCESS: u16 = 0x0500;
1630 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1631 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1632
1633 let mut body = Vec::new();
1634 body.push(0x02); body.push(0); body.push(0);
1637 body.extend_from_slice(&program_number.to_be_bytes());
1638 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));
1644 body.push(prog_ca.len() as u8);
1645 body.extend_from_slice(&prog_ca);
1646 body.push(0x1B);
1648 body.push(0xE0 | 0x01);
1649 body.push(0x00);
1650 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1651 body.push(es0_ca.len() as u8);
1652 body.extend_from_slice(&es0_ca);
1653 body.push(0x0F);
1655 body.push(0xE0 | 0x01);
1656 body.push(0x01);
1657 body.push(0xF0);
1658 body.push(0x00);
1659
1660 let section_length = body.len() - 3 + 4;
1661 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1662 body[2] = section_length as u8;
1663 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1664 body.extend_from_slice(&crc.to_be_bytes());
1665 body
1666 }
1667
1668 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
1672 let mut body = Vec::new();
1673 body.push(0x02);
1674 body.push(0);
1675 body.push(0);
1676 body.extend_from_slice(&program_number.to_be_bytes());
1677 body.push(0xC3);
1678 body.push(0x00);
1679 body.push(0x00);
1680 body.push(0xE0 | 0x01);
1681 body.push(0x00);
1682 body.push(0xF0); body.push(0x00);
1684 body.push(0x1B);
1686 body.push(0xE0 | 0x01);
1687 body.push(0x00);
1688 body.push(0xF0);
1689 body.push(0x00);
1690
1691 let section_length = body.len() - 3 + 4;
1692 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1693 body[2] = section_length as u8;
1694 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1695 body.extend_from_slice(&crc.to_be_bytes());
1696 body
1697 }
1698
1699 #[test]
1700 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
1701 use broadcast_common::Parse;
1702
1703 let mut d = driver_with_sessions();
1704 d.take_notifications();
1705
1706 let pmt_bytes = build_ca_pmt_fixture(1546);
1707 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1708
1709 d.add_service(&pmt).unwrap();
1710 d.device_mut().inbound.push_back(sb());
1711 d.pump(Duration::from_millis(10)).unwrap();
1712
1713 let expected =
1717 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
1718 assert_apdu_on_session(&d, CA_SESSION, &expected);
1719
1720 let svc = d
1722 .managed_ca()
1723 .services()
1724 .get(&1546)
1725 .expect("program_number 1546 must be tracked after add_service");
1726 assert_eq!(svc.es_pids, vec![0x0100, 0x0101]);
1727 assert_eq!(svc.ca_pids, vec![0x0064, 0x0065]);
1728 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
1729 assert_eq!(svc.last_ca_enable, None);
1730 }
1731
1732 #[test]
1733 fn add_service_rejects_pmt_without_ca_descriptor() {
1734 use broadcast_common::Parse;
1735
1736 let mut d = driver_with_sessions();
1737 let pmt_bytes = build_clear_pmt_fixture(999);
1738 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1739
1740 let err = d.add_service(&pmt).unwrap_err();
1741 assert!(
1742 matches!(
1743 err,
1744 CaError::NoCaDescriptor {
1745 program_number: 999
1746 }
1747 ),
1748 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
1749 );
1750 assert!(
1751 d.managed_ca().services().is_empty(),
1752 "a rejected PMT must not be recorded"
1753 );
1754 }
1755
1756 #[test]
1768 fn requery_tick_serialize_failure_errors_from_pump_instead_of_panicking() {
1769 use broadcast_common::Parse;
1770
1771 let clock = TestClock::new();
1772 let mut d = driver_with_sessions().with_clock(clock.as_fn());
1773
1774 let good_bytes = build_ca_pmt_fixture(1);
1777 let good = PmtSection::parse(&good_bytes).unwrap();
1778 assert_eq!(good.program_number, 1, "fixture precondition");
1779 d.add_service(&good).unwrap();
1780
1781 let pmt_bytes = build_ca_pmt_fixture(1546);
1786 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1787 let built = d
1788 .build_ca_pmt_filtered(
1789 &pmt,
1790 CaPmtListManagement::Update,
1791 CaPmtCmdId::OkDescrambling,
1792 )
1793 .try_to_bytes()
1794 .expect("fixture precondition: the good PMT encodes");
1795 let corrupt_raw = pmt_bytes[..pmt_bytes.len() - 6].to_vec();
1796 d.managed.record(
1797 1546,
1798 managed::service_of(&pmt, CaPmtCmdId::OkDescrambling, built, corrupt_raw),
1799 );
1800 d.take_notifications();
1801 for _ in 0..16 {
1805 d.device_mut().inbound.push_back(sb());
1806 d.pump(Duration::from_millis(10)).unwrap();
1807 }
1808 d.set_requery_interval(Duration::from_secs(5));
1809
1810 clock.advance(Duration::from_secs(5));
1814 let err = d
1815 .pump(Duration::ZERO)
1816 .expect_err("re-query of a corrupt stored PMT must error, not panic");
1817 assert!(
1818 matches!(
1819 err.get_ref(),
1820 Some(e) if e.downcast_ref::<CaError>()
1821 .is_some_and(|ca| matches!(ca, CaError::PmtParse(_)))
1822 ),
1823 "expected CaError::PmtParse from the corrupt stored bytes, got {err:?}"
1824 );
1825 assert!(
1826 err.to_string().contains("stored PMT re-parse failed"),
1827 "error must name the re-parse failure, got: {err}"
1828 );
1829 assert!(d.pump(Duration::ZERO).is_ok());
1833 clock.advance(Duration::from_secs(5));
1834 let err2 = d
1835 .pump(Duration::ZERO)
1836 .expect_err("the corrupt service must fail every re-query");
1837 assert!(
1838 err2.to_string().contains("stored PMT re-parse failed"),
1839 "failure must persist, got: {err2}"
1840 );
1841 }
1842
1843 fn corrupt_ca_section_ctor() -> PmtSection<'static> {
1849 const LOOP: usize = 5000;
1850 let mut prog_info: Vec<u8> = Vec::with_capacity(LOOP);
1851 for _ in 0..LOOP / 4 {
1852 prog_info.extend_from_slice(&[0x09u8, 0x02, 0x0B, 0x00]);
1854 }
1855 assert_eq!(prog_info.len(), LOOP);
1856 PmtSection::new(
1857 1,
1858 1,
1859 true,
1860 0,
1861 0,
1862 0x0200,
1863 dvb_si::descriptors::DescriptorLoop::new(Box::leak(prog_info.into_boxed_slice())),
1864 Vec::new(),
1865 )
1866 }
1867
1868 #[test]
1873 fn add_service_serialize_failure_returns_err_instead_of_panicking() {
1874 let mut d = driver_with_sessions();
1875 d.managed.set_cam_caids([0x0B00u16].into_iter().collect());
1876 let pmt = corrupt_ca_section_ctor();
1877 let err = d
1878 .add_service(&pmt)
1879 .expect_err("a ca_pmt with no valid encoding must error, not panic");
1880 assert!(
1881 matches!(err, CaError::Serialize(_)),
1882 "expected CaError::Serialize, got {err:?}"
1883 );
1884 assert!(
1885 d.managed_ca().services().is_empty(),
1886 "a rejected PMT must not be recorded"
1887 );
1888 }
1889
1890 #[test]
1891 fn add_service_second_call_uses_add_list_management() {
1892 use broadcast_common::Parse;
1893
1894 let mut d = driver_with_sessions();
1895 d.take_notifications();
1896
1897 let pmt1_bytes = build_ca_pmt_fixture(1546);
1898 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
1899 d.add_service(&pmt1).unwrap();
1900 d.device_mut().inbound.push_back(sb());
1901 d.pump(Duration::from_millis(10)).unwrap();
1902
1903 let pmt2_bytes = build_ca_pmt_fixture(1547);
1904 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
1905 d.add_service(&pmt2).unwrap();
1906 d.device_mut().inbound.push_back(sb());
1907 d.pump(Duration::from_millis(10)).unwrap();
1908
1909 let expected2 =
1911 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
1912 assert_apdu_on_session(&d, CA_SESSION, &expected2);
1913
1914 assert_eq!(d.managed_ca().services().len(), 2);
1915 }
1916
1917 fn build_ca_pmt_fixture_two_caids(program_number: u16) -> Vec<u8> {
1928 const VIACCESS: u16 = 0x0500;
1929 const NAGRA: u16 = 0x1800;
1930 let prog_ca_a = ca_descriptor(VIACCESS, 0x0064);
1931 let prog_ca_b = ca_descriptor(NAGRA, 0x0066);
1932 let mut prog_ca = Vec::new();
1933 prog_ca.extend_from_slice(&prog_ca_a);
1934 prog_ca.extend_from_slice(&prog_ca_b);
1935
1936 let mut body = Vec::new();
1937 body.push(0x02); body.push(0);
1939 body.push(0);
1940 body.extend_from_slice(&program_number.to_be_bytes());
1941 body.push(0xC3);
1942 body.push(0x00);
1943 body.push(0x00);
1944 body.push(0xE0 | 0x01); body.push(0x00);
1946 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1947 body.push(prog_ca.len() as u8);
1948 body.extend_from_slice(&prog_ca);
1949 body.push(0x0F);
1952 body.push(0xE0 | 0x01);
1953 body.push(0x00);
1954 body.push(0xF0);
1955 body.push(0x00);
1956
1957 let section_length = body.len() - 3 + 4;
1958 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1959 body[2] = section_length as u8;
1960 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1961 body.extend_from_slice(&crc.to_be_bytes());
1962 body
1963 }
1964
1965 #[test]
1966 fn add_service_filters_ca_descriptors_to_cam_advertised_caids() {
1967 use broadcast_common::Parse;
1968 use dvb_ci::objects::ca_info::CaInfo;
1969
1970 let mut d = driver_with_sessions();
1971 d.take_notifications();
1972
1973 feed(
1976 &mut d,
1977 r_apdu(
1978 CA_SESSION,
1979 &ser(&CaInfo {
1980 ca_system_ids: vec![0x0500],
1981 }),
1982 ),
1983 );
1984 d.take_notifications();
1985
1986 let pmt_bytes = build_ca_pmt_fixture_two_caids(1560);
1987 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1988
1989 let expected_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
1994 &pmt,
1995 &[0x0500],
1996 CaPmtListManagement::Only,
1997 CaPmtCmdId::OkDescrambling,
1998 )
1999 .to_bytes();
2000 let unfiltered =
2001 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2002 assert_ne!(
2003 expected_filtered, unfiltered,
2004 "precondition: the fixture's two CAIDs must make filtered != unfiltered"
2005 );
2006 let sends_filtered_before = count_apdu_on_session(&d, CA_SESSION, &expected_filtered);
2007 let sends_unfiltered_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered);
2008
2009 d.add_service(&pmt).unwrap();
2010 d.device_mut().inbound.push_back(sb());
2011 d.pump(Duration::from_millis(10)).unwrap();
2012
2013 assert_eq!(
2014 count_apdu_on_session(&d, CA_SESSION, &expected_filtered),
2015 sends_filtered_before + 1,
2016 "add_service must send the CAID-filtered ca_pmt"
2017 );
2018 assert_eq!(
2019 count_apdu_on_session(&d, CA_SESSION, &unfiltered),
2020 sends_unfiltered_before,
2021 "add_service must NOT send the unfiltered ca_pmt once the CAM's CAIDs are known"
2022 );
2023 }
2024
2025 #[test]
2026 fn requery_timer_filters_ca_descriptors_to_cam_advertised_caids() {
2027 use broadcast_common::Parse;
2028 use dvb_ci::objects::ca_info::CaInfo;
2029
2030 let clock = TestClock::new();
2031 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2032 d.set_requery_interval(Duration::from_secs(10));
2035 d.take_notifications();
2036
2037 let pmt_bytes = build_ca_pmt_fixture_two_caids(1561);
2038 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2039 d.add_service(&pmt).unwrap();
2042 d.device_mut().inbound.push_back(sb());
2043 clock.advance(Duration::from_millis(10));
2044 d.pump(Duration::from_millis(10)).unwrap();
2045
2046 feed(
2048 &mut d,
2049 r_apdu(
2050 CA_SESSION,
2051 &ser(&CaInfo {
2052 ca_system_ids: vec![0x0500],
2053 }),
2054 ),
2055 );
2056 d.take_notifications();
2057
2058 let expected_query_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2061 &pmt,
2062 &[0x0500],
2063 CaPmtListManagement::Update,
2064 CaPmtCmdId::Query,
2065 )
2066 .to_bytes();
2067 let expected_ok_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2068 &pmt,
2069 &[0x0500],
2070 CaPmtListManagement::Update,
2071 CaPmtCmdId::OkDescrambling,
2072 )
2073 .to_bytes();
2074 let unfiltered_query =
2075 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2076 let unfiltered_ok = build_ca_pmt(
2077 &pmt,
2078 CaPmtListManagement::Update,
2079 CaPmtCmdId::OkDescrambling,
2080 )
2081 .to_bytes();
2082 assert_ne!(
2083 expected_query_filtered, unfiltered_query,
2084 "precondition: the fixture's two CAIDs must make filtered != unfiltered (query)"
2085 );
2086 assert_ne!(
2087 expected_ok_filtered, unfiltered_ok,
2088 "precondition: the fixture's two CAIDs must make filtered != unfiltered (ok_descrambling)"
2089 );
2090 let sends_query_before = count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered);
2091 let sends_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered);
2092 let sends_unfiltered_query_before =
2093 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query);
2094 let sends_unfiltered_ok_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok);
2095
2096 clock.advance(Duration::from_secs(11));
2097 d.pump(Duration::from_secs(11)).unwrap();
2098 feed(&mut d, sb());
2099
2100 assert_eq!(
2101 count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered),
2102 sends_query_before + 1,
2103 "the re-query `query` half must be CAID-filtered to the CAM's advertised CAIDs"
2104 );
2105 assert_eq!(
2106 count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered),
2107 sends_ok_before + 1,
2108 "the re-query `ok_descrambling` half must be CAID-filtered to the CAM's advertised CAIDs"
2109 );
2110 assert_eq!(
2111 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query),
2112 sends_unfiltered_query_before,
2113 "the re-query resend must NOT be the unfiltered `query` ca_pmt"
2114 );
2115 assert_eq!(
2116 count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok),
2117 sends_unfiltered_ok_before,
2118 "the re-query resend must NOT be the unfiltered `ok_descrambling` ca_pmt"
2119 );
2120 }
2121
2122 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
2132 const EXTENSION_HEADER_LEN: u16 = 5;
2133 const CRC_LEN: u16 = 4;
2134 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
2135 let mut v = Vec::new();
2136 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
2138 v.push((section_length & 0xFF) as u8);
2139 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
2144 let crc = broadcast_common::crc32_mpeg2::compute(&v);
2145 v.extend_from_slice(&crc.to_be_bytes());
2146 v
2147 }
2148
2149 #[test]
2150 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
2151 use broadcast_common::Parse;
2152 use dvb_ci::objects::ca_info::CaInfo;
2153 use dvb_si::tables::cat::CatSection;
2154
2155 let mut d = driver_with_sessions();
2156 d.take_notifications();
2157
2158 feed(
2160 &mut d,
2161 r_apdu(
2162 CA_SESSION,
2163 &ser(&CaInfo {
2164 ca_system_ids: vec![0x0648, 0x0100],
2165 }),
2166 ),
2167 );
2168 d.take_notifications();
2169
2170 let mut descriptors = Vec::new();
2173 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2174 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
2175 let cat_bytes = build_cat_fixture(&descriptors);
2176 let cat = CatSection::parse(&cat_bytes).unwrap();
2177
2178 d.set_cat(&cat).unwrap();
2179
2180 assert_eq!(
2181 d.emm_pids(),
2182 &[0x1FF0],
2183 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
2184 );
2185 }
2186
2187 #[test]
2188 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
2189 use broadcast_common::Parse;
2190 use dvb_ci::objects::ca_info::CaInfo;
2191 use dvb_si::tables::cat::CatSection;
2192
2193 let mut d = driver_with_sessions();
2194 d.take_notifications();
2195
2196 let mut descriptors = Vec::new();
2197 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2198 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
2199 let cat_bytes = build_cat_fixture(&descriptors);
2200 let cat = CatSection::parse(&cat_bytes).unwrap();
2201
2202 d.set_cat(&cat).unwrap();
2205 assert!(
2206 d.emm_pids().is_empty(),
2207 "emm_pids must be empty before any ca_info arrives, got {:?}",
2208 d.emm_pids()
2209 );
2210
2211 feed(
2214 &mut d,
2215 r_apdu(
2216 CA_SESSION,
2217 &ser(&CaInfo {
2218 ca_system_ids: vec![0x0648, 0x0100],
2219 }),
2220 ),
2221 );
2222 d.take_notifications();
2223
2224 assert_eq!(
2225 d.emm_pids(),
2226 &[0x1FF0],
2227 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
2228 );
2229 }
2230
2231 #[test]
2237 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
2238 use broadcast_common::Parse;
2239 use dvb_ci::objects::ca_info::CaInfo;
2240 use dvb_si::tables::cat::CatSection;
2241
2242 let mut d = driver_with_sessions();
2243 d.take_notifications();
2244
2245 feed(
2247 &mut d,
2248 r_apdu(
2249 CA_SESSION,
2250 &ser(&CaInfo {
2251 ca_system_ids: vec![0x0648, 0x0100],
2252 }),
2253 ),
2254 );
2255 d.take_notifications();
2256
2257 let mut descriptors = Vec::new();
2259 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2260 descriptors.extend_from_slice(&ca_descriptor(0x0100, 0x1FF0));
2261 let cat_bytes = build_cat_fixture(&descriptors);
2262 let cat = CatSection::parse(&cat_bytes).unwrap();
2263
2264 d.set_cat(&cat).unwrap();
2265
2266 assert_eq!(
2267 d.emm_pids(),
2268 &[0x1FF0],
2269 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
2270 d.emm_pids()
2271 );
2272 }
2273
2274 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
2278 const VIACCESS: u16 = 0x0500;
2279 let prog_ca = ca_descriptor(VIACCESS, 0x0074);
2280 let es0_ca = ca_descriptor(VIACCESS, 0x0075);
2281
2282 let mut body = Vec::new();
2283 body.push(0x02); body.push(0);
2285 body.push(0);
2286 body.extend_from_slice(&program_number.to_be_bytes());
2287 body.push(0xC3);
2288 body.push(0x00);
2289 body.push(0x00);
2290 body.push(0xE0 | 0x02); body.push(0x00);
2292 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2293 body.push(prog_ca.len() as u8);
2294 body.extend_from_slice(&prog_ca);
2295 body.push(0x1B);
2297 body.push(0xE0 | 0x02);
2298 body.push(0x00);
2299 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2300 body.push(es0_ca.len() as u8);
2301 body.extend_from_slice(&es0_ca);
2302 body.push(0x0F);
2304 body.push(0xE0 | 0x02);
2305 body.push(0x01);
2306 body.push(0xF0);
2307 body.push(0x00);
2308
2309 let section_length = body.len() - 3 + 4;
2310 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2311 body[2] = section_length as u8;
2312 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2313 body.extend_from_slice(&crc.to_be_bytes());
2314 body
2315 }
2316
2317 #[test]
2318 fn descramble_pids_is_the_union_of_active_services_es_pids() {
2319 use broadcast_common::Parse;
2320
2321 let mut d = driver_with_sessions();
2322 d.take_notifications();
2323
2324 assert!(
2325 d.descramble_pids().is_empty(),
2326 "no service added yet: descramble_pids must be empty"
2327 );
2328
2329 let pmt1_bytes = build_ca_pmt_fixture(1546);
2330 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2331 d.add_service(&pmt1).unwrap();
2332 d.device_mut().inbound.push_back(sb());
2333 d.pump(Duration::from_millis(10)).unwrap();
2334
2335 assert_eq!(d.descramble_pids(), &[0x0100, 0x0101]);
2336
2337 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2338 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2339 d.add_service(&pmt2).unwrap();
2340 d.device_mut().inbound.push_back(sb());
2341 d.pump(Duration::from_millis(10)).unwrap();
2342
2343 assert_eq!(
2345 d.descramble_pids(),
2346 &[0x0100, 0x0101, 0x0200, 0x0201],
2347 "descramble_pids must be the union across both added services"
2348 );
2349 }
2350
2351 pub(crate) fn ca_pmt_reply_for(
2356 program_number: u16,
2357 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
2358 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2359 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2360 program_number,
2361 version_number: 1,
2362 current_next_indicator: true,
2363 ca_enable,
2364 streams: vec![],
2365 }
2366 }
2367
2368 #[test]
2369 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
2370 use broadcast_common::Parse;
2371 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2372
2373 let clock = TestClock::new();
2374 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2375 d.set_requery_interval(Duration::from_secs(10));
2378 d.take_notifications();
2379
2380 let pmt_bytes = build_ca_pmt_fixture(1546);
2381 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2382 d.add_service(&pmt).unwrap();
2383 d.device_mut().inbound.push_back(sb());
2384 clock.advance(Duration::from_millis(10));
2385 d.pump(Duration::from_millis(10)).unwrap();
2386 d.take_notifications();
2387
2388 let expected_initial_ca_pmt =
2392 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2393 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
2394
2395 let expected_query =
2401 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2402 let expected_ok_descrambling = build_ca_pmt(
2403 &pmt,
2404 CaPmtListManagement::Update,
2405 CaPmtCmdId::OkDescrambling,
2406 )
2407 .to_bytes();
2408 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2409 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2410
2411 let mut all_notes = Vec::new();
2412
2413 feed(
2417 &mut d,
2418 r_apdu(
2419 CA_SESSION,
2420 &ser(&ca_pmt_reply_for(
2421 1546,
2422 Some(CaEnable::NotPossibleNoEntitlement),
2423 )),
2424 ),
2425 );
2426 all_notes.extend(d.take_notifications());
2427
2428 clock.advance(Duration::from_secs(11));
2438 d.pump(Duration::from_secs(11)).unwrap();
2439 all_notes.extend(d.take_notifications());
2440 feed(&mut d, sb());
2441
2442 assert_eq!(
2443 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2444 query_sends_before + 1,
2445 "expected the re-query timer to resend the `query` half exactly once"
2446 );
2447 assert_eq!(
2448 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2449 ok_sends_before + 1,
2450 "expected the re-query timer to resend the `ok_descrambling` half exactly once"
2451 );
2452
2453 feed(
2456 &mut d,
2457 r_apdu(
2458 CA_SESSION,
2459 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
2460 ),
2461 );
2462 all_notes.extend(d.take_notifications());
2463
2464 let hits = all_notes
2465 .iter()
2466 .filter(|n| {
2467 matches!(
2468 n,
2469 Notification::Entitlement {
2470 program_number: 1546,
2471 ca_enable: CaEnable::Possible,
2472 descrambling_ok: true,
2473 }
2474 )
2475 })
2476 .count();
2477 assert_eq!(
2478 hits, 1,
2479 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
2480 );
2481 }
2482
2483 #[test]
2484 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
2485 use broadcast_common::Parse;
2486 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2487
2488 let mut d = driver_with_sessions();
2489 d.set_requery_interval(Duration::from_secs(10));
2492 d.take_notifications();
2493
2494 let pmt_bytes = build_ca_pmt_fixture(1547);
2495 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2496 d.add_service(&pmt).unwrap();
2497 d.device_mut().inbound.push_back(sb());
2498 d.pump(Duration::from_millis(10)).unwrap();
2499 d.take_notifications();
2500
2501 feed(
2505 &mut d,
2506 r_apdu(
2507 CA_SESSION,
2508 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2509 ),
2510 );
2511 d.take_notifications();
2512
2513 for _ in 0..2 {
2516 d.pump(Duration::from_secs(11)).unwrap();
2517 d.take_notifications();
2518 feed(
2519 &mut d,
2520 r_apdu(
2521 CA_SESSION,
2522 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2523 ),
2524 );
2525 let notes = d.take_notifications();
2526 assert!(
2527 !notes
2528 .iter()
2529 .any(|n| matches!(n, Notification::Entitlement { .. })),
2530 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
2531 );
2532 }
2533 }
2534
2535 #[test]
2536 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
2537 use broadcast_common::Parse;
2538 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2539
2540 let mut d = driver_with_sessions();
2541 d.take_notifications();
2542
2543 let pmt_bytes = build_ca_pmt_fixture(1548);
2544 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2545 d.add_service(&pmt).unwrap();
2546 d.device_mut().inbound.push_back(sb());
2547 d.pump(Duration::from_millis(10)).unwrap();
2548 d.take_notifications();
2549
2550 feed(
2552 &mut d,
2553 r_apdu(
2554 CA_SESSION,
2555 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
2556 ),
2557 );
2558 d.take_notifications();
2559
2560 feed(
2564 &mut d,
2565 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
2566 );
2567 let notes = d.take_notifications();
2568 assert!(
2569 !notes
2570 .iter()
2571 .any(|n| matches!(n, Notification::Entitlement { .. })),
2572 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
2573 );
2574 }
2575
2576 #[test]
2577 fn requery_disabled_by_default_sends_nothing() {
2578 use broadcast_common::Parse;
2579
2580 let mut d = driver_with_sessions();
2585 d.take_notifications();
2586
2587 let pmt_bytes = build_ca_pmt_fixture(1549);
2588 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2589 d.add_service(&pmt).unwrap();
2590 d.device_mut().inbound.push_back(sb());
2591 d.pump(Duration::from_millis(10)).unwrap();
2592
2593 let expected_query =
2596 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2597 let expected_ok_descrambling = build_ca_pmt(
2598 &pmt,
2599 CaPmtListManagement::Update,
2600 CaPmtCmdId::OkDescrambling,
2601 )
2602 .to_bytes();
2603 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2604 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2605
2606 d.pump(Duration::from_secs(1_000_000)).unwrap();
2608 feed(&mut d, sb());
2609 feed(&mut d, sb());
2610 assert_eq!(
2611 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2612 query_sends_before,
2613 "the default (Duration::ZERO) must never send a re-query `query`"
2614 );
2615 assert_eq!(
2616 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2617 ok_sends_before,
2618 "the default (Duration::ZERO) must never send a re-query `ok_descrambling`"
2619 );
2620 }
2621
2622 #[test]
2623 fn set_requery_interval_zero_disables_resend_after_being_enabled() {
2624 use broadcast_common::Parse;
2625
2626 let mut d = driver_with_sessions();
2627 d.set_requery_interval(Duration::from_secs(5));
2630 d.set_requery_interval(Duration::ZERO);
2631 d.take_notifications();
2632
2633 let pmt_bytes = build_ca_pmt_fixture(1549);
2634 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2635 d.add_service(&pmt).unwrap();
2636 d.device_mut().inbound.push_back(sb());
2637 d.pump(Duration::from_millis(10)).unwrap();
2638
2639 let expected_ca_pmt = build_ca_pmt(
2643 &pmt,
2644 CaPmtListManagement::Update,
2645 CaPmtCmdId::OkDescrambling,
2646 )
2647 .to_bytes();
2648 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
2649
2650 d.pump(Duration::from_secs(1000)).unwrap();
2652 feed(&mut d, sb());
2653 feed(&mut d, sb());
2654
2655 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
2656 assert_eq!(
2657 sends_after, sends_before,
2658 "Duration::ZERO must disable the re-query resend"
2659 );
2660 }
2661
2662 #[test]
2663 fn requery_timer_resends_every_active_service_not_just_one() {
2664 use broadcast_common::Parse;
2665
2666 let clock = TestClock::new();
2667 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2668 d.set_requery_interval(Duration::from_secs(10));
2671 d.take_notifications();
2672
2673 let pmt1_bytes = build_ca_pmt_fixture(1546);
2676 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2677 d.add_service(&pmt1).unwrap();
2678 d.device_mut().inbound.push_back(sb());
2679 clock.advance(Duration::from_millis(10));
2680 d.pump(Duration::from_millis(10)).unwrap();
2681
2682 let pmt2_bytes = build_ca_pmt_fixture(1547);
2683 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2684 d.add_service(&pmt2).unwrap();
2685 d.device_mut().inbound.push_back(sb());
2686 clock.advance(Duration::from_millis(10));
2687 d.pump(Duration::from_millis(10)).unwrap();
2688 d.take_notifications();
2689
2690 let expected1_query =
2696 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2697 let expected1_ok = build_ca_pmt(
2698 &pmt1,
2699 CaPmtListManagement::Update,
2700 CaPmtCmdId::OkDescrambling,
2701 )
2702 .to_bytes();
2703 let expected2_query =
2704 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2705 let expected2_ok = build_ca_pmt(
2706 &pmt2,
2707 CaPmtListManagement::Update,
2708 CaPmtCmdId::OkDescrambling,
2709 )
2710 .to_bytes();
2711 let sends_before1_query = count_apdu_on_session(&d, CA_SESSION, &expected1_query);
2712 let sends_before1_ok = count_apdu_on_session(&d, CA_SESSION, &expected1_ok);
2713 let sends_before2_query = count_apdu_on_session(&d, CA_SESSION, &expected2_query);
2714 let sends_before2_ok = count_apdu_on_session(&d, CA_SESSION, &expected2_ok);
2715
2716 clock.advance(Duration::from_secs(11));
2722 d.pump(Duration::from_secs(11)).unwrap();
2723 feed(&mut d, sb());
2724 feed(&mut d, sb());
2725
2726 assert_eq!(
2727 count_apdu_on_session(&d, CA_SESSION, &expected1_query),
2728 sends_before1_query + 1,
2729 "expected service 1546's `query` resent exactly once on the shared tick"
2730 );
2731 assert_eq!(
2732 count_apdu_on_session(&d, CA_SESSION, &expected1_ok),
2733 sends_before1_ok + 1,
2734 "expected service 1546's `ok_descrambling` resent exactly once on the shared tick"
2735 );
2736 assert_eq!(
2737 count_apdu_on_session(&d, CA_SESSION, &expected2_query),
2738 sends_before2_query + 1,
2739 "expected service 1547's `query` resent exactly once on the shared tick"
2740 );
2741 assert_eq!(
2742 count_apdu_on_session(&d, CA_SESSION, &expected2_ok),
2743 sends_before2_ok + 1,
2744 "expected service 1547's `ok_descrambling` resent exactly once on the shared tick"
2745 );
2746 }
2747
2748 #[test]
2752 fn requery_after_remove_resends_only_the_surviving_service() {
2753 use broadcast_common::Parse;
2754
2755 let clock = TestClock::new();
2756 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2757 d.set_requery_interval(Duration::from_secs(10));
2760 d.take_notifications();
2761
2762 let pmt1_bytes = build_ca_pmt_fixture(1546);
2763 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2764 d.add_service(&pmt1).unwrap();
2765 d.device_mut().inbound.push_back(sb());
2766 clock.advance(Duration::from_millis(10));
2767 d.pump(Duration::from_millis(10)).unwrap();
2768
2769 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2770 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2771 d.add_service(&pmt2).unwrap();
2772 d.device_mut().inbound.push_back(sb());
2773 clock.advance(Duration::from_millis(10));
2774 d.pump(Duration::from_millis(10)).unwrap();
2775
2776 d.remove_service(1546).unwrap();
2778 d.device_mut().inbound.push_back(sb());
2779 clock.advance(Duration::from_millis(10));
2780 d.pump(Duration::from_millis(10)).unwrap();
2781 d.take_notifications();
2782
2783 let expected_survivor_query =
2789 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2790 let expected_survivor_ok = build_ca_pmt(
2791 &pmt2,
2792 CaPmtListManagement::Update,
2793 CaPmtCmdId::OkDescrambling,
2794 )
2795 .to_bytes();
2796 let expected_removed_query =
2797 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2798 let expected_removed_ok = build_ca_pmt(
2799 &pmt1,
2800 CaPmtListManagement::Update,
2801 CaPmtCmdId::OkDescrambling,
2802 )
2803 .to_bytes();
2804 let sends_survivor_query_before =
2805 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query);
2806 let sends_survivor_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok);
2807 let sends_removed_query_before =
2808 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query);
2809 let sends_removed_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok);
2810
2811 clock.advance(Duration::from_secs(11));
2812 d.pump(Duration::from_secs(11)).unwrap();
2813 feed(&mut d, sb());
2814
2815 assert_eq!(
2816 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query),
2817 sends_survivor_query_before + 1,
2818 "the surviving service's `query` must be re-sent"
2819 );
2820 assert_eq!(
2821 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok),
2822 sends_survivor_ok_before + 1,
2823 "the surviving service's `ok_descrambling` must be re-sent"
2824 );
2825 assert_eq!(
2826 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query),
2827 sends_removed_query_before,
2828 "the removed service's `query` must NOT be re-sent"
2829 );
2830 assert_eq!(
2831 count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok),
2832 sends_removed_ok_before,
2833 "the removed service's `ok_descrambling` must NOT be re-sent"
2834 );
2835 }
2836
2837 #[test]
2840 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
2841 use broadcast_common::Parse;
2842
2843 let mut d = driver_with_sessions();
2844 d.take_notifications();
2845
2846 let pmt1_bytes = build_ca_pmt_fixture(1546);
2850 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2851 d.add_service(&pmt1).unwrap();
2852 d.device_mut().inbound.push_back(sb());
2853 d.pump(Duration::from_millis(10)).unwrap();
2854
2855 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2856 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2857 d.add_service(&pmt2).unwrap();
2858 d.device_mut().inbound.push_back(sb());
2859 d.pump(Duration::from_millis(10)).unwrap();
2860
2861 d.remove_service(1546).unwrap();
2862 d.device_mut().inbound.push_back(sb());
2863 d.pump(Duration::from_millis(10)).unwrap();
2864
2865 let expected =
2869 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
2870 assert_apdu_on_session(&d, CA_SESSION, &expected);
2871
2872 assert_eq!(
2873 d.descramble_pids(),
2874 &[0x0200, 0x0201],
2875 "1546's ES PIDs must be gone; 1547's must remain"
2876 );
2877 assert!(
2878 d.managed_ca().services().get(&1546).is_none(),
2879 "1546 must no longer be tracked"
2880 );
2881 assert!(
2882 d.managed_ca().services().get(&1547).is_some(),
2883 "1547 must remain tracked"
2884 );
2885 }
2886
2887 #[test]
2888 fn remove_service_of_untracked_program_is_a_no_op() {
2889 let mut d = driver_with_sessions();
2890 d.take_notifications();
2891
2892 let ops_before = d.device().ops.len();
2893 d.remove_service(0xFFFF).unwrap();
2894 assert_eq!(
2895 d.device().ops.len(),
2896 ops_before,
2897 "removing an untracked program must not send anything to the device"
2898 );
2899 assert!(
2900 d.managed_ca().services().is_empty(),
2901 "removing an untracked program must not disturb the (empty) managed set"
2902 );
2903 }
2904
2905 #[test]
2906 fn cam_removed_edge_clears_managed_state() {
2907 use broadcast_common::Parse;
2908 use dvb_ci::objects::ca_info::CaInfo;
2909 use dvb_si::tables::cat::CatSection;
2910
2911 let mut d = driver_with_sessions();
2912 d.take_notifications();
2913
2914 let pmt_bytes = build_ca_pmt_fixture(1546);
2915 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2916 d.add_service(&pmt).unwrap();
2917 d.device_mut().inbound.push_back(sb());
2918 d.pump(Duration::from_millis(10)).unwrap();
2919
2920 feed(
2923 &mut d,
2924 r_apdu(
2925 CA_SESSION,
2926 &ser(&CaInfo {
2927 ca_system_ids: vec![0x0648],
2928 }),
2929 ),
2930 );
2931 d.take_notifications();
2932 let mut descriptors = Vec::new();
2933 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2934 let cat_bytes = build_cat_fixture(&descriptors);
2935 let cat = CatSection::parse(&cat_bytes).unwrap();
2936 d.set_cat(&cat).unwrap();
2937
2938 assert!(
2939 !d.managed_ca().services().is_empty(),
2940 "precondition: a service is tracked"
2941 );
2942 assert!(
2943 !d.descramble_pids().is_empty(),
2944 "precondition: descramble_pids populated"
2945 );
2946 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
2947
2948 d.device_mut().slot.module_present = false;
2950 d.pump(Duration::from_millis(10)).unwrap();
2951 let notes = d.take_notifications();
2952 assert!(
2953 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
2954 "expected CamRemoved, got {notes:?}"
2955 );
2956
2957 assert!(
2958 d.managed_ca().services().is_empty(),
2959 "services must be cleared on CamRemoved"
2960 );
2961 assert!(
2962 d.descramble_pids().is_empty(),
2963 "descramble_pids must be cleared on CamRemoved"
2964 );
2965 assert!(
2966 d.emm_pids().is_empty(),
2967 "emm_pids must be cleared on CamRemoved"
2968 );
2969 }
2970}