1use crate::event::{Action, Event, HostRequest, Notification};
8use crate::resource::{
9 ApplicationInformation, ConditionalAccess, DateTime, Mmi, Resource, ResourceManager,
10 ResourceOut,
11};
12use crate::session::{SessionLayer, SessionOut};
13use crate::transport::{Out as TransportOut, Transport};
14
15use dvb_ci::builder::{build_ca_pmt, build_ca_pmt_for_caids};
16use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
17use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
18use dvb_ci::resource::{
19 ResourceId, APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI,
20 RESOURCE_MANAGER,
21};
22use dvb_common::{Parse, Serialize};
23use dvb_si::tables::pmt::PmtSection;
24
25fn ser_apdu<S: Serialize>(s: &S) -> Vec<u8> {
27 let mut b = vec![0u8; s.serialized_len()];
28 match s.serialize_into(&mut b) {
29 Ok(n) => b.truncate(n),
30 Err(_) => b.clear(),
31 }
32 b
33}
34
35pub struct CiStack {
37 transport: Transport,
38 session: SessionLayer,
39 resources: Vec<Box<dyn Resource>>,
41 host_provided: Vec<ResourceId>,
46 cam_caids: Vec<u16>,
49}
50
51impl Default for CiStack {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl CiStack {
58 #[must_use]
62 pub fn new() -> Self {
63 let host_provided = vec![
73 RESOURCE_MANAGER,
74 APPLICATION_INFORMATION,
75 CONDITIONAL_ACCESS_SUPPORT,
76 DATE_TIME,
77 MMI,
78 ];
79 Self {
80 transport: Transport::new(1),
81 session: SessionLayer::new(),
82 resources: vec![
83 Box::new(ResourceManager::new(host_provided.clone())),
84 Box::new(ApplicationInformation),
85 Box::new(ConditionalAccess),
86 Box::new(DateTime::new()),
87 Box::new(Mmi),
88 ],
89 host_provided,
90 cam_caids: Vec::new(),
91 }
92 }
93
94 pub fn register(&mut self, resource: Box<dyn Resource>) -> &mut Self {
96 self.resources.push(resource);
97 self
98 }
99
100 fn handler_index(&self, resource: ResourceId) -> Option<usize> {
102 self.resources.iter().position(|r| r.id() == resource)
103 }
104
105 pub fn handle(&mut self, event: Event<'_>) -> Vec<Action> {
107 match event {
108 Event::Host(HostRequest::Init) => {
109 let mut actions = vec![Action::Reset, Action::QuerySlot];
110 let out = self.transport.init();
111 actions.extend(self.emit_transport(out));
112 actions
113 }
114 Event::Tick { elapsed } => {
115 let out = self.transport.tick(elapsed);
116 let mut actions = self.emit_transport(out);
117 for (session_nb, resource) in self.session.sessions() {
119 if let Some(i) = self.handler_index(resource) {
120 let out = self.resources[i].tick(elapsed);
121 actions.extend(self.process_resource_out(session_nb, out));
122 }
123 }
124 actions
125 }
126 Event::Readable(frame) => {
127 let out = self.transport.on_frame(frame);
128 self.emit_transport(out)
129 }
130 Event::Host(HostRequest::SendCaPmt(apdu)) => {
131 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, apdu)
132 }
133 Event::Host(HostRequest::Descramble(pmt)) => self.descramble(pmt),
134 Event::Host(HostRequest::DescramblePrograms(pmts)) => self.descramble_programs(pmts),
135 Event::Host(HostRequest::AddProgram(pmt)) => self.add_program(pmt),
136 Event::Host(HostRequest::RemoveProgram(pmt)) => self.remove_program(pmt),
137 Event::Host(HostRequest::EnterMenu) => {
138 let apdu = ser_apdu(&dvb_ci::objects::application_info::EnterMenu);
139 self.send_to_resource(APPLICATION_INFORMATION, &apdu)
140 }
141 Event::Host(HostRequest::MmiMenuAnswer(choice_ref)) => {
142 let apdu = ser_apdu(&MenuAnsw { choice_ref });
143 self.send_to_resource(MMI, &apdu)
144 }
145 Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
146 let apdu = ser_apdu(&Answ {
147 answ_id: AnswId::Answer,
148 text_chars: text,
149 });
150 self.send_to_resource(MMI, &apdu)
151 }
152 Event::Host(HostRequest::MmiCancel) => {
153 let apdu = ser_apdu(&Answ {
154 answ_id: AnswId::Cancel,
155 text_chars: &[],
156 });
157 self.send_to_resource(MMI, &apdu)
158 }
159 Event::Host(HostRequest::Shutdown) => Vec::new(),
160 }
161 }
162
163 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
167 if let Notification::CaInfo { ca_system_ids } = note {
172 self.cam_caids = ca_system_ids.clone();
173 }
174 Vec::new()
175 }
176
177 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
187 self.send_ca_pmt_for(pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling)
188 }
189
190 fn descramble_programs(&mut self, pmts: &[&[u8]]) -> Vec<Action> {
197 let mut actions = Vec::new();
198 let n = pmts.len();
199 for (i, pmt) in pmts.iter().enumerate() {
200 let lm = match (n, i) {
201 (1, _) => CaPmtListManagement::Only,
202 (_, 0) => CaPmtListManagement::First,
203 (_, i) if i == n - 1 => CaPmtListManagement::Last,
204 _ => CaPmtListManagement::More,
205 };
206 actions.extend(self.send_ca_pmt_for(pmt, lm, CaPmtCmdId::OkDescrambling));
207 }
208 actions
209 }
210
211 fn add_program(&mut self, pmt: &[u8]) -> Vec<Action> {
214 self.send_ca_pmt_for(pmt, CaPmtListManagement::Add, CaPmtCmdId::OkDescrambling)
215 }
216
217 fn remove_program(&mut self, pmt: &[u8]) -> Vec<Action> {
220 self.send_ca_pmt_for(pmt, CaPmtListManagement::Update, CaPmtCmdId::NotSelected)
221 }
222
223 fn send_ca_pmt_for(
226 &mut self,
227 pmt: &[u8],
228 list_management: CaPmtListManagement,
229 cmd_id: CaPmtCmdId,
230 ) -> Vec<Action> {
231 match self.build_ca_pmt_bytes(pmt, list_management, cmd_id) {
232 Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
233 Err(detail) => vec![Action::Notify(Notification::Error { detail })],
234 }
235 }
236
237 fn build_ca_pmt_bytes(
241 &self,
242 pmt: &[u8],
243 list_management: CaPmtListManagement,
244 cmd_id: CaPmtCmdId,
245 ) -> Result<Vec<u8>, String> {
246 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
247 let built = if self.cam_caids.is_empty() {
248 build_ca_pmt(&parsed, list_management, cmd_id)
249 } else {
250 build_ca_pmt_for_caids(&parsed, &self.cam_caids, list_management, cmd_id)
251 };
252 Ok(built.to_bytes())
253 }
254
255 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
257 let nb = (1u16..=u16::MAX).find(|&n| self.session.resource_of(n) == Some(resource));
259 match nb {
260 Some(nb) => {
261 let spdu = self.session.send_apdu(nb, apdu);
262 let out = self.transport.send_spdu(&spdu);
263 self.emit_transport(out)
264 }
265 None => vec![Action::Notify(Notification::Error {
266 detail: format!("no open session for resource {}", resource.name()),
267 })],
268 }
269 }
270
271 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
274 let mut actions = Vec::new();
275 for w in out.writes {
276 actions.push(Action::Write(w));
277 }
278 if let Some(after) = out.timer {
279 actions.push(Action::SetTimer { after });
280 }
281 if let Some(err) = out.error {
282 actions.push(Action::Notify(Notification::Error {
283 detail: err.to_string(),
284 }));
285 }
286 for spdu in out.spdus {
287 actions.extend(self.drive_session(&spdu));
288 }
289 actions
290 }
291
292 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
294 let host_provided = self.host_provided.clone();
300 let SessionOut {
301 spdus,
302 apdus,
303 opened,
304 closed,
305 } = self.session.on_spdu(spdu, |r| host_provided.contains(&r));
306
307 let mut actions = Vec::new();
308 for s in spdus {
310 actions.extend(self.send_spdu_actions(&s));
311 }
312 for (session_nb, resource) in opened {
313 actions.push(Action::Notify(Notification::SessionOpened { resource }));
314 if let Some(i) = self.handler_index(resource) {
316 let out = self.resources[i].on_open();
317 actions.extend(self.process_resource_out(session_nb, out));
318 }
319 }
320 for session_nb in closed {
321 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
322 }
323 for (session_nb, apdu) in apdus {
325 if let Some(resource) = self.session.resource_of(session_nb) {
326 if let Some(i) = self.handler_index(resource) {
327 let out = self.resources[i].on_apdu(&apdu);
328 actions.extend(self.process_resource_out(session_nb, out));
329 }
330 }
331 }
332 actions
333 }
334
335 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
337 let t = self.transport.send_spdu(spdu);
338 let mut actions = Vec::new();
339 for w in t.writes {
340 actions.push(Action::Write(w));
341 }
342 if let Some(after) = t.timer {
343 actions.push(Action::SetTimer { after });
344 }
345 actions
346 }
347
348 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
351 let mut actions = Vec::new();
352 for apdu in out.apdus {
353 let spdu = self.session.send_apdu(session_nb, &apdu);
354 actions.extend(self.send_spdu_actions(&spdu));
355 }
356 for note in out.notify {
357 let follow = self.on_ca_notification(¬e);
359 actions.push(Action::Notify(note));
360 actions.extend(follow);
361 }
362 for resource in out.open {
363 let spdu = self.session.create_session(resource);
364 actions.extend(self.send_spdu_actions(&spdu));
365 }
366 actions
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::transport::DEFAULT_POLL_INTERVAL;
374 use dvb_ci::resource::RESOURCE_MANAGER;
375 use dvb_ci::spdu::{tags as spdu_tags, OpenSessionRequest};
376 use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
377 use dvb_common::Serialize;
378
379 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
380 let mut b = vec![0u8; s.serialized_len()];
381 match s.serialize_into(&mut b) {
382 Ok(n) => b.truncate(n),
383 Err(_) => b.clear(),
384 }
385 b
386 }
387
388 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
390 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
391 v.extend_from_slice(spdu);
392 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
393 v
394 }
395
396 #[test]
397 fn init_resets_and_opens_transport() {
398 let mut s = CiStack::new();
399 let a = s.handle(Event::Host(HostRequest::Init));
400 assert_eq!(a[0], Action::Reset);
401 assert_eq!(a[1], Action::QuerySlot);
402 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
403 }
404
405 #[test]
406 fn full_pipeline_opens_a_session_for_a_provided_resource() {
407 let mut s = CiStack::new();
408 s.handle(Event::Host(HostRequest::Init));
409 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
411 let osr = ser(&OpenSessionRequest {
414 resource: RESOURCE_MANAGER,
415 });
416 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
417
418 assert!(actions.iter().any(|x| matches!(
420 x,
421 Action::Notify(Notification::SessionOpened {
422 resource
423 }) if *resource == RESOURCE_MANAGER
424 )));
425 let wrote_osr = actions.iter().any(|x| match x {
427 Action::Write(w) => w
428 .windows(1)
429 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
430 _ => false,
431 });
432 assert!(wrote_osr, "open_session_response must be sent down");
433
434 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
436 assert!(nb.is_some());
437 }
438
439 #[test]
440 fn tick_drives_poll_when_active() {
441 let mut s = CiStack::new();
442 s.handle(Event::Host(HostRequest::Init));
443 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
444 let a = s.handle(Event::Tick {
445 elapsed: DEFAULT_POLL_INTERVAL,
446 });
447 assert!(a
448 .iter()
449 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST))));
450 }
451
452 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
458 let mut all = Vec::new();
459 for _ in 0..16 {
460 let a = s.handle(Event::Readable(&[
461 tpdu_tags::SB,
462 0x02,
463 0x01,
464 SbValue::new(false).0,
465 ]));
466 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
467 all.extend(a);
468 if !wrote {
469 break;
470 }
471 }
472 all
473 }
474
475 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
478 use dvb_ci::spdu::SessionNumber;
479 let mut spdu = ser(&SessionNumber { session_nb });
480 spdu.extend_from_slice(apdu);
481 r_data(1, &spdu)
482 }
483
484 fn build_pmt() -> Vec<u8> {
487 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
488 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
489 let mut program_info = Vec::new();
490 program_info.extend_from_slice(&prog_ca);
491 program_info.extend_from_slice(®);
492 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
493
494 let mut body = Vec::new();
495 body.push(0x02); body.push(0);
497 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
501 body.push(0x00);
502 body.push(0xE0 | 0x02); body.push(0x00);
504 let pil = program_info.len();
505 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
506 body.push(pil as u8);
507 body.extend_from_slice(&program_info);
508 body.push(0x03);
510 body.push(0xE0 | 0x02);
511 body.push(0x01);
512 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
513 body.push(lang.len() as u8);
514 body.extend_from_slice(&lang);
515
516 let section_length = body.len() - 3 + 4;
517 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
518 body[2] = section_length as u8;
519 let crc = dvb_common::crc32_mpeg2::compute(&body);
520 body.extend_from_slice(&crc.to_be_bytes());
521 body
522 }
523
524 fn stack_with_ca_session() -> CiStack {
529 use dvb_ci::objects::ca_info::CaInfo;
530 use dvb_ci::objects::resource_manager::Profile;
531 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
532 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
533
534 let mut s = CiStack::new();
535 s.handle(Event::Host(HostRequest::Init));
536 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
537 s.handle(Event::Readable(&r_data(
539 1,
540 &ser(&OpenSessionRequest {
541 resource: RESOURCE_MANAGER,
542 }),
543 )));
544 s.handle(Event::Readable(&r_apdu(
547 1,
548 &ser(&Profile {
549 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI],
550 }),
551 )));
552 pump_sbs(&mut s); for (nb, res) in [
556 (2u16, APPLICATION_INFORMATION),
557 (3, CONDITIONAL_ACCESS_SUPPORT),
558 (4, MMI),
559 ] {
560 s.handle(Event::Readable(&r_data(
561 1,
562 &ser(&CreateSessionResponse {
563 status: SessionStatus::Ok,
564 resource: res,
565 session_nb: nb,
566 }),
567 )));
568 pump_sbs(&mut s);
569 }
570 let ca_nb = s
572 .session
573 .sessions()
574 .into_iter()
575 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
576 .map(|(n, _)| n)
577 .expect("CA session open");
578 s.handle(Event::Readable(&r_apdu(
579 ca_nb,
580 &ser(&CaInfo {
581 ca_system_ids: vec![0x0B00, 0x1800],
582 }),
583 )));
584 s
585 }
586
587 #[test]
588 fn descramble_sends_ok_descrambling_filtered() {
589 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
590 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
591 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
592
593 let mut s = stack_with_ca_session();
594 let ca_nb = s
595 .session
596 .sessions()
597 .into_iter()
598 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
599 .map(|(n, _)| n)
600 .unwrap();
601
602 let pmt = build_pmt();
605 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
606 actions.extend(pump_sbs(&mut s));
609 let c = first_ca_pmt(&actions).expect("ca_pmt sent");
610 assert_eq!(c.cmd_id, CaPmtCmdId::OkDescrambling);
611 assert_eq!(
613 c.program_ca_descriptors.as_slice(),
614 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
615 );
616
617 let reply = s.handle(Event::Readable(&r_apdu(
619 ca_nb,
620 &ser(&CaPmtReply {
621 program_number: 1,
622 version_number: 1,
623 current_next_indicator: true,
624 ca_enable: Some(CaEnable::Possible),
625 streams: vec![],
626 }),
627 )));
628 assert!(reply.iter().any(|a| matches!(
629 a,
630 Action::Notify(Notification::CaPmtReply {
631 descrambling_ok: true,
632 ..
633 })
634 )));
635 }
636
637 fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
639 actions
640 .iter()
641 .any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
642 }
643
644 #[test]
645 fn mmi_menu_answer_sends_menu_answ() {
646 let mut s = stack_with_ca_session();
647 let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
648 acts.extend(pump_sbs(&mut s));
649 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
651 }
652
653 #[test]
654 fn mmi_enquiry_answer_sends_answ() {
655 let mut s = stack_with_ca_session();
656 let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
657 acts.extend(pump_sbs(&mut s));
658 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
660 }
661
662 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
665 use dvb_ci::objects::ca_pmt::CaPmt;
666 use dvb_common::Parse;
667 let tag = [0x9F, 0x80, 0x32];
668 let mut out = Vec::new();
669 for a in actions {
670 if let Action::Write(w) = a {
671 if let Some(pos) = w.windows(3).position(|x| x == tag) {
672 if let Ok(p) = CaPmt::parse(&w[pos..]) {
673 out.push(CaPmtSummary {
674 list_management: p.list_management,
675 cmd_id: p.cmd_id.expect("programme cmd_id present"),
676 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
677 });
678 }
679 }
680 }
681 }
682 out
683 }
684
685 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
687 all_ca_pmts(actions).into_iter().next()
688 }
689
690 struct CaPmtSummary {
691 list_management: dvb_ci::objects::ca_pmt::CaPmtListManagement,
692 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
693 program_ca_descriptors: Vec<u8>,
694 }
695
696 #[test]
697 fn descramble_programs_emits_first_more_last() {
698 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
699
700 let mut s = stack_with_ca_session();
701 let pmt = build_pmt();
702 let mut acts = s.handle(Event::Host(HostRequest::DescramblePrograms(&[
704 &pmt, &pmt, &pmt,
705 ])));
706 acts.extend(pump_sbs(&mut s));
707 let lms: Vec<_> = all_ca_pmts(&acts)
708 .iter()
709 .map(|c| c.list_management)
710 .collect();
711 assert_eq!(
712 lms,
713 vec![
714 CaPmtListManagement::First,
715 CaPmtListManagement::More,
716 CaPmtListManagement::Last,
717 ]
718 );
719 assert!(all_ca_pmts(&acts)
720 .iter()
721 .all(|c| c.cmd_id == CaPmtCmdId::OkDescrambling));
722 }
723
724 #[test]
725 fn add_and_remove_program_use_add_update() {
726 use dvb_ci::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
727
728 let mut s = stack_with_ca_session();
729 let pmt = build_pmt();
730
731 let mut add = s.handle(Event::Host(HostRequest::AddProgram(&pmt)));
732 add.extend(pump_sbs(&mut s));
733 let a = first_ca_pmt(&add).expect("add ca_pmt");
734 assert_eq!(a.list_management, CaPmtListManagement::Add);
735 assert_eq!(a.cmd_id, CaPmtCmdId::OkDescrambling);
736
737 let mut rm = s.handle(Event::Host(HostRequest::RemoveProgram(&pmt)));
738 rm.extend(pump_sbs(&mut s));
739 let r = first_ca_pmt(&rm).expect("remove ca_pmt");
740 assert_eq!(r.list_management, CaPmtListManagement::Update);
741 assert_eq!(r.cmd_id, CaPmtCmdId::NotSelected);
742 }
743}