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 for r in [APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, MMI] {
156 if self.module_resources.contains(&r) {
157 out.open.push(r);
158 }
159 }
160 }
161 out
162 }
163}
164
165#[derive(Debug, Default)]
168pub struct ApplicationInformation;
169
170impl Resource for ApplicationInformation {
171 fn id(&self) -> ResourceId {
172 APPLICATION_INFORMATION
173 }
174
175 fn on_open(&mut self) -> ResourceOut {
176 ResourceOut {
177 apdus: vec![ser(&ApplicationInfoEnq)],
178 ..ResourceOut::default()
179 }
180 }
181
182 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
183 let mut out = ResourceOut::default();
184 if peek_tag(apdu) == Some(tag::APPLICATION_INFO) {
185 if let Ok(ai) = ApplicationInfo::parse(apdu) {
186 out.notify.push(Notification::ApplicationInfo {
187 application_type: ai.application_type.to_u8(),
188 manufacturer: ai.application_manufacturer,
189 code: ai.manufacturer_code,
190 menu: String::from_utf8_lossy(ai.menu_string).into_owned(),
191 });
192 }
193 }
194 out
195 }
196}
197
198#[derive(Debug, Default)]
203pub struct ConditionalAccess;
204
205impl Resource for ConditionalAccess {
206 fn id(&self) -> ResourceId {
207 CONDITIONAL_ACCESS_SUPPORT
208 }
209
210 fn on_open(&mut self) -> ResourceOut {
211 ResourceOut {
212 apdus: vec![ser(&CaInfoEnq)],
213 ..ResourceOut::default()
214 }
215 }
216
217 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
218 let mut out = ResourceOut::default();
219 match peek_tag(apdu) {
220 Some(t) if t == tag::CA_INFO => {
221 if let Ok(ci) = CaInfo::parse(apdu) {
222 out.notify.push(Notification::CaInfo {
223 ca_system_ids: ci.ca_system_ids,
224 });
225 }
226 }
227 Some(t) if t == tag::CA_PMT_REPLY => {
228 if let Ok(r) = CaPmtReply::parse(apdu) {
229 let descrambling_ok = r.ca_enable.is_some_and(|e| {
230 matches!(
231 e,
232 CaEnable::Possible
233 | CaEnable::PossiblePurchaseDialogue
234 | CaEnable::PossibleTechnicalDialogue
235 )
236 });
237 out.notify.push(Notification::CaPmtReply {
238 program_number: r.program_number,
239 descrambling_ok,
240 });
241 }
242 }
243 _ => {}
244 }
245 out
246 }
247}
248
249const SECS_PER_DAY: u64 = 86_400;
250const MJD_UNIX_EPOCH: u64 = 40_587;
252
253fn bcd(v: u64) -> u8 {
254 (((v / 10) << 4) | (v % 10)) as u8
255}
256
257fn unix_to_mjd_bcd(unix_secs: u64) -> [u8; UTC_TIME_LEN] {
260 let mjd = (MJD_UNIX_EPOCH + unix_secs / SECS_PER_DAY) as u16;
261 let sod = unix_secs % SECS_PER_DAY;
262 [
263 (mjd >> 8) as u8,
264 mjd as u8,
265 bcd(sod / 3600),
266 bcd((sod % 3600) / 60),
267 bcd(sod % 60),
268 ]
269}
270
271fn system_utc() -> [u8; UTC_TIME_LEN] {
272 let secs = std::time::SystemTime::now()
273 .duration_since(std::time::UNIX_EPOCH)
274 .map(|d| d.as_secs())
275 .unwrap_or(0);
276 unix_to_mjd_bcd(secs)
277}
278
279pub struct DateTime {
283 clock: fn() -> [u8; UTC_TIME_LEN],
284 interval: u8,
285 since: Duration,
286}
287
288impl Default for DateTime {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294impl DateTime {
295 #[must_use]
297 pub fn new() -> Self {
298 Self {
299 clock: system_utc,
300 interval: 0,
301 since: Duration::ZERO,
302 }
303 }
304
305 #[must_use]
307 pub fn with_clock(clock: fn() -> [u8; UTC_TIME_LEN]) -> Self {
308 Self {
309 clock,
310 interval: 0,
311 since: Duration::ZERO,
312 }
313 }
314
315 fn reply(&self) -> Vec<u8> {
316 ser(&CiDateTime {
317 utc_time: (self.clock)(),
318 local_offset: None,
319 })
320 }
321}
322
323impl Resource for DateTime {
324 fn id(&self) -> ResourceId {
325 DATE_TIME
326 }
327
328 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
329 let mut out = ResourceOut::default();
330 if peek_tag(apdu) == Some(tag::DATE_TIME_ENQ) {
331 if let Ok(enq) = DateTimeEnq::parse(apdu) {
332 self.interval = enq.response_interval;
333 self.since = Duration::ZERO;
334 out.apdus.push(self.reply());
335 }
336 }
337 out
338 }
339
340 fn tick(&mut self, elapsed: Duration) -> ResourceOut {
341 let mut out = ResourceOut::default();
342 if self.interval > 0 {
343 self.since += elapsed;
344 if self.since >= Duration::from_secs(u64::from(self.interval)) {
345 self.since = Duration::ZERO;
346 out.apdus.push(self.reply());
347 }
348 }
349 out
350 }
351}
352
353#[derive(Debug, Default)]
358pub struct Mmi;
359
360impl Resource for Mmi {
361 fn id(&self) -> ResourceId {
362 MMI
363 }
364
365 fn on_apdu(&mut self, apdu: &[u8]) -> ResourceOut {
366 let mut out = ResourceOut::default();
367 match peek_tag(apdu) {
368 Some(t) if t == tag::ENQ => {
369 if let Ok(e) = Enq::parse(apdu) {
370 out.notify.push(Notification::Mmi(MmiEvent::Enquiry {
371 prompt: text(e.text_chars),
372 blind: e.blind_answer,
373 answer_len: e.answer_text_length,
374 }));
375 }
376 }
377 Some(t) if t == tag::MENU_LAST => {
378 if let Ok(m) = Menu::parse(apdu) {
379 out.notify.push(Notification::Mmi(MmiEvent::Menu {
380 title: text(m.title.text_chars),
381 items: m.choices.iter().map(|c| text(c.text_chars)).collect(),
382 }));
383 }
384 }
385 Some(t) if t == tag::CLOSE_MMI => {
386 out.notify.push(Notification::Mmi(MmiEvent::Close));
387 }
388 _ => {}
389 }
390 out
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use dvb_ci::objects::resource_manager::Profile;
398
399 #[test]
400 fn on_open_sends_profile_enq() {
401 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
402 let out = rm.on_open();
403 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
404 }
405
406 #[test]
407 fn module_profile_triggers_profile_change_camready_and_opens() {
408 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
413 rm.on_open();
414 let module_profile = ser(&Profile {
415 resources: vec![APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT],
416 });
417 let o = rm.on_apdu(&module_profile);
418 assert!(o.notify.contains(&Notification::CamReady));
419 assert_eq!(o.apdus.len(), 1, "host sends profile_change");
420 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE_CHANGE));
421 assert!(o.open.contains(&APPLICATION_INFORMATION));
422 assert!(o.open.contains(&CONDITIONAL_ACCESS_SUPPORT));
423 assert!(!o.open.contains(&MMI), "module didn't advertise MMI");
424 }
425
426 #[test]
427 fn answers_a_module_profile_enquiry_without_re_readying() {
428 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
429 rm.on_open();
430 rm.on_apdu(&ser(&Profile {
431 resources: vec![APPLICATION_INFORMATION],
432 }));
433 let o = rm.on_apdu(&ser(&ProfileEnq));
435 assert_eq!(o.apdus.len(), 1);
436 assert_eq!(peek_tag(&o.apdus[0]), Some(tag::PROFILE));
437 assert!(!o.notify.contains(&Notification::CamReady));
438 }
439
440 #[test]
441 fn mmi_surfaces_enquiry_and_close() {
442 let mut h = Mmi;
443 let enq = ser(&Enq {
445 blind_answer: true,
446 answer_text_length: 4,
447 text_chars: b"PIN?",
448 });
449 assert_eq!(
450 h.on_apdu(&enq).notify,
451 vec![Notification::Mmi(MmiEvent::Enquiry {
452 prompt: "PIN?".to_string(),
453 blind: true,
454 answer_len: 4,
455 })]
456 );
457 let close = [0x9F, 0x88, 0x00, 0x01, 0x00];
459 assert_eq!(
460 h.on_apdu(&close).notify,
461 vec![Notification::Mmi(MmiEvent::Close)]
462 );
463 }
464
465 #[test]
466 fn profile_change_re_enquires() {
467 let mut rm = ResourceManager::new(vec![RESOURCE_MANAGER]);
468 let out = rm.on_apdu(&ser(&dvb_ci::objects::resource_manager::ProfileChange));
469 assert_eq!(out.apdus, vec![ser(&ProfileEnq)]);
470 }
471
472 #[test]
473 fn application_information_surfaces_notification() {
474 use dvb_ci::objects::application_info::ApplicationType;
475 let mut h = ApplicationInformation;
476 assert_eq!(h.on_open().apdus, vec![ser(&ApplicationInfoEnq)]);
477 let ai = ser(&ApplicationInfo {
478 application_type: ApplicationType::ConditionalAccess,
479 application_manufacturer: 0x1234,
480 manufacturer_code: 0x5678,
481 menu_string: b"Acme CAM",
482 });
483 let out = h.on_apdu(&ai);
484 assert_eq!(
485 out.notify,
486 vec![Notification::ApplicationInfo {
487 application_type: 0x01,
488 manufacturer: 0x1234,
489 code: 0x5678,
490 menu: "Acme CAM".to_string(),
491 }]
492 );
493 }
494
495 #[test]
496 fn mjd_bcd_encoding_is_correct() {
497 assert_eq!(unix_to_mjd_bcd(0), [0x9E, 0x8B, 0x00, 0x00, 0x00]);
499 let secs = SECS_PER_DAY + 13 * 3600 + 45 * 60 + 9;
501 assert_eq!(unix_to_mjd_bcd(secs), [0x9E, 0x8C, 0x13, 0x45, 0x09]);
502 }
503
504 #[test]
505 fn date_time_replies_to_enq_and_resends_on_interval() {
506 let fixed = || [0x9E, 0x7B, 0x00, 0x00, 0x00];
507 let mut h = DateTime::with_clock(fixed);
508 let enq = ser(&DateTimeEnq {
510 response_interval: 5,
511 });
512 let out = h.on_apdu(&enq);
513 assert_eq!(out.apdus.len(), 1);
514 assert_eq!(peek_tag(&out.apdus[0]), Some(tag::DATE_TIME));
515 assert!(h.tick(Duration::from_secs(3)).apdus.is_empty());
517 assert_eq!(h.tick(Duration::from_secs(3)).apdus.len(), 1);
519 }
520
521 #[test]
522 fn date_time_interval_zero_does_not_resend() {
523 let mut h = DateTime::with_clock(|| [0u8; UTC_TIME_LEN]);
524 h.on_apdu(&ser(&DateTimeEnq {
525 response_interval: 0,
526 }));
527 assert!(h.tick(Duration::from_secs(60)).apdus.is_empty());
528 }
529
530 #[test]
531 fn conditional_access_surfaces_ca_info_and_pmt_reply() {
532 let mut h = ConditionalAccess;
533 assert_eq!(h.on_open().apdus, vec![ser(&CaInfoEnq)]);
534 let ci = ser(&CaInfo {
536 ca_system_ids: vec![0x0B00, 0x1800],
537 });
538 assert_eq!(
539 h.on_apdu(&ci).notify,
540 vec![Notification::CaInfo {
541 ca_system_ids: vec![0x0B00, 0x1800],
542 }]
543 );
544 let reply = ser(&CaPmtReply {
546 program_number: 0x0042,
547 version_number: 0,
548 current_next_indicator: true,
549 ca_enable: Some(CaEnable::Possible),
550 streams: vec![],
551 });
552 assert_eq!(
553 h.on_apdu(&reply).notify,
554 vec![Notification::CaPmtReply {
555 program_number: 0x0042,
556 descrambling_ok: true,
557 }]
558 );
559 }
560}