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