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 fn build_service_bytes(
268 &self,
269 pmt: &PmtSection<'_>,
270 list_management: CaPmtListManagement,
271 cmd_id: CaPmtCmdId,
272 ) -> Result<(Vec<u8>, Vec<u8>), CaError> {
273 if !managed::pmt_has_ca(pmt) {
274 return Err(CaError::NoCaDescriptor {
275 program_number: pmt.program_number,
276 });
277 }
278 let built_bytes = self
280 .build_ca_pmt_filtered(pmt, list_management, cmd_id)
281 .try_to_bytes()?;
282 let mut pmt_raw = vec![0u8; pmt.serialized_len()];
283 let n = pmt.serialize_into(&mut pmt_raw)?;
284 pmt_raw.truncate(n);
285 Ok((built_bytes, pmt_raw))
286 }
287
288 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
308 let list_management = if self.managed.is_empty() {
309 CaPmtListManagement::Only
310 } else {
311 CaPmtListManagement::Add
312 };
313 let cmd_id = CaPmtCmdId::OkDescrambling;
314 let (built_bytes, pmt_raw) = self.build_service_bytes(pmt, list_management, cmd_id)?;
315 self.send_ca_pmt(&built_bytes)?;
316 self.managed.record(
317 pmt.program_number,
318 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
319 );
320 Ok(())
321 }
322
323 pub fn update_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
348 if !self.managed.services().contains_key(&pmt.program_number) {
349 return Err(CaError::NotManaged {
350 program_number: pmt.program_number,
351 });
352 }
353 let list_management = CaPmtListManagement::Update;
354 let cmd_id = CaPmtCmdId::OkDescrambling;
355 let (built_bytes, pmt_raw) = self.build_service_bytes(pmt, list_management, cmd_id)?;
356 self.send_ca_pmt(&built_bytes)?;
357 self.managed.record(
358 pmt.program_number,
359 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
360 );
361 Ok(())
362 }
363
364 pub fn remove_service(&mut self, program_number: u16) -> Result<(), CaError> {
378 let raw = self
379 .managed
380 .services()
381 .get(&program_number)
382 .map(|s| s.pmt_raw.clone());
383 let Some(raw) = raw else {
384 return Ok(());
385 };
386 self.remove_program(&raw)?;
387 self.managed.remove(program_number);
388 Ok(())
389 }
390
391 pub fn set_requery_interval(&mut self, interval: Duration) {
400 self.managed.set_requery_interval(interval);
401 }
402
403 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
418 let entries = cat.ca_descriptors().map_err(CaError::Cat)?;
419 self.managed.set_cat(&entries);
420 Ok(())
421 }
422
423 #[must_use]
427 pub fn emm_pids(&self) -> &[Pid] {
428 self.managed.emm_pids()
429 }
430
431 #[must_use]
434 pub fn descramble_pids(&self) -> &[Pid] {
435 self.managed.descramble_pids()
436 }
437
438 #[must_use]
443 pub fn ca_pids(&self) -> &[Pid] {
444 self.managed.ca_pids()
445 }
446
447 #[must_use]
455 pub fn required_pids(&self) -> Vec<Pid> {
456 self.managed.required_pids()
457 }
458
459 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
461 let actions = self
462 .stack
463 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
464 self.run(actions)
465 }
466
467 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
469 let actions = self
470 .stack
471 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
472 self.run(actions)
473 }
474
475 pub fn mmi_cancel(&mut self) -> io::Result<()> {
477 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
478 self.run(actions)
479 }
480
481 pub fn enter_menu(&mut self) -> io::Result<()> {
484 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
485 self.run(actions)
486 }
487
488 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
497 let now = (self.clock)();
508 let mut elapsed = self
509 .last_pump
510 .map_or(timeout, |prev| now.saturating_duration_since(prev));
511 self.last_pump = Some(now);
512
513 if let Some(until) = self.settle_until {
518 if now < until {
519 self.run(vec![Action::QuerySlot])?;
520 return Ok(false);
521 }
522 self.settle_until = None;
523 let mut pending = std::mem::take(&mut self.deferred_writes).into_iter();
524 while let Some(w) = pending.next() {
525 match self.device.write(&w) {
526 Ok(()) => {}
527 Err(e)
532 if e.kind() == io::ErrorKind::InvalidInput
533 && self.slot_ready_deadline.is_some_and(|d| now < d) =>
534 {
535 self.deferred_writes = std::iter::once(w).chain(pending).collect();
536 self.settle_until = Some(now + SLOT_RETRY);
537 return Ok(false);
538 }
539 Err(e) => return Err(e),
540 }
541 }
542 self.slot_ready_deadline = None;
543 elapsed = Duration::ZERO;
546 }
547
548 self.run(vec![Action::QuerySlot])?;
549 if self.device.poll(timeout)? {
550 let n = self.device.read(&mut self.buf)?;
551 if n > 0 {
552 let actions = self.stack.handle(Event::Readable(&self.buf[..n]));
556 self.run(actions)?;
557 return Ok(true);
558 }
559 }
560 let actions = self.stack.handle(Event::Tick { elapsed });
561 self.run(actions)?;
562 self.requery_tick(elapsed)?;
563 Ok(false)
564 }
565
566 fn requery_tick(&mut self, elapsed: Duration) -> io::Result<()> {
610 if let Some(err) = self.requery_pending.take() {
614 return Err(io::Error::other(err));
615 }
616 if !self.managed.tick(elapsed) {
617 return Ok(());
618 }
619 let mut ca_pmts: Vec<(Vec<u8>, Vec<u8>)> =
620 Vec::with_capacity(self.managed.services().len());
621 for s in self.managed.services().values() {
622 let outcome: Result<(Vec<u8>, Vec<u8>), CaError> = (|| {
627 let pmt = PmtSection::parse(&s.pmt_raw).map_err(CaError::PmtParse)?;
628 let query = self
629 .build_ca_pmt_filtered(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query)
630 .try_to_bytes()?;
631 let ok_descrambling = self
632 .build_ca_pmt_filtered(
633 &pmt,
634 CaPmtListManagement::Update,
635 CaPmtCmdId::OkDescrambling,
636 )
637 .try_to_bytes()?;
638 Ok((query, ok_descrambling))
639 })();
640 match outcome {
641 Ok(pair) => ca_pmts.push(pair),
642 Err(err) => {
643 self.requery_pending = Some(err);
647 }
648 }
649 }
650 for (query, ok_descrambling) in ca_pmts {
651 self.send_ca_pmt(&query)?;
652 self.send_ca_pmt(&ok_descrambling)?;
653 }
654 if let Some(err) = self.requery_pending.take() {
655 return Err(io::Error::other(err));
656 }
657 Ok(())
658 }
659
660 pub fn pump_with<F: FnMut(&Notification)>(
669 &mut self,
670 timeout: Duration,
671 mut handler: F,
672 ) -> io::Result<bool> {
673 let progressed = self.pump(timeout)?;
674 for n in self.take_notifications() {
675 handler(&n);
676 }
677 Ok(progressed)
678 }
679
680 pub fn pump_hotplug<F: FnMut(HotPlug)>(
684 &mut self,
685 timeout: Duration,
686 mut handler: F,
687 ) -> io::Result<bool> {
688 self.pump_with(timeout, |n| {
689 if let Some(h) = n.hotplug() {
690 handler(h);
691 }
692 })
693 }
694
695 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
697 for action in actions {
698 match action {
699 Action::Write(bytes) => {
700 if self.settle_until.is_some() {
701 self.deferred_writes.push(bytes);
702 } else {
703 self.device.write(&bytes)?;
704 }
705 }
706 Action::Reset => {
707 self.device.reset()?;
708 self.deferred_writes.clear();
711 let settle = self.device.reset_settle();
712 let now = (self.clock)();
713 self.settle_until = (!settle.is_zero()).then(|| now + settle);
714 self.slot_ready_deadline =
715 (!settle.is_zero()).then(|| now + settle + SLOT_READY_GRACE);
716 }
717 Action::QuerySlot => {
718 let info = self.device.slot_info()?;
719 self.handle_slot_info(info)?;
720 }
721 Action::SetTimer { after } => self.next_timer = Some(after),
722 Action::Notify(n) => {
723 let inferred = self.infer_card(&n);
724 self.notifications.push(n);
725 self.notifications.extend(inferred);
726 }
727 }
728 }
729 Ok(())
730 }
731
732 fn handle_slot_info(&mut self, info: SlotInfo) -> io::Result<()> {
739 let prev = self.last_slot.replace(info);
740 match prev {
741 Some(prev) if !prev.module_present && info.module_present => {
742 self.notifications
743 .push(Notification::HotPlug(HotPlug::CamPresent));
744 self.reset_module_state();
745 let actions = self.stack.handle(Event::Host(HostRequest::Init));
749 self.run(actions)?;
750 }
751 Some(prev) if prev.module_present && !info.module_present => {
752 self.notifications
753 .push(Notification::HotPlug(HotPlug::CamRemoved));
754 self.reset_module_state();
755 }
756 _ => {}
757 }
758 Ok(())
759 }
760
761 fn reset_module_state(&mut self) {
771 self.stack = CiStack::new();
772 self.settle_until = None;
773 self.slot_ready_deadline = None;
774 self.deferred_writes.clear();
775 self.next_timer = None;
776 self.last_caids = None;
777 self.last_descrambling_ok = None;
778 self.last_mmi_card_present = None;
779 self.managed.clear();
780 }
781
782 fn infer_card(&mut self, note: &Notification) -> Vec<Notification> {
789 match note {
790 Notification::CaInfo { ca_system_ids } => {
791 let new_set: BTreeSet<u16> = ca_system_ids.iter().copied().collect();
792 let mut out = Vec::new();
793 if let Some(prev) = &self.last_caids {
794 if prev.is_empty() && !new_set.is_empty() {
795 out.push(Notification::HotPlug(HotPlug::CardInserted));
796 } else if !prev.is_empty() && new_set.is_empty() {
797 out.push(Notification::HotPlug(HotPlug::CardRemoved));
798 } else if !prev.is_empty() && !new_set.is_empty() && *prev != new_set {
799 out.push(Notification::HotPlug(HotPlug::CardChanged));
800 }
801 }
802 self.managed.set_cam_caids(new_set.clone());
806 self.last_caids = Some(new_set);
807 out
808 }
809 Notification::CaPmtReply {
810 program_number,
811 ca_enable,
812 descrambling_ok,
813 } => {
814 let mut out = Vec::new();
815 if let Some(prev) = self.last_descrambling_ok {
816 if !prev && *descrambling_ok {
817 out.push(Notification::HotPlug(HotPlug::CardInserted));
818 } else if prev && !*descrambling_ok {
819 out.push(Notification::HotPlug(HotPlug::CardRemoved));
820 }
821 }
822 self.last_descrambling_ok = Some(*descrambling_ok);
823 if let Some((v, ok)) =
827 self.managed
828 .record_reply(*program_number, *ca_enable, *descrambling_ok)
829 {
830 out.push(Notification::Entitlement {
831 program_number: *program_number,
832 ca_enable: v,
833 descrambling_ok: ok,
834 });
835 }
836 out
837 }
838 Notification::Mmi(ev) => {
839 let present = Self::mmi_text(ev).and_then(|text| {
840 let lower = text.to_lowercase();
841 if MMI_CARD_ABSENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
842 Some(false)
843 } else if MMI_CARD_PRESENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
844 Some(true)
845 } else {
846 None
847 }
848 });
849 match present {
850 Some(p) if self.last_mmi_card_present != Some(p) => {
852 self.last_mmi_card_present = Some(p);
853 vec![Notification::HotPlug(if p {
854 HotPlug::CardInserted
855 } else {
856 HotPlug::CardRemoved
857 })]
858 }
859 _ => Vec::new(),
860 }
861 }
862 _ => Vec::new(),
863 }
864 }
865
866 fn mmi_text(ev: &MmiEvent) -> Option<String> {
870 match ev {
871 MmiEvent::Menu(m) | MmiEvent::List(m) => {
872 let mut s = format!("{} {} {}", m.title, m.subtitle, m.bottom);
873 for choice in &m.choices {
874 s.push(' ');
875 s.push_str(choice);
876 }
877 Some(s)
878 }
879 MmiEvent::Enquiry { prompt, .. } => Some(prompt.clone()),
880 MmiEvent::Close => None,
881 }
882 }
883}
884
885#[cfg(test)]
886pub(crate) mod tests {
887 use super::*;
888 use crate::device::{DeviceOp, MockCaDevice};
889 use crate::event::{HostControlEvent, HotPlug, Notification};
890 use broadcast_common::Serialize;
891 use dvb_ci::tpdu::tags;
892
893 pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
894 let mut b = vec![0u8; s.serialized_len()];
895 match s.serialize_into(&mut b) {
896 Ok(n) => b.truncate(n),
897 Err(_) => b.clear(),
898 }
899 b
900 }
901
902 #[derive(Clone)]
908 pub(crate) struct TestClock(std::rc::Rc<std::cell::Cell<Instant>>);
909
910 impl TestClock {
911 pub(crate) fn new() -> Self {
912 Self(std::rc::Rc::new(std::cell::Cell::new(Instant::now())))
913 }
914
915 pub(crate) fn advance(&self, d: Duration) {
917 self.0.set(self.0.get() + d);
918 }
919
920 pub(crate) fn as_fn(&self) -> impl Fn() -> Instant + 'static {
922 let cell = self.0.clone();
923 move || cell.get()
924 }
925 }
926
927 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
930 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
931 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
932 v.extend_from_slice(spdu);
933 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
934 v
935 }
936
937 pub(crate) fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
940 use dvb_ci::spdu::SessionNumber;
941 let mut spdu = ser(&SessionNumber {
942 length_form: dvb_ci::length::LengthForm::Minimal,
943 session_nb,
944 });
945 spdu.extend_from_slice(apdu);
946 r_data(1, &spdu)
947 }
948
949 pub(crate) fn sb() -> Vec<u8> {
952 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
953 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
954 }
955
956 pub(crate) fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
959 d.device_mut().inbound.push_back(frame);
960 d.pump(Duration::from_millis(10)).unwrap();
961 for _ in 0..8 {
962 d.device_mut().inbound.push_back(sb());
963 d.pump(Duration::from_millis(10)).unwrap();
964 }
965 }
966
967 pub(crate) fn driver_with_sessions() -> Driver<MockCaDevice> {
971 use dvb_ci::objects::resource_manager::Profile;
972 use dvb_ci::resource::{
973 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
974 RESOURCE_MANAGER,
975 };
976 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
977
978 let mut d = Driver::new(MockCaDevice::new([]));
979 d.init().unwrap();
980 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
982 feed(
984 &mut d,
985 r_data(
986 1,
987 &ser(&OpenSessionRequest {
988 length_form: dvb_ci::length::LengthForm::Minimal,
989 resource: RESOURCE_MANAGER,
990 }),
991 ),
992 );
993 feed(
996 &mut d,
997 r_apdu(
998 1,
999 &ser(&Profile {
1000 length_form: dvb_ci::length::LengthForm::Minimal,
1001 resources: vec![
1002 APPLICATION_INFORMATION,
1003 CONDITIONAL_ACCESS_SUPPORT,
1004 MMI,
1005 HOST_CONTROL,
1006 ],
1007 }),
1008 ),
1009 );
1010 for (nb, res) in [
1012 (2u16, APPLICATION_INFORMATION),
1013 (3, CONDITIONAL_ACCESS_SUPPORT),
1014 (4, MMI),
1015 (5, HOST_CONTROL),
1016 ] {
1017 feed(
1018 &mut d,
1019 r_data(
1020 1,
1021 &ser(&CreateSessionResponse {
1022 length_form: dvb_ci::length::LengthForm::Minimal,
1023 status: SessionStatus::Ok,
1024 resource: res,
1025 session_nb: nb,
1026 }),
1027 ),
1028 );
1029 }
1030 d
1031 }
1032
1033 const RM_SESSION: u16 = 1;
1037 pub(crate) const CA_SESSION: u16 = 3;
1038 const MMI_SESSION: u16 = 4;
1039 const HOST_CONTROL_SESSION: u16 = 5;
1040
1041 #[test]
1042 fn host_control_tune_apdu_surfaces_notification_via_driver() {
1043 use dvb_ci::objects::host_control::Tune;
1044
1045 let mut d = driver_with_sessions();
1046 let hc_nb = HOST_CONTROL_SESSION;
1047 d.take_notifications(); let tune = Tune {
1051 length_form: dvb_ci::length::LengthForm::Minimal,
1052 network_id: 0x1122,
1053 original_network_id: 0x3344,
1054 transport_stream_id: 0x5566,
1055 service_id: 0x7788,
1056 };
1057 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
1058
1059 let notes = d.take_notifications();
1061 assert!(
1062 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
1063 network_id: 0x1122,
1064 original_network_id: 0x3344,
1065 transport_stream_id: 0x5566,
1066 service_id: 0x7788,
1067 })),
1068 "expected HostControl(Tune) notification, got {notes:?}"
1069 );
1070 }
1071
1072 #[test]
1075 fn padded_tune_from_the_cam_surfaces_an_error_notification() {
1076 let mut d = driver_with_sessions();
1077 d.take_notifications();
1078 let padded = [
1080 0x9F, 0x84, 0x00, 0x09, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xEE,
1081 ];
1082 feed(&mut d, r_apdu(HOST_CONTROL_SESSION, &padded));
1083 let notes = d.take_notifications();
1084 assert!(
1085 notes.iter().any(
1086 |n| matches!(n, Notification::Error { detail } if detail.contains("trailing bytes"))
1087 ),
1088 "expected an Error notification naming the trailing bytes, got {notes:?}"
1089 );
1090 assert!(
1091 !notes
1092 .iter()
1093 .any(|n| matches!(n, Notification::HostControl(_))),
1094 "a refused tune must not also surface as a HostControl event: {notes:?}"
1095 );
1096 }
1097
1098 #[test]
1101 fn malformed_mmi_apdu_from_the_cam_surfaces_an_error_notification() {
1102 let mut d = driver_with_sessions();
1103 d.take_notifications();
1104 feed(&mut d, r_apdu(MMI_SESSION, &[0x9F, 0x88, 0x07, 0x01, 0x00]));
1106 let notes = d.take_notifications();
1107 assert!(
1108 notes
1109 .iter()
1110 .any(|n| matches!(n, Notification::Error { .. })),
1111 "expected an Error notification, got {notes:?}"
1112 );
1113 }
1114
1115 #[test]
1116 fn profile_reply_advertises_host_control() {
1117 use broadcast_common::Parse;
1118 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
1119 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
1120
1121 let mut d = Driver::new(MockCaDevice::new([]));
1122 d.init().unwrap();
1123 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1124 feed(
1126 &mut d,
1127 r_data(
1128 1,
1129 &ser(&dvb_ci::spdu::OpenSessionRequest {
1130 length_form: dvb_ci::length::LengthForm::Minimal,
1131 resource: RESOURCE_MANAGER,
1132 }),
1133 ),
1134 );
1135 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq::default())));
1137
1138 let want = dvb_ci::tag::PROFILE.to_bytes();
1141 let found = d.device().ops.iter().any(|op| {
1142 if let DeviceOp::Write(w) = op
1143 && let Some(pos) = w.windows(3).position(|x| x == want)
1144 && let Ok(p) = Profile::parse(&w[pos..])
1145 {
1146 return p.resources.contains(&HOST_CONTROL);
1147 }
1148 false
1149 });
1150 assert!(found, "profile reply must advertise HOST_CONTROL");
1151 }
1152
1153 #[test]
1154 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
1155 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
1156
1157 let mut d = driver_with_sessions();
1158 let mmi_nb = MMI_SESSION;
1159
1160 d.mmi_menu_answer(2).unwrap();
1163 d.device_mut().inbound.push_back(sb());
1164 d.pump(Duration::from_millis(10)).unwrap();
1165 assert_apdu_on_session(
1166 &d,
1167 mmi_nb,
1168 &ser(&MenuAnsw {
1169 length_form: dvb_ci::length::LengthForm::Minimal,
1170 choice_ref: 2,
1171 }),
1172 );
1173
1174 d.mmi_enquiry_answer(b"1234").unwrap();
1176 d.device_mut().inbound.push_back(sb());
1177 d.pump(Duration::from_millis(10)).unwrap();
1178 assert_apdu_on_session(
1179 &d,
1180 mmi_nb,
1181 &ser(&Answ {
1182 length_form: dvb_ci::length::LengthForm::Minimal,
1183 answ_id: AnswId::Answer,
1184 text_chars: b"1234",
1185 }),
1186 );
1187 }
1188
1189 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
1192 use dvb_ci::spdu::SessionNumber;
1193 let mut want = ser(&SessionNumber {
1194 length_form: dvb_ci::length::LengthForm::Minimal,
1195 session_nb,
1196 });
1197 want.extend_from_slice(apdu);
1198 let hit = d.device().ops.iter().any(|op| match op {
1199 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1200 _ => false,
1201 });
1202 assert!(
1203 hit,
1204 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
1205 );
1206 }
1207
1208 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
1212 use dvb_ci::spdu::SessionNumber;
1213 let mut want = ser(&SessionNumber {
1214 length_form: dvb_ci::length::LengthForm::Minimal,
1215 session_nb,
1216 });
1217 want.extend_from_slice(apdu);
1218 d.device()
1219 .ops
1220 .iter()
1221 .filter(|op| match op {
1222 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1223 _ => false,
1224 })
1225 .count()
1226 }
1227
1228 struct SettleDevice {
1231 inner: MockCaDevice,
1232 settle: Duration,
1233 }
1234
1235 impl CaDevice for SettleDevice {
1236 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1237 self.inner.read(buf)
1238 }
1239 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1240 self.inner.write(buf)
1241 }
1242 fn reset(&mut self) -> io::Result<()> {
1243 self.inner.reset()
1244 }
1245 fn reset_settle(&self) -> Duration {
1246 self.settle
1247 }
1248 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1249 self.inner.slot_info()
1250 }
1251 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1252 self.inner.poll(timeout)
1253 }
1254 }
1255
1256 const SETTLE: Duration = Duration::from_secs(3);
1257
1258 fn settling_driver(clock: &TestClock) -> Driver<SettleDevice> {
1259 Driver::new(SettleDevice {
1260 inner: MockCaDevice::new([]),
1261 settle: SETTLE,
1262 })
1263 .with_clock(clock.as_fn())
1264 }
1265
1266 fn create_tc_writes(d: &Driver<SettleDevice>) -> usize {
1267 d.device()
1268 .inner
1269 .ops
1270 .iter()
1271 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1272 .count()
1273 }
1274
1275 struct NotRunningDevice {
1279 inner: SettleDevice,
1280 refusals_left: u32,
1281 refused: u32,
1282 }
1283
1284 impl CaDevice for NotRunningDevice {
1285 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1286 self.inner.read(buf)
1287 }
1288 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1289 if self.refusals_left > 0 {
1290 self.refusals_left -= 1;
1291 self.refused += 1;
1292 return Err(io::Error::from(io::ErrorKind::InvalidInput));
1293 }
1294 self.inner.write(buf)
1295 }
1296 fn reset(&mut self) -> io::Result<()> {
1297 self.inner.reset()
1298 }
1299 fn reset_settle(&self) -> Duration {
1300 self.inner.reset_settle()
1301 }
1302 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1303 self.inner.slot_info()
1304 }
1305 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1306 self.inner.poll(timeout)
1307 }
1308 }
1309
1310 fn not_running_driver(clock: &TestClock, refusals: u32) -> Driver<NotRunningDevice> {
1311 Driver::new(NotRunningDevice {
1312 inner: SettleDevice {
1313 inner: MockCaDevice::new([]),
1314 settle: SETTLE,
1315 },
1316 refusals_left: refusals,
1317 refused: 0,
1318 })
1319 .with_clock(clock.as_fn())
1320 }
1321
1322 fn create_tc_count(d: &Driver<NotRunningDevice>) -> usize {
1323 d.device()
1324 .inner
1325 .inner
1326 .ops
1327 .iter()
1328 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1329 .count()
1330 }
1331
1332 #[test]
1337 fn post_reset_einval_is_retried_until_the_slot_is_running() {
1338 let clock = TestClock::new();
1339 let mut d = not_running_driver(&clock, 3);
1340 d.init().unwrap();
1341 clock.advance(SETTLE + Duration::from_millis(100));
1342 for _ in 0..10 {
1343 d.pump(Duration::from_millis(10))
1344 .expect("a not-yet-RUNNING slot (EINVAL) must be retried, not reported");
1345 if create_tc_count(&d) == 1 {
1346 break;
1347 }
1348 clock.advance(Duration::from_millis(300));
1349 }
1350 assert_eq!(d.device().refused, 3, "the three refusals were consumed");
1351 assert_eq!(create_tc_count(&d), 1, "Create_T_C delivered exactly once");
1352 }
1353
1354 #[test]
1357 fn post_reset_einval_is_reported_once_the_grace_window_has_passed() {
1358 let clock = TestClock::new();
1359 let mut d = not_running_driver(&clock, u32::MAX);
1360 let start = clock.0.get();
1361 d.init().unwrap();
1362 clock.advance(SETTLE + Duration::from_millis(100));
1363 let mut failure = None;
1364 for _ in 0..80 {
1365 match d.pump(Duration::from_millis(10)) {
1366 Ok(_) => clock.advance(Duration::from_millis(300)),
1367 Err(e) => {
1368 failure = Some(e);
1369 break;
1370 }
1371 }
1372 }
1373 let e = failure.expect("a slot that never runs must eventually be reported");
1374 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1375 assert!(
1376 clock.0.get() >= start + SETTLE + SLOT_READY_GRACE,
1377 "reported before the grace window ended"
1378 );
1379 assert_eq!(create_tc_count(&d), 0);
1380 }
1381
1382 #[test]
1387 fn requery_armed_before_the_handshake_sends_nothing() {
1388 let clock = TestClock::new();
1389 let mut d = Driver::new(MockCaDevice::new([])).with_clock(clock.as_fn());
1390 d.set_requery_interval(Duration::from_secs(10));
1391 d.init().unwrap();
1392 for _ in 0..40 {
1393 clock.advance(Duration::from_secs(1));
1394 d.pump(Duration::from_millis(100)).unwrap();
1395 }
1396 let ca_pmt_tag = [0x9F, 0x80, 0x32];
1397 let ca_pmt_writes = d
1398 .device()
1399 .ops
1400 .iter()
1401 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == ca_pmt_tag)))
1402 .count();
1403 assert_eq!(
1404 ca_pmt_writes, 0,
1405 "re-query wrote a ca_pmt before any service"
1406 );
1407 }
1408
1409 #[test]
1412 fn reset_does_not_block_pump() {
1413 let clock = TestClock::new();
1414 let mut d = settling_driver(&clock);
1415 let t = std::time::Instant::now();
1416 d.init().unwrap();
1417 d.pump(Duration::from_millis(10)).unwrap();
1418 assert!(t.elapsed() < Duration::from_secs(1), "reset/pump blocked");
1419 assert_eq!(d.device().inner.ops[0], DeviceOp::Reset);
1420 }
1421
1422 #[test]
1426 fn no_transport_traffic_until_settle_elapsed() {
1427 let clock = TestClock::new();
1428 let mut d = settling_driver(&clock);
1429 d.device_mut()
1430 .inner
1431 .inbound
1432 .push_back(vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1433 d.init().unwrap();
1434 for _ in 0..5 {
1435 clock.advance(Duration::from_millis(500));
1436 d.pump(Duration::from_millis(10)).unwrap();
1437 }
1438 assert_eq!(create_tc_writes(&d), 0);
1440 assert_eq!(
1441 d.device().inner.inbound.len(),
1442 1,
1443 "frame read while settling"
1444 );
1445
1446 clock.advance(Duration::from_millis(600));
1447 d.pump(Duration::from_millis(10)).unwrap();
1448 assert_eq!(create_tc_writes(&d), 1);
1449
1450 let mut z = Driver::new(MockCaDevice::new([]));
1451 z.init().unwrap();
1452 assert_eq!(
1453 d.device().inner.written()[..3],
1454 z.device().written()[..3],
1455 "post-settle Create_T_C differs from the zero-settle sequence"
1456 );
1457 assert!(d.device().inner.inbound.is_empty());
1460 }
1461
1462 #[test]
1465 fn second_reset_restarts_settle() {
1466 let clock = TestClock::new();
1467 let mut d = settling_driver(&clock);
1468 d.init().unwrap();
1469 clock.advance(Duration::from_secs(2));
1470 d.pump(Duration::from_millis(10)).unwrap();
1471 d.init().unwrap(); clock.advance(Duration::from_secs(2)); d.pump(Duration::from_millis(10)).unwrap();
1474 assert_eq!(create_tc_writes(&d), 0, "first deadline wrongly honoured");
1475 clock.advance(Duration::from_millis(1500)); d.pump(Duration::from_millis(10)).unwrap();
1477 assert_eq!(create_tc_writes(&d), 1);
1478 }
1479
1480 #[test]
1483 fn slot_sampling_continues_while_settling() {
1484 let clock = TestClock::new();
1485 let mut d = settling_driver(&clock);
1486 d.init().unwrap();
1487 d.take_notifications();
1488 d.device_mut().inner.slot.module_present = false;
1489 clock.advance(Duration::from_millis(500));
1490 d.pump(Duration::from_millis(10)).unwrap();
1491 let notes = d.take_notifications();
1492 assert!(
1493 notes
1494 .iter()
1495 .any(|n| matches!(n, Notification::HotPlug(HotPlug::CamRemoved))),
1496 "{notes:?}"
1497 );
1498 assert_eq!(create_tc_writes(&d), 0);
1499 }
1500
1501 #[test]
1502 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
1503 let mut d = Driver::new(MockCaDevice::new([]));
1504 d.init().unwrap();
1505 let ops = &d.device().ops;
1506 assert_eq!(ops[0], DeviceOp::Reset);
1507 assert_eq!(ops[1], DeviceOp::SlotInfo);
1508 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
1509 }
1510
1511 #[test]
1512 fn reads_reply_then_polls_on_pump() {
1513 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1515 let clock = TestClock::new();
1516 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1517 d.init().unwrap();
1518 assert!(d.pump(Duration::from_millis(100)).unwrap());
1520 clock.advance(Duration::from_millis(100));
1525 assert!(!d.pump(Duration::from_millis(100)).unwrap());
1527 let last = d.device().ops.last().unwrap();
1528 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1529 }
1530
1531 #[test]
1537 fn pump_advances_the_stack_clock_by_real_elapsed_time_not_by_timeout() {
1538 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1539 let clock = TestClock::new();
1540 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1541 d.init().unwrap();
1542 assert!(d.pump(Duration::from_secs(1)).unwrap());
1543
1544 clock.advance(Duration::from_millis(1));
1548 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1549 assert!(
1550 d.device().ops.last().is_none_or(|op| !matches!(
1551 op,
1552 DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)
1553 )),
1554 "must not poll after only 1ms of real elapsed time, regardless of the 1s `timeout` argument"
1555 );
1556
1557 clock.advance(Duration::from_millis(100));
1559 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1560 let last = d.device().ops.last().unwrap();
1561 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1562 }
1563
1564 #[test]
1567 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
1568 let mut dev = MockCaDevice::new([]);
1569 dev.slot = SlotInfo {
1570 num: 0,
1571 module_ready: false,
1572 module_present: false,
1573 };
1574 let mut d = Driver::new(dev);
1575 d.init().unwrap();
1576 let notes = d.take_notifications();
1579 assert!(
1580 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1581 "baseline observation must not fire CamPresent, got {notes:?}"
1582 );
1583 let resets_before = d
1584 .device()
1585 .ops
1586 .iter()
1587 .filter(|o| **o == DeviceOp::Reset)
1588 .count();
1589
1590 d.device_mut().slot = SlotInfo {
1592 num: 0,
1593 module_ready: true,
1594 module_present: true,
1595 };
1596 d.pump(Duration::from_millis(10)).unwrap();
1597
1598 let notes = d.take_notifications();
1599 let cam_present_count = notes
1600 .iter()
1601 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
1602 .count();
1603 assert_eq!(
1604 cam_present_count, 1,
1605 "expected exactly one CamPresent, got {notes:?}"
1606 );
1607 let resets_after = d
1609 .device()
1610 .ops
1611 .iter()
1612 .filter(|o| **o == DeviceOp::Reset)
1613 .count();
1614 assert_eq!(
1615 resets_after,
1616 resets_before + 1,
1617 "expected one fresh Reset on re-insert"
1618 );
1619 assert!(
1620 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
1621 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
1622 d.device().ops.last()
1623 );
1624 }
1625
1626 #[test]
1627 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
1628 let mut d = driver_with_sessions();
1629 d.take_notifications();
1630
1631 d.device_mut().slot.module_present = false;
1633 d.pump(Duration::from_millis(10)).unwrap();
1634 let notes = d.take_notifications();
1635 assert!(
1636 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
1637 "expected CamRemoved, got {notes:?}"
1638 );
1639
1640 d.mmi_menu_answer(0).unwrap();
1644 let notes = d.take_notifications();
1645 assert!(
1646 notes
1647 .iter()
1648 .any(|n| matches!(n, Notification::Error { .. })),
1649 "expected no open MMI session after teardown, got {notes:?}"
1650 );
1651
1652 let resets_before = d
1654 .device()
1655 .ops
1656 .iter()
1657 .filter(|o| **o == DeviceOp::Reset)
1658 .count();
1659 d.device_mut().slot.module_present = true;
1660 d.device_mut().slot.module_ready = true;
1661 d.pump(Duration::from_millis(10)).unwrap();
1662 let notes = d.take_notifications();
1663 assert!(
1664 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1665 "expected CamPresent on re-insert, got {notes:?}"
1666 );
1667 let resets_after = d
1668 .device()
1669 .ops
1670 .iter()
1671 .filter(|o| **o == DeviceOp::Reset)
1672 .count();
1673 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1674 }
1675
1676 #[test]
1677 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1678 let mut d = Driver::new(MockCaDevice::new([]));
1679 d.init().unwrap();
1680 d.take_notifications();
1681
1682 for _ in 0..5 {
1683 d.pump(Duration::from_millis(10)).unwrap();
1684 }
1685 let notes = d.take_notifications();
1686 assert!(
1687 !notes.iter().any(|n| matches!(
1688 n,
1689 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1690 )),
1691 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1692 );
1693 }
1694
1695 #[test]
1696 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1697 use dvb_ci::objects::ca_info::CaInfo;
1698
1699 let mut d = driver_with_sessions();
1700 d.take_notifications();
1701
1702 feed(
1704 &mut d,
1705 r_apdu(
1706 CA_SESSION,
1707 &ser(&CaInfo {
1708 length_form: dvb_ci::length::LengthForm::Minimal,
1709 ca_system_ids: vec![],
1710 }),
1711 ),
1712 );
1713 let notes = d.take_notifications();
1714 assert!(
1715 !notes.iter().any(|n| matches!(
1716 n,
1717 Notification::HotPlug(
1718 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1719 )
1720 )),
1721 "first ca_info must only establish the baseline, got {notes:?}"
1722 );
1723
1724 feed(
1726 &mut d,
1727 r_apdu(
1728 CA_SESSION,
1729 &ser(&CaInfo {
1730 length_form: dvb_ci::length::LengthForm::Minimal,
1731 ca_system_ids: vec![0x0B00],
1732 }),
1733 ),
1734 );
1735 let notes = d.take_notifications();
1736 assert!(
1737 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1738 "expected CardInserted, got {notes:?}"
1739 );
1740
1741 feed(
1743 &mut d,
1744 r_apdu(
1745 CA_SESSION,
1746 &ser(&CaInfo {
1747 length_form: dvb_ci::length::LengthForm::Minimal,
1748 ca_system_ids: vec![0x1800],
1749 }),
1750 ),
1751 );
1752 let notes = d.take_notifications();
1753 assert!(
1754 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1755 "expected CardChanged, got {notes:?}"
1756 );
1757 }
1758
1759 #[test]
1760 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1761 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1762
1763 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1764 CaPmtReply {
1765 reserved_ca_enable: Default::default(),
1766 reserved_version: Default::default(),
1767 length_form: dvb_ci::length::LengthForm::Minimal,
1768 program_number: 1,
1769 version_number: 1,
1770 current_next_indicator: true,
1771 ca_enable,
1772 streams: vec![],
1773 }
1774 }
1775
1776 let mut d = driver_with_sessions();
1777 d.take_notifications();
1778
1779 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1781 let notes = d.take_notifications();
1782 assert!(
1783 !notes.iter().any(|n| matches!(
1784 n,
1785 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1786 )),
1787 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1788 );
1789
1790 feed(
1792 &mut d,
1793 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1794 );
1795 let notes = d.take_notifications();
1796 assert!(
1797 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1798 "expected CardInserted, got {notes:?}"
1799 );
1800
1801 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1803 let notes = d.take_notifications();
1804 assert!(
1805 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1806 "expected CardRemoved, got {notes:?}"
1807 );
1808 }
1809
1810 #[test]
1811 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1812 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1813
1814 let mut d = driver_with_sessions();
1815 d.take_notifications();
1816
1817 feed(
1820 &mut d,
1821 r_apdu(
1822 CA_SESSION,
1823 &ser(&CaPmtReply {
1824 reserved_ca_enable: Default::default(),
1825 reserved_version: Default::default(),
1826 length_form: dvb_ci::length::LengthForm::Minimal,
1827 program_number: 7,
1828 version_number: 1,
1829 current_next_indicator: true,
1830 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1831 streams: vec![],
1832 }),
1833 ),
1834 );
1835 let notes = d.take_notifications();
1836 assert!(
1837 notes.contains(&Notification::CaPmtReply {
1838 program_number: 7,
1839 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1840 descrambling_ok: true,
1841 }),
1842 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1843 );
1844 }
1845
1846 #[test]
1847 fn ca_pmt_reply_flag_clear_surfaces_none() {
1848 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1849
1850 let mut d = driver_with_sessions();
1851 d.take_notifications();
1852
1853 feed(
1856 &mut d,
1857 r_apdu(
1858 CA_SESSION,
1859 &ser(&CaPmtReply {
1860 reserved_ca_enable: Default::default(),
1861 reserved_version: Default::default(),
1862 length_form: dvb_ci::length::LengthForm::Minimal,
1863 program_number: 7,
1864 version_number: 1,
1865 current_next_indicator: true,
1866 ca_enable: None,
1867 streams: vec![],
1868 }),
1869 ),
1870 );
1871 let notes = d.take_notifications();
1872 assert!(
1873 notes.contains(&Notification::CaPmtReply {
1874 program_number: 7,
1875 ca_enable: None,
1876 descrambling_ok: false,
1877 }),
1878 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1879 );
1880 }
1881
1882 #[test]
1883 fn mmi_no_card_text_infers_card_removed() {
1884 use dvb_ci::objects::mmi_high::Enq;
1885
1886 let mut d = driver_with_sessions();
1887 d.take_notifications();
1888
1889 feed(
1890 &mut d,
1891 r_apdu(
1892 MMI_SESSION,
1893 &ser(&Enq {
1894 reserved: Default::default(),
1895 length_form: dvb_ci::length::LengthForm::Minimal,
1896 blind_answer: false,
1897 answer_text_length: 0,
1898 text_chars: b"NO CARD detected - please insert your smart card",
1899 }),
1900 ),
1901 );
1902
1903 let notes = d.take_notifications();
1904 assert!(
1905 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1906 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1907 );
1908 }
1909
1910 #[test]
1913 fn mmi_card_keyword_heuristic_is_edge_triggered() {
1914 use dvb_ci::objects::mmi_high::Enq;
1915
1916 let enq = |text: &'static [u8]| {
1917 r_apdu(
1918 MMI_SESSION,
1919 &ser(&Enq {
1920 reserved: Default::default(),
1921 length_form: dvb_ci::length::LengthForm::Minimal,
1922 blind_answer: false,
1923 answer_text_length: 0,
1924 text_chars: text,
1925 }),
1926 )
1927 };
1928 let mut d = driver_with_sessions();
1929 d.take_notifications();
1930 let count = |notes: &[Notification], hp: HotPlug| {
1931 notes
1932 .iter()
1933 .filter(|n| **n == Notification::HotPlug(hp))
1934 .count()
1935 };
1936
1937 for _ in 0..3 {
1939 feed(&mut d, enq(b"No card - please insert your smart card"));
1940 }
1941 let notes = d.take_notifications();
1942 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1943 assert_eq!(count(¬es, HotPlug::CardInserted), 0);
1944
1945 for _ in 0..2 {
1947 feed(&mut d, enq(b"Your entitlement is valid"));
1948 }
1949 let notes = d.take_notifications();
1950 assert_eq!(count(¬es, HotPlug::CardInserted), 1, "{notes:?}");
1951 assert_eq!(count(¬es, HotPlug::CardRemoved), 0);
1952
1953 feed(&mut d, enq(b"Card removed"));
1955 feed(&mut d, enq(b"Card removed"));
1956 let notes = d.take_notifications();
1957 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1958 }
1959
1960 #[test]
1961 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1962 let mut dev = MockCaDevice::new([]);
1963 dev.slot = SlotInfo {
1964 num: 0,
1965 module_ready: false,
1966 module_present: false,
1967 };
1968 let mut d = Driver::new(dev);
1969 d.init().unwrap();
1970 d.take_notifications(); d.device_mut().slot = SlotInfo {
1974 num: 0,
1975 module_ready: true,
1976 module_present: true,
1977 };
1978
1979 let mut seen = Vec::new();
1980 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1981 .unwrap();
1982
1983 assert_eq!(
1984 seen,
1985 vec![HotPlug::CamPresent],
1986 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1987 );
1988 }
1989
1990 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: Pid) -> [u8; 6] {
1995 [
1996 0x09,
1997 0x04,
1998 (ca_system_id >> 8) as u8,
1999 ca_system_id as u8,
2000 0xE0 | ((pid.value() >> 8) as u8 & 0x1F),
2001 pid.value() as u8,
2002 ]
2003 }
2004
2005 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
2022 const VIACCESS: u16 = 0x0500;
2023 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2024 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2025
2026 let mut body = Vec::new();
2027 body.push(0x02); body.push(0); body.push(0);
2030 body.extend_from_slice(&program_number.to_be_bytes());
2031 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
2036 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2037 body.push(prog_ca.len() as u8);
2038 body.extend_from_slice(&prog_ca);
2039 body.push(0x1B);
2041 body.push(0xE0 | 0x01);
2042 body.push(0x00);
2043 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2044 body.push(es0_ca.len() as u8);
2045 body.extend_from_slice(&es0_ca);
2046 body.push(0x0F);
2048 body.push(0xE0 | 0x01);
2049 body.push(0x01);
2050 body.push(0xF0);
2051 body.push(0x00);
2052
2053 let section_length = body.len() - 3 + 4;
2054 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2055 body[2] = section_length as u8;
2056 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2057 body.extend_from_slice(&crc.to_be_bytes());
2058 body
2059 }
2060
2061 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
2068 const VIACCESS: u16 = 0x0500;
2069 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2070 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2071
2072 let mut body = Vec::new();
2073 body.push(0x02); body.push(0); body.push(0);
2076 body.extend_from_slice(&program_number.to_be_bytes());
2077 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));
2083 body.push(prog_ca.len() as u8);
2084 body.extend_from_slice(&prog_ca);
2085 body.push(0x1B);
2087 body.push(0xE0 | 0x01);
2088 body.push(0x00);
2089 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2090 body.push(es0_ca.len() as u8);
2091 body.extend_from_slice(&es0_ca);
2092 body.push(0x0F);
2094 body.push(0xE0 | 0x01);
2095 body.push(0x01);
2096 body.push(0xF0);
2097 body.push(0x00);
2098
2099 let section_length = body.len() - 3 + 4;
2100 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2101 body[2] = section_length as u8;
2102 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2103 body.extend_from_slice(&crc.to_be_bytes());
2104 body
2105 }
2106
2107 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
2111 let mut body = Vec::new();
2112 body.push(0x02);
2113 body.push(0);
2114 body.push(0);
2115 body.extend_from_slice(&program_number.to_be_bytes());
2116 body.push(0xC3);
2117 body.push(0x00);
2118 body.push(0x00);
2119 body.push(0xE0 | 0x01);
2120 body.push(0x00);
2121 body.push(0xF0); body.push(0x00);
2123 body.push(0x1B);
2125 body.push(0xE0 | 0x01);
2126 body.push(0x00);
2127 body.push(0xF0);
2128 body.push(0x00);
2129
2130 let section_length = body.len() - 3 + 4;
2131 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2132 body[2] = section_length as u8;
2133 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2134 body.extend_from_slice(&crc.to_be_bytes());
2135 body
2136 }
2137
2138 #[test]
2139 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
2140 use broadcast_common::Parse;
2141
2142 let mut d = driver_with_sessions();
2143 d.take_notifications();
2144
2145 let pmt_bytes = build_ca_pmt_fixture(1546);
2146 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2147
2148 d.add_service(&pmt).unwrap();
2149 d.device_mut().inbound.push_back(sb());
2150 d.pump(Duration::from_millis(10)).unwrap();
2151
2152 let expected =
2156 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2157 assert_apdu_on_session(&d, CA_SESSION, &expected);
2158
2159 let svc = d
2161 .managed_ca()
2162 .services()
2163 .get(&1546)
2164 .expect("program_number 1546 must be tracked after add_service");
2165 assert_eq!(svc.es_pids, vec![Pid::new(0x0100), Pid::new(0x0101)]);
2166 assert_eq!(svc.ca_pids, vec![Pid::new(0x0064), Pid::new(0x0065)]);
2167 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
2168 assert_eq!(svc.last_ca_enable, None);
2169 }
2170
2171 #[test]
2172 fn add_service_rejects_pmt_without_ca_descriptor() {
2173 use broadcast_common::Parse;
2174
2175 let mut d = driver_with_sessions();
2176 let pmt_bytes = build_clear_pmt_fixture(999);
2177 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2178
2179 let err = d.add_service(&pmt).unwrap_err();
2180 assert!(
2181 matches!(
2182 err,
2183 CaError::NoCaDescriptor {
2184 program_number: 999
2185 }
2186 ),
2187 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
2188 );
2189 assert!(
2190 d.managed_ca().services().is_empty(),
2191 "a rejected PMT must not be recorded"
2192 );
2193 }
2194
2195 #[test]
2207 fn requery_tick_serialize_failure_errors_from_pump_instead_of_panicking() {
2208 use broadcast_common::Parse;
2209
2210 let clock = TestClock::new();
2211 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2212
2213 let good_bytes = build_ca_pmt_fixture(1);
2216 let good = PmtSection::parse(&good_bytes).unwrap();
2217 assert_eq!(good.program_number, 1, "fixture precondition");
2218 d.add_service(&good).unwrap();
2219
2220 let pmt_bytes = build_ca_pmt_fixture(1546);
2225 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2226 let built = d
2227 .build_ca_pmt_filtered(
2228 &pmt,
2229 CaPmtListManagement::Update,
2230 CaPmtCmdId::OkDescrambling,
2231 )
2232 .try_to_bytes()
2233 .expect("fixture precondition: the good PMT encodes");
2234 let corrupt_raw = pmt_bytes[..pmt_bytes.len() - 6].to_vec();
2235 d.managed.record(
2236 1546,
2237 managed::service_of(&pmt, CaPmtCmdId::OkDescrambling, built, corrupt_raw),
2238 );
2239 d.take_notifications();
2240 for _ in 0..16 {
2244 d.device_mut().inbound.push_back(sb());
2245 d.pump(Duration::from_millis(10)).unwrap();
2246 }
2247 d.set_requery_interval(Duration::from_secs(5));
2248
2249 clock.advance(Duration::from_secs(5));
2253 let err = d
2254 .pump(Duration::ZERO)
2255 .expect_err("re-query of a corrupt stored PMT must error, not panic");
2256 assert!(
2257 matches!(
2258 err.get_ref(),
2259 Some(e) if e.downcast_ref::<CaError>()
2260 .is_some_and(|ca| matches!(ca, CaError::PmtParse(_)))
2261 ),
2262 "expected CaError::PmtParse from the corrupt stored bytes, got {err:?}"
2263 );
2264 assert!(
2265 err.to_string().contains("stored PMT re-parse failed"),
2266 "error must name the re-parse failure, got: {err}"
2267 );
2268 assert!(d.pump(Duration::ZERO).is_ok());
2272 clock.advance(Duration::from_secs(5));
2273 let err2 = d
2274 .pump(Duration::ZERO)
2275 .expect_err("the corrupt service must fail every re-query");
2276 assert!(
2277 err2.to_string().contains("stored PMT re-parse failed"),
2278 "failure must persist, got: {err2}"
2279 );
2280 }
2281
2282 fn corrupt_ca_section_ctor() -> PmtSection<'static> {
2288 const LOOP: usize = 5000;
2289 let mut prog_info: Vec<u8> = Vec::with_capacity(LOOP);
2290 for _ in 0..LOOP / 4 {
2291 prog_info.extend_from_slice(&[0x09u8, 0x02, 0x0B, 0x00]);
2293 }
2294 assert_eq!(prog_info.len(), LOOP);
2295 PmtSection::new(
2296 1,
2297 1,
2298 true,
2299 0,
2300 0,
2301 Pid::new(0x0200),
2302 dvb_si::descriptors::DescriptorLoop::new(Box::leak(prog_info.into_boxed_slice())),
2303 Vec::new(),
2304 )
2305 }
2306
2307 #[test]
2312 fn add_service_serialize_failure_returns_err_instead_of_panicking() {
2313 let mut d = driver_with_sessions();
2314 d.managed.set_cam_caids([0x0B00u16].into_iter().collect());
2315 let pmt = corrupt_ca_section_ctor();
2316 let err = d
2317 .add_service(&pmt)
2318 .expect_err("a ca_pmt with no valid encoding must error, not panic");
2319 assert!(
2320 matches!(err, CaError::Serialize(_)),
2321 "expected CaError::Serialize, got {err:?}"
2322 );
2323 assert!(
2324 d.managed_ca().services().is_empty(),
2325 "a rejected PMT must not be recorded"
2326 );
2327 }
2328
2329 #[test]
2330 fn add_service_second_call_uses_add_list_management() {
2331 use broadcast_common::Parse;
2332
2333 let mut d = driver_with_sessions();
2334 d.take_notifications();
2335
2336 let pmt1_bytes = build_ca_pmt_fixture(1546);
2337 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2338 d.add_service(&pmt1).unwrap();
2339 d.device_mut().inbound.push_back(sb());
2340 d.pump(Duration::from_millis(10)).unwrap();
2341
2342 let pmt2_bytes = build_ca_pmt_fixture(1547);
2343 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2344 d.add_service(&pmt2).unwrap();
2345 d.device_mut().inbound.push_back(sb());
2346 d.pump(Duration::from_millis(10)).unwrap();
2347
2348 let expected2 =
2350 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
2351 assert_apdu_on_session(&d, CA_SESSION, &expected2);
2352
2353 assert_eq!(d.managed_ca().services().len(), 2);
2354 }
2355
2356 fn build_ca_pmt_fixture_two_caids(program_number: u16) -> Vec<u8> {
2367 const VIACCESS: u16 = 0x0500;
2368 const NAGRA: u16 = 0x1800;
2369 let prog_ca_a = ca_descriptor(VIACCESS, Pid::new(0x0064));
2370 let prog_ca_b = ca_descriptor(NAGRA, Pid::new(0x0066));
2371 let mut prog_ca = Vec::new();
2372 prog_ca.extend_from_slice(&prog_ca_a);
2373 prog_ca.extend_from_slice(&prog_ca_b);
2374
2375 let mut body = Vec::new();
2376 body.push(0x02); body.push(0);
2378 body.push(0);
2379 body.extend_from_slice(&program_number.to_be_bytes());
2380 body.push(0xC3);
2381 body.push(0x00);
2382 body.push(0x00);
2383 body.push(0xE0 | 0x01); body.push(0x00);
2385 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2386 body.push(prog_ca.len() as u8);
2387 body.extend_from_slice(&prog_ca);
2388 body.push(0x0F);
2391 body.push(0xE0 | 0x01);
2392 body.push(0x00);
2393 body.push(0xF0);
2394 body.push(0x00);
2395
2396 let section_length = body.len() - 3 + 4;
2397 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2398 body[2] = section_length as u8;
2399 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2400 body.extend_from_slice(&crc.to_be_bytes());
2401 body
2402 }
2403
2404 #[test]
2405 fn add_service_filters_ca_descriptors_to_cam_advertised_caids() {
2406 use broadcast_common::Parse;
2407 use dvb_ci::objects::ca_info::CaInfo;
2408
2409 let mut d = driver_with_sessions();
2410 d.take_notifications();
2411
2412 feed(
2415 &mut d,
2416 r_apdu(
2417 CA_SESSION,
2418 &ser(&CaInfo {
2419 length_form: dvb_ci::length::LengthForm::Minimal,
2420 ca_system_ids: vec![0x0500],
2421 }),
2422 ),
2423 );
2424 d.take_notifications();
2425
2426 let pmt_bytes = build_ca_pmt_fixture_two_caids(1560);
2427 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2428
2429 let expected_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2434 &pmt,
2435 &[0x0500],
2436 CaPmtListManagement::Only,
2437 CaPmtCmdId::OkDescrambling,
2438 )
2439 .to_bytes();
2440 let unfiltered =
2441 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2442 assert_ne!(
2443 expected_filtered, unfiltered,
2444 "precondition: the fixture's two CAIDs must make filtered != unfiltered"
2445 );
2446 let sends_filtered_before = count_apdu_on_session(&d, CA_SESSION, &expected_filtered);
2447 let sends_unfiltered_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered);
2448
2449 d.add_service(&pmt).unwrap();
2450 d.device_mut().inbound.push_back(sb());
2451 d.pump(Duration::from_millis(10)).unwrap();
2452
2453 assert_eq!(
2454 count_apdu_on_session(&d, CA_SESSION, &expected_filtered),
2455 sends_filtered_before + 1,
2456 "add_service must send the CAID-filtered ca_pmt"
2457 );
2458 assert_eq!(
2459 count_apdu_on_session(&d, CA_SESSION, &unfiltered),
2460 sends_unfiltered_before,
2461 "add_service must NOT send the unfiltered ca_pmt once the CAM's CAIDs are known"
2462 );
2463 }
2464
2465 #[test]
2466 fn requery_timer_filters_ca_descriptors_to_cam_advertised_caids() {
2467 use broadcast_common::Parse;
2468 use dvb_ci::objects::ca_info::CaInfo;
2469
2470 let clock = TestClock::new();
2471 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2472 d.set_requery_interval(Duration::from_secs(10));
2475 d.take_notifications();
2476
2477 let pmt_bytes = build_ca_pmt_fixture_two_caids(1561);
2478 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2479 d.add_service(&pmt).unwrap();
2482 d.device_mut().inbound.push_back(sb());
2483 clock.advance(Duration::from_millis(10));
2484 d.pump(Duration::from_millis(10)).unwrap();
2485
2486 feed(
2488 &mut d,
2489 r_apdu(
2490 CA_SESSION,
2491 &ser(&CaInfo {
2492 length_form: dvb_ci::length::LengthForm::Minimal,
2493 ca_system_ids: vec![0x0500],
2494 }),
2495 ),
2496 );
2497 d.take_notifications();
2498
2499 let expected_query_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2502 &pmt,
2503 &[0x0500],
2504 CaPmtListManagement::Update,
2505 CaPmtCmdId::Query,
2506 )
2507 .to_bytes();
2508 let expected_ok_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2509 &pmt,
2510 &[0x0500],
2511 CaPmtListManagement::Update,
2512 CaPmtCmdId::OkDescrambling,
2513 )
2514 .to_bytes();
2515 let unfiltered_query =
2516 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2517 let unfiltered_ok = build_ca_pmt(
2518 &pmt,
2519 CaPmtListManagement::Update,
2520 CaPmtCmdId::OkDescrambling,
2521 )
2522 .to_bytes();
2523 assert_ne!(
2524 expected_query_filtered, unfiltered_query,
2525 "precondition: the fixture's two CAIDs must make filtered != unfiltered (query)"
2526 );
2527 assert_ne!(
2528 expected_ok_filtered, unfiltered_ok,
2529 "precondition: the fixture's two CAIDs must make filtered != unfiltered (ok_descrambling)"
2530 );
2531 let sends_query_before = count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered);
2532 let sends_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered);
2533 let sends_unfiltered_query_before =
2534 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query);
2535 let sends_unfiltered_ok_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok);
2536
2537 clock.advance(Duration::from_secs(11));
2538 d.pump(Duration::from_secs(11)).unwrap();
2539 feed(&mut d, sb());
2540
2541 assert_eq!(
2542 count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered),
2543 sends_query_before + 1,
2544 "the re-query `query` half must be CAID-filtered to the CAM's advertised CAIDs"
2545 );
2546 assert_eq!(
2547 count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered),
2548 sends_ok_before + 1,
2549 "the re-query `ok_descrambling` half must be CAID-filtered to the CAM's advertised CAIDs"
2550 );
2551 assert_eq!(
2552 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query),
2553 sends_unfiltered_query_before,
2554 "the re-query resend must NOT be the unfiltered `query` ca_pmt"
2555 );
2556 assert_eq!(
2557 count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok),
2558 sends_unfiltered_ok_before,
2559 "the re-query resend must NOT be the unfiltered `ok_descrambling` ca_pmt"
2560 );
2561 }
2562
2563 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
2573 const EXTENSION_HEADER_LEN: u16 = 5;
2574 const CRC_LEN: u16 = 4;
2575 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
2576 let mut v = Vec::new();
2577 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
2579 v.push((section_length & 0xFF) as u8);
2580 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
2585 let crc = broadcast_common::crc32_mpeg2::compute(&v);
2586 v.extend_from_slice(&crc.to_be_bytes());
2587 v
2588 }
2589
2590 #[test]
2591 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
2592 use broadcast_common::Parse;
2593 use dvb_ci::objects::ca_info::CaInfo;
2594 use dvb_si::tables::cat::CatSection;
2595
2596 let mut d = driver_with_sessions();
2597 d.take_notifications();
2598
2599 feed(
2601 &mut d,
2602 r_apdu(
2603 CA_SESSION,
2604 &ser(&CaInfo {
2605 length_form: dvb_ci::length::LengthForm::Minimal,
2606 ca_system_ids: vec![0x0648, 0x0100],
2607 }),
2608 ),
2609 );
2610 d.take_notifications();
2611
2612 let mut descriptors = Vec::new();
2615 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2616 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2617 let cat_bytes = build_cat_fixture(&descriptors);
2618 let cat = CatSection::parse(&cat_bytes).unwrap();
2619
2620 d.set_cat(&cat).unwrap();
2621
2622 assert_eq!(
2623 d.emm_pids(),
2624 &[Pid::new(0x1FF0)],
2625 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
2626 );
2627 }
2628
2629 #[test]
2630 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
2631 use broadcast_common::Parse;
2632 use dvb_ci::objects::ca_info::CaInfo;
2633 use dvb_si::tables::cat::CatSection;
2634
2635 let mut d = driver_with_sessions();
2636 d.take_notifications();
2637
2638 let mut descriptors = Vec::new();
2639 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2640 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2641 let cat_bytes = build_cat_fixture(&descriptors);
2642 let cat = CatSection::parse(&cat_bytes).unwrap();
2643
2644 d.set_cat(&cat).unwrap();
2647 assert!(
2648 d.emm_pids().is_empty(),
2649 "emm_pids must be empty before any ca_info arrives, got {:?}",
2650 d.emm_pids()
2651 );
2652
2653 feed(
2656 &mut d,
2657 r_apdu(
2658 CA_SESSION,
2659 &ser(&CaInfo {
2660 length_form: dvb_ci::length::LengthForm::Minimal,
2661 ca_system_ids: vec![0x0648, 0x0100],
2662 }),
2663 ),
2664 );
2665 d.take_notifications();
2666
2667 assert_eq!(
2668 d.emm_pids(),
2669 &[Pid::new(0x1FF0)],
2670 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
2671 );
2672 }
2673
2674 #[test]
2680 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
2681 use broadcast_common::Parse;
2682 use dvb_ci::objects::ca_info::CaInfo;
2683 use dvb_si::tables::cat::CatSection;
2684
2685 let mut d = driver_with_sessions();
2686 d.take_notifications();
2687
2688 feed(
2690 &mut d,
2691 r_apdu(
2692 CA_SESSION,
2693 &ser(&CaInfo {
2694 length_form: dvb_ci::length::LengthForm::Minimal,
2695 ca_system_ids: vec![0x0648, 0x0100],
2696 }),
2697 ),
2698 );
2699 d.take_notifications();
2700
2701 let mut descriptors = Vec::new();
2703 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2704 descriptors.extend_from_slice(&ca_descriptor(0x0100, Pid::new(0x1FF0)));
2705 let cat_bytes = build_cat_fixture(&descriptors);
2706 let cat = CatSection::parse(&cat_bytes).unwrap();
2707
2708 d.set_cat(&cat).unwrap();
2709
2710 assert_eq!(
2711 d.emm_pids(),
2712 &[Pid::new(0x1FF0)],
2713 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
2714 d.emm_pids()
2715 );
2716 }
2717
2718 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
2722 const VIACCESS: u16 = 0x0500;
2723 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0074));
2724 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0075));
2725
2726 let mut body = Vec::new();
2727 body.push(0x02); body.push(0);
2729 body.push(0);
2730 body.extend_from_slice(&program_number.to_be_bytes());
2731 body.push(0xC3);
2732 body.push(0x00);
2733 body.push(0x00);
2734 body.push(0xE0 | 0x02); body.push(0x00);
2736 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2737 body.push(prog_ca.len() as u8);
2738 body.extend_from_slice(&prog_ca);
2739 body.push(0x1B);
2741 body.push(0xE0 | 0x02);
2742 body.push(0x00);
2743 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2744 body.push(es0_ca.len() as u8);
2745 body.extend_from_slice(&es0_ca);
2746 body.push(0x0F);
2748 body.push(0xE0 | 0x02);
2749 body.push(0x01);
2750 body.push(0xF0);
2751 body.push(0x00);
2752
2753 let section_length = body.len() - 3 + 4;
2754 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2755 body[2] = section_length as u8;
2756 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2757 body.extend_from_slice(&crc.to_be_bytes());
2758 body
2759 }
2760
2761 #[test]
2762 fn descramble_pids_is_the_union_of_active_services_es_pids() {
2763 use broadcast_common::Parse;
2764
2765 let mut d = driver_with_sessions();
2766 d.take_notifications();
2767
2768 assert!(
2769 d.descramble_pids().is_empty(),
2770 "no service added yet: descramble_pids must be empty"
2771 );
2772
2773 let pmt1_bytes = build_ca_pmt_fixture(1546);
2774 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2775 d.add_service(&pmt1).unwrap();
2776 d.device_mut().inbound.push_back(sb());
2777 d.pump(Duration::from_millis(10)).unwrap();
2778
2779 assert_eq!(d.descramble_pids(), &[Pid::new(0x0100), Pid::new(0x0101)]);
2780
2781 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2782 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2783 d.add_service(&pmt2).unwrap();
2784 d.device_mut().inbound.push_back(sb());
2785 d.pump(Duration::from_millis(10)).unwrap();
2786
2787 assert_eq!(
2789 d.descramble_pids(),
2790 &[
2791 Pid::new(0x0100),
2792 Pid::new(0x0101),
2793 Pid::new(0x0200),
2794 Pid::new(0x0201)
2795 ],
2796 "descramble_pids must be the union across both added services"
2797 );
2798 }
2799
2800 pub(crate) fn ca_pmt_reply_for(
2805 program_number: u16,
2806 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
2807 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2808 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2809 length_form: dvb_ci::length::LengthForm::Minimal,
2810 program_number,
2811 version_number: 1,
2812 current_next_indicator: true,
2813 ca_enable,
2814 streams: vec![],
2815 reserved_version: Default::default(),
2816 reserved_ca_enable: Default::default(),
2817 }
2818 }
2819
2820 #[test]
2821 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
2822 use broadcast_common::Parse;
2823 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2824
2825 let clock = TestClock::new();
2826 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2827 d.set_requery_interval(Duration::from_secs(10));
2830 d.take_notifications();
2831
2832 let pmt_bytes = build_ca_pmt_fixture(1546);
2833 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2834 d.add_service(&pmt).unwrap();
2835 d.device_mut().inbound.push_back(sb());
2836 clock.advance(Duration::from_millis(10));
2837 d.pump(Duration::from_millis(10)).unwrap();
2838 d.take_notifications();
2839
2840 let expected_initial_ca_pmt =
2844 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2845 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
2846
2847 let expected_query =
2853 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2854 let expected_ok_descrambling = build_ca_pmt(
2855 &pmt,
2856 CaPmtListManagement::Update,
2857 CaPmtCmdId::OkDescrambling,
2858 )
2859 .to_bytes();
2860 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2861 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2862
2863 let mut all_notes = Vec::new();
2864
2865 feed(
2869 &mut d,
2870 r_apdu(
2871 CA_SESSION,
2872 &ser(&ca_pmt_reply_for(
2873 1546,
2874 Some(CaEnable::NotPossibleNoEntitlement),
2875 )),
2876 ),
2877 );
2878 all_notes.extend(d.take_notifications());
2879
2880 clock.advance(Duration::from_secs(11));
2890 d.pump(Duration::from_secs(11)).unwrap();
2891 all_notes.extend(d.take_notifications());
2892 feed(&mut d, sb());
2893
2894 assert_eq!(
2895 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2896 query_sends_before + 1,
2897 "expected the re-query timer to resend the `query` half exactly once"
2898 );
2899 assert_eq!(
2900 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2901 ok_sends_before + 1,
2902 "expected the re-query timer to resend the `ok_descrambling` half exactly once"
2903 );
2904
2905 feed(
2908 &mut d,
2909 r_apdu(
2910 CA_SESSION,
2911 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
2912 ),
2913 );
2914 all_notes.extend(d.take_notifications());
2915
2916 let hits = all_notes
2917 .iter()
2918 .filter(|n| {
2919 matches!(
2920 n,
2921 Notification::Entitlement {
2922 program_number: 1546,
2923 ca_enable: CaEnable::Possible,
2924 descrambling_ok: true,
2925 }
2926 )
2927 })
2928 .count();
2929 assert_eq!(
2930 hits, 1,
2931 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
2932 );
2933 }
2934
2935 #[test]
2936 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
2937 use broadcast_common::Parse;
2938 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2939
2940 let mut d = driver_with_sessions();
2941 d.set_requery_interval(Duration::from_secs(10));
2944 d.take_notifications();
2945
2946 let pmt_bytes = build_ca_pmt_fixture(1547);
2947 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2948 d.add_service(&pmt).unwrap();
2949 d.device_mut().inbound.push_back(sb());
2950 d.pump(Duration::from_millis(10)).unwrap();
2951 d.take_notifications();
2952
2953 feed(
2957 &mut d,
2958 r_apdu(
2959 CA_SESSION,
2960 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2961 ),
2962 );
2963 d.take_notifications();
2964
2965 for _ in 0..2 {
2968 d.pump(Duration::from_secs(11)).unwrap();
2969 d.take_notifications();
2970 feed(
2971 &mut d,
2972 r_apdu(
2973 CA_SESSION,
2974 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2975 ),
2976 );
2977 let notes = d.take_notifications();
2978 assert!(
2979 !notes
2980 .iter()
2981 .any(|n| matches!(n, Notification::Entitlement { .. })),
2982 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
2983 );
2984 }
2985 }
2986
2987 #[test]
2988 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
2989 use broadcast_common::Parse;
2990 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2991
2992 let mut d = driver_with_sessions();
2993 d.take_notifications();
2994
2995 let pmt_bytes = build_ca_pmt_fixture(1548);
2996 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2997 d.add_service(&pmt).unwrap();
2998 d.device_mut().inbound.push_back(sb());
2999 d.pump(Duration::from_millis(10)).unwrap();
3000 d.take_notifications();
3001
3002 feed(
3004 &mut d,
3005 r_apdu(
3006 CA_SESSION,
3007 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
3008 ),
3009 );
3010 d.take_notifications();
3011
3012 feed(
3016 &mut d,
3017 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
3018 );
3019 let notes = d.take_notifications();
3020 assert!(
3021 !notes
3022 .iter()
3023 .any(|n| matches!(n, Notification::Entitlement { .. })),
3024 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
3025 );
3026 }
3027
3028 #[test]
3029 fn requery_disabled_by_default_sends_nothing() {
3030 use broadcast_common::Parse;
3031
3032 let mut d = driver_with_sessions();
3036 d.take_notifications();
3037
3038 let pmt_bytes = build_ca_pmt_fixture(1549);
3039 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3040 d.add_service(&pmt).unwrap();
3041 d.device_mut().inbound.push_back(sb());
3042 d.pump(Duration::from_millis(10)).unwrap();
3043
3044 let expected_query =
3047 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3048 let expected_ok_descrambling = build_ca_pmt(
3049 &pmt,
3050 CaPmtListManagement::Update,
3051 CaPmtCmdId::OkDescrambling,
3052 )
3053 .to_bytes();
3054 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
3055 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
3056
3057 d.pump(Duration::from_secs(1_000_000)).unwrap();
3059 feed(&mut d, sb());
3060 feed(&mut d, sb());
3061 assert_eq!(
3062 count_apdu_on_session(&d, CA_SESSION, &expected_query),
3063 query_sends_before,
3064 "the default (Duration::ZERO) must never send a re-query `query`"
3065 );
3066 assert_eq!(
3067 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
3068 ok_sends_before,
3069 "the default (Duration::ZERO) must never send a re-query `ok_descrambling`"
3070 );
3071 }
3072
3073 #[test]
3074 fn set_requery_interval_zero_disables_resend_after_being_enabled() {
3075 use broadcast_common::Parse;
3076
3077 let mut d = driver_with_sessions();
3078 d.set_requery_interval(Duration::from_secs(5));
3081 d.set_requery_interval(Duration::ZERO);
3082 d.take_notifications();
3083
3084 let pmt_bytes = build_ca_pmt_fixture(1549);
3085 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3086 d.add_service(&pmt).unwrap();
3087 d.device_mut().inbound.push_back(sb());
3088 d.pump(Duration::from_millis(10)).unwrap();
3089
3090 let expected_ca_pmt = build_ca_pmt(
3094 &pmt,
3095 CaPmtListManagement::Update,
3096 CaPmtCmdId::OkDescrambling,
3097 )
3098 .to_bytes();
3099 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3100
3101 d.pump(Duration::from_secs(1000)).unwrap();
3103 feed(&mut d, sb());
3104 feed(&mut d, sb());
3105
3106 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3107 assert_eq!(
3108 sends_after, sends_before,
3109 "Duration::ZERO must disable the re-query resend"
3110 );
3111 }
3112
3113 #[test]
3114 fn requery_timer_resends_every_active_service_not_just_one() {
3115 use broadcast_common::Parse;
3116
3117 let clock = TestClock::new();
3118 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3119 d.set_requery_interval(Duration::from_secs(10));
3122 d.take_notifications();
3123
3124 let pmt1_bytes = build_ca_pmt_fixture(1546);
3127 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3128 d.add_service(&pmt1).unwrap();
3129 d.device_mut().inbound.push_back(sb());
3130 clock.advance(Duration::from_millis(10));
3131 d.pump(Duration::from_millis(10)).unwrap();
3132
3133 let pmt2_bytes = build_ca_pmt_fixture(1547);
3134 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3135 d.add_service(&pmt2).unwrap();
3136 d.device_mut().inbound.push_back(sb());
3137 clock.advance(Duration::from_millis(10));
3138 d.pump(Duration::from_millis(10)).unwrap();
3139 d.take_notifications();
3140
3141 let expected1_query =
3147 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3148 let expected1_ok = build_ca_pmt(
3149 &pmt1,
3150 CaPmtListManagement::Update,
3151 CaPmtCmdId::OkDescrambling,
3152 )
3153 .to_bytes();
3154 let expected2_query =
3155 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3156 let expected2_ok = build_ca_pmt(
3157 &pmt2,
3158 CaPmtListManagement::Update,
3159 CaPmtCmdId::OkDescrambling,
3160 )
3161 .to_bytes();
3162 let sends_before1_query = count_apdu_on_session(&d, CA_SESSION, &expected1_query);
3163 let sends_before1_ok = count_apdu_on_session(&d, CA_SESSION, &expected1_ok);
3164 let sends_before2_query = count_apdu_on_session(&d, CA_SESSION, &expected2_query);
3165 let sends_before2_ok = count_apdu_on_session(&d, CA_SESSION, &expected2_ok);
3166
3167 clock.advance(Duration::from_secs(11));
3173 d.pump(Duration::from_secs(11)).unwrap();
3174 feed(&mut d, sb());
3175 feed(&mut d, sb());
3176
3177 assert_eq!(
3178 count_apdu_on_session(&d, CA_SESSION, &expected1_query),
3179 sends_before1_query + 1,
3180 "expected service 1546's `query` resent exactly once on the shared tick"
3181 );
3182 assert_eq!(
3183 count_apdu_on_session(&d, CA_SESSION, &expected1_ok),
3184 sends_before1_ok + 1,
3185 "expected service 1546's `ok_descrambling` resent exactly once on the shared tick"
3186 );
3187 assert_eq!(
3188 count_apdu_on_session(&d, CA_SESSION, &expected2_query),
3189 sends_before2_query + 1,
3190 "expected service 1547's `query` resent exactly once on the shared tick"
3191 );
3192 assert_eq!(
3193 count_apdu_on_session(&d, CA_SESSION, &expected2_ok),
3194 sends_before2_ok + 1,
3195 "expected service 1547's `ok_descrambling` resent exactly once on the shared tick"
3196 );
3197 }
3198
3199 #[test]
3203 fn requery_after_remove_resends_only_the_surviving_service() {
3204 use broadcast_common::Parse;
3205
3206 let clock = TestClock::new();
3207 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3208 d.set_requery_interval(Duration::from_secs(10));
3211 d.take_notifications();
3212
3213 let pmt1_bytes = build_ca_pmt_fixture(1546);
3214 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3215 d.add_service(&pmt1).unwrap();
3216 d.device_mut().inbound.push_back(sb());
3217 clock.advance(Duration::from_millis(10));
3218 d.pump(Duration::from_millis(10)).unwrap();
3219
3220 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3221 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3222 d.add_service(&pmt2).unwrap();
3223 d.device_mut().inbound.push_back(sb());
3224 clock.advance(Duration::from_millis(10));
3225 d.pump(Duration::from_millis(10)).unwrap();
3226
3227 d.remove_service(1546).unwrap();
3229 d.device_mut().inbound.push_back(sb());
3230 clock.advance(Duration::from_millis(10));
3231 d.pump(Duration::from_millis(10)).unwrap();
3232 d.take_notifications();
3233
3234 let expected_survivor_query =
3240 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3241 let expected_survivor_ok = build_ca_pmt(
3242 &pmt2,
3243 CaPmtListManagement::Update,
3244 CaPmtCmdId::OkDescrambling,
3245 )
3246 .to_bytes();
3247 let expected_removed_query =
3248 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3249 let expected_removed_ok = build_ca_pmt(
3250 &pmt1,
3251 CaPmtListManagement::Update,
3252 CaPmtCmdId::OkDescrambling,
3253 )
3254 .to_bytes();
3255 let sends_survivor_query_before =
3256 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query);
3257 let sends_survivor_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok);
3258 let sends_removed_query_before =
3259 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query);
3260 let sends_removed_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok);
3261
3262 clock.advance(Duration::from_secs(11));
3263 d.pump(Duration::from_secs(11)).unwrap();
3264 feed(&mut d, sb());
3265
3266 assert_eq!(
3267 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query),
3268 sends_survivor_query_before + 1,
3269 "the surviving service's `query` must be re-sent"
3270 );
3271 assert_eq!(
3272 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok),
3273 sends_survivor_ok_before + 1,
3274 "the surviving service's `ok_descrambling` must be re-sent"
3275 );
3276 assert_eq!(
3277 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query),
3278 sends_removed_query_before,
3279 "the removed service's `query` must NOT be re-sent"
3280 );
3281 assert_eq!(
3282 count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok),
3283 sends_removed_ok_before,
3284 "the removed service's `ok_descrambling` must NOT be re-sent"
3285 );
3286 }
3287
3288 #[test]
3291 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
3292 use broadcast_common::Parse;
3293
3294 let mut d = driver_with_sessions();
3295 d.take_notifications();
3296
3297 let pmt1_bytes = build_ca_pmt_fixture(1546);
3301 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3302 d.add_service(&pmt1).unwrap();
3303 d.device_mut().inbound.push_back(sb());
3304 d.pump(Duration::from_millis(10)).unwrap();
3305
3306 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3307 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3308 d.add_service(&pmt2).unwrap();
3309 d.device_mut().inbound.push_back(sb());
3310 d.pump(Duration::from_millis(10)).unwrap();
3311
3312 d.remove_service(1546).unwrap();
3313 d.device_mut().inbound.push_back(sb());
3314 d.pump(Duration::from_millis(10)).unwrap();
3315
3316 let expected =
3320 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3321 assert_apdu_on_session(&d, CA_SESSION, &expected);
3322
3323 assert_eq!(
3324 d.descramble_pids(),
3325 &[Pid::new(0x0200), Pid::new(0x0201)],
3326 "1546's ES PIDs must be gone; 1547's must remain"
3327 );
3328 assert!(
3329 d.managed_ca().services().get(&1546).is_none(),
3330 "1546 must no longer be tracked"
3331 );
3332 assert!(
3333 d.managed_ca().services().get(&1547).is_some(),
3334 "1547 must remain tracked"
3335 );
3336 }
3337
3338 #[test]
3339 fn remove_service_of_untracked_program_is_a_no_op() {
3340 let mut d = driver_with_sessions();
3341 d.take_notifications();
3342
3343 let ops_before = d.device().ops.len();
3344 d.remove_service(0xFFFF).unwrap();
3345 assert_eq!(
3346 d.device().ops.len(),
3347 ops_before,
3348 "removing an untracked program must not send anything to the device"
3349 );
3350 assert!(
3351 d.managed_ca().services().is_empty(),
3352 "removing an untracked program must not disturb the (empty) managed set"
3353 );
3354 }
3355
3356 #[test]
3357 fn cam_removed_edge_clears_managed_state() {
3358 use broadcast_common::Parse;
3359 use dvb_ci::objects::ca_info::CaInfo;
3360 use dvb_si::tables::cat::CatSection;
3361
3362 let mut d = driver_with_sessions();
3363 d.take_notifications();
3364
3365 let pmt_bytes = build_ca_pmt_fixture(1546);
3366 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3367 d.add_service(&pmt).unwrap();
3368 d.device_mut().inbound.push_back(sb());
3369 d.pump(Duration::from_millis(10)).unwrap();
3370
3371 feed(
3374 &mut d,
3375 r_apdu(
3376 CA_SESSION,
3377 &ser(&CaInfo {
3378 length_form: dvb_ci::length::LengthForm::Minimal,
3379 ca_system_ids: vec![0x0648],
3380 }),
3381 ),
3382 );
3383 d.take_notifications();
3384 let mut descriptors = Vec::new();
3385 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
3386 let cat_bytes = build_cat_fixture(&descriptors);
3387 let cat = CatSection::parse(&cat_bytes).unwrap();
3388 d.set_cat(&cat).unwrap();
3389
3390 assert!(
3391 !d.managed_ca().services().is_empty(),
3392 "precondition: a service is tracked"
3393 );
3394 assert!(
3395 !d.descramble_pids().is_empty(),
3396 "precondition: descramble_pids populated"
3397 );
3398 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
3399
3400 d.device_mut().slot.module_present = false;
3402 d.pump(Duration::from_millis(10)).unwrap();
3403 let notes = d.take_notifications();
3404 assert!(
3405 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
3406 "expected CamRemoved, got {notes:?}"
3407 );
3408
3409 assert!(
3410 d.managed_ca().services().is_empty(),
3411 "services must be cleared on CamRemoved"
3412 );
3413 assert!(
3414 d.descramble_pids().is_empty(),
3415 "descramble_pids must be cleared on CamRemoved"
3416 );
3417 assert!(
3418 d.emm_pids().is_empty(),
3419 "emm_pids must be cleared on CamRemoved"
3420 );
3421 }
3422
3423 #[test]
3424 fn update_service_not_managed_returns_not_managed_and_sends_nothing() {
3425 use broadcast_common::Parse;
3426
3427 let mut d = driver_with_sessions();
3428 d.take_notifications();
3429
3430 let pmt_bytes = build_clear_pmt_fixture(9999);
3431 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3432
3433 let before = d.device().ops.len();
3434 let err = d
3435 .update_service(&pmt)
3436 .expect_err("unknown program must error");
3437 assert!(
3438 matches!(
3439 err,
3440 CaError::NotManaged {
3441 program_number: 9999
3442 }
3443 ),
3444 "got {err:?}"
3445 );
3446 assert_eq!(d.device().ops.len(), before, "must send nothing");
3447 assert!(d.managed_ca().services().is_empty());
3448 }
3449
3450 #[test]
3451 fn update_service_no_ca_descriptor_leaves_record_unchanged() {
3452 use broadcast_common::Parse;
3453
3454 let mut d = driver_with_sessions();
3455 d.take_notifications();
3456
3457 let pmt_bytes = build_ca_pmt_fixture(1546);
3458 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3459 d.add_service(&pmt).unwrap();
3460
3461 let svc_before = d.managed_ca().services().get(&1546).unwrap().clone();
3462
3463 let clear_bytes = build_clear_pmt_fixture(1546);
3464 let clear_pmt = PmtSection::parse(&clear_bytes).unwrap();
3465 let err = d
3466 .update_service(&clear_pmt)
3467 .expect_err("CA-free PMT must error");
3468 assert!(
3469 matches!(
3470 err,
3471 CaError::NoCaDescriptor {
3472 program_number: 1546
3473 }
3474 ),
3475 "got {err:?}"
3476 );
3477
3478 let svc_after = d.managed_ca().services().get(&1546).unwrap();
3479 assert_eq!(svc_before, *svc_after, "record must be unchanged");
3480 }
3481
3482 #[test]
3488 fn update_service_sends_one_update_ca_pmt_with_the_new_pids_and_replaces_only_that_record() {
3489 use broadcast_common::Parse;
3490
3491 let mut d = driver_with_sessions();
3492 d.take_notifications();
3493
3494 let a_bytes = build_ca_pmt_fixture(1546);
3495 let a = PmtSection::parse(&a_bytes).unwrap();
3496 d.add_service(&a).unwrap();
3497 d.device_mut().inbound.push_back(sb());
3498 d.pump(Duration::from_millis(10)).unwrap();
3499
3500 let b_bytes = build_ca_pmt_fixture_dedicated_pcr(1547);
3501 let b = PmtSection::parse(&b_bytes).unwrap();
3502 d.add_service(&b).unwrap();
3503 d.device_mut().inbound.push_back(sb());
3504 d.pump(Duration::from_millis(10)).unwrap();
3505
3506 let a_old_record = d.managed_ca().services().get(&1546).unwrap().clone();
3507 let b_record_before = d.managed_ca().services().get(&1547).unwrap().clone();
3508 assert_eq!(
3509 a_old_record.ca_pids,
3510 vec![Pid::new(0x0064), Pid::new(0x0065)],
3511 "fixture sanity: A starts on the 0x0064/0x0065 ECM PIDs"
3512 );
3513
3514 let a2_bytes = build_ca_pmt_fixture_distinct_pids(1546);
3516 let a2 = PmtSection::parse(&a2_bytes).unwrap();
3517 let update =
3518 build_ca_pmt(&a2, CaPmtListManagement::Update, CaPmtCmdId::OkDescrambling).to_bytes();
3519 let writes_before = d.device().ops.len();
3520
3521 d.update_service(&a2).unwrap();
3522 d.device_mut().inbound.push_back(sb());
3523 d.pump(Duration::from_millis(10)).unwrap();
3524
3525 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &update), 1);
3527 let new_ca_pmt_writes = d.device().ops[writes_before..]
3528 .iter()
3529 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == [0x9F, 0x80, 0x32])))
3530 .count();
3531 assert_eq!(new_ca_pmt_writes, 1, "exactly one ca_pmt APDU may be sent");
3532
3533 let new_prog_ca = ca_descriptor(0x0500, Pid::new(0x0074));
3535 let old_prog_ca = ca_descriptor(0x0500, Pid::new(0x0064));
3536 assert!(update.windows(new_prog_ca.len()).any(|x| x == new_prog_ca));
3537 assert!(!update.windows(old_prog_ca.len()).any(|x| x == old_prog_ca));
3538
3539 let a_new_record = d.managed_ca().services().get(&1546).unwrap();
3541 assert_eq!(
3542 a_new_record.ca_pids,
3543 vec![Pid::new(0x0074), Pid::new(0x0075)]
3544 );
3545 assert_eq!(a_new_record.built_ca_pmt, update);
3546 assert_ne!(a_new_record.built_ca_pmt, a_old_record.built_ca_pmt);
3547 assert_ne!(a_new_record.pmt_raw, a_old_record.pmt_raw);
3548 assert_eq!(
3549 a_new_record.pmt_raw, a2_bytes,
3550 "raw PMT must be the new section"
3551 );
3552
3553 assert_eq!(
3555 *d.managed_ca().services().get(&1547).unwrap(),
3556 b_record_before
3557 );
3558 assert_eq!(d.managed_ca().services().len(), 2);
3559 }
3560
3561 #[test]
3564 fn remove_service_after_update_service_uses_the_new_pmt() {
3565 use broadcast_common::Parse;
3566
3567 let mut d = driver_with_sessions();
3568 d.take_notifications();
3569
3570 let a_bytes = build_ca_pmt_fixture(1546);
3571 let a = PmtSection::parse(&a_bytes).unwrap();
3572 d.add_service(&a).unwrap();
3573 d.device_mut().inbound.push_back(sb());
3574 d.pump(Duration::from_millis(10)).unwrap();
3575 let a2_bytes = build_ca_pmt_fixture_distinct_pids(1546);
3576 let a2 = PmtSection::parse(&a2_bytes).unwrap();
3577 d.update_service(&a2).unwrap();
3578 d.device_mut().inbound.push_back(sb());
3579 d.pump(Duration::from_millis(10)).unwrap();
3580
3581 d.remove_service(1546).unwrap();
3582 d.device_mut().inbound.push_back(sb());
3583 d.pump(Duration::from_millis(10)).unwrap();
3584
3585 let removal_new =
3586 build_ca_pmt(&a2, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3587 let removal_old =
3588 build_ca_pmt(&a, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3589 assert_ne!(
3590 removal_new, removal_old,
3591 "fixture sanity: the two PMTs differ"
3592 );
3593 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &removal_new), 1);
3594 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &removal_old), 0);
3595 assert!(d.managed_ca().services().is_empty());
3596 }
3597
3598 #[test]
3601 fn update_service_filters_caids_like_add_service() {
3602 use broadcast_common::Parse;
3603
3604 let mut d = driver_with_sessions();
3605 d.take_notifications();
3606
3607 let two_caids = build_ca_pmt_fixture_two_caids(1546);
3608 let pmt = PmtSection::parse(&two_caids).unwrap();
3609 d.add_service(&pmt).unwrap();
3610 let added = d
3611 .managed_ca()
3612 .services()
3613 .get(&1546)
3614 .unwrap()
3615 .built_ca_pmt
3616 .clone();
3617
3618 d.update_service(&pmt).unwrap();
3619 let updated = d
3620 .managed_ca()
3621 .services()
3622 .get(&1546)
3623 .unwrap()
3624 .built_ca_pmt
3625 .clone();
3626
3627 assert_eq!(added.len(), updated.len());
3629 let diff: Vec<usize> = (0..added.len())
3630 .filter(|&i| added[i] != updated[i])
3631 .collect();
3632 assert_eq!(
3633 diff.len(),
3634 1,
3635 "only list_management may differ, got offsets {diff:?}"
3636 );
3637 assert_eq!(updated[diff[0]], 0x05, "list_management must be update");
3638 }
3639
3640 #[test]
3641 fn update_service_serialize_failure_leaves_record_unchanged() {
3642 let mut d = driver_with_sessions();
3643 let pmt_bytes = build_ca_pmt_fixture(1);
3645 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3646 d.add_service(&pmt).unwrap();
3647
3648 let svc_before = d.managed_ca().services().get(&1).unwrap().clone();
3649
3650 let corrupt = corrupt_ca_section_ctor();
3651 let err = d
3652 .update_service(&corrupt)
3653 .expect_err("unencodable PMT must error");
3654 assert!(
3655 matches!(err, CaError::Serialize(_)),
3656 "expected CaError::Serialize, got {err:?}"
3657 );
3658
3659 let svc_after = d.managed_ca().services().get(&1).unwrap();
3660 assert_eq!(svc_before, *svc_after, "record must be unchanged");
3661 }
3662}