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(
1125 &mut d,
1126 r_data(
1127 1,
1128 &ser(&dvb_ci::spdu::OpenSessionRequest {
1129 length_form: dvb_ci::length::LengthForm::Minimal,
1130 resource: RESOURCE_MANAGER,
1131 }),
1132 ),
1133 );
1134 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq::default())));
1136
1137 let want = dvb_ci::tag::PROFILE.to_bytes();
1140 let found = d.device().ops.iter().any(|op| {
1141 if let DeviceOp::Write(w) = op
1142 && let Some(pos) = w.windows(3).position(|x| x == want)
1143 && let Ok(p) = Profile::parse(&w[pos..])
1144 {
1145 return p.resources.contains(&HOST_CONTROL);
1146 }
1147 false
1148 });
1149 assert!(found, "profile reply must advertise HOST_CONTROL");
1150 }
1151
1152 #[test]
1153 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
1154 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
1155
1156 let mut d = driver_with_sessions();
1157 let mmi_nb = MMI_SESSION;
1158
1159 d.mmi_menu_answer(2).unwrap();
1162 d.device_mut().inbound.push_back(sb());
1163 d.pump(Duration::from_millis(10)).unwrap();
1164 assert_apdu_on_session(
1165 &d,
1166 mmi_nb,
1167 &ser(&MenuAnsw {
1168 length_form: dvb_ci::length::LengthForm::Minimal,
1169 choice_ref: 2,
1170 }),
1171 );
1172
1173 d.mmi_enquiry_answer(b"1234").unwrap();
1175 d.device_mut().inbound.push_back(sb());
1176 d.pump(Duration::from_millis(10)).unwrap();
1177 assert_apdu_on_session(
1178 &d,
1179 mmi_nb,
1180 &ser(&Answ {
1181 length_form: dvb_ci::length::LengthForm::Minimal,
1182 answ_id: AnswId::Answer,
1183 text_chars: b"1234",
1184 }),
1185 );
1186 }
1187
1188 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
1191 use dvb_ci::spdu::SessionNumber;
1192 let mut want = ser(&SessionNumber {
1193 length_form: dvb_ci::length::LengthForm::Minimal,
1194 session_nb,
1195 });
1196 want.extend_from_slice(apdu);
1197 let hit = d.device().ops.iter().any(|op| match op {
1198 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1199 _ => false,
1200 });
1201 assert!(
1202 hit,
1203 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
1204 );
1205 }
1206
1207 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
1211 use dvb_ci::spdu::SessionNumber;
1212 let mut want = ser(&SessionNumber {
1213 length_form: dvb_ci::length::LengthForm::Minimal,
1214 session_nb,
1215 });
1216 want.extend_from_slice(apdu);
1217 d.device()
1218 .ops
1219 .iter()
1220 .filter(|op| match op {
1221 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1222 _ => false,
1223 })
1224 .count()
1225 }
1226
1227 struct SettleDevice {
1230 inner: MockCaDevice,
1231 settle: Duration,
1232 }
1233
1234 impl CaDevice for SettleDevice {
1235 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1236 self.inner.read(buf)
1237 }
1238 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1239 self.inner.write(buf)
1240 }
1241 fn reset(&mut self) -> io::Result<()> {
1242 self.inner.reset()
1243 }
1244 fn reset_settle(&self) -> Duration {
1245 self.settle
1246 }
1247 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1248 self.inner.slot_info()
1249 }
1250 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1251 self.inner.poll(timeout)
1252 }
1253 }
1254
1255 const SETTLE: Duration = Duration::from_secs(3);
1256
1257 fn settling_driver(clock: &TestClock) -> Driver<SettleDevice> {
1258 Driver::new(SettleDevice {
1259 inner: MockCaDevice::new([]),
1260 settle: SETTLE,
1261 })
1262 .with_clock(clock.as_fn())
1263 }
1264
1265 fn create_tc_writes(d: &Driver<SettleDevice>) -> usize {
1266 d.device()
1267 .inner
1268 .ops
1269 .iter()
1270 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1271 .count()
1272 }
1273
1274 struct NotRunningDevice {
1278 inner: SettleDevice,
1279 refusals_left: u32,
1280 refused: u32,
1281 }
1282
1283 impl CaDevice for NotRunningDevice {
1284 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1285 self.inner.read(buf)
1286 }
1287 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1288 if self.refusals_left > 0 {
1289 self.refusals_left -= 1;
1290 self.refused += 1;
1291 return Err(io::Error::from(io::ErrorKind::InvalidInput));
1292 }
1293 self.inner.write(buf)
1294 }
1295 fn reset(&mut self) -> io::Result<()> {
1296 self.inner.reset()
1297 }
1298 fn reset_settle(&self) -> Duration {
1299 self.inner.reset_settle()
1300 }
1301 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1302 self.inner.slot_info()
1303 }
1304 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1305 self.inner.poll(timeout)
1306 }
1307 }
1308
1309 fn not_running_driver(clock: &TestClock, refusals: u32) -> Driver<NotRunningDevice> {
1310 Driver::new(NotRunningDevice {
1311 inner: SettleDevice {
1312 inner: MockCaDevice::new([]),
1313 settle: SETTLE,
1314 },
1315 refusals_left: refusals,
1316 refused: 0,
1317 })
1318 .with_clock(clock.as_fn())
1319 }
1320
1321 fn create_tc_count(d: &Driver<NotRunningDevice>) -> usize {
1322 d.device()
1323 .inner
1324 .inner
1325 .ops
1326 .iter()
1327 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1328 .count()
1329 }
1330
1331 #[test]
1336 fn post_reset_einval_is_retried_until_the_slot_is_running() {
1337 let clock = TestClock::new();
1338 let mut d = not_running_driver(&clock, 3);
1339 d.init().unwrap();
1340 clock.advance(SETTLE + Duration::from_millis(100));
1341 for _ in 0..10 {
1342 d.pump(Duration::from_millis(10))
1343 .expect("a not-yet-RUNNING slot (EINVAL) must be retried, not reported");
1344 if create_tc_count(&d) == 1 {
1345 break;
1346 }
1347 clock.advance(Duration::from_millis(300));
1348 }
1349 assert_eq!(d.device().refused, 3, "the three refusals were consumed");
1350 assert_eq!(create_tc_count(&d), 1, "Create_T_C delivered exactly once");
1351 }
1352
1353 #[test]
1356 fn post_reset_einval_is_reported_once_the_grace_window_has_passed() {
1357 let clock = TestClock::new();
1358 let mut d = not_running_driver(&clock, u32::MAX);
1359 let start = clock.0.get();
1360 d.init().unwrap();
1361 clock.advance(SETTLE + Duration::from_millis(100));
1362 let mut failure = None;
1363 for _ in 0..80 {
1364 match d.pump(Duration::from_millis(10)) {
1365 Ok(_) => clock.advance(Duration::from_millis(300)),
1366 Err(e) => {
1367 failure = Some(e);
1368 break;
1369 }
1370 }
1371 }
1372 let e = failure.expect("a slot that never runs must eventually be reported");
1373 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1374 assert!(
1375 clock.0.get() >= start + SETTLE + SLOT_READY_GRACE,
1376 "reported before the grace window ended"
1377 );
1378 assert_eq!(create_tc_count(&d), 0);
1379 }
1380
1381 #[test]
1386 fn requery_armed_before_the_handshake_sends_nothing() {
1387 let clock = TestClock::new();
1388 let mut d = Driver::new(MockCaDevice::new([])).with_clock(clock.as_fn());
1389 d.set_requery_interval(Duration::from_secs(10));
1390 d.init().unwrap();
1391 for _ in 0..40 {
1392 clock.advance(Duration::from_secs(1));
1393 d.pump(Duration::from_millis(100)).unwrap();
1394 }
1395 let ca_pmt_tag = [0x9F, 0x80, 0x32];
1396 let ca_pmt_writes = d
1397 .device()
1398 .ops
1399 .iter()
1400 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == ca_pmt_tag)))
1401 .count();
1402 assert_eq!(
1403 ca_pmt_writes, 0,
1404 "re-query wrote a ca_pmt before any service"
1405 );
1406 }
1407
1408 #[test]
1411 fn reset_does_not_block_pump() {
1412 let clock = TestClock::new();
1413 let mut d = settling_driver(&clock);
1414 let t = std::time::Instant::now();
1415 d.init().unwrap();
1416 d.pump(Duration::from_millis(10)).unwrap();
1417 assert!(t.elapsed() < Duration::from_secs(1), "reset/pump blocked");
1418 assert_eq!(d.device().inner.ops[0], DeviceOp::Reset);
1419 }
1420
1421 #[test]
1425 fn no_transport_traffic_until_settle_elapsed() {
1426 let clock = TestClock::new();
1427 let mut d = settling_driver(&clock);
1428 d.device_mut()
1429 .inner
1430 .inbound
1431 .push_back(vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1432 d.init().unwrap();
1433 for _ in 0..5 {
1434 clock.advance(Duration::from_millis(500));
1435 d.pump(Duration::from_millis(10)).unwrap();
1436 }
1437 assert_eq!(create_tc_writes(&d), 0);
1439 assert_eq!(
1440 d.device().inner.inbound.len(),
1441 1,
1442 "frame read while settling"
1443 );
1444
1445 clock.advance(Duration::from_millis(600));
1446 d.pump(Duration::from_millis(10)).unwrap();
1447 assert_eq!(create_tc_writes(&d), 1);
1448
1449 let mut z = Driver::new(MockCaDevice::new([]));
1450 z.init().unwrap();
1451 assert_eq!(
1452 d.device().inner.written()[..3],
1453 z.device().written()[..3],
1454 "post-settle Create_T_C differs from the zero-settle sequence"
1455 );
1456 assert!(d.device().inner.inbound.is_empty());
1459 }
1460
1461 #[test]
1464 fn second_reset_restarts_settle() {
1465 let clock = TestClock::new();
1466 let mut d = settling_driver(&clock);
1467 d.init().unwrap();
1468 clock.advance(Duration::from_secs(2));
1469 d.pump(Duration::from_millis(10)).unwrap();
1470 d.init().unwrap(); clock.advance(Duration::from_secs(2)); d.pump(Duration::from_millis(10)).unwrap();
1473 assert_eq!(create_tc_writes(&d), 0, "first deadline wrongly honoured");
1474 clock.advance(Duration::from_millis(1500)); d.pump(Duration::from_millis(10)).unwrap();
1476 assert_eq!(create_tc_writes(&d), 1);
1477 }
1478
1479 #[test]
1482 fn slot_sampling_continues_while_settling() {
1483 let clock = TestClock::new();
1484 let mut d = settling_driver(&clock);
1485 d.init().unwrap();
1486 d.take_notifications();
1487 d.device_mut().inner.slot.module_present = false;
1488 clock.advance(Duration::from_millis(500));
1489 d.pump(Duration::from_millis(10)).unwrap();
1490 let notes = d.take_notifications();
1491 assert!(
1492 notes
1493 .iter()
1494 .any(|n| matches!(n, Notification::HotPlug(HotPlug::CamRemoved))),
1495 "{notes:?}"
1496 );
1497 assert_eq!(create_tc_writes(&d), 0);
1498 }
1499
1500 #[test]
1501 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
1502 let mut d = Driver::new(MockCaDevice::new([]));
1503 d.init().unwrap();
1504 let ops = &d.device().ops;
1505 assert_eq!(ops[0], DeviceOp::Reset);
1506 assert_eq!(ops[1], DeviceOp::SlotInfo);
1507 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
1508 }
1509
1510 #[test]
1511 fn reads_reply_then_polls_on_pump() {
1512 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1514 let clock = TestClock::new();
1515 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1516 d.init().unwrap();
1517 assert!(d.pump(Duration::from_millis(100)).unwrap());
1519 clock.advance(Duration::from_millis(100));
1524 assert!(!d.pump(Duration::from_millis(100)).unwrap());
1526 let last = d.device().ops.last().unwrap();
1527 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1528 }
1529
1530 #[test]
1536 fn pump_advances_the_stack_clock_by_real_elapsed_time_not_by_timeout() {
1537 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1538 let clock = TestClock::new();
1539 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1540 d.init().unwrap();
1541 assert!(d.pump(Duration::from_secs(1)).unwrap());
1542
1543 clock.advance(Duration::from_millis(1));
1547 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1548 assert!(
1549 d.device().ops.last().is_none_or(|op| !matches!(
1550 op,
1551 DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)
1552 )),
1553 "must not poll after only 1ms of real elapsed time, regardless of the 1s `timeout` argument"
1554 );
1555
1556 clock.advance(Duration::from_millis(100));
1557 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1558 let last = d.device().ops.last().unwrap();
1559 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1560 }
1561
1562 #[test]
1565 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
1566 let mut dev = MockCaDevice::new([]);
1567 dev.slot = SlotInfo {
1568 num: 0,
1569 module_ready: false,
1570 module_present: false,
1571 };
1572 let mut d = Driver::new(dev);
1573 d.init().unwrap();
1574 let notes = d.take_notifications();
1577 assert!(
1578 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1579 "baseline observation must not fire CamPresent, got {notes:?}"
1580 );
1581 let resets_before = d
1582 .device()
1583 .ops
1584 .iter()
1585 .filter(|o| **o == DeviceOp::Reset)
1586 .count();
1587
1588 d.device_mut().slot = SlotInfo {
1590 num: 0,
1591 module_ready: true,
1592 module_present: true,
1593 };
1594 d.pump(Duration::from_millis(10)).unwrap();
1595
1596 let notes = d.take_notifications();
1597 let cam_present_count = notes
1598 .iter()
1599 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
1600 .count();
1601 assert_eq!(
1602 cam_present_count, 1,
1603 "expected exactly one CamPresent, got {notes:?}"
1604 );
1605 let resets_after = d
1607 .device()
1608 .ops
1609 .iter()
1610 .filter(|o| **o == DeviceOp::Reset)
1611 .count();
1612 assert_eq!(
1613 resets_after,
1614 resets_before + 1,
1615 "expected one fresh Reset on re-insert"
1616 );
1617 assert!(
1618 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
1619 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
1620 d.device().ops.last()
1621 );
1622 }
1623
1624 #[test]
1625 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
1626 let mut d = driver_with_sessions();
1627 d.take_notifications();
1628
1629 d.device_mut().slot.module_present = false;
1631 d.pump(Duration::from_millis(10)).unwrap();
1632 let notes = d.take_notifications();
1633 assert!(
1634 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
1635 "expected CamRemoved, got {notes:?}"
1636 );
1637
1638 d.mmi_menu_answer(0).unwrap();
1642 let notes = d.take_notifications();
1643 assert!(
1644 notes
1645 .iter()
1646 .any(|n| matches!(n, Notification::Error { .. })),
1647 "expected no open MMI session after teardown, got {notes:?}"
1648 );
1649
1650 let resets_before = d
1652 .device()
1653 .ops
1654 .iter()
1655 .filter(|o| **o == DeviceOp::Reset)
1656 .count();
1657 d.device_mut().slot.module_present = true;
1658 d.device_mut().slot.module_ready = true;
1659 d.pump(Duration::from_millis(10)).unwrap();
1660 let notes = d.take_notifications();
1661 assert!(
1662 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1663 "expected CamPresent on re-insert, got {notes:?}"
1664 );
1665 let resets_after = d
1666 .device()
1667 .ops
1668 .iter()
1669 .filter(|o| **o == DeviceOp::Reset)
1670 .count();
1671 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1672 }
1673
1674 #[test]
1675 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1676 let mut d = Driver::new(MockCaDevice::new([]));
1677 d.init().unwrap();
1678 d.take_notifications();
1679
1680 for _ in 0..5 {
1681 d.pump(Duration::from_millis(10)).unwrap();
1682 }
1683 let notes = d.take_notifications();
1684 assert!(
1685 !notes.iter().any(|n| matches!(
1686 n,
1687 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1688 )),
1689 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1690 );
1691 }
1692
1693 #[test]
1694 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1695 use dvb_ci::objects::ca_info::CaInfo;
1696
1697 let mut d = driver_with_sessions();
1698 d.take_notifications();
1699
1700 feed(
1702 &mut d,
1703 r_apdu(
1704 CA_SESSION,
1705 &ser(&CaInfo {
1706 length_form: dvb_ci::length::LengthForm::Minimal,
1707 ca_system_ids: vec![],
1708 }),
1709 ),
1710 );
1711 let notes = d.take_notifications();
1712 assert!(
1713 !notes.iter().any(|n| matches!(
1714 n,
1715 Notification::HotPlug(
1716 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1717 )
1718 )),
1719 "first ca_info must only establish the baseline, got {notes:?}"
1720 );
1721
1722 feed(
1724 &mut d,
1725 r_apdu(
1726 CA_SESSION,
1727 &ser(&CaInfo {
1728 length_form: dvb_ci::length::LengthForm::Minimal,
1729 ca_system_ids: vec![0x0B00],
1730 }),
1731 ),
1732 );
1733 let notes = d.take_notifications();
1734 assert!(
1735 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1736 "expected CardInserted, got {notes:?}"
1737 );
1738
1739 feed(
1741 &mut d,
1742 r_apdu(
1743 CA_SESSION,
1744 &ser(&CaInfo {
1745 length_form: dvb_ci::length::LengthForm::Minimal,
1746 ca_system_ids: vec![0x1800],
1747 }),
1748 ),
1749 );
1750 let notes = d.take_notifications();
1751 assert!(
1752 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1753 "expected CardChanged, got {notes:?}"
1754 );
1755 }
1756
1757 #[test]
1758 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1759 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1760
1761 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1762 CaPmtReply {
1763 reserved_ca_enable: Default::default(),
1764 reserved_version: Default::default(),
1765 length_form: dvb_ci::length::LengthForm::Minimal,
1766 program_number: 1,
1767 version_number: 1,
1768 current_next_indicator: true,
1769 ca_enable,
1770 streams: vec![],
1771 }
1772 }
1773
1774 let mut d = driver_with_sessions();
1775 d.take_notifications();
1776
1777 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1779 let notes = d.take_notifications();
1780 assert!(
1781 !notes.iter().any(|n| matches!(
1782 n,
1783 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1784 )),
1785 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1786 );
1787
1788 feed(
1790 &mut d,
1791 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1792 );
1793 let notes = d.take_notifications();
1794 assert!(
1795 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1796 "expected CardInserted, got {notes:?}"
1797 );
1798
1799 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1801 let notes = d.take_notifications();
1802 assert!(
1803 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1804 "expected CardRemoved, got {notes:?}"
1805 );
1806 }
1807
1808 #[test]
1809 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1810 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1811
1812 let mut d = driver_with_sessions();
1813 d.take_notifications();
1814
1815 feed(
1818 &mut d,
1819 r_apdu(
1820 CA_SESSION,
1821 &ser(&CaPmtReply {
1822 reserved_ca_enable: Default::default(),
1823 reserved_version: Default::default(),
1824 length_form: dvb_ci::length::LengthForm::Minimal,
1825 program_number: 7,
1826 version_number: 1,
1827 current_next_indicator: true,
1828 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1829 streams: vec![],
1830 }),
1831 ),
1832 );
1833 let notes = d.take_notifications();
1834 assert!(
1835 notes.contains(&Notification::CaPmtReply {
1836 program_number: 7,
1837 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1838 descrambling_ok: true,
1839 }),
1840 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1841 );
1842 }
1843
1844 #[test]
1845 fn ca_pmt_reply_flag_clear_surfaces_none() {
1846 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1847
1848 let mut d = driver_with_sessions();
1849 d.take_notifications();
1850
1851 feed(
1854 &mut d,
1855 r_apdu(
1856 CA_SESSION,
1857 &ser(&CaPmtReply {
1858 reserved_ca_enable: Default::default(),
1859 reserved_version: Default::default(),
1860 length_form: dvb_ci::length::LengthForm::Minimal,
1861 program_number: 7,
1862 version_number: 1,
1863 current_next_indicator: true,
1864 ca_enable: None,
1865 streams: vec![],
1866 }),
1867 ),
1868 );
1869 let notes = d.take_notifications();
1870 assert!(
1871 notes.contains(&Notification::CaPmtReply {
1872 program_number: 7,
1873 ca_enable: None,
1874 descrambling_ok: false,
1875 }),
1876 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1877 );
1878 }
1879
1880 #[test]
1881 fn mmi_no_card_text_infers_card_removed() {
1882 use dvb_ci::objects::mmi_high::Enq;
1883
1884 let mut d = driver_with_sessions();
1885 d.take_notifications();
1886
1887 feed(
1888 &mut d,
1889 r_apdu(
1890 MMI_SESSION,
1891 &ser(&Enq {
1892 reserved: Default::default(),
1893 length_form: dvb_ci::length::LengthForm::Minimal,
1894 blind_answer: false,
1895 answer_text_length: 0,
1896 text_chars: b"NO CARD detected - please insert your smart card",
1897 }),
1898 ),
1899 );
1900
1901 let notes = d.take_notifications();
1902 assert!(
1903 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1904 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1905 );
1906 }
1907
1908 #[test]
1911 fn mmi_card_keyword_heuristic_is_edge_triggered() {
1912 use dvb_ci::objects::mmi_high::Enq;
1913
1914 let enq = |text: &'static [u8]| {
1915 r_apdu(
1916 MMI_SESSION,
1917 &ser(&Enq {
1918 reserved: Default::default(),
1919 length_form: dvb_ci::length::LengthForm::Minimal,
1920 blind_answer: false,
1921 answer_text_length: 0,
1922 text_chars: text,
1923 }),
1924 )
1925 };
1926 let mut d = driver_with_sessions();
1927 d.take_notifications();
1928 let count = |notes: &[Notification], hp: HotPlug| {
1929 notes
1930 .iter()
1931 .filter(|n| **n == Notification::HotPlug(hp))
1932 .count()
1933 };
1934
1935 for _ in 0..3 {
1937 feed(&mut d, enq(b"No card - please insert your smart card"));
1938 }
1939 let notes = d.take_notifications();
1940 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1941 assert_eq!(count(¬es, HotPlug::CardInserted), 0);
1942
1943 for _ in 0..2 {
1945 feed(&mut d, enq(b"Your entitlement is valid"));
1946 }
1947 let notes = d.take_notifications();
1948 assert_eq!(count(¬es, HotPlug::CardInserted), 1, "{notes:?}");
1949 assert_eq!(count(¬es, HotPlug::CardRemoved), 0);
1950
1951 feed(&mut d, enq(b"Card removed"));
1953 feed(&mut d, enq(b"Card removed"));
1954 let notes = d.take_notifications();
1955 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1956 }
1957
1958 #[test]
1959 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1960 let mut dev = MockCaDevice::new([]);
1961 dev.slot = SlotInfo {
1962 num: 0,
1963 module_ready: false,
1964 module_present: false,
1965 };
1966 let mut d = Driver::new(dev);
1967 d.init().unwrap();
1968 d.take_notifications(); d.device_mut().slot = SlotInfo {
1972 num: 0,
1973 module_ready: true,
1974 module_present: true,
1975 };
1976
1977 let mut seen = Vec::new();
1978 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1979 .unwrap();
1980
1981 assert_eq!(
1982 seen,
1983 vec![HotPlug::CamPresent],
1984 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1985 );
1986 }
1987
1988 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: Pid) -> [u8; 6] {
1993 [
1994 0x09,
1995 0x04,
1996 (ca_system_id >> 8) as u8,
1997 ca_system_id as u8,
1998 0xE0 | ((pid.value() >> 8) as u8 & 0x1F),
1999 pid.value() as u8,
2000 ]
2001 }
2002
2003 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
2020 const VIACCESS: u16 = 0x0500;
2021 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2022 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2023
2024 let mut body = Vec::new();
2025 body.push(0x02); body.push(0); body.push(0);
2028 body.extend_from_slice(&program_number.to_be_bytes());
2029 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
2034 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2035 body.push(prog_ca.len() as u8);
2036 body.extend_from_slice(&prog_ca);
2037 body.push(0x1B);
2039 body.push(0xE0 | 0x01);
2040 body.push(0x00);
2041 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2042 body.push(es0_ca.len() as u8);
2043 body.extend_from_slice(&es0_ca);
2044 body.push(0x0F);
2046 body.push(0xE0 | 0x01);
2047 body.push(0x01);
2048 body.push(0xF0);
2049 body.push(0x00);
2050
2051 let section_length = body.len() - 3 + 4;
2052 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2053 body[2] = section_length as u8;
2054 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2055 body.extend_from_slice(&crc.to_be_bytes());
2056 body
2057 }
2058
2059 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
2066 const VIACCESS: u16 = 0x0500;
2067 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2068 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2069
2070 let mut body = Vec::new();
2071 body.push(0x02); body.push(0); body.push(0);
2074 body.extend_from_slice(&program_number.to_be_bytes());
2075 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));
2081 body.push(prog_ca.len() as u8);
2082 body.extend_from_slice(&prog_ca);
2083 body.push(0x1B);
2085 body.push(0xE0 | 0x01);
2086 body.push(0x00);
2087 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2088 body.push(es0_ca.len() as u8);
2089 body.extend_from_slice(&es0_ca);
2090 body.push(0x0F);
2092 body.push(0xE0 | 0x01);
2093 body.push(0x01);
2094 body.push(0xF0);
2095 body.push(0x00);
2096
2097 let section_length = body.len() - 3 + 4;
2098 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2099 body[2] = section_length as u8;
2100 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2101 body.extend_from_slice(&crc.to_be_bytes());
2102 body
2103 }
2104
2105 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
2109 let mut body = Vec::new();
2110 body.push(0x02);
2111 body.push(0);
2112 body.push(0);
2113 body.extend_from_slice(&program_number.to_be_bytes());
2114 body.push(0xC3);
2115 body.push(0x00);
2116 body.push(0x00);
2117 body.push(0xE0 | 0x01);
2118 body.push(0x00);
2119 body.push(0xF0); body.push(0x00);
2121 body.push(0x1B);
2123 body.push(0xE0 | 0x01);
2124 body.push(0x00);
2125 body.push(0xF0);
2126 body.push(0x00);
2127
2128 let section_length = body.len() - 3 + 4;
2129 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2130 body[2] = section_length as u8;
2131 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2132 body.extend_from_slice(&crc.to_be_bytes());
2133 body
2134 }
2135
2136 #[test]
2137 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
2138 use broadcast_common::Parse;
2139
2140 let mut d = driver_with_sessions();
2141 d.take_notifications();
2142
2143 let pmt_bytes = build_ca_pmt_fixture(1546);
2144 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2145
2146 d.add_service(&pmt).unwrap();
2147 d.device_mut().inbound.push_back(sb());
2148 d.pump(Duration::from_millis(10)).unwrap();
2149
2150 let expected =
2154 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2155 assert_apdu_on_session(&d, CA_SESSION, &expected);
2156
2157 let svc = d
2159 .managed_ca()
2160 .services()
2161 .get(&1546)
2162 .expect("program_number 1546 must be tracked after add_service");
2163 assert_eq!(svc.es_pids, vec![Pid::new(0x0100), Pid::new(0x0101)]);
2164 assert_eq!(svc.ca_pids, vec![Pid::new(0x0064), Pid::new(0x0065)]);
2165 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
2166 assert_eq!(svc.last_ca_enable, None);
2167 }
2168
2169 #[test]
2170 fn add_service_rejects_pmt_without_ca_descriptor() {
2171 use broadcast_common::Parse;
2172
2173 let mut d = driver_with_sessions();
2174 let pmt_bytes = build_clear_pmt_fixture(999);
2175 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2176
2177 let err = d.add_service(&pmt).unwrap_err();
2178 assert!(
2179 matches!(
2180 err,
2181 CaError::NoCaDescriptor {
2182 program_number: 999
2183 }
2184 ),
2185 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
2186 );
2187 assert!(
2188 d.managed_ca().services().is_empty(),
2189 "a rejected PMT must not be recorded"
2190 );
2191 }
2192
2193 #[test]
2205 fn requery_tick_serialize_failure_errors_from_pump_instead_of_panicking() {
2206 use broadcast_common::Parse;
2207
2208 let clock = TestClock::new();
2209 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2210
2211 let good_bytes = build_ca_pmt_fixture(1);
2214 let good = PmtSection::parse(&good_bytes).unwrap();
2215 assert_eq!(good.program_number, 1, "fixture precondition");
2216 d.add_service(&good).unwrap();
2217
2218 let pmt_bytes = build_ca_pmt_fixture(1546);
2223 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2224 let built = d
2225 .build_ca_pmt_filtered(
2226 &pmt,
2227 CaPmtListManagement::Update,
2228 CaPmtCmdId::OkDescrambling,
2229 )
2230 .try_to_bytes()
2231 .expect("fixture precondition: the good PMT encodes");
2232 let corrupt_raw = pmt_bytes[..pmt_bytes.len() - 6].to_vec();
2233 d.managed.record(
2234 1546,
2235 managed::service_of(&pmt, CaPmtCmdId::OkDescrambling, built, corrupt_raw),
2236 );
2237 d.take_notifications();
2238 for _ in 0..16 {
2242 d.device_mut().inbound.push_back(sb());
2243 d.pump(Duration::from_millis(10)).unwrap();
2244 }
2245 d.set_requery_interval(Duration::from_secs(5));
2246
2247 clock.advance(Duration::from_secs(5));
2251 let err = d
2252 .pump(Duration::ZERO)
2253 .expect_err("re-query of a corrupt stored PMT must error, not panic");
2254 assert!(
2255 matches!(
2256 err.get_ref(),
2257 Some(e) if e.downcast_ref::<CaError>()
2258 .is_some_and(|ca| matches!(ca, CaError::PmtParse(_)))
2259 ),
2260 "expected CaError::PmtParse from the corrupt stored bytes, got {err:?}"
2261 );
2262 assert!(
2263 err.to_string().contains("stored PMT re-parse failed"),
2264 "error must name the re-parse failure, got: {err}"
2265 );
2266 assert!(d.pump(Duration::ZERO).is_ok());
2270 clock.advance(Duration::from_secs(5));
2271 let err2 = d
2272 .pump(Duration::ZERO)
2273 .expect_err("the corrupt service must fail every re-query");
2274 assert!(
2275 err2.to_string().contains("stored PMT re-parse failed"),
2276 "failure must persist, got: {err2}"
2277 );
2278 }
2279
2280 fn corrupt_ca_section_ctor() -> PmtSection<'static> {
2286 const LOOP: usize = 5000;
2287 let mut prog_info: Vec<u8> = Vec::with_capacity(LOOP);
2288 for _ in 0..LOOP / 4 {
2289 prog_info.extend_from_slice(&[0x09u8, 0x02, 0x0B, 0x00]);
2291 }
2292 assert_eq!(prog_info.len(), LOOP);
2293 PmtSection::new(
2294 1,
2295 1,
2296 true,
2297 0,
2298 0,
2299 Pid::new(0x0200),
2300 dvb_si::descriptors::DescriptorLoop::new(Box::leak(prog_info.into_boxed_slice())),
2301 Vec::new(),
2302 )
2303 }
2304
2305 #[test]
2310 fn add_service_serialize_failure_returns_err_instead_of_panicking() {
2311 let mut d = driver_with_sessions();
2312 d.managed.set_cam_caids([0x0B00u16].into_iter().collect());
2313 let pmt = corrupt_ca_section_ctor();
2314 let err = d
2315 .add_service(&pmt)
2316 .expect_err("a ca_pmt with no valid encoding must error, not panic");
2317 assert!(
2318 matches!(err, CaError::Serialize(_)),
2319 "expected CaError::Serialize, got {err:?}"
2320 );
2321 assert!(
2322 d.managed_ca().services().is_empty(),
2323 "a rejected PMT must not be recorded"
2324 );
2325 }
2326
2327 #[test]
2328 fn add_service_second_call_uses_add_list_management() {
2329 use broadcast_common::Parse;
2330
2331 let mut d = driver_with_sessions();
2332 d.take_notifications();
2333
2334 let pmt1_bytes = build_ca_pmt_fixture(1546);
2335 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2336 d.add_service(&pmt1).unwrap();
2337 d.device_mut().inbound.push_back(sb());
2338 d.pump(Duration::from_millis(10)).unwrap();
2339
2340 let pmt2_bytes = build_ca_pmt_fixture(1547);
2341 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2342 d.add_service(&pmt2).unwrap();
2343 d.device_mut().inbound.push_back(sb());
2344 d.pump(Duration::from_millis(10)).unwrap();
2345
2346 let expected2 =
2348 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
2349 assert_apdu_on_session(&d, CA_SESSION, &expected2);
2350
2351 assert_eq!(d.managed_ca().services().len(), 2);
2352 }
2353
2354 fn build_ca_pmt_fixture_two_caids(program_number: u16) -> Vec<u8> {
2365 const VIACCESS: u16 = 0x0500;
2366 const NAGRA: u16 = 0x1800;
2367 let prog_ca_a = ca_descriptor(VIACCESS, Pid::new(0x0064));
2368 let prog_ca_b = ca_descriptor(NAGRA, Pid::new(0x0066));
2369 let mut prog_ca = Vec::new();
2370 prog_ca.extend_from_slice(&prog_ca_a);
2371 prog_ca.extend_from_slice(&prog_ca_b);
2372
2373 let mut body = Vec::new();
2374 body.push(0x02); body.push(0);
2376 body.push(0);
2377 body.extend_from_slice(&program_number.to_be_bytes());
2378 body.push(0xC3);
2379 body.push(0x00);
2380 body.push(0x00);
2381 body.push(0xE0 | 0x01); body.push(0x00);
2383 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2384 body.push(prog_ca.len() as u8);
2385 body.extend_from_slice(&prog_ca);
2386 body.push(0x0F);
2389 body.push(0xE0 | 0x01);
2390 body.push(0x00);
2391 body.push(0xF0);
2392 body.push(0x00);
2393
2394 let section_length = body.len() - 3 + 4;
2395 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2396 body[2] = section_length as u8;
2397 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2398 body.extend_from_slice(&crc.to_be_bytes());
2399 body
2400 }
2401
2402 #[test]
2403 fn add_service_filters_ca_descriptors_to_cam_advertised_caids() {
2404 use broadcast_common::Parse;
2405 use dvb_ci::objects::ca_info::CaInfo;
2406
2407 let mut d = driver_with_sessions();
2408 d.take_notifications();
2409
2410 feed(
2413 &mut d,
2414 r_apdu(
2415 CA_SESSION,
2416 &ser(&CaInfo {
2417 length_form: dvb_ci::length::LengthForm::Minimal,
2418 ca_system_ids: vec![0x0500],
2419 }),
2420 ),
2421 );
2422 d.take_notifications();
2423
2424 let pmt_bytes = build_ca_pmt_fixture_two_caids(1560);
2425 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2426
2427 let expected_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2432 &pmt,
2433 &[0x0500],
2434 CaPmtListManagement::Only,
2435 CaPmtCmdId::OkDescrambling,
2436 )
2437 .to_bytes();
2438 let unfiltered =
2439 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2440 assert_ne!(
2441 expected_filtered, unfiltered,
2442 "precondition: the fixture's two CAIDs must make filtered != unfiltered"
2443 );
2444 let sends_filtered_before = count_apdu_on_session(&d, CA_SESSION, &expected_filtered);
2445 let sends_unfiltered_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered);
2446
2447 d.add_service(&pmt).unwrap();
2448 d.device_mut().inbound.push_back(sb());
2449 d.pump(Duration::from_millis(10)).unwrap();
2450
2451 assert_eq!(
2452 count_apdu_on_session(&d, CA_SESSION, &expected_filtered),
2453 sends_filtered_before + 1,
2454 "add_service must send the CAID-filtered ca_pmt"
2455 );
2456 assert_eq!(
2457 count_apdu_on_session(&d, CA_SESSION, &unfiltered),
2458 sends_unfiltered_before,
2459 "add_service must NOT send the unfiltered ca_pmt once the CAM's CAIDs are known"
2460 );
2461 }
2462
2463 #[test]
2464 fn requery_timer_filters_ca_descriptors_to_cam_advertised_caids() {
2465 use broadcast_common::Parse;
2466 use dvb_ci::objects::ca_info::CaInfo;
2467
2468 let clock = TestClock::new();
2469 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2470 d.set_requery_interval(Duration::from_secs(10));
2473 d.take_notifications();
2474
2475 let pmt_bytes = build_ca_pmt_fixture_two_caids(1561);
2476 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2477 d.add_service(&pmt).unwrap();
2480 d.device_mut().inbound.push_back(sb());
2481 clock.advance(Duration::from_millis(10));
2482 d.pump(Duration::from_millis(10)).unwrap();
2483
2484 feed(
2486 &mut d,
2487 r_apdu(
2488 CA_SESSION,
2489 &ser(&CaInfo {
2490 length_form: dvb_ci::length::LengthForm::Minimal,
2491 ca_system_ids: vec![0x0500],
2492 }),
2493 ),
2494 );
2495 d.take_notifications();
2496
2497 let expected_query_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2500 &pmt,
2501 &[0x0500],
2502 CaPmtListManagement::Update,
2503 CaPmtCmdId::Query,
2504 )
2505 .to_bytes();
2506 let expected_ok_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2507 &pmt,
2508 &[0x0500],
2509 CaPmtListManagement::Update,
2510 CaPmtCmdId::OkDescrambling,
2511 )
2512 .to_bytes();
2513 let unfiltered_query =
2514 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2515 let unfiltered_ok = build_ca_pmt(
2516 &pmt,
2517 CaPmtListManagement::Update,
2518 CaPmtCmdId::OkDescrambling,
2519 )
2520 .to_bytes();
2521 assert_ne!(
2522 expected_query_filtered, unfiltered_query,
2523 "precondition: the fixture's two CAIDs must make filtered != unfiltered (query)"
2524 );
2525 assert_ne!(
2526 expected_ok_filtered, unfiltered_ok,
2527 "precondition: the fixture's two CAIDs must make filtered != unfiltered (ok_descrambling)"
2528 );
2529 let sends_query_before = count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered);
2530 let sends_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered);
2531 let sends_unfiltered_query_before =
2532 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query);
2533 let sends_unfiltered_ok_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok);
2534
2535 clock.advance(Duration::from_secs(11));
2536 d.pump(Duration::from_secs(11)).unwrap();
2537 feed(&mut d, sb());
2538
2539 assert_eq!(
2540 count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered),
2541 sends_query_before + 1,
2542 "the re-query `query` half must be CAID-filtered to the CAM's advertised CAIDs"
2543 );
2544 assert_eq!(
2545 count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered),
2546 sends_ok_before + 1,
2547 "the re-query `ok_descrambling` half must be CAID-filtered to the CAM's advertised CAIDs"
2548 );
2549 assert_eq!(
2550 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query),
2551 sends_unfiltered_query_before,
2552 "the re-query resend must NOT be the unfiltered `query` ca_pmt"
2553 );
2554 assert_eq!(
2555 count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok),
2556 sends_unfiltered_ok_before,
2557 "the re-query resend must NOT be the unfiltered `ok_descrambling` ca_pmt"
2558 );
2559 }
2560
2561 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
2571 const EXTENSION_HEADER_LEN: u16 = 5;
2572 const CRC_LEN: u16 = 4;
2573 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
2574 let mut v = Vec::new();
2575 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
2577 v.push((section_length & 0xFF) as u8);
2578 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
2583 let crc = broadcast_common::crc32_mpeg2::compute(&v);
2584 v.extend_from_slice(&crc.to_be_bytes());
2585 v
2586 }
2587
2588 #[test]
2589 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
2590 use broadcast_common::Parse;
2591 use dvb_ci::objects::ca_info::CaInfo;
2592 use dvb_si::tables::cat::CatSection;
2593
2594 let mut d = driver_with_sessions();
2595 d.take_notifications();
2596
2597 feed(
2599 &mut d,
2600 r_apdu(
2601 CA_SESSION,
2602 &ser(&CaInfo {
2603 length_form: dvb_ci::length::LengthForm::Minimal,
2604 ca_system_ids: vec![0x0648, 0x0100],
2605 }),
2606 ),
2607 );
2608 d.take_notifications();
2609
2610 let mut descriptors = Vec::new();
2613 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2614 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2615 let cat_bytes = build_cat_fixture(&descriptors);
2616 let cat = CatSection::parse(&cat_bytes).unwrap();
2617
2618 d.set_cat(&cat).unwrap();
2619
2620 assert_eq!(
2621 d.emm_pids(),
2622 &[Pid::new(0x1FF0)],
2623 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
2624 );
2625 }
2626
2627 #[test]
2628 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
2629 use broadcast_common::Parse;
2630 use dvb_ci::objects::ca_info::CaInfo;
2631 use dvb_si::tables::cat::CatSection;
2632
2633 let mut d = driver_with_sessions();
2634 d.take_notifications();
2635
2636 let mut descriptors = Vec::new();
2637 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2638 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2639 let cat_bytes = build_cat_fixture(&descriptors);
2640 let cat = CatSection::parse(&cat_bytes).unwrap();
2641
2642 d.set_cat(&cat).unwrap();
2645 assert!(
2646 d.emm_pids().is_empty(),
2647 "emm_pids must be empty before any ca_info arrives, got {:?}",
2648 d.emm_pids()
2649 );
2650
2651 feed(
2654 &mut d,
2655 r_apdu(
2656 CA_SESSION,
2657 &ser(&CaInfo {
2658 length_form: dvb_ci::length::LengthForm::Minimal,
2659 ca_system_ids: vec![0x0648, 0x0100],
2660 }),
2661 ),
2662 );
2663 d.take_notifications();
2664
2665 assert_eq!(
2666 d.emm_pids(),
2667 &[Pid::new(0x1FF0)],
2668 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
2669 );
2670 }
2671
2672 #[test]
2678 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
2679 use broadcast_common::Parse;
2680 use dvb_ci::objects::ca_info::CaInfo;
2681 use dvb_si::tables::cat::CatSection;
2682
2683 let mut d = driver_with_sessions();
2684 d.take_notifications();
2685
2686 feed(
2688 &mut d,
2689 r_apdu(
2690 CA_SESSION,
2691 &ser(&CaInfo {
2692 length_form: dvb_ci::length::LengthForm::Minimal,
2693 ca_system_ids: vec![0x0648, 0x0100],
2694 }),
2695 ),
2696 );
2697 d.take_notifications();
2698
2699 let mut descriptors = Vec::new();
2701 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2702 descriptors.extend_from_slice(&ca_descriptor(0x0100, Pid::new(0x1FF0)));
2703 let cat_bytes = build_cat_fixture(&descriptors);
2704 let cat = CatSection::parse(&cat_bytes).unwrap();
2705
2706 d.set_cat(&cat).unwrap();
2707
2708 assert_eq!(
2709 d.emm_pids(),
2710 &[Pid::new(0x1FF0)],
2711 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
2712 d.emm_pids()
2713 );
2714 }
2715
2716 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
2720 const VIACCESS: u16 = 0x0500;
2721 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0074));
2722 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0075));
2723
2724 let mut body = Vec::new();
2725 body.push(0x02); body.push(0);
2727 body.push(0);
2728 body.extend_from_slice(&program_number.to_be_bytes());
2729 body.push(0xC3);
2730 body.push(0x00);
2731 body.push(0x00);
2732 body.push(0xE0 | 0x02); body.push(0x00);
2734 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2735 body.push(prog_ca.len() as u8);
2736 body.extend_from_slice(&prog_ca);
2737 body.push(0x1B);
2739 body.push(0xE0 | 0x02);
2740 body.push(0x00);
2741 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2742 body.push(es0_ca.len() as u8);
2743 body.extend_from_slice(&es0_ca);
2744 body.push(0x0F);
2746 body.push(0xE0 | 0x02);
2747 body.push(0x01);
2748 body.push(0xF0);
2749 body.push(0x00);
2750
2751 let section_length = body.len() - 3 + 4;
2752 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2753 body[2] = section_length as u8;
2754 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2755 body.extend_from_slice(&crc.to_be_bytes());
2756 body
2757 }
2758
2759 #[test]
2760 fn descramble_pids_is_the_union_of_active_services_es_pids() {
2761 use broadcast_common::Parse;
2762
2763 let mut d = driver_with_sessions();
2764 d.take_notifications();
2765
2766 assert!(
2767 d.descramble_pids().is_empty(),
2768 "no service added yet: descramble_pids must be empty"
2769 );
2770
2771 let pmt1_bytes = build_ca_pmt_fixture(1546);
2772 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2773 d.add_service(&pmt1).unwrap();
2774 d.device_mut().inbound.push_back(sb());
2775 d.pump(Duration::from_millis(10)).unwrap();
2776
2777 assert_eq!(d.descramble_pids(), &[Pid::new(0x0100), Pid::new(0x0101)]);
2778
2779 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2780 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2781 d.add_service(&pmt2).unwrap();
2782 d.device_mut().inbound.push_back(sb());
2783 d.pump(Duration::from_millis(10)).unwrap();
2784
2785 assert_eq!(
2787 d.descramble_pids(),
2788 &[
2789 Pid::new(0x0100),
2790 Pid::new(0x0101),
2791 Pid::new(0x0200),
2792 Pid::new(0x0201)
2793 ],
2794 "descramble_pids must be the union across both added services"
2795 );
2796 }
2797
2798 pub(crate) fn ca_pmt_reply_for(
2803 program_number: u16,
2804 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
2805 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2806 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2807 length_form: dvb_ci::length::LengthForm::Minimal,
2808 program_number,
2809 version_number: 1,
2810 current_next_indicator: true,
2811 ca_enable,
2812 streams: vec![],
2813 reserved_version: Default::default(),
2814 reserved_ca_enable: Default::default(),
2815 }
2816 }
2817
2818 #[test]
2819 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
2820 use broadcast_common::Parse;
2821 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2822
2823 let clock = TestClock::new();
2824 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2825 d.set_requery_interval(Duration::from_secs(10));
2828 d.take_notifications();
2829
2830 let pmt_bytes = build_ca_pmt_fixture(1546);
2831 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2832 d.add_service(&pmt).unwrap();
2833 d.device_mut().inbound.push_back(sb());
2834 clock.advance(Duration::from_millis(10));
2835 d.pump(Duration::from_millis(10)).unwrap();
2836 d.take_notifications();
2837
2838 let expected_initial_ca_pmt =
2842 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2843 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
2844
2845 let expected_query =
2851 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2852 let expected_ok_descrambling = build_ca_pmt(
2853 &pmt,
2854 CaPmtListManagement::Update,
2855 CaPmtCmdId::OkDescrambling,
2856 )
2857 .to_bytes();
2858 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2859 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2860
2861 let mut all_notes = Vec::new();
2862
2863 feed(
2867 &mut d,
2868 r_apdu(
2869 CA_SESSION,
2870 &ser(&ca_pmt_reply_for(
2871 1546,
2872 Some(CaEnable::NotPossibleNoEntitlement),
2873 )),
2874 ),
2875 );
2876 all_notes.extend(d.take_notifications());
2877
2878 clock.advance(Duration::from_secs(11));
2888 d.pump(Duration::from_secs(11)).unwrap();
2889 all_notes.extend(d.take_notifications());
2890 feed(&mut d, sb());
2891
2892 assert_eq!(
2893 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2894 query_sends_before + 1,
2895 "expected the re-query timer to resend the `query` half exactly once"
2896 );
2897 assert_eq!(
2898 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2899 ok_sends_before + 1,
2900 "expected the re-query timer to resend the `ok_descrambling` half exactly once"
2901 );
2902
2903 feed(
2906 &mut d,
2907 r_apdu(
2908 CA_SESSION,
2909 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
2910 ),
2911 );
2912 all_notes.extend(d.take_notifications());
2913
2914 let hits = all_notes
2915 .iter()
2916 .filter(|n| {
2917 matches!(
2918 n,
2919 Notification::Entitlement {
2920 program_number: 1546,
2921 ca_enable: CaEnable::Possible,
2922 descrambling_ok: true,
2923 }
2924 )
2925 })
2926 .count();
2927 assert_eq!(
2928 hits, 1,
2929 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
2930 );
2931 }
2932
2933 #[test]
2934 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
2935 use broadcast_common::Parse;
2936 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2937
2938 let mut d = driver_with_sessions();
2939 d.set_requery_interval(Duration::from_secs(10));
2942 d.take_notifications();
2943
2944 let pmt_bytes = build_ca_pmt_fixture(1547);
2945 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2946 d.add_service(&pmt).unwrap();
2947 d.device_mut().inbound.push_back(sb());
2948 d.pump(Duration::from_millis(10)).unwrap();
2949 d.take_notifications();
2950
2951 feed(
2955 &mut d,
2956 r_apdu(
2957 CA_SESSION,
2958 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2959 ),
2960 );
2961 d.take_notifications();
2962
2963 for _ in 0..2 {
2966 d.pump(Duration::from_secs(11)).unwrap();
2967 d.take_notifications();
2968 feed(
2969 &mut d,
2970 r_apdu(
2971 CA_SESSION,
2972 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2973 ),
2974 );
2975 let notes = d.take_notifications();
2976 assert!(
2977 !notes
2978 .iter()
2979 .any(|n| matches!(n, Notification::Entitlement { .. })),
2980 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
2981 );
2982 }
2983 }
2984
2985 #[test]
2986 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
2987 use broadcast_common::Parse;
2988 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2989
2990 let mut d = driver_with_sessions();
2991 d.take_notifications();
2992
2993 let pmt_bytes = build_ca_pmt_fixture(1548);
2994 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2995 d.add_service(&pmt).unwrap();
2996 d.device_mut().inbound.push_back(sb());
2997 d.pump(Duration::from_millis(10)).unwrap();
2998 d.take_notifications();
2999
3000 feed(
3002 &mut d,
3003 r_apdu(
3004 CA_SESSION,
3005 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
3006 ),
3007 );
3008 d.take_notifications();
3009
3010 feed(
3014 &mut d,
3015 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
3016 );
3017 let notes = d.take_notifications();
3018 assert!(
3019 !notes
3020 .iter()
3021 .any(|n| matches!(n, Notification::Entitlement { .. })),
3022 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
3023 );
3024 }
3025
3026 #[test]
3027 fn requery_disabled_by_default_sends_nothing() {
3028 use broadcast_common::Parse;
3029
3030 let mut d = driver_with_sessions();
3034 d.take_notifications();
3035
3036 let pmt_bytes = build_ca_pmt_fixture(1549);
3037 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3038 d.add_service(&pmt).unwrap();
3039 d.device_mut().inbound.push_back(sb());
3040 d.pump(Duration::from_millis(10)).unwrap();
3041
3042 let expected_query =
3045 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3046 let expected_ok_descrambling = build_ca_pmt(
3047 &pmt,
3048 CaPmtListManagement::Update,
3049 CaPmtCmdId::OkDescrambling,
3050 )
3051 .to_bytes();
3052 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
3053 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
3054
3055 d.pump(Duration::from_secs(1_000_000)).unwrap();
3057 feed(&mut d, sb());
3058 feed(&mut d, sb());
3059 assert_eq!(
3060 count_apdu_on_session(&d, CA_SESSION, &expected_query),
3061 query_sends_before,
3062 "the default (Duration::ZERO) must never send a re-query `query`"
3063 );
3064 assert_eq!(
3065 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
3066 ok_sends_before,
3067 "the default (Duration::ZERO) must never send a re-query `ok_descrambling`"
3068 );
3069 }
3070
3071 #[test]
3072 fn set_requery_interval_zero_disables_resend_after_being_enabled() {
3073 use broadcast_common::Parse;
3074
3075 let mut d = driver_with_sessions();
3076 d.set_requery_interval(Duration::from_secs(5));
3079 d.set_requery_interval(Duration::ZERO);
3080 d.take_notifications();
3081
3082 let pmt_bytes = build_ca_pmt_fixture(1549);
3083 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3084 d.add_service(&pmt).unwrap();
3085 d.device_mut().inbound.push_back(sb());
3086 d.pump(Duration::from_millis(10)).unwrap();
3087
3088 let expected_ca_pmt = build_ca_pmt(
3092 &pmt,
3093 CaPmtListManagement::Update,
3094 CaPmtCmdId::OkDescrambling,
3095 )
3096 .to_bytes();
3097 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3098
3099 d.pump(Duration::from_secs(1000)).unwrap();
3101 feed(&mut d, sb());
3102 feed(&mut d, sb());
3103
3104 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3105 assert_eq!(
3106 sends_after, sends_before,
3107 "Duration::ZERO must disable the re-query resend"
3108 );
3109 }
3110
3111 #[test]
3112 fn requery_timer_resends_every_active_service_not_just_one() {
3113 use broadcast_common::Parse;
3114
3115 let clock = TestClock::new();
3116 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3117 d.set_requery_interval(Duration::from_secs(10));
3120 d.take_notifications();
3121
3122 let pmt1_bytes = build_ca_pmt_fixture(1546);
3125 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3126 d.add_service(&pmt1).unwrap();
3127 d.device_mut().inbound.push_back(sb());
3128 clock.advance(Duration::from_millis(10));
3129 d.pump(Duration::from_millis(10)).unwrap();
3130
3131 let pmt2_bytes = build_ca_pmt_fixture(1547);
3132 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3133 d.add_service(&pmt2).unwrap();
3134 d.device_mut().inbound.push_back(sb());
3135 clock.advance(Duration::from_millis(10));
3136 d.pump(Duration::from_millis(10)).unwrap();
3137 d.take_notifications();
3138
3139 let expected1_query =
3145 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3146 let expected1_ok = build_ca_pmt(
3147 &pmt1,
3148 CaPmtListManagement::Update,
3149 CaPmtCmdId::OkDescrambling,
3150 )
3151 .to_bytes();
3152 let expected2_query =
3153 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3154 let expected2_ok = build_ca_pmt(
3155 &pmt2,
3156 CaPmtListManagement::Update,
3157 CaPmtCmdId::OkDescrambling,
3158 )
3159 .to_bytes();
3160 let sends_before1_query = count_apdu_on_session(&d, CA_SESSION, &expected1_query);
3161 let sends_before1_ok = count_apdu_on_session(&d, CA_SESSION, &expected1_ok);
3162 let sends_before2_query = count_apdu_on_session(&d, CA_SESSION, &expected2_query);
3163 let sends_before2_ok = count_apdu_on_session(&d, CA_SESSION, &expected2_ok);
3164
3165 clock.advance(Duration::from_secs(11));
3171 d.pump(Duration::from_secs(11)).unwrap();
3172 feed(&mut d, sb());
3173 feed(&mut d, sb());
3174
3175 assert_eq!(
3176 count_apdu_on_session(&d, CA_SESSION, &expected1_query),
3177 sends_before1_query + 1,
3178 "expected service 1546's `query` resent exactly once on the shared tick"
3179 );
3180 assert_eq!(
3181 count_apdu_on_session(&d, CA_SESSION, &expected1_ok),
3182 sends_before1_ok + 1,
3183 "expected service 1546's `ok_descrambling` resent exactly once on the shared tick"
3184 );
3185 assert_eq!(
3186 count_apdu_on_session(&d, CA_SESSION, &expected2_query),
3187 sends_before2_query + 1,
3188 "expected service 1547's `query` resent exactly once on the shared tick"
3189 );
3190 assert_eq!(
3191 count_apdu_on_session(&d, CA_SESSION, &expected2_ok),
3192 sends_before2_ok + 1,
3193 "expected service 1547's `ok_descrambling` resent exactly once on the shared tick"
3194 );
3195 }
3196
3197 #[test]
3201 fn requery_after_remove_resends_only_the_surviving_service() {
3202 use broadcast_common::Parse;
3203
3204 let clock = TestClock::new();
3205 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3206 d.set_requery_interval(Duration::from_secs(10));
3209 d.take_notifications();
3210
3211 let pmt1_bytes = build_ca_pmt_fixture(1546);
3212 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3213 d.add_service(&pmt1).unwrap();
3214 d.device_mut().inbound.push_back(sb());
3215 clock.advance(Duration::from_millis(10));
3216 d.pump(Duration::from_millis(10)).unwrap();
3217
3218 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3219 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3220 d.add_service(&pmt2).unwrap();
3221 d.device_mut().inbound.push_back(sb());
3222 clock.advance(Duration::from_millis(10));
3223 d.pump(Duration::from_millis(10)).unwrap();
3224
3225 d.remove_service(1546).unwrap();
3227 d.device_mut().inbound.push_back(sb());
3228 clock.advance(Duration::from_millis(10));
3229 d.pump(Duration::from_millis(10)).unwrap();
3230 d.take_notifications();
3231
3232 let expected_survivor_query =
3238 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3239 let expected_survivor_ok = build_ca_pmt(
3240 &pmt2,
3241 CaPmtListManagement::Update,
3242 CaPmtCmdId::OkDescrambling,
3243 )
3244 .to_bytes();
3245 let expected_removed_query =
3246 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3247 let expected_removed_ok = build_ca_pmt(
3248 &pmt1,
3249 CaPmtListManagement::Update,
3250 CaPmtCmdId::OkDescrambling,
3251 )
3252 .to_bytes();
3253 let sends_survivor_query_before =
3254 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query);
3255 let sends_survivor_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok);
3256 let sends_removed_query_before =
3257 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query);
3258 let sends_removed_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok);
3259
3260 clock.advance(Duration::from_secs(11));
3261 d.pump(Duration::from_secs(11)).unwrap();
3262 feed(&mut d, sb());
3263
3264 assert_eq!(
3265 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query),
3266 sends_survivor_query_before + 1,
3267 "the surviving service's `query` must be re-sent"
3268 );
3269 assert_eq!(
3270 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok),
3271 sends_survivor_ok_before + 1,
3272 "the surviving service's `ok_descrambling` must be re-sent"
3273 );
3274 assert_eq!(
3275 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query),
3276 sends_removed_query_before,
3277 "the removed service's `query` must NOT be re-sent"
3278 );
3279 assert_eq!(
3280 count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok),
3281 sends_removed_ok_before,
3282 "the removed service's `ok_descrambling` must NOT be re-sent"
3283 );
3284 }
3285
3286 #[test]
3289 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
3290 use broadcast_common::Parse;
3291
3292 let mut d = driver_with_sessions();
3293 d.take_notifications();
3294
3295 let pmt1_bytes = build_ca_pmt_fixture(1546);
3299 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3300 d.add_service(&pmt1).unwrap();
3301 d.device_mut().inbound.push_back(sb());
3302 d.pump(Duration::from_millis(10)).unwrap();
3303
3304 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3305 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3306 d.add_service(&pmt2).unwrap();
3307 d.device_mut().inbound.push_back(sb());
3308 d.pump(Duration::from_millis(10)).unwrap();
3309
3310 d.remove_service(1546).unwrap();
3311 d.device_mut().inbound.push_back(sb());
3312 d.pump(Duration::from_millis(10)).unwrap();
3313
3314 let expected =
3318 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3319 assert_apdu_on_session(&d, CA_SESSION, &expected);
3320
3321 assert_eq!(
3322 d.descramble_pids(),
3323 &[Pid::new(0x0200), Pid::new(0x0201)],
3324 "1546's ES PIDs must be gone; 1547's must remain"
3325 );
3326 assert!(
3327 d.managed_ca().services().get(&1546).is_none(),
3328 "1546 must no longer be tracked"
3329 );
3330 assert!(
3331 d.managed_ca().services().get(&1547).is_some(),
3332 "1547 must remain tracked"
3333 );
3334 }
3335
3336 #[test]
3337 fn remove_service_of_untracked_program_is_a_no_op() {
3338 let mut d = driver_with_sessions();
3339 d.take_notifications();
3340
3341 let ops_before = d.device().ops.len();
3342 d.remove_service(0xFFFF).unwrap();
3343 assert_eq!(
3344 d.device().ops.len(),
3345 ops_before,
3346 "removing an untracked program must not send anything to the device"
3347 );
3348 assert!(
3349 d.managed_ca().services().is_empty(),
3350 "removing an untracked program must not disturb the (empty) managed set"
3351 );
3352 }
3353
3354 #[test]
3355 fn cam_removed_edge_clears_managed_state() {
3356 use broadcast_common::Parse;
3357 use dvb_ci::objects::ca_info::CaInfo;
3358 use dvb_si::tables::cat::CatSection;
3359
3360 let mut d = driver_with_sessions();
3361 d.take_notifications();
3362
3363 let pmt_bytes = build_ca_pmt_fixture(1546);
3364 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3365 d.add_service(&pmt).unwrap();
3366 d.device_mut().inbound.push_back(sb());
3367 d.pump(Duration::from_millis(10)).unwrap();
3368
3369 feed(
3372 &mut d,
3373 r_apdu(
3374 CA_SESSION,
3375 &ser(&CaInfo {
3376 length_form: dvb_ci::length::LengthForm::Minimal,
3377 ca_system_ids: vec![0x0648],
3378 }),
3379 ),
3380 );
3381 d.take_notifications();
3382 let mut descriptors = Vec::new();
3383 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
3384 let cat_bytes = build_cat_fixture(&descriptors);
3385 let cat = CatSection::parse(&cat_bytes).unwrap();
3386 d.set_cat(&cat).unwrap();
3387
3388 assert!(
3389 !d.managed_ca().services().is_empty(),
3390 "precondition: a service is tracked"
3391 );
3392 assert!(
3393 !d.descramble_pids().is_empty(),
3394 "precondition: descramble_pids populated"
3395 );
3396 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
3397
3398 d.device_mut().slot.module_present = false;
3400 d.pump(Duration::from_millis(10)).unwrap();
3401 let notes = d.take_notifications();
3402 assert!(
3403 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
3404 "expected CamRemoved, got {notes:?}"
3405 );
3406
3407 assert!(
3408 d.managed_ca().services().is_empty(),
3409 "services must be cleared on CamRemoved"
3410 );
3411 assert!(
3412 d.descramble_pids().is_empty(),
3413 "descramble_pids must be cleared on CamRemoved"
3414 );
3415 assert!(
3416 d.emm_pids().is_empty(),
3417 "emm_pids must be cleared on CamRemoved"
3418 );
3419 }
3420
3421 #[test]
3422 fn update_service_not_managed_returns_not_managed_and_sends_nothing() {
3423 use broadcast_common::Parse;
3424
3425 let mut d = driver_with_sessions();
3426 d.take_notifications();
3427
3428 let pmt_bytes = build_clear_pmt_fixture(9999);
3429 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3430
3431 let before = d.device().ops.len();
3432 let err = d
3433 .update_service(&pmt)
3434 .expect_err("unknown program must error");
3435 assert!(
3436 matches!(
3437 err,
3438 CaError::NotManaged {
3439 program_number: 9999
3440 }
3441 ),
3442 "got {err:?}"
3443 );
3444 assert_eq!(d.device().ops.len(), before, "must send nothing");
3445 assert!(d.managed_ca().services().is_empty());
3446 }
3447
3448 #[test]
3449 fn update_service_no_ca_descriptor_leaves_record_unchanged() {
3450 use broadcast_common::Parse;
3451
3452 let mut d = driver_with_sessions();
3453 d.take_notifications();
3454
3455 let pmt_bytes = build_ca_pmt_fixture(1546);
3456 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3457 d.add_service(&pmt).unwrap();
3458
3459 let svc_before = d.managed_ca().services().get(&1546).unwrap().clone();
3460
3461 let clear_bytes = build_clear_pmt_fixture(1546);
3462 let clear_pmt = PmtSection::parse(&clear_bytes).unwrap();
3463 let err = d
3464 .update_service(&clear_pmt)
3465 .expect_err("CA-free PMT must error");
3466 assert!(
3467 matches!(
3468 err,
3469 CaError::NoCaDescriptor {
3470 program_number: 1546
3471 }
3472 ),
3473 "got {err:?}"
3474 );
3475
3476 let svc_after = d.managed_ca().services().get(&1546).unwrap();
3477 assert_eq!(svc_before, *svc_after, "record must be unchanged");
3478 }
3479
3480 #[test]
3486 fn update_service_sends_one_update_ca_pmt_with_the_new_pids_and_replaces_only_that_record() {
3487 use broadcast_common::Parse;
3488
3489 let mut d = driver_with_sessions();
3490 d.take_notifications();
3491
3492 let a_bytes = build_ca_pmt_fixture(1546);
3493 let a = PmtSection::parse(&a_bytes).unwrap();
3494 d.add_service(&a).unwrap();
3495 d.device_mut().inbound.push_back(sb());
3496 d.pump(Duration::from_millis(10)).unwrap();
3497
3498 let b_bytes = build_ca_pmt_fixture_dedicated_pcr(1547);
3499 let b = PmtSection::parse(&b_bytes).unwrap();
3500 d.add_service(&b).unwrap();
3501 d.device_mut().inbound.push_back(sb());
3502 d.pump(Duration::from_millis(10)).unwrap();
3503
3504 let a_old_record = d.managed_ca().services().get(&1546).unwrap().clone();
3505 let b_record_before = d.managed_ca().services().get(&1547).unwrap().clone();
3506 assert_eq!(
3507 a_old_record.ca_pids,
3508 vec![Pid::new(0x0064), Pid::new(0x0065)],
3509 "fixture sanity: A starts on the 0x0064/0x0065 ECM PIDs"
3510 );
3511
3512 let a2_bytes = build_ca_pmt_fixture_distinct_pids(1546);
3514 let a2 = PmtSection::parse(&a2_bytes).unwrap();
3515 let update =
3516 build_ca_pmt(&a2, CaPmtListManagement::Update, CaPmtCmdId::OkDescrambling).to_bytes();
3517 let writes_before = d.device().ops.len();
3518
3519 d.update_service(&a2).unwrap();
3520 d.device_mut().inbound.push_back(sb());
3521 d.pump(Duration::from_millis(10)).unwrap();
3522
3523 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &update), 1);
3525 let new_ca_pmt_writes = d.device().ops[writes_before..]
3526 .iter()
3527 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == [0x9F, 0x80, 0x32])))
3528 .count();
3529 assert_eq!(new_ca_pmt_writes, 1, "exactly one ca_pmt APDU may be sent");
3530
3531 let new_prog_ca = ca_descriptor(0x0500, Pid::new(0x0074));
3533 let old_prog_ca = ca_descriptor(0x0500, Pid::new(0x0064));
3534 assert!(update.windows(new_prog_ca.len()).any(|x| x == new_prog_ca));
3535 assert!(!update.windows(old_prog_ca.len()).any(|x| x == old_prog_ca));
3536
3537 let a_new_record = d.managed_ca().services().get(&1546).unwrap();
3539 assert_eq!(
3540 a_new_record.ca_pids,
3541 vec![Pid::new(0x0074), Pid::new(0x0075)]
3542 );
3543 assert_eq!(a_new_record.built_ca_pmt, update);
3544 assert_ne!(a_new_record.built_ca_pmt, a_old_record.built_ca_pmt);
3545 assert_ne!(a_new_record.pmt_raw, a_old_record.pmt_raw);
3546 assert_eq!(
3547 a_new_record.pmt_raw, a2_bytes,
3548 "raw PMT must be the new section"
3549 );
3550
3551 assert_eq!(
3553 *d.managed_ca().services().get(&1547).unwrap(),
3554 b_record_before
3555 );
3556 assert_eq!(d.managed_ca().services().len(), 2);
3557 }
3558
3559 #[test]
3562 fn remove_service_after_update_service_uses_the_new_pmt() {
3563 use broadcast_common::Parse;
3564
3565 let mut d = driver_with_sessions();
3566 d.take_notifications();
3567
3568 let a_bytes = build_ca_pmt_fixture(1546);
3569 let a = PmtSection::parse(&a_bytes).unwrap();
3570 d.add_service(&a).unwrap();
3571 d.device_mut().inbound.push_back(sb());
3572 d.pump(Duration::from_millis(10)).unwrap();
3573 let a2_bytes = build_ca_pmt_fixture_distinct_pids(1546);
3574 let a2 = PmtSection::parse(&a2_bytes).unwrap();
3575 d.update_service(&a2).unwrap();
3576 d.device_mut().inbound.push_back(sb());
3577 d.pump(Duration::from_millis(10)).unwrap();
3578
3579 d.remove_service(1546).unwrap();
3580 d.device_mut().inbound.push_back(sb());
3581 d.pump(Duration::from_millis(10)).unwrap();
3582
3583 let removal_new =
3584 build_ca_pmt(&a2, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3585 let removal_old =
3586 build_ca_pmt(&a, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3587 assert_ne!(
3588 removal_new, removal_old,
3589 "fixture sanity: the two PMTs differ"
3590 );
3591 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &removal_new), 1);
3592 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &removal_old), 0);
3593 assert!(d.managed_ca().services().is_empty());
3594 }
3595
3596 #[test]
3599 fn update_service_filters_caids_like_add_service() {
3600 use broadcast_common::Parse;
3601
3602 let mut d = driver_with_sessions();
3603 d.take_notifications();
3604
3605 let two_caids = build_ca_pmt_fixture_two_caids(1546);
3606 let pmt = PmtSection::parse(&two_caids).unwrap();
3607 d.add_service(&pmt).unwrap();
3608 let added = d
3609 .managed_ca()
3610 .services()
3611 .get(&1546)
3612 .unwrap()
3613 .built_ca_pmt
3614 .clone();
3615
3616 d.update_service(&pmt).unwrap();
3617 let updated = d
3618 .managed_ca()
3619 .services()
3620 .get(&1546)
3621 .unwrap()
3622 .built_ca_pmt
3623 .clone();
3624
3625 assert_eq!(added.len(), updated.len());
3627 let diff: Vec<usize> = (0..added.len())
3628 .filter(|&i| added[i] != updated[i])
3629 .collect();
3630 assert_eq!(
3631 diff.len(),
3632 1,
3633 "only list_management may differ, got offsets {diff:?}"
3634 );
3635 assert_eq!(updated[diff[0]], 0x05, "list_management must be update");
3636 }
3637
3638 #[test]
3639 fn update_service_serialize_failure_leaves_record_unchanged() {
3640 let mut d = driver_with_sessions();
3641 let pmt_bytes = build_ca_pmt_fixture(1);
3643 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3644 d.add_service(&pmt).unwrap();
3645
3646 let svc_before = d.managed_ca().services().get(&1).unwrap().clone();
3647
3648 let corrupt = corrupt_ca_section_ctor();
3649 let err = d
3650 .update_service(&corrupt)
3651 .expect_err("unencodable PMT must error");
3652 assert!(
3653 matches!(err, CaError::Serialize(_)),
3654 "expected CaError::Serialize, got {err:?}"
3655 );
3656
3657 let svc_after = d.managed_ca().services().get(&1).unwrap();
3658 assert_eq!(svc_before, *svc_after, "record must be unchanged");
3659 }
3660}