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, 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),
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() {
140 if let Some(i) = self.handler_index(resource) {
141 match self.resources[i].tick(elapsed) {
142 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
143 Err(e) => actions.push(err_action(e)),
144 }
145 }
146 }
147 actions
148 }
149 Event::Readable(frame) => {
150 let out = self.transport.on_frame(frame);
151 self.emit_transport(out)
152 }
153 Event::Host(HostRequest::SendCaPmt(apdu)) => {
154 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, apdu)
155 }
156 Event::Host(HostRequest::Descramble(pmt)) => self.descramble(pmt),
157 Event::Host(HostRequest::DescramblePrograms(pmts)) => self.descramble_programs(pmts),
158 Event::Host(HostRequest::AddProgram(pmt)) => self.add_program(pmt),
159 Event::Host(HostRequest::RemoveProgram(pmt)) => self.remove_program(pmt),
160 Event::Host(HostRequest::EnterMenu) => {
161 match crate::resource::ser(&dvb_ci::objects::application_info::EnterMenu::default())
162 {
163 Ok(apdu) => self.send_to_resource(APPLICATION_INFORMATION, &apdu),
164 Err(e) => vec![err_action(e)],
165 }
166 }
167 Event::Host(HostRequest::MmiMenuAnswer(choice_ref)) => {
168 match crate::resource::ser(&MenuAnsw {
169 length_form: dvb_ci::length::LengthForm::Minimal,
170 choice_ref,
171 }) {
172 Ok(apdu) => self.send_to_resource(MMI, &apdu),
173 Err(e) => vec![err_action(e)],
174 }
175 }
176 Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
177 match crate::resource::ser(&Answ {
178 length_form: dvb_ci::length::LengthForm::Minimal,
179 answ_id: AnswId::Answer,
180 text_chars: text,
181 }) {
182 Ok(apdu) => self.send_to_resource(MMI, &apdu),
183 Err(e) => vec![err_action(e)],
184 }
185 }
186 Event::Host(HostRequest::MmiCancel) => match crate::resource::ser(&Answ {
187 length_form: dvb_ci::length::LengthForm::Minimal,
188 answ_id: AnswId::Cancel,
189 text_chars: &[],
190 }) {
191 Ok(apdu) => self.send_to_resource(MMI, &apdu),
192 Err(e) => vec![err_action(e)],
193 },
194 Event::Host(HostRequest::Shutdown) => {
195 self.transport = Transport::new(1);
203 self.session = SessionLayer::new();
204 self.cam_caids = Vec::new();
205 for r in &mut self.resources {
206 r.reset();
207 }
208 vec![Action::Reset]
209 }
210 }
211 }
212
213 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
217 if let Notification::CaInfo { ca_system_ids } = note {
222 self.cam_caids = ca_system_ids.clone();
223 }
224 Vec::new()
225 }
226
227 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
237 self.send_ca_pmt_for(pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling)
238 }
239
240 fn descramble_programs(&mut self, pmts: &[&[u8]]) -> Vec<Action> {
247 let mut actions = Vec::new();
248 let n = pmts.len();
249 for (i, pmt) in pmts.iter().enumerate() {
250 let lm = match (n, i) {
251 (1, _) => CaPmtListManagement::Only,
252 (_, 0) => CaPmtListManagement::First,
253 (_, i) if i == n - 1 => CaPmtListManagement::Last,
254 _ => CaPmtListManagement::More,
255 };
256 actions.extend(self.send_ca_pmt_for(pmt, lm, CaPmtCmdId::OkDescrambling));
257 }
258 actions
259 }
260
261 fn add_program(&mut self, pmt: &[u8]) -> Vec<Action> {
264 self.send_ca_pmt_for(pmt, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling)
265 }
266
267 fn remove_program(&mut self, pmt: &[u8]) -> Vec<Action> {
270 self.send_ca_pmt_for(pmt, CaPmtListManagement::Update, CaPmtCmdId::NotSelected)
271 }
272
273 fn send_ca_pmt_for(
276 &mut self,
277 pmt: &[u8],
278 list_management: CaPmtListManagement,
279 cmd_id: CaPmtCmdId,
280 ) -> Vec<Action> {
281 match self.build_ca_pmt_bytes(pmt, list_management, cmd_id) {
282 Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
283 Err(detail) => vec![Action::Notify(Notification::Error { detail })],
284 }
285 }
286
287 fn build_ca_pmt_bytes(
291 &self,
292 pmt: &[u8],
293 list_management: CaPmtListManagement,
294 cmd_id: CaPmtCmdId,
295 ) -> Result<Vec<u8>, String> {
296 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
297 let built = if self.cam_caids.is_empty() {
298 build_ca_pmt(&parsed, list_management, cmd_id)
299 } else {
300 build_ca_pmt_for_caids(&parsed, &self.cam_caids, list_management, cmd_id)
301 };
302 built
307 .try_to_bytes()
308 .map_err(|e| format!("ca_pmt serialization failed: {e}"))
309 }
310
311 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
313 let nb = self.session.session_for(resource);
314 match nb {
315 Some(nb) => match self.session.send_apdu(nb, apdu) {
316 Ok(spdu) => {
317 let out = self.transport.send_spdu(&spdu);
318 self.emit_transport(out)
319 }
320 Err(e) => vec![err_action(e)],
321 },
322 None => vec![Action::Notify(Notification::Error {
323 detail: format!("no open session for resource {}", resource.name()),
324 })],
325 }
326 }
327
328 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
331 let mut actions = Vec::new();
332 for w in out.writes {
333 actions.push(Action::Write(w));
334 }
335 if let Some(after) = out.timer {
336 actions.push(Action::SetTimer { after });
337 }
338 if let Some(err) = out.error {
339 actions.push(Action::Notify(Notification::Error {
340 detail: err.to_string(),
341 }));
342 }
343 for spdu in out.spdus {
344 actions.extend(self.drive_session(&spdu));
345 }
346 actions
347 }
348
349 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
351 let host_provided = self.host_provided.clone();
357 let SessionOut {
358 spdus,
359 apdus,
360 opened,
361 closed,
362 } = match self.session.on_spdu(spdu, |r| host_provided.contains(&r)) {
363 Ok(out) => out,
364 Err(e) => return vec![err_action(e)],
365 };
366
367 let mut actions = Vec::new();
368 for s in spdus {
370 actions.extend(self.send_spdu_actions(&s));
371 }
372 for (session_nb, resource) in opened {
373 actions.push(Action::Notify(Notification::SessionOpened { resource }));
374 if let Some(i) = self.handler_index(resource) {
376 match self.resources[i].on_open() {
377 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
378 Err(e) => actions.push(err_action(e)),
379 }
380 }
381 }
382 for session_nb in closed {
383 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
384 }
385 for (session_nb, apdu) in apdus {
387 if let Some(resource) = self.session.resource_of(session_nb)
388 && let Some(i) = self.handler_index(resource)
389 {
390 match self.resources[i].on_apdu(&apdu) {
391 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
392 Err(e) => actions.push(err_action(e)),
393 }
394 }
395 }
396 actions
397 }
398
399 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
401 let t = self.transport.send_spdu(spdu);
402 let mut actions = Vec::new();
403 for w in t.writes {
404 actions.push(Action::Write(w));
405 }
406 if let Some(after) = t.timer {
407 actions.push(Action::SetTimer { after });
408 }
409 actions
410 }
411
412 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
415 let mut actions = Vec::new();
416 for apdu in out.apdus {
417 match self.session.send_apdu(session_nb, &apdu) {
418 Ok(spdu) => actions.extend(self.send_spdu_actions(&spdu)),
419 Err(e) => actions.push(err_action(e)),
420 }
421 }
422 for note in out.notify {
423 let follow = self.on_ca_notification(¬e);
425 actions.push(Action::Notify(note));
426 actions.extend(follow);
427 }
428 for resource in out.open {
429 match self.session.create_session(resource) {
430 Ok(spdu) => actions.extend(self.send_spdu_actions(&spdu)),
431 Err(e) => actions.push(err_action(e)),
432 }
433 }
434 actions
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::transport::DEFAULT_POLL_INTERVAL;
442 use broadcast_common::Serialize;
443 use dvb_ci::resource::RESOURCE_MANAGER;
444 use dvb_ci::spdu::{OpenSessionRequest, tags as spdu_tags};
445 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
446
447 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
448 let mut b = vec![0u8; s.serialized_len()];
449 match s.serialize_into(&mut b) {
450 Ok(n) => b.truncate(n),
451 Err(_) => b.clear(),
452 }
453 b
454 }
455
456 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
458 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
459 v.extend_from_slice(spdu);
460 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
461 v
462 }
463
464 #[test]
465 fn init_resets_and_opens_transport() {
466 let mut s = CiStack::new();
467 let a = s.handle(Event::Host(HostRequest::Init));
468 assert_eq!(a[0], Action::Reset);
469 assert_eq!(a[1], Action::QuerySlot);
470 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
471 }
472
473 #[test]
477 fn shutdown_resets_device_and_clears_stack_state() {
478 let mut s = CiStack::new();
479 s.handle(Event::Host(HostRequest::Init));
480 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
481 let osr = ser(&OpenSessionRequest {
482 length_form: dvb_ci::length::LengthForm::Minimal,
483 resource: RESOURCE_MANAGER,
484 });
485 s.handle(Event::Readable(&r_data(1, &osr)));
486 assert!(!s.session.is_empty(), "precondition: a session is open");
487 assert_eq!(s.transport.state(), crate::transport::TcState::Active);
488
489 let actions = s.handle(Event::Host(HostRequest::Shutdown));
490 assert_eq!(actions, vec![Action::Reset]);
491 assert!(
492 s.session.is_empty(),
493 "Shutdown must clear the session table"
494 );
495 assert_eq!(
496 s.transport.state(),
497 crate::transport::TcState::Idle,
498 "Shutdown must tear down the transport connection"
499 );
500 }
501
502 #[test]
508 fn init_clears_stale_session_state_from_a_previous_connection() {
509 let mut s = CiStack::new();
510 s.handle(Event::Host(HostRequest::Init));
511 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
512 let osr = ser(&OpenSessionRequest {
513 length_form: dvb_ci::length::LengthForm::Minimal,
514 resource: RESOURCE_MANAGER,
515 });
516 s.handle(Event::Readable(&r_data(1, &osr)));
517 assert!(
518 !s.session.is_empty(),
519 "precondition: a stale session is open"
520 );
521
522 s.handle(Event::Host(HostRequest::Init));
525 assert!(
526 s.session.is_empty(),
527 "Init must not inherit the previous connection's session table"
528 );
529 }
530
531 #[test]
538 fn re_init_after_a_completed_handshake_fires_cam_ready_again() {
539 let mut s = CiStack::new();
540 s.handle(Event::Host(HostRequest::Init));
541 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
542 let osr = ser(&OpenSessionRequest {
543 length_form: dvb_ci::length::LengthForm::Minimal,
544 resource: RESOURCE_MANAGER,
545 });
546 s.handle(Event::Readable(&r_data(1, &osr)));
547 let first = s.handle(Event::Readable(&r_apdu(
548 1,
549 &ser(&dvb_ci::objects::resource_manager::Profile {
550 length_form: dvb_ci::length::LengthForm::Minimal,
551 resources: vec![],
552 }),
553 )));
554 assert!(
555 first
556 .iter()
557 .any(|a| matches!(a, Action::Notify(Notification::CamReady))),
558 "precondition: the first handshake fires CamReady"
559 );
560
561 s.handle(Event::Host(HostRequest::Init));
565 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
566 let osr2 = ser(&OpenSessionRequest {
567 length_form: dvb_ci::length::LengthForm::Minimal,
568 resource: RESOURCE_MANAGER,
569 });
570 s.handle(Event::Readable(&r_data(1, &osr2)));
571 let second = s.handle(Event::Readable(&r_apdu(
572 1,
573 &ser(&dvb_ci::objects::resource_manager::Profile {
574 length_form: dvb_ci::length::LengthForm::Minimal,
575 resources: vec![],
576 }),
577 )));
578 assert!(
579 second
580 .iter()
581 .any(|a| matches!(a, Action::Notify(Notification::CamReady))),
582 "a fresh handshake after Init must fire CamReady again \
583 (pre-fix: ResourceManager.ready stayed latched from the first \
584 handshake, so profile_change/CamReady never re-fired)"
585 );
586 }
587
588 #[test]
589 fn full_pipeline_opens_a_session_for_a_provided_resource() {
590 let mut s = CiStack::new();
591 s.handle(Event::Host(HostRequest::Init));
592 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
594 let osr = ser(&OpenSessionRequest {
597 length_form: dvb_ci::length::LengthForm::Minimal,
598 resource: RESOURCE_MANAGER,
599 });
600 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
601
602 assert!(actions.iter().any(|x| matches!(
604 x,
605 Action::Notify(Notification::SessionOpened {
606 resource
607 }) if *resource == RESOURCE_MANAGER
608 )));
609 let wrote_osr = actions.iter().any(|x| match x {
611 Action::Write(w) => w
612 .windows(1)
613 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
614 _ => false,
615 });
616 assert!(wrote_osr, "open_session_response must be sent down");
617
618 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
620 assert!(nb.is_some());
621 }
622
623 #[test]
624 fn tick_drives_poll_when_active() {
625 let mut s = CiStack::new();
626 s.handle(Event::Host(HostRequest::Init));
627 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
628 let a = s.handle(Event::Tick {
629 elapsed: DEFAULT_POLL_INTERVAL,
630 });
631 assert!(
632 a.iter()
633 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST)))
634 );
635 }
636
637 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
643 let mut all = Vec::new();
644 for _ in 0..16 {
645 let a = s.handle(Event::Readable(&[
646 tpdu_tags::SB,
647 0x02,
648 0x01,
649 SbValue::new(false).0,
650 ]));
651 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
652 all.extend(a);
653 if !wrote {
654 break;
655 }
656 }
657 all
658 }
659
660 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
663 use dvb_ci::spdu::SessionNumber;
664 let mut spdu = ser(&SessionNumber {
665 length_form: dvb_ci::length::LengthForm::Minimal,
666 session_nb,
667 });
668 spdu.extend_from_slice(apdu);
669 r_data(1, &spdu)
670 }
671
672 fn build_pmt() -> Vec<u8> {
675 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
676 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
677 let mut program_info = Vec::new();
678 program_info.extend_from_slice(&prog_ca);
679 program_info.extend_from_slice(®);
680 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
681
682 let mut body = Vec::new();
683 body.push(0x02); body.push(0);
685 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
689 body.push(0x00);
690 body.push(0xE0 | 0x02); body.push(0x00);
692 let pil = program_info.len();
693 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
694 body.push(pil as u8);
695 body.extend_from_slice(&program_info);
696 body.push(0x03);
698 body.push(0xE0 | 0x02);
699 body.push(0x01);
700 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
701 body.push(lang.len() as u8);
702 body.extend_from_slice(&lang);
703
704 let section_length = body.len() - 3 + 4;
705 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
706 body[2] = section_length as u8;
707 let crc = broadcast_common::crc32_mpeg2::compute(&body);
708 body.extend_from_slice(&crc.to_be_bytes());
709 body
710 }
711
712 fn stack_with_ca_session() -> CiStack {
717 use dvb_ci::objects::ca_info::CaInfo;
718 use dvb_ci::objects::resource_manager::Profile;
719 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
720 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
721
722 let mut s = CiStack::new();
723 s.handle(Event::Host(HostRequest::Init));
724 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
725 s.handle(Event::Readable(&r_data(
727 1,
728 &ser(&OpenSessionRequest {
729 length_form: dvb_ci::length::LengthForm::Minimal,
730 resource: RESOURCE_MANAGER,
731 }),
732 )));
733 s.handle(Event::Readable(&r_apdu(
736 1,
737 &ser(&Profile {
738 length_form: dvb_ci::length::LengthForm::Minimal,
739 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI],
740 }),
741 )));
742 pump_sbs(&mut s); for (nb, res) in [
746 (2u16, APPLICATION_INFORMATION),
747 (3, CONDITIONAL_ACCESS_SUPPORT),
748 (4, MMI),
749 ] {
750 s.handle(Event::Readable(&r_data(
751 1,
752 &ser(&CreateSessionResponse {
753 length_form: dvb_ci::length::LengthForm::Minimal,
754 status: SessionStatus::Ok,
755 resource: res,
756 session_nb: nb,
757 }),
758 )));
759 pump_sbs(&mut s);
760 }
761 let ca_nb = s
763 .session
764 .sessions()
765 .into_iter()
766 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
767 .map(|(n, _)| n)
768 .expect("CA session open");
769 s.handle(Event::Readable(&r_apdu(
770 ca_nb,
771 &ser(&CaInfo {
772 length_form: dvb_ci::length::LengthForm::Minimal,
773 ca_system_ids: vec![0x0B00, 0x1800],
774 }),
775 )));
776 s
777 }
778
779 #[test]
787 fn descramble_of_a_corrupt_pmt_surfaces_an_error_not_a_panic() {
788 let mut s = stack_with_ca_session();
789 let mut pmt = build_pmt();
792 pmt.truncate(pmt.len() - 8);
793 let actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
794 assert!(
795 actions.iter().any(|a| matches!(
796 a,
797 Action::Notify(Notification::Error { detail })
798 if detail.contains("invalid PMT")
799 )),
800 "expected an invalid-PMT Notification::Error, got {actions:?}"
801 );
802 assert!(
803 !actions.iter().any(|a| matches!(a, Action::Write(_))),
804 "a failed PMT must send nothing"
805 );
806 }
807
808 #[test]
809 fn descramble_sends_ok_descrambling_filtered() {
810 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
811 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
812 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
813
814 let mut s = stack_with_ca_session();
815 let ca_nb = s
816 .session
817 .sessions()
818 .into_iter()
819 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
820 .map(|(n, _)| n)
821 .unwrap();
822
823 let pmt = build_pmt();
826 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
827 actions.extend(pump_sbs(&mut s));
830 let c = first_ca_pmt(&actions).expect("ca_pmt sent");
831 assert_eq!(c.cmd_id, CaPmtCmdId::OkDescrambling);
832 assert_eq!(
834 c.program_ca_descriptors.as_slice(),
835 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
836 );
837
838 let reply = s.handle(Event::Readable(&r_apdu(
840 ca_nb,
841 &ser(&CaPmtReply {
842 reserved_ca_enable: Default::default(),
843 reserved_version: Default::default(),
844 length_form: dvb_ci::length::LengthForm::Minimal,
845 program_number: 1,
846 version_number: 1,
847 current_next_indicator: true,
848 ca_enable: Some(CaEnable::Possible),
849 streams: vec![],
850 }),
851 )));
852 assert!(reply.iter().any(|a| matches!(
853 a,
854 Action::Notify(Notification::CaPmtReply {
855 descrambling_ok: true,
856 ..
857 })
858 )));
859 }
860
861 fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
863 actions
864 .iter()
865 .any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
866 }
867
868 #[test]
869 fn mmi_menu_answer_sends_menu_answ() {
870 let mut s = stack_with_ca_session();
871 let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
872 acts.extend(pump_sbs(&mut s));
873 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
875 }
876
877 #[test]
878 fn mmi_enquiry_answer_sends_answ() {
879 let mut s = stack_with_ca_session();
880 let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
881 acts.extend(pump_sbs(&mut s));
882 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
884 }
885
886 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
889 use broadcast_common::Parse;
890 use dvb_ci::objects::ca_pmt::CaPmt;
891 let tag = [0x9F, 0x80, 0x32];
892 let mut out = Vec::new();
893 for a in actions {
894 if let Action::Write(w) = a
895 && let Some(pos) = w.windows(3).position(|x| x == tag)
896 && let Ok(p) = CaPmt::parse(&w[pos..])
897 {
898 out.push(CaPmtSummary {
899 list_management: p.list_management,
900 cmd_id: p.cmd_id.expect("programme cmd_id present"),
901 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
902 });
903 }
904 }
905 out
906 }
907
908 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
910 all_ca_pmts(actions).into_iter().next()
911 }
912
913 struct CaPmtSummary {
914 list_management: dvb_ci::objects::ca_pmt::CaPmtListManagement,
915 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
916 program_ca_descriptors: Vec<u8>,
917 }
918
919 #[test]
920 fn descramble_programs_emits_first_more_last() {
921 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
922
923 let mut s = stack_with_ca_session();
924 let pmt = build_pmt();
925 let mut acts = s.handle(Event::Host(HostRequest::DescramblePrograms(&[
927 &pmt, &pmt, &pmt,
928 ])));
929 acts.extend(pump_sbs(&mut s));
930 let lms: Vec<_> = all_ca_pmts(&acts)
931 .iter()
932 .map(|c| c.list_management)
933 .collect();
934 assert_eq!(
935 lms,
936 vec![
937 CaPmtListManagement::First,
938 CaPmtListManagement::More,
939 CaPmtListManagement::Last,
940 ]
941 );
942 assert!(
943 all_ca_pmts(&acts)
944 .iter()
945 .all(|c| c.cmd_id == CaPmtCmdId::OkDescrambling)
946 );
947 }
948
949 #[test]
950 fn add_and_remove_program_use_add_update() {
951 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
952
953 let mut s = stack_with_ca_session();
954 let pmt = build_pmt();
955
956 let mut add = s.handle(Event::Host(HostRequest::AddProgram(&pmt)));
957 add.extend(pump_sbs(&mut s));
958 let a = first_ca_pmt(&add).expect("add ca_pmt");
959 assert_eq!(a.list_management, CaPmtListManagement::Add);
960 assert_eq!(a.cmd_id, CaPmtCmdId::OkDescrambling);
961
962 let mut rm = s.handle(Event::Host(HostRequest::RemoveProgram(&pmt)));
963 rm.extend(pump_sbs(&mut s));
964 let r = first_ca_pmt(&rm).expect("remove ca_pmt");
965 assert_eq!(r.list_management, CaPmtListManagement::Update);
966 assert_eq!(r.cmd_id, CaPmtCmdId::NotSelected);
967 }
968}