Skip to main content

dvb_ci_runtime/
driver.rs

1//! The driver — the one place I/O happens. It pumps a [`CaDevice`] against the
2//! sans-IO [`CiStack`]: reads frames in, executes the stack's [`Action`]s
3//! (writes/ioctls) out, tracks the requested poll timer, and collects
4//! [`Notification`]s for the host application.
5
6use std::io;
7use std::time::Duration;
8
9use crate::device::CaDevice;
10use crate::event::{Action, Event, HostRequest, Notification};
11use crate::stack::CiStack;
12
13/// Drives a [`CaDevice`] with the [`CiStack`].
14pub struct Driver<D: CaDevice> {
15    device: D,
16    stack: CiStack,
17    notifications: Vec<Notification>,
18    /// Delay the stack last asked to be polled after (`None` = none pending).
19    next_timer: Option<Duration>,
20    /// Read buffer for one link-layer frame.
21    buf: Vec<u8>,
22}
23
24impl<D: CaDevice> Driver<D> {
25    /// New driver over `device`, single transport connection.
26    #[must_use]
27    pub fn new(device: D) -> Self {
28        Self {
29            device,
30            stack: CiStack::new(),
31            notifications: Vec::new(),
32            next_timer: None,
33            buf: vec![0u8; 4096],
34        }
35    }
36
37    /// Borrow the underlying device (e.g. to inspect a mock's recorded ops).
38    pub fn device(&self) -> &D {
39        &self.device
40    }
41
42    /// Mutably borrow the underlying device (e.g. to script a mock's inbound
43    /// frames between pumps).
44    pub fn device_mut(&mut self) -> &mut D {
45        &mut self.device
46    }
47
48    /// The poll delay the stack most recently requested, if any.
49    pub fn next_timer(&self) -> Option<Duration> {
50        self.next_timer
51    }
52
53    /// Drain the notifications collected so far.
54    pub fn take_notifications(&mut self) -> Vec<Notification> {
55        core::mem::take(&mut self.notifications)
56    }
57
58    /// Bring the interface up (reset + open the transport connection).
59    pub fn init(&mut self) -> io::Result<()> {
60        let actions = self.stack.handle(Event::Host(HostRequest::Init));
61        self.run(actions)
62    }
63
64    /// Request the module descramble the services in `ca_pmt` (a serialized
65    /// `ca_pmt` APDU body, e.g. from `dvb_ci::build_ca_pmt`).
66    pub fn send_ca_pmt(&mut self, ca_pmt: &[u8]) -> io::Result<()> {
67        let actions = self
68            .stack
69            .handle(Event::Host(HostRequest::SendCaPmt(ca_pmt)));
70        self.run(actions)
71    }
72
73    /// Descramble the services in a PMT section: the stack filters the PMT's
74    /// `CA_descriptor`s to the CAM's advertised CAIDs and sends a `ca_pmt`
75    /// (`list_management = only`, `cmd_id = ok_descrambling`). The outcome
76    /// surfaces as [`Notification::CaPmtReply`]. Call after the CAM is ready and
77    /// its `ca_info` has been received (otherwise no CAID filter is applied).
78    pub fn descramble(&mut self, pmt_section: &[u8]) -> io::Result<()> {
79        let actions = self
80            .stack
81            .handle(Event::Host(HostRequest::Descramble(pmt_section)));
82        self.run(actions)
83    }
84
85    /// Descramble a set of programmes in one CA-PMT list (`first`/`more`/`last`),
86    /// replacing any previously selected set. Each element is a raw PMT section.
87    pub fn descramble_programs(&mut self, pmt_sections: &[&[u8]]) -> io::Result<()> {
88        let actions = self
89            .stack
90            .handle(Event::Host(HostRequest::DescramblePrograms(pmt_sections)));
91        self.run(actions)
92    }
93
94    /// Add one programme to the descrambled set (`list_management = add`) without
95    /// re-listing the others — for a capacity manager adding a viewer's service.
96    pub fn add_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
97        let actions = self
98            .stack
99            .handle(Event::Host(HostRequest::AddProgram(pmt_section)));
100        self.run(actions)
101    }
102
103    /// Remove one programme from the descrambled set (`list_management = update`,
104    /// `cmd_id = not_selected`) — tells the CAM to stop descrambling it.
105    pub fn remove_program(&mut self, pmt_section: &[u8]) -> io::Result<()> {
106        let actions = self
107            .stack
108            .handle(Event::Host(HostRequest::RemoveProgram(pmt_section)));
109        self.run(actions)
110    }
111
112    /// Answer an MMI menu/list by 1-based `choice_ref` (0 = back/cancel).
113    pub fn mmi_menu_answer(&mut self, choice_ref: u8) -> io::Result<()> {
114        let actions = self
115            .stack
116            .handle(Event::Host(HostRequest::MmiMenuAnswer(choice_ref)));
117        self.run(actions)
118    }
119
120    /// Answer an MMI enquiry with the user's input (EN 300 468 Annex A bytes).
121    pub fn mmi_enquiry_answer(&mut self, text: &[u8]) -> io::Result<()> {
122        let actions = self
123            .stack
124            .handle(Event::Host(HostRequest::MmiEnquiryAnswer(text)));
125        self.run(actions)
126    }
127
128    /// Abort the current MMI dialogue (`answ` with `answ_id = cancel`).
129    pub fn mmi_cancel(&mut self) -> io::Result<()> {
130        let actions = self.stack.handle(Event::Host(HostRequest::MmiCancel));
131        self.run(actions)
132    }
133
134    /// Ask the module to open its MMI menu (`enter_menu`) — e.g. to read card /
135    /// entitlement info from the module's own menus.
136    pub fn enter_menu(&mut self) -> io::Result<()> {
137        let actions = self.stack.handle(Event::Host(HostRequest::EnterMenu));
138        self.run(actions)
139    }
140
141    /// One pump step: if the device is readable within `timeout`, read a frame
142    /// and feed it; otherwise advance the stack's timers by `timeout` (driving
143    /// the poll cadence). Returns whether a frame was processed.
144    pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
145        if self.device.poll(timeout)? {
146            let n = self.device.read(&mut self.buf)?;
147            if n > 0 {
148                let frame = self.buf[..n].to_vec();
149                let actions = self.stack.handle(Event::Readable(&frame));
150                self.run(actions)?;
151                return Ok(true);
152            }
153        }
154        let actions = self.stack.handle(Event::Tick { elapsed: timeout });
155        self.run(actions)?;
156        Ok(false)
157    }
158
159    /// Execute the stack's actions against the device.
160    fn run(&mut self, actions: Vec<Action>) -> io::Result<()> {
161        for action in actions {
162            match action {
163                Action::Write(bytes) => self.device.write(&bytes)?,
164                Action::Reset => self.device.reset()?,
165                Action::QuerySlot => {
166                    self.device.slot_info()?;
167                }
168                Action::SetTimer { after } => self.next_timer = Some(after),
169                Action::Notify(n) => self.notifications.push(n),
170            }
171        }
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::device::{DeviceOp, MockCaDevice};
180    use crate::event::{HostControlEvent, Notification};
181    use broadcast_common::Serialize;
182    use dvb_ci::tpdu::tags;
183
184    fn ser<S: Serialize>(s: &S) -> Vec<u8> {
185        let mut b = vec![0u8; s.serialized_len()];
186        match s.serialize_into(&mut b) {
187            Ok(n) => b.truncate(n),
188            Err(_) => b.clear(),
189        }
190        b
191    }
192
193    /// Wrap an SPDU as a module→host `T_Data_Last` R_TPDU (+ trailing T_SB,
194    /// data_available clear) on transport connection `tcid`.
195    fn r_data(tcid: u8, spdu: &[u8]) -> Vec<u8> {
196        use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
197        let mut v = vec![tpdu_tags::DATA_LAST, (1 + spdu.len()) as u8, tcid];
198        v.extend_from_slice(spdu);
199        v.extend_from_slice(&[tpdu_tags::SB, 0x02, tcid, SbValue::new(false).0]);
200        v
201    }
202
203    /// Wrap an APDU for delivery on `session_nb` (session_number prefix), then as
204    /// a module→host R_TPDU on tcid 1.
205    fn r_apdu(session_nb: u16, apdu: &[u8]) -> Vec<u8> {
206        use dvb_ci::spdu::SessionNumber;
207        let mut spdu = ser(&SessionNumber { session_nb });
208        spdu.extend_from_slice(apdu);
209        r_data(1, &spdu)
210    }
211
212    /// A standalone module→host `T_SB` (data_available clear) ack — flushes one
213    /// queued host write per turn (#337).
214    fn sb() -> Vec<u8> {
215        use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
216        vec![tpdu_tags::SB, 0x02, 0x01, SbValue::new(false).0]
217    }
218
219    /// Feed one scripted module frame into the mock and pump it, then pump a
220    /// handful of SB acks so any queued host writes flush.
221    fn feed(d: &mut Driver<MockCaDevice>, frame: Vec<u8>) {
222        d.device_mut().inbound.push_back(frame);
223        d.pump(Duration::from_millis(10)).unwrap();
224        for _ in 0..8 {
225            d.device_mut().inbound.push_back(sb());
226            d.pump(Duration::from_millis(10)).unwrap();
227        }
228    }
229
230    /// Drive the EN 50221 handshake through the `Driver` until host_control and
231    /// the other module-provided sessions are open (mirrors the stack-level
232    /// `stack_with_ca_session`, but exercises the real driver I/O path).
233    fn driver_with_sessions() -> Driver<MockCaDevice> {
234        use dvb_ci::objects::resource_manager::Profile;
235        use dvb_ci::resource::{
236            APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, HOST_CONTROL, MMI,
237            RESOURCE_MANAGER,
238        };
239        use dvb_ci::spdu::{CreateSessionResponse, OpenSessionRequest, SessionStatus};
240
241        let mut d = Driver::new(MockCaDevice::new([]));
242        d.init().unwrap();
243        // module accepts the transport connection
244        feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
245        // module opens the host's resource_manager → RM session 1
246        feed(
247            &mut d,
248            r_data(
249                1,
250                &ser(&OpenSessionRequest {
251                    resource: RESOURCE_MANAGER,
252                }),
253            ),
254        );
255        // module's profile → host: CamReady + profile_change + create_session for
256        // each module-provided resource.
257        feed(
258            &mut d,
259            r_apdu(
260                1,
261                &ser(&Profile {
262                    resources: vec![
263                        APPLICATION_INFORMATION,
264                        CONDITIONAL_ACCESS_SUPPORT,
265                        MMI,
266                        HOST_CONTROL,
267                    ],
268                }),
269            ),
270        );
271        // module accepts each create_session (session nbs 2..=5 in registration order)
272        for (nb, res) in [
273            (2u16, APPLICATION_INFORMATION),
274            (3, CONDITIONAL_ACCESS_SUPPORT),
275            (4, MMI),
276            (5, HOST_CONTROL),
277        ] {
278            feed(
279                &mut d,
280                r_data(
281                    1,
282                    &ser(&CreateSessionResponse {
283                        status: SessionStatus::Ok,
284                        resource: res,
285                        session_nb: nb,
286                    }),
287                ),
288            );
289        }
290        d
291    }
292
293    // Session numbers the module allocates in `driver_with_sessions`, in
294    // registration order: RM=1, app_info=2, conditional_access=3, mmi=4,
295    // host_control=5. (Asserted by `handshake_opens_expected_sessions`.)
296    const RM_SESSION: u16 = 1;
297    const MMI_SESSION: u16 = 4;
298    const HOST_CONTROL_SESSION: u16 = 5;
299
300    #[test]
301    fn host_control_tune_apdu_surfaces_notification_via_driver() {
302        use dvb_ci::objects::host_control::Tune;
303
304        let mut d = driver_with_sessions();
305        let hc_nb = HOST_CONTROL_SESSION;
306        d.take_notifications(); // drop handshake notifications
307
308        // Module (CAM) sends a Tune request on its host_control session.
309        let tune = Tune {
310            network_id: 0x1122,
311            original_network_id: 0x3344,
312            transport_stream_id: 0x5566,
313            service_id: 0x7788,
314        };
315        feed(&mut d, r_apdu(hc_nb, &ser(&tune)));
316
317        // The runtime surfaces the decoded HostControl(Tune) notification.
318        let notes = d.take_notifications();
319        assert!(
320            notes.contains(&Notification::HostControl(HostControlEvent::Tune {
321                network_id: 0x1122,
322                original_network_id: 0x3344,
323                transport_stream_id: 0x5566,
324                service_id: 0x7788,
325            })),
326            "expected HostControl(Tune) notification, got {notes:?}"
327        );
328    }
329
330    #[test]
331    fn profile_reply_advertises_host_control() {
332        use broadcast_common::Parse;
333        use dvb_ci::objects::resource_manager::{Profile, ProfileEnq};
334        use dvb_ci::resource::{HOST_CONTROL, RESOURCE_MANAGER};
335
336        let mut d = Driver::new(MockCaDevice::new([]));
337        d.init().unwrap();
338        feed(&mut d, vec![tags::C_T_C_REPLY, 0x01, 0x01]);
339        // Open RM, then the module enquires the host profile.
340        feed(
341            &mut d,
342            r_data(
343                1,
344                &ser(&dvb_ci::spdu::OpenSessionRequest {
345                    resource: RESOURCE_MANAGER,
346                }),
347            ),
348        );
349        // Module → profile_enq on the RM session → host replies with its profile.
350        feed(&mut d, r_apdu(RM_SESSION, &ser(&ProfileEnq)));
351
352        // Find the host's `profile` reply (tag 9F 80 11) in the written frames and
353        // confirm it lists HOST_CONTROL.
354        let want = dvb_ci::tag::PROFILE.to_bytes();
355        let found = d.device().ops.iter().any(|op| {
356            if let DeviceOp::Write(w) = op {
357                if let Some(pos) = w.windows(3).position(|x| x == want) {
358                    if let Ok(p) = Profile::parse(&w[pos..]) {
359                        return p.resources.contains(&HOST_CONTROL);
360                    }
361                }
362            }
363            false
364        });
365        assert!(found, "profile reply must advertise HOST_CONTROL");
366    }
367
368    #[test]
369    fn mmi_menu_answ_and_answ_are_byte_exact_on_the_mmi_session() {
370        use dvb_ci::objects::mmi_high::{Answ, AnswId, MenuAnsw};
371
372        let mut d = driver_with_sessions();
373        let mmi_nb = MMI_SESSION;
374
375        // menu_answ(choice_ref = 2): the driver method must put the exact dvb-ci
376        // MenuAnsw serialization on the wire, on the MMI session.
377        d.mmi_menu_answer(2).unwrap();
378        d.device_mut().inbound.push_back(sb());
379        d.pump(Duration::from_millis(10)).unwrap();
380        assert_apdu_on_session(&d, mmi_nb, &ser(&MenuAnsw { choice_ref: 2 }));
381
382        // answ(answer, "1234"): byte-exact Answ serialization on the MMI session.
383        d.mmi_enquiry_answer(b"1234").unwrap();
384        d.device_mut().inbound.push_back(sb());
385        d.pump(Duration::from_millis(10)).unwrap();
386        assert_apdu_on_session(
387            &d,
388            mmi_nb,
389            &ser(&Answ {
390                answ_id: AnswId::Answer,
391                text_chars: b"1234",
392            }),
393        );
394    }
395
396    /// Assert some host write carries `session_number(session_nb)` immediately
397    /// followed by the exact `apdu` bytes (byte-exact APDU on the right session).
398    fn assert_apdu_on_session(d: &Driver<MockCaDevice>, session_nb: u16, apdu: &[u8]) {
399        use dvb_ci::spdu::SessionNumber;
400        let mut want = ser(&SessionNumber { session_nb });
401        want.extend_from_slice(apdu);
402        let hit = d.device().ops.iter().any(|op| match op {
403            DeviceOp::Write(w) => w.windows(want.len()).any(|x| x == want.as_slice()),
404            _ => false,
405        });
406        assert!(
407            hit,
408            "expected APDU {apdu:02X?} on session {session_nb} (session-prefixed {want:02X?}) in writes"
409        );
410    }
411
412    #[test]
413    fn init_drives_reset_slotinfo_and_create_tc_to_device() {
414        let mut d = Driver::new(MockCaDevice::new([]));
415        d.init().unwrap();
416        let ops = &d.device().ops;
417        assert_eq!(ops[0], DeviceOp::Reset);
418        assert_eq!(ops[1], DeviceOp::SlotInfo);
419        assert!(matches!(&ops[2], DeviceOp::Write(w) if w[0] == tags::CREATE_T_C));
420    }
421
422    #[test]
423    fn reads_reply_then_polls_on_pump() {
424        // Script the module accepting the connection.
425        let dev = MockCaDevice::new([vec![tags::C_T_C_REPLY, 0x01, 0x01]]);
426        let mut d = Driver::new(dev);
427        d.init().unwrap();
428        // first pump reads the C_T_C_Reply (activates the connection)
429        assert!(d.pump(Duration::from_millis(100)).unwrap());
430        // next pump has nothing to read → ticks → emits a poll write
431        assert!(!d.pump(Duration::from_millis(100)).unwrap());
432        let last = d.device().ops.last().unwrap();
433        assert!(matches!(last, DeviceOp::Write(w) if w.first() == Some(&tags::DATA_LAST)));
434    }
435}