1use mpeg_ts::pid::Pid;
7use std::collections::BTreeSet;
8use std::io;
9use std::time::{Duration, Instant};
10
11use broadcast_common::Serialize;
12use dvb_ci::builder::{build_ca_pmt, build_ca_pmt_for_caids};
13use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
14use dvb_si::tables::cat::CatSection;
15use dvb_si::tables::pmt::PmtSection;
16
17use broadcast_common::Parse;
18
19use crate::device::{CaDevice, SlotInfo};
20use crate::event::{Action, Event, HostRequest, HotPlug, MmiEvent, Notification};
21use crate::managed::{self, CaError, ManagedCa};
22use crate::stack::CiStack;
23
24const MMI_CARD_ABSENT_KEYWORDS: &[&str] = &[
29 "no card",
30 "insert card",
31 "insert smart card",
32 "card removed",
33 "please insert",
34];
35
36const MMI_CARD_PRESENT_KEYWORDS: &[&str] = &["entitlement", "card valid", "subscription active"];
41
42type Clock = Box<dyn Fn() -> Instant>;
47
48fn default_clock() -> Clock {
49 Box::new(Instant::now)
50}
51
52pub struct Driver<D: CaDevice> {
54 device: D,
55 stack: CiStack,
56 notifications: Vec<Notification>,
57 next_timer: Option<Duration>,
59 buf: Vec<u8>,
61 last_slot: Option<SlotInfo>,
68 last_caids: Option<BTreeSet<u16>>,
71 last_descrambling_ok: Option<bool>,
74 last_mmi_card_present: Option<bool>,
80 managed: ManagedCa,
83 last_pump: Option<Instant>,
88 clock: Clock,
90 requery_pending: Option<CaError>,
94 settle_until: Option<Instant>,
98 deferred_writes: Vec<Vec<u8>>,
101 slot_ready_deadline: Option<Instant>,
106}
107
108const SLOT_READY_GRACE: Duration = Duration::from_secs(12);
116const SLOT_RETRY: Duration = Duration::from_millis(250);
118
119impl<D: CaDevice> Driver<D> {
120 #[must_use]
122 pub fn new(device: D) -> Self {
123 Self {
124 device,
125 stack: CiStack::new(),
126 notifications: Vec::new(),
127 next_timer: None,
128 buf: vec![0u8; 4096],
129 last_slot: None,
130 last_caids: None,
131 last_descrambling_ok: None,
132 last_mmi_card_present: None,
133 managed: ManagedCa::new(),
134 last_pump: None,
135 clock: default_clock(),
136 requery_pending: None,
137 settle_until: None,
138 deferred_writes: Vec::new(),
139 slot_ready_deadline: None,
140 }
141 }
142
143 #[must_use]
147 pub fn with_clock(mut self, clock: impl Fn() -> Instant + 'static) -> Self {
148 self.clock = Box::new(clock);
149 self
150 }
151
152 pub fn managed_ca(&self) -> &ManagedCa {
155 &self.managed
156 }
157
158 pub fn device(&self) -> &D {
160 &self.device
161 }
162
163 pub fn device_mut(&mut self) -> &mut D {
166 &mut self.device
167 }
168
169 pub fn next_timer(&self) -> Option<Duration> {
171 self.next_timer
172 }
173
174 pub fn take_notifications(&mut self) -> Vec<Notification> {
176 core::mem::take(&mut self.notifications)
177 }
178
179 pub fn init(&mut self) -> io::Result<()> {
181 let actions = self.stack.handle(Event::Host(HostRequest::Init));
182 self.run(actions)
183 }
184
185 pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
188 let actions = self
189 .stack
190 .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
191 self.run(actions)
192 }
193
194 pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
200 let actions = self
201 .stack
202 .handle(Event::Host(HostRequest::Descramble(pmt_section)));
203 self.run(actions)
204 }
205
206 pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
209 let actions = self
210 .stack
211 .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
212 self.run(actions)
213 }
214
215 pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
218 let actions = self
219 .stack
220 .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
221 self.run(actions)
222 }
223
224 pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
227 let actions = self
228 .stack
229 .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
230 self.run(actions)
231 }
232
233 fn build_ca_pmt_filtered(
245 &self,
246 pmt: &PmtSection<'_>,
247 list_management: CaPmtListManagement,
248 cmd_id: CaPmtCmdId,
249 ) -> dvb_ci::builder::CaPmtBuilt {
250 let cam_caids = self.managed.cam_caids();
251 if cam_caids.is_empty() {
252 build_ca_pmt(pmt, list_management, cmd_id)
253 } else {
254 let allowed: Vec<u16> = cam_caids.iter().copied().collect();
255 build_ca_pmt_for_caids(pmt, &allowed, list_management, cmd_id)
256 }
257 }
258
259 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
279 if !managed::pmt_has_ca(pmt) {
280 return Err(CaError::NoCaDescriptor {
281 program_number: pmt.program_number,
282 });
283 }
284 let list_management = if self.managed.is_empty() {
285 CaPmtListManagement::Only
286 } else {
287 CaPmtListManagement::Add
288 };
289 let cmd_id = CaPmtCmdId::OkDescrambling;
290 let built_bytes = self
297 .build_ca_pmt_filtered(pmt, list_management, cmd_id)
298 .try_to_bytes()?;
299 let mut pmt_raw = vec![0u8; pmt.serialized_len()];
307 let n = pmt.serialize_into(&mut pmt_raw)?;
308 pmt_raw.truncate(n);
309 self.send_ca_pmt(&built_bytes)?;
310 self.managed.record(
311 pmt.program_number,
312 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
313 );
314 Ok(())
315 }
316
317 pub fn remove_service(&mut self, program_number: u16) -> Result<(), CaError> {
331 let raw = self
332 .managed
333 .services()
334 .get(&program_number)
335 .map(|s| s.pmt_raw.clone());
336 let Some(raw) = raw else {
337 return Ok(());
338 };
339 self.remove_program(&raw)?;
340 self.managed.remove(program_number);
341 Ok(())
342 }
343
344 pub fn set_requery_interval(&mut self, interval: Duration) {
353 self.managed.set_requery_interval(interval);
354 }
355
356 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
371 let entries = cat.ca_descriptors().map_err(CaError::Cat)?;
372 self.managed.set_cat(&entries);
373 Ok(())
374 }
375
376 #[must_use]
380 pub fn emm_pids(&self) -> &[Pid] {
381 self.managed.emm_pids()
382 }
383
384 #[must_use]
387 pub fn descramble_pids(&self) -> &[Pid] {
388 self.managed.descramble_pids()
389 }
390
391 #[must_use]
396 pub fn ca_pids(&self) -> &[Pid] {
397 self.managed.ca_pids()
398 }
399
400 #[must_use]
408 pub fn required_pids(&self) -> Vec<Pid> {
409 self.managed.required_pids()
410 }
411
412 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
414 let actions = self
415 .stack
416 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
417 self.run(actions)
418 }
419
420 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
422 let actions = self
423 .stack
424 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
425 self.run(actions)
426 }
427
428 pub fn mmi_cancel(&mut self) -> io::Result<()> {
430 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
431 self.run(actions)
432 }
433
434 pub fn enter_menu(&mut self) -> io::Result<()> {
437 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
438 self.run(actions)
439 }
440
441 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
450 let now = (self.clock)();
461 let mut elapsed = self
462 .last_pump
463 .map_or(timeout, |prev| now.saturating_duration_since(prev));
464 self.last_pump = Some(now);
465
466 if let Some(until) = self.settle_until {
471 if now < until {
472 self.run(vec![Action::QuerySlot])?;
473 return Ok(false);
474 }
475 self.settle_until = None;
476 let mut pending = std::mem::take(&mut self.deferred_writes).into_iter();
477 while let Some(w) = pending.next() {
478 match self.device.write(&w) {
479 Ok(()) => {}
480 Err(e)
485 if e.kind() == io::ErrorKind::InvalidInput
486 && self.slot_ready_deadline.is_some_and(|d| now < d) =>
487 {
488 self.deferred_writes = std::iter::once(w).chain(pending).collect();
489 self.settle_until = Some(now + SLOT_RETRY);
490 return Ok(false);
491 }
492 Err(e) => return Err(e),
493 }
494 }
495 self.slot_ready_deadline = None;
496 elapsed = Duration::ZERO;
499 }
500
501 self.run(vec![Action::QuerySlot])?;
502 if self.device.poll(timeout)? {
503 let n = self.device.read(&mut self.buf)?;
504 if n > 0 {
505 let actions = self.stack.handle(Event::Readable(&self.buf[..n]));
509 self.run(actions)?;
510 return Ok(true);
511 }
512 }
513 let actions = self.stack.handle(Event::Tick { elapsed });
514 self.run(actions)?;
515 self.requery_tick(elapsed)?;
516 Ok(false)
517 }
518
519 fn requery_tick(&mut self, elapsed: Duration) -> io::Result<()> {
563 if let Some(err) = self.requery_pending.take() {
567 return Err(io::Error::other(err));
568 }
569 if !self.managed.tick(elapsed) {
570 return Ok(());
571 }
572 let mut ca_pmts: Vec<(Vec<u8>, Vec<u8>)> =
573 Vec::with_capacity(self.managed.services().len());
574 for s in self.managed.services().values() {
575 let outcome: Result<(Vec<u8>, Vec<u8>), CaError> = (|| {
580 let pmt = PmtSection::parse(&s.pmt_raw).map_err(CaError::PmtParse)?;
581 let query = self
582 .build_ca_pmt_filtered(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query)
583 .try_to_bytes()?;
584 let ok_descrambling = self
585 .build_ca_pmt_filtered(
586 &pmt,
587 CaPmtListManagement::Update,
588 CaPmtCmdId::OkDescrambling,
589 )
590 .try_to_bytes()?;
591 Ok((query, ok_descrambling))
592 })();
593 match outcome {
594 Ok(pair) => ca_pmts.push(pair),
595 Err(err) => {
596 self.requery_pending = Some(err);
600 }
601 }
602 }
603 for (query, ok_descrambling) in ca_pmts {
604 self.send_ca_pmt(&query)?;
605 self.send_ca_pmt(&ok_descrambling)?;
606 }
607 if let Some(err) = self.requery_pending.take() {
608 return Err(io::Error::other(err));
609 }
610 Ok(())
611 }
612
613 pub fn pump_with<F: FnMut(&Notification)>(
622 &mut self,
623 timeout: Duration,
624 mut handler: F,
625 ) -> io::Result<bool> {
626 let progressed = self.pump(timeout)?;
627 for n in self.take_notifications() {
628 handler(&n);
629 }
630 Ok(progressed)
631 }
632
633 pub fn pump_hotplug<F: FnMut(HotPlug)>(
637 &mut self,
638 timeout: Duration,
639 mut handler: F,
640 ) -> io::Result<bool> {
641 self.pump_with(timeout, |n| {
642 if let Some(h) = n.hotplug() {
643 handler(h);
644 }
645 })
646 }
647
648 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
650 for action in actions {
651 match action {
652 Action::Write(bytes) => {
653 if self.settle_until.is_some() {
654 self.deferred_writes.push(bytes);
655 } else {
656 self.device.write(&bytes)?;
657 }
658 }
659 Action::Reset => {
660 self.device.reset()?;
661 self.deferred_writes.clear();
664 let settle = self.device.reset_settle();
665 let now = (self.clock)();
666 self.settle_until = (!settle.is_zero()).then(|| now + settle);
667 self.slot_ready_deadline =
668 (!settle.is_zero()).then(|| now + settle + SLOT_READY_GRACE);
669 }
670 Action::QuerySlot => {
671 let info = self.device.slot_info()?;
672 self.handle_slot_info(info)?;
673 }
674 Action::SetTimer { after } => self.next_timer = Some(after),
675 Action::Notify(n) => {
676 let inferred = self.infer_card(&n);
677 self.notifications.push(n);
678 self.notifications.extend(inferred);
679 }
680 }
681 }
682 Ok(())
683 }
684
685 fn handle_slot_info(&mut self, info: SlotInfo) -> io::Result<()> {
692 let prev = self.last_slot.replace(info);
693 match prev {
694 Some(prev) if !prev.module_present && info.module_present => {
695 self.notifications
696 .push(Notification::HotPlug(HotPlug::CamPresent));
697 self.reset_module_state();
698 let actions = self.stack.handle(Event::Host(HostRequest::Init));
702 self.run(actions)?;
703 }
704 Some(prev) if prev.module_present && !info.module_present => {
705 self.notifications
706 .push(Notification::HotPlug(HotPlug::CamRemoved));
707 self.reset_module_state();
708 }
709 _ => {}
710 }
711 Ok(())
712 }
713
714 fn reset_module_state(&mut self) {
724 self.stack = CiStack::new();
725 self.settle_until = None;
726 self.slot_ready_deadline = None;
727 self.deferred_writes.clear();
728 self.next_timer = None;
729 self.last_caids = None;
730 self.last_descrambling_ok = None;
731 self.last_mmi_card_present = None;
732 self.managed.clear();
733 }
734
735 fn infer_card(&mut self, note: &Notification) -> Vec<Notification> {
742 match note {
743 Notification::CaInfo { ca_system_ids } => {
744 let new_set: BTreeSet<u16> = ca_system_ids.iter().copied().collect();
745 let mut out = Vec::new();
746 if let Some(prev) = &self.last_caids {
747 if prev.is_empty() && !new_set.is_empty() {
748 out.push(Notification::HotPlug(HotPlug::CardInserted));
749 } else if !prev.is_empty() && new_set.is_empty() {
750 out.push(Notification::HotPlug(HotPlug::CardRemoved));
751 } else if !prev.is_empty() && !new_set.is_empty() && *prev != new_set {
752 out.push(Notification::HotPlug(HotPlug::CardChanged));
753 }
754 }
755 self.managed.set_cam_caids(new_set.clone());
759 self.last_caids = Some(new_set);
760 out
761 }
762 Notification::CaPmtReply {
763 program_number,
764 ca_enable,
765 descrambling_ok,
766 } => {
767 let mut out = Vec::new();
768 if let Some(prev) = self.last_descrambling_ok {
769 if !prev && *descrambling_ok {
770 out.push(Notification::HotPlug(HotPlug::CardInserted));
771 } else if prev && !*descrambling_ok {
772 out.push(Notification::HotPlug(HotPlug::CardRemoved));
773 }
774 }
775 self.last_descrambling_ok = Some(*descrambling_ok);
776 if let Some((v, ok)) =
780 self.managed
781 .record_reply(*program_number, *ca_enable, *descrambling_ok)
782 {
783 out.push(Notification::Entitlement {
784 program_number: *program_number,
785 ca_enable: v,
786 descrambling_ok: ok,
787 });
788 }
789 out
790 }
791 Notification::Mmi(ev) => {
792 let present = Self::mmi_text(ev).and_then(|text| {
793 let lower = text.to_lowercase();
794 if MMI_CARD_ABSENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
795 Some(false)
796 } else if MMI_CARD_PRESENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
797 Some(true)
798 } else {
799 None
800 }
801 });
802 match present {
803 Some(p) if self.last_mmi_card_present != Some(p) => {
805 self.last_mmi_card_present = Some(p);
806 vec![Notification::HotPlug(if p {
807 HotPlug::CardInserted
808 } else {
809 HotPlug::CardRemoved
810 })]
811 }
812 _ => Vec::new(),
813 }
814 }
815 _ => Vec::new(),
816 }
817 }
818
819 fn mmi_text(ev: &MmiEvent) -> Option<String> {
823 match ev {
824 MmiEvent::Menu(m) | MmiEvent::List(m) => {
825 let mut s = format!("{} {} {}", m.title, m.subtitle, m.bottom);
826 for choice in &m.choices {
827 s.push(' ');
828 s.push_str(choice);
829 }
830 Some(s)
831 }
832 MmiEvent::Enquiry { prompt, .. } => Some(prompt.clone()),
833 MmiEvent::Close => None,
834 }
835 }
836}
837
838#[cfg(test)]
839pub(crate) mod tests {
840 use super::*;
841 use crate::device::{DeviceOp, MockCaDevice};
842 use crate::event::{HostControlEvent, HotPlug, Notification};
843 use broadcast_common::Serialize;
844 use dvb_ci::tpdu::tags;
845
846 pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
847 let mut b = vec![0u8; s.serialized_len()];
848 match s.serialize_into(&mut b) {
849 Ok(n) => b.truncate(n),
850 Err(_) => b.clear(),
851 }
852 b
853 }
854
855 #[derive(Clone)]
861 pub(crate) struct TestClock(std::rc::Rc<std::cell::Cell<Instant>>);
862
863 impl TestClock {
864 pub(crate) fn new() -> Self {
865 Self(std::rc::Rc::new(std::cell::Cell::new(Instant::now())))
866 }
867
868 pub(crate) fn advance(&self, d: Duration) {
870 self.0.set(self.0.get() + d);
871 }
872
873 pub(crate) fn as_fn(&self) -> impl Fn() -> Instant + 'static {
875 let cell = self.0.clone();
876 move || cell.get()
877 }
878 }
879
880 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
883 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
884 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
885 v.extend_from_slice(spdu);
886 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
887 v
888 }
889
890 pub(crate) fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
893 use dvb_ci::spdu::SessionNumber;
894 let mut spdu = ser(&SessionNumber {
895 length_form: dvb_ci::length::LengthForm::Minimal,
896 session_nb,
897 });
898 spdu.extend_from_slice(apdu);
899 r_data(1, &spdu)
900 }
901
902 pub(crate) fn sb() -> Vec<u8> {
905 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
906 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
907 }
908
909 pub(crate) fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
912 d.device_mut().inbound.push_back(frame);
913 d.pump(Duration::from_millis(10)).unwrap();
914 for _ in 0..8 {
915 d.device_mut().inbound.push_back(sb());
916 d.pump(Duration::from_millis(10)).unwrap();
917 }
918 }
919
920 pub(crate) fn driver_with_sessions() -> Driver<MockCaDevice> {
924 use dvb_ci::objects::resource_manager::Profile;
925 use dvb_ci::resource::{
926 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
927 RESOURCE_MANAGER,
928 };
929 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
930
931 let mut d = Driver::new(MockCaDevice::new([]));
932 d.init().unwrap();
933 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
935 feed(
937 &mut d,
938 r_data(
939 1,
940 &ser(&OpenSessionRequest {
941 length_form: dvb_ci::length::LengthForm::Minimal,
942 resource: RESOURCE_MANAGER,
943 }),
944 ),
945 );
946 feed(
949 &mut d,
950 r_apdu(
951 1,
952 &ser(&Profile {
953 length_form: dvb_ci::length::LengthForm::Minimal,
954 resources: vec![
955 APPLICATION_INFORMATION,
956 CONDITIONAL_ACCESS_SUPPORT,
957 MMI,
958 HOST_CONTROL,
959 ],
960 }),
961 ),
962 );
963 for (nb, res) in [
965 (2u16, APPLICATION_INFORMATION),
966 (3, CONDITIONAL_ACCESS_SUPPORT),
967 (4, MMI),
968 (5, HOST_CONTROL),
969 ] {
970 feed(
971 &mut d,
972 r_data(
973 1,
974 &ser(&CreateSessionResponse {
975 length_form: dvb_ci::length::LengthForm::Minimal,
976 status: SessionStatus::Ok,
977 resource: res,
978 session_nb: nb,
979 }),
980 ),
981 );
982 }
983 d
984 }
985
986 const RM_SESSION: u16 = 1;
990 pub(crate) const CA_SESSION: u16 = 3;
991 const MMI_SESSION: u16 = 4;
992 const HOST_CONTROL_SESSION: u16 = 5;
993
994 #[test]
995 fn host_control_tune_apdu_surfaces_notification_via_driver() {
996 use dvb_ci::objects::host_control::Tune;
997
998 let mut d = driver_with_sessions();
999 let hc_nb = HOST_CONTROL_SESSION;
1000 d.take_notifications(); let tune = Tune {
1004 length_form: dvb_ci::length::LengthForm::Minimal,
1005 network_id: 0x1122,
1006 original_network_id: 0x3344,
1007 transport_stream_id: 0x5566,
1008 service_id: 0x7788,
1009 };
1010 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
1011
1012 let notes = d.take_notifications();
1014 assert!(
1015 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
1016 network_id: 0x1122,
1017 original_network_id: 0x3344,
1018 transport_stream_id: 0x5566,
1019 service_id: 0x7788,
1020 })),
1021 "expected HostControl(Tune) notification, got {notes:?}"
1022 );
1023 }
1024
1025 #[test]
1028 fn padded_tune_from_the_cam_surfaces_an_error_notification() {
1029 let mut d = driver_with_sessions();
1030 d.take_notifications();
1031 let padded = [
1033 0x9F, 0x84, 0x00, 0x09, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xEE,
1034 ];
1035 feed(&mut d, r_apdu(HOST_CONTROL_SESSION, &padded));
1036 let notes = d.take_notifications();
1037 assert!(
1038 notes.iter().any(
1039 |n| matches!(n, Notification::Error { detail } if detail.contains("trailing bytes"))
1040 ),
1041 "expected an Error notification naming the trailing bytes, got {notes:?}"
1042 );
1043 assert!(
1044 !notes
1045 .iter()
1046 .any(|n| matches!(n, Notification::HostControl(_))),
1047 "a refused tune must not also surface as a HostControl event: {notes:?}"
1048 );
1049 }
1050
1051 #[test]
1054 fn malformed_mmi_apdu_from_the_cam_surfaces_an_error_notification() {
1055 let mut d = driver_with_sessions();
1056 d.take_notifications();
1057 feed(&mut d, r_apdu(MMI_SESSION, &[0x9F, 0x88, 0x07, 0x01, 0x00]));
1059 let notes = d.take_notifications();
1060 assert!(
1061 notes
1062 .iter()
1063 .any(|n| matches!(n, Notification::Error { .. })),
1064 "expected an Error notification, got {notes:?}"
1065 );
1066 }
1067
1068 #[test]
1069 fn profile_reply_advertises_host_control() {
1070 use broadcast_common::Parse;
1071 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
1072 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
1073
1074 let mut d = Driver::new(MockCaDevice::new([]));
1075 d.init().unwrap();
1076 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1077 feed(
1079 &mut d,
1080 r_data(
1081 1,
1082 &ser(&dvb_ci::spdu::OpenSessionRequest {
1083 length_form: dvb_ci::length::LengthForm::Minimal,
1084 resource: RESOURCE_MANAGER,
1085 }),
1086 ),
1087 );
1088 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq::default())));
1090
1091 let want = dvb_ci::tag::PROFILE.to_bytes();
1094 let found = d.device().ops.iter().any(|op| {
1095 if let DeviceOp::Write(w) = op
1096 && let Some(pos) = w.windows(3).position(|x| x == want)
1097 && let Ok(p) = Profile::parse(&w[pos..])
1098 {
1099 return p.resources.contains(&HOST_CONTROL);
1100 }
1101 false
1102 });
1103 assert!(found, "profile reply must advertise HOST_CONTROL");
1104 }
1105
1106 #[test]
1107 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
1108 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
1109
1110 let mut d = driver_with_sessions();
1111 let mmi_nb = MMI_SESSION;
1112
1113 d.mmi_menu_answer(2).unwrap();
1116 d.device_mut().inbound.push_back(sb());
1117 d.pump(Duration::from_millis(10)).unwrap();
1118 assert_apdu_on_session(
1119 &d,
1120 mmi_nb,
1121 &ser(&MenuAnsw {
1122 length_form: dvb_ci::length::LengthForm::Minimal,
1123 choice_ref: 2,
1124 }),
1125 );
1126
1127 d.mmi_enquiry_answer(b"1234").unwrap();
1129 d.device_mut().inbound.push_back(sb());
1130 d.pump(Duration::from_millis(10)).unwrap();
1131 assert_apdu_on_session(
1132 &d,
1133 mmi_nb,
1134 &ser(&Answ {
1135 length_form: dvb_ci::length::LengthForm::Minimal,
1136 answ_id: AnswId::Answer,
1137 text_chars: b"1234",
1138 }),
1139 );
1140 }
1141
1142 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
1145 use dvb_ci::spdu::SessionNumber;
1146 let mut want = ser(&SessionNumber {
1147 length_form: dvb_ci::length::LengthForm::Minimal,
1148 session_nb,
1149 });
1150 want.extend_from_slice(apdu);
1151 let hit = d.device().ops.iter().any(|op| match op {
1152 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1153 _ => false,
1154 });
1155 assert!(
1156 hit,
1157 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
1158 );
1159 }
1160
1161 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
1165 use dvb_ci::spdu::SessionNumber;
1166 let mut want = ser(&SessionNumber {
1167 length_form: dvb_ci::length::LengthForm::Minimal,
1168 session_nb,
1169 });
1170 want.extend_from_slice(apdu);
1171 d.device()
1172 .ops
1173 .iter()
1174 .filter(|op| match op {
1175 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1176 _ => false,
1177 })
1178 .count()
1179 }
1180
1181 struct SettleDevice {
1184 inner: MockCaDevice,
1185 settle: Duration,
1186 }
1187
1188 impl CaDevice for SettleDevice {
1189 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1190 self.inner.read(buf)
1191 }
1192 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1193 self.inner.write(buf)
1194 }
1195 fn reset(&mut self) -> io::Result<()> {
1196 self.inner.reset()
1197 }
1198 fn reset_settle(&self) -> Duration {
1199 self.settle
1200 }
1201 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1202 self.inner.slot_info()
1203 }
1204 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1205 self.inner.poll(timeout)
1206 }
1207 }
1208
1209 const SETTLE: Duration = Duration::from_secs(3);
1210
1211 fn settling_driver(clock: &TestClock) -> Driver<SettleDevice> {
1212 Driver::new(SettleDevice {
1213 inner: MockCaDevice::new([]),
1214 settle: SETTLE,
1215 })
1216 .with_clock(clock.as_fn())
1217 }
1218
1219 fn create_tc_writes(d: &Driver<SettleDevice>) -> usize {
1220 d.device()
1221 .inner
1222 .ops
1223 .iter()
1224 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1225 .count()
1226 }
1227
1228 struct NotRunningDevice {
1232 inner: SettleDevice,
1233 refusals_left: u32,
1234 refused: u32,
1235 }
1236
1237 impl CaDevice for NotRunningDevice {
1238 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1239 self.inner.read(buf)
1240 }
1241 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1242 if self.refusals_left > 0 {
1243 self.refusals_left -= 1;
1244 self.refused += 1;
1245 return Err(io::Error::from(io::ErrorKind::InvalidInput));
1246 }
1247 self.inner.write(buf)
1248 }
1249 fn reset(&mut self) -> io::Result<()> {
1250 self.inner.reset()
1251 }
1252 fn reset_settle(&self) -> Duration {
1253 self.inner.reset_settle()
1254 }
1255 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1256 self.inner.slot_info()
1257 }
1258 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1259 self.inner.poll(timeout)
1260 }
1261 }
1262
1263 fn not_running_driver(clock: &TestClock, refusals: u32) -> Driver<NotRunningDevice> {
1264 Driver::new(NotRunningDevice {
1265 inner: SettleDevice {
1266 inner: MockCaDevice::new([]),
1267 settle: SETTLE,
1268 },
1269 refusals_left: refusals,
1270 refused: 0,
1271 })
1272 .with_clock(clock.as_fn())
1273 }
1274
1275 fn create_tc_count(d: &Driver<NotRunningDevice>) -> usize {
1276 d.device()
1277 .inner
1278 .inner
1279 .ops
1280 .iter()
1281 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1282 .count()
1283 }
1284
1285 #[test]
1290 fn post_reset_einval_is_retried_until_the_slot_is_running() {
1291 let clock = TestClock::new();
1292 let mut d = not_running_driver(&clock, 3);
1293 d.init().unwrap();
1294 clock.advance(SETTLE + Duration::from_millis(100));
1295 for _ in 0..10 {
1296 d.pump(Duration::from_millis(10))
1297 .expect("a not-yet-RUNNING slot (EINVAL) must be retried, not reported");
1298 if create_tc_count(&d) == 1 {
1299 break;
1300 }
1301 clock.advance(Duration::from_millis(300));
1302 }
1303 assert_eq!(d.device().refused, 3, "the three refusals were consumed");
1304 assert_eq!(create_tc_count(&d), 1, "Create_T_C delivered exactly once");
1305 }
1306
1307 #[test]
1310 fn post_reset_einval_is_reported_once_the_grace_window_has_passed() {
1311 let clock = TestClock::new();
1312 let mut d = not_running_driver(&clock, u32::MAX);
1313 let start = clock.0.get();
1314 d.init().unwrap();
1315 clock.advance(SETTLE + Duration::from_millis(100));
1316 let mut failure = None;
1317 for _ in 0..80 {
1318 match d.pump(Duration::from_millis(10)) {
1319 Ok(_) => clock.advance(Duration::from_millis(300)),
1320 Err(e) => {
1321 failure = Some(e);
1322 break;
1323 }
1324 }
1325 }
1326 let e = failure.expect("a slot that never runs must eventually be reported");
1327 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1328 assert!(
1329 clock.0.get() >= start + SETTLE + SLOT_READY_GRACE,
1330 "reported before the grace window ended"
1331 );
1332 assert_eq!(create_tc_count(&d), 0);
1333 }
1334
1335 #[test]
1340 fn requery_armed_before_the_handshake_sends_nothing() {
1341 let clock = TestClock::new();
1342 let mut d = Driver::new(MockCaDevice::new([])).with_clock(clock.as_fn());
1343 d.set_requery_interval(Duration::from_secs(10));
1344 d.init().unwrap();
1345 for _ in 0..40 {
1346 clock.advance(Duration::from_secs(1));
1347 d.pump(Duration::from_millis(100)).unwrap();
1348 }
1349 let ca_pmt_tag = [0x9F, 0x80, 0x32];
1350 let ca_pmt_writes = d
1351 .device()
1352 .ops
1353 .iter()
1354 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == ca_pmt_tag)))
1355 .count();
1356 assert_eq!(
1357 ca_pmt_writes, 0,
1358 "re-query wrote a ca_pmt before any service"
1359 );
1360 }
1361
1362 #[test]
1365 fn reset_does_not_block_pump() {
1366 let clock = TestClock::new();
1367 let mut d = settling_driver(&clock);
1368 let t = std::time::Instant::now();
1369 d.init().unwrap();
1370 d.pump(Duration::from_millis(10)).unwrap();
1371 assert!(t.elapsed() < Duration::from_secs(1), "reset/pump blocked");
1372 assert_eq!(d.device().inner.ops[0], DeviceOp::Reset);
1373 }
1374
1375 #[test]
1379 fn no_transport_traffic_until_settle_elapsed() {
1380 let clock = TestClock::new();
1381 let mut d = settling_driver(&clock);
1382 d.device_mut()
1383 .inner
1384 .inbound
1385 .push_back(vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1386 d.init().unwrap();
1387 for _ in 0..5 {
1388 clock.advance(Duration::from_millis(500));
1389 d.pump(Duration::from_millis(10)).unwrap();
1390 }
1391 assert_eq!(create_tc_writes(&d), 0);
1393 assert_eq!(
1394 d.device().inner.inbound.len(),
1395 1,
1396 "frame read while settling"
1397 );
1398
1399 clock.advance(Duration::from_millis(600));
1400 d.pump(Duration::from_millis(10)).unwrap();
1401 assert_eq!(create_tc_writes(&d), 1);
1402
1403 let mut z = Driver::new(MockCaDevice::new([]));
1404 z.init().unwrap();
1405 assert_eq!(
1406 d.device().inner.written()[..3],
1407 z.device().written()[..3],
1408 "post-settle Create_T_C differs from the zero-settle sequence"
1409 );
1410 assert!(d.device().inner.inbound.is_empty());
1413 }
1414
1415 #[test]
1418 fn second_reset_restarts_settle() {
1419 let clock = TestClock::new();
1420 let mut d = settling_driver(&clock);
1421 d.init().unwrap();
1422 clock.advance(Duration::from_secs(2));
1423 d.pump(Duration::from_millis(10)).unwrap();
1424 d.init().unwrap(); clock.advance(Duration::from_secs(2)); d.pump(Duration::from_millis(10)).unwrap();
1427 assert_eq!(create_tc_writes(&d), 0, "first deadline wrongly honoured");
1428 clock.advance(Duration::from_millis(1500)); d.pump(Duration::from_millis(10)).unwrap();
1430 assert_eq!(create_tc_writes(&d), 1);
1431 }
1432
1433 #[test]
1436 fn slot_sampling_continues_while_settling() {
1437 let clock = TestClock::new();
1438 let mut d = settling_driver(&clock);
1439 d.init().unwrap();
1440 d.take_notifications();
1441 d.device_mut().inner.slot.module_present = false;
1442 clock.advance(Duration::from_millis(500));
1443 d.pump(Duration::from_millis(10)).unwrap();
1444 let notes = d.take_notifications();
1445 assert!(
1446 notes
1447 .iter()
1448 .any(|n| matches!(n, Notification::HotPlug(HotPlug::CamRemoved))),
1449 "{notes:?}"
1450 );
1451 assert_eq!(create_tc_writes(&d), 0);
1452 }
1453
1454 #[test]
1455 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
1456 let mut d = Driver::new(MockCaDevice::new([]));
1457 d.init().unwrap();
1458 let ops = &d.device().ops;
1459 assert_eq!(ops[0], DeviceOp::Reset);
1460 assert_eq!(ops[1], DeviceOp::SlotInfo);
1461 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
1462 }
1463
1464 #[test]
1465 fn reads_reply_then_polls_on_pump() {
1466 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1468 let clock = TestClock::new();
1469 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1470 d.init().unwrap();
1471 assert!(d.pump(Duration::from_millis(100)).unwrap());
1473 clock.advance(Duration::from_millis(100));
1478 assert!(!d.pump(Duration::from_millis(100)).unwrap());
1480 let last = d.device().ops.last().unwrap();
1481 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1482 }
1483
1484 #[test]
1490 fn pump_advances_the_stack_clock_by_real_elapsed_time_not_by_timeout() {
1491 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1492 let clock = TestClock::new();
1493 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1494 d.init().unwrap();
1495 assert!(d.pump(Duration::from_secs(1)).unwrap());
1496
1497 clock.advance(Duration::from_millis(1));
1501 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1502 assert!(
1503 d.device().ops.last().is_none_or(|op| !matches!(
1504 op,
1505 DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)
1506 )),
1507 "must not poll after only 1ms of real elapsed time, regardless of the 1s `timeout` argument"
1508 );
1509
1510 clock.advance(Duration::from_millis(100));
1512 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1513 let last = d.device().ops.last().unwrap();
1514 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1515 }
1516
1517 #[test]
1520 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
1521 let mut dev = MockCaDevice::new([]);
1522 dev.slot = SlotInfo {
1523 num: 0,
1524 module_ready: false,
1525 module_present: false,
1526 };
1527 let mut d = Driver::new(dev);
1528 d.init().unwrap();
1529 let notes = d.take_notifications();
1532 assert!(
1533 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1534 "baseline observation must not fire CamPresent, got {notes:?}"
1535 );
1536 let resets_before = d
1537 .device()
1538 .ops
1539 .iter()
1540 .filter(|o| **o == DeviceOp::Reset)
1541 .count();
1542
1543 d.device_mut().slot = SlotInfo {
1545 num: 0,
1546 module_ready: true,
1547 module_present: true,
1548 };
1549 d.pump(Duration::from_millis(10)).unwrap();
1550
1551 let notes = d.take_notifications();
1552 let cam_present_count = notes
1553 .iter()
1554 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
1555 .count();
1556 assert_eq!(
1557 cam_present_count, 1,
1558 "expected exactly one CamPresent, got {notes:?}"
1559 );
1560 let resets_after = d
1562 .device()
1563 .ops
1564 .iter()
1565 .filter(|o| **o == DeviceOp::Reset)
1566 .count();
1567 assert_eq!(
1568 resets_after,
1569 resets_before + 1,
1570 "expected one fresh Reset on re-insert"
1571 );
1572 assert!(
1573 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
1574 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
1575 d.device().ops.last()
1576 );
1577 }
1578
1579 #[test]
1580 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
1581 let mut d = driver_with_sessions();
1582 d.take_notifications();
1583
1584 d.device_mut().slot.module_present = false;
1586 d.pump(Duration::from_millis(10)).unwrap();
1587 let notes = d.take_notifications();
1588 assert!(
1589 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
1590 "expected CamRemoved, got {notes:?}"
1591 );
1592
1593 d.mmi_menu_answer(0).unwrap();
1597 let notes = d.take_notifications();
1598 assert!(
1599 notes
1600 .iter()
1601 .any(|n| matches!(n, Notification::Error { .. })),
1602 "expected no open MMI session after teardown, got {notes:?}"
1603 );
1604
1605 let resets_before = d
1607 .device()
1608 .ops
1609 .iter()
1610 .filter(|o| **o == DeviceOp::Reset)
1611 .count();
1612 d.device_mut().slot.module_present = true;
1613 d.device_mut().slot.module_ready = true;
1614 d.pump(Duration::from_millis(10)).unwrap();
1615 let notes = d.take_notifications();
1616 assert!(
1617 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1618 "expected CamPresent on re-insert, got {notes:?}"
1619 );
1620 let resets_after = d
1621 .device()
1622 .ops
1623 .iter()
1624 .filter(|o| **o == DeviceOp::Reset)
1625 .count();
1626 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1627 }
1628
1629 #[test]
1630 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1631 let mut d = Driver::new(MockCaDevice::new([]));
1632 d.init().unwrap();
1633 d.take_notifications();
1634
1635 for _ in 0..5 {
1636 d.pump(Duration::from_millis(10)).unwrap();
1637 }
1638 let notes = d.take_notifications();
1639 assert!(
1640 !notes.iter().any(|n| matches!(
1641 n,
1642 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1643 )),
1644 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1645 );
1646 }
1647
1648 #[test]
1649 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1650 use dvb_ci::objects::ca_info::CaInfo;
1651
1652 let mut d = driver_with_sessions();
1653 d.take_notifications();
1654
1655 feed(
1657 &mut d,
1658 r_apdu(
1659 CA_SESSION,
1660 &ser(&CaInfo {
1661 length_form: dvb_ci::length::LengthForm::Minimal,
1662 ca_system_ids: vec![],
1663 }),
1664 ),
1665 );
1666 let notes = d.take_notifications();
1667 assert!(
1668 !notes.iter().any(|n| matches!(
1669 n,
1670 Notification::HotPlug(
1671 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1672 )
1673 )),
1674 "first ca_info must only establish the baseline, got {notes:?}"
1675 );
1676
1677 feed(
1679 &mut d,
1680 r_apdu(
1681 CA_SESSION,
1682 &ser(&CaInfo {
1683 length_form: dvb_ci::length::LengthForm::Minimal,
1684 ca_system_ids: vec![0x0B00],
1685 }),
1686 ),
1687 );
1688 let notes = d.take_notifications();
1689 assert!(
1690 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1691 "expected CardInserted, got {notes:?}"
1692 );
1693
1694 feed(
1696 &mut d,
1697 r_apdu(
1698 CA_SESSION,
1699 &ser(&CaInfo {
1700 length_form: dvb_ci::length::LengthForm::Minimal,
1701 ca_system_ids: vec![0x1800],
1702 }),
1703 ),
1704 );
1705 let notes = d.take_notifications();
1706 assert!(
1707 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1708 "expected CardChanged, got {notes:?}"
1709 );
1710 }
1711
1712 #[test]
1713 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1714 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1715
1716 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1717 CaPmtReply {
1718 reserved_ca_enable: Default::default(),
1719 reserved_version: Default::default(),
1720 length_form: dvb_ci::length::LengthForm::Minimal,
1721 program_number: 1,
1722 version_number: 1,
1723 current_next_indicator: true,
1724 ca_enable,
1725 streams: vec![],
1726 }
1727 }
1728
1729 let mut d = driver_with_sessions();
1730 d.take_notifications();
1731
1732 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1734 let notes = d.take_notifications();
1735 assert!(
1736 !notes.iter().any(|n| matches!(
1737 n,
1738 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1739 )),
1740 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1741 );
1742
1743 feed(
1745 &mut d,
1746 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1747 );
1748 let notes = d.take_notifications();
1749 assert!(
1750 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1751 "expected CardInserted, got {notes:?}"
1752 );
1753
1754 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1756 let notes = d.take_notifications();
1757 assert!(
1758 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1759 "expected CardRemoved, got {notes:?}"
1760 );
1761 }
1762
1763 #[test]
1764 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1765 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1766
1767 let mut d = driver_with_sessions();
1768 d.take_notifications();
1769
1770 feed(
1773 &mut d,
1774 r_apdu(
1775 CA_SESSION,
1776 &ser(&CaPmtReply {
1777 reserved_ca_enable: Default::default(),
1778 reserved_version: Default::default(),
1779 length_form: dvb_ci::length::LengthForm::Minimal,
1780 program_number: 7,
1781 version_number: 1,
1782 current_next_indicator: true,
1783 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1784 streams: vec![],
1785 }),
1786 ),
1787 );
1788 let notes = d.take_notifications();
1789 assert!(
1790 notes.contains(&Notification::CaPmtReply {
1791 program_number: 7,
1792 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1793 descrambling_ok: true,
1794 }),
1795 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1796 );
1797 }
1798
1799 #[test]
1800 fn ca_pmt_reply_flag_clear_surfaces_none() {
1801 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1802
1803 let mut d = driver_with_sessions();
1804 d.take_notifications();
1805
1806 feed(
1809 &mut d,
1810 r_apdu(
1811 CA_SESSION,
1812 &ser(&CaPmtReply {
1813 reserved_ca_enable: Default::default(),
1814 reserved_version: Default::default(),
1815 length_form: dvb_ci::length::LengthForm::Minimal,
1816 program_number: 7,
1817 version_number: 1,
1818 current_next_indicator: true,
1819 ca_enable: None,
1820 streams: vec![],
1821 }),
1822 ),
1823 );
1824 let notes = d.take_notifications();
1825 assert!(
1826 notes.contains(&Notification::CaPmtReply {
1827 program_number: 7,
1828 ca_enable: None,
1829 descrambling_ok: false,
1830 }),
1831 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1832 );
1833 }
1834
1835 #[test]
1836 fn mmi_no_card_text_infers_card_removed() {
1837 use dvb_ci::objects::mmi_high::Enq;
1838
1839 let mut d = driver_with_sessions();
1840 d.take_notifications();
1841
1842 feed(
1843 &mut d,
1844 r_apdu(
1845 MMI_SESSION,
1846 &ser(&Enq {
1847 reserved: Default::default(),
1848 length_form: dvb_ci::length::LengthForm::Minimal,
1849 blind_answer: false,
1850 answer_text_length: 0,
1851 text_chars: b"NO CARD detected - please insert your smart card",
1852 }),
1853 ),
1854 );
1855
1856 let notes = d.take_notifications();
1857 assert!(
1858 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1859 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1860 );
1861 }
1862
1863 #[test]
1866 fn mmi_card_keyword_heuristic_is_edge_triggered() {
1867 use dvb_ci::objects::mmi_high::Enq;
1868
1869 let enq = |text: &'static [u8]| {
1870 r_apdu(
1871 MMI_SESSION,
1872 &ser(&Enq {
1873 reserved: Default::default(),
1874 length_form: dvb_ci::length::LengthForm::Minimal,
1875 blind_answer: false,
1876 answer_text_length: 0,
1877 text_chars: text,
1878 }),
1879 )
1880 };
1881 let mut d = driver_with_sessions();
1882 d.take_notifications();
1883 let count = |notes: &[Notification], hp: HotPlug| {
1884 notes
1885 .iter()
1886 .filter(|n| **n == Notification::HotPlug(hp))
1887 .count()
1888 };
1889
1890 for _ in 0..3 {
1892 feed(&mut d, enq(b"No card - please insert your smart card"));
1893 }
1894 let notes = d.take_notifications();
1895 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1896 assert_eq!(count(¬es, HotPlug::CardInserted), 0);
1897
1898 for _ in 0..2 {
1900 feed(&mut d, enq(b"Your entitlement is valid"));
1901 }
1902 let notes = d.take_notifications();
1903 assert_eq!(count(¬es, HotPlug::CardInserted), 1, "{notes:?}");
1904 assert_eq!(count(¬es, HotPlug::CardRemoved), 0);
1905
1906 feed(&mut d, enq(b"Card removed"));
1908 feed(&mut d, enq(b"Card removed"));
1909 let notes = d.take_notifications();
1910 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1911 }
1912
1913 #[test]
1914 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1915 let mut dev = MockCaDevice::new([]);
1916 dev.slot = SlotInfo {
1917 num: 0,
1918 module_ready: false,
1919 module_present: false,
1920 };
1921 let mut d = Driver::new(dev);
1922 d.init().unwrap();
1923 d.take_notifications(); d.device_mut().slot = SlotInfo {
1927 num: 0,
1928 module_ready: true,
1929 module_present: true,
1930 };
1931
1932 let mut seen = Vec::new();
1933 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1934 .unwrap();
1935
1936 assert_eq!(
1937 seen,
1938 vec![HotPlug::CamPresent],
1939 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1940 );
1941 }
1942
1943 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: Pid) -> [u8; 6] {
1948 [
1949 0x09,
1950 0x04,
1951 (ca_system_id >> 8) as u8,
1952 ca_system_id as u8,
1953 0xE0 | ((pid.value() >> 8) as u8 & 0x1F),
1954 pid.value() as u8,
1955 ]
1956 }
1957
1958 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
1975 const VIACCESS: u16 = 0x0500;
1976 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
1977 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
1978
1979 let mut body = Vec::new();
1980 body.push(0x02); body.push(0); body.push(0);
1983 body.extend_from_slice(&program_number.to_be_bytes());
1984 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
1989 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1990 body.push(prog_ca.len() as u8);
1991 body.extend_from_slice(&prog_ca);
1992 body.push(0x1B);
1994 body.push(0xE0 | 0x01);
1995 body.push(0x00);
1996 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1997 body.push(es0_ca.len() as u8);
1998 body.extend_from_slice(&es0_ca);
1999 body.push(0x0F);
2001 body.push(0xE0 | 0x01);
2002 body.push(0x01);
2003 body.push(0xF0);
2004 body.push(0x00);
2005
2006 let section_length = body.len() - 3 + 4;
2007 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2008 body[2] = section_length as u8;
2009 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2010 body.extend_from_slice(&crc.to_be_bytes());
2011 body
2012 }
2013
2014 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
2021 const VIACCESS: u16 = 0x0500;
2022 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2023 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2024
2025 let mut body = Vec::new();
2026 body.push(0x02); body.push(0); body.push(0);
2029 body.extend_from_slice(&program_number.to_be_bytes());
2030 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));
2036 body.push(prog_ca.len() as u8);
2037 body.extend_from_slice(&prog_ca);
2038 body.push(0x1B);
2040 body.push(0xE0 | 0x01);
2041 body.push(0x00);
2042 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2043 body.push(es0_ca.len() as u8);
2044 body.extend_from_slice(&es0_ca);
2045 body.push(0x0F);
2047 body.push(0xE0 | 0x01);
2048 body.push(0x01);
2049 body.push(0xF0);
2050 body.push(0x00);
2051
2052 let section_length = body.len() - 3 + 4;
2053 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2054 body[2] = section_length as u8;
2055 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2056 body.extend_from_slice(&crc.to_be_bytes());
2057 body
2058 }
2059
2060 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
2064 let mut body = Vec::new();
2065 body.push(0x02);
2066 body.push(0);
2067 body.push(0);
2068 body.extend_from_slice(&program_number.to_be_bytes());
2069 body.push(0xC3);
2070 body.push(0x00);
2071 body.push(0x00);
2072 body.push(0xE0 | 0x01);
2073 body.push(0x00);
2074 body.push(0xF0); body.push(0x00);
2076 body.push(0x1B);
2078 body.push(0xE0 | 0x01);
2079 body.push(0x00);
2080 body.push(0xF0);
2081 body.push(0x00);
2082
2083 let section_length = body.len() - 3 + 4;
2084 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2085 body[2] = section_length as u8;
2086 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2087 body.extend_from_slice(&crc.to_be_bytes());
2088 body
2089 }
2090
2091 #[test]
2092 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
2093 use broadcast_common::Parse;
2094
2095 let mut d = driver_with_sessions();
2096 d.take_notifications();
2097
2098 let pmt_bytes = build_ca_pmt_fixture(1546);
2099 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2100
2101 d.add_service(&pmt).unwrap();
2102 d.device_mut().inbound.push_back(sb());
2103 d.pump(Duration::from_millis(10)).unwrap();
2104
2105 let expected =
2109 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2110 assert_apdu_on_session(&d, CA_SESSION, &expected);
2111
2112 let svc = d
2114 .managed_ca()
2115 .services()
2116 .get(&1546)
2117 .expect("program_number 1546 must be tracked after add_service");
2118 assert_eq!(svc.es_pids, vec![Pid::new(0x0100), Pid::new(0x0101)]);
2119 assert_eq!(svc.ca_pids, vec![Pid::new(0x0064), Pid::new(0x0065)]);
2120 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
2121 assert_eq!(svc.last_ca_enable, None);
2122 }
2123
2124 #[test]
2125 fn add_service_rejects_pmt_without_ca_descriptor() {
2126 use broadcast_common::Parse;
2127
2128 let mut d = driver_with_sessions();
2129 let pmt_bytes = build_clear_pmt_fixture(999);
2130 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2131
2132 let err = d.add_service(&pmt).unwrap_err();
2133 assert!(
2134 matches!(
2135 err,
2136 CaError::NoCaDescriptor {
2137 program_number: 999
2138 }
2139 ),
2140 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
2141 );
2142 assert!(
2143 d.managed_ca().services().is_empty(),
2144 "a rejected PMT must not be recorded"
2145 );
2146 }
2147
2148 #[test]
2160 fn requery_tick_serialize_failure_errors_from_pump_instead_of_panicking() {
2161 use broadcast_common::Parse;
2162
2163 let clock = TestClock::new();
2164 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2165
2166 let good_bytes = build_ca_pmt_fixture(1);
2169 let good = PmtSection::parse(&good_bytes).unwrap();
2170 assert_eq!(good.program_number, 1, "fixture precondition");
2171 d.add_service(&good).unwrap();
2172
2173 let pmt_bytes = build_ca_pmt_fixture(1546);
2178 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2179 let built = d
2180 .build_ca_pmt_filtered(
2181 &pmt,
2182 CaPmtListManagement::Update,
2183 CaPmtCmdId::OkDescrambling,
2184 )
2185 .try_to_bytes()
2186 .expect("fixture precondition: the good PMT encodes");
2187 let corrupt_raw = pmt_bytes[..pmt_bytes.len() - 6].to_vec();
2188 d.managed.record(
2189 1546,
2190 managed::service_of(&pmt, CaPmtCmdId::OkDescrambling, built, corrupt_raw),
2191 );
2192 d.take_notifications();
2193 for _ in 0..16 {
2197 d.device_mut().inbound.push_back(sb());
2198 d.pump(Duration::from_millis(10)).unwrap();
2199 }
2200 d.set_requery_interval(Duration::from_secs(5));
2201
2202 clock.advance(Duration::from_secs(5));
2206 let err = d
2207 .pump(Duration::ZERO)
2208 .expect_err("re-query of a corrupt stored PMT must error, not panic");
2209 assert!(
2210 matches!(
2211 err.get_ref(),
2212 Some(e) if e.downcast_ref::<CaError>()
2213 .is_some_and(|ca| matches!(ca, CaError::PmtParse(_)))
2214 ),
2215 "expected CaError::PmtParse from the corrupt stored bytes, got {err:?}"
2216 );
2217 assert!(
2218 err.to_string().contains("stored PMT re-parse failed"),
2219 "error must name the re-parse failure, got: {err}"
2220 );
2221 assert!(d.pump(Duration::ZERO).is_ok());
2225 clock.advance(Duration::from_secs(5));
2226 let err2 = d
2227 .pump(Duration::ZERO)
2228 .expect_err("the corrupt service must fail every re-query");
2229 assert!(
2230 err2.to_string().contains("stored PMT re-parse failed"),
2231 "failure must persist, got: {err2}"
2232 );
2233 }
2234
2235 fn corrupt_ca_section_ctor() -> PmtSection<'static> {
2241 const LOOP: usize = 5000;
2242 let mut prog_info: Vec<u8> = Vec::with_capacity(LOOP);
2243 for _ in 0..LOOP / 4 {
2244 prog_info.extend_from_slice(&[0x09u8, 0x02, 0x0B, 0x00]);
2246 }
2247 assert_eq!(prog_info.len(), LOOP);
2248 PmtSection::new(
2249 1,
2250 1,
2251 true,
2252 0,
2253 0,
2254 Pid::new(0x0200),
2255 dvb_si::descriptors::DescriptorLoop::new(Box::leak(prog_info.into_boxed_slice())),
2256 Vec::new(),
2257 )
2258 }
2259
2260 #[test]
2265 fn add_service_serialize_failure_returns_err_instead_of_panicking() {
2266 let mut d = driver_with_sessions();
2267 d.managed.set_cam_caids([0x0B00u16].into_iter().collect());
2268 let pmt = corrupt_ca_section_ctor();
2269 let err = d
2270 .add_service(&pmt)
2271 .expect_err("a ca_pmt with no valid encoding must error, not panic");
2272 assert!(
2273 matches!(err, CaError::Serialize(_)),
2274 "expected CaError::Serialize, got {err:?}"
2275 );
2276 assert!(
2277 d.managed_ca().services().is_empty(),
2278 "a rejected PMT must not be recorded"
2279 );
2280 }
2281
2282 #[test]
2283 fn add_service_second_call_uses_add_list_management() {
2284 use broadcast_common::Parse;
2285
2286 let mut d = driver_with_sessions();
2287 d.take_notifications();
2288
2289 let pmt1_bytes = build_ca_pmt_fixture(1546);
2290 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2291 d.add_service(&pmt1).unwrap();
2292 d.device_mut().inbound.push_back(sb());
2293 d.pump(Duration::from_millis(10)).unwrap();
2294
2295 let pmt2_bytes = build_ca_pmt_fixture(1547);
2296 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2297 d.add_service(&pmt2).unwrap();
2298 d.device_mut().inbound.push_back(sb());
2299 d.pump(Duration::from_millis(10)).unwrap();
2300
2301 let expected2 =
2303 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
2304 assert_apdu_on_session(&d, CA_SESSION, &expected2);
2305
2306 assert_eq!(d.managed_ca().services().len(), 2);
2307 }
2308
2309 fn build_ca_pmt_fixture_two_caids(program_number: u16) -> Vec<u8> {
2320 const VIACCESS: u16 = 0x0500;
2321 const NAGRA: u16 = 0x1800;
2322 let prog_ca_a = ca_descriptor(VIACCESS, Pid::new(0x0064));
2323 let prog_ca_b = ca_descriptor(NAGRA, Pid::new(0x0066));
2324 let mut prog_ca = Vec::new();
2325 prog_ca.extend_from_slice(&prog_ca_a);
2326 prog_ca.extend_from_slice(&prog_ca_b);
2327
2328 let mut body = Vec::new();
2329 body.push(0x02); body.push(0);
2331 body.push(0);
2332 body.extend_from_slice(&program_number.to_be_bytes());
2333 body.push(0xC3);
2334 body.push(0x00);
2335 body.push(0x00);
2336 body.push(0xE0 | 0x01); body.push(0x00);
2338 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2339 body.push(prog_ca.len() as u8);
2340 body.extend_from_slice(&prog_ca);
2341 body.push(0x0F);
2344 body.push(0xE0 | 0x01);
2345 body.push(0x00);
2346 body.push(0xF0);
2347 body.push(0x00);
2348
2349 let section_length = body.len() - 3 + 4;
2350 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2351 body[2] = section_length as u8;
2352 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2353 body.extend_from_slice(&crc.to_be_bytes());
2354 body
2355 }
2356
2357 #[test]
2358 fn add_service_filters_ca_descriptors_to_cam_advertised_caids() {
2359 use broadcast_common::Parse;
2360 use dvb_ci::objects::ca_info::CaInfo;
2361
2362 let mut d = driver_with_sessions();
2363 d.take_notifications();
2364
2365 feed(
2368 &mut d,
2369 r_apdu(
2370 CA_SESSION,
2371 &ser(&CaInfo {
2372 length_form: dvb_ci::length::LengthForm::Minimal,
2373 ca_system_ids: vec![0x0500],
2374 }),
2375 ),
2376 );
2377 d.take_notifications();
2378
2379 let pmt_bytes = build_ca_pmt_fixture_two_caids(1560);
2380 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2381
2382 let expected_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2387 &pmt,
2388 &[0x0500],
2389 CaPmtListManagement::Only,
2390 CaPmtCmdId::OkDescrambling,
2391 )
2392 .to_bytes();
2393 let unfiltered =
2394 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2395 assert_ne!(
2396 expected_filtered, unfiltered,
2397 "precondition: the fixture's two CAIDs must make filtered != unfiltered"
2398 );
2399 let sends_filtered_before = count_apdu_on_session(&d, CA_SESSION, &expected_filtered);
2400 let sends_unfiltered_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered);
2401
2402 d.add_service(&pmt).unwrap();
2403 d.device_mut().inbound.push_back(sb());
2404 d.pump(Duration::from_millis(10)).unwrap();
2405
2406 assert_eq!(
2407 count_apdu_on_session(&d, CA_SESSION, &expected_filtered),
2408 sends_filtered_before + 1,
2409 "add_service must send the CAID-filtered ca_pmt"
2410 );
2411 assert_eq!(
2412 count_apdu_on_session(&d, CA_SESSION, &unfiltered),
2413 sends_unfiltered_before,
2414 "add_service must NOT send the unfiltered ca_pmt once the CAM's CAIDs are known"
2415 );
2416 }
2417
2418 #[test]
2419 fn requery_timer_filters_ca_descriptors_to_cam_advertised_caids() {
2420 use broadcast_common::Parse;
2421 use dvb_ci::objects::ca_info::CaInfo;
2422
2423 let clock = TestClock::new();
2424 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2425 d.set_requery_interval(Duration::from_secs(10));
2428 d.take_notifications();
2429
2430 let pmt_bytes = build_ca_pmt_fixture_two_caids(1561);
2431 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2432 d.add_service(&pmt).unwrap();
2435 d.device_mut().inbound.push_back(sb());
2436 clock.advance(Duration::from_millis(10));
2437 d.pump(Duration::from_millis(10)).unwrap();
2438
2439 feed(
2441 &mut d,
2442 r_apdu(
2443 CA_SESSION,
2444 &ser(&CaInfo {
2445 length_form: dvb_ci::length::LengthForm::Minimal,
2446 ca_system_ids: vec![0x0500],
2447 }),
2448 ),
2449 );
2450 d.take_notifications();
2451
2452 let expected_query_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2455 &pmt,
2456 &[0x0500],
2457 CaPmtListManagement::Update,
2458 CaPmtCmdId::Query,
2459 )
2460 .to_bytes();
2461 let expected_ok_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2462 &pmt,
2463 &[0x0500],
2464 CaPmtListManagement::Update,
2465 CaPmtCmdId::OkDescrambling,
2466 )
2467 .to_bytes();
2468 let unfiltered_query =
2469 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2470 let unfiltered_ok = build_ca_pmt(
2471 &pmt,
2472 CaPmtListManagement::Update,
2473 CaPmtCmdId::OkDescrambling,
2474 )
2475 .to_bytes();
2476 assert_ne!(
2477 expected_query_filtered, unfiltered_query,
2478 "precondition: the fixture's two CAIDs must make filtered != unfiltered (query)"
2479 );
2480 assert_ne!(
2481 expected_ok_filtered, unfiltered_ok,
2482 "precondition: the fixture's two CAIDs must make filtered != unfiltered (ok_descrambling)"
2483 );
2484 let sends_query_before = count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered);
2485 let sends_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered);
2486 let sends_unfiltered_query_before =
2487 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query);
2488 let sends_unfiltered_ok_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok);
2489
2490 clock.advance(Duration::from_secs(11));
2491 d.pump(Duration::from_secs(11)).unwrap();
2492 feed(&mut d, sb());
2493
2494 assert_eq!(
2495 count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered),
2496 sends_query_before + 1,
2497 "the re-query `query` half must be CAID-filtered to the CAM's advertised CAIDs"
2498 );
2499 assert_eq!(
2500 count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered),
2501 sends_ok_before + 1,
2502 "the re-query `ok_descrambling` half must be CAID-filtered to the CAM's advertised CAIDs"
2503 );
2504 assert_eq!(
2505 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query),
2506 sends_unfiltered_query_before,
2507 "the re-query resend must NOT be the unfiltered `query` ca_pmt"
2508 );
2509 assert_eq!(
2510 count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok),
2511 sends_unfiltered_ok_before,
2512 "the re-query resend must NOT be the unfiltered `ok_descrambling` ca_pmt"
2513 );
2514 }
2515
2516 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
2526 const EXTENSION_HEADER_LEN: u16 = 5;
2527 const CRC_LEN: u16 = 4;
2528 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
2529 let mut v = Vec::new();
2530 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
2532 v.push((section_length & 0xFF) as u8);
2533 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
2538 let crc = broadcast_common::crc32_mpeg2::compute(&v);
2539 v.extend_from_slice(&crc.to_be_bytes());
2540 v
2541 }
2542
2543 #[test]
2544 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
2545 use broadcast_common::Parse;
2546 use dvb_ci::objects::ca_info::CaInfo;
2547 use dvb_si::tables::cat::CatSection;
2548
2549 let mut d = driver_with_sessions();
2550 d.take_notifications();
2551
2552 feed(
2554 &mut d,
2555 r_apdu(
2556 CA_SESSION,
2557 &ser(&CaInfo {
2558 length_form: dvb_ci::length::LengthForm::Minimal,
2559 ca_system_ids: vec![0x0648, 0x0100],
2560 }),
2561 ),
2562 );
2563 d.take_notifications();
2564
2565 let mut descriptors = Vec::new();
2568 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2569 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2570 let cat_bytes = build_cat_fixture(&descriptors);
2571 let cat = CatSection::parse(&cat_bytes).unwrap();
2572
2573 d.set_cat(&cat).unwrap();
2574
2575 assert_eq!(
2576 d.emm_pids(),
2577 &[Pid::new(0x1FF0)],
2578 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
2579 );
2580 }
2581
2582 #[test]
2583 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
2584 use broadcast_common::Parse;
2585 use dvb_ci::objects::ca_info::CaInfo;
2586 use dvb_si::tables::cat::CatSection;
2587
2588 let mut d = driver_with_sessions();
2589 d.take_notifications();
2590
2591 let mut descriptors = Vec::new();
2592 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2593 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2594 let cat_bytes = build_cat_fixture(&descriptors);
2595 let cat = CatSection::parse(&cat_bytes).unwrap();
2596
2597 d.set_cat(&cat).unwrap();
2600 assert!(
2601 d.emm_pids().is_empty(),
2602 "emm_pids must be empty before any ca_info arrives, got {:?}",
2603 d.emm_pids()
2604 );
2605
2606 feed(
2609 &mut d,
2610 r_apdu(
2611 CA_SESSION,
2612 &ser(&CaInfo {
2613 length_form: dvb_ci::length::LengthForm::Minimal,
2614 ca_system_ids: vec![0x0648, 0x0100],
2615 }),
2616 ),
2617 );
2618 d.take_notifications();
2619
2620 assert_eq!(
2621 d.emm_pids(),
2622 &[Pid::new(0x1FF0)],
2623 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
2624 );
2625 }
2626
2627 #[test]
2633 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
2634 use broadcast_common::Parse;
2635 use dvb_ci::objects::ca_info::CaInfo;
2636 use dvb_si::tables::cat::CatSection;
2637
2638 let mut d = driver_with_sessions();
2639 d.take_notifications();
2640
2641 feed(
2643 &mut d,
2644 r_apdu(
2645 CA_SESSION,
2646 &ser(&CaInfo {
2647 length_form: dvb_ci::length::LengthForm::Minimal,
2648 ca_system_ids: vec![0x0648, 0x0100],
2649 }),
2650 ),
2651 );
2652 d.take_notifications();
2653
2654 let mut descriptors = Vec::new();
2656 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2657 descriptors.extend_from_slice(&ca_descriptor(0x0100, Pid::new(0x1FF0)));
2658 let cat_bytes = build_cat_fixture(&descriptors);
2659 let cat = CatSection::parse(&cat_bytes).unwrap();
2660
2661 d.set_cat(&cat).unwrap();
2662
2663 assert_eq!(
2664 d.emm_pids(),
2665 &[Pid::new(0x1FF0)],
2666 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
2667 d.emm_pids()
2668 );
2669 }
2670
2671 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
2675 const VIACCESS: u16 = 0x0500;
2676 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0074));
2677 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0075));
2678
2679 let mut body = Vec::new();
2680 body.push(0x02); body.push(0);
2682 body.push(0);
2683 body.extend_from_slice(&program_number.to_be_bytes());
2684 body.push(0xC3);
2685 body.push(0x00);
2686 body.push(0x00);
2687 body.push(0xE0 | 0x02); body.push(0x00);
2689 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2690 body.push(prog_ca.len() as u8);
2691 body.extend_from_slice(&prog_ca);
2692 body.push(0x1B);
2694 body.push(0xE0 | 0x02);
2695 body.push(0x00);
2696 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2697 body.push(es0_ca.len() as u8);
2698 body.extend_from_slice(&es0_ca);
2699 body.push(0x0F);
2701 body.push(0xE0 | 0x02);
2702 body.push(0x01);
2703 body.push(0xF0);
2704 body.push(0x00);
2705
2706 let section_length = body.len() - 3 + 4;
2707 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2708 body[2] = section_length as u8;
2709 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2710 body.extend_from_slice(&crc.to_be_bytes());
2711 body
2712 }
2713
2714 #[test]
2715 fn descramble_pids_is_the_union_of_active_services_es_pids() {
2716 use broadcast_common::Parse;
2717
2718 let mut d = driver_with_sessions();
2719 d.take_notifications();
2720
2721 assert!(
2722 d.descramble_pids().is_empty(),
2723 "no service added yet: descramble_pids must be empty"
2724 );
2725
2726 let pmt1_bytes = build_ca_pmt_fixture(1546);
2727 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2728 d.add_service(&pmt1).unwrap();
2729 d.device_mut().inbound.push_back(sb());
2730 d.pump(Duration::from_millis(10)).unwrap();
2731
2732 assert_eq!(d.descramble_pids(), &[Pid::new(0x0100), Pid::new(0x0101)]);
2733
2734 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2735 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2736 d.add_service(&pmt2).unwrap();
2737 d.device_mut().inbound.push_back(sb());
2738 d.pump(Duration::from_millis(10)).unwrap();
2739
2740 assert_eq!(
2742 d.descramble_pids(),
2743 &[
2744 Pid::new(0x0100),
2745 Pid::new(0x0101),
2746 Pid::new(0x0200),
2747 Pid::new(0x0201)
2748 ],
2749 "descramble_pids must be the union across both added services"
2750 );
2751 }
2752
2753 pub(crate) fn ca_pmt_reply_for(
2758 program_number: u16,
2759 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
2760 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2761 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2762 length_form: dvb_ci::length::LengthForm::Minimal,
2763 program_number,
2764 version_number: 1,
2765 current_next_indicator: true,
2766 ca_enable,
2767 streams: vec![],
2768 reserved_version: Default::default(),
2769 reserved_ca_enable: Default::default(),
2770 }
2771 }
2772
2773 #[test]
2774 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
2775 use broadcast_common::Parse;
2776 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2777
2778 let clock = TestClock::new();
2779 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2780 d.set_requery_interval(Duration::from_secs(10));
2783 d.take_notifications();
2784
2785 let pmt_bytes = build_ca_pmt_fixture(1546);
2786 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2787 d.add_service(&pmt).unwrap();
2788 d.device_mut().inbound.push_back(sb());
2789 clock.advance(Duration::from_millis(10));
2790 d.pump(Duration::from_millis(10)).unwrap();
2791 d.take_notifications();
2792
2793 let expected_initial_ca_pmt =
2797 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2798 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
2799
2800 let expected_query =
2806 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2807 let expected_ok_descrambling = build_ca_pmt(
2808 &pmt,
2809 CaPmtListManagement::Update,
2810 CaPmtCmdId::OkDescrambling,
2811 )
2812 .to_bytes();
2813 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2814 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2815
2816 let mut all_notes = Vec::new();
2817
2818 feed(
2822 &mut d,
2823 r_apdu(
2824 CA_SESSION,
2825 &ser(&ca_pmt_reply_for(
2826 1546,
2827 Some(CaEnable::NotPossibleNoEntitlement),
2828 )),
2829 ),
2830 );
2831 all_notes.extend(d.take_notifications());
2832
2833 clock.advance(Duration::from_secs(11));
2843 d.pump(Duration::from_secs(11)).unwrap();
2844 all_notes.extend(d.take_notifications());
2845 feed(&mut d, sb());
2846
2847 assert_eq!(
2848 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2849 query_sends_before + 1,
2850 "expected the re-query timer to resend the `query` half exactly once"
2851 );
2852 assert_eq!(
2853 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2854 ok_sends_before + 1,
2855 "expected the re-query timer to resend the `ok_descrambling` half exactly once"
2856 );
2857
2858 feed(
2861 &mut d,
2862 r_apdu(
2863 CA_SESSION,
2864 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
2865 ),
2866 );
2867 all_notes.extend(d.take_notifications());
2868
2869 let hits = all_notes
2870 .iter()
2871 .filter(|n| {
2872 matches!(
2873 n,
2874 Notification::Entitlement {
2875 program_number: 1546,
2876 ca_enable: CaEnable::Possible,
2877 descrambling_ok: true,
2878 }
2879 )
2880 })
2881 .count();
2882 assert_eq!(
2883 hits, 1,
2884 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
2885 );
2886 }
2887
2888 #[test]
2889 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
2890 use broadcast_common::Parse;
2891 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2892
2893 let mut d = driver_with_sessions();
2894 d.set_requery_interval(Duration::from_secs(10));
2897 d.take_notifications();
2898
2899 let pmt_bytes = build_ca_pmt_fixture(1547);
2900 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2901 d.add_service(&pmt).unwrap();
2902 d.device_mut().inbound.push_back(sb());
2903 d.pump(Duration::from_millis(10)).unwrap();
2904 d.take_notifications();
2905
2906 feed(
2910 &mut d,
2911 r_apdu(
2912 CA_SESSION,
2913 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2914 ),
2915 );
2916 d.take_notifications();
2917
2918 for _ in 0..2 {
2921 d.pump(Duration::from_secs(11)).unwrap();
2922 d.take_notifications();
2923 feed(
2924 &mut d,
2925 r_apdu(
2926 CA_SESSION,
2927 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2928 ),
2929 );
2930 let notes = d.take_notifications();
2931 assert!(
2932 !notes
2933 .iter()
2934 .any(|n| matches!(n, Notification::Entitlement { .. })),
2935 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
2936 );
2937 }
2938 }
2939
2940 #[test]
2941 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
2942 use broadcast_common::Parse;
2943 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2944
2945 let mut d = driver_with_sessions();
2946 d.take_notifications();
2947
2948 let pmt_bytes = build_ca_pmt_fixture(1548);
2949 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2950 d.add_service(&pmt).unwrap();
2951 d.device_mut().inbound.push_back(sb());
2952 d.pump(Duration::from_millis(10)).unwrap();
2953 d.take_notifications();
2954
2955 feed(
2957 &mut d,
2958 r_apdu(
2959 CA_SESSION,
2960 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
2961 ),
2962 );
2963 d.take_notifications();
2964
2965 feed(
2969 &mut d,
2970 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
2971 );
2972 let notes = d.take_notifications();
2973 assert!(
2974 !notes
2975 .iter()
2976 .any(|n| matches!(n, Notification::Entitlement { .. })),
2977 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
2978 );
2979 }
2980
2981 #[test]
2982 fn requery_disabled_by_default_sends_nothing() {
2983 use broadcast_common::Parse;
2984
2985 let mut d = driver_with_sessions();
2989 d.take_notifications();
2990
2991 let pmt_bytes = build_ca_pmt_fixture(1549);
2992 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2993 d.add_service(&pmt).unwrap();
2994 d.device_mut().inbound.push_back(sb());
2995 d.pump(Duration::from_millis(10)).unwrap();
2996
2997 let expected_query =
3000 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3001 let expected_ok_descrambling = build_ca_pmt(
3002 &pmt,
3003 CaPmtListManagement::Update,
3004 CaPmtCmdId::OkDescrambling,
3005 )
3006 .to_bytes();
3007 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
3008 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
3009
3010 d.pump(Duration::from_secs(1_000_000)).unwrap();
3012 feed(&mut d, sb());
3013 feed(&mut d, sb());
3014 assert_eq!(
3015 count_apdu_on_session(&d, CA_SESSION, &expected_query),
3016 query_sends_before,
3017 "the default (Duration::ZERO) must never send a re-query `query`"
3018 );
3019 assert_eq!(
3020 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
3021 ok_sends_before,
3022 "the default (Duration::ZERO) must never send a re-query `ok_descrambling`"
3023 );
3024 }
3025
3026 #[test]
3027 fn set_requery_interval_zero_disables_resend_after_being_enabled() {
3028 use broadcast_common::Parse;
3029
3030 let mut d = driver_with_sessions();
3031 d.set_requery_interval(Duration::from_secs(5));
3034 d.set_requery_interval(Duration::ZERO);
3035 d.take_notifications();
3036
3037 let pmt_bytes = build_ca_pmt_fixture(1549);
3038 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3039 d.add_service(&pmt).unwrap();
3040 d.device_mut().inbound.push_back(sb());
3041 d.pump(Duration::from_millis(10)).unwrap();
3042
3043 let expected_ca_pmt = build_ca_pmt(
3047 &pmt,
3048 CaPmtListManagement::Update,
3049 CaPmtCmdId::OkDescrambling,
3050 )
3051 .to_bytes();
3052 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3053
3054 d.pump(Duration::from_secs(1000)).unwrap();
3056 feed(&mut d, sb());
3057 feed(&mut d, sb());
3058
3059 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3060 assert_eq!(
3061 sends_after, sends_before,
3062 "Duration::ZERO must disable the re-query resend"
3063 );
3064 }
3065
3066 #[test]
3067 fn requery_timer_resends_every_active_service_not_just_one() {
3068 use broadcast_common::Parse;
3069
3070 let clock = TestClock::new();
3071 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3072 d.set_requery_interval(Duration::from_secs(10));
3075 d.take_notifications();
3076
3077 let pmt1_bytes = build_ca_pmt_fixture(1546);
3080 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3081 d.add_service(&pmt1).unwrap();
3082 d.device_mut().inbound.push_back(sb());
3083 clock.advance(Duration::from_millis(10));
3084 d.pump(Duration::from_millis(10)).unwrap();
3085
3086 let pmt2_bytes = build_ca_pmt_fixture(1547);
3087 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3088 d.add_service(&pmt2).unwrap();
3089 d.device_mut().inbound.push_back(sb());
3090 clock.advance(Duration::from_millis(10));
3091 d.pump(Duration::from_millis(10)).unwrap();
3092 d.take_notifications();
3093
3094 let expected1_query =
3100 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3101 let expected1_ok = build_ca_pmt(
3102 &pmt1,
3103 CaPmtListManagement::Update,
3104 CaPmtCmdId::OkDescrambling,
3105 )
3106 .to_bytes();
3107 let expected2_query =
3108 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3109 let expected2_ok = build_ca_pmt(
3110 &pmt2,
3111 CaPmtListManagement::Update,
3112 CaPmtCmdId::OkDescrambling,
3113 )
3114 .to_bytes();
3115 let sends_before1_query = count_apdu_on_session(&d, CA_SESSION, &expected1_query);
3116 let sends_before1_ok = count_apdu_on_session(&d, CA_SESSION, &expected1_ok);
3117 let sends_before2_query = count_apdu_on_session(&d, CA_SESSION, &expected2_query);
3118 let sends_before2_ok = count_apdu_on_session(&d, CA_SESSION, &expected2_ok);
3119
3120 clock.advance(Duration::from_secs(11));
3126 d.pump(Duration::from_secs(11)).unwrap();
3127 feed(&mut d, sb());
3128 feed(&mut d, sb());
3129
3130 assert_eq!(
3131 count_apdu_on_session(&d, CA_SESSION, &expected1_query),
3132 sends_before1_query + 1,
3133 "expected service 1546's `query` resent exactly once on the shared tick"
3134 );
3135 assert_eq!(
3136 count_apdu_on_session(&d, CA_SESSION, &expected1_ok),
3137 sends_before1_ok + 1,
3138 "expected service 1546's `ok_descrambling` resent exactly once on the shared tick"
3139 );
3140 assert_eq!(
3141 count_apdu_on_session(&d, CA_SESSION, &expected2_query),
3142 sends_before2_query + 1,
3143 "expected service 1547's `query` resent exactly once on the shared tick"
3144 );
3145 assert_eq!(
3146 count_apdu_on_session(&d, CA_SESSION, &expected2_ok),
3147 sends_before2_ok + 1,
3148 "expected service 1547's `ok_descrambling` resent exactly once on the shared tick"
3149 );
3150 }
3151
3152 #[test]
3156 fn requery_after_remove_resends_only_the_surviving_service() {
3157 use broadcast_common::Parse;
3158
3159 let clock = TestClock::new();
3160 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3161 d.set_requery_interval(Duration::from_secs(10));
3164 d.take_notifications();
3165
3166 let pmt1_bytes = build_ca_pmt_fixture(1546);
3167 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3168 d.add_service(&pmt1).unwrap();
3169 d.device_mut().inbound.push_back(sb());
3170 clock.advance(Duration::from_millis(10));
3171 d.pump(Duration::from_millis(10)).unwrap();
3172
3173 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3174 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3175 d.add_service(&pmt2).unwrap();
3176 d.device_mut().inbound.push_back(sb());
3177 clock.advance(Duration::from_millis(10));
3178 d.pump(Duration::from_millis(10)).unwrap();
3179
3180 d.remove_service(1546).unwrap();
3182 d.device_mut().inbound.push_back(sb());
3183 clock.advance(Duration::from_millis(10));
3184 d.pump(Duration::from_millis(10)).unwrap();
3185 d.take_notifications();
3186
3187 let expected_survivor_query =
3193 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3194 let expected_survivor_ok = build_ca_pmt(
3195 &pmt2,
3196 CaPmtListManagement::Update,
3197 CaPmtCmdId::OkDescrambling,
3198 )
3199 .to_bytes();
3200 let expected_removed_query =
3201 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3202 let expected_removed_ok = build_ca_pmt(
3203 &pmt1,
3204 CaPmtListManagement::Update,
3205 CaPmtCmdId::OkDescrambling,
3206 )
3207 .to_bytes();
3208 let sends_survivor_query_before =
3209 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query);
3210 let sends_survivor_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok);
3211 let sends_removed_query_before =
3212 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query);
3213 let sends_removed_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok);
3214
3215 clock.advance(Duration::from_secs(11));
3216 d.pump(Duration::from_secs(11)).unwrap();
3217 feed(&mut d, sb());
3218
3219 assert_eq!(
3220 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query),
3221 sends_survivor_query_before + 1,
3222 "the surviving service's `query` must be re-sent"
3223 );
3224 assert_eq!(
3225 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok),
3226 sends_survivor_ok_before + 1,
3227 "the surviving service's `ok_descrambling` must be re-sent"
3228 );
3229 assert_eq!(
3230 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query),
3231 sends_removed_query_before,
3232 "the removed service's `query` must NOT be re-sent"
3233 );
3234 assert_eq!(
3235 count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok),
3236 sends_removed_ok_before,
3237 "the removed service's `ok_descrambling` must NOT be re-sent"
3238 );
3239 }
3240
3241 #[test]
3244 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
3245 use broadcast_common::Parse;
3246
3247 let mut d = driver_with_sessions();
3248 d.take_notifications();
3249
3250 let pmt1_bytes = build_ca_pmt_fixture(1546);
3254 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3255 d.add_service(&pmt1).unwrap();
3256 d.device_mut().inbound.push_back(sb());
3257 d.pump(Duration::from_millis(10)).unwrap();
3258
3259 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3260 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3261 d.add_service(&pmt2).unwrap();
3262 d.device_mut().inbound.push_back(sb());
3263 d.pump(Duration::from_millis(10)).unwrap();
3264
3265 d.remove_service(1546).unwrap();
3266 d.device_mut().inbound.push_back(sb());
3267 d.pump(Duration::from_millis(10)).unwrap();
3268
3269 let expected =
3273 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3274 assert_apdu_on_session(&d, CA_SESSION, &expected);
3275
3276 assert_eq!(
3277 d.descramble_pids(),
3278 &[Pid::new(0x0200), Pid::new(0x0201)],
3279 "1546's ES PIDs must be gone; 1547's must remain"
3280 );
3281 assert!(
3282 d.managed_ca().services().get(&1546).is_none(),
3283 "1546 must no longer be tracked"
3284 );
3285 assert!(
3286 d.managed_ca().services().get(&1547).is_some(),
3287 "1547 must remain tracked"
3288 );
3289 }
3290
3291 #[test]
3292 fn remove_service_of_untracked_program_is_a_no_op() {
3293 let mut d = driver_with_sessions();
3294 d.take_notifications();
3295
3296 let ops_before = d.device().ops.len();
3297 d.remove_service(0xFFFF).unwrap();
3298 assert_eq!(
3299 d.device().ops.len(),
3300 ops_before,
3301 "removing an untracked program must not send anything to the device"
3302 );
3303 assert!(
3304 d.managed_ca().services().is_empty(),
3305 "removing an untracked program must not disturb the (empty) managed set"
3306 );
3307 }
3308
3309 #[test]
3310 fn cam_removed_edge_clears_managed_state() {
3311 use broadcast_common::Parse;
3312 use dvb_ci::objects::ca_info::CaInfo;
3313 use dvb_si::tables::cat::CatSection;
3314
3315 let mut d = driver_with_sessions();
3316 d.take_notifications();
3317
3318 let pmt_bytes = build_ca_pmt_fixture(1546);
3319 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3320 d.add_service(&pmt).unwrap();
3321 d.device_mut().inbound.push_back(sb());
3322 d.pump(Duration::from_millis(10)).unwrap();
3323
3324 feed(
3327 &mut d,
3328 r_apdu(
3329 CA_SESSION,
3330 &ser(&CaInfo {
3331 length_form: dvb_ci::length::LengthForm::Minimal,
3332 ca_system_ids: vec![0x0648],
3333 }),
3334 ),
3335 );
3336 d.take_notifications();
3337 let mut descriptors = Vec::new();
3338 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
3339 let cat_bytes = build_cat_fixture(&descriptors);
3340 let cat = CatSection::parse(&cat_bytes).unwrap();
3341 d.set_cat(&cat).unwrap();
3342
3343 assert!(
3344 !d.managed_ca().services().is_empty(),
3345 "precondition: a service is tracked"
3346 );
3347 assert!(
3348 !d.descramble_pids().is_empty(),
3349 "precondition: descramble_pids populated"
3350 );
3351 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
3352
3353 d.device_mut().slot.module_present = false;
3355 d.pump(Duration::from_millis(10)).unwrap();
3356 let notes = d.take_notifications();
3357 assert!(
3358 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
3359 "expected CamRemoved, got {notes:?}"
3360 );
3361
3362 assert!(
3363 d.managed_ca().services().is_empty(),
3364 "services must be cleared on CamRemoved"
3365 );
3366 assert!(
3367 d.descramble_pids().is_empty(),
3368 "descramble_pids must be cleared on CamRemoved"
3369 );
3370 assert!(
3371 d.emm_pids().is_empty(),
3372 "emm_pids must be cleared on CamRemoved"
3373 );
3374 }
3375}