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::EnterMenu) => {
135 let apdu = ser_apdu(&dvb_ci::objects::application_info::EnterMenu);
136 self.send_to_resource(APPLICATION_INFORMATION, &apdu)
137 }
138 Event::Host(HostRequest::MmiMenuAnswer(choice_ref)) => {
139 let apdu = ser_apdu(&MenuAnsw { choice_ref });
140 self.send_to_resource(MMI, &apdu)
141 }
142 Event::Host(HostRequest::MmiEnquiryAnswer(text)) => {
143 let apdu = ser_apdu(&Answ {
144 answ_id: AnswId::Answer,
145 text_chars: text,
146 });
147 self.send_to_resource(MMI, &apdu)
148 }
149 Event::Host(HostRequest::MmiCancel) => {
150 let apdu = ser_apdu(&Answ {
151 answ_id: AnswId::Cancel,
152 text_chars: &[],
153 });
154 self.send_to_resource(MMI, &apdu)
155 }
156 Event::Host(HostRequest::Shutdown) => Vec::new(),
157 }
158 }
159
160 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
164 if let Notification::CaInfo { ca_system_ids } = note {
169 self.cam_caids = ca_system_ids.clone();
170 }
171 Vec::new()
172 }
173
174 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
184 let bytes = match self.build_ca_pmt_bytes(pmt, CaPmtCmdId::OkDescrambling) {
185 Ok(b) => b,
186 Err(detail) => return vec![Action::Notify(Notification::Error { detail })],
187 };
188 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes)
189 }
190
191 fn build_ca_pmt_bytes(&self, pmt: &[u8], cmd_id: CaPmtCmdId) -> Result<Vec<u8>, String> {
195 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
196 let lm = CaPmtListManagement::Only;
197 let built = if self.cam_caids.is_empty() {
198 build_ca_pmt(&parsed, lm, cmd_id)
199 } else {
200 build_ca_pmt_for_caids(&parsed, &self.cam_caids, lm, cmd_id)
201 };
202 Ok(built.to_bytes())
203 }
204
205 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
207 let nb = (1u16..=u16::MAX).find(|&n| self.session.resource_of(n) == Some(resource));
209 match nb {
210 Some(nb) => {
211 let spdu = self.session.send_apdu(nb, apdu);
212 let out = self.transport.send_spdu(&spdu);
213 self.emit_transport(out)
214 }
215 None => vec![Action::Notify(Notification::Error {
216 detail: format!("no open session for resource {}", resource.name()),
217 })],
218 }
219 }
220
221 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
224 let mut actions = Vec::new();
225 for w in out.writes {
226 actions.push(Action::Write(w));
227 }
228 if let Some(after) = out.timer {
229 actions.push(Action::SetTimer { after });
230 }
231 if let Some(err) = out.error {
232 actions.push(Action::Notify(Notification::Error {
233 detail: err.to_string(),
234 }));
235 }
236 for spdu in out.spdus {
237 actions.extend(self.drive_session(&spdu));
238 }
239 actions
240 }
241
242 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
244 let host_provided = self.host_provided.clone();
250 let SessionOut {
251 spdus,
252 apdus,
253 opened,
254 closed,
255 } = self.session.on_spdu(spdu, |r| host_provided.contains(&r));
256
257 let mut actions = Vec::new();
258 for s in spdus {
260 actions.extend(self.send_spdu_actions(&s));
261 }
262 for (session_nb, resource) in opened {
263 actions.push(Action::Notify(Notification::SessionOpened { resource }));
264 if let Some(i) = self.handler_index(resource) {
266 let out = self.resources[i].on_open();
267 actions.extend(self.process_resource_out(session_nb, out));
268 }
269 }
270 for session_nb in closed {
271 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
272 }
273 for (session_nb, apdu) in apdus {
275 if let Some(resource) = self.session.resource_of(session_nb) {
276 if let Some(i) = self.handler_index(resource) {
277 let out = self.resources[i].on_apdu(&apdu);
278 actions.extend(self.process_resource_out(session_nb, out));
279 }
280 }
281 }
282 actions
283 }
284
285 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
287 let t = self.transport.send_spdu(spdu);
288 let mut actions = Vec::new();
289 for w in t.writes {
290 actions.push(Action::Write(w));
291 }
292 if let Some(after) = t.timer {
293 actions.push(Action::SetTimer { after });
294 }
295 actions
296 }
297
298 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
301 let mut actions = Vec::new();
302 for apdu in out.apdus {
303 let spdu = self.session.send_apdu(session_nb, &apdu);
304 actions.extend(self.send_spdu_actions(&spdu));
305 }
306 for note in out.notify {
307 let follow = self.on_ca_notification(¬e);
309 actions.push(Action::Notify(note));
310 actions.extend(follow);
311 }
312 for resource in out.open {
313 let spdu = self.session.create_session(resource);
314 actions.extend(self.send_spdu_actions(&spdu));
315 }
316 actions
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::transport::DEFAULT_POLL_INTERVAL;
324 use dvb_ci::resource::RESOURCE_MANAGER;
325 use dvb_ci::spdu::{tags as spdu_tags, OpenSessionRequest};
326 use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
327 use dvb_common::Serialize;
328
329 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
330 let mut b = vec![0u8; s.serialized_len()];
331 match s.serialize_into(&mut b) {
332 Ok(n) => b.truncate(n),
333 Err(_) => b.clear(),
334 }
335 b
336 }
337
338 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
340 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
341 v.extend_from_slice(spdu);
342 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
343 v
344 }
345
346 #[test]
347 fn init_resets_and_opens_transport() {
348 let mut s = CiStack::new();
349 let a = s.handle(Event::Host(HostRequest::Init));
350 assert_eq!(a[0], Action::Reset);
351 assert_eq!(a[1], Action::QuerySlot);
352 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
353 }
354
355 #[test]
356 fn full_pipeline_opens_a_session_for_a_provided_resource() {
357 let mut s = CiStack::new();
358 s.handle(Event::Host(HostRequest::Init));
359 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
361 let osr = ser(&OpenSessionRequest {
364 resource: RESOURCE_MANAGER,
365 });
366 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
367
368 assert!(actions.iter().any(|x| matches!(
370 x,
371 Action::Notify(Notification::SessionOpened {
372 resource
373 }) if *resource == RESOURCE_MANAGER
374 )));
375 let wrote_osr = actions.iter().any(|x| match x {
377 Action::Write(w) => w
378 .windows(1)
379 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
380 _ => false,
381 });
382 assert!(wrote_osr, "open_session_response must be sent down");
383
384 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
386 assert!(nb.is_some());
387 }
388
389 #[test]
390 fn tick_drives_poll_when_active() {
391 let mut s = CiStack::new();
392 s.handle(Event::Host(HostRequest::Init));
393 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
394 let a = s.handle(Event::Tick {
395 elapsed: DEFAULT_POLL_INTERVAL,
396 });
397 assert!(a
398 .iter()
399 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST))));
400 }
401
402 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
408 let mut all = Vec::new();
409 for _ in 0..16 {
410 let a = s.handle(Event::Readable(&[
411 tpdu_tags::SB,
412 0x02,
413 0x01,
414 SbValue::new(false).0,
415 ]));
416 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
417 all.extend(a);
418 if !wrote {
419 break;
420 }
421 }
422 all
423 }
424
425 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
428 use dvb_ci::spdu::SessionNumber;
429 let mut spdu = ser(&SessionNumber { session_nb });
430 spdu.extend_from_slice(apdu);
431 r_data(1, &spdu)
432 }
433
434 fn build_pmt() -> Vec<u8> {
437 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
438 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
439 let mut program_info = Vec::new();
440 program_info.extend_from_slice(&prog_ca);
441 program_info.extend_from_slice(®);
442 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
443
444 let mut body = Vec::new();
445 body.push(0x02); body.push(0);
447 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
451 body.push(0x00);
452 body.push(0xE0 | 0x02); body.push(0x00);
454 let pil = program_info.len();
455 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
456 body.push(pil as u8);
457 body.extend_from_slice(&program_info);
458 body.push(0x03);
460 body.push(0xE0 | 0x02);
461 body.push(0x01);
462 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
463 body.push(lang.len() as u8);
464 body.extend_from_slice(&lang);
465
466 let section_length = body.len() - 3 + 4;
467 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
468 body[2] = section_length as u8;
469 let crc = dvb_common::crc32_mpeg2::compute(&body);
470 body.extend_from_slice(&crc.to_be_bytes());
471 body
472 }
473
474 fn stack_with_ca_session() -> CiStack {
479 use dvb_ci::objects::ca_info::CaInfo;
480 use dvb_ci::objects::resource_manager::Profile;
481 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
482 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
483
484 let mut s = CiStack::new();
485 s.handle(Event::Host(HostRequest::Init));
486 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
487 s.handle(Event::Readable(&r_data(
489 1,
490 &ser(&OpenSessionRequest {
491 resource: RESOURCE_MANAGER,
492 }),
493 )));
494 s.handle(Event::Readable(&r_apdu(
497 1,
498 &ser(&Profile {
499 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI],
500 }),
501 )));
502 pump_sbs(&mut s); for (nb, res) in [
506 (2u16, APPLICATION_INFORMATION),
507 (3, CONDITIONAL_ACCESS_SUPPORT),
508 (4, MMI),
509 ] {
510 s.handle(Event::Readable(&r_data(
511 1,
512 &ser(&CreateSessionResponse {
513 status: SessionStatus::Ok,
514 resource: res,
515 session_nb: nb,
516 }),
517 )));
518 pump_sbs(&mut s);
519 }
520 let ca_nb = s
522 .session
523 .sessions()
524 .into_iter()
525 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
526 .map(|(n, _)| n)
527 .expect("CA session open");
528 s.handle(Event::Readable(&r_apdu(
529 ca_nb,
530 &ser(&CaInfo {
531 ca_system_ids: vec![0x0B00, 0x1800],
532 }),
533 )));
534 s
535 }
536
537 #[test]
538 fn descramble_sends_ok_descrambling_filtered() {
539 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
540 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
541 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
542
543 let mut s = stack_with_ca_session();
544 let ca_nb = s
545 .session
546 .sessions()
547 .into_iter()
548 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
549 .map(|(n, _)| n)
550 .unwrap();
551
552 let pmt = build_pmt();
555 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
556 actions.extend(pump_sbs(&mut s));
559 let c = first_ca_pmt(&actions).expect("ca_pmt sent");
560 assert_eq!(c.cmd_id, CaPmtCmdId::OkDescrambling);
561 assert_eq!(
563 c.program_ca_descriptors.as_slice(),
564 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
565 );
566
567 let reply = s.handle(Event::Readable(&r_apdu(
569 ca_nb,
570 &ser(&CaPmtReply {
571 program_number: 1,
572 version_number: 1,
573 current_next_indicator: true,
574 ca_enable: Some(CaEnable::Possible),
575 streams: vec![],
576 }),
577 )));
578 assert!(reply.iter().any(|a| matches!(
579 a,
580 Action::Notify(Notification::CaPmtReply {
581 descrambling_ok: true,
582 ..
583 })
584 )));
585 }
586
587 fn wrote_apdu(actions: &[Action], want: [u8; 3]) -> bool {
589 actions
590 .iter()
591 .any(|a| matches!(a, Action::Write(w) if w.windows(3).any(|x| x == want)))
592 }
593
594 #[test]
595 fn mmi_menu_answer_sends_menu_answ() {
596 let mut s = stack_with_ca_session();
597 let mut acts = s.handle(Event::Host(HostRequest::MmiMenuAnswer(2)));
598 acts.extend(pump_sbs(&mut s));
599 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x0B]));
601 }
602
603 #[test]
604 fn mmi_enquiry_answer_sends_answ() {
605 let mut s = stack_with_ca_session();
606 let mut acts = s.handle(Event::Host(HostRequest::MmiEnquiryAnswer(b"1234")));
607 acts.extend(pump_sbs(&mut s));
608 assert!(wrote_apdu(&acts, [0x9F, 0x88, 0x08]));
610 }
611
612 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
615 use dvb_ci::objects::ca_pmt::CaPmt;
616 use dvb_common::Parse;
617 let tag = [0x9F, 0x80, 0x32];
618 let mut out = Vec::new();
619 for a in actions {
620 if let Action::Write(w) = a {
621 if let Some(pos) = w.windows(3).position(|x| x == tag) {
622 if let Ok(p) = CaPmt::parse(&w[pos..]) {
623 out.push(CaPmtSummary {
624 cmd_id: p.cmd_id.expect("programme cmd_id present"),
625 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
626 });
627 }
628 }
629 }
630 }
631 out
632 }
633
634 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
636 all_ca_pmts(actions).into_iter().next()
637 }
638
639 struct CaPmtSummary {
640 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
641 program_ca_descriptors: Vec<u8>,
642 }
643}