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