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