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::{ResourceId, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI, RESOURCE_MANAGER};
19use dvb_common::{Parse, Serialize};
20use dvb_si::tables::pmt::PmtSection;
21
22fn ser_apdu<S: Serialize>(s: &S) -> Vec<u8> {
24 let mut b = vec![0u8; s.serialized_len()];
25 match s.serialize_into(&mut b) {
26 Ok(n) => b.truncate(n),
27 Err(_) => b.clear(),
28 }
29 b
30}
31
32pub struct CiStack {
34 transport: Transport,
35 session: SessionLayer,
36 provided: Vec<ResourceId>,
38 resources: Vec<Box<dyn Resource>>,
40 cam_caids: Vec<u16>,
43 pending_descramble: Option<Vec<u8>>,
46}
47
48impl Default for CiStack {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl CiStack {
55 #[must_use]
59 pub fn new() -> Self {
60 let provided = vec![RESOURCE_MANAGER, DATE_TIME];
63 Self {
64 transport: Transport::new(1),
65 session: SessionLayer::new(),
66 resources: vec![
67 Box::new(ResourceManager::new(provided.clone())),
68 Box::new(ApplicationInformation),
69 Box::new(ConditionalAccess),
70 Box::new(DateTime::new()),
71 Box::new(Mmi),
72 ],
73 provided,
74 cam_caids: Vec::new(),
75 pending_descramble: None,
76 }
77 }
78
79 pub fn register(&mut self, resource: Box<dyn Resource>) -> &mut Self {
81 self.resources.push(resource);
82 self
83 }
84
85 fn handler_index(&self, resource: ResourceId) -> Option<usize> {
87 self.resources.iter().position(|r| r.id() == resource)
88 }
89
90 pub fn handle(&mut self, event: Event<'_>) -> Vec<Action> {
92 match event {
93 Event::Host(HostRequest::Init) => {
94 let mut actions = vec![Action::Reset, Action::QuerySlot];
95 let out = self.transport.init();
96 actions.extend(self.emit_transport(out));
97 actions
98 }
99 Event::Tick { elapsed } => {
100 let out = self.transport.tick(elapsed);
101 let mut actions = self.emit_transport(out);
102 for (session_nb, resource) in self.session.sessions() {
104 if let Some(i) = self.handler_index(resource) {
105 let out = self.resources[i].tick(elapsed);
106 actions.extend(self.process_resource_out(session_nb, out));
107 }
108 }
109 actions
110 }
111 Event::Readable(frame) => {
112 let out = self.transport.on_frame(frame);
113 self.emit_transport(out)
114 }
115 Event::Host(HostRequest::SendCaPmt(apdu)) => {
116 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, apdu)
117 }
118 Event::Host(HostRequest::Descramble(pmt)) => self.descramble(pmt),
119 Event::Host(HostRequest::MmiMenuAnswer(choice_ref)) => {
120 let apdu = ser_apdu(&MenuAnsw { choice_ref });
121 self.send_to_resource(MMI, &apdu)
122 }
123 Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
124 let apdu = ser_apdu(&Answ {
125 answ_id: AnswId::Answer,
126 text_chars: text,
127 });
128 self.send_to_resource(MMI, &apdu)
129 }
130 Event::Host(HostRequest::MmiCancel) => {
131 let apdu = ser_apdu(&Answ {
132 answ_id: AnswId::Cancel,
133 text_chars: &[],
134 });
135 self.send_to_resource(MMI, &apdu)
136 }
137 Event::Host(HostRequest::Shutdown) => Vec::new(),
138 }
139 }
140
141 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
145 match note {
146 Notification::CaInfo { ca_system_ids } => {
147 self.cam_caids = ca_system_ids.clone();
148 Vec::new()
149 }
150 Notification::CaPmtReply {
151 descrambling_ok, ..
152 } => match self.pending_descramble.take() {
153 Some(pmt) if *descrambling_ok => {
154 match self.build_ca_pmt_bytes(&pmt, CaPmtCmdId::OkDescrambling) {
155 Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
156 Err(detail) => vec![Action::Notify(Notification::Error { detail })],
157 }
158 }
159 _ => Vec::new(),
160 },
161 _ => Vec::new(),
162 }
163 }
164
165 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
169 let bytes = match self.build_ca_pmt_bytes(pmt, CaPmtCmdId::Query) {
170 Ok(b) => b,
171 Err(detail) => return vec![Action::Notify(Notification::Error { detail })],
172 };
173 self.pending_descramble = Some(pmt.to_vec());
174 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes)
175 }
176
177 fn build_ca_pmt_bytes(&self, pmt: &[u8], cmd_id: CaPmtCmdId) -> Result<Vec<u8>, String> {
181 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
182 let lm = CaPmtListManagement::Only;
183 let built = if self.cam_caids.is_empty() {
184 build_ca_pmt(&parsed, lm, cmd_id)
185 } else {
186 build_ca_pmt_for_caids(&parsed, &self.cam_caids, lm, cmd_id)
187 };
188 Ok(built.to_bytes())
189 }
190
191 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
193 let nb = (1u16..=u16::MAX).find(|&n| self.session.resource_of(n) == Some(resource));
195 match nb {
196 Some(nb) => {
197 let spdu = self.session.send_apdu(nb, apdu);
198 let out = self.transport.send_spdu(&spdu);
199 self.emit_transport(out)
200 }
201 None => vec![Action::Notify(Notification::Error {
202 detail: format!("no open session for resource {}", resource.name()),
203 })],
204 }
205 }
206
207 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
210 let mut actions = Vec::new();
211 for w in out.writes {
212 actions.push(Action::Write(w));
213 }
214 if let Some(after) = out.timer {
215 actions.push(Action::SetTimer { after });
216 }
217 if let Some(err) = out.error {
218 actions.push(Action::Notify(Notification::Error {
219 detail: err.to_string(),
220 }));
221 }
222 for spdu in out.spdus {
223 actions.extend(self.drive_session(&spdu));
224 }
225 actions
226 }
227
228 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
230 let provided = self.provided.clone();
231 let SessionOut {
232 spdus,
233 apdus,
234 opened,
235 closed,
236 } = self.session.on_spdu(spdu, |r| provided.contains(&r));
237
238 let mut actions = Vec::new();
239 for s in spdus {
241 actions.extend(self.send_spdu_actions(&s));
242 }
243 for (session_nb, resource) in opened {
244 actions.push(Action::Notify(Notification::SessionOpened { resource }));
245 if let Some(i) = self.handler_index(resource) {
247 let out = self.resources[i].on_open();
248 actions.extend(self.process_resource_out(session_nb, out));
249 }
250 }
251 for session_nb in closed {
252 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
253 }
254 for (session_nb, apdu) in apdus {
256 if let Some(resource) = self.session.resource_of(session_nb) {
257 if let Some(i) = self.handler_index(resource) {
258 let out = self.resources[i].on_apdu(&apdu);
259 actions.extend(self.process_resource_out(session_nb, out));
260 }
261 }
262 }
263 actions
264 }
265
266 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
268 let t = self.transport.send_spdu(spdu);
269 let mut actions = Vec::new();
270 for w in t.writes {
271 actions.push(Action::Write(w));
272 }
273 if let Some(after) = t.timer {
274 actions.push(Action::SetTimer { after });
275 }
276 actions
277 }
278
279 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
282 let mut actions = Vec::new();
283 for apdu in out.apdus {
284 let spdu = self.session.send_apdu(session_nb, &apdu);
285 actions.extend(self.send_spdu_actions(&spdu));
286 }
287 for note in out.notify {
288 let follow = self.on_ca_notification(¬e);
290 actions.push(Action::Notify(note));
291 actions.extend(follow);
292 }
293 for resource in out.open {
294 let spdu = self.session.create_session(resource);
295 actions.extend(self.send_spdu_actions(&spdu));
296 }
297 actions
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::transport::DEFAULT_POLL_INTERVAL;
305 use dvb_ci::resource::RESOURCE_MANAGER;
306 use dvb_ci::spdu::{tags as spdu_tags, OpenSessionRequest};
307 use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
308 use dvb_common::Serialize;
309
310 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
311 let mut b = vec![0u8; s.serialized_len()];
312 match s.serialize_into(&mut b) {
313 Ok(n) => b.truncate(n),
314 Err(_) => b.clear(),
315 }
316 b
317 }
318
319 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
321 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
322 v.extend_from_slice(spdu);
323 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
324 v
325 }
326
327 #[test]
328 fn init_resets_and_opens_transport() {
329 let mut s = CiStack::new();
330 let a = s.handle(Event::Host(HostRequest::Init));
331 assert_eq!(a[0], Action::Reset);
332 assert_eq!(a[1], Action::QuerySlot);
333 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
334 }
335
336 #[test]
337 fn full_pipeline_opens_a_session_for_a_provided_resource() {
338 let mut s = CiStack::new();
339 s.handle(Event::Host(HostRequest::Init));
340 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
342 let osr = ser(&OpenSessionRequest {
345 resource: RESOURCE_MANAGER,
346 });
347 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
348
349 assert!(actions.iter().any(|x| matches!(
351 x,
352 Action::Notify(Notification::SessionOpened {
353 resource
354 }) if *resource == RESOURCE_MANAGER
355 )));
356 let wrote_osr = actions.iter().any(|x| match x {
358 Action::Write(w) => w
359 .windows(1)
360 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
361 _ => false,
362 });
363 assert!(wrote_osr, "open_session_response must be sent down");
364
365 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
367 assert!(nb.is_some());
368 }
369
370 #[test]
371 fn tick_drives_poll_when_active() {
372 let mut s = CiStack::new();
373 s.handle(Event::Host(HostRequest::Init));
374 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
375 let a = s.handle(Event::Tick {
376 elapsed: DEFAULT_POLL_INTERVAL,
377 });
378 assert!(a
379 .iter()
380 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST))));
381 }
382
383 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
389 let mut all = Vec::new();
390 for _ in 0..16 {
391 let a = s.handle(Event::Readable(&[
392 tpdu_tags::SB,
393 0x02,
394 0x01,
395 SbValue::new(false).0,
396 ]));
397 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
398 all.extend(a);
399 if !wrote {
400 break;
401 }
402 }
403 all
404 }
405
406 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
409 use dvb_ci::spdu::SessionNumber;
410 let mut spdu = ser(&SessionNumber { session_nb });
411 spdu.extend_from_slice(apdu);
412 r_data(1, &spdu)
413 }
414
415 fn build_pmt() -> Vec<u8> {
418 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
419 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
420 let mut program_info = Vec::new();
421 program_info.extend_from_slice(&prog_ca);
422 program_info.extend_from_slice(®);
423 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
424
425 let mut body = Vec::new();
426 body.push(0x02); body.push(0);
428 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
432 body.push(0x00);
433 body.push(0xE0 | 0x02); body.push(0x00);
435 let pil = program_info.len();
436 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
437 body.push(pil as u8);
438 body.extend_from_slice(&program_info);
439 body.push(0x03);
441 body.push(0xE0 | 0x02);
442 body.push(0x01);
443 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
444 body.push(lang.len() as u8);
445 body.extend_from_slice(&lang);
446
447 let section_length = body.len() - 3 + 4;
448 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
449 body[2] = section_length as u8;
450 let crc = dvb_common::crc32_mpeg2::compute(&body);
451 body.extend_from_slice(&crc.to_be_bytes());
452 body
453 }
454
455 fn stack_with_ca_session() -> CiStack {
458 use dvb_ci::objects::ca_info::CaInfo;
459 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
460 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
461 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
462
463 let mut s = CiStack::new();
464 s.handle(Event::Host(HostRequest::Init));
465 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
466 s.handle(Event::Readable(&r_data(
468 1,
469 &ser(&OpenSessionRequest {
470 resource: RESOURCE_MANAGER,
471 }),
472 )));
473 s.handle(Event::Readable(&r_apdu(1, &ser(&ProfileEnq))));
475 s.handle(Event::Readable(&r_apdu(
476 1,
477 &ser(&Profile {
478 resources: vec![
479 RESOURCE_MANAGER,
480 APPLICATION_INFORMATION,
481 CONDITIONAL_ACCESS_SUPPORT,
482 MMI,
483 ],
484 }),
485 )));
486 for (nb, res) in [
490 (2u16, APPLICATION_INFORMATION),
491 (3, CONDITIONAL_ACCESS_SUPPORT),
492 (4, MMI),
493 ] {
494 s.handle(Event::Readable(&r_data(
495 1,
496 &ser(&CreateSessionResponse {
497 status: SessionStatus::Ok,
498 resource: res,
499 session_nb: nb,
500 }),
501 )));
502 }
503 let ca_nb = s
505 .session
506 .sessions()
507 .into_iter()
508 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
509 .map(|(n, _)| n)
510 .expect("CA session open");
511 s.handle(Event::Readable(&r_apdu(
512 ca_nb,
513 &ser(&CaInfo {
514 ca_system_ids: vec![0x0B00, 0x1800],
515 }),
516 )));
517 s
518 }
519
520 #[test]
521 fn descramble_filters_then_queries_then_oks() {
522 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
523 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
524 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
525
526 let mut s = stack_with_ca_session();
527 let ca_nb = s
528 .session
529 .sessions()
530 .into_iter()
531 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
532 .map(|(n, _)| n)
533 .unwrap();
534
535 let pmt = build_pmt();
536 let mut query_actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
537 query_actions.extend(pump_sbs(&mut s));
540 let q = first_ca_pmt(&query_actions).expect("ca_pmt query sent");
542 assert_eq!(q.cmd_id, CaPmtCmdId::Query);
543 assert_eq!(
544 q.program_ca_descriptors.as_slice(),
545 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
546 );
547
548 let mut ok_actions = s.handle(Event::Readable(&r_apdu(
550 ca_nb,
551 &ser(&CaPmtReply {
552 program_number: 1,
553 version_number: 1,
554 current_next_indicator: true,
555 ca_enable: Some(CaEnable::Possible),
556 streams: vec![],
557 }),
558 )));
559 assert!(ok_actions.iter().any(|a| matches!(
560 a,
561 Action::Notify(Notification::CaPmtReply {
562 descrambling_ok: true,
563 ..
564 })
565 )));
566 ok_actions.extend(pump_sbs(&mut s));
567 assert!(
568 all_ca_pmts(&ok_actions)
569 .iter()
570 .any(|c| c.cmd_id == CaPmtCmdId::OkDescrambling),
571 "ca_pmt ok_descrambling sent after a positive reply"
572 );
573 }
574
575 #[test]
576 fn descramble_reply_not_possible_sends_no_ok() {
577 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
578 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
579 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
580
581 let mut s = stack_with_ca_session();
582 let ca_nb = s
583 .session
584 .sessions()
585 .into_iter()
586 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
587 .map(|(n, _)| n)
588 .unwrap();
589 let pmt = build_pmt();
590 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
591 actions.extend(s.handle(Event::Readable(&r_apdu(
593 ca_nb,
594 &ser(&CaPmtReply {
595 program_number: 1,
596 version_number: 1,
597 current_next_indicator: true,
598 ca_enable: None,
599 streams: vec![],
600 }),
601 ))));
602 actions.extend(pump_sbs(&mut s));
603 assert!(
605 all_ca_pmts(&actions)
606 .iter()
607 .all(|c| c.cmd_id != CaPmtCmdId::OkDescrambling),
608 "no ok_descrambling without a positive reply"
609 );
610 }
611
612 fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
614 actions
615 .iter()
616 .any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
617 }
618
619 #[test]
620 fn mmi_menu_answer_sends_menu_answ() {
621 let mut s = stack_with_ca_session();
622 let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
623 acts.extend(pump_sbs(&mut s));
624 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
626 }
627
628 #[test]
629 fn mmi_enquiry_answer_sends_answ() {
630 let mut s = stack_with_ca_session();
631 let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
632 acts.extend(pump_sbs(&mut s));
633 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
635 }
636
637 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
640 use dvb_ci::objects::ca_pmt::CaPmt;
641 use dvb_common::Parse;
642 let tag = [0x9F, 0x80, 0x32];
643 let mut out = Vec::new();
644 for a in actions {
645 if let Action::Write(w) = a {
646 if let Some(pos) = w.windows(3).position(|x| x == tag) {
647 if let Ok(p) = CaPmt::parse(&w[pos..]) {
648 out.push(CaPmtSummary {
649 cmd_id: p.cmd_id.expect("programme cmd_id present"),
650 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
651 });
652 }
653 }
654 }
655 }
656 out
657 }
658
659 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
661 all_ca_pmts(actions).into_iter().next()
662 }
663
664 struct CaPmtSummary {
665 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
666 program_ca_descriptors: Vec<u8>,
667 }
668}