1use crate::event::{Action, Event, HostRequest, Notification};
9use crate::resource::{
10 ApplicationInformation, ConditionalAccess, DateTime, HostControl, Mmi, Resource,
11 ResourceManager, ResourceOut,
12};
13use crate::session::{SessionLayer, SessionOut};
14use crate::transport::{Out as TransportOut, TcState, Transport};
15
16use broadcast_common::Parse;
17use dvb_ci::builder::{build_ca_pmt, build_ca_pmt_for_caids};
18use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
19use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
20use dvb_ci::resource::{
21 APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, HOST_CONTROL, MMI,
22 RESOURCE_MANAGER, ResourceId,
23};
24use dvb_si::tables::pmt::PmtSection;
25
26fn err_action(e: dvb_ci::Error) -> Action {
31 Action::Notify(Notification::Error {
32 detail: e.to_string(),
33 })
34}
35
36pub struct CiStack {
38 transport: Transport,
39 session: SessionLayer,
40 resources: Vec<Box<dyn Resource>>,
42 host_provided: Vec<ResourceId>,
47 cam_caids: Vec<u16>,
50}
51
52impl Default for CiStack {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl CiStack {
59 #[must_use]
63 pub fn new() -> Self {
64 let host_provided = vec![
74 RESOURCE_MANAGER,
75 APPLICATION_INFORMATION,
76 CONDITIONAL_ACCESS_SUPPORT,
77 DATE_TIME,
78 MMI,
79 HOST_CONTROL,
80 ];
81 Self {
82 transport: Transport::new(1),
83 session: SessionLayer::new(),
84 resources: vec![
85 Box::new(ResourceManager::new(host_provided.clone())),
86 Box::new(ApplicationInformation),
87 Box::new(ConditionalAccess),
88 Box::new(DateTime::new()),
89 Box::new(Mmi::default()),
90 Box::new(HostControl),
91 ],
92 host_provided,
93 cam_caids: Vec::new(),
94 }
95 }
96
97 pub fn register(&mut self, resource: Box<dyn Resource>) -> &mut Self {
99 self.resources.push(resource);
100 self
101 }
102
103 fn handler_index(&self, resource: ResourceId) -> Option<usize> {
105 self.resources.iter().position(|r| r.id() == resource)
106 }
107
108 pub fn handle(&mut self, event: Event<'_>) -> Vec<Action> {
110 match event {
111 Event::Host(HostRequest::Init) => {
112 self.session = SessionLayer::new();
121 self.cam_caids = Vec::new();
122 for r in &mut self.resources {
128 r.reset();
129 }
130 let mut actions = vec![Action::Reset, Action::QuerySlot];
131 let out = self.transport.init();
132 actions.extend(self.emit_transport(out));
133 actions
134 }
135 Event::Tick { elapsed } => {
136 let out = self.transport.tick(elapsed);
137 let mut actions = self.emit_transport(out);
138 for (session_nb, resource) in self.session.sessions() {
139 if let Some(i) = self.handler_index(resource) {
140 match self.resources[i].tick(elapsed) {
141 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
142 Err(e) => actions.push(err_action(e)),
143 }
144 }
145 }
146 actions
147 }
148 Event::Readable(frame) => {
149 let out = self.transport.on_frame(frame);
150 self.emit_transport(out)
151 }
152 Event::Host(HostRequest::SendCaPmt(apdu)) => {
153 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, apdu)
154 }
155 Event::Host(HostRequest::Descramble(pmt)) => self.descramble(pmt),
156 Event::Host(HostRequest::DescramblePrograms(pmts)) => self.descramble_programs(pmts),
157 Event::Host(HostRequest::AddProgram(pmt)) => self.add_program(pmt),
158 Event::Host(HostRequest::RemoveProgram(pmt)) => self.remove_program(pmt),
159 Event::Host(HostRequest::EnterMenu) => {
160 match crate::resource::ser(&dvb_ci::objects::application_info::EnterMenu::default())
161 {
162 Ok(apdu) => self.send_to_resource(APPLICATION_INFORMATION, &apdu),
163 Err(e) => vec![err_action(e)],
164 }
165 }
166 Event::Host(HostRequest::MmiMenuAnswer(choice_ref)) => {
167 match crate::resource::ser(&MenuAnsw {
168 length_form: dvb_ci::length::LengthForm::Minimal,
169 choice_ref,
170 }) {
171 Ok(apdu) => self.send_to_resource(MMI, &apdu),
172 Err(e) => vec![err_action(e)],
173 }
174 }
175 Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
176 match crate::resource::ser(&Answ {
177 length_form: dvb_ci::length::LengthForm::Minimal,
178 answ_id: AnswId::Answer,
179 text_chars: text,
180 }) {
181 Ok(apdu) => self.send_to_resource(MMI, &apdu),
182 Err(e) => vec![err_action(e)],
183 }
184 }
185 Event::Host(HostRequest::MmiCancel) => match crate::resource::ser(&Answ {
186 length_form: dvb_ci::length::LengthForm::Minimal,
187 answ_id: AnswId::Cancel,
188 text_chars: &[],
189 }) {
190 Ok(apdu) => self.send_to_resource(MMI, &apdu),
191 Err(e) => vec![err_action(e)],
192 },
193 Event::Host(HostRequest::Shutdown) => {
194 self.transport = Transport::new(1);
202 self.session = SessionLayer::new();
203 self.cam_caids = Vec::new();
204 for r in &mut self.resources {
205 r.reset();
206 }
207 vec![Action::Reset]
208 }
209 }
210 }
211
212 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
216 if let Notification::CaInfo { ca_system_ids } = note {
221 self.cam_caids = ca_system_ids.clone();
222 }
223 Vec::new()
224 }
225
226 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
236 self.send_ca_pmt_for(pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling)
237 }
238
239 fn descramble_programs(&mut self, pmts: &[&[u8]]) -> Vec<Action> {
246 let mut actions = Vec::new();
247 let n = pmts.len();
248 for (i, pmt) in pmts.iter().enumerate() {
249 let lm = match (n, i) {
250 (1, _) => CaPmtListManagement::Only,
251 (_, 0) => CaPmtListManagement::First,
252 (_, i) if i == n - 1 => CaPmtListManagement::Last,
253 _ => CaPmtListManagement::More,
254 };
255 actions.extend(self.send_ca_pmt_for(pmt, lm, CaPmtCmdId::OkDescrambling));
256 }
257 actions
258 }
259
260 fn add_program(&mut self, pmt: &[u8]) -> Vec<Action> {
263 self.send_ca_pmt_for(pmt, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling)
264 }
265
266 fn remove_program(&mut self, pmt: &[u8]) -> Vec<Action> {
269 self.send_ca_pmt_for(pmt, CaPmtListManagement::Update, CaPmtCmdId::NotSelected)
270 }
271
272 fn send_ca_pmt_for(
275 &mut self,
276 pmt: &[u8],
277 list_management: CaPmtListManagement,
278 cmd_id: CaPmtCmdId,
279 ) -> Vec<Action> {
280 match self.build_ca_pmt_bytes(pmt, list_management, cmd_id) {
281 Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
282 Err(detail) => vec![Action::Notify(Notification::Error { detail })],
283 }
284 }
285
286 fn build_ca_pmt_bytes(
290 &self,
291 pmt: &[u8],
292 list_management: CaPmtListManagement,
293 cmd_id: CaPmtCmdId,
294 ) -> Result<Vec<u8>, String> {
295 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
296 let built = if self.cam_caids.is_empty() {
297 build_ca_pmt(&parsed, list_management, cmd_id)
298 } else {
299 build_ca_pmt_for_caids(&parsed, &self.cam_caids, list_management, cmd_id)
300 };
301 built
306 .try_to_bytes()
307 .map_err(|e| format!("ca_pmt serialization failed: {e}"))
308 }
309
310 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
312 let nb = self.session.session_for(resource);
313 match nb {
314 Some(_) if self.transport.state() != TcState::Active => {
315 vec![Action::Notify(Notification::Error {
316 detail: "transport connection is not active".to_owned(),
317 })]
318 }
319 Some(nb) => match self.session.send_apdu(nb, apdu) {
320 Ok(spdu) => {
321 let out = self.transport.send_spdu(&spdu);
322 self.emit_transport(out)
323 }
324 Err(e) => vec![err_action(e)],
325 },
326 None => vec![Action::Notify(Notification::Error {
327 detail: format!("no open session for resource {}", resource.name()),
328 })],
329 }
330 }
331
332 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
335 let mut actions = Vec::new();
336 for w in out.writes {
337 actions.push(Action::Write(w));
338 }
339 if let Some(after) = out.timer {
340 actions.push(Action::SetTimer { after });
341 }
342 if let Some(err) = out.error {
343 actions.push(Action::Notify(Notification::Error {
344 detail: err.to_string(),
345 }));
346 }
347 for spdu in out.spdus {
348 actions.extend(self.drive_session(&spdu));
349 }
350 actions
351 }
352
353 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
355 let host_provided = self.host_provided.clone();
361 let SessionOut {
362 spdus,
363 apdus,
364 opened,
365 closed,
366 } = match self.session.on_spdu(spdu, |r| host_provided.contains(&r)) {
367 Ok(out) => out,
368 Err(e) => return vec![err_action(e)],
369 };
370
371 let mut actions = Vec::new();
372 for s in spdus {
374 actions.extend(self.send_spdu_actions(&s));
375 }
376 for (session_nb, resource) in opened {
377 actions.push(Action::Notify(Notification::SessionOpened { resource }));
378 if let Some(i) = self.handler_index(resource) {
380 match self.resources[i].on_open() {
381 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
382 Err(e) => actions.push(err_action(e)),
383 }
384 }
385 }
386 for session_nb in closed {
387 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
388 }
389 for (session_nb, apdu) in apdus {
391 if let Some(resource) = self.session.resource_of(session_nb)
392 && let Some(i) = self.handler_index(resource)
393 {
394 match self.resources[i].on_apdu(&apdu) {
395 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
396 Err(e) => actions.push(err_action(e)),
397 }
398 }
399 }
400 actions
401 }
402
403 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
405 let t = self.transport.send_spdu(spdu);
406 let mut actions = Vec::new();
407 for w in t.writes {
408 actions.push(Action::Write(w));
409 }
410 if let Some(after) = t.timer {
411 actions.push(Action::SetTimer { after });
412 }
413 actions
414 }
415
416 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
419 let mut actions = Vec::new();
420 for apdu in out.apdus {
421 match self.session.send_apdu(session_nb, &apdu) {
422 Ok(spdu) => actions.extend(self.send_spdu_actions(&spdu)),
423 Err(e) => actions.push(err_action(e)),
424 }
425 }
426 for note in out.notify {
427 let follow = self.on_ca_notification(¬e);
429 actions.push(Action::Notify(note));
430 actions.extend(follow);
431 }
432 for resource in out.open {
433 match self.session.create_session(resource) {
434 Ok(spdu) => actions.extend(self.send_spdu_actions(&spdu)),
435 Err(e) => actions.push(err_action(e)),
436 }
437 }
438 actions
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::transport::DEFAULT_POLL_INTERVAL;
446 use broadcast_common::Serialize;
447 use dvb_ci::resource::RESOURCE_MANAGER;
448 use dvb_ci::spdu::{OpenSessionRequest, tags as spdu_tags};
449 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
450
451 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
452 let mut b = vec![0u8; s.serialized_len()];
453 match s.serialize_into(&mut b) {
454 Ok(n) => b.truncate(n),
455 Err(_) => b.clear(),
456 }
457 b
458 }
459
460 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
462 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
463 v.extend_from_slice(spdu);
464 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
465 v
466 }
467
468 #[test]
469 fn init_resets_and_opens_transport() {
470 let mut s = CiStack::new();
471 let a = s.handle(Event::Host(HostRequest::Init));
472 assert_eq!(a[0], Action::Reset);
473 assert_eq!(a[1], Action::QuerySlot);
474 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
475 }
476
477 #[test]
481 fn shutdown_resets_device_and_clears_stack_state() {
482 let mut s = CiStack::new();
483 s.handle(Event::Host(HostRequest::Init));
484 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
485 let osr = ser(&OpenSessionRequest {
486 length_form: dvb_ci::length::LengthForm::Minimal,
487 resource: RESOURCE_MANAGER,
488 });
489 s.handle(Event::Readable(&r_data(1, &osr)));
490 assert!(!s.session.is_empty(), "precondition: a session is open");
491 assert_eq!(s.transport.state(), crate::transport::TcState::Active);
492
493 let actions = s.handle(Event::Host(HostRequest::Shutdown));
494 assert_eq!(actions, vec![Action::Reset]);
495 assert!(
496 s.session.is_empty(),
497 "Shutdown must clear the session table"
498 );
499 assert_eq!(
500 s.transport.state(),
501 crate::transport::TcState::Idle,
502 "Shutdown must tear down the transport connection"
503 );
504 }
505
506 #[test]
512 fn init_clears_stale_session_state_from_a_previous_connection() {
513 let mut s = CiStack::new();
514 s.handle(Event::Host(HostRequest::Init));
515 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
516 let osr = ser(&OpenSessionRequest {
517 length_form: dvb_ci::length::LengthForm::Minimal,
518 resource: RESOURCE_MANAGER,
519 });
520 s.handle(Event::Readable(&r_data(1, &osr)));
521 assert!(
522 !s.session.is_empty(),
523 "precondition: a stale session is open"
524 );
525
526 s.handle(Event::Host(HostRequest::Init));
529 assert!(
530 s.session.is_empty(),
531 "Init must not inherit the previous connection's session table"
532 );
533 }
534
535 #[test]
542 fn re_init_after_a_completed_handshake_fires_cam_ready_again() {
543 let mut s = CiStack::new();
544 s.handle(Event::Host(HostRequest::Init));
545 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
546 let osr = ser(&OpenSessionRequest {
547 length_form: dvb_ci::length::LengthForm::Minimal,
548 resource: RESOURCE_MANAGER,
549 });
550 s.handle(Event::Readable(&r_data(1, &osr)));
551 let first = s.handle(Event::Readable(&r_apdu(
552 1,
553 &ser(&dvb_ci::objects::resource_manager::Profile {
554 length_form: dvb_ci::length::LengthForm::Minimal,
555 resources: vec![],
556 }),
557 )));
558 assert!(
559 first
560 .iter()
561 .any(|a| matches!(a, Action::Notify(Notification::CamReady))),
562 "precondition: the first handshake fires CamReady"
563 );
564
565 s.handle(Event::Host(HostRequest::Init));
569 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
570 let osr2 = ser(&OpenSessionRequest {
571 length_form: dvb_ci::length::LengthForm::Minimal,
572 resource: RESOURCE_MANAGER,
573 });
574 s.handle(Event::Readable(&r_data(1, &osr2)));
575 let second = s.handle(Event::Readable(&r_apdu(
576 1,
577 &ser(&dvb_ci::objects::resource_manager::Profile {
578 length_form: dvb_ci::length::LengthForm::Minimal,
579 resources: vec![],
580 }),
581 )));
582 assert!(
583 second
584 .iter()
585 .any(|a| matches!(a, Action::Notify(Notification::CamReady))),
586 "a fresh handshake after Init must fire CamReady again \
587 (pre-fix: ResourceManager.ready stayed latched from the first \
588 handshake, so profile_change/CamReady never re-fired)"
589 );
590 }
591
592 #[test]
593 fn full_pipeline_opens_a_session_for_a_provided_resource() {
594 let mut s = CiStack::new();
595 s.handle(Event::Host(HostRequest::Init));
596 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
598 let osr = ser(&OpenSessionRequest {
601 length_form: dvb_ci::length::LengthForm::Minimal,
602 resource: RESOURCE_MANAGER,
603 });
604 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
605
606 assert!(actions.iter().any(|x| matches!(
608 x,
609 Action::Notify(Notification::SessionOpened {
610 resource
611 }) if *resource == RESOURCE_MANAGER
612 )));
613 let wrote_osr = actions.iter().any(|x| match x {
615 Action::Write(w) => w
616 .windows(1)
617 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
618 _ => false,
619 });
620 assert!(wrote_osr, "open_session_response must be sent down");
621
622 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
624 assert!(nb.is_some());
625 }
626
627 #[test]
628 fn tick_drives_poll_when_active() {
629 let mut s = CiStack::new();
630 s.handle(Event::Host(HostRequest::Init));
631 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
632 let a = s.handle(Event::Tick {
633 elapsed: DEFAULT_POLL_INTERVAL,
634 });
635 assert!(
636 a.iter()
637 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST)))
638 );
639 }
640
641 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
647 let mut all = Vec::new();
648 for _ in 0..16 {
649 let a = s.handle(Event::Readable(&[
650 tpdu_tags::SB,
651 0x02,
652 0x01,
653 SbValue::new(false).0,
654 ]));
655 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
656 all.extend(a);
657 if !wrote {
658 break;
659 }
660 }
661 all
662 }
663
664 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
667 use dvb_ci::spdu::SessionNumber;
668 let mut spdu = ser(&SessionNumber {
669 length_form: dvb_ci::length::LengthForm::Minimal,
670 session_nb,
671 });
672 spdu.extend_from_slice(apdu);
673 r_data(1, &spdu)
674 }
675
676 fn build_pmt() -> Vec<u8> {
679 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
680 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
681 let mut program_info = Vec::new();
682 program_info.extend_from_slice(&prog_ca);
683 program_info.extend_from_slice(®);
684 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
685
686 let mut body = Vec::new();
687 body.push(0x02); body.push(0);
689 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
693 body.push(0x00);
694 body.push(0xE0 | 0x02); body.push(0x00);
696 let pil = program_info.len();
697 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
698 body.push(pil as u8);
699 body.extend_from_slice(&program_info);
700 body.push(0x03);
702 body.push(0xE0 | 0x02);
703 body.push(0x01);
704 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
705 body.push(lang.len() as u8);
706 body.extend_from_slice(&lang);
707
708 let section_length = body.len() - 3 + 4;
709 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
710 body[2] = section_length as u8;
711 let crc = broadcast_common::crc32_mpeg2::compute(&body);
712 body.extend_from_slice(&crc.to_be_bytes());
713 body
714 }
715
716 fn stack_with_ca_session() -> CiStack {
721 use dvb_ci::objects::ca_info::CaInfo;
722 use dvb_ci::objects::resource_manager::Profile;
723 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
724 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
725
726 let mut s = CiStack::new();
727 s.handle(Event::Host(HostRequest::Init));
728 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
729 s.handle(Event::Readable(&r_data(
731 1,
732 &ser(&OpenSessionRequest {
733 length_form: dvb_ci::length::LengthForm::Minimal,
734 resource: RESOURCE_MANAGER,
735 }),
736 )));
737 s.handle(Event::Readable(&r_apdu(
740 1,
741 &ser(&Profile {
742 length_form: dvb_ci::length::LengthForm::Minimal,
743 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI],
744 }),
745 )));
746 pump_sbs(&mut s); for (nb, res) in [
750 (2u16, APPLICATION_INFORMATION),
751 (3, CONDITIONAL_ACCESS_SUPPORT),
752 (4, MMI),
753 ] {
754 s.handle(Event::Readable(&r_data(
755 1,
756 &ser(&CreateSessionResponse {
757 length_form: dvb_ci::length::LengthForm::Minimal,
758 status: SessionStatus::Ok,
759 resource: res,
760 session_nb: nb,
761 }),
762 )));
763 pump_sbs(&mut s);
764 }
765 let ca_nb = s
767 .session
768 .sessions()
769 .into_iter()
770 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
771 .map(|(n, _)| n)
772 .expect("CA session open");
773 s.handle(Event::Readable(&r_apdu(
774 ca_nb,
775 &ser(&CaInfo {
776 length_form: dvb_ci::length::LengthForm::Minimal,
777 ca_system_ids: vec![0x0B00, 0x1800],
778 }),
779 )));
780 s
781 }
782
783 #[test]
791 fn descramble_of_a_corrupt_pmt_surfaces_an_error_not_a_panic() {
792 let mut s = stack_with_ca_session();
793 let mut pmt = build_pmt();
796 pmt.truncate(pmt.len() - 8);
797 let actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
798 assert!(
799 actions.iter().any(|a| matches!(
800 a,
801 Action::Notify(Notification::Error { detail })
802 if detail.contains("invalid PMT")
803 )),
804 "expected an invalid-PMT Notification::Error, got {actions:?}"
805 );
806 assert!(
807 !actions.iter().any(|a| matches!(a, Action::Write(_))),
808 "a failed PMT must send nothing"
809 );
810 }
811
812 #[test]
813 fn descramble_sends_ok_descrambling_filtered() {
814 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
815 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
816 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
817
818 let mut s = stack_with_ca_session();
819 let ca_nb = s
820 .session
821 .sessions()
822 .into_iter()
823 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
824 .map(|(n, _)| n)
825 .unwrap();
826
827 let pmt = build_pmt();
830 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
831 actions.extend(pump_sbs(&mut s));
834 let c = first_ca_pmt(&actions).expect("ca_pmt sent");
835 assert_eq!(c.cmd_id, CaPmtCmdId::OkDescrambling);
836 assert_eq!(
838 c.program_ca_descriptors.as_slice(),
839 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
840 );
841
842 let reply = s.handle(Event::Readable(&r_apdu(
844 ca_nb,
845 &ser(&CaPmtReply {
846 reserved_ca_enable: Default::default(),
847 reserved_version: Default::default(),
848 length_form: dvb_ci::length::LengthForm::Minimal,
849 program_number: 1,
850 version_number: 1,
851 current_next_indicator: true,
852 ca_enable: Some(CaEnable::Possible),
853 streams: vec![],
854 }),
855 )));
856 assert!(reply.iter().any(|a| matches!(
857 a,
858 Action::Notify(Notification::CaPmtReply {
859 descrambling_ok: true,
860 ..
861 })
862 )));
863 }
864
865 fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
867 actions
868 .iter()
869 .any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
870 }
871
872 #[test]
873 fn mmi_menu_answer_sends_menu_answ() {
874 let mut s = stack_with_ca_session();
875 let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
876 acts.extend(pump_sbs(&mut s));
877 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
879 }
880
881 #[test]
882 fn mmi_enquiry_answer_sends_answ() {
883 let mut s = stack_with_ca_session();
884 let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
885 acts.extend(pump_sbs(&mut s));
886 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
888 }
889
890 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
893 use broadcast_common::Parse;
894 use dvb_ci::objects::ca_pmt::CaPmt;
895 let tag = [0x9F, 0x80, 0x32];
896 let mut out = Vec::new();
897 for a in actions {
898 if let Action::Write(w) = a
899 && let Some(pos) = w.windows(3).position(|x| x == tag)
900 && let Ok(p) = CaPmt::parse(&w[pos..])
901 {
902 out.push(CaPmtSummary {
903 list_management: p.list_management,
904 cmd_id: p.cmd_id.expect("programme cmd_id present"),
905 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
906 });
907 }
908 }
909 out
910 }
911
912 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
914 all_ca_pmts(actions).into_iter().next()
915 }
916
917 struct CaPmtSummary {
918 list_management: dvb_ci::objects::ca_pmt::CaPmtListManagement,
919 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
920 program_ca_descriptors: Vec<u8>,
921 }
922
923 #[test]
924 fn descramble_programs_emits_first_more_last() {
925 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
926
927 let mut s = stack_with_ca_session();
928 let pmt = build_pmt();
929 let mut acts = s.handle(Event::Host(HostRequest::DescramblePrograms(&[
931 &pmt, &pmt, &pmt,
932 ])));
933 acts.extend(pump_sbs(&mut s));
934 let lms: Vec<_> = all_ca_pmts(&acts)
935 .iter()
936 .map(|c| c.list_management)
937 .collect();
938 assert_eq!(
939 lms,
940 vec![
941 CaPmtListManagement::First,
942 CaPmtListManagement::More,
943 CaPmtListManagement::Last,
944 ]
945 );
946 assert!(
947 all_ca_pmts(&acts)
948 .iter()
949 .all(|c| c.cmd_id == CaPmtCmdId::OkDescrambling)
950 );
951 }
952
953 #[test]
954 fn add_and_remove_program_use_add_update() {
955 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
956
957 let mut s = stack_with_ca_session();
958 let pmt = build_pmt();
959
960 let mut add = s.handle(Event::Host(HostRequest::AddProgram(&pmt)));
961 add.extend(pump_sbs(&mut s));
962 let a = first_ca_pmt(&add).expect("add ca_pmt");
963 assert_eq!(a.list_management, CaPmtListManagement::Add);
964 assert_eq!(a.cmd_id, CaPmtCmdId::OkDescrambling);
965
966 let mut rm = s.handle(Event::Host(HostRequest::RemoveProgram(&pmt)));
967 rm.extend(pump_sbs(&mut s));
968 let r = first_ca_pmt(&rm).expect("remove ca_pmt");
969 assert_eq!(r.list_management, CaPmtListManagement::Update);
970 assert_eq!(r.cmd_id, CaPmtCmdId::NotSelected);
971 }
972}