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) {
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 { choice_ref }) {
168 Ok(apdu) => self.send_to_resource(MMI, &apdu),
169 Err(e) => vec![err_action(e)],
170 }
171 }
172 Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
173 match crate::resource::ser(&Answ {
174 answ_id: AnswId::Answer,
175 text_chars: text,
176 }) {
177 Ok(apdu) => self.send_to_resource(MMI, &apdu),
178 Err(e) => vec![err_action(e)],
179 }
180 }
181 Event::Host(HostRequest::MmiCancel) => match crate::resource::ser(&Answ {
182 answ_id: AnswId::Cancel,
183 text_chars: &[],
184 }) {
185 Ok(apdu) => self.send_to_resource(MMI, &apdu),
186 Err(e) => vec![err_action(e)],
187 },
188 Event::Host(HostRequest::Shutdown) => {
189 self.transport = Transport::new(1);
197 self.session = SessionLayer::new();
198 self.cam_caids = Vec::new();
199 for r in &mut self.resources {
200 r.reset();
201 }
202 vec![Action::Reset]
203 }
204 }
205 }
206
207 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
211 if let Notification::CaInfo { ca_system_ids } = note {
216 self.cam_caids = ca_system_ids.clone();
217 }
218 Vec::new()
219 }
220
221 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
231 self.send_ca_pmt_for(pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling)
232 }
233
234 fn descramble_programs(&mut self, pmts: &[&[u8]]) -> Vec<Action> {
241 let mut actions = Vec::new();
242 let n = pmts.len();
243 for (i, pmt) in pmts.iter().enumerate() {
244 let lm = match (n, i) {
245 (1, _) => CaPmtListManagement::Only,
246 (_, 0) => CaPmtListManagement::First,
247 (_, i) if i == n - 1 => CaPmtListManagement::Last,
248 _ => CaPmtListManagement::More,
249 };
250 actions.extend(self.send_ca_pmt_for(pmt, lm, CaPmtCmdId::OkDescrambling));
251 }
252 actions
253 }
254
255 fn add_program(&mut self, pmt: &[u8]) -> Vec<Action> {
258 self.send_ca_pmt_for(pmt, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling)
259 }
260
261 fn remove_program(&mut self, pmt: &[u8]) -> Vec<Action> {
264 self.send_ca_pmt_for(pmt, CaPmtListManagement::Update, CaPmtCmdId::NotSelected)
265 }
266
267 fn send_ca_pmt_for(
270 &mut self,
271 pmt: &[u8],
272 list_management: CaPmtListManagement,
273 cmd_id: CaPmtCmdId,
274 ) -> Vec<Action> {
275 match self.build_ca_pmt_bytes(pmt, list_management, cmd_id) {
276 Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
277 Err(detail) => vec![Action::Notify(Notification::Error { detail })],
278 }
279 }
280
281 fn build_ca_pmt_bytes(
285 &self,
286 pmt: &[u8],
287 list_management: CaPmtListManagement,
288 cmd_id: CaPmtCmdId,
289 ) -> Result<Vec<u8>, String> {
290 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
291 let built = if self.cam_caids.is_empty() {
292 build_ca_pmt(&parsed, list_management, cmd_id)
293 } else {
294 build_ca_pmt_for_caids(&parsed, &self.cam_caids, list_management, cmd_id)
295 };
296 built
301 .try_to_bytes()
302 .map_err(|e| format!("ca_pmt serialization failed: {e}"))
303 }
304
305 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
307 let nb = self.session.session_for(resource);
308 match nb {
309 Some(nb) => match self.session.send_apdu(nb, apdu) {
310 Ok(spdu) => {
311 let out = self.transport.send_spdu(&spdu);
312 self.emit_transport(out)
313 }
314 Err(e) => vec![err_action(e)],
315 },
316 None => vec![Action::Notify(Notification::Error {
317 detail: format!("no open session for resource {}", resource.name()),
318 })],
319 }
320 }
321
322 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
325 let mut actions = Vec::new();
326 for w in out.writes {
327 actions.push(Action::Write(w));
328 }
329 if let Some(after) = out.timer {
330 actions.push(Action::SetTimer { after });
331 }
332 if let Some(err) = out.error {
333 actions.push(Action::Notify(Notification::Error {
334 detail: err.to_string(),
335 }));
336 }
337 for spdu in out.spdus {
338 actions.extend(self.drive_session(&spdu));
339 }
340 actions
341 }
342
343 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
345 let host_provided = self.host_provided.clone();
351 let SessionOut {
352 spdus,
353 apdus,
354 opened,
355 closed,
356 } = match self.session.on_spdu(spdu, |r| host_provided.contains(&r)) {
357 Ok(out) => out,
358 Err(e) => return vec![err_action(e)],
359 };
360
361 let mut actions = Vec::new();
362 for s in spdus {
364 actions.extend(self.send_spdu_actions(&s));
365 }
366 for (session_nb, resource) in opened {
367 actions.push(Action::Notify(Notification::SessionOpened { resource }));
368 if let Some(i) = self.handler_index(resource) {
370 match self.resources[i].on_open() {
371 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
372 Err(e) => actions.push(err_action(e)),
373 }
374 }
375 }
376 for session_nb in closed {
377 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
378 }
379 for (session_nb, apdu) in apdus {
381 if let Some(resource) = self.session.resource_of(session_nb)
382 && let Some(i) = self.handler_index(resource)
383 {
384 match self.resources[i].on_apdu(&apdu) {
385 Ok(out) => actions.extend(self.process_resource_out(session_nb, out)),
386 Err(e) => actions.push(err_action(e)),
387 }
388 }
389 }
390 actions
391 }
392
393 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
395 let t = self.transport.send_spdu(spdu);
396 let mut actions = Vec::new();
397 for w in t.writes {
398 actions.push(Action::Write(w));
399 }
400 if let Some(after) = t.timer {
401 actions.push(Action::SetTimer { after });
402 }
403 actions
404 }
405
406 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
409 let mut actions = Vec::new();
410 for apdu in out.apdus {
411 match self.session.send_apdu(session_nb, &apdu) {
412 Ok(spdu) => actions.extend(self.send_spdu_actions(&spdu)),
413 Err(e) => actions.push(err_action(e)),
414 }
415 }
416 for note in out.notify {
417 let follow = self.on_ca_notification(¬e);
419 actions.push(Action::Notify(note));
420 actions.extend(follow);
421 }
422 for resource in out.open {
423 match self.session.create_session(resource) {
424 Ok(spdu) => actions.extend(self.send_spdu_actions(&spdu)),
425 Err(e) => actions.push(err_action(e)),
426 }
427 }
428 actions
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use crate::transport::DEFAULT_POLL_INTERVAL;
436 use broadcast_common::Serialize;
437 use dvb_ci::resource::RESOURCE_MANAGER;
438 use dvb_ci::spdu::{OpenSessionRequest, tags as spdu_tags};
439 use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
440
441 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
442 let mut b = vec![0u8; s.serialized_len()];
443 match s.serialize_into(&mut b) {
444 Ok(n) => b.truncate(n),
445 Err(_) => b.clear(),
446 }
447 b
448 }
449
450 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
452 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
453 v.extend_from_slice(spdu);
454 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
455 v
456 }
457
458 #[test]
459 fn init_resets_and_opens_transport() {
460 let mut s = CiStack::new();
461 let a = s.handle(Event::Host(HostRequest::Init));
462 assert_eq!(a[0], Action::Reset);
463 assert_eq!(a[1], Action::QuerySlot);
464 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
465 }
466
467 #[test]
471 fn shutdown_resets_device_and_clears_stack_state() {
472 let mut s = CiStack::new();
473 s.handle(Event::Host(HostRequest::Init));
474 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
475 let osr = ser(&OpenSessionRequest {
476 resource: RESOURCE_MANAGER,
477 });
478 s.handle(Event::Readable(&r_data(1, &osr)));
479 assert!(!s.session.is_empty(), "precondition: a session is open");
480 assert_eq!(s.transport.state(), crate::transport::TcState::Active);
481
482 let actions = s.handle(Event::Host(HostRequest::Shutdown));
483 assert_eq!(actions, vec![Action::Reset]);
484 assert!(
485 s.session.is_empty(),
486 "Shutdown must clear the session table"
487 );
488 assert_eq!(
489 s.transport.state(),
490 crate::transport::TcState::Idle,
491 "Shutdown must tear down the transport connection"
492 );
493 }
494
495 #[test]
501 fn init_clears_stale_session_state_from_a_previous_connection() {
502 let mut s = CiStack::new();
503 s.handle(Event::Host(HostRequest::Init));
504 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
505 let osr = ser(&OpenSessionRequest {
506 resource: RESOURCE_MANAGER,
507 });
508 s.handle(Event::Readable(&r_data(1, &osr)));
509 assert!(
510 !s.session.is_empty(),
511 "precondition: a stale session is open"
512 );
513
514 s.handle(Event::Host(HostRequest::Init));
517 assert!(
518 s.session.is_empty(),
519 "Init must not inherit the previous connection's session table"
520 );
521 }
522
523 #[test]
530 fn re_init_after_a_completed_handshake_fires_cam_ready_again() {
531 let mut s = CiStack::new();
532 s.handle(Event::Host(HostRequest::Init));
533 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
534 let osr = ser(&OpenSessionRequest {
535 resource: RESOURCE_MANAGER,
536 });
537 s.handle(Event::Readable(&r_data(1, &osr)));
538 let first = s.handle(Event::Readable(&r_apdu(
539 1,
540 &ser(&dvb_ci::objects::resource_manager::Profile { resources: vec![] }),
541 )));
542 assert!(
543 first
544 .iter()
545 .any(|a| matches!(a, Action::Notify(Notification::CamReady))),
546 "precondition: the first handshake fires CamReady"
547 );
548
549 s.handle(Event::Host(HostRequest::Init));
553 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
554 let osr2 = ser(&OpenSessionRequest {
555 resource: RESOURCE_MANAGER,
556 });
557 s.handle(Event::Readable(&r_data(1, &osr2)));
558 let second = s.handle(Event::Readable(&r_apdu(
559 1,
560 &ser(&dvb_ci::objects::resource_manager::Profile { resources: vec![] }),
561 )));
562 assert!(
563 second
564 .iter()
565 .any(|a| matches!(a, Action::Notify(Notification::CamReady))),
566 "a fresh handshake after Init must fire CamReady again \
567 (pre-fix: ResourceManager.ready stayed latched from the first \
568 handshake, so profile_change/CamReady never re-fired)"
569 );
570 }
571
572 #[test]
573 fn full_pipeline_opens_a_session_for_a_provided_resource() {
574 let mut s = CiStack::new();
575 s.handle(Event::Host(HostRequest::Init));
576 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
578 let osr = ser(&OpenSessionRequest {
581 resource: RESOURCE_MANAGER,
582 });
583 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
584
585 assert!(actions.iter().any(|x| matches!(
587 x,
588 Action::Notify(Notification::SessionOpened {
589 resource
590 }) if *resource == RESOURCE_MANAGER
591 )));
592 let wrote_osr = actions.iter().any(|x| match x {
594 Action::Write(w) => w
595 .windows(1)
596 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
597 _ => false,
598 });
599 assert!(wrote_osr, "open_session_response must be sent down");
600
601 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
603 assert!(nb.is_some());
604 }
605
606 #[test]
607 fn tick_drives_poll_when_active() {
608 let mut s = CiStack::new();
609 s.handle(Event::Host(HostRequest::Init));
610 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
611 let a = s.handle(Event::Tick {
612 elapsed: DEFAULT_POLL_INTERVAL,
613 });
614 assert!(
615 a.iter()
616 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST)))
617 );
618 }
619
620 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
626 let mut all = Vec::new();
627 for _ in 0..16 {
628 let a = s.handle(Event::Readable(&[
629 tpdu_tags::SB,
630 0x02,
631 0x01,
632 SbValue::new(false).0,
633 ]));
634 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
635 all.extend(a);
636 if !wrote {
637 break;
638 }
639 }
640 all
641 }
642
643 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
646 use dvb_ci::spdu::SessionNumber;
647 let mut spdu = ser(&SessionNumber { session_nb });
648 spdu.extend_from_slice(apdu);
649 r_data(1, &spdu)
650 }
651
652 fn build_pmt() -> Vec<u8> {
655 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
656 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
657 let mut program_info = Vec::new();
658 program_info.extend_from_slice(&prog_ca);
659 program_info.extend_from_slice(®);
660 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
661
662 let mut body = Vec::new();
663 body.push(0x02); body.push(0);
665 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
669 body.push(0x00);
670 body.push(0xE0 | 0x02); body.push(0x00);
672 let pil = program_info.len();
673 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
674 body.push(pil as u8);
675 body.extend_from_slice(&program_info);
676 body.push(0x03);
678 body.push(0xE0 | 0x02);
679 body.push(0x01);
680 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
681 body.push(lang.len() as u8);
682 body.extend_from_slice(&lang);
683
684 let section_length = body.len() - 3 + 4;
685 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
686 body[2] = section_length as u8;
687 let crc = broadcast_common::crc32_mpeg2::compute(&body);
688 body.extend_from_slice(&crc.to_be_bytes());
689 body
690 }
691
692 fn stack_with_ca_session() -> CiStack {
697 use dvb_ci::objects::ca_info::CaInfo;
698 use dvb_ci::objects::resource_manager::Profile;
699 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
700 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
701
702 let mut s = CiStack::new();
703 s.handle(Event::Host(HostRequest::Init));
704 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
705 s.handle(Event::Readable(&r_data(
707 1,
708 &ser(&OpenSessionRequest {
709 resource: RESOURCE_MANAGER,
710 }),
711 )));
712 s.handle(Event::Readable(&r_apdu(
715 1,
716 &ser(&Profile {
717 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI],
718 }),
719 )));
720 pump_sbs(&mut s); for (nb, res) in [
724 (2u16, APPLICATION_INFORMATION),
725 (3, CONDITIONAL_ACCESS_SUPPORT),
726 (4, MMI),
727 ] {
728 s.handle(Event::Readable(&r_data(
729 1,
730 &ser(&CreateSessionResponse {
731 status: SessionStatus::Ok,
732 resource: res,
733 session_nb: nb,
734 }),
735 )));
736 pump_sbs(&mut s);
737 }
738 let ca_nb = s
740 .session
741 .sessions()
742 .into_iter()
743 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
744 .map(|(n, _)| n)
745 .expect("CA session open");
746 s.handle(Event::Readable(&r_apdu(
747 ca_nb,
748 &ser(&CaInfo {
749 ca_system_ids: vec![0x0B00, 0x1800],
750 }),
751 )));
752 s
753 }
754
755 #[test]
763 fn descramble_of_a_corrupt_pmt_surfaces_an_error_not_a_panic() {
764 let mut s = stack_with_ca_session();
765 let mut pmt = build_pmt();
768 pmt.truncate(pmt.len() - 8);
769 let actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
770 assert!(
771 actions.iter().any(|a| matches!(
772 a,
773 Action::Notify(Notification::Error { detail })
774 if detail.contains("invalid PMT")
775 )),
776 "expected an invalid-PMT Notification::Error, got {actions:?}"
777 );
778 assert!(
779 !actions.iter().any(|a| matches!(a, Action::Write(_))),
780 "a failed PMT must send nothing"
781 );
782 }
783
784 #[test]
785 fn descramble_sends_ok_descrambling_filtered() {
786 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
787 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
788 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
789
790 let mut s = stack_with_ca_session();
791 let ca_nb = s
792 .session
793 .sessions()
794 .into_iter()
795 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
796 .map(|(n, _)| n)
797 .unwrap();
798
799 let pmt = build_pmt();
802 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
803 actions.extend(pump_sbs(&mut s));
806 let c = first_ca_pmt(&actions).expect("ca_pmt sent");
807 assert_eq!(c.cmd_id, CaPmtCmdId::OkDescrambling);
808 assert_eq!(
810 c.program_ca_descriptors.as_slice(),
811 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
812 );
813
814 let reply = s.handle(Event::Readable(&r_apdu(
816 ca_nb,
817 &ser(&CaPmtReply {
818 program_number: 1,
819 version_number: 1,
820 current_next_indicator: true,
821 ca_enable: Some(CaEnable::Possible),
822 streams: vec![],
823 }),
824 )));
825 assert!(reply.iter().any(|a| matches!(
826 a,
827 Action::Notify(Notification::CaPmtReply {
828 descrambling_ok: true,
829 ..
830 })
831 )));
832 }
833
834 fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
836 actions
837 .iter()
838 .any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
839 }
840
841 #[test]
842 fn mmi_menu_answer_sends_menu_answ() {
843 let mut s = stack_with_ca_session();
844 let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
845 acts.extend(pump_sbs(&mut s));
846 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
848 }
849
850 #[test]
851 fn mmi_enquiry_answer_sends_answ() {
852 let mut s = stack_with_ca_session();
853 let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
854 acts.extend(pump_sbs(&mut s));
855 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
857 }
858
859 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
862 use broadcast_common::Parse;
863 use dvb_ci::objects::ca_pmt::CaPmt;
864 let tag = [0x9F, 0x80, 0x32];
865 let mut out = Vec::new();
866 for a in actions {
867 if let Action::Write(w) = a
868 && let Some(pos) = w.windows(3).position(|x| x == tag)
869 && let Ok(p) = CaPmt::parse(&w[pos..])
870 {
871 out.push(CaPmtSummary {
872 list_management: p.list_management,
873 cmd_id: p.cmd_id.expect("programme cmd_id present"),
874 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
875 });
876 }
877 }
878 out
879 }
880
881 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
883 all_ca_pmts(actions).into_iter().next()
884 }
885
886 struct CaPmtSummary {
887 list_management: dvb_ci::objects::ca_pmt::CaPmtListManagement,
888 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
889 program_ca_descriptors: Vec<u8>,
890 }
891
892 #[test]
893 fn descramble_programs_emits_first_more_last() {
894 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
895
896 let mut s = stack_with_ca_session();
897 let pmt = build_pmt();
898 let mut acts = s.handle(Event::Host(HostRequest::DescramblePrograms(&[
900 &pmt, &pmt, &pmt,
901 ])));
902 acts.extend(pump_sbs(&mut s));
903 let lms: Vec<_> = all_ca_pmts(&acts)
904 .iter()
905 .map(|c| c.list_management)
906 .collect();
907 assert_eq!(
908 lms,
909 vec![
910 CaPmtListManagement::First,
911 CaPmtListManagement::More,
912 CaPmtListManagement::Last,
913 ]
914 );
915 assert!(
916 all_ca_pmts(&acts)
917 .iter()
918 .all(|c| c.cmd_id == CaPmtCmdId::OkDescrambling)
919 );
920 }
921
922 #[test]
923 fn add_and_remove_program_use_add_update() {
924 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
925
926 let mut s = stack_with_ca_session();
927 let pmt = build_pmt();
928
929 let mut add = s.handle(Event::Host(HostRequest::AddProgram(&pmt)));
930 add.extend(pump_sbs(&mut s));
931 let a = first_ca_pmt(&add).expect("add ca_pmt");
932 assert_eq!(a.list_management, CaPmtListManagement::Add);
933 assert_eq!(a.cmd_id, CaPmtCmdId::OkDescrambling);
934
935 let mut rm = s.handle(Event::Host(HostRequest::RemoveProgram(&pmt)));
936 rm.extend(pump_sbs(&mut s));
937 let r = first_ca_pmt(&rm).expect("remove ca_pmt");
938 assert_eq!(r.list_management, CaPmtListManagement::Update);
939 assert_eq!(r.cmd_id, CaPmtCmdId::NotSelected);
940 }
941}