1use std::time::Duration;
11
12use dvb_ci::objects::application_info::{ApplicationInfo, ApplicationInfoEnq};
13use dvb_ci::objects::ca_info::{CaInfo, CaInfoEnq};
14use dvb_ci::objects::ca_pmt_reply::{CaEnable, CaPmtReply};
15use dvb_ci::objects::date_time::{DateTime as CiDateTime, DateTimeEnq, UTC_TIME_LEN};
16use dvb_ci::objects::mmi_high::{Enq, Menu};
17use dvb_ci::objects::resource_manager::{Profile, ProfileChange, ProfileEnq};
18use dvb_ci::resource::{
19 ResourceId, APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, DATE_TIME, MMI,
20 RESOURCE_MANAGER,
21};
22use dvb_ci::tag::{self, ApduTag};
23use dvb_common::{Parse, Serialize};
24
25use crate::event::{MmiEvent, Notification};
26
27fn text(chars: &[u8]) -> String {
30 String::from_utf8_lossy(chars).into_owned()
31}
32
33pub(crate) fn ser<S: Serialize>(s: &S) -> Vec<u8> {
34 let mut b = vec![0u8; s.serialized_len()];
35 match s.serialize_into(&mut b) {
36 Ok(n) => b.truncate(n),
37 Err(_) => b.clear(),
38 }
39 b
40}
41
42pub(crate) fn peek_tag(apdu: &[u8]) -> Option<ApduTag> {
44 (apdu.len() >= 3).then(|| ApduTag::from_bytes(apdu[0], apdu[1], apdu[2]))
45}
46
47#[derive(Debug, Default, Clone, PartialEq, Eq)]
49pub struct ResourceOut {
50 pub apdus: Vec<Vec<u8>>,
52 pub notify: Vec<Notification>,
54 pub open: Vec<ResourceId>,
56}
57
58pub trait Resource {
60 fn id(&self) -> ResourceId;
62 fn on_open(&mut self) -> ResourceOut {
64 ResourceOut::default()
65 }
66 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut;
68 fn tick(&mut self, _elapsed: Duration) -> ResourceOut {
70 ResourceOut::default()
71 }
72}
73
74#[derive(Debug)]
78pub struct ResourceManager {
79 host_resources: Vec<ResourceId>,
80 module_resources: Vec<ResourceId>,
81 module_profiled: bool,
82 ready: bool,
83}
84
85impl ResourceManager {
86 #[must_use]
88 pub fn new(host_resources: Vec<ResourceId>) -> Self {
89 Self {
90 host_resources,
91 module_resources: Vec::new(),
92 module_profiled: false,
93 ready: false,
94 }
95 }
96
97 #[must_use]
99 pub fn module_resources(&self) -> &[ResourceId] {
100 &self.module_resources
101 }
102}
103
104impl Resource for ResourceManager {
105 fn id(&self) -> ResourceId {
106 RESOURCE_MANAGER
107 }
108
109 fn on_open(&mut self) -> ResourceOut {
110 ResourceOut {
112 apdus: vec![ser(&ProfileEnq)],
113 ..ResourceOut::default()
114 }
115 }
116
117 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
118 let mut out = ResourceOut::default();
119 match peek_tag(apdu) {
120 Some(t) if t == tag::PROFILE_ENQ => {
122 out.apdus.push(ser(&Profile {
123 resources: self.host_resources.clone(),
124 }));
125 }
126 Some(t) if t == tag::PROFILE => {
128 if let Ok(p) = Profile::parse(apdu) {
129 self.module_resources = p.resources;
130 self.module_profiled = true;
131 }
132 }
133 Some(t) if t == tag::PROFILE_CHANGE => {
135 out.apdus.push(ser(&ProfileEnq));
136 self.module_profiled = false;
137 self.ready = false;
138 }
139 _ => {}
140 }
141 if self.module_profiled && !self.ready {
152 self.ready = true;
153 out.apdus.push(ser(&ProfileChange));
154 out.notify.push(Notification::CamReady);
155 out.open.push(APPLICATION_INFORMATION);
163 out.open.push(CONDITIONAL_ACCESS_SUPPORT);
164 out.open.push(MMI);
165 }
166 out
167 }
168}
169
170#[derive(Debug, Default)]
173pub struct ApplicationInformation;
174
175impl Resource for ApplicationInformation {
176 fn id(&self) -> ResourceId {
177 APPLICATION_INFORMATION
178 }
179
180 fn on_open(&mut self) -> ResourceOut {
181 ResourceOut {
182 apdus: vec![ser(&ApplicationInfoEnq)],
183 ..ResourceOut::default()
184 }
185 }
186
187 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
188 let mut out = ResourceOut::default();
189 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
190 if let Ok(ai) = ApplicationInfo::parse(apdu) {
191 out.notify.push(Notification::ApplicationInfo {
192 application_type: ai.application_type.to_u8(),
193 manufacturer: ai.application_manufacturer,
194 code: ai.manufacturer_code,
195 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
196 });
197 }
198 }
199 out
200 }
201}
202
203#[derive(Debug, Default)]
208pub struct ConditionalAccess;
209
210impl Resource for ConditionalAccess {
211 fn id(&self) -> ResourceId {
212 CONDITIONAL_ACCESS_SUPPORT
213 }
214
215 fn on_open(&mut self) -> ResourceOut {
216 ResourceOut {
217 apdus: vec![ser(&CaInfoEnq)],
218 ..ResourceOut::default()
219 }
220 }
221
222 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
223 let mut out = ResourceOut::default();
224 match peek_tag(apdu) {
225 Some(t) if t == tag::CA_INFO => {
226 if let Ok(ci) = CaInfo::parse(apdu) {
227 out.notify.push(Notification::CaInfo {
228 ca_system_ids: ci.ca_system_ids,
229 });
230 }
231 }
232 Some(t) if t == tag::CA_PMT_REPLY => {
233 if let Ok(r) = CaPmtReply::parse(apdu) {
234 let descrambling_ok = r.ca_enable.is_some_and(|e| {
235 matches!(
236 e,
237 CaEnable::Possible
238 | CaEnable::PossiblePurchaseDialogue
239 | CaEnable::PossibleTechnicalDialogue
240 )
241 });
242 out.notify.push(Notification::CaPmtReply {
243 program_number: r.program_number,
244 descrambling_ok,
245 });
246 }
247 }
248 _ => {}
249 }
250 out
251 }
252}
253
254const SECS_PER_DAY: u64 = 86_400;
255const MJD_UNIX_EPOCH: u64 = 40_587;
257
258fn bcd(v: u64) -> u8 {
259 (((v / 10) << 4) | (v % 10)) as u8
260}
261
262fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
265 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
266 let sod = unix_secs % SECS_PER_DAY;
267 [
268 (mjd >> 8) as u8,
269 mjd as u8,
270 bcd(sod / 3600),
271 bcd((sod % 3600) / 60),
272 bcd(sod % 60),
273 ]
274}
275
276fn system_utc() -> [u8; UTC_TIME_LEN] {
277 let secs = std::time::SystemTime::now()
278 .duration_since(std::time::UNIX_EPOCH)
279 .map(|d| d.as_secs())
280 .unwrap_or(0);
281 unix_to_mjd_bcd(secs)
282}
283
284pub struct DateTime {
288 clock: fn() -> [u8; UTC_TIME_LEN],
289 interval: u8,
290 since: Duration,
291}
292
293impl Default for DateTime {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299impl DateTime {
300 #[must_use]
302 pub fn new() -> Self {
303 Self {
304 clock: system_utc,
305 interval: 0,
306 since: Duration::ZERO,
307 }
308 }
309
310 #[must_use]
312 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
313 Self {
314 clock,
315 interval: 0,
316 since: Duration::ZERO,
317 }
318 }
319
320 fn reply(&self) -> Vec<u8> {
321 ser(&CiDateTime {
322 utc_time: (self.clock)(),
323 local_offset: None,
324 })
325 }
326}
327
328impl Resource for DateTime {
329 fn id(&self) -> ResourceId {
330 DATE_TIME
331 }
332
333 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
334 let mut out = ResourceOut::default();
335 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
336 if let Ok(enq) = DateTimeEnq::parse(apdu) {
337 self.interval = enq.response_interval;
338 self.since = Duration::ZERO;
339 out.apdus.push(self.reply());
340 }
341 }
342 out
343 }
344
345 fn tick(&mut self, elapsed: Duration) -> ResourceOut {
346 let mut out = ResourceOut::default();
347 if self.interval > 0 {
348 self.since += elapsed;
349 if self.since >= Duration::from_secs(u64::from(self.interval)) {
350 self.since = Duration::ZERO;
351 out.apdus.push(self.reply());
352 }
353 }
354 out
355 }
356}
357
358#[derive(Debug, Default)]
363pub struct Mmi;
364
365impl Resource for Mmi {
366 fn id(&self) -> ResourceId {
367 MMI
368 }
369
370 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
371 let mut out = ResourceOut::default();
372 match peek_tag(apdu) {
373 Some(t) if t == tag::ENQ => {
374 if let Ok(e) = Enq::parse(apdu) {
375 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
376 prompt: text(e.text_chars),
377 blind: e.blind_answer,
378 answer_len: e.answer_text_length,
379 }));
380 }
381 }
382 Some(t) if t == tag::MENU_LAST => {
383 if let Ok(m) = Menu::parse(apdu) {
384 out.notify.push(Notification::Mmi(MmiEvent::Menu {
385 title: text(m.title.text_chars),
386 items: m.choices.iter().map(|c| text(c.text_chars)).collect(),
387 }));
388 }
389 }
390 Some(t) if t == tag::CLOSE_MMI => {
391 out.notify.push(Notification::Mmi(MmiEvent::Close));
392 }
393 _ => {}
394 }
395 out
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use dvb_ci::objects::resource_manager::Profile;
403
404 #[test]
405 fn on_open_sends_profile_enq() {
406 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
407 let out = rm.on_open();
408 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
409 }
410
411 #[test]
412 fn module_profile_triggers_profile_change_camready_and_opens() {
413 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
419 rm.on_open();
420 let empty_profile = ser(&Profile { resources: vec![] });
421 let o = rm.on_apdu(&empty_profile);
422 assert!(o.notify.contains(&Notification::CamReady));
423 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
424 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
425 assert!(o.open.contains(&APPLICATION_INFORMATION));
426 assert!(o.open.contains(&CONDITIONAL_ACCESS_SUPPORT));
427 assert!(o.open.contains(&MMI));
428 }
429
430 #[test]
431 fn answers_a_module_profile_enquiry_without_re_readying() {
432 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
433 rm.on_open();
434 rm.on_apdu(&ser(&Profile {
435 resources: vec![APPLICATION_INFORMATION],
436 }));
437 let o = rm.on_apdu(&ser(&ProfileEnq));
439 assert_eq!(o.apdus.len(), 1);
440 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
441 assert!(!o.notify.contains(&Notification::CamReady));
442 }
443
444 #[test]
445 fn mmi_surfaces_enquiry_and_close() {
446 let mut h = Mmi;
447 let enq = ser(&Enq {
449 blind_answer: true,
450 answer_text_length: 4,
451 text_chars: b"PIN?",
452 });
453 assert_eq!(
454 h.on_apdu(&enq).notify,
455 vec![Notification::Mmi(MmiEvent::Enquiry {
456 prompt: "PIN?".to_string(),
457 blind: true,
458 answer_len: 4,
459 })]
460 );
461 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
463 assert_eq!(
464 h.on_apdu(&close).notify,
465 vec![Notification::Mmi(MmiEvent::Close)]
466 );
467 }
468
469 #[test]
470 fn profile_change_re_enquires() {
471 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
472 let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
473 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
474 }
475
476 #[test]
477 fn application_information_surfaces_notification() {
478 use dvb_ci::objects::application_info::ApplicationType;
479 let mut h = ApplicationInformation;
480 assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
481 let ai = ser(&ApplicationInfo {
482 application_type: ApplicationType::ConditionalAccess,
483 application_manufacturer: 0x1234,
484 manufacturer_code: 0x5678,
485 menu_string: b"Acme CAM",
486 });
487 let out = h.on_apdu(&ai);
488 assert_eq!(
489 out.notify,
490 vec![Notification::ApplicationInfo {
491 application_type: 0x01,
492 manufacturer: 0x1234,
493 code: 0x5678,
494 menu: "Acme CAM".to_string(),
495 }]
496 );
497 }
498
499 #[test]
500 fn mjd_bcd_encoding_is_correct() {
501 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
503 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
505 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
506 }
507
508 #[test]
509 fn date_time_replies_to_enq_and_resends_on_interval() {
510 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
511 let mut h = DateTime::with_clock(fixed);
512 let enq = ser(&DateTimeEnq {
514 response_interval: 5,
515 });
516 let out = h.on_apdu(&enq);
517 assert_eq!(out.apdus.len(), 1);
518 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
519 assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
521 assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
523 }
524
525 #[test]
526 fn date_time_interval_zero_does_not_resend() {
527 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
528 h.on_apdu(&ser(&DateTimeEnq {
529 response_interval: 0,
530 }));
531 assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
532 }
533
534 #[test]
535 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
536 let mut h = ConditionalAccess;
537 assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
538 let ci = ser(&CaInfo {
540 ca_system_ids: vec![0x0B00, 0x1800],
541 });
542 assert_eq!(
543 h.on_apdu(&ci).notify,
544 vec![Notification::CaInfo {
545 ca_system_ids: vec![0x0B00, 0x1800],
546 }]
547 );
548 let reply = ser(&CaPmtReply {
550 program_number: 0x0042,
551 version_number: 0,
552 current_next_indicator: true,
553 ca_enable: Some(CaEnable::Possible),
554 streams: vec![],
555 });
556 assert_eq!(
557 h.on_apdu(&reply).notify,
558 vec![Notification::CaPmtReply {
559 program_number: 0x0042,
560 descrambling_ok: true,
561 }]
562 );
563 }
564}