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 fn send_ca_pmt_checked(&mut self, ca_pmt: &[u8]) -> Result<(), CaError> {
195 let before = self.notifications.len();
196 self.send_ca_pmt(ca_pmt)?;
197 let refusal = self.notifications[before..].iter().find_map(|n| match n {
198 Notification::Error { detail } => Some(detail.clone()),
199 _ => None,
200 });
201 refusal.map_or(Ok(()), |detail| Err(CaError::NotSent(detail)))
202 }
203
204 pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
210 let actions = self
211 .stack
212 .handle(Event::Host(HostRequest::Descramble(pmt_section)));
213 self.run(actions)
214 }
215
216 pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
219 let actions = self
220 .stack
221 .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
222 self.run(actions)
223 }
224
225 pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
228 let actions = self
229 .stack
230 .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
231 self.run(actions)
232 }
233
234 pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
237 let actions = self
238 .stack
239 .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
240 self.run(actions)
241 }
242
243 fn build_ca_pmt_filtered(
255 &self,
256 pmt: &PmtSection<'_>,
257 list_management: CaPmtListManagement,
258 cmd_id: CaPmtCmdId,
259 ) -> dvb_ci::builder::CaPmtBuilt {
260 let cam_caids = self.managed.cam_caids();
261 if cam_caids.is_empty() {
262 build_ca_pmt(pmt, list_management, cmd_id)
263 } else {
264 let allowed: Vec<u16> = cam_caids.iter().copied().collect();
265 build_ca_pmt_for_caids(pmt, &allowed, list_management, cmd_id)
266 }
267 }
268
269 fn build_service_bytes(
278 &self,
279 pmt: &PmtSection<'_>,
280 list_management: CaPmtListManagement,
281 cmd_id: CaPmtCmdId,
282 ) -> Result<(Vec<u8>, Vec<u8>), CaError> {
283 if !managed::pmt_has_ca(pmt) {
284 return Err(CaError::NoCaDescriptor {
285 program_number: pmt.program_number,
286 });
287 }
288 let built_bytes = self
290 .build_ca_pmt_filtered(pmt, list_management, cmd_id)
291 .try_to_bytes()?;
292 let mut pmt_raw = vec![0u8; pmt.serialized_len()];
293 let n = pmt.serialize_into(&mut pmt_raw)?;
294 pmt_raw.truncate(n);
295 Ok((built_bytes, pmt_raw))
296 }
297
298 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
320 let list_management = if self.managed.is_empty() {
321 CaPmtListManagement::Only
322 } else {
323 CaPmtListManagement::Add
324 };
325 let cmd_id = CaPmtCmdId::OkDescrambling;
326 let (built_bytes, pmt_raw) = self.build_service_bytes(pmt, list_management, cmd_id)?;
327 self.send_ca_pmt_checked(&built_bytes)?;
328 self.managed.record(
329 pmt.program_number,
330 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
331 );
332 Ok(())
333 }
334
335 pub fn update_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
360 if !self.managed.services().contains_key(&pmt.program_number) {
361 return Err(CaError::NotManaged {
362 program_number: pmt.program_number,
363 });
364 }
365 let list_management = CaPmtListManagement::Update;
366 let cmd_id = CaPmtCmdId::OkDescrambling;
367 let (built_bytes, pmt_raw) = self.build_service_bytes(pmt, list_management, cmd_id)?;
368 self.send_ca_pmt_checked(&built_bytes)?;
369 self.managed.record(
370 pmt.program_number,
371 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
372 );
373 Ok(())
374 }
375
376 pub fn remove_service(&mut self, program_number: u16) -> Result<(), CaError> {
390 let raw = self
391 .managed
392 .services()
393 .get(&program_number)
394 .map(|s| s.pmt_raw.clone());
395 let Some(raw) = raw else {
396 return Ok(());
397 };
398 self.remove_program(&raw)?;
399 self.managed.remove(program_number);
400 Ok(())
401 }
402
403 pub fn set_requery_interval(&mut self, interval: Duration) {
412 self.managed.set_requery_interval(interval);
413 }
414
415 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
430 let entries = cat.ca_descriptors().map_err(CaError::Cat)?;
431 self.managed.set_cat(&entries);
432 Ok(())
433 }
434
435 #[must_use]
439 pub fn emm_pids(&self) -> &[Pid] {
440 self.managed.emm_pids()
441 }
442
443 #[must_use]
446 pub fn descramble_pids(&self) -> &[Pid] {
447 self.managed.descramble_pids()
448 }
449
450 #[must_use]
455 pub fn ca_pids(&self) -> &[Pid] {
456 self.managed.ca_pids()
457 }
458
459 #[must_use]
467 pub fn required_pids(&self) -> Vec<Pid> {
468 self.managed.required_pids()
469 }
470
471 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
473 let actions = self
474 .stack
475 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
476 self.run(actions)
477 }
478
479 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
481 let actions = self
482 .stack
483 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
484 self.run(actions)
485 }
486
487 pub fn mmi_cancel(&mut self) -> io::Result<()> {
489 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
490 self.run(actions)
491 }
492
493 pub fn enter_menu(&mut self) -> io::Result<()> {
496 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
497 self.run(actions)
498 }
499
500 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
509 let now = (self.clock)();
520 let measured = self.last_pump.is_some();
521 let mut elapsed = self
522 .last_pump
523 .map_or(timeout, |prev| now.saturating_duration_since(prev));
524 self.last_pump = Some(now);
525
526 if let Some(until) = self.settle_until {
531 if now < until {
532 self.run(vec![Action::QuerySlot])?;
533 return Ok(false);
534 }
535 self.settle_until = None;
536 let mut pending = std::mem::take(&mut self.deferred_writes).into_iter();
537 while let Some(w) = pending.next() {
538 match self.device.write(&w) {
539 Ok(()) => {}
540 Err(e)
545 if e.kind() == io::ErrorKind::InvalidInput
546 && self.slot_ready_deadline.is_some_and(|d| now < d) =>
547 {
548 self.deferred_writes = std::iter::once(w).chain(pending).collect();
549 self.settle_until = Some(now + SLOT_RETRY);
550 return Ok(false);
551 }
552 Err(e) => return Err(e),
553 }
554 }
555 self.slot_ready_deadline = None;
556 elapsed = Duration::ZERO;
559 }
560
561 self.run(vec![Action::QuerySlot])?;
562 if self.device.poll(timeout)? {
563 let n = self.device.read(&mut self.buf)?;
564 if n > 0 {
565 let actions = self.stack.handle(Event::Readable(&self.buf[..n]));
569 self.run(actions)?;
570 if measured && !elapsed.is_zero() {
571 let actions = self.stack.handle(Event::Tick { elapsed });
572 self.run(actions)?;
573 self.requery_tick(elapsed)?;
574 }
575 return Ok(true);
576 }
577 }
578 let actions = self.stack.handle(Event::Tick { elapsed });
579 self.run(actions)?;
580 self.requery_tick(elapsed)?;
581 Ok(false)
582 }
583
584 fn requery_tick(&mut self, elapsed: Duration) -> io::Result<()> {
628 if let Some(err) = self.requery_pending.take() {
632 return Err(io::Error::other(err));
633 }
634 if !self.managed.tick(elapsed) {
635 return Ok(());
636 }
637 let mut ca_pmts: Vec<(Vec<u8>, Vec<u8>)> =
638 Vec::with_capacity(self.managed.services().len());
639 for s in self.managed.services().values() {
640 let outcome: Result<(Vec<u8>, Vec<u8>), CaError> = (|| {
645 let pmt = PmtSection::parse(&s.pmt_raw).map_err(CaError::PmtParse)?;
646 let query = self
647 .build_ca_pmt_filtered(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query)
648 .try_to_bytes()?;
649 let ok_descrambling = self
650 .build_ca_pmt_filtered(
651 &pmt,
652 CaPmtListManagement::Update,
653 CaPmtCmdId::OkDescrambling,
654 )
655 .try_to_bytes()?;
656 Ok((query, ok_descrambling))
657 })();
658 match outcome {
659 Ok(pair) => ca_pmts.push(pair),
660 Err(err) => {
661 self.requery_pending = Some(err);
665 }
666 }
667 }
668 for (query, ok_descrambling) in ca_pmts {
669 self.send_ca_pmt(&query)?;
670 self.send_ca_pmt(&ok_descrambling)?;
671 }
672 if let Some(err) = self.requery_pending.take() {
673 return Err(io::Error::other(err));
674 }
675 Ok(())
676 }
677
678 pub fn pump_with<F: FnMut(&Notification)>(
687 &mut self,
688 timeout: Duration,
689 mut handler: F,
690 ) -> io::Result<bool> {
691 let progressed = self.pump(timeout)?;
692 for n in self.take_notifications() {
693 handler(&n);
694 }
695 Ok(progressed)
696 }
697
698 pub fn pump_hotplug<F: FnMut(HotPlug)>(
702 &mut self,
703 timeout: Duration,
704 mut handler: F,
705 ) -> io::Result<bool> {
706 self.pump_with(timeout, |n| {
707 if let Some(h) = n.hotplug() {
708 handler(h);
709 }
710 })
711 }
712
713 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
715 for action in actions {
716 match action {
717 Action::Write(bytes) => {
718 if self.settle_until.is_some() {
719 self.deferred_writes.push(bytes);
720 } else {
721 self.device.write(&bytes)?;
722 }
723 }
724 Action::Reset => {
725 self.device.reset()?;
726 self.deferred_writes.clear();
729 let settle = self.device.reset_settle();
730 let now = (self.clock)();
731 self.settle_until = (!settle.is_zero()).then(|| now + settle);
732 self.slot_ready_deadline =
733 (!settle.is_zero()).then(|| now + settle + SLOT_READY_GRACE);
734 }
735 Action::QuerySlot => {
736 let info = self.device.slot_info()?;
737 self.handle_slot_info(info)?;
738 }
739 Action::SetTimer { after } => self.next_timer = Some(after),
740 Action::Notify(n) => {
741 let inferred = self.infer_card(&n);
742 self.notifications.push(n);
743 self.notifications.extend(inferred);
744 }
745 }
746 }
747 Ok(())
748 }
749
750 fn handle_slot_info(&mut self, info: SlotInfo) -> io::Result<()> {
757 let prev = self.last_slot.replace(info);
758 match prev {
759 Some(prev) if !prev.module_present && info.module_present => {
760 self.notifications
761 .push(Notification::HotPlug(HotPlug::CamPresent));
762 self.reset_module_state();
763 let actions = self.stack.handle(Event::Host(HostRequest::Init));
767 self.run(actions)?;
768 }
769 Some(prev) if prev.module_present && !info.module_present => {
770 self.notifications
771 .push(Notification::HotPlug(HotPlug::CamRemoved));
772 self.reset_module_state();
773 }
774 _ => {}
775 }
776 Ok(())
777 }
778
779 fn reset_module_state(&mut self) {
789 self.stack = CiStack::new();
790 self.settle_until = None;
791 self.slot_ready_deadline = None;
792 self.deferred_writes.clear();
793 self.next_timer = None;
794 self.last_caids = None;
795 self.last_descrambling_ok = None;
796 self.last_mmi_card_present = None;
797 self.managed.clear();
798 }
799
800 fn infer_card(&mut self, note: &Notification) -> Vec<Notification> {
807 match note {
808 Notification::CaInfo { ca_system_ids } => {
809 let new_set: BTreeSet<u16> = ca_system_ids.iter().copied().collect();
810 let mut out = Vec::new();
811 if let Some(prev) = &self.last_caids {
812 if prev.is_empty() && !new_set.is_empty() {
813 out.push(Notification::HotPlug(HotPlug::CardInserted));
814 } else if !prev.is_empty() && new_set.is_empty() {
815 out.push(Notification::HotPlug(HotPlug::CardRemoved));
816 } else if !prev.is_empty() && !new_set.is_empty() && *prev != new_set {
817 out.push(Notification::HotPlug(HotPlug::CardChanged));
818 }
819 }
820 self.managed.set_cam_caids(new_set.clone());
824 self.last_caids = Some(new_set);
825 out
826 }
827 Notification::CaPmtReply {
828 program_number,
829 ca_enable,
830 descrambling_ok,
831 } => {
832 let mut out = Vec::new();
833 if let Some(prev) = self.last_descrambling_ok {
834 if !prev && *descrambling_ok {
835 out.push(Notification::HotPlug(HotPlug::CardInserted));
836 } else if prev && !*descrambling_ok {
837 out.push(Notification::HotPlug(HotPlug::CardRemoved));
838 }
839 }
840 self.last_descrambling_ok = Some(*descrambling_ok);
841 if let Some((v, ok)) =
845 self.managed
846 .record_reply(*program_number, *ca_enable, *descrambling_ok)
847 {
848 out.push(Notification::Entitlement {
849 program_number: *program_number,
850 ca_enable: v,
851 descrambling_ok: ok,
852 });
853 }
854 out
855 }
856 Notification::Mmi(ev) => {
857 let present = Self::mmi_text(ev).and_then(|text| {
858 let lower = text.to_lowercase();
859 if MMI_CARD_ABSENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
860 Some(false)
861 } else if MMI_CARD_PRESENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
862 Some(true)
863 } else {
864 None
865 }
866 });
867 match present {
868 Some(p) if self.last_mmi_card_present != Some(p) => {
870 self.last_mmi_card_present = Some(p);
871 vec![Notification::HotPlug(if p {
872 HotPlug::CardInserted
873 } else {
874 HotPlug::CardRemoved
875 })]
876 }
877 _ => Vec::new(),
878 }
879 }
880 _ => Vec::new(),
881 }
882 }
883
884 fn mmi_text(ev: &MmiEvent) -> Option<String> {
888 match ev {
889 MmiEvent::Menu(m) | MmiEvent::List(m) => {
890 let mut s = format!("{} {} {}", m.title, m.subtitle, m.bottom);
891 for choice in &m.choices {
892 s.push(' ');
893 s.push_str(choice);
894 }
895 Some(s)
896 }
897 MmiEvent::Enquiry { prompt, .. } => Some(prompt.clone()),
898 MmiEvent::Close => None,
899 }
900 }
901}
902
903#[cfg(test)]
904pub(crate) mod tests {
905 use super::*;
906 use crate::device::{DeviceOp, MockCaDevice};
907 use crate::event::{HostControlEvent, HotPlug, Notification};
908 use broadcast_common::Serialize;
909 use dvb_ci::tpdu::tags;
910
911 pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
912 let mut b = vec![0u8; s.serialized_len()];
913 match s.serialize_into(&mut b) {
914 Ok(n) => b.truncate(n),
915 Err(_) => b.clear(),
916 }
917 b
918 }
919
920 #[derive(Clone)]
926 pub(crate) struct TestClock(std::rc::Rc<std::cell::Cell<Instant>>);
927
928 impl TestClock {
929 pub(crate) fn new() -> Self {
930 Self(std::rc::Rc::new(std::cell::Cell::new(Instant::now())))
931 }
932
933 pub(crate) fn advance(&self, d: Duration) {
935 self.0.set(self.0.get() + d);
936 }
937
938 pub(crate) fn as_fn(&self) -> impl Fn() -> Instant + 'static {
940 let cell = self.0.clone();
941 move || cell.get()
942 }
943 }
944
945 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
948 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
949 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
950 v.extend_from_slice(spdu);
951 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
952 v
953 }
954
955 pub(crate) fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
958 use dvb_ci::spdu::SessionNumber;
959 let mut spdu = ser(&SessionNumber {
960 length_form: dvb_ci::length::LengthForm::Minimal,
961 session_nb,
962 });
963 spdu.extend_from_slice(apdu);
964 r_data(1, &spdu)
965 }
966
967 pub(crate) fn sb() -> Vec<u8> {
970 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
971 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
972 }
973
974 pub(crate) fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
977 d.device_mut().inbound.push_back(frame);
978 d.pump(Duration::from_millis(10)).unwrap();
979 for _ in 0..8 {
980 d.device_mut().inbound.push_back(sb());
981 d.pump(Duration::from_millis(10)).unwrap();
982 }
983 }
984
985 pub(crate) fn driver_with_sessions() -> Driver<MockCaDevice> {
989 use dvb_ci::objects::resource_manager::Profile;
990 use dvb_ci::resource::{
991 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
992 RESOURCE_MANAGER,
993 };
994 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
995
996 let mut d = Driver::new(MockCaDevice::new([]));
997 d.init().unwrap();
998 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1000 feed(
1002 &mut d,
1003 r_data(
1004 1,
1005 &ser(&OpenSessionRequest {
1006 length_form: dvb_ci::length::LengthForm::Minimal,
1007 resource: RESOURCE_MANAGER,
1008 }),
1009 ),
1010 );
1011 feed(
1014 &mut d,
1015 r_apdu(
1016 1,
1017 &ser(&Profile {
1018 length_form: dvb_ci::length::LengthForm::Minimal,
1019 resources: vec![
1020 APPLICATION_INFORMATION,
1021 CONDITIONAL_ACCESS_SUPPORT,
1022 MMI,
1023 HOST_CONTROL,
1024 ],
1025 }),
1026 ),
1027 );
1028 for (nb, res) in [
1030 (2u16, APPLICATION_INFORMATION),
1031 (3, CONDITIONAL_ACCESS_SUPPORT),
1032 (4, MMI),
1033 (5, HOST_CONTROL),
1034 ] {
1035 feed(
1036 &mut d,
1037 r_data(
1038 1,
1039 &ser(&CreateSessionResponse {
1040 length_form: dvb_ci::length::LengthForm::Minimal,
1041 status: SessionStatus::Ok,
1042 resource: res,
1043 session_nb: nb,
1044 }),
1045 ),
1046 );
1047 }
1048 d
1049 }
1050
1051 const RM_SESSION: u16 = 1;
1055 pub(crate) const CA_SESSION: u16 = 3;
1056 const MMI_SESSION: u16 = 4;
1057 const HOST_CONTROL_SESSION: u16 = 5;
1058
1059 #[test]
1060 fn host_control_tune_apdu_surfaces_notification_via_driver() {
1061 use dvb_ci::objects::host_control::Tune;
1062
1063 let mut d = driver_with_sessions();
1064 let hc_nb = HOST_CONTROL_SESSION;
1065 d.take_notifications(); let tune = Tune {
1069 length_form: dvb_ci::length::LengthForm::Minimal,
1070 network_id: 0x1122,
1071 original_network_id: 0x3344,
1072 transport_stream_id: 0x5566,
1073 service_id: 0x7788,
1074 };
1075 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
1076
1077 let notes = d.take_notifications();
1079 assert!(
1080 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
1081 network_id: 0x1122,
1082 original_network_id: 0x3344,
1083 transport_stream_id: 0x5566,
1084 service_id: 0x7788,
1085 })),
1086 "expected HostControl(Tune) notification, got {notes:?}"
1087 );
1088 }
1089
1090 #[test]
1093 fn padded_tune_from_the_cam_surfaces_an_error_notification() {
1094 let mut d = driver_with_sessions();
1095 d.take_notifications();
1096 let padded = [
1098 0x9F, 0x84, 0x00, 0x09, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xEE,
1099 ];
1100 feed(&mut d, r_apdu(HOST_CONTROL_SESSION, &padded));
1101 let notes = d.take_notifications();
1102 assert!(
1103 notes.iter().any(
1104 |n| matches!(n, Notification::Error { detail } if detail.contains("trailing bytes"))
1105 ),
1106 "expected an Error notification naming the trailing bytes, got {notes:?}"
1107 );
1108 assert!(
1109 !notes
1110 .iter()
1111 .any(|n| matches!(n, Notification::HostControl(_))),
1112 "a refused tune must not also surface as a HostControl event: {notes:?}"
1113 );
1114 }
1115
1116 #[test]
1119 fn malformed_mmi_apdu_from_the_cam_surfaces_an_error_notification() {
1120 let mut d = driver_with_sessions();
1121 d.take_notifications();
1122 feed(&mut d, r_apdu(MMI_SESSION, &[0x9F, 0x88, 0x07, 0x01, 0x00]));
1124 let notes = d.take_notifications();
1125 assert!(
1126 notes
1127 .iter()
1128 .any(|n| matches!(n, Notification::Error { .. })),
1129 "expected an Error notification, got {notes:?}"
1130 );
1131 }
1132
1133 #[test]
1134 fn profile_reply_advertises_host_control() {
1135 use broadcast_common::Parse;
1136 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
1137 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
1138
1139 let mut d = Driver::new(MockCaDevice::new([]));
1140 d.init().unwrap();
1141 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1142 feed(
1143 &mut d,
1144 r_data(
1145 1,
1146 &ser(&dvb_ci::spdu::OpenSessionRequest {
1147 length_form: dvb_ci::length::LengthForm::Minimal,
1148 resource: RESOURCE_MANAGER,
1149 }),
1150 ),
1151 );
1152 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq::default())));
1154
1155 let want = dvb_ci::tag::PROFILE.to_bytes();
1158 let found = d.device().ops.iter().any(|op| {
1159 if let DeviceOp::Write(w) = op
1160 && let Some(pos) = w.windows(3).position(|x| x == want)
1161 && let Ok(p) = Profile::parse(&w[pos..])
1162 {
1163 return p.resources.contains(&HOST_CONTROL);
1164 }
1165 false
1166 });
1167 assert!(found, "profile reply must advertise HOST_CONTROL");
1168 }
1169
1170 #[test]
1171 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
1172 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
1173
1174 let mut d = driver_with_sessions();
1175 let mmi_nb = MMI_SESSION;
1176
1177 d.mmi_menu_answer(2).unwrap();
1180 d.device_mut().inbound.push_back(sb());
1181 d.pump(Duration::from_millis(10)).unwrap();
1182 assert_apdu_on_session(
1183 &d,
1184 mmi_nb,
1185 &ser(&MenuAnsw {
1186 length_form: dvb_ci::length::LengthForm::Minimal,
1187 choice_ref: 2,
1188 }),
1189 );
1190
1191 d.mmi_enquiry_answer(b"1234").unwrap();
1193 d.device_mut().inbound.push_back(sb());
1194 d.pump(Duration::from_millis(10)).unwrap();
1195 assert_apdu_on_session(
1196 &d,
1197 mmi_nb,
1198 &ser(&Answ {
1199 length_form: dvb_ci::length::LengthForm::Minimal,
1200 answ_id: AnswId::Answer,
1201 text_chars: b"1234",
1202 }),
1203 );
1204 }
1205
1206 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
1209 use dvb_ci::spdu::SessionNumber;
1210 let mut want = ser(&SessionNumber {
1211 length_form: dvb_ci::length::LengthForm::Minimal,
1212 session_nb,
1213 });
1214 want.extend_from_slice(apdu);
1215 let hit = d.device().ops.iter().any(|op| match op {
1216 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1217 _ => false,
1218 });
1219 assert!(
1220 hit,
1221 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
1222 );
1223 }
1224
1225 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
1229 use dvb_ci::spdu::SessionNumber;
1230 let mut want = ser(&SessionNumber {
1231 length_form: dvb_ci::length::LengthForm::Minimal,
1232 session_nb,
1233 });
1234 want.extend_from_slice(apdu);
1235 d.device()
1236 .ops
1237 .iter()
1238 .filter(|op| match op {
1239 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1240 _ => false,
1241 })
1242 .count()
1243 }
1244
1245 struct SettleDevice {
1248 inner: MockCaDevice,
1249 settle: Duration,
1250 }
1251
1252 impl CaDevice for SettleDevice {
1253 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1254 self.inner.read(buf)
1255 }
1256 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1257 self.inner.write(buf)
1258 }
1259 fn reset(&mut self) -> io::Result<()> {
1260 self.inner.reset()
1261 }
1262 fn reset_settle(&self) -> Duration {
1263 self.settle
1264 }
1265 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1266 self.inner.slot_info()
1267 }
1268 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1269 self.inner.poll(timeout)
1270 }
1271 }
1272
1273 const SETTLE: Duration = Duration::from_secs(3);
1274
1275 fn settling_driver(clock: &TestClock) -> Driver<SettleDevice> {
1276 Driver::new(SettleDevice {
1277 inner: MockCaDevice::new([]),
1278 settle: SETTLE,
1279 })
1280 .with_clock(clock.as_fn())
1281 }
1282
1283 fn create_tc_writes(d: &Driver<SettleDevice>) -> usize {
1284 d.device()
1285 .inner
1286 .ops
1287 .iter()
1288 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1289 .count()
1290 }
1291
1292 struct NotRunningDevice {
1296 inner: SettleDevice,
1297 refusals_left: u32,
1298 refused: u32,
1299 }
1300
1301 impl CaDevice for NotRunningDevice {
1302 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1303 self.inner.read(buf)
1304 }
1305 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1306 if self.refusals_left > 0 {
1307 self.refusals_left -= 1;
1308 self.refused += 1;
1309 return Err(io::Error::from(io::ErrorKind::InvalidInput));
1310 }
1311 self.inner.write(buf)
1312 }
1313 fn reset(&mut self) -> io::Result<()> {
1314 self.inner.reset()
1315 }
1316 fn reset_settle(&self) -> Duration {
1317 self.inner.reset_settle()
1318 }
1319 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1320 self.inner.slot_info()
1321 }
1322 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1323 self.inner.poll(timeout)
1324 }
1325 }
1326
1327 fn not_running_driver(clock: &TestClock, refusals: u32) -> Driver<NotRunningDevice> {
1328 Driver::new(NotRunningDevice {
1329 inner: SettleDevice {
1330 inner: MockCaDevice::new([]),
1331 settle: SETTLE,
1332 },
1333 refusals_left: refusals,
1334 refused: 0,
1335 })
1336 .with_clock(clock.as_fn())
1337 }
1338
1339 fn create_tc_count(d: &Driver<NotRunningDevice>) -> usize {
1340 d.device()
1341 .inner
1342 .inner
1343 .ops
1344 .iter()
1345 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1346 .count()
1347 }
1348
1349 #[test]
1354 fn post_reset_einval_is_retried_until_the_slot_is_running() {
1355 let clock = TestClock::new();
1356 let mut d = not_running_driver(&clock, 3);
1357 d.init().unwrap();
1358 clock.advance(SETTLE + Duration::from_millis(100));
1359 for _ in 0..10 {
1360 d.pump(Duration::from_millis(10))
1361 .expect("a not-yet-RUNNING slot (EINVAL) must be retried, not reported");
1362 if create_tc_count(&d) == 1 {
1363 break;
1364 }
1365 clock.advance(Duration::from_millis(300));
1366 }
1367 assert_eq!(d.device().refused, 3, "the three refusals were consumed");
1368 assert_eq!(create_tc_count(&d), 1, "Create_T_C delivered exactly once");
1369 }
1370
1371 #[test]
1374 fn post_reset_einval_is_reported_once_the_grace_window_has_passed() {
1375 let clock = TestClock::new();
1376 let mut d = not_running_driver(&clock, u32::MAX);
1377 let start = clock.0.get();
1378 d.init().unwrap();
1379 clock.advance(SETTLE + Duration::from_millis(100));
1380 let mut failure = None;
1381 for _ in 0..80 {
1382 match d.pump(Duration::from_millis(10)) {
1383 Ok(_) => clock.advance(Duration::from_millis(300)),
1384 Err(e) => {
1385 failure = Some(e);
1386 break;
1387 }
1388 }
1389 }
1390 let e = failure.expect("a slot that never runs must eventually be reported");
1391 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1392 assert!(
1393 clock.0.get() >= start + SETTLE + SLOT_READY_GRACE,
1394 "reported before the grace window ended"
1395 );
1396 assert_eq!(create_tc_count(&d), 0);
1397 }
1398
1399 #[test]
1404 fn requery_armed_before_the_handshake_sends_nothing() {
1405 let clock = TestClock::new();
1406 let mut d = Driver::new(MockCaDevice::new([])).with_clock(clock.as_fn());
1407 d.set_requery_interval(Duration::from_secs(10));
1408 d.init().unwrap();
1409 for _ in 0..40 {
1410 clock.advance(Duration::from_secs(1));
1411 d.pump(Duration::from_millis(100)).unwrap();
1412 }
1413 let ca_pmt_tag = [0x9F, 0x80, 0x32];
1414 let ca_pmt_writes = d
1415 .device()
1416 .ops
1417 .iter()
1418 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == ca_pmt_tag)))
1419 .count();
1420 assert_eq!(
1421 ca_pmt_writes, 0,
1422 "re-query wrote a ca_pmt before any service"
1423 );
1424 }
1425
1426 #[test]
1429 fn reset_does_not_block_pump() {
1430 let clock = TestClock::new();
1431 let mut d = settling_driver(&clock);
1432 let t = std::time::Instant::now();
1433 d.init().unwrap();
1434 d.pump(Duration::from_millis(10)).unwrap();
1435 assert!(t.elapsed() < Duration::from_secs(1), "reset/pump blocked");
1436 assert_eq!(d.device().inner.ops[0], DeviceOp::Reset);
1437 }
1438
1439 #[test]
1443 fn no_transport_traffic_until_settle_elapsed() {
1444 let clock = TestClock::new();
1445 let mut d = settling_driver(&clock);
1446 d.device_mut()
1447 .inner
1448 .inbound
1449 .push_back(vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1450 d.init().unwrap();
1451 for _ in 0..5 {
1452 clock.advance(Duration::from_millis(500));
1453 d.pump(Duration::from_millis(10)).unwrap();
1454 }
1455 assert_eq!(create_tc_writes(&d), 0);
1457 assert_eq!(
1458 d.device().inner.inbound.len(),
1459 1,
1460 "frame read while settling"
1461 );
1462
1463 clock.advance(Duration::from_millis(600));
1464 d.pump(Duration::from_millis(10)).unwrap();
1465 assert_eq!(create_tc_writes(&d), 1);
1466
1467 let mut z = Driver::new(MockCaDevice::new([]));
1468 z.init().unwrap();
1469 assert_eq!(
1470 d.device().inner.written()[..3],
1471 z.device().written()[..3],
1472 "post-settle Create_T_C differs from the zero-settle sequence"
1473 );
1474 assert!(d.device().inner.inbound.is_empty());
1477 }
1478
1479 #[test]
1482 fn second_reset_restarts_settle() {
1483 let clock = TestClock::new();
1484 let mut d = settling_driver(&clock);
1485 d.init().unwrap();
1486 clock.advance(Duration::from_secs(2));
1487 d.pump(Duration::from_millis(10)).unwrap();
1488 d.init().unwrap(); clock.advance(Duration::from_secs(2)); d.pump(Duration::from_millis(10)).unwrap();
1491 assert_eq!(create_tc_writes(&d), 0, "first deadline wrongly honoured");
1492 clock.advance(Duration::from_millis(1500)); d.pump(Duration::from_millis(10)).unwrap();
1494 assert_eq!(create_tc_writes(&d), 1);
1495 }
1496
1497 #[test]
1500 fn slot_sampling_continues_while_settling() {
1501 let clock = TestClock::new();
1502 let mut d = settling_driver(&clock);
1503 d.init().unwrap();
1504 d.take_notifications();
1505 d.device_mut().inner.slot.module_present = false;
1506 clock.advance(Duration::from_millis(500));
1507 d.pump(Duration::from_millis(10)).unwrap();
1508 let notes = d.take_notifications();
1509 assert!(
1510 notes
1511 .iter()
1512 .any(|n| matches!(n, Notification::HotPlug(HotPlug::CamRemoved))),
1513 "{notes:?}"
1514 );
1515 assert_eq!(create_tc_writes(&d), 0);
1516 }
1517
1518 #[test]
1519 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
1520 let mut d = Driver::new(MockCaDevice::new([]));
1521 d.init().unwrap();
1522 let ops = &d.device().ops;
1523 assert_eq!(ops[0], DeviceOp::Reset);
1524 assert_eq!(ops[1], DeviceOp::SlotInfo);
1525 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
1526 }
1527
1528 #[test]
1529 fn reads_reply_then_polls_on_pump() {
1530 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1532 let clock = TestClock::new();
1533 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1534 d.init().unwrap();
1535 assert!(d.pump(Duration::from_millis(100)).unwrap());
1537 clock.advance(Duration::from_millis(100));
1542 assert!(!d.pump(Duration::from_millis(100)).unwrap());
1544 let last = d.device().ops.last().unwrap();
1545 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1546 }
1547
1548 #[test]
1554 fn pump_advances_the_stack_clock_by_real_elapsed_time_not_by_timeout() {
1555 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1556 let clock = TestClock::new();
1557 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1558 d.init().unwrap();
1559 assert!(d.pump(Duration::from_secs(1)).unwrap());
1560
1561 clock.advance(Duration::from_millis(1));
1565 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1566 assert!(
1567 d.device().ops.last().is_none_or(|op| !matches!(
1568 op,
1569 DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)
1570 )),
1571 "must not poll after only 1ms of real elapsed time, regardless of the 1s `timeout` argument"
1572 );
1573
1574 clock.advance(Duration::from_millis(100));
1575 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1576 let last = d.device().ops.last().unwrap();
1577 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1578 }
1579
1580 #[test]
1583 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
1584 let mut dev = MockCaDevice::new([]);
1585 dev.slot = SlotInfo {
1586 num: 0,
1587 module_ready: false,
1588 module_present: false,
1589 };
1590 let mut d = Driver::new(dev);
1591 d.init().unwrap();
1592 let notes = d.take_notifications();
1595 assert!(
1596 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1597 "baseline observation must not fire CamPresent, got {notes:?}"
1598 );
1599 let resets_before = d
1600 .device()
1601 .ops
1602 .iter()
1603 .filter(|o| **o == DeviceOp::Reset)
1604 .count();
1605
1606 d.device_mut().slot = SlotInfo {
1608 num: 0,
1609 module_ready: true,
1610 module_present: true,
1611 };
1612 d.pump(Duration::from_millis(10)).unwrap();
1613
1614 let notes = d.take_notifications();
1615 let cam_present_count = notes
1616 .iter()
1617 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
1618 .count();
1619 assert_eq!(
1620 cam_present_count, 1,
1621 "expected exactly one CamPresent, got {notes:?}"
1622 );
1623 let resets_after = d
1625 .device()
1626 .ops
1627 .iter()
1628 .filter(|o| **o == DeviceOp::Reset)
1629 .count();
1630 assert_eq!(
1631 resets_after,
1632 resets_before + 1,
1633 "expected one fresh Reset on re-insert"
1634 );
1635 assert!(
1636 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
1637 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
1638 d.device().ops.last()
1639 );
1640 }
1641
1642 #[test]
1643 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
1644 let mut d = driver_with_sessions();
1645 d.take_notifications();
1646
1647 d.device_mut().slot.module_present = false;
1649 d.pump(Duration::from_millis(10)).unwrap();
1650 let notes = d.take_notifications();
1651 assert!(
1652 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
1653 "expected CamRemoved, got {notes:?}"
1654 );
1655
1656 d.mmi_menu_answer(0).unwrap();
1660 let notes = d.take_notifications();
1661 assert!(
1662 notes
1663 .iter()
1664 .any(|n| matches!(n, Notification::Error { .. })),
1665 "expected no open MMI session after teardown, got {notes:?}"
1666 );
1667
1668 let resets_before = d
1670 .device()
1671 .ops
1672 .iter()
1673 .filter(|o| **o == DeviceOp::Reset)
1674 .count();
1675 d.device_mut().slot.module_present = true;
1676 d.device_mut().slot.module_ready = true;
1677 d.pump(Duration::from_millis(10)).unwrap();
1678 let notes = d.take_notifications();
1679 assert!(
1680 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1681 "expected CamPresent on re-insert, got {notes:?}"
1682 );
1683 let resets_after = d
1684 .device()
1685 .ops
1686 .iter()
1687 .filter(|o| **o == DeviceOp::Reset)
1688 .count();
1689 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1690 }
1691
1692 #[test]
1693 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1694 let mut d = Driver::new(MockCaDevice::new([]));
1695 d.init().unwrap();
1696 d.take_notifications();
1697
1698 for _ in 0..5 {
1699 d.pump(Duration::from_millis(10)).unwrap();
1700 }
1701 let notes = d.take_notifications();
1702 assert!(
1703 !notes.iter().any(|n| matches!(
1704 n,
1705 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1706 )),
1707 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1708 );
1709 }
1710
1711 #[test]
1712 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1713 use dvb_ci::objects::ca_info::CaInfo;
1714
1715 let mut d = driver_with_sessions();
1716 d.take_notifications();
1717
1718 feed(
1720 &mut d,
1721 r_apdu(
1722 CA_SESSION,
1723 &ser(&CaInfo {
1724 length_form: dvb_ci::length::LengthForm::Minimal,
1725 ca_system_ids: vec![],
1726 }),
1727 ),
1728 );
1729 let notes = d.take_notifications();
1730 assert!(
1731 !notes.iter().any(|n| matches!(
1732 n,
1733 Notification::HotPlug(
1734 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1735 )
1736 )),
1737 "first ca_info must only establish the baseline, got {notes:?}"
1738 );
1739
1740 feed(
1742 &mut d,
1743 r_apdu(
1744 CA_SESSION,
1745 &ser(&CaInfo {
1746 length_form: dvb_ci::length::LengthForm::Minimal,
1747 ca_system_ids: vec![0x0B00],
1748 }),
1749 ),
1750 );
1751 let notes = d.take_notifications();
1752 assert!(
1753 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1754 "expected CardInserted, got {notes:?}"
1755 );
1756
1757 feed(
1759 &mut d,
1760 r_apdu(
1761 CA_SESSION,
1762 &ser(&CaInfo {
1763 length_form: dvb_ci::length::LengthForm::Minimal,
1764 ca_system_ids: vec![0x1800],
1765 }),
1766 ),
1767 );
1768 let notes = d.take_notifications();
1769 assert!(
1770 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1771 "expected CardChanged, got {notes:?}"
1772 );
1773 }
1774
1775 #[test]
1776 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1777 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1778
1779 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1780 CaPmtReply {
1781 reserved_ca_enable: Default::default(),
1782 reserved_version: Default::default(),
1783 length_form: dvb_ci::length::LengthForm::Minimal,
1784 program_number: 1,
1785 version_number: 1,
1786 current_next_indicator: true,
1787 ca_enable,
1788 streams: vec![],
1789 }
1790 }
1791
1792 let mut d = driver_with_sessions();
1793 d.take_notifications();
1794
1795 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1797 let notes = d.take_notifications();
1798 assert!(
1799 !notes.iter().any(|n| matches!(
1800 n,
1801 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1802 )),
1803 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1804 );
1805
1806 feed(
1808 &mut d,
1809 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1810 );
1811 let notes = d.take_notifications();
1812 assert!(
1813 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1814 "expected CardInserted, got {notes:?}"
1815 );
1816
1817 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1819 let notes = d.take_notifications();
1820 assert!(
1821 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1822 "expected CardRemoved, got {notes:?}"
1823 );
1824 }
1825
1826 #[test]
1827 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1828 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1829
1830 let mut d = driver_with_sessions();
1831 d.take_notifications();
1832
1833 feed(
1836 &mut d,
1837 r_apdu(
1838 CA_SESSION,
1839 &ser(&CaPmtReply {
1840 reserved_ca_enable: Default::default(),
1841 reserved_version: Default::default(),
1842 length_form: dvb_ci::length::LengthForm::Minimal,
1843 program_number: 7,
1844 version_number: 1,
1845 current_next_indicator: true,
1846 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1847 streams: vec![],
1848 }),
1849 ),
1850 );
1851 let notes = d.take_notifications();
1852 assert!(
1853 notes.contains(&Notification::CaPmtReply {
1854 program_number: 7,
1855 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1856 descrambling_ok: true,
1857 }),
1858 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1859 );
1860 }
1861
1862 #[test]
1863 fn ca_pmt_reply_flag_clear_surfaces_none() {
1864 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1865
1866 let mut d = driver_with_sessions();
1867 d.take_notifications();
1868
1869 feed(
1872 &mut d,
1873 r_apdu(
1874 CA_SESSION,
1875 &ser(&CaPmtReply {
1876 reserved_ca_enable: Default::default(),
1877 reserved_version: Default::default(),
1878 length_form: dvb_ci::length::LengthForm::Minimal,
1879 program_number: 7,
1880 version_number: 1,
1881 current_next_indicator: true,
1882 ca_enable: None,
1883 streams: vec![],
1884 }),
1885 ),
1886 );
1887 let notes = d.take_notifications();
1888 assert!(
1889 notes.contains(&Notification::CaPmtReply {
1890 program_number: 7,
1891 ca_enable: None,
1892 descrambling_ok: false,
1893 }),
1894 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1895 );
1896 }
1897
1898 #[test]
1899 fn mmi_no_card_text_infers_card_removed() {
1900 use dvb_ci::objects::mmi_high::Enq;
1901
1902 let mut d = driver_with_sessions();
1903 d.take_notifications();
1904
1905 feed(
1906 &mut d,
1907 r_apdu(
1908 MMI_SESSION,
1909 &ser(&Enq {
1910 reserved: Default::default(),
1911 length_form: dvb_ci::length::LengthForm::Minimal,
1912 blind_answer: false,
1913 answer_text_length: 0,
1914 text_chars: b"NO CARD detected - please insert your smart card",
1915 }),
1916 ),
1917 );
1918
1919 let notes = d.take_notifications();
1920 assert!(
1921 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1922 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1923 );
1924 }
1925
1926 #[test]
1929 fn mmi_card_keyword_heuristic_is_edge_triggered() {
1930 use dvb_ci::objects::mmi_high::Enq;
1931
1932 let enq = |text: &'static [u8]| {
1933 r_apdu(
1934 MMI_SESSION,
1935 &ser(&Enq {
1936 reserved: Default::default(),
1937 length_form: dvb_ci::length::LengthForm::Minimal,
1938 blind_answer: false,
1939 answer_text_length: 0,
1940 text_chars: text,
1941 }),
1942 )
1943 };
1944 let mut d = driver_with_sessions();
1945 d.take_notifications();
1946 let count = |notes: &[Notification], hp: HotPlug| {
1947 notes
1948 .iter()
1949 .filter(|n| **n == Notification::HotPlug(hp))
1950 .count()
1951 };
1952
1953 for _ in 0..3 {
1955 feed(&mut d, enq(b"No card - please insert your smart card"));
1956 }
1957 let notes = d.take_notifications();
1958 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1959 assert_eq!(count(¬es, HotPlug::CardInserted), 0);
1960
1961 for _ in 0..2 {
1963 feed(&mut d, enq(b"Your entitlement is valid"));
1964 }
1965 let notes = d.take_notifications();
1966 assert_eq!(count(¬es, HotPlug::CardInserted), 1, "{notes:?}");
1967 assert_eq!(count(¬es, HotPlug::CardRemoved), 0);
1968
1969 feed(&mut d, enq(b"Card removed"));
1971 feed(&mut d, enq(b"Card removed"));
1972 let notes = d.take_notifications();
1973 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1974 }
1975
1976 #[test]
1977 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1978 let mut dev = MockCaDevice::new([]);
1979 dev.slot = SlotInfo {
1980 num: 0,
1981 module_ready: false,
1982 module_present: false,
1983 };
1984 let mut d = Driver::new(dev);
1985 d.init().unwrap();
1986 d.take_notifications(); d.device_mut().slot = SlotInfo {
1990 num: 0,
1991 module_ready: true,
1992 module_present: true,
1993 };
1994
1995 let mut seen = Vec::new();
1996 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1997 .unwrap();
1998
1999 assert_eq!(
2000 seen,
2001 vec![HotPlug::CamPresent],
2002 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
2003 );
2004 }
2005
2006 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: Pid) -> [u8; 6] {
2011 [
2012 0x09,
2013 0x04,
2014 (ca_system_id >> 8) as u8,
2015 ca_system_id as u8,
2016 0xE0 | ((pid.value() >> 8) as u8 & 0x1F),
2017 pid.value() as u8,
2018 ]
2019 }
2020
2021 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
2038 const VIACCESS: u16 = 0x0500;
2039 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2040 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2041
2042 let mut body = Vec::new();
2043 body.push(0x02); body.push(0); body.push(0);
2046 body.extend_from_slice(&program_number.to_be_bytes());
2047 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
2052 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2053 body.push(prog_ca.len() as u8);
2054 body.extend_from_slice(&prog_ca);
2055 body.push(0x1B);
2057 body.push(0xE0 | 0x01);
2058 body.push(0x00);
2059 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2060 body.push(es0_ca.len() as u8);
2061 body.extend_from_slice(&es0_ca);
2062 body.push(0x0F);
2064 body.push(0xE0 | 0x01);
2065 body.push(0x01);
2066 body.push(0xF0);
2067 body.push(0x00);
2068
2069 let section_length = body.len() - 3 + 4;
2070 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2071 body[2] = section_length as u8;
2072 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2073 body.extend_from_slice(&crc.to_be_bytes());
2074 body
2075 }
2076
2077 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
2084 const VIACCESS: u16 = 0x0500;
2085 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0064));
2086 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0065));
2087
2088 let mut body = Vec::new();
2089 body.push(0x02); body.push(0); body.push(0);
2092 body.extend_from_slice(&program_number.to_be_bytes());
2093 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));
2099 body.push(prog_ca.len() as u8);
2100 body.extend_from_slice(&prog_ca);
2101 body.push(0x1B);
2103 body.push(0xE0 | 0x01);
2104 body.push(0x00);
2105 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2106 body.push(es0_ca.len() as u8);
2107 body.extend_from_slice(&es0_ca);
2108 body.push(0x0F);
2110 body.push(0xE0 | 0x01);
2111 body.push(0x01);
2112 body.push(0xF0);
2113 body.push(0x00);
2114
2115 let section_length = body.len() - 3 + 4;
2116 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2117 body[2] = section_length as u8;
2118 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2119 body.extend_from_slice(&crc.to_be_bytes());
2120 body
2121 }
2122
2123 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
2127 let mut body = Vec::new();
2128 body.push(0x02);
2129 body.push(0);
2130 body.push(0);
2131 body.extend_from_slice(&program_number.to_be_bytes());
2132 body.push(0xC3);
2133 body.push(0x00);
2134 body.push(0x00);
2135 body.push(0xE0 | 0x01);
2136 body.push(0x00);
2137 body.push(0xF0); body.push(0x00);
2139 body.push(0x1B);
2141 body.push(0xE0 | 0x01);
2142 body.push(0x00);
2143 body.push(0xF0);
2144 body.push(0x00);
2145
2146 let section_length = body.len() - 3 + 4;
2147 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2148 body[2] = section_length as u8;
2149 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2150 body.extend_from_slice(&crc.to_be_bytes());
2151 body
2152 }
2153
2154 #[test]
2155 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
2156 use broadcast_common::Parse;
2157
2158 let mut d = driver_with_sessions();
2159 d.take_notifications();
2160
2161 let pmt_bytes = build_ca_pmt_fixture(1546);
2162 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2163
2164 d.add_service(&pmt).unwrap();
2165 d.device_mut().inbound.push_back(sb());
2166 d.pump(Duration::from_millis(10)).unwrap();
2167
2168 let expected =
2172 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2173 assert_apdu_on_session(&d, CA_SESSION, &expected);
2174
2175 let svc = d
2177 .managed_ca()
2178 .services()
2179 .get(&1546)
2180 .expect("program_number 1546 must be tracked after add_service");
2181 assert_eq!(svc.es_pids, vec![Pid::new(0x0100), Pid::new(0x0101)]);
2182 assert_eq!(svc.ca_pids, vec![Pid::new(0x0064), Pid::new(0x0065)]);
2183 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
2184 assert_eq!(svc.last_ca_enable, None);
2185 }
2186
2187 #[test]
2188 fn add_service_rejects_pmt_without_ca_descriptor() {
2189 use broadcast_common::Parse;
2190
2191 let mut d = driver_with_sessions();
2192 let pmt_bytes = build_clear_pmt_fixture(999);
2193 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2194
2195 let err = d.add_service(&pmt).unwrap_err();
2196 assert!(
2197 matches!(
2198 err,
2199 CaError::NoCaDescriptor {
2200 program_number: 999
2201 }
2202 ),
2203 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
2204 );
2205 assert!(
2206 d.managed_ca().services().is_empty(),
2207 "a rejected PMT must not be recorded"
2208 );
2209 }
2210
2211 #[test]
2223 fn requery_tick_serialize_failure_errors_from_pump_instead_of_panicking() {
2224 use broadcast_common::Parse;
2225
2226 let clock = TestClock::new();
2227 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2228
2229 let good_bytes = build_ca_pmt_fixture(1);
2232 let good = PmtSection::parse(&good_bytes).unwrap();
2233 assert_eq!(good.program_number, 1, "fixture precondition");
2234 d.add_service(&good).unwrap();
2235
2236 let pmt_bytes = build_ca_pmt_fixture(1546);
2241 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2242 let built = d
2243 .build_ca_pmt_filtered(
2244 &pmt,
2245 CaPmtListManagement::Update,
2246 CaPmtCmdId::OkDescrambling,
2247 )
2248 .try_to_bytes()
2249 .expect("fixture precondition: the good PMT encodes");
2250 let corrupt_raw = pmt_bytes[..pmt_bytes.len() - 6].to_vec();
2251 d.managed.record(
2252 1546,
2253 managed::service_of(&pmt, CaPmtCmdId::OkDescrambling, built, corrupt_raw),
2254 );
2255 d.take_notifications();
2256 for _ in 0..16 {
2260 d.device_mut().inbound.push_back(sb());
2261 d.pump(Duration::from_millis(10)).unwrap();
2262 }
2263 d.set_requery_interval(Duration::from_secs(5));
2264
2265 clock.advance(Duration::from_secs(5));
2269 let err = d
2270 .pump(Duration::ZERO)
2271 .expect_err("re-query of a corrupt stored PMT must error, not panic");
2272 assert!(
2273 matches!(
2274 err.get_ref(),
2275 Some(e) if e.downcast_ref::<CaError>()
2276 .is_some_and(|ca| matches!(ca, CaError::PmtParse(_)))
2277 ),
2278 "expected CaError::PmtParse from the corrupt stored bytes, got {err:?}"
2279 );
2280 assert!(
2281 err.to_string().contains("stored PMT re-parse failed"),
2282 "error must name the re-parse failure, got: {err}"
2283 );
2284 assert!(d.pump(Duration::ZERO).is_ok());
2288 clock.advance(Duration::from_secs(5));
2289 let err2 = d
2290 .pump(Duration::ZERO)
2291 .expect_err("the corrupt service must fail every re-query");
2292 assert!(
2293 err2.to_string().contains("stored PMT re-parse failed"),
2294 "failure must persist, got: {err2}"
2295 );
2296 }
2297
2298 fn corrupt_ca_section_ctor() -> PmtSection<'static> {
2304 const LOOP: usize = 5000;
2305 let mut prog_info: Vec<u8> = Vec::with_capacity(LOOP);
2306 for _ in 0..LOOP / 4 {
2307 prog_info.extend_from_slice(&[0x09u8, 0x02, 0x0B, 0x00]);
2309 }
2310 assert_eq!(prog_info.len(), LOOP);
2311 PmtSection::new(
2312 1,
2313 dvb_si::tables::SectionVersion::new(1, true, 0, 0),
2314 Pid::new(0x0200),
2315 dvb_si::descriptors::DescriptorLoop::new(Box::leak(prog_info.into_boxed_slice())),
2316 Vec::new(),
2317 )
2318 }
2319
2320 #[test]
2325 fn add_service_serialize_failure_returns_err_instead_of_panicking() {
2326 let mut d = driver_with_sessions();
2327 d.managed.set_cam_caids([0x0B00u16].into_iter().collect());
2328 let pmt = corrupt_ca_section_ctor();
2329 let err = d
2330 .add_service(&pmt)
2331 .expect_err("a ca_pmt with no valid encoding must error, not panic");
2332 assert!(
2333 matches!(err, CaError::Serialize(_)),
2334 "expected CaError::Serialize, got {err:?}"
2335 );
2336 assert!(
2337 d.managed_ca().services().is_empty(),
2338 "a rejected PMT must not be recorded"
2339 );
2340 }
2341
2342 #[test]
2343 fn add_service_second_call_uses_add_list_management() {
2344 use broadcast_common::Parse;
2345
2346 let mut d = driver_with_sessions();
2347 d.take_notifications();
2348
2349 let pmt1_bytes = build_ca_pmt_fixture(1546);
2350 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2351 d.add_service(&pmt1).unwrap();
2352 d.device_mut().inbound.push_back(sb());
2353 d.pump(Duration::from_millis(10)).unwrap();
2354
2355 let pmt2_bytes = build_ca_pmt_fixture(1547);
2356 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2357 d.add_service(&pmt2).unwrap();
2358 d.device_mut().inbound.push_back(sb());
2359 d.pump(Duration::from_millis(10)).unwrap();
2360
2361 let expected2 =
2363 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
2364 assert_apdu_on_session(&d, CA_SESSION, &expected2);
2365
2366 assert_eq!(d.managed_ca().services().len(), 2);
2367 }
2368
2369 fn build_ca_pmt_fixture_two_caids(program_number: u16) -> Vec<u8> {
2380 const VIACCESS: u16 = 0x0500;
2381 const NAGRA: u16 = 0x1800;
2382 let prog_ca_a = ca_descriptor(VIACCESS, Pid::new(0x0064));
2383 let prog_ca_b = ca_descriptor(NAGRA, Pid::new(0x0066));
2384 let mut prog_ca = Vec::new();
2385 prog_ca.extend_from_slice(&prog_ca_a);
2386 prog_ca.extend_from_slice(&prog_ca_b);
2387
2388 let mut body = Vec::new();
2389 body.push(0x02); body.push(0);
2391 body.push(0);
2392 body.extend_from_slice(&program_number.to_be_bytes());
2393 body.push(0xC3);
2394 body.push(0x00);
2395 body.push(0x00);
2396 body.push(0xE0 | 0x01); body.push(0x00);
2398 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2399 body.push(prog_ca.len() as u8);
2400 body.extend_from_slice(&prog_ca);
2401 body.push(0x0F);
2404 body.push(0xE0 | 0x01);
2405 body.push(0x00);
2406 body.push(0xF0);
2407 body.push(0x00);
2408
2409 let section_length = body.len() - 3 + 4;
2410 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2411 body[2] = section_length as u8;
2412 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2413 body.extend_from_slice(&crc.to_be_bytes());
2414 body
2415 }
2416
2417 #[test]
2418 fn add_service_filters_ca_descriptors_to_cam_advertised_caids() {
2419 use broadcast_common::Parse;
2420 use dvb_ci::objects::ca_info::CaInfo;
2421
2422 let mut d = driver_with_sessions();
2423 d.take_notifications();
2424
2425 feed(
2428 &mut d,
2429 r_apdu(
2430 CA_SESSION,
2431 &ser(&CaInfo {
2432 length_form: dvb_ci::length::LengthForm::Minimal,
2433 ca_system_ids: vec![0x0500],
2434 }),
2435 ),
2436 );
2437 d.take_notifications();
2438
2439 let pmt_bytes = build_ca_pmt_fixture_two_caids(1560);
2440 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2441
2442 let expected_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2447 &pmt,
2448 &[0x0500],
2449 CaPmtListManagement::Only,
2450 CaPmtCmdId::OkDescrambling,
2451 )
2452 .to_bytes();
2453 let unfiltered =
2454 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2455 assert_ne!(
2456 expected_filtered, unfiltered,
2457 "precondition: the fixture's two CAIDs must make filtered != unfiltered"
2458 );
2459 let sends_filtered_before = count_apdu_on_session(&d, CA_SESSION, &expected_filtered);
2460 let sends_unfiltered_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered);
2461
2462 d.add_service(&pmt).unwrap();
2463 d.device_mut().inbound.push_back(sb());
2464 d.pump(Duration::from_millis(10)).unwrap();
2465
2466 assert_eq!(
2467 count_apdu_on_session(&d, CA_SESSION, &expected_filtered),
2468 sends_filtered_before + 1,
2469 "add_service must send the CAID-filtered ca_pmt"
2470 );
2471 assert_eq!(
2472 count_apdu_on_session(&d, CA_SESSION, &unfiltered),
2473 sends_unfiltered_before,
2474 "add_service must NOT send the unfiltered ca_pmt once the CAM's CAIDs are known"
2475 );
2476 }
2477
2478 #[test]
2479 fn requery_timer_filters_ca_descriptors_to_cam_advertised_caids() {
2480 use broadcast_common::Parse;
2481 use dvb_ci::objects::ca_info::CaInfo;
2482
2483 let clock = TestClock::new();
2484 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2485 d.set_requery_interval(Duration::from_secs(10));
2488 d.take_notifications();
2489
2490 let pmt_bytes = build_ca_pmt_fixture_two_caids(1561);
2491 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2492 d.add_service(&pmt).unwrap();
2495 d.device_mut().inbound.push_back(sb());
2496 clock.advance(Duration::from_millis(10));
2497 d.pump(Duration::from_millis(10)).unwrap();
2498
2499 feed(
2501 &mut d,
2502 r_apdu(
2503 CA_SESSION,
2504 &ser(&CaInfo {
2505 length_form: dvb_ci::length::LengthForm::Minimal,
2506 ca_system_ids: vec![0x0500],
2507 }),
2508 ),
2509 );
2510 d.take_notifications();
2511
2512 let expected_query_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2515 &pmt,
2516 &[0x0500],
2517 CaPmtListManagement::Update,
2518 CaPmtCmdId::Query,
2519 )
2520 .to_bytes();
2521 let expected_ok_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2522 &pmt,
2523 &[0x0500],
2524 CaPmtListManagement::Update,
2525 CaPmtCmdId::OkDescrambling,
2526 )
2527 .to_bytes();
2528 let unfiltered_query =
2529 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2530 let unfiltered_ok = build_ca_pmt(
2531 &pmt,
2532 CaPmtListManagement::Update,
2533 CaPmtCmdId::OkDescrambling,
2534 )
2535 .to_bytes();
2536 assert_ne!(
2537 expected_query_filtered, unfiltered_query,
2538 "precondition: the fixture's two CAIDs must make filtered != unfiltered (query)"
2539 );
2540 assert_ne!(
2541 expected_ok_filtered, unfiltered_ok,
2542 "precondition: the fixture's two CAIDs must make filtered != unfiltered (ok_descrambling)"
2543 );
2544 let sends_query_before = count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered);
2545 let sends_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered);
2546 let sends_unfiltered_query_before =
2547 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query);
2548 let sends_unfiltered_ok_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok);
2549
2550 clock.advance(Duration::from_secs(11));
2551 d.pump(Duration::from_secs(11)).unwrap();
2552 feed(&mut d, sb());
2553
2554 assert_eq!(
2555 count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered),
2556 sends_query_before + 1,
2557 "the re-query `query` half must be CAID-filtered to the CAM's advertised CAIDs"
2558 );
2559 assert_eq!(
2560 count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered),
2561 sends_ok_before + 1,
2562 "the re-query `ok_descrambling` half must be CAID-filtered to the CAM's advertised CAIDs"
2563 );
2564 assert_eq!(
2565 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query),
2566 sends_unfiltered_query_before,
2567 "the re-query resend must NOT be the unfiltered `query` ca_pmt"
2568 );
2569 assert_eq!(
2570 count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok),
2571 sends_unfiltered_ok_before,
2572 "the re-query resend must NOT be the unfiltered `ok_descrambling` ca_pmt"
2573 );
2574 }
2575
2576 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
2586 const EXTENSION_HEADER_LEN: u16 = 5;
2587 const CRC_LEN: u16 = 4;
2588 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
2589 let mut v = Vec::new();
2590 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
2592 v.push((section_length & 0xFF) as u8);
2593 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
2598 let crc = broadcast_common::crc32_mpeg2::compute(&v);
2599 v.extend_from_slice(&crc.to_be_bytes());
2600 v
2601 }
2602
2603 #[test]
2604 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
2605 use broadcast_common::Parse;
2606 use dvb_ci::objects::ca_info::CaInfo;
2607 use dvb_si::tables::cat::CatSection;
2608
2609 let mut d = driver_with_sessions();
2610 d.take_notifications();
2611
2612 feed(
2614 &mut d,
2615 r_apdu(
2616 CA_SESSION,
2617 &ser(&CaInfo {
2618 length_form: dvb_ci::length::LengthForm::Minimal,
2619 ca_system_ids: vec![0x0648, 0x0100],
2620 }),
2621 ),
2622 );
2623 d.take_notifications();
2624
2625 let mut descriptors = Vec::new();
2628 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2629 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2630 let cat_bytes = build_cat_fixture(&descriptors);
2631 let cat = CatSection::parse(&cat_bytes).unwrap();
2632
2633 d.set_cat(&cat).unwrap();
2634
2635 assert_eq!(
2636 d.emm_pids(),
2637 &[Pid::new(0x1FF0)],
2638 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
2639 );
2640 }
2641
2642 #[test]
2643 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
2644 use broadcast_common::Parse;
2645 use dvb_ci::objects::ca_info::CaInfo;
2646 use dvb_si::tables::cat::CatSection;
2647
2648 let mut d = driver_with_sessions();
2649 d.take_notifications();
2650
2651 let mut descriptors = Vec::new();
2652 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2653 descriptors.extend_from_slice(&ca_descriptor(0x0500, Pid::new(0x1FF1)));
2654 let cat_bytes = build_cat_fixture(&descriptors);
2655 let cat = CatSection::parse(&cat_bytes).unwrap();
2656
2657 d.set_cat(&cat).unwrap();
2660 assert!(
2661 d.emm_pids().is_empty(),
2662 "emm_pids must be empty before any ca_info arrives, got {:?}",
2663 d.emm_pids()
2664 );
2665
2666 feed(
2669 &mut d,
2670 r_apdu(
2671 CA_SESSION,
2672 &ser(&CaInfo {
2673 length_form: dvb_ci::length::LengthForm::Minimal,
2674 ca_system_ids: vec![0x0648, 0x0100],
2675 }),
2676 ),
2677 );
2678 d.take_notifications();
2679
2680 assert_eq!(
2681 d.emm_pids(),
2682 &[Pid::new(0x1FF0)],
2683 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
2684 );
2685 }
2686
2687 #[test]
2693 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
2694 use broadcast_common::Parse;
2695 use dvb_ci::objects::ca_info::CaInfo;
2696 use dvb_si::tables::cat::CatSection;
2697
2698 let mut d = driver_with_sessions();
2699 d.take_notifications();
2700
2701 feed(
2703 &mut d,
2704 r_apdu(
2705 CA_SESSION,
2706 &ser(&CaInfo {
2707 length_form: dvb_ci::length::LengthForm::Minimal,
2708 ca_system_ids: vec![0x0648, 0x0100],
2709 }),
2710 ),
2711 );
2712 d.take_notifications();
2713
2714 let mut descriptors = Vec::new();
2716 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
2717 descriptors.extend_from_slice(&ca_descriptor(0x0100, Pid::new(0x1FF0)));
2718 let cat_bytes = build_cat_fixture(&descriptors);
2719 let cat = CatSection::parse(&cat_bytes).unwrap();
2720
2721 d.set_cat(&cat).unwrap();
2722
2723 assert_eq!(
2724 d.emm_pids(),
2725 &[Pid::new(0x1FF0)],
2726 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
2727 d.emm_pids()
2728 );
2729 }
2730
2731 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
2735 const VIACCESS: u16 = 0x0500;
2736 let prog_ca = ca_descriptor(VIACCESS, Pid::new(0x0074));
2737 let es0_ca = ca_descriptor(VIACCESS, Pid::new(0x0075));
2738
2739 let mut body = Vec::new();
2740 body.push(0x02); body.push(0);
2742 body.push(0);
2743 body.extend_from_slice(&program_number.to_be_bytes());
2744 body.push(0xC3);
2745 body.push(0x00);
2746 body.push(0x00);
2747 body.push(0xE0 | 0x02); body.push(0x00);
2749 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2750 body.push(prog_ca.len() as u8);
2751 body.extend_from_slice(&prog_ca);
2752 body.push(0x1B);
2754 body.push(0xE0 | 0x02);
2755 body.push(0x00);
2756 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2757 body.push(es0_ca.len() as u8);
2758 body.extend_from_slice(&es0_ca);
2759 body.push(0x0F);
2761 body.push(0xE0 | 0x02);
2762 body.push(0x01);
2763 body.push(0xF0);
2764 body.push(0x00);
2765
2766 let section_length = body.len() - 3 + 4;
2767 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2768 body[2] = section_length as u8;
2769 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2770 body.extend_from_slice(&crc.to_be_bytes());
2771 body
2772 }
2773
2774 #[test]
2775 fn descramble_pids_is_the_union_of_active_services_es_pids() {
2776 use broadcast_common::Parse;
2777
2778 let mut d = driver_with_sessions();
2779 d.take_notifications();
2780
2781 assert!(
2782 d.descramble_pids().is_empty(),
2783 "no service added yet: descramble_pids must be empty"
2784 );
2785
2786 let pmt1_bytes = build_ca_pmt_fixture(1546);
2787 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2788 d.add_service(&pmt1).unwrap();
2789 d.device_mut().inbound.push_back(sb());
2790 d.pump(Duration::from_millis(10)).unwrap();
2791
2792 assert_eq!(d.descramble_pids(), &[Pid::new(0x0100), Pid::new(0x0101)]);
2793
2794 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2795 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2796 d.add_service(&pmt2).unwrap();
2797 d.device_mut().inbound.push_back(sb());
2798 d.pump(Duration::from_millis(10)).unwrap();
2799
2800 assert_eq!(
2802 d.descramble_pids(),
2803 &[
2804 Pid::new(0x0100),
2805 Pid::new(0x0101),
2806 Pid::new(0x0200),
2807 Pid::new(0x0201)
2808 ],
2809 "descramble_pids must be the union across both added services"
2810 );
2811 }
2812
2813 pub(crate) fn ca_pmt_reply_for(
2818 program_number: u16,
2819 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
2820 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2821 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2822 length_form: dvb_ci::length::LengthForm::Minimal,
2823 program_number,
2824 version_number: 1,
2825 current_next_indicator: true,
2826 ca_enable,
2827 streams: vec![],
2828 reserved_version: Default::default(),
2829 reserved_ca_enable: Default::default(),
2830 }
2831 }
2832
2833 #[test]
2834 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
2835 use broadcast_common::Parse;
2836 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2837
2838 let clock = TestClock::new();
2839 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2840 d.set_requery_interval(Duration::from_secs(10));
2843 d.take_notifications();
2844
2845 let pmt_bytes = build_ca_pmt_fixture(1546);
2846 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2847 d.add_service(&pmt).unwrap();
2848 d.device_mut().inbound.push_back(sb());
2849 clock.advance(Duration::from_millis(10));
2850 d.pump(Duration::from_millis(10)).unwrap();
2851 d.take_notifications();
2852
2853 let expected_initial_ca_pmt =
2857 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2858 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
2859
2860 let expected_query =
2866 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2867 let expected_ok_descrambling = build_ca_pmt(
2868 &pmt,
2869 CaPmtListManagement::Update,
2870 CaPmtCmdId::OkDescrambling,
2871 )
2872 .to_bytes();
2873 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2874 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2875
2876 let mut all_notes = Vec::new();
2877
2878 feed(
2882 &mut d,
2883 r_apdu(
2884 CA_SESSION,
2885 &ser(&ca_pmt_reply_for(
2886 1546,
2887 Some(CaEnable::NotPossibleNoEntitlement),
2888 )),
2889 ),
2890 );
2891 all_notes.extend(d.take_notifications());
2892
2893 clock.advance(Duration::from_secs(11));
2903 d.pump(Duration::from_secs(11)).unwrap();
2904 all_notes.extend(d.take_notifications());
2905 feed(&mut d, sb());
2906
2907 assert_eq!(
2908 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2909 query_sends_before + 1,
2910 "expected the re-query timer to resend the `query` half exactly once"
2911 );
2912 assert_eq!(
2913 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2914 ok_sends_before + 1,
2915 "expected the re-query timer to resend the `ok_descrambling` half exactly once"
2916 );
2917
2918 feed(
2921 &mut d,
2922 r_apdu(
2923 CA_SESSION,
2924 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
2925 ),
2926 );
2927 all_notes.extend(d.take_notifications());
2928
2929 let hits = all_notes
2930 .iter()
2931 .filter(|n| {
2932 matches!(
2933 n,
2934 Notification::Entitlement {
2935 program_number: 1546,
2936 ca_enable: CaEnable::Possible,
2937 descrambling_ok: true,
2938 }
2939 )
2940 })
2941 .count();
2942 assert_eq!(
2943 hits, 1,
2944 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
2945 );
2946 }
2947
2948 #[test]
2949 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
2950 use broadcast_common::Parse;
2951 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2952
2953 let mut d = driver_with_sessions();
2954 d.set_requery_interval(Duration::from_secs(10));
2957 d.take_notifications();
2958
2959 let pmt_bytes = build_ca_pmt_fixture(1547);
2960 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2961 d.add_service(&pmt).unwrap();
2962 d.device_mut().inbound.push_back(sb());
2963 d.pump(Duration::from_millis(10)).unwrap();
2964 d.take_notifications();
2965
2966 feed(
2970 &mut d,
2971 r_apdu(
2972 CA_SESSION,
2973 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2974 ),
2975 );
2976 d.take_notifications();
2977
2978 for _ in 0..2 {
2981 d.pump(Duration::from_secs(11)).unwrap();
2982 d.take_notifications();
2983 feed(
2984 &mut d,
2985 r_apdu(
2986 CA_SESSION,
2987 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2988 ),
2989 );
2990 let notes = d.take_notifications();
2991 assert!(
2992 !notes
2993 .iter()
2994 .any(|n| matches!(n, Notification::Entitlement { .. })),
2995 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
2996 );
2997 }
2998 }
2999
3000 #[test]
3001 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
3002 use broadcast_common::Parse;
3003 use dvb_ci::objects::ca_pmt_reply::CaEnable;
3004
3005 let mut d = driver_with_sessions();
3006 d.take_notifications();
3007
3008 let pmt_bytes = build_ca_pmt_fixture(1548);
3009 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3010 d.add_service(&pmt).unwrap();
3011 d.device_mut().inbound.push_back(sb());
3012 d.pump(Duration::from_millis(10)).unwrap();
3013 d.take_notifications();
3014
3015 feed(
3017 &mut d,
3018 r_apdu(
3019 CA_SESSION,
3020 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
3021 ),
3022 );
3023 d.take_notifications();
3024
3025 feed(
3029 &mut d,
3030 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
3031 );
3032 let notes = d.take_notifications();
3033 assert!(
3034 !notes
3035 .iter()
3036 .any(|n| matches!(n, Notification::Entitlement { .. })),
3037 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
3038 );
3039 }
3040
3041 #[test]
3042 fn requery_disabled_by_default_sends_nothing() {
3043 use broadcast_common::Parse;
3044
3045 let mut d = driver_with_sessions();
3049 d.take_notifications();
3050
3051 let pmt_bytes = build_ca_pmt_fixture(1549);
3052 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3053 d.add_service(&pmt).unwrap();
3054 d.device_mut().inbound.push_back(sb());
3055 d.pump(Duration::from_millis(10)).unwrap();
3056
3057 let expected_query =
3060 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3061 let expected_ok_descrambling = build_ca_pmt(
3062 &pmt,
3063 CaPmtListManagement::Update,
3064 CaPmtCmdId::OkDescrambling,
3065 )
3066 .to_bytes();
3067 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
3068 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
3069
3070 d.pump(Duration::from_secs(1_000_000)).unwrap();
3072 feed(&mut d, sb());
3073 feed(&mut d, sb());
3074 assert_eq!(
3075 count_apdu_on_session(&d, CA_SESSION, &expected_query),
3076 query_sends_before,
3077 "the default (Duration::ZERO) must never send a re-query `query`"
3078 );
3079 assert_eq!(
3080 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
3081 ok_sends_before,
3082 "the default (Duration::ZERO) must never send a re-query `ok_descrambling`"
3083 );
3084 }
3085
3086 #[test]
3087 fn set_requery_interval_zero_disables_resend_after_being_enabled() {
3088 use broadcast_common::Parse;
3089
3090 let mut d = driver_with_sessions();
3091 d.set_requery_interval(Duration::from_secs(5));
3094 d.set_requery_interval(Duration::ZERO);
3095 d.take_notifications();
3096
3097 let pmt_bytes = build_ca_pmt_fixture(1549);
3098 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3099 d.add_service(&pmt).unwrap();
3100 d.device_mut().inbound.push_back(sb());
3101 d.pump(Duration::from_millis(10)).unwrap();
3102
3103 let expected_ca_pmt = build_ca_pmt(
3107 &pmt,
3108 CaPmtListManagement::Update,
3109 CaPmtCmdId::OkDescrambling,
3110 )
3111 .to_bytes();
3112 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3113
3114 d.pump(Duration::from_secs(1000)).unwrap();
3116 feed(&mut d, sb());
3117 feed(&mut d, sb());
3118
3119 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
3120 assert_eq!(
3121 sends_after, sends_before,
3122 "Duration::ZERO must disable the re-query resend"
3123 );
3124 }
3125
3126 #[test]
3127 fn requery_timer_resends_every_active_service_not_just_one() {
3128 use broadcast_common::Parse;
3129
3130 let clock = TestClock::new();
3131 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3132 d.set_requery_interval(Duration::from_secs(10));
3135 d.take_notifications();
3136
3137 let pmt1_bytes = build_ca_pmt_fixture(1546);
3140 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3141 d.add_service(&pmt1).unwrap();
3142 d.device_mut().inbound.push_back(sb());
3143 clock.advance(Duration::from_millis(10));
3144 d.pump(Duration::from_millis(10)).unwrap();
3145
3146 let pmt2_bytes = build_ca_pmt_fixture(1547);
3147 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3148 d.add_service(&pmt2).unwrap();
3149 d.device_mut().inbound.push_back(sb());
3150 clock.advance(Duration::from_millis(10));
3151 d.pump(Duration::from_millis(10)).unwrap();
3152 d.take_notifications();
3153
3154 let expected1_query =
3160 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3161 let expected1_ok = build_ca_pmt(
3162 &pmt1,
3163 CaPmtListManagement::Update,
3164 CaPmtCmdId::OkDescrambling,
3165 )
3166 .to_bytes();
3167 let expected2_query =
3168 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3169 let expected2_ok = build_ca_pmt(
3170 &pmt2,
3171 CaPmtListManagement::Update,
3172 CaPmtCmdId::OkDescrambling,
3173 )
3174 .to_bytes();
3175 let sends_before1_query = count_apdu_on_session(&d, CA_SESSION, &expected1_query);
3176 let sends_before1_ok = count_apdu_on_session(&d, CA_SESSION, &expected1_ok);
3177 let sends_before2_query = count_apdu_on_session(&d, CA_SESSION, &expected2_query);
3178 let sends_before2_ok = count_apdu_on_session(&d, CA_SESSION, &expected2_ok);
3179
3180 clock.advance(Duration::from_secs(11));
3186 d.pump(Duration::from_secs(11)).unwrap();
3187 feed(&mut d, sb());
3188 feed(&mut d, sb());
3189
3190 assert_eq!(
3191 count_apdu_on_session(&d, CA_SESSION, &expected1_query),
3192 sends_before1_query + 1,
3193 "expected service 1546's `query` resent exactly once on the shared tick"
3194 );
3195 assert_eq!(
3196 count_apdu_on_session(&d, CA_SESSION, &expected1_ok),
3197 sends_before1_ok + 1,
3198 "expected service 1546's `ok_descrambling` resent exactly once on the shared tick"
3199 );
3200 assert_eq!(
3201 count_apdu_on_session(&d, CA_SESSION, &expected2_query),
3202 sends_before2_query + 1,
3203 "expected service 1547's `query` resent exactly once on the shared tick"
3204 );
3205 assert_eq!(
3206 count_apdu_on_session(&d, CA_SESSION, &expected2_ok),
3207 sends_before2_ok + 1,
3208 "expected service 1547's `ok_descrambling` resent exactly once on the shared tick"
3209 );
3210 }
3211
3212 #[test]
3216 fn requery_after_remove_resends_only_the_surviving_service() {
3217 use broadcast_common::Parse;
3218
3219 let clock = TestClock::new();
3220 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3221 d.set_requery_interval(Duration::from_secs(10));
3224 d.take_notifications();
3225
3226 let pmt1_bytes = build_ca_pmt_fixture(1546);
3227 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3228 d.add_service(&pmt1).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
3233 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3234 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3235 d.add_service(&pmt2).unwrap();
3236 d.device_mut().inbound.push_back(sb());
3237 clock.advance(Duration::from_millis(10));
3238 d.pump(Duration::from_millis(10)).unwrap();
3239
3240 d.remove_service(1546).unwrap();
3242 d.device_mut().inbound.push_back(sb());
3243 clock.advance(Duration::from_millis(10));
3244 d.pump(Duration::from_millis(10)).unwrap();
3245 d.take_notifications();
3246
3247 let expected_survivor_query =
3253 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3254 let expected_survivor_ok = build_ca_pmt(
3255 &pmt2,
3256 CaPmtListManagement::Update,
3257 CaPmtCmdId::OkDescrambling,
3258 )
3259 .to_bytes();
3260 let expected_removed_query =
3261 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
3262 let expected_removed_ok = build_ca_pmt(
3263 &pmt1,
3264 CaPmtListManagement::Update,
3265 CaPmtCmdId::OkDescrambling,
3266 )
3267 .to_bytes();
3268 let sends_survivor_query_before =
3269 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query);
3270 let sends_survivor_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok);
3271 let sends_removed_query_before =
3272 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query);
3273 let sends_removed_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok);
3274
3275 clock.advance(Duration::from_secs(11));
3276 d.pump(Duration::from_secs(11)).unwrap();
3277 feed(&mut d, sb());
3278
3279 assert_eq!(
3280 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query),
3281 sends_survivor_query_before + 1,
3282 "the surviving service's `query` must be re-sent"
3283 );
3284 assert_eq!(
3285 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok),
3286 sends_survivor_ok_before + 1,
3287 "the surviving service's `ok_descrambling` must be re-sent"
3288 );
3289 assert_eq!(
3290 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query),
3291 sends_removed_query_before,
3292 "the removed service's `query` must NOT be re-sent"
3293 );
3294 assert_eq!(
3295 count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok),
3296 sends_removed_ok_before,
3297 "the removed service's `ok_descrambling` must NOT be re-sent"
3298 );
3299 }
3300
3301 #[test]
3304 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
3305 use broadcast_common::Parse;
3306
3307 let mut d = driver_with_sessions();
3308 d.take_notifications();
3309
3310 let pmt1_bytes = build_ca_pmt_fixture(1546);
3314 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3315 d.add_service(&pmt1).unwrap();
3316 d.device_mut().inbound.push_back(sb());
3317 d.pump(Duration::from_millis(10)).unwrap();
3318
3319 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3320 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3321 d.add_service(&pmt2).unwrap();
3322 d.device_mut().inbound.push_back(sb());
3323 d.pump(Duration::from_millis(10)).unwrap();
3324
3325 d.remove_service(1546).unwrap();
3326 d.device_mut().inbound.push_back(sb());
3327 d.pump(Duration::from_millis(10)).unwrap();
3328
3329 let expected =
3333 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3334 assert_apdu_on_session(&d, CA_SESSION, &expected);
3335
3336 assert_eq!(
3337 d.descramble_pids(),
3338 &[Pid::new(0x0200), Pid::new(0x0201)],
3339 "1546's ES PIDs must be gone; 1547's must remain"
3340 );
3341 assert!(
3342 d.managed_ca().services().get(&1546).is_none(),
3343 "1546 must no longer be tracked"
3344 );
3345 assert!(
3346 d.managed_ca().services().get(&1547).is_some(),
3347 "1547 must remain tracked"
3348 );
3349 }
3350
3351 #[test]
3352 fn remove_service_of_untracked_program_is_a_no_op() {
3353 let mut d = driver_with_sessions();
3354 d.take_notifications();
3355
3356 let ops_before = d.device().ops.len();
3357 d.remove_service(0xFFFF).unwrap();
3358 assert_eq!(
3359 d.device().ops.len(),
3360 ops_before,
3361 "removing an untracked program must not send anything to the device"
3362 );
3363 assert!(
3364 d.managed_ca().services().is_empty(),
3365 "removing an untracked program must not disturb the (empty) managed set"
3366 );
3367 }
3368
3369 #[test]
3370 fn cam_removed_edge_clears_managed_state() {
3371 use broadcast_common::Parse;
3372 use dvb_ci::objects::ca_info::CaInfo;
3373 use dvb_si::tables::cat::CatSection;
3374
3375 let mut d = driver_with_sessions();
3376 d.take_notifications();
3377
3378 let pmt_bytes = build_ca_pmt_fixture(1546);
3379 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3380 d.add_service(&pmt).unwrap();
3381 d.device_mut().inbound.push_back(sb());
3382 d.pump(Duration::from_millis(10)).unwrap();
3383
3384 feed(
3387 &mut d,
3388 r_apdu(
3389 CA_SESSION,
3390 &ser(&CaInfo {
3391 length_form: dvb_ci::length::LengthForm::Minimal,
3392 ca_system_ids: vec![0x0648],
3393 }),
3394 ),
3395 );
3396 d.take_notifications();
3397 let mut descriptors = Vec::new();
3398 descriptors.extend_from_slice(&ca_descriptor(0x0648, Pid::new(0x1FF0)));
3399 let cat_bytes = build_cat_fixture(&descriptors);
3400 let cat = CatSection::parse(&cat_bytes).unwrap();
3401 d.set_cat(&cat).unwrap();
3402
3403 assert!(
3404 !d.managed_ca().services().is_empty(),
3405 "precondition: a service is tracked"
3406 );
3407 assert!(
3408 !d.descramble_pids().is_empty(),
3409 "precondition: descramble_pids populated"
3410 );
3411 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
3412
3413 d.device_mut().slot.module_present = false;
3415 d.pump(Duration::from_millis(10)).unwrap();
3416 let notes = d.take_notifications();
3417 assert!(
3418 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
3419 "expected CamRemoved, got {notes:?}"
3420 );
3421
3422 assert!(
3423 d.managed_ca().services().is_empty(),
3424 "services must be cleared on CamRemoved"
3425 );
3426 assert!(
3427 d.descramble_pids().is_empty(),
3428 "descramble_pids must be cleared on CamRemoved"
3429 );
3430 assert!(
3431 d.emm_pids().is_empty(),
3432 "emm_pids must be cleared on CamRemoved"
3433 );
3434 }
3435
3436 #[test]
3437 fn update_service_not_managed_returns_not_managed_and_sends_nothing() {
3438 use broadcast_common::Parse;
3439
3440 let mut d = driver_with_sessions();
3441 d.take_notifications();
3442
3443 let pmt_bytes = build_clear_pmt_fixture(9999);
3444 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3445
3446 let before = d.device().ops.len();
3447 let err = d
3448 .update_service(&pmt)
3449 .expect_err("unknown program must error");
3450 assert!(
3451 matches!(
3452 err,
3453 CaError::NotManaged {
3454 program_number: 9999
3455 }
3456 ),
3457 "got {err:?}"
3458 );
3459 assert_eq!(d.device().ops.len(), before, "must send nothing");
3460 assert!(d.managed_ca().services().is_empty());
3461 }
3462
3463 #[test]
3464 fn update_service_no_ca_descriptor_leaves_record_unchanged() {
3465 use broadcast_common::Parse;
3466
3467 let mut d = driver_with_sessions();
3468 d.take_notifications();
3469
3470 let pmt_bytes = build_ca_pmt_fixture(1546);
3471 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3472 d.add_service(&pmt).unwrap();
3473
3474 let svc_before = d.managed_ca().services().get(&1546).unwrap().clone();
3475
3476 let clear_bytes = build_clear_pmt_fixture(1546);
3477 let clear_pmt = PmtSection::parse(&clear_bytes).unwrap();
3478 let err = d
3479 .update_service(&clear_pmt)
3480 .expect_err("CA-free PMT must error");
3481 assert!(
3482 matches!(
3483 err,
3484 CaError::NoCaDescriptor {
3485 program_number: 1546
3486 }
3487 ),
3488 "got {err:?}"
3489 );
3490
3491 let svc_after = d.managed_ca().services().get(&1546).unwrap();
3492 assert_eq!(svc_before, *svc_after, "record must be unchanged");
3493 }
3494
3495 #[test]
3501 fn update_service_sends_one_update_ca_pmt_with_the_new_pids_and_replaces_only_that_record() {
3502 use broadcast_common::Parse;
3503
3504 let mut d = driver_with_sessions();
3505 d.take_notifications();
3506
3507 let a_bytes = build_ca_pmt_fixture(1546);
3508 let a = PmtSection::parse(&a_bytes).unwrap();
3509 d.add_service(&a).unwrap();
3510 d.device_mut().inbound.push_back(sb());
3511 d.pump(Duration::from_millis(10)).unwrap();
3512
3513 let b_bytes = build_ca_pmt_fixture_dedicated_pcr(1547);
3514 let b = PmtSection::parse(&b_bytes).unwrap();
3515 d.add_service(&b).unwrap();
3516 d.device_mut().inbound.push_back(sb());
3517 d.pump(Duration::from_millis(10)).unwrap();
3518
3519 let a_old_record = d.managed_ca().services().get(&1546).unwrap().clone();
3520 let b_record_before = d.managed_ca().services().get(&1547).unwrap().clone();
3521 assert_eq!(
3522 a_old_record.ca_pids,
3523 vec![Pid::new(0x0064), Pid::new(0x0065)],
3524 "fixture sanity: A starts on the 0x0064/0x0065 ECM PIDs"
3525 );
3526
3527 let a2_bytes = build_ca_pmt_fixture_distinct_pids(1546);
3529 let a2 = PmtSection::parse(&a2_bytes).unwrap();
3530 let update =
3531 build_ca_pmt(&a2, CaPmtListManagement::Update, CaPmtCmdId::OkDescrambling).to_bytes();
3532 let writes_before = d.device().ops.len();
3533
3534 d.update_service(&a2).unwrap();
3535 d.device_mut().inbound.push_back(sb());
3536 d.pump(Duration::from_millis(10)).unwrap();
3537
3538 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &update), 1);
3540 let new_ca_pmt_writes = d.device().ops[writes_before..]
3541 .iter()
3542 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == [0x9F, 0x80, 0x32])))
3543 .count();
3544 assert_eq!(new_ca_pmt_writes, 1, "exactly one ca_pmt APDU may be sent");
3545
3546 let new_prog_ca = ca_descriptor(0x0500, Pid::new(0x0074));
3548 let old_prog_ca = ca_descriptor(0x0500, Pid::new(0x0064));
3549 assert!(update.windows(new_prog_ca.len()).any(|x| x == new_prog_ca));
3550 assert!(!update.windows(old_prog_ca.len()).any(|x| x == old_prog_ca));
3551
3552 let a_new_record = d.managed_ca().services().get(&1546).unwrap();
3554 assert_eq!(
3555 a_new_record.ca_pids,
3556 vec![Pid::new(0x0074), Pid::new(0x0075)]
3557 );
3558 assert_eq!(a_new_record.built_ca_pmt, update);
3559 assert_ne!(a_new_record.built_ca_pmt, a_old_record.built_ca_pmt);
3560 assert_ne!(a_new_record.pmt_raw, a_old_record.pmt_raw);
3561 assert_eq!(
3562 a_new_record.pmt_raw, a2_bytes,
3563 "raw PMT must be the new section"
3564 );
3565
3566 assert_eq!(
3568 *d.managed_ca().services().get(&1547).unwrap(),
3569 b_record_before
3570 );
3571 assert_eq!(d.managed_ca().services().len(), 2);
3572 }
3573
3574 #[test]
3577 fn remove_service_after_update_service_uses_the_new_pmt() {
3578 use broadcast_common::Parse;
3579
3580 let mut d = driver_with_sessions();
3581 d.take_notifications();
3582
3583 let a_bytes = build_ca_pmt_fixture(1546);
3584 let a = PmtSection::parse(&a_bytes).unwrap();
3585 d.add_service(&a).unwrap();
3586 d.device_mut().inbound.push_back(sb());
3587 d.pump(Duration::from_millis(10)).unwrap();
3588 let a2_bytes = build_ca_pmt_fixture_distinct_pids(1546);
3589 let a2 = PmtSection::parse(&a2_bytes).unwrap();
3590 d.update_service(&a2).unwrap();
3591 d.device_mut().inbound.push_back(sb());
3592 d.pump(Duration::from_millis(10)).unwrap();
3593
3594 d.remove_service(1546).unwrap();
3595 d.device_mut().inbound.push_back(sb());
3596 d.pump(Duration::from_millis(10)).unwrap();
3597
3598 let removal_new =
3599 build_ca_pmt(&a2, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3600 let removal_old =
3601 build_ca_pmt(&a, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3602 assert_ne!(
3603 removal_new, removal_old,
3604 "fixture sanity: the two PMTs differ"
3605 );
3606 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &removal_new), 1);
3607 assert_eq!(count_apdu_on_session(&d, CA_SESSION, &removal_old), 0);
3608 assert!(d.managed_ca().services().is_empty());
3609 }
3610
3611 #[test]
3614 fn update_service_filters_caids_like_add_service() {
3615 use broadcast_common::Parse;
3616
3617 let mut d = driver_with_sessions();
3618 d.take_notifications();
3619
3620 let two_caids = build_ca_pmt_fixture_two_caids(1546);
3621 let pmt = PmtSection::parse(&two_caids).unwrap();
3622 d.add_service(&pmt).unwrap();
3623 let added = d
3624 .managed_ca()
3625 .services()
3626 .get(&1546)
3627 .unwrap()
3628 .built_ca_pmt
3629 .clone();
3630
3631 d.update_service(&pmt).unwrap();
3632 let updated = d
3633 .managed_ca()
3634 .services()
3635 .get(&1546)
3636 .unwrap()
3637 .built_ca_pmt
3638 .clone();
3639
3640 assert_eq!(added.len(), updated.len());
3642 let diff: Vec<usize> = (0..added.len())
3643 .filter(|&i| added[i] != updated[i])
3644 .collect();
3645 assert_eq!(
3646 diff.len(),
3647 1,
3648 "only list_management may differ, got offsets {diff:?}"
3649 );
3650 assert_eq!(updated[diff[0]], 0x05, "list_management must be update");
3651 }
3652
3653 #[test]
3654 fn update_service_serialize_failure_leaves_record_unchanged() {
3655 let mut d = driver_with_sessions();
3656 let pmt_bytes = build_ca_pmt_fixture(1);
3658 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3659 d.add_service(&pmt).unwrap();
3660
3661 let svc_before = d.managed_ca().services().get(&1).unwrap().clone();
3662
3663 let corrupt = corrupt_ca_section_ctor();
3664 let err = d
3665 .update_service(&corrupt)
3666 .expect_err("unencodable PMT must error");
3667 assert!(
3668 matches!(err, CaError::Serialize(_)),
3669 "expected CaError::Serialize, got {err:?}"
3670 );
3671
3672 let svc_after = d.managed_ca().services().get(&1).unwrap();
3673 assert_eq!(svc_before, *svc_after, "record must be unchanged");
3674 }
3675
3676 #[test]
3677 fn add_service_without_an_open_ca_session_errors_and_records_nothing() {
3678 let mut d = Driver::new(MockCaDevice::new([]));
3679 let pmt_bytes = build_ca_pmt_fixture(1546);
3680 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3681 let result = d.add_service(&pmt);
3682 let wrote_ca_pmt = d.device().ops.iter().any(|op| match op {
3683 DeviceOp::Write(w) => w.windows(3).any(|x| x == [0x9F, 0x80, 0x32]),
3684 _ => false,
3685 });
3686 assert!(!wrote_ca_pmt, "precondition: nothing can have been sent");
3687 assert!(matches!(result, Err(CaError::NotSent(_))), "{result:?}");
3688 assert!(d.managed_ca().services().is_empty());
3689 }
3690
3691 #[test]
3692 fn setup_timeout_is_followed_by_a_new_create_t_c() {
3693 let mut d = Driver::new(MockCaDevice::new([]));
3694 let clock = TestClock::new();
3695 d = d.with_clock(clock.as_fn());
3696 d.init().unwrap();
3697 for _ in 0..60 {
3698 clock.advance(Duration::from_millis(100));
3699 d.pump(Duration::from_millis(100)).unwrap();
3700 }
3701 let create_t_c = d
3702 .device()
3703 .ops
3704 .iter()
3705 .filter(|op| matches!(op, DeviceOp::Write(w) if w.first() == Some(&tags::CREATE_T_C)))
3706 .count();
3707 assert!(
3708 create_t_c > 1,
3709 "6 s of a silent CAM produced {create_t_c} Create_T_C"
3710 );
3711 }
3712
3713 #[test]
3714 fn readable_pumps_still_advance_the_requery_timer() {
3715 let clock = TestClock::new();
3716 let mut d = driver_with_sessions().with_clock(clock.as_fn());
3717 let pmt_bytes = build_ca_pmt_fixture(1546);
3718 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3719 d.add_service(&pmt).unwrap();
3720 d.set_requery_interval(Duration::from_secs(1));
3721 d.pump(Duration::from_millis(10)).unwrap();
3722 let ca_pmt_writes = |d: &Driver<MockCaDevice>| {
3723 d.device()
3724 .ops
3725 .iter()
3726 .filter(|op| matches!(op, DeviceOp::Write(w) if w.windows(3).any(|x| x == [0x9F, 0x80, 0x32])))
3727 .count()
3728 };
3729 let before = ca_pmt_writes(&d);
3730 for _ in 0..8 {
3731 clock.advance(Duration::from_millis(500));
3732 d.device_mut().inbound.push_back(sb());
3733 d.pump(Duration::from_millis(10)).unwrap();
3734 }
3735 assert!(
3736 ca_pmt_writes(&d) > before,
3737 "4 s passed with a 1 s requery interval but no re-query was sent"
3738 );
3739 }
3740}