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