1use std::collections::BTreeSet;
7use std::io;
8use std::time::{Duration, Instant};
9
10use broadcast_common::Serialize;
11use dvb_ci::builder::{build_ca_pmt, build_ca_pmt_for_caids};
12use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
13use dvb_si::tables::cat::CatSection;
14use dvb_si::tables::pmt::PmtSection;
15
16use broadcast_common::Parse;
17
18use crate::device::{CaDevice, SlotInfo};
19use crate::event::{Action, Event, HostRequest, HotPlug, MmiEvent, Notification};
20use crate::managed::{self, CaError, ManagedCa};
21use crate::stack::CiStack;
22
23const MMI_CARD_ABSENT_KEYWORDS: &[&str] = &[
28 "no card",
29 "insert card",
30 "insert smart card",
31 "card removed",
32 "please insert",
33];
34
35const MMI_CARD_PRESENT_KEYWORDS: &[&str] = &["entitlement", "card valid", "subscription active"];
40
41type Clock = Box<dyn Fn() -> Instant>;
46
47fn default_clock() -> Clock {
48 Box::new(Instant::now)
49}
50
51pub struct Driver<D: CaDevice> {
53 device: D,
54 stack: CiStack,
55 notifications: Vec<Notification>,
56 next_timer: Option<Duration>,
58 buf: Vec<u8>,
60 last_slot: Option<SlotInfo>,
67 last_caids: Option<BTreeSet<u16>>,
70 last_descrambling_ok: Option<bool>,
73 last_mmi_card_present: Option<bool>,
79 managed: ManagedCa,
82 last_pump: Option<Instant>,
87 clock: Clock,
89 requery_pending: Option<CaError>,
93 settle_until: Option<Instant>,
97 deferred_writes: Vec<Vec<u8>>,
100}
101
102impl<D: CaDevice> Driver<D> {
103 #[must_use]
105 pub fn new(device: D) -> Self {
106 Self {
107 device,
108 stack: CiStack::new(),
109 notifications: Vec::new(),
110 next_timer: None,
111 buf: vec![0u8; 4096],
112 last_slot: None,
113 last_caids: None,
114 last_descrambling_ok: None,
115 last_mmi_card_present: None,
116 managed: ManagedCa::new(),
117 last_pump: None,
118 clock: default_clock(),
119 requery_pending: None,
120 settle_until: None,
121 deferred_writes: Vec::new(),
122 }
123 }
124
125 #[must_use]
129 pub fn with_clock(mut self, clock: impl Fn() -> Instant + 'static) -> Self {
130 self.clock = Box::new(clock);
131 self
132 }
133
134 pub fn managed_ca(&self) -> &ManagedCa {
137 &self.managed
138 }
139
140 pub fn device(&self) -> &D {
142 &self.device
143 }
144
145 pub fn device_mut(&mut self) -> &mut D {
148 &mut self.device
149 }
150
151 pub fn next_timer(&self) -> Option<Duration> {
153 self.next_timer
154 }
155
156 pub fn take_notifications(&mut self) -> Vec<Notification> {
158 core::mem::take(&mut self.notifications)
159 }
160
161 pub fn init(&mut self) -> io::Result<()> {
163 let actions = self.stack.handle(Event::Host(HostRequest::Init));
164 self.run(actions)
165 }
166
167 pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
170 let actions = self
171 .stack
172 .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
173 self.run(actions)
174 }
175
176 pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
182 let actions = self
183 .stack
184 .handle(Event::Host(HostRequest::Descramble(pmt_section)));
185 self.run(actions)
186 }
187
188 pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
191 let actions = self
192 .stack
193 .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
194 self.run(actions)
195 }
196
197 pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
200 let actions = self
201 .stack
202 .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
203 self.run(actions)
204 }
205
206 pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
209 let actions = self
210 .stack
211 .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
212 self.run(actions)
213 }
214
215 fn build_ca_pmt_filtered(
227 &self,
228 pmt: &PmtSection<'_>,
229 list_management: CaPmtListManagement,
230 cmd_id: CaPmtCmdId,
231 ) -> dvb_ci::builder::CaPmtBuilt {
232 let cam_caids = self.managed.cam_caids();
233 if cam_caids.is_empty() {
234 build_ca_pmt(pmt, list_management, cmd_id)
235 } else {
236 let allowed: Vec<u16> = cam_caids.iter().copied().collect();
237 build_ca_pmt_for_caids(pmt, &allowed, list_management, cmd_id)
238 }
239 }
240
241 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
261 if !managed::pmt_has_ca(pmt) {
262 return Err(CaError::NoCaDescriptor {
263 program_number: pmt.program_number,
264 });
265 }
266 let list_management = if self.managed.is_empty() {
267 CaPmtListManagement::Only
268 } else {
269 CaPmtListManagement::Add
270 };
271 let cmd_id = CaPmtCmdId::OkDescrambling;
272 let built_bytes = self
279 .build_ca_pmt_filtered(pmt, list_management, cmd_id)
280 .try_to_bytes()?;
281 let mut pmt_raw = vec![0u8; pmt.serialized_len()];
289 let n = pmt.serialize_into(&mut pmt_raw)?;
290 pmt_raw.truncate(n);
291 self.send_ca_pmt(&built_bytes)?;
292 self.managed.record(
293 pmt.program_number,
294 managed::service_of(pmt, cmd_id, built_bytes, pmt_raw),
295 );
296 Ok(())
297 }
298
299 pub fn remove_service(&mut self, program_number: u16) -> Result<(), CaError> {
313 let raw = self
314 .managed
315 .services()
316 .get(&program_number)
317 .map(|s| s.pmt_raw.clone());
318 let Some(raw) = raw else {
319 return Ok(());
320 };
321 self.remove_program(&raw)?;
322 self.managed.remove(program_number);
323 Ok(())
324 }
325
326 pub fn set_requery_interval(&mut self, interval: Duration) {
335 self.managed.set_requery_interval(interval);
336 }
337
338 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
353 let entries = cat.ca_descriptors().map_err(CaError::Cat)?;
354 self.managed.set_cat(&entries);
355 Ok(())
356 }
357
358 #[must_use]
362 pub fn emm_pids(&self) -> &[u16] {
363 self.managed.emm_pids()
364 }
365
366 #[must_use]
369 pub fn descramble_pids(&self) -> &[u16] {
370 self.managed.descramble_pids()
371 }
372
373 #[must_use]
378 pub fn ca_pids(&self) -> &[u16] {
379 self.managed.ca_pids()
380 }
381
382 #[must_use]
390 pub fn required_pids(&self) -> Vec<u16> {
391 self.managed.required_pids()
392 }
393
394 pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
396 let actions = self
397 .stack
398 .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
399 self.run(actions)
400 }
401
402 pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
404 let actions = self
405 .stack
406 .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
407 self.run(actions)
408 }
409
410 pub fn mmi_cancel(&mut self) -> io::Result<()> {
412 let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
413 self.run(actions)
414 }
415
416 pub fn enter_menu(&mut self) -> io::Result<()> {
419 let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
420 self.run(actions)
421 }
422
423 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
432 let now = (self.clock)();
443 let mut elapsed = self
444 .last_pump
445 .map_or(timeout, |prev| now.saturating_duration_since(prev));
446 self.last_pump = Some(now);
447
448 if let Some(until) = self.settle_until {
453 if now < until {
454 self.run(vec![Action::QuerySlot])?;
455 return Ok(false);
456 }
457 self.settle_until = None;
458 for w in std::mem::take(&mut self.deferred_writes) {
459 self.device.write(&w)?;
460 }
461 elapsed = Duration::ZERO;
464 }
465
466 self.run(vec![Action::QuerySlot])?;
467 if self.device.poll(timeout)? {
468 let n = self.device.read(&mut self.buf)?;
469 if n > 0 {
470 let actions = self.stack.handle(Event::Readable(&self.buf[..n]));
474 self.run(actions)?;
475 return Ok(true);
476 }
477 }
478 let actions = self.stack.handle(Event::Tick { elapsed });
479 self.run(actions)?;
480 self.requery_tick(elapsed)?;
481 Ok(false)
482 }
483
484 fn requery_tick(&mut self, elapsed: Duration) -> io::Result<()> {
528 if let Some(err) = self.requery_pending.take() {
532 return Err(io::Error::other(err));
533 }
534 if !self.managed.tick(elapsed) {
535 return Ok(());
536 }
537 let mut ca_pmts: Vec<(Vec<u8>, Vec<u8>)> =
538 Vec::with_capacity(self.managed.services().len());
539 for s in self.managed.services().values() {
540 let outcome: Result<(Vec<u8>, Vec<u8>), CaError> = (|| {
545 let pmt = PmtSection::parse(&s.pmt_raw).map_err(CaError::PmtParse)?;
546 let query = self
547 .build_ca_pmt_filtered(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query)
548 .try_to_bytes()?;
549 let ok_descrambling = self
550 .build_ca_pmt_filtered(
551 &pmt,
552 CaPmtListManagement::Update,
553 CaPmtCmdId::OkDescrambling,
554 )
555 .try_to_bytes()?;
556 Ok((query, ok_descrambling))
557 })();
558 match outcome {
559 Ok(pair) => ca_pmts.push(pair),
560 Err(err) => {
561 self.requery_pending = Some(err);
565 }
566 }
567 }
568 for (query, ok_descrambling) in ca_pmts {
569 self.send_ca_pmt(&query)?;
570 self.send_ca_pmt(&ok_descrambling)?;
571 }
572 if let Some(err) = self.requery_pending.take() {
573 return Err(io::Error::other(err));
574 }
575 Ok(())
576 }
577
578 pub fn pump_with<F: FnMut(&Notification)>(
587 &mut self,
588 timeout: Duration,
589 mut handler: F,
590 ) -> io::Result<bool> {
591 let progressed = self.pump(timeout)?;
592 for n in self.take_notifications() {
593 handler(&n);
594 }
595 Ok(progressed)
596 }
597
598 pub fn pump_hotplug<F: FnMut(HotPlug)>(
602 &mut self,
603 timeout: Duration,
604 mut handler: F,
605 ) -> io::Result<bool> {
606 self.pump_with(timeout, |n| {
607 if let Some(h) = n.hotplug() {
608 handler(h);
609 }
610 })
611 }
612
613 fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
615 for action in actions {
616 match action {
617 Action::Write(bytes) => {
618 if self.settle_until.is_some() {
619 self.deferred_writes.push(bytes);
620 } else {
621 self.device.write(&bytes)?;
622 }
623 }
624 Action::Reset => {
625 self.device.reset()?;
626 self.deferred_writes.clear();
629 let settle = self.device.reset_settle();
630 self.settle_until = (!settle.is_zero()).then(|| (self.clock)() + settle);
631 }
632 Action::QuerySlot => {
633 let info = self.device.slot_info()?;
634 self.handle_slot_info(info)?;
635 }
636 Action::SetTimer { after } => self.next_timer = Some(after),
637 Action::Notify(n) => {
638 let inferred = self.infer_card(&n);
639 self.notifications.push(n);
640 self.notifications.extend(inferred);
641 }
642 }
643 }
644 Ok(())
645 }
646
647 fn handle_slot_info(&mut self, info: SlotInfo) -> io::Result<()> {
654 let prev = self.last_slot.replace(info);
655 match prev {
656 Some(prev) if !prev.module_present && info.module_present => {
657 self.notifications
658 .push(Notification::HotPlug(HotPlug::CamPresent));
659 self.reset_module_state();
660 let actions = self.stack.handle(Event::Host(HostRequest::Init));
664 self.run(actions)?;
665 }
666 Some(prev) if prev.module_present && !info.module_present => {
667 self.notifications
668 .push(Notification::HotPlug(HotPlug::CamRemoved));
669 self.reset_module_state();
670 }
671 _ => {}
672 }
673 Ok(())
674 }
675
676 fn reset_module_state(&mut self) {
686 self.stack = CiStack::new();
687 self.settle_until = None;
688 self.deferred_writes.clear();
689 self.next_timer = None;
690 self.last_caids = None;
691 self.last_descrambling_ok = None;
692 self.last_mmi_card_present = None;
693 self.managed.clear();
694 }
695
696 fn infer_card(&mut self, note: &Notification) -> Vec<Notification> {
703 match note {
704 Notification::CaInfo { ca_system_ids } => {
705 let new_set: BTreeSet<u16> = ca_system_ids.iter().copied().collect();
706 let mut out = Vec::new();
707 if let Some(prev) = &self.last_caids {
708 if prev.is_empty() && !new_set.is_empty() {
709 out.push(Notification::HotPlug(HotPlug::CardInserted));
710 } else if !prev.is_empty() && new_set.is_empty() {
711 out.push(Notification::HotPlug(HotPlug::CardRemoved));
712 } else if !prev.is_empty() && !new_set.is_empty() && *prev != new_set {
713 out.push(Notification::HotPlug(HotPlug::CardChanged));
714 }
715 }
716 self.managed.set_cam_caids(new_set.clone());
720 self.last_caids = Some(new_set);
721 out
722 }
723 Notification::CaPmtReply {
724 program_number,
725 ca_enable,
726 descrambling_ok,
727 } => {
728 let mut out = Vec::new();
729 if let Some(prev) = self.last_descrambling_ok {
730 if !prev && *descrambling_ok {
731 out.push(Notification::HotPlug(HotPlug::CardInserted));
732 } else if prev && !*descrambling_ok {
733 out.push(Notification::HotPlug(HotPlug::CardRemoved));
734 }
735 }
736 self.last_descrambling_ok = Some(*descrambling_ok);
737 if let Some((v, ok)) =
741 self.managed
742 .record_reply(*program_number, *ca_enable, *descrambling_ok)
743 {
744 out.push(Notification::Entitlement {
745 program_number: *program_number,
746 ca_enable: v,
747 descrambling_ok: ok,
748 });
749 }
750 out
751 }
752 Notification::Mmi(ev) => {
753 let present = Self::mmi_text(ev).and_then(|text| {
754 let lower = text.to_lowercase();
755 if MMI_CARD_ABSENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
756 Some(false)
757 } else if MMI_CARD_PRESENT_KEYWORDS.iter().any(|k| lower.contains(k)) {
758 Some(true)
759 } else {
760 None
761 }
762 });
763 match present {
764 Some(p) if self.last_mmi_card_present != Some(p) => {
766 self.last_mmi_card_present = Some(p);
767 vec![Notification::HotPlug(if p {
768 HotPlug::CardInserted
769 } else {
770 HotPlug::CardRemoved
771 })]
772 }
773 _ => Vec::new(),
774 }
775 }
776 _ => Vec::new(),
777 }
778 }
779
780 fn mmi_text(ev: &MmiEvent) -> Option<String> {
784 match ev {
785 MmiEvent::Menu(m) | MmiEvent::List(m) => {
786 let mut s = format!("{} {} {}", m.title, m.subtitle, m.bottom);
787 for choice in &m.choices {
788 s.push(' ');
789 s.push_str(choice);
790 }
791 Some(s)
792 }
793 MmiEvent::Enquiry { prompt, .. } => Some(prompt.clone()),
794 MmiEvent::Close => None,
795 }
796 }
797}
798
799#[cfg(test)]
800pub(crate) mod tests {
801 use super::*;
802 use crate::device::{DeviceOp, MockCaDevice};
803 use crate::event::{HostControlEvent, HotPlug, Notification};
804 use broadcast_common::Serialize;
805 use dvb_ci::tpdu::tags;
806
807 pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
808 let mut b = vec![0u8; s.serialized_len()];
809 match s.serialize_into(&mut b) {
810 Ok(n) => b.truncate(n),
811 Err(_) => b.clear(),
812 }
813 b
814 }
815
816 #[derive(Clone)]
822 pub(crate) struct TestClock(std::rc::Rc<std::cell::Cell<Instant>>);
823
824 impl TestClock {
825 pub(crate) fn new() -> Self {
826 Self(std::rc::Rc::new(std::cell::Cell::new(Instant::now())))
827 }
828
829 pub(crate) fn advance(&self, d: Duration) {
831 self.0.set(self.0.get() + d);
832 }
833
834 pub(crate) fn as_fn(&self) -> impl Fn() -> Instant + 'static {
836 let cell = self.0.clone();
837 move || cell.get()
838 }
839 }
840
841 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
844 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
845 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
846 v.extend_from_slice(spdu);
847 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
848 v
849 }
850
851 pub(crate) fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
854 use dvb_ci::spdu::SessionNumber;
855 let mut spdu = ser(&SessionNumber { session_nb });
856 spdu.extend_from_slice(apdu);
857 r_data(1, &spdu)
858 }
859
860 pub(crate) fn sb() -> Vec<u8> {
863 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
864 vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
865 }
866
867 pub(crate) fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
870 d.device_mut().inbound.push_back(frame);
871 d.pump(Duration::from_millis(10)).unwrap();
872 for _ in 0..8 {
873 d.device_mut().inbound.push_back(sb());
874 d.pump(Duration::from_millis(10)).unwrap();
875 }
876 }
877
878 pub(crate) fn driver_with_sessions() -> Driver<MockCaDevice> {
882 use dvb_ci::objects::resource_manager::Profile;
883 use dvb_ci::resource::{
884 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
885 RESOURCE_MANAGER,
886 };
887 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
888
889 let mut d = Driver::new(MockCaDevice::new([]));
890 d.init().unwrap();
891 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
893 feed(
895 &mut d,
896 r_data(
897 1,
898 &ser(&OpenSessionRequest {
899 resource: RESOURCE_MANAGER,
900 }),
901 ),
902 );
903 feed(
906 &mut d,
907 r_apdu(
908 1,
909 &ser(&Profile {
910 resources: vec![
911 APPLICATION_INFORMATION,
912 CONDITIONAL_ACCESS_SUPPORT,
913 MMI,
914 HOST_CONTROL,
915 ],
916 }),
917 ),
918 );
919 for (nb, res) in [
921 (2u16, APPLICATION_INFORMATION),
922 (3, CONDITIONAL_ACCESS_SUPPORT),
923 (4, MMI),
924 (5, HOST_CONTROL),
925 ] {
926 feed(
927 &mut d,
928 r_data(
929 1,
930 &ser(&CreateSessionResponse {
931 status: SessionStatus::Ok,
932 resource: res,
933 session_nb: nb,
934 }),
935 ),
936 );
937 }
938 d
939 }
940
941 const RM_SESSION: u16 = 1;
945 pub(crate) const CA_SESSION: u16 = 3;
946 const MMI_SESSION: u16 = 4;
947 const HOST_CONTROL_SESSION: u16 = 5;
948
949 #[test]
950 fn host_control_tune_apdu_surfaces_notification_via_driver() {
951 use dvb_ci::objects::host_control::Tune;
952
953 let mut d = driver_with_sessions();
954 let hc_nb = HOST_CONTROL_SESSION;
955 d.take_notifications(); let tune = Tune {
959 network_id: 0x1122,
960 original_network_id: 0x3344,
961 transport_stream_id: 0x5566,
962 service_id: 0x7788,
963 };
964 feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
965
966 let notes = d.take_notifications();
968 assert!(
969 notes.contains(&Notification::HostControl(HostControlEvent::Tune {
970 network_id: 0x1122,
971 original_network_id: 0x3344,
972 transport_stream_id: 0x5566,
973 service_id: 0x7788,
974 })),
975 "expected HostControl(Tune) notification, got {notes:?}"
976 );
977 }
978
979 #[test]
982 fn padded_tune_from_the_cam_surfaces_an_error_notification() {
983 let mut d = driver_with_sessions();
984 d.take_notifications();
985 let padded = [
987 0x9F, 0x84, 0x00, 0x09, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xEE,
988 ];
989 feed(&mut d, r_apdu(HOST_CONTROL_SESSION, &padded));
990 let notes = d.take_notifications();
991 assert!(
992 notes.iter().any(
993 |n| matches!(n, Notification::Error { detail } if detail.contains("trailing bytes"))
994 ),
995 "expected an Error notification naming the trailing bytes, got {notes:?}"
996 );
997 assert!(
998 !notes
999 .iter()
1000 .any(|n| matches!(n, Notification::HostControl(_))),
1001 "a refused tune must not also surface as a HostControl event: {notes:?}"
1002 );
1003 }
1004
1005 #[test]
1008 fn malformed_mmi_apdu_from_the_cam_surfaces_an_error_notification() {
1009 let mut d = driver_with_sessions();
1010 d.take_notifications();
1011 feed(&mut d, r_apdu(MMI_SESSION, &[0x9F, 0x88, 0x07, 0x01, 0x00]));
1013 let notes = d.take_notifications();
1014 assert!(
1015 notes
1016 .iter()
1017 .any(|n| matches!(n, Notification::Error { .. })),
1018 "expected an Error notification, got {notes:?}"
1019 );
1020 }
1021
1022 #[test]
1023 fn profile_reply_advertises_host_control() {
1024 use broadcast_common::Parse;
1025 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
1026 use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
1027
1028 let mut d = Driver::new(MockCaDevice::new([]));
1029 d.init().unwrap();
1030 feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1031 feed(
1033 &mut d,
1034 r_data(
1035 1,
1036 &ser(&dvb_ci::spdu::OpenSessionRequest {
1037 resource: RESOURCE_MANAGER,
1038 }),
1039 ),
1040 );
1041 feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq)));
1043
1044 let want = dvb_ci::tag::PROFILE.to_bytes();
1047 let found = d.device().ops.iter().any(|op| {
1048 if let DeviceOp::Write(w) = op
1049 && let Some(pos) = w.windows(3).position(|x| x == want)
1050 && let Ok(p) = Profile::parse(&w[pos..])
1051 {
1052 return p.resources.contains(&HOST_CONTROL);
1053 }
1054 false
1055 });
1056 assert!(found, "profile reply must advertise HOST_CONTROL");
1057 }
1058
1059 #[test]
1060 fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
1061 use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
1062
1063 let mut d = driver_with_sessions();
1064 let mmi_nb = MMI_SESSION;
1065
1066 d.mmi_menu_answer(2).unwrap();
1069 d.device_mut().inbound.push_back(sb());
1070 d.pump(Duration::from_millis(10)).unwrap();
1071 assert_apdu_on_session(&d, mmi_nb, &ser(&MenuAnsw { choice_ref: 2 }));
1072
1073 d.mmi_enquiry_answer(b"1234").unwrap();
1075 d.device_mut().inbound.push_back(sb());
1076 d.pump(Duration::from_millis(10)).unwrap();
1077 assert_apdu_on_session(
1078 &d,
1079 mmi_nb,
1080 &ser(&Answ {
1081 answ_id: AnswId::Answer,
1082 text_chars: b"1234",
1083 }),
1084 );
1085 }
1086
1087 fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
1090 use dvb_ci::spdu::SessionNumber;
1091 let mut want = ser(&SessionNumber { session_nb });
1092 want.extend_from_slice(apdu);
1093 let hit = d.device().ops.iter().any(|op| match op {
1094 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1095 _ => false,
1096 });
1097 assert!(
1098 hit,
1099 "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
1100 );
1101 }
1102
1103 fn count_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) -> usize {
1107 use dvb_ci::spdu::SessionNumber;
1108 let mut want = ser(&SessionNumber { session_nb });
1109 want.extend_from_slice(apdu);
1110 d.device()
1111 .ops
1112 .iter()
1113 .filter(|op| match op {
1114 DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
1115 _ => false,
1116 })
1117 .count()
1118 }
1119
1120 struct SettleDevice {
1123 inner: MockCaDevice,
1124 settle: Duration,
1125 }
1126
1127 impl CaDevice for SettleDevice {
1128 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1129 self.inner.read(buf)
1130 }
1131 fn write(&mut self, buf: &[u8]) -> io::Result<()> {
1132 self.inner.write(buf)
1133 }
1134 fn reset(&mut self) -> io::Result<()> {
1135 self.inner.reset()
1136 }
1137 fn reset_settle(&self) -> Duration {
1138 self.settle
1139 }
1140 fn slot_info(&mut self) -> io::Result<SlotInfo> {
1141 self.inner.slot_info()
1142 }
1143 fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
1144 self.inner.poll(timeout)
1145 }
1146 }
1147
1148 const SETTLE: Duration = Duration::from_secs(3);
1149
1150 fn settling_driver(clock: &TestClock) -> Driver<SettleDevice> {
1151 Driver::new(SettleDevice {
1152 inner: MockCaDevice::new([]),
1153 settle: SETTLE,
1154 })
1155 .with_clock(clock.as_fn())
1156 }
1157
1158 fn create_tc_writes(d: &Driver<SettleDevice>) -> usize {
1159 d.device()
1160 .inner
1161 .ops
1162 .iter()
1163 .filter(|o| matches!(o, DeviceOp::Write(w) if w[0] == tags::CREATE_T_C))
1164 .count()
1165 }
1166
1167 #[test]
1170 fn reset_does_not_block_pump() {
1171 let clock = TestClock::new();
1172 let mut d = settling_driver(&clock);
1173 let t = std::time::Instant::now();
1174 d.init().unwrap();
1175 d.pump(Duration::from_millis(10)).unwrap();
1176 assert!(t.elapsed() < Duration::from_secs(1), "reset/pump blocked");
1177 assert_eq!(d.device().inner.ops[0], DeviceOp::Reset);
1178 }
1179
1180 #[test]
1184 fn no_transport_traffic_until_settle_elapsed() {
1185 let clock = TestClock::new();
1186 let mut d = settling_driver(&clock);
1187 d.device_mut()
1188 .inner
1189 .inbound
1190 .push_back(vec![tags::C_T_C_REPLY, 0x01, 0x01]);
1191 d.init().unwrap();
1192 for _ in 0..5 {
1193 clock.advance(Duration::from_millis(500));
1194 d.pump(Duration::from_millis(10)).unwrap();
1195 }
1196 assert_eq!(create_tc_writes(&d), 0);
1198 assert_eq!(
1199 d.device().inner.inbound.len(),
1200 1,
1201 "frame read while settling"
1202 );
1203
1204 clock.advance(Duration::from_millis(600));
1205 d.pump(Duration::from_millis(10)).unwrap();
1206 assert_eq!(create_tc_writes(&d), 1);
1207
1208 let mut z = Driver::new(MockCaDevice::new([]));
1209 z.init().unwrap();
1210 assert_eq!(
1211 d.device().inner.written()[..3],
1212 z.device().written()[..3],
1213 "post-settle Create_T_C differs from the zero-settle sequence"
1214 );
1215 assert!(d.device().inner.inbound.is_empty());
1218 }
1219
1220 #[test]
1223 fn second_reset_restarts_settle() {
1224 let clock = TestClock::new();
1225 let mut d = settling_driver(&clock);
1226 d.init().unwrap();
1227 clock.advance(Duration::from_secs(2));
1228 d.pump(Duration::from_millis(10)).unwrap();
1229 d.init().unwrap(); clock.advance(Duration::from_secs(2)); d.pump(Duration::from_millis(10)).unwrap();
1232 assert_eq!(create_tc_writes(&d), 0, "first deadline wrongly honoured");
1233 clock.advance(Duration::from_millis(1500)); d.pump(Duration::from_millis(10)).unwrap();
1235 assert_eq!(create_tc_writes(&d), 1);
1236 }
1237
1238 #[test]
1241 fn slot_sampling_continues_while_settling() {
1242 let clock = TestClock::new();
1243 let mut d = settling_driver(&clock);
1244 d.init().unwrap();
1245 d.take_notifications();
1246 d.device_mut().inner.slot.module_present = false;
1247 clock.advance(Duration::from_millis(500));
1248 d.pump(Duration::from_millis(10)).unwrap();
1249 let notes = d.take_notifications();
1250 assert!(
1251 notes
1252 .iter()
1253 .any(|n| matches!(n, Notification::HotPlug(HotPlug::CamRemoved))),
1254 "{notes:?}"
1255 );
1256 assert_eq!(create_tc_writes(&d), 0);
1257 }
1258
1259 #[test]
1260 fn init_drives_reset_slotinfo_and_create_tc_to_device() {
1261 let mut d = Driver::new(MockCaDevice::new([]));
1262 d.init().unwrap();
1263 let ops = &d.device().ops;
1264 assert_eq!(ops[0], DeviceOp::Reset);
1265 assert_eq!(ops[1], DeviceOp::SlotInfo);
1266 assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
1267 }
1268
1269 #[test]
1270 fn reads_reply_then_polls_on_pump() {
1271 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1273 let clock = TestClock::new();
1274 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1275 d.init().unwrap();
1276 assert!(d.pump(Duration::from_millis(100)).unwrap());
1278 clock.advance(Duration::from_millis(100));
1283 assert!(!d.pump(Duration::from_millis(100)).unwrap());
1285 let last = d.device().ops.last().unwrap();
1286 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1287 }
1288
1289 #[test]
1295 fn pump_advances_the_stack_clock_by_real_elapsed_time_not_by_timeout() {
1296 let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
1297 let clock = TestClock::new();
1298 let mut d = Driver::new(dev).with_clock(clock.as_fn());
1299 d.init().unwrap();
1300 assert!(d.pump(Duration::from_secs(1)).unwrap());
1301
1302 clock.advance(Duration::from_millis(1));
1306 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1307 assert!(
1308 d.device().ops.last().is_none_or(|op| !matches!(
1309 op,
1310 DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)
1311 )),
1312 "must not poll after only 1ms of real elapsed time, regardless of the 1s `timeout` argument"
1313 );
1314
1315 clock.advance(Duration::from_millis(100));
1317 assert!(!d.pump(Duration::from_secs(1)).unwrap());
1318 let last = d.device().ops.last().unwrap();
1319 assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
1320 }
1321
1322 #[test]
1325 fn cam_insert_edge_emits_cam_present_once_and_redrives_handshake() {
1326 let mut dev = MockCaDevice::new([]);
1327 dev.slot = SlotInfo {
1328 num: 0,
1329 module_ready: false,
1330 module_present: false,
1331 };
1332 let mut d = Driver::new(dev);
1333 d.init().unwrap();
1334 let notes = d.take_notifications();
1337 assert!(
1338 !notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1339 "baseline observation must not fire CamPresent, got {notes:?}"
1340 );
1341 let resets_before = d
1342 .device()
1343 .ops
1344 .iter()
1345 .filter(|o| **o == DeviceOp::Reset)
1346 .count();
1347
1348 d.device_mut().slot = SlotInfo {
1350 num: 0,
1351 module_ready: true,
1352 module_present: true,
1353 };
1354 d.pump(Duration::from_millis(10)).unwrap();
1355
1356 let notes = d.take_notifications();
1357 let cam_present_count = notes
1358 .iter()
1359 .filter(|n| **n == Notification::HotPlug(HotPlug::CamPresent))
1360 .count();
1361 assert_eq!(
1362 cam_present_count, 1,
1363 "expected exactly one CamPresent, got {notes:?}"
1364 );
1365 let resets_after = d
1367 .device()
1368 .ops
1369 .iter()
1370 .filter(|o| **o == DeviceOp::Reset)
1371 .count();
1372 assert_eq!(
1373 resets_after,
1374 resets_before + 1,
1375 "expected one fresh Reset on re-insert"
1376 );
1377 assert!(
1378 matches!(d.device().ops.last(), Some(DeviceOp::Write(w)) if w[0] == tags::CREATE_T_C),
1379 "expected the handshake re-driven (CREATE_T_C written), got {:?}",
1380 d.device().ops.last()
1381 );
1382 }
1383
1384 #[test]
1385 fn cam_remove_edge_emits_cam_removed_and_re_insert_re_handshakes() {
1386 let mut d = driver_with_sessions();
1387 d.take_notifications();
1388
1389 d.device_mut().slot.module_present = false;
1391 d.pump(Duration::from_millis(10)).unwrap();
1392 let notes = d.take_notifications();
1393 assert!(
1394 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
1395 "expected CamRemoved, got {notes:?}"
1396 );
1397
1398 d.mmi_menu_answer(0).unwrap();
1402 let notes = d.take_notifications();
1403 assert!(
1404 notes
1405 .iter()
1406 .any(|n| matches!(n, Notification::Error { .. })),
1407 "expected no open MMI session after teardown, got {notes:?}"
1408 );
1409
1410 let resets_before = d
1412 .device()
1413 .ops
1414 .iter()
1415 .filter(|o| **o == DeviceOp::Reset)
1416 .count();
1417 d.device_mut().slot.module_present = true;
1418 d.device_mut().slot.module_ready = true;
1419 d.pump(Duration::from_millis(10)).unwrap();
1420 let notes = d.take_notifications();
1421 assert!(
1422 notes.contains(&Notification::HotPlug(HotPlug::CamPresent)),
1423 "expected CamPresent on re-insert, got {notes:?}"
1424 );
1425 let resets_after = d
1426 .device()
1427 .ops
1428 .iter()
1429 .filter(|o| **o == DeviceOp::Reset)
1430 .count();
1431 assert_eq!(resets_after, resets_before + 1, "expected a fresh Reset");
1432 }
1433
1434 #[test]
1435 fn slot_status_unchanged_across_polls_emits_no_hotplug_notifications() {
1436 let mut d = Driver::new(MockCaDevice::new([]));
1437 d.init().unwrap();
1438 d.take_notifications();
1439
1440 for _ in 0..5 {
1441 d.pump(Duration::from_millis(10)).unwrap();
1442 }
1443 let notes = d.take_notifications();
1444 assert!(
1445 !notes.iter().any(|n| matches!(
1446 n,
1447 Notification::HotPlug(HotPlug::CamPresent | HotPlug::CamRemoved)
1448 )),
1449 "unchanged slot status must not emit hot-plug notifications, got {notes:?}"
1450 );
1451 }
1452
1453 #[test]
1454 fn ca_info_caid_set_change_infers_card_inserted_then_changed() {
1455 use dvb_ci::objects::ca_info::CaInfo;
1456
1457 let mut d = driver_with_sessions();
1458 d.take_notifications();
1459
1460 feed(
1462 &mut d,
1463 r_apdu(
1464 CA_SESSION,
1465 &ser(&CaInfo {
1466 ca_system_ids: vec![],
1467 }),
1468 ),
1469 );
1470 let notes = d.take_notifications();
1471 assert!(
1472 !notes.iter().any(|n| matches!(
1473 n,
1474 Notification::HotPlug(
1475 HotPlug::CardInserted | HotPlug::CardChanged | HotPlug::CardRemoved
1476 )
1477 )),
1478 "first ca_info must only establish the baseline, got {notes:?}"
1479 );
1480
1481 feed(
1483 &mut d,
1484 r_apdu(
1485 CA_SESSION,
1486 &ser(&CaInfo {
1487 ca_system_ids: vec![0x0B00],
1488 }),
1489 ),
1490 );
1491 let notes = d.take_notifications();
1492 assert!(
1493 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1494 "expected CardInserted, got {notes:?}"
1495 );
1496
1497 feed(
1499 &mut d,
1500 r_apdu(
1501 CA_SESSION,
1502 &ser(&CaInfo {
1503 ca_system_ids: vec![0x1800],
1504 }),
1505 ),
1506 );
1507 let notes = d.take_notifications();
1508 assert!(
1509 notes.contains(&Notification::HotPlug(HotPlug::CardChanged)),
1510 "expected CardChanged, got {notes:?}"
1511 );
1512 }
1513
1514 #[test]
1515 fn ca_pmt_reply_descrambling_transition_infers_card_present_then_removed() {
1516 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1517
1518 fn reply(ca_enable: Option<CaEnable>) -> CaPmtReply {
1519 CaPmtReply {
1520 program_number: 1,
1521 version_number: 1,
1522 current_next_indicator: true,
1523 ca_enable,
1524 streams: vec![],
1525 }
1526 }
1527
1528 let mut d = driver_with_sessions();
1529 d.take_notifications();
1530
1531 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1533 let notes = d.take_notifications();
1534 assert!(
1535 !notes.iter().any(|n| matches!(
1536 n,
1537 Notification::HotPlug(HotPlug::CardInserted | HotPlug::CardRemoved)
1538 )),
1539 "first ca_pmt_reply must only establish the baseline, got {notes:?}"
1540 );
1541
1542 feed(
1544 &mut d,
1545 r_apdu(CA_SESSION, &ser(&reply(Some(CaEnable::Possible)))),
1546 );
1547 let notes = d.take_notifications();
1548 assert!(
1549 notes.contains(&Notification::HotPlug(HotPlug::CardInserted)),
1550 "expected CardInserted, got {notes:?}"
1551 );
1552
1553 feed(&mut d, r_apdu(CA_SESSION, &ser(&reply(None))));
1555 let notes = d.take_notifications();
1556 assert!(
1557 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1558 "expected CardRemoved, got {notes:?}"
1559 );
1560 }
1561
1562 #[test]
1563 fn ca_pmt_reply_surfaces_typed_ca_enable() {
1564 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
1565
1566 let mut d = driver_with_sessions();
1567 d.take_notifications();
1568
1569 feed(
1572 &mut d,
1573 r_apdu(
1574 CA_SESSION,
1575 &ser(&CaPmtReply {
1576 program_number: 7,
1577 version_number: 1,
1578 current_next_indicator: true,
1579 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1580 streams: vec![],
1581 }),
1582 ),
1583 );
1584 let notes = d.take_notifications();
1585 assert!(
1586 notes.contains(&Notification::CaPmtReply {
1587 program_number: 7,
1588 ca_enable: Some(CaEnable::PossibleTechnicalDialogue),
1589 descrambling_ok: true,
1590 }),
1591 "expected typed ca_enable on CaPmtReply, got {notes:?}"
1592 );
1593 }
1594
1595 #[test]
1596 fn ca_pmt_reply_flag_clear_surfaces_none() {
1597 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
1598
1599 let mut d = driver_with_sessions();
1600 d.take_notifications();
1601
1602 feed(
1605 &mut d,
1606 r_apdu(
1607 CA_SESSION,
1608 &ser(&CaPmtReply {
1609 program_number: 7,
1610 version_number: 1,
1611 current_next_indicator: true,
1612 ca_enable: None,
1613 streams: vec![],
1614 }),
1615 ),
1616 );
1617 let notes = d.take_notifications();
1618 assert!(
1619 notes.contains(&Notification::CaPmtReply {
1620 program_number: 7,
1621 ca_enable: None,
1622 descrambling_ok: false,
1623 }),
1624 "expected ca_enable None on flag-clear CaPmtReply, got {notes:?}"
1625 );
1626 }
1627
1628 #[test]
1629 fn mmi_no_card_text_infers_card_removed() {
1630 use dvb_ci::objects::mmi_high::Enq;
1631
1632 let mut d = driver_with_sessions();
1633 d.take_notifications();
1634
1635 feed(
1636 &mut d,
1637 r_apdu(
1638 MMI_SESSION,
1639 &ser(&Enq {
1640 blind_answer: false,
1641 answer_text_length: 0,
1642 text_chars: b"NO CARD detected - please insert your smart card",
1643 }),
1644 ),
1645 );
1646
1647 let notes = d.take_notifications();
1648 assert!(
1649 notes.contains(&Notification::HotPlug(HotPlug::CardRemoved)),
1650 "expected CardRemoved inferred from MMI 'no card' text, got {notes:?}"
1651 );
1652 }
1653
1654 #[test]
1657 fn mmi_card_keyword_heuristic_is_edge_triggered() {
1658 use dvb_ci::objects::mmi_high::Enq;
1659
1660 let enq = |text: &'static [u8]| {
1661 r_apdu(
1662 MMI_SESSION,
1663 &ser(&Enq {
1664 blind_answer: false,
1665 answer_text_length: 0,
1666 text_chars: text,
1667 }),
1668 )
1669 };
1670 let mut d = driver_with_sessions();
1671 d.take_notifications();
1672 let count = |notes: &[Notification], hp: HotPlug| {
1673 notes
1674 .iter()
1675 .filter(|n| **n == Notification::HotPlug(hp))
1676 .count()
1677 };
1678
1679 for _ in 0..3 {
1681 feed(&mut d, enq(b"No card - please insert your smart card"));
1682 }
1683 let notes = d.take_notifications();
1684 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1685 assert_eq!(count(¬es, HotPlug::CardInserted), 0);
1686
1687 for _ in 0..2 {
1689 feed(&mut d, enq(b"Your entitlement is valid"));
1690 }
1691 let notes = d.take_notifications();
1692 assert_eq!(count(¬es, HotPlug::CardInserted), 1, "{notes:?}");
1693 assert_eq!(count(¬es, HotPlug::CardRemoved), 0);
1694
1695 feed(&mut d, enq(b"Card removed"));
1697 feed(&mut d, enq(b"Card removed"));
1698 let notes = d.take_notifications();
1699 assert_eq!(count(¬es, HotPlug::CardRemoved), 1, "{notes:?}");
1700 }
1701
1702 #[test]
1703 fn pump_hotplug_delivers_cam_present_via_closure_exactly_once() {
1704 let mut dev = MockCaDevice::new([]);
1705 dev.slot = SlotInfo {
1706 num: 0,
1707 module_ready: false,
1708 module_present: false,
1709 };
1710 let mut d = Driver::new(dev);
1711 d.init().unwrap();
1712 d.take_notifications(); d.device_mut().slot = SlotInfo {
1716 num: 0,
1717 module_ready: true,
1718 module_present: true,
1719 };
1720
1721 let mut seen = Vec::new();
1722 d.pump_hotplug(Duration::from_millis(10), |hp| seen.push(hp))
1723 .unwrap();
1724
1725 assert_eq!(
1726 seen,
1727 vec![HotPlug::CamPresent],
1728 "expected the closure to receive HotPlug::CamPresent exactly once, got {seen:?}"
1729 );
1730 }
1731
1732 pub(crate) fn ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
1737 [
1738 0x09,
1739 0x04,
1740 (ca_system_id >> 8) as u8,
1741 ca_system_id as u8,
1742 0xE0 | ((pid >> 8) as u8 & 0x1F),
1743 pid as u8,
1744 ]
1745 }
1746
1747 pub(crate) fn build_ca_pmt_fixture(program_number: u16) -> Vec<u8> {
1764 const VIACCESS: u16 = 0x0500;
1765 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1766 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1767
1768 let mut body = Vec::new();
1769 body.push(0x02); body.push(0); body.push(0);
1772 body.extend_from_slice(&program_number.to_be_bytes());
1773 body.push(0xC3); body.push(0x00); body.push(0x00); body.push(0xE0 | 0x01); body.push(0x00);
1778 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
1779 body.push(prog_ca.len() as u8);
1780 body.extend_from_slice(&prog_ca);
1781 body.push(0x1B);
1783 body.push(0xE0 | 0x01);
1784 body.push(0x00);
1785 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1786 body.push(es0_ca.len() as u8);
1787 body.extend_from_slice(&es0_ca);
1788 body.push(0x0F);
1790 body.push(0xE0 | 0x01);
1791 body.push(0x01);
1792 body.push(0xF0);
1793 body.push(0x00);
1794
1795 let section_length = body.len() - 3 + 4;
1796 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1797 body[2] = section_length as u8;
1798 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1799 body.extend_from_slice(&crc.to_be_bytes());
1800 body
1801 }
1802
1803 pub(crate) fn build_ca_pmt_fixture_dedicated_pcr(program_number: u16) -> Vec<u8> {
1810 const VIACCESS: u16 = 0x0500;
1811 let prog_ca = ca_descriptor(VIACCESS, 0x0064);
1812 let es0_ca = ca_descriptor(VIACCESS, 0x0065);
1813
1814 let mut body = Vec::new();
1815 body.push(0x02); body.push(0); body.push(0);
1818 body.extend_from_slice(&program_number.to_be_bytes());
1819 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));
1825 body.push(prog_ca.len() as u8);
1826 body.extend_from_slice(&prog_ca);
1827 body.push(0x1B);
1829 body.push(0xE0 | 0x01);
1830 body.push(0x00);
1831 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
1832 body.push(es0_ca.len() as u8);
1833 body.extend_from_slice(&es0_ca);
1834 body.push(0x0F);
1836 body.push(0xE0 | 0x01);
1837 body.push(0x01);
1838 body.push(0xF0);
1839 body.push(0x00);
1840
1841 let section_length = body.len() - 3 + 4;
1842 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1843 body[2] = section_length as u8;
1844 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1845 body.extend_from_slice(&crc.to_be_bytes());
1846 body
1847 }
1848
1849 pub(crate) fn build_clear_pmt_fixture(program_number: u16) -> Vec<u8> {
1853 let mut body = Vec::new();
1854 body.push(0x02);
1855 body.push(0);
1856 body.push(0);
1857 body.extend_from_slice(&program_number.to_be_bytes());
1858 body.push(0xC3);
1859 body.push(0x00);
1860 body.push(0x00);
1861 body.push(0xE0 | 0x01);
1862 body.push(0x00);
1863 body.push(0xF0); body.push(0x00);
1865 body.push(0x1B);
1867 body.push(0xE0 | 0x01);
1868 body.push(0x00);
1869 body.push(0xF0);
1870 body.push(0x00);
1871
1872 let section_length = body.len() - 3 + 4;
1873 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
1874 body[2] = section_length as u8;
1875 let crc = broadcast_common::crc32_mpeg2::compute(&body);
1876 body.extend_from_slice(&crc.to_be_bytes());
1877 body
1878 }
1879
1880 #[test]
1881 fn add_service_builds_and_sends_ca_pmt_matching_builder_oracle() {
1882 use broadcast_common::Parse;
1883
1884 let mut d = driver_with_sessions();
1885 d.take_notifications();
1886
1887 let pmt_bytes = build_ca_pmt_fixture(1546);
1888 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1889
1890 d.add_service(&pmt).unwrap();
1891 d.device_mut().inbound.push_back(sb());
1892 d.pump(Duration::from_millis(10)).unwrap();
1893
1894 let expected =
1898 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
1899 assert_apdu_on_session(&d, CA_SESSION, &expected);
1900
1901 let svc = d
1903 .managed_ca()
1904 .services()
1905 .get(&1546)
1906 .expect("program_number 1546 must be tracked after add_service");
1907 assert_eq!(svc.es_pids, vec![0x0100, 0x0101]);
1908 assert_eq!(svc.ca_pids, vec![0x0064, 0x0065]);
1909 assert_eq!(svc.cmd, CaPmtCmdId::OkDescrambling);
1910 assert_eq!(svc.last_ca_enable, None);
1911 }
1912
1913 #[test]
1914 fn add_service_rejects_pmt_without_ca_descriptor() {
1915 use broadcast_common::Parse;
1916
1917 let mut d = driver_with_sessions();
1918 let pmt_bytes = build_clear_pmt_fixture(999);
1919 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1920
1921 let err = d.add_service(&pmt).unwrap_err();
1922 assert!(
1923 matches!(
1924 err,
1925 CaError::NoCaDescriptor {
1926 program_number: 999
1927 }
1928 ),
1929 "expected NoCaDescriptor{{program_number: 999}}, got {err:?}"
1930 );
1931 assert!(
1932 d.managed_ca().services().is_empty(),
1933 "a rejected PMT must not be recorded"
1934 );
1935 }
1936
1937 #[test]
1949 fn requery_tick_serialize_failure_errors_from_pump_instead_of_panicking() {
1950 use broadcast_common::Parse;
1951
1952 let clock = TestClock::new();
1953 let mut d = driver_with_sessions().with_clock(clock.as_fn());
1954
1955 let good_bytes = build_ca_pmt_fixture(1);
1958 let good = PmtSection::parse(&good_bytes).unwrap();
1959 assert_eq!(good.program_number, 1, "fixture precondition");
1960 d.add_service(&good).unwrap();
1961
1962 let pmt_bytes = build_ca_pmt_fixture(1546);
1967 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
1968 let built = d
1969 .build_ca_pmt_filtered(
1970 &pmt,
1971 CaPmtListManagement::Update,
1972 CaPmtCmdId::OkDescrambling,
1973 )
1974 .try_to_bytes()
1975 .expect("fixture precondition: the good PMT encodes");
1976 let corrupt_raw = pmt_bytes[..pmt_bytes.len() - 6].to_vec();
1977 d.managed.record(
1978 1546,
1979 managed::service_of(&pmt, CaPmtCmdId::OkDescrambling, built, corrupt_raw),
1980 );
1981 d.take_notifications();
1982 for _ in 0..16 {
1986 d.device_mut().inbound.push_back(sb());
1987 d.pump(Duration::from_millis(10)).unwrap();
1988 }
1989 d.set_requery_interval(Duration::from_secs(5));
1990
1991 clock.advance(Duration::from_secs(5));
1995 let err = d
1996 .pump(Duration::ZERO)
1997 .expect_err("re-query of a corrupt stored PMT must error, not panic");
1998 assert!(
1999 matches!(
2000 err.get_ref(),
2001 Some(e) if e.downcast_ref::<CaError>()
2002 .is_some_and(|ca| matches!(ca, CaError::PmtParse(_)))
2003 ),
2004 "expected CaError::PmtParse from the corrupt stored bytes, got {err:?}"
2005 );
2006 assert!(
2007 err.to_string().contains("stored PMT re-parse failed"),
2008 "error must name the re-parse failure, got: {err}"
2009 );
2010 assert!(d.pump(Duration::ZERO).is_ok());
2014 clock.advance(Duration::from_secs(5));
2015 let err2 = d
2016 .pump(Duration::ZERO)
2017 .expect_err("the corrupt service must fail every re-query");
2018 assert!(
2019 err2.to_string().contains("stored PMT re-parse failed"),
2020 "failure must persist, got: {err2}"
2021 );
2022 }
2023
2024 fn corrupt_ca_section_ctor() -> PmtSection<'static> {
2030 const LOOP: usize = 5000;
2031 let mut prog_info: Vec<u8> = Vec::with_capacity(LOOP);
2032 for _ in 0..LOOP / 4 {
2033 prog_info.extend_from_slice(&[0x09u8, 0x02, 0x0B, 0x00]);
2035 }
2036 assert_eq!(prog_info.len(), LOOP);
2037 PmtSection::new(
2038 1,
2039 1,
2040 true,
2041 0,
2042 0,
2043 0x0200,
2044 dvb_si::descriptors::DescriptorLoop::new(Box::leak(prog_info.into_boxed_slice())),
2045 Vec::new(),
2046 )
2047 }
2048
2049 #[test]
2054 fn add_service_serialize_failure_returns_err_instead_of_panicking() {
2055 let mut d = driver_with_sessions();
2056 d.managed.set_cam_caids([0x0B00u16].into_iter().collect());
2057 let pmt = corrupt_ca_section_ctor();
2058 let err = d
2059 .add_service(&pmt)
2060 .expect_err("a ca_pmt with no valid encoding must error, not panic");
2061 assert!(
2062 matches!(err, CaError::Serialize(_)),
2063 "expected CaError::Serialize, got {err:?}"
2064 );
2065 assert!(
2066 d.managed_ca().services().is_empty(),
2067 "a rejected PMT must not be recorded"
2068 );
2069 }
2070
2071 #[test]
2072 fn add_service_second_call_uses_add_list_management() {
2073 use broadcast_common::Parse;
2074
2075 let mut d = driver_with_sessions();
2076 d.take_notifications();
2077
2078 let pmt1_bytes = build_ca_pmt_fixture(1546);
2079 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2080 d.add_service(&pmt1).unwrap();
2081 d.device_mut().inbound.push_back(sb());
2082 d.pump(Duration::from_millis(10)).unwrap();
2083
2084 let pmt2_bytes = build_ca_pmt_fixture(1547);
2085 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2086 d.add_service(&pmt2).unwrap();
2087 d.device_mut().inbound.push_back(sb());
2088 d.pump(Duration::from_millis(10)).unwrap();
2089
2090 let expected2 =
2092 build_ca_pmt(&pmt2, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling).to_bytes();
2093 assert_apdu_on_session(&d, CA_SESSION, &expected2);
2094
2095 assert_eq!(d.managed_ca().services().len(), 2);
2096 }
2097
2098 fn build_ca_pmt_fixture_two_caids(program_number: u16) -> Vec<u8> {
2109 const VIACCESS: u16 = 0x0500;
2110 const NAGRA: u16 = 0x1800;
2111 let prog_ca_a = ca_descriptor(VIACCESS, 0x0064);
2112 let prog_ca_b = ca_descriptor(NAGRA, 0x0066);
2113 let mut prog_ca = Vec::new();
2114 prog_ca.extend_from_slice(&prog_ca_a);
2115 prog_ca.extend_from_slice(&prog_ca_b);
2116
2117 let mut body = Vec::new();
2118 body.push(0x02); body.push(0);
2120 body.push(0);
2121 body.extend_from_slice(&program_number.to_be_bytes());
2122 body.push(0xC3);
2123 body.push(0x00);
2124 body.push(0x00);
2125 body.push(0xE0 | 0x01); body.push(0x00);
2127 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2128 body.push(prog_ca.len() as u8);
2129 body.extend_from_slice(&prog_ca);
2130 body.push(0x0F);
2133 body.push(0xE0 | 0x01);
2134 body.push(0x00);
2135 body.push(0xF0);
2136 body.push(0x00);
2137
2138 let section_length = body.len() - 3 + 4;
2139 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2140 body[2] = section_length as u8;
2141 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2142 body.extend_from_slice(&crc.to_be_bytes());
2143 body
2144 }
2145
2146 #[test]
2147 fn add_service_filters_ca_descriptors_to_cam_advertised_caids() {
2148 use broadcast_common::Parse;
2149 use dvb_ci::objects::ca_info::CaInfo;
2150
2151 let mut d = driver_with_sessions();
2152 d.take_notifications();
2153
2154 feed(
2157 &mut d,
2158 r_apdu(
2159 CA_SESSION,
2160 &ser(&CaInfo {
2161 ca_system_ids: vec![0x0500],
2162 }),
2163 ),
2164 );
2165 d.take_notifications();
2166
2167 let pmt_bytes = build_ca_pmt_fixture_two_caids(1560);
2168 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2169
2170 let expected_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2175 &pmt,
2176 &[0x0500],
2177 CaPmtListManagement::Only,
2178 CaPmtCmdId::OkDescrambling,
2179 )
2180 .to_bytes();
2181 let unfiltered =
2182 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2183 assert_ne!(
2184 expected_filtered, unfiltered,
2185 "precondition: the fixture's two CAIDs must make filtered != unfiltered"
2186 );
2187 let sends_filtered_before = count_apdu_on_session(&d, CA_SESSION, &expected_filtered);
2188 let sends_unfiltered_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered);
2189
2190 d.add_service(&pmt).unwrap();
2191 d.device_mut().inbound.push_back(sb());
2192 d.pump(Duration::from_millis(10)).unwrap();
2193
2194 assert_eq!(
2195 count_apdu_on_session(&d, CA_SESSION, &expected_filtered),
2196 sends_filtered_before + 1,
2197 "add_service must send the CAID-filtered ca_pmt"
2198 );
2199 assert_eq!(
2200 count_apdu_on_session(&d, CA_SESSION, &unfiltered),
2201 sends_unfiltered_before,
2202 "add_service must NOT send the unfiltered ca_pmt once the CAM's CAIDs are known"
2203 );
2204 }
2205
2206 #[test]
2207 fn requery_timer_filters_ca_descriptors_to_cam_advertised_caids() {
2208 use broadcast_common::Parse;
2209 use dvb_ci::objects::ca_info::CaInfo;
2210
2211 let clock = TestClock::new();
2212 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2213 d.set_requery_interval(Duration::from_secs(10));
2216 d.take_notifications();
2217
2218 let pmt_bytes = build_ca_pmt_fixture_two_caids(1561);
2219 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2220 d.add_service(&pmt).unwrap();
2223 d.device_mut().inbound.push_back(sb());
2224 clock.advance(Duration::from_millis(10));
2225 d.pump(Duration::from_millis(10)).unwrap();
2226
2227 feed(
2229 &mut d,
2230 r_apdu(
2231 CA_SESSION,
2232 &ser(&CaInfo {
2233 ca_system_ids: vec![0x0500],
2234 }),
2235 ),
2236 );
2237 d.take_notifications();
2238
2239 let expected_query_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2242 &pmt,
2243 &[0x0500],
2244 CaPmtListManagement::Update,
2245 CaPmtCmdId::Query,
2246 )
2247 .to_bytes();
2248 let expected_ok_filtered = dvb_ci::builder::build_ca_pmt_for_caids(
2249 &pmt,
2250 &[0x0500],
2251 CaPmtListManagement::Update,
2252 CaPmtCmdId::OkDescrambling,
2253 )
2254 .to_bytes();
2255 let unfiltered_query =
2256 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2257 let unfiltered_ok = build_ca_pmt(
2258 &pmt,
2259 CaPmtListManagement::Update,
2260 CaPmtCmdId::OkDescrambling,
2261 )
2262 .to_bytes();
2263 assert_ne!(
2264 expected_query_filtered, unfiltered_query,
2265 "precondition: the fixture's two CAIDs must make filtered != unfiltered (query)"
2266 );
2267 assert_ne!(
2268 expected_ok_filtered, unfiltered_ok,
2269 "precondition: the fixture's two CAIDs must make filtered != unfiltered (ok_descrambling)"
2270 );
2271 let sends_query_before = count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered);
2272 let sends_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered);
2273 let sends_unfiltered_query_before =
2274 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query);
2275 let sends_unfiltered_ok_before = count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok);
2276
2277 clock.advance(Duration::from_secs(11));
2278 d.pump(Duration::from_secs(11)).unwrap();
2279 feed(&mut d, sb());
2280
2281 assert_eq!(
2282 count_apdu_on_session(&d, CA_SESSION, &expected_query_filtered),
2283 sends_query_before + 1,
2284 "the re-query `query` half must be CAID-filtered to the CAM's advertised CAIDs"
2285 );
2286 assert_eq!(
2287 count_apdu_on_session(&d, CA_SESSION, &expected_ok_filtered),
2288 sends_ok_before + 1,
2289 "the re-query `ok_descrambling` half must be CAID-filtered to the CAM's advertised CAIDs"
2290 );
2291 assert_eq!(
2292 count_apdu_on_session(&d, CA_SESSION, &unfiltered_query),
2293 sends_unfiltered_query_before,
2294 "the re-query resend must NOT be the unfiltered `query` ca_pmt"
2295 );
2296 assert_eq!(
2297 count_apdu_on_session(&d, CA_SESSION, &unfiltered_ok),
2298 sends_unfiltered_ok_before,
2299 "the re-query resend must NOT be the unfiltered `ok_descrambling` ca_pmt"
2300 );
2301 }
2302
2303 pub(crate) fn build_cat_fixture(descriptors: &[u8]) -> Vec<u8> {
2313 const EXTENSION_HEADER_LEN: u16 = 5;
2314 const CRC_LEN: u16 = 4;
2315 let section_length = EXTENSION_HEADER_LEN + descriptors.len() as u16 + CRC_LEN;
2316 let mut v = Vec::new();
2317 v.push(0x01); v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
2319 v.push((section_length & 0xFF) as u8);
2320 v.extend_from_slice(&[0xFF, 0xFF]); v.push(0xC1); v.push(0x00); v.push(0x00); v.extend_from_slice(descriptors);
2325 let crc = broadcast_common::crc32_mpeg2::compute(&v);
2326 v.extend_from_slice(&crc.to_be_bytes());
2327 v
2328 }
2329
2330 #[test]
2331 fn set_cat_computes_emm_pids_as_cat_inter_ca_info_caids() {
2332 use broadcast_common::Parse;
2333 use dvb_ci::objects::ca_info::CaInfo;
2334 use dvb_si::tables::cat::CatSection;
2335
2336 let mut d = driver_with_sessions();
2337 d.take_notifications();
2338
2339 feed(
2341 &mut d,
2342 r_apdu(
2343 CA_SESSION,
2344 &ser(&CaInfo {
2345 ca_system_ids: vec![0x0648, 0x0100],
2346 }),
2347 ),
2348 );
2349 d.take_notifications();
2350
2351 let mut descriptors = Vec::new();
2354 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2355 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
2356 let cat_bytes = build_cat_fixture(&descriptors);
2357 let cat = CatSection::parse(&cat_bytes).unwrap();
2358
2359 d.set_cat(&cat).unwrap();
2360
2361 assert_eq!(
2362 d.emm_pids(),
2363 &[0x1FF0],
2364 "0x0500 -> 0x1FF1 must be excluded: the CAM never advertised CAID 0x0500"
2365 );
2366 }
2367
2368 #[test]
2369 fn set_cat_before_ca_info_is_not_an_error_and_recomputes_once_ca_info_arrives() {
2370 use broadcast_common::Parse;
2371 use dvb_ci::objects::ca_info::CaInfo;
2372 use dvb_si::tables::cat::CatSection;
2373
2374 let mut d = driver_with_sessions();
2375 d.take_notifications();
2376
2377 let mut descriptors = Vec::new();
2378 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2379 descriptors.extend_from_slice(&ca_descriptor(0x0500, 0x1FF1));
2380 let cat_bytes = build_cat_fixture(&descriptors);
2381 let cat = CatSection::parse(&cat_bytes).unwrap();
2382
2383 d.set_cat(&cat).unwrap();
2386 assert!(
2387 d.emm_pids().is_empty(),
2388 "emm_pids must be empty before any ca_info arrives, got {:?}",
2389 d.emm_pids()
2390 );
2391
2392 feed(
2395 &mut d,
2396 r_apdu(
2397 CA_SESSION,
2398 &ser(&CaInfo {
2399 ca_system_ids: vec![0x0648, 0x0100],
2400 }),
2401 ),
2402 );
2403 d.take_notifications();
2404
2405 assert_eq!(
2406 d.emm_pids(),
2407 &[0x1FF0],
2408 "emm_pids must recompute once ca_info arrives, using the CAT stored by the earlier set_cat"
2409 );
2410 }
2411
2412 #[test]
2418 fn set_cat_emm_pids_dedups_when_two_caids_share_one_emm_pid() {
2419 use broadcast_common::Parse;
2420 use dvb_ci::objects::ca_info::CaInfo;
2421 use dvb_si::tables::cat::CatSection;
2422
2423 let mut d = driver_with_sessions();
2424 d.take_notifications();
2425
2426 feed(
2428 &mut d,
2429 r_apdu(
2430 CA_SESSION,
2431 &ser(&CaInfo {
2432 ca_system_ids: vec![0x0648, 0x0100],
2433 }),
2434 ),
2435 );
2436 d.take_notifications();
2437
2438 let mut descriptors = Vec::new();
2440 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
2441 descriptors.extend_from_slice(&ca_descriptor(0x0100, 0x1FF0));
2442 let cat_bytes = build_cat_fixture(&descriptors);
2443 let cat = CatSection::parse(&cat_bytes).unwrap();
2444
2445 d.set_cat(&cat).unwrap();
2446
2447 assert_eq!(
2448 d.emm_pids(),
2449 &[0x1FF0],
2450 "0x1FF0 must appear exactly once even though two CAM-advertised CAIDs map to it, got {:?}",
2451 d.emm_pids()
2452 );
2453 }
2454
2455 fn build_ca_pmt_fixture_distinct_pids(program_number: u16) -> Vec<u8> {
2459 const VIACCESS: u16 = 0x0500;
2460 let prog_ca = ca_descriptor(VIACCESS, 0x0074);
2461 let es0_ca = ca_descriptor(VIACCESS, 0x0075);
2462
2463 let mut body = Vec::new();
2464 body.push(0x02); body.push(0);
2466 body.push(0);
2467 body.extend_from_slice(&program_number.to_be_bytes());
2468 body.push(0xC3);
2469 body.push(0x00);
2470 body.push(0x00);
2471 body.push(0xE0 | 0x02); body.push(0x00);
2473 body.push(0xF0 | ((prog_ca.len() >> 8) as u8 & 0x0F));
2474 body.push(prog_ca.len() as u8);
2475 body.extend_from_slice(&prog_ca);
2476 body.push(0x1B);
2478 body.push(0xE0 | 0x02);
2479 body.push(0x00);
2480 body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
2481 body.push(es0_ca.len() as u8);
2482 body.extend_from_slice(&es0_ca);
2483 body.push(0x0F);
2485 body.push(0xE0 | 0x02);
2486 body.push(0x01);
2487 body.push(0xF0);
2488 body.push(0x00);
2489
2490 let section_length = body.len() - 3 + 4;
2491 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
2492 body[2] = section_length as u8;
2493 let crc = broadcast_common::crc32_mpeg2::compute(&body);
2494 body.extend_from_slice(&crc.to_be_bytes());
2495 body
2496 }
2497
2498 #[test]
2499 fn descramble_pids_is_the_union_of_active_services_es_pids() {
2500 use broadcast_common::Parse;
2501
2502 let mut d = driver_with_sessions();
2503 d.take_notifications();
2504
2505 assert!(
2506 d.descramble_pids().is_empty(),
2507 "no service added yet: descramble_pids must be empty"
2508 );
2509
2510 let pmt1_bytes = build_ca_pmt_fixture(1546);
2511 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2512 d.add_service(&pmt1).unwrap();
2513 d.device_mut().inbound.push_back(sb());
2514 d.pump(Duration::from_millis(10)).unwrap();
2515
2516 assert_eq!(d.descramble_pids(), &[0x0100, 0x0101]);
2517
2518 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2519 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2520 d.add_service(&pmt2).unwrap();
2521 d.device_mut().inbound.push_back(sb());
2522 d.pump(Duration::from_millis(10)).unwrap();
2523
2524 assert_eq!(
2526 d.descramble_pids(),
2527 &[0x0100, 0x0101, 0x0200, 0x0201],
2528 "descramble_pids must be the union across both added services"
2529 );
2530 }
2531
2532 pub(crate) fn ca_pmt_reply_for(
2537 program_number: u16,
2538 ca_enable: Option<dvb_ci::objects::ca_pmt_reply::CaEnable>,
2539 ) -> dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2540 dvb_ci::objects::ca_pmt_reply::CaPmtReply {
2541 program_number,
2542 version_number: 1,
2543 current_next_indicator: true,
2544 ca_enable,
2545 streams: vec![],
2546 }
2547 }
2548
2549 #[test]
2550 fn requery_timer_resends_ca_pmt_then_reply_change_emits_one_entitlement() {
2551 use broadcast_common::Parse;
2552 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2553
2554 let clock = TestClock::new();
2555 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2556 d.set_requery_interval(Duration::from_secs(10));
2559 d.take_notifications();
2560
2561 let pmt_bytes = build_ca_pmt_fixture(1546);
2562 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2563 d.add_service(&pmt).unwrap();
2564 d.device_mut().inbound.push_back(sb());
2565 clock.advance(Duration::from_millis(10));
2566 d.pump(Duration::from_millis(10)).unwrap();
2567 d.take_notifications();
2568
2569 let expected_initial_ca_pmt =
2573 build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling).to_bytes();
2574 assert_apdu_on_session(&d, CA_SESSION, &expected_initial_ca_pmt);
2575
2576 let expected_query =
2582 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2583 let expected_ok_descrambling = build_ca_pmt(
2584 &pmt,
2585 CaPmtListManagement::Update,
2586 CaPmtCmdId::OkDescrambling,
2587 )
2588 .to_bytes();
2589 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2590 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2591
2592 let mut all_notes = Vec::new();
2593
2594 feed(
2598 &mut d,
2599 r_apdu(
2600 CA_SESSION,
2601 &ser(&ca_pmt_reply_for(
2602 1546,
2603 Some(CaEnable::NotPossibleNoEntitlement),
2604 )),
2605 ),
2606 );
2607 all_notes.extend(d.take_notifications());
2608
2609 clock.advance(Duration::from_secs(11));
2619 d.pump(Duration::from_secs(11)).unwrap();
2620 all_notes.extend(d.take_notifications());
2621 feed(&mut d, sb());
2622
2623 assert_eq!(
2624 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2625 query_sends_before + 1,
2626 "expected the re-query timer to resend the `query` half exactly once"
2627 );
2628 assert_eq!(
2629 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2630 ok_sends_before + 1,
2631 "expected the re-query timer to resend the `ok_descrambling` half exactly once"
2632 );
2633
2634 feed(
2637 &mut d,
2638 r_apdu(
2639 CA_SESSION,
2640 &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
2641 ),
2642 );
2643 all_notes.extend(d.take_notifications());
2644
2645 let hits = all_notes
2646 .iter()
2647 .filter(|n| {
2648 matches!(
2649 n,
2650 Notification::Entitlement {
2651 program_number: 1546,
2652 ca_enable: CaEnable::Possible,
2653 descrambling_ok: true,
2654 }
2655 )
2656 })
2657 .count();
2658 assert_eq!(
2659 hits, 1,
2660 "expected exactly one Entitlement{{program_number:1546, ca_enable:Possible, descrambling_ok:true}}, got {all_notes:?}"
2661 );
2662 }
2663
2664 #[test]
2665 fn requery_timer_unchanged_reply_across_two_requeries_emits_no_entitlement() {
2666 use broadcast_common::Parse;
2667 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2668
2669 let mut d = driver_with_sessions();
2670 d.set_requery_interval(Duration::from_secs(10));
2673 d.take_notifications();
2674
2675 let pmt_bytes = build_ca_pmt_fixture(1547);
2676 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2677 d.add_service(&pmt).unwrap();
2678 d.device_mut().inbound.push_back(sb());
2679 d.pump(Duration::from_millis(10)).unwrap();
2680 d.take_notifications();
2681
2682 feed(
2686 &mut d,
2687 r_apdu(
2688 CA_SESSION,
2689 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2690 ),
2691 );
2692 d.take_notifications();
2693
2694 for _ in 0..2 {
2697 d.pump(Duration::from_secs(11)).unwrap();
2698 d.take_notifications();
2699 feed(
2700 &mut d,
2701 r_apdu(
2702 CA_SESSION,
2703 &ser(&ca_pmt_reply_for(1547, Some(CaEnable::Possible))),
2704 ),
2705 );
2706 let notes = d.take_notifications();
2707 assert!(
2708 !notes
2709 .iter()
2710 .any(|n| matches!(n, Notification::Entitlement { .. })),
2711 "unchanged status across a re-query must not emit Entitlement, got {notes:?}"
2712 );
2713 }
2714 }
2715
2716 #[test]
2717 fn requery_reply_withdrawn_to_none_emits_no_entitlement() {
2718 use broadcast_common::Parse;
2719 use dvb_ci::objects::ca_pmt_reply::CaEnable;
2720
2721 let mut d = driver_with_sessions();
2722 d.take_notifications();
2723
2724 let pmt_bytes = build_ca_pmt_fixture(1548);
2725 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2726 d.add_service(&pmt).unwrap();
2727 d.device_mut().inbound.push_back(sb());
2728 d.pump(Duration::from_millis(10)).unwrap();
2729 d.take_notifications();
2730
2731 feed(
2733 &mut d,
2734 r_apdu(
2735 CA_SESSION,
2736 &ser(&ca_pmt_reply_for(1548, Some(CaEnable::Possible))),
2737 ),
2738 );
2739 d.take_notifications();
2740
2741 feed(
2745 &mut d,
2746 r_apdu(CA_SESSION, &ser(&ca_pmt_reply_for(1548, None))),
2747 );
2748 let notes = d.take_notifications();
2749 assert!(
2750 !notes
2751 .iter()
2752 .any(|n| matches!(n, Notification::Entitlement { .. })),
2753 "ca_enable transitioning to None must not emit Entitlement, got {notes:?}"
2754 );
2755 }
2756
2757 #[test]
2758 fn requery_disabled_by_default_sends_nothing() {
2759 use broadcast_common::Parse;
2760
2761 let mut d = driver_with_sessions();
2766 d.take_notifications();
2767
2768 let pmt_bytes = build_ca_pmt_fixture(1549);
2769 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2770 d.add_service(&pmt).unwrap();
2771 d.device_mut().inbound.push_back(sb());
2772 d.pump(Duration::from_millis(10)).unwrap();
2773
2774 let expected_query =
2777 build_ca_pmt(&pmt, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2778 let expected_ok_descrambling = build_ca_pmt(
2779 &pmt,
2780 CaPmtListManagement::Update,
2781 CaPmtCmdId::OkDescrambling,
2782 )
2783 .to_bytes();
2784 let query_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_query);
2785 let ok_sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling);
2786
2787 d.pump(Duration::from_secs(1_000_000)).unwrap();
2789 feed(&mut d, sb());
2790 feed(&mut d, sb());
2791 assert_eq!(
2792 count_apdu_on_session(&d, CA_SESSION, &expected_query),
2793 query_sends_before,
2794 "the default (Duration::ZERO) must never send a re-query `query`"
2795 );
2796 assert_eq!(
2797 count_apdu_on_session(&d, CA_SESSION, &expected_ok_descrambling),
2798 ok_sends_before,
2799 "the default (Duration::ZERO) must never send a re-query `ok_descrambling`"
2800 );
2801 }
2802
2803 #[test]
2804 fn set_requery_interval_zero_disables_resend_after_being_enabled() {
2805 use broadcast_common::Parse;
2806
2807 let mut d = driver_with_sessions();
2808 d.set_requery_interval(Duration::from_secs(5));
2811 d.set_requery_interval(Duration::ZERO);
2812 d.take_notifications();
2813
2814 let pmt_bytes = build_ca_pmt_fixture(1549);
2815 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
2816 d.add_service(&pmt).unwrap();
2817 d.device_mut().inbound.push_back(sb());
2818 d.pump(Duration::from_millis(10)).unwrap();
2819
2820 let expected_ca_pmt = build_ca_pmt(
2824 &pmt,
2825 CaPmtListManagement::Update,
2826 CaPmtCmdId::OkDescrambling,
2827 )
2828 .to_bytes();
2829 let sends_before = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
2830
2831 d.pump(Duration::from_secs(1000)).unwrap();
2833 feed(&mut d, sb());
2834 feed(&mut d, sb());
2835
2836 let sends_after = count_apdu_on_session(&d, CA_SESSION, &expected_ca_pmt);
2837 assert_eq!(
2838 sends_after, sends_before,
2839 "Duration::ZERO must disable the re-query resend"
2840 );
2841 }
2842
2843 #[test]
2844 fn requery_timer_resends_every_active_service_not_just_one() {
2845 use broadcast_common::Parse;
2846
2847 let clock = TestClock::new();
2848 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2849 d.set_requery_interval(Duration::from_secs(10));
2852 d.take_notifications();
2853
2854 let pmt1_bytes = build_ca_pmt_fixture(1546);
2857 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2858 d.add_service(&pmt1).unwrap();
2859 d.device_mut().inbound.push_back(sb());
2860 clock.advance(Duration::from_millis(10));
2861 d.pump(Duration::from_millis(10)).unwrap();
2862
2863 let pmt2_bytes = build_ca_pmt_fixture(1547);
2864 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2865 d.add_service(&pmt2).unwrap();
2866 d.device_mut().inbound.push_back(sb());
2867 clock.advance(Duration::from_millis(10));
2868 d.pump(Duration::from_millis(10)).unwrap();
2869 d.take_notifications();
2870
2871 let expected1_query =
2877 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2878 let expected1_ok = build_ca_pmt(
2879 &pmt1,
2880 CaPmtListManagement::Update,
2881 CaPmtCmdId::OkDescrambling,
2882 )
2883 .to_bytes();
2884 let expected2_query =
2885 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2886 let expected2_ok = build_ca_pmt(
2887 &pmt2,
2888 CaPmtListManagement::Update,
2889 CaPmtCmdId::OkDescrambling,
2890 )
2891 .to_bytes();
2892 let sends_before1_query = count_apdu_on_session(&d, CA_SESSION, &expected1_query);
2893 let sends_before1_ok = count_apdu_on_session(&d, CA_SESSION, &expected1_ok);
2894 let sends_before2_query = count_apdu_on_session(&d, CA_SESSION, &expected2_query);
2895 let sends_before2_ok = count_apdu_on_session(&d, CA_SESSION, &expected2_ok);
2896
2897 clock.advance(Duration::from_secs(11));
2903 d.pump(Duration::from_secs(11)).unwrap();
2904 feed(&mut d, sb());
2905 feed(&mut d, sb());
2906
2907 assert_eq!(
2908 count_apdu_on_session(&d, CA_SESSION, &expected1_query),
2909 sends_before1_query + 1,
2910 "expected service 1546's `query` resent exactly once on the shared tick"
2911 );
2912 assert_eq!(
2913 count_apdu_on_session(&d, CA_SESSION, &expected1_ok),
2914 sends_before1_ok + 1,
2915 "expected service 1546's `ok_descrambling` resent exactly once on the shared tick"
2916 );
2917 assert_eq!(
2918 count_apdu_on_session(&d, CA_SESSION, &expected2_query),
2919 sends_before2_query + 1,
2920 "expected service 1547's `query` resent exactly once on the shared tick"
2921 );
2922 assert_eq!(
2923 count_apdu_on_session(&d, CA_SESSION, &expected2_ok),
2924 sends_before2_ok + 1,
2925 "expected service 1547's `ok_descrambling` resent exactly once on the shared tick"
2926 );
2927 }
2928
2929 #[test]
2933 fn requery_after_remove_resends_only_the_surviving_service() {
2934 use broadcast_common::Parse;
2935
2936 let clock = TestClock::new();
2937 let mut d = driver_with_sessions().with_clock(clock.as_fn());
2938 d.set_requery_interval(Duration::from_secs(10));
2941 d.take_notifications();
2942
2943 let pmt1_bytes = build_ca_pmt_fixture(1546);
2944 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
2945 d.add_service(&pmt1).unwrap();
2946 d.device_mut().inbound.push_back(sb());
2947 clock.advance(Duration::from_millis(10));
2948 d.pump(Duration::from_millis(10)).unwrap();
2949
2950 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
2951 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
2952 d.add_service(&pmt2).unwrap();
2953 d.device_mut().inbound.push_back(sb());
2954 clock.advance(Duration::from_millis(10));
2955 d.pump(Duration::from_millis(10)).unwrap();
2956
2957 d.remove_service(1546).unwrap();
2959 d.device_mut().inbound.push_back(sb());
2960 clock.advance(Duration::from_millis(10));
2961 d.pump(Duration::from_millis(10)).unwrap();
2962 d.take_notifications();
2963
2964 let expected_survivor_query =
2970 build_ca_pmt(&pmt2, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2971 let expected_survivor_ok = build_ca_pmt(
2972 &pmt2,
2973 CaPmtListManagement::Update,
2974 CaPmtCmdId::OkDescrambling,
2975 )
2976 .to_bytes();
2977 let expected_removed_query =
2978 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::Query).to_bytes();
2979 let expected_removed_ok = build_ca_pmt(
2980 &pmt1,
2981 CaPmtListManagement::Update,
2982 CaPmtCmdId::OkDescrambling,
2983 )
2984 .to_bytes();
2985 let sends_survivor_query_before =
2986 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query);
2987 let sends_survivor_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok);
2988 let sends_removed_query_before =
2989 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query);
2990 let sends_removed_ok_before = count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok);
2991
2992 clock.advance(Duration::from_secs(11));
2993 d.pump(Duration::from_secs(11)).unwrap();
2994 feed(&mut d, sb());
2995
2996 assert_eq!(
2997 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_query),
2998 sends_survivor_query_before + 1,
2999 "the surviving service's `query` must be re-sent"
3000 );
3001 assert_eq!(
3002 count_apdu_on_session(&d, CA_SESSION, &expected_survivor_ok),
3003 sends_survivor_ok_before + 1,
3004 "the surviving service's `ok_descrambling` must be re-sent"
3005 );
3006 assert_eq!(
3007 count_apdu_on_session(&d, CA_SESSION, &expected_removed_query),
3008 sends_removed_query_before,
3009 "the removed service's `query` must NOT be re-sent"
3010 );
3011 assert_eq!(
3012 count_apdu_on_session(&d, CA_SESSION, &expected_removed_ok),
3013 sends_removed_ok_before,
3014 "the removed service's `ok_descrambling` must NOT be re-sent"
3015 );
3016 }
3017
3018 #[test]
3021 fn remove_service_sends_update_not_selected_and_drops_from_managed_state() {
3022 use broadcast_common::Parse;
3023
3024 let mut d = driver_with_sessions();
3025 d.take_notifications();
3026
3027 let pmt1_bytes = build_ca_pmt_fixture(1546);
3031 let pmt1 = PmtSection::parse(&pmt1_bytes).unwrap();
3032 d.add_service(&pmt1).unwrap();
3033 d.device_mut().inbound.push_back(sb());
3034 d.pump(Duration::from_millis(10)).unwrap();
3035
3036 let pmt2_bytes = build_ca_pmt_fixture_distinct_pids(1547);
3037 let pmt2 = PmtSection::parse(&pmt2_bytes).unwrap();
3038 d.add_service(&pmt2).unwrap();
3039 d.device_mut().inbound.push_back(sb());
3040 d.pump(Duration::from_millis(10)).unwrap();
3041
3042 d.remove_service(1546).unwrap();
3043 d.device_mut().inbound.push_back(sb());
3044 d.pump(Duration::from_millis(10)).unwrap();
3045
3046 let expected =
3050 build_ca_pmt(&pmt1, CaPmtListManagement::Update, CaPmtCmdId::NotSelected).to_bytes();
3051 assert_apdu_on_session(&d, CA_SESSION, &expected);
3052
3053 assert_eq!(
3054 d.descramble_pids(),
3055 &[0x0200, 0x0201],
3056 "1546's ES PIDs must be gone; 1547's must remain"
3057 );
3058 assert!(
3059 d.managed_ca().services().get(&1546).is_none(),
3060 "1546 must no longer be tracked"
3061 );
3062 assert!(
3063 d.managed_ca().services().get(&1547).is_some(),
3064 "1547 must remain tracked"
3065 );
3066 }
3067
3068 #[test]
3069 fn remove_service_of_untracked_program_is_a_no_op() {
3070 let mut d = driver_with_sessions();
3071 d.take_notifications();
3072
3073 let ops_before = d.device().ops.len();
3074 d.remove_service(0xFFFF).unwrap();
3075 assert_eq!(
3076 d.device().ops.len(),
3077 ops_before,
3078 "removing an untracked program must not send anything to the device"
3079 );
3080 assert!(
3081 d.managed_ca().services().is_empty(),
3082 "removing an untracked program must not disturb the (empty) managed set"
3083 );
3084 }
3085
3086 #[test]
3087 fn cam_removed_edge_clears_managed_state() {
3088 use broadcast_common::Parse;
3089 use dvb_ci::objects::ca_info::CaInfo;
3090 use dvb_si::tables::cat::CatSection;
3091
3092 let mut d = driver_with_sessions();
3093 d.take_notifications();
3094
3095 let pmt_bytes = build_ca_pmt_fixture(1546);
3096 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
3097 d.add_service(&pmt).unwrap();
3098 d.device_mut().inbound.push_back(sb());
3099 d.pump(Duration::from_millis(10)).unwrap();
3100
3101 feed(
3104 &mut d,
3105 r_apdu(
3106 CA_SESSION,
3107 &ser(&CaInfo {
3108 ca_system_ids: vec![0x0648],
3109 }),
3110 ),
3111 );
3112 d.take_notifications();
3113 let mut descriptors = Vec::new();
3114 descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
3115 let cat_bytes = build_cat_fixture(&descriptors);
3116 let cat = CatSection::parse(&cat_bytes).unwrap();
3117 d.set_cat(&cat).unwrap();
3118
3119 assert!(
3120 !d.managed_ca().services().is_empty(),
3121 "precondition: a service is tracked"
3122 );
3123 assert!(
3124 !d.descramble_pids().is_empty(),
3125 "precondition: descramble_pids populated"
3126 );
3127 assert!(!d.emm_pids().is_empty(), "precondition: emm_pids populated");
3128
3129 d.device_mut().slot.module_present = false;
3131 d.pump(Duration::from_millis(10)).unwrap();
3132 let notes = d.take_notifications();
3133 assert!(
3134 notes.contains(&Notification::HotPlug(HotPlug::CamRemoved)),
3135 "expected CamRemoved, got {notes:?}"
3136 );
3137
3138 assert!(
3139 d.managed_ca().services().is_empty(),
3140 "services must be cleared on CamRemoved"
3141 );
3142 assert!(
3143 d.descramble_pids().is_empty(),
3144 "descramble_pids must be cleared on CamRemoved"
3145 );
3146 assert!(
3147 d.emm_pids().is_empty(),
3148 "emm_pids must be cleared on CamRemoved"
3149 );
3150 }
3151}