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::resource::{ResourceId, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, RESOURCE_MANAGER};
18use dvb_common::Parse;
19use dvb_si::tables::pmt::PmtSection;
20
21pub struct CiStack {
23 transport: Transport,
24 session: SessionLayer,
25 provided: Vec<ResourceId>,
27 resources: Vec<Box<dyn Resource>>,
29 cam_caids: Vec<u16>,
32 pending_descramble: Option<Vec<u8>>,
35}
36
37impl Default for CiStack {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl CiStack {
44 #[must_use]
48 pub fn new() -> Self {
49 let provided = vec![RESOURCE_MANAGER, DATE_TIME];
52 Self {
53 transport: Transport::new(1),
54 session: SessionLayer::new(),
55 resources: vec![
56 Box::new(ResourceManager::new(provided.clone())),
57 Box::new(ApplicationInformation),
58 Box::new(ConditionalAccess),
59 Box::new(DateTime::new()),
60 Box::new(Mmi),
61 ],
62 provided,
63 cam_caids: Vec::new(),
64 pending_descramble: None,
65 }
66 }
67
68 pub fn register(&mut self, resource: Box<dyn Resource>) -> &mut Self {
70 self.resources.push(resource);
71 self
72 }
73
74 fn handler_index(&self, resource: ResourceId) -> Option<usize> {
76 self.resources.iter().position(|r| r.id() == resource)
77 }
78
79 pub fn handle(&mut self, event: Event<'_>) -> Vec<Action> {
81 match event {
82 Event::Host(HostRequest::Init) => {
83 let mut actions = vec![Action::Reset, Action::QuerySlot];
84 let out = self.transport.init();
85 actions.extend(self.emit_transport(out));
86 actions
87 }
88 Event::Tick { elapsed } => {
89 let out = self.transport.tick(elapsed);
90 let mut actions = self.emit_transport(out);
91 for (session_nb, resource) in self.session.sessions() {
93 if let Some(i) = self.handler_index(resource) {
94 let out = self.resources[i].tick(elapsed);
95 actions.extend(self.process_resource_out(session_nb, out));
96 }
97 }
98 actions
99 }
100 Event::Readable(frame) => {
101 let out = self.transport.on_frame(frame);
102 self.emit_transport(out)
103 }
104 Event::Host(HostRequest::SendCaPmt(apdu)) => {
105 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, apdu)
106 }
107 Event::Host(HostRequest::Descramble(pmt)) => self.descramble(pmt),
108 Event::Host(HostRequest::Shutdown) => Vec::new(),
109 }
110 }
111
112 fn on_ca_notification(&mut self, note: &Notification) -> Vec<Action> {
116 match note {
117 Notification::CaInfo { ca_system_ids } => {
118 self.cam_caids = ca_system_ids.clone();
119 Vec::new()
120 }
121 Notification::CaPmtReply {
122 descrambling_ok, ..
123 } => match self.pending_descramble.take() {
124 Some(pmt) if *descrambling_ok => {
125 match self.build_ca_pmt_bytes(&pmt, CaPmtCmdId::OkDescrambling) {
126 Ok(bytes) => self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes),
127 Err(detail) => vec![Action::Notify(Notification::Error { detail })],
128 }
129 }
130 _ => Vec::new(),
131 },
132 _ => Vec::new(),
133 }
134 }
135
136 fn descramble(&mut self, pmt: &[u8]) -> Vec<Action> {
140 let bytes = match self.build_ca_pmt_bytes(pmt, CaPmtCmdId::Query) {
141 Ok(b) => b,
142 Err(detail) => return vec![Action::Notify(Notification::Error { detail })],
143 };
144 self.pending_descramble = Some(pmt.to_vec());
145 self.send_to_resource(CONDITIONAL_ACCESS_SUPPORT, &bytes)
146 }
147
148 fn build_ca_pmt_bytes(&self, pmt: &[u8], cmd_id: CaPmtCmdId) -> Result<Vec<u8>, String> {
152 let parsed = PmtSection::parse(pmt).map_err(|e| format!("invalid PMT: {e}"))?;
153 let lm = CaPmtListManagement::Only;
154 let built = if self.cam_caids.is_empty() {
155 build_ca_pmt(&parsed, lm, cmd_id)
156 } else {
157 build_ca_pmt_for_caids(&parsed, &self.cam_caids, lm, cmd_id)
158 };
159 Ok(built.to_bytes())
160 }
161
162 fn send_to_resource(&mut self, resource: ResourceId, apdu: &[u8]) -> Vec<Action> {
164 let nb = (1u16..=u16::MAX).find(|&n| self.session.resource_of(n) == Some(resource));
166 match nb {
167 Some(nb) => {
168 let spdu = self.session.send_apdu(nb, apdu);
169 let out = self.transport.send_spdu(&spdu);
170 self.emit_transport(out)
171 }
172 None => vec![Action::Notify(Notification::Error {
173 detail: format!("no open session for resource {}", resource.name()),
174 })],
175 }
176 }
177
178 fn emit_transport(&mut self, out: TransportOut) -> Vec<Action> {
181 let mut actions = Vec::new();
182 for w in out.writes {
183 actions.push(Action::Write(w));
184 }
185 if let Some(after) = out.timer {
186 actions.push(Action::SetTimer { after });
187 }
188 if let Some(err) = out.error {
189 actions.push(Action::Notify(Notification::Error {
190 detail: err.to_string(),
191 }));
192 }
193 for spdu in out.spdus {
194 actions.extend(self.drive_session(&spdu));
195 }
196 actions
197 }
198
199 fn drive_session(&mut self, spdu: &[u8]) -> Vec<Action> {
201 let provided = self.provided.clone();
202 let SessionOut {
203 spdus,
204 apdus,
205 opened,
206 closed,
207 } = self.session.on_spdu(spdu, |r| provided.contains(&r));
208
209 let mut actions = Vec::new();
210 for s in spdus {
212 actions.extend(self.send_spdu_actions(&s));
213 }
214 for (session_nb, resource) in opened {
215 actions.push(Action::Notify(Notification::SessionOpened { resource }));
216 if let Some(i) = self.handler_index(resource) {
218 let out = self.resources[i].on_open();
219 actions.extend(self.process_resource_out(session_nb, out));
220 }
221 }
222 for session_nb in closed {
223 actions.push(Action::Notify(Notification::SessionClosed { session_nb }));
224 }
225 for (session_nb, apdu) in apdus {
227 if let Some(resource) = self.session.resource_of(session_nb) {
228 if let Some(i) = self.handler_index(resource) {
229 let out = self.resources[i].on_apdu(&apdu);
230 actions.extend(self.process_resource_out(session_nb, out));
231 }
232 }
233 }
234 actions
235 }
236
237 fn send_spdu_actions(&mut self, spdu: &[u8]) -> Vec<Action> {
239 let t = self.transport.send_spdu(spdu);
240 let mut actions = Vec::new();
241 for w in t.writes {
242 actions.push(Action::Write(w));
243 }
244 if let Some(after) = t.timer {
245 actions.push(Action::SetTimer { after });
246 }
247 actions
248 }
249
250 fn process_resource_out(&mut self, session_nb: u16, out: ResourceOut) -> Vec<Action> {
253 let mut actions = Vec::new();
254 for apdu in out.apdus {
255 let spdu = self.session.send_apdu(session_nb, &apdu);
256 actions.extend(self.send_spdu_actions(&spdu));
257 }
258 for note in out.notify {
259 let follow = self.on_ca_notification(¬e);
261 actions.push(Action::Notify(note));
262 actions.extend(follow);
263 }
264 for resource in out.open {
265 let spdu = self.session.create_session(resource);
266 actions.extend(self.send_spdu_actions(&spdu));
267 }
268 actions
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::transport::DEFAULT_POLL_INTERVAL;
276 use dvb_ci::resource::RESOURCE_MANAGER;
277 use dvb_ci::spdu::{tags as spdu_tags, OpenSessionRequest};
278 use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
279 use dvb_common::Serialize;
280
281 fn ser<S: Serialize>(s: &S) -> Vec<u8> {
282 let mut b = vec![0u8; s.serialized_len()];
283 match s.serialize_into(&mut b) {
284 Ok(n) => b.truncate(n),
285 Err(_) => b.clear(),
286 }
287 b
288 }
289
290 fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
292 let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
293 v.extend_from_slice(spdu);
294 v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
295 v
296 }
297
298 #[test]
299 fn init_resets_and_opens_transport() {
300 let mut s = CiStack::new();
301 let a = s.handle(Event::Host(HostRequest::Init));
302 assert_eq!(a[0], Action::Reset);
303 assert_eq!(a[1], Action::QuerySlot);
304 assert!(matches!(&a[2], Action::Write(w) if w[0] == tpdu_tags::CREATE_T_C));
305 }
306
307 #[test]
308 fn full_pipeline_opens_a_session_for_a_provided_resource() {
309 let mut s = CiStack::new();
310 s.handle(Event::Host(HostRequest::Init));
311 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
313 let osr = ser(&OpenSessionRequest {
316 resource: RESOURCE_MANAGER,
317 });
318 let actions = s.handle(Event::Readable(&r_data(1, &osr)));
319
320 assert!(actions.iter().any(|x| matches!(
322 x,
323 Action::Notify(Notification::SessionOpened {
324 resource
325 }) if *resource == RESOURCE_MANAGER
326 )));
327 let wrote_osr = actions.iter().any(|x| match x {
329 Action::Write(w) => w
330 .windows(1)
331 .any(|_| w.contains(&spdu_tags::OPEN_SESSION_RESPONSE)),
332 _ => false,
333 });
334 assert!(wrote_osr, "open_session_response must be sent down");
335
336 let nb = (1u16..16).find(|&n| s.session.resource_of(n).is_some());
338 assert!(nb.is_some());
339 }
340
341 #[test]
342 fn tick_drives_poll_when_active() {
343 let mut s = CiStack::new();
344 s.handle(Event::Host(HostRequest::Init));
345 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
346 let a = s.handle(Event::Tick {
347 elapsed: DEFAULT_POLL_INTERVAL,
348 });
349 assert!(a
350 .iter()
351 .any(|x| matches!(x, Action::Write(w) if w.first() == Some(&tpdu_tags::DATA_LAST))));
352 }
353
354 fn pump_sbs(s: &mut CiStack) -> Vec<Action> {
360 let mut all = Vec::new();
361 for _ in 0..16 {
362 let a = s.handle(Event::Readable(&[
363 tpdu_tags::SB,
364 0x02,
365 0x01,
366 SbValue::new(false).0,
367 ]));
368 let wrote = a.iter().any(|x| matches!(x, Action::Write(_)));
369 all.extend(a);
370 if !wrote {
371 break;
372 }
373 }
374 all
375 }
376
377 fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
380 use dvb_ci::spdu::SessionNumber;
381 let mut spdu = ser(&SessionNumber { session_nb });
382 spdu.extend_from_slice(apdu);
383 r_data(1, &spdu)
384 }
385
386 fn build_pmt() -> Vec<u8> {
389 let prog_ca = [0x09u8, 0x04, 0x0B, 0x00, 0xE1, 0x00];
390 let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
391 let mut program_info = Vec::new();
392 program_info.extend_from_slice(&prog_ca);
393 program_info.extend_from_slice(®);
394 let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
395
396 let mut body = Vec::new();
397 body.push(0x02); body.push(0);
399 body.push(0); body.extend_from_slice(&[0x00, 0x01]); body.push(0xC3); body.push(0x00);
403 body.push(0x00);
404 body.push(0xE0 | 0x02); body.push(0x00);
406 let pil = program_info.len();
407 body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
408 body.push(pil as u8);
409 body.extend_from_slice(&program_info);
410 body.push(0x03);
412 body.push(0xE0 | 0x02);
413 body.push(0x01);
414 body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
415 body.push(lang.len() as u8);
416 body.extend_from_slice(&lang);
417
418 let section_length = body.len() - 3 + 4;
419 body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
420 body[2] = section_length as u8;
421 let crc = dvb_common::crc32_mpeg2::compute(&body);
422 body.extend_from_slice(&crc.to_be_bytes());
423 body
424 }
425
426 fn stack_with_ca_session() -> CiStack {
429 use dvb_ci::objects::ca_info::CaInfo;
430 use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
431 use dvb_ci::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI};
432 use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
433
434 let mut s = CiStack::new();
435 s.handle(Event::Host(HostRequest::Init));
436 s.handle(Event::Readable(&[tpdu_tags::C_T_C_REPLY, 0x01, 0x01]));
437 s.handle(Event::Readable(&r_data(
439 1,
440 &ser(&OpenSessionRequest {
441 resource: RESOURCE_MANAGER,
442 }),
443 )));
444 s.handle(Event::Readable(&r_apdu(1, &ser(&ProfileEnq))));
446 s.handle(Event::Readable(&r_apdu(
447 1,
448 &ser(&Profile {
449 resources: vec![
450 RESOURCE_MANAGER,
451 APPLICATION_INFORMATION,
452 CONDITIONAL_ACCESS_SUPPORT,
453 MMI,
454 ],
455 }),
456 )));
457 for (nb, res) in [
461 (2u16, APPLICATION_INFORMATION),
462 (3, CONDITIONAL_ACCESS_SUPPORT),
463 (4, MMI),
464 ] {
465 s.handle(Event::Readable(&r_data(
466 1,
467 &ser(&CreateSessionResponse {
468 status: SessionStatus::Ok,
469 resource: res,
470 session_nb: nb,
471 }),
472 )));
473 }
474 let ca_nb = s
476 .session
477 .sessions()
478 .into_iter()
479 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
480 .map(|(n, _)| n)
481 .expect("CA session open");
482 s.handle(Event::Readable(&r_apdu(
483 ca_nb,
484 &ser(&CaInfo {
485 ca_system_ids: vec![0x0B00, 0x1800],
486 }),
487 )));
488 s
489 }
490
491 #[test]
492 fn descramble_filters_then_queries_then_oks() {
493 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
494 use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
495 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
496
497 let mut s = stack_with_ca_session();
498 let ca_nb = s
499 .session
500 .sessions()
501 .into_iter()
502 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
503 .map(|(n, _)| n)
504 .unwrap();
505
506 let pmt = build_pmt();
507 let mut query_actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
508 query_actions.extend(pump_sbs(&mut s));
511 let q = first_ca_pmt(&query_actions).expect("ca_pmt query sent");
513 assert_eq!(q.cmd_id, CaPmtCmdId::Query);
514 assert_eq!(
515 q.program_ca_descriptors.as_slice(),
516 &[0x09, 0x04, 0x0B, 0x00, 0xE1, 0x00]
517 );
518
519 let mut ok_actions = s.handle(Event::Readable(&r_apdu(
521 ca_nb,
522 &ser(&CaPmtReply {
523 program_number: 1,
524 version_number: 1,
525 current_next_indicator: true,
526 ca_enable: Some(CaEnable::Possible),
527 streams: vec![],
528 }),
529 )));
530 assert!(ok_actions.iter().any(|a| matches!(
531 a,
532 Action::Notify(Notification::CaPmtReply {
533 descrambling_ok: true,
534 ..
535 })
536 )));
537 ok_actions.extend(pump_sbs(&mut s));
538 assert!(
539 all_ca_pmts(&ok_actions)
540 .iter()
541 .any(|c| c.cmd_id == CaPmtCmdId::OkDescrambling),
542 "ca_pmt ok_descrambling sent after a positive reply"
543 );
544 }
545
546 #[test]
547 fn descramble_reply_not_possible_sends_no_ok() {
548 use dvb_ci::objects::ca_pmt::CaPmtCmdId;
549 use dvb_ci::objects::ca_pmt_reply::CaPmtReply;
550 use dvb_ci::resource::CONDITIONAL_ACCESS_SUPPORT;
551
552 let mut s = stack_with_ca_session();
553 let ca_nb = s
554 .session
555 .sessions()
556 .into_iter()
557 .find(|&(_, r)| r == CONDITIONAL_ACCESS_SUPPORT)
558 .map(|(n, _)| n)
559 .unwrap();
560 let pmt = build_pmt();
561 let mut actions = s.handle(Event::Host(HostRequest::Descramble(&pmt)));
562 actions.extend(s.handle(Event::Readable(&r_apdu(
564 ca_nb,
565 &ser(&CaPmtReply {
566 program_number: 1,
567 version_number: 1,
568 current_next_indicator: true,
569 ca_enable: None,
570 streams: vec![],
571 }),
572 ))));
573 actions.extend(pump_sbs(&mut s));
574 assert!(
576 all_ca_pmts(&actions)
577 .iter()
578 .all(|c| c.cmd_id != CaPmtCmdId::OkDescrambling),
579 "no ok_descrambling without a positive reply"
580 );
581 }
582
583 fn all_ca_pmts(actions: &[Action]) -> Vec<CaPmtSummary> {
586 use dvb_ci::objects::ca_pmt::CaPmt;
587 use dvb_common::Parse;
588 let tag = [0x9F, 0x80, 0x32];
589 let mut out = Vec::new();
590 for a in actions {
591 if let Action::Write(w) = a {
592 if let Some(pos) = w.windows(3).position(|x| x == tag) {
593 if let Ok(p) = CaPmt::parse(&w[pos..]) {
594 out.push(CaPmtSummary {
595 cmd_id: p.cmd_id.expect("programme cmd_id present"),
596 program_ca_descriptors: p.program_ca_descriptors.to_vec(),
597 });
598 }
599 }
600 }
601 }
602 out
603 }
604
605 fn first_ca_pmt(actions: &[Action]) -> Option<CaPmtSummary> {
607 all_ca_pmts(actions).into_iter().next()
608 }
609
610 struct CaPmtSummary {
611 cmd_id: dvb_ci::objects::ca_pmt::CaPmtCmdId,
612 program_ca_descriptors: Vec<u8>,
613 }
614}