Skip to main content

dvb_ci_runtime/
dataplane.rs

1//! The CI **TS data-plane** device: [`CiDataDevice`].
2//!
3//! Separate-CI hardware (CXD2099-class CI bridges — a PCIe/USB CI card with no
4//! integrated demod) exposes two devices. The **control plane** is the CA device
5//! ([`CaDevice`](crate::device::CaDevice), `caM`): the EN 50221 APDU exchange
6//! (resource manager, sessions, `ca_pmt`). The **data plane** is `ciM`: the host
7//! **writes** the scrambled Transport Stream into it and **reads** the
8//! descrambled TS back out. This module abstracts the data plane the same way
9//! [`CaDevice`](crate::device::CaDevice) abstracts the control plane, so the
10//! host-fed descramble path can be driven by a real device (`linux` feature) or
11//! an in-memory mock.
12//!
13//! All TS I/O is in whole 188-byte packets ([`TS_PACKET_LEN`]); a non-aligned
14//! buffer is rejected with [`io::ErrorKind::InvalidInput`].
15
16use std::io;
17use std::time::Duration;
18
19/// MPEG-2 TS packet length (ISO/IEC 13818-1): 188 bytes.
20pub const TS_PACKET_LEN: usize = 188;
21
22/// The TS data-plane device of a separate-CI module (`/dev/dvb/adapterN/ciM`).
23///
24/// The host pushes scrambled TS in with [`write`](Self::write) and pulls the
25/// descrambled TS out with [`read`](Self::read). Implementations:
26/// [`MockCiDataDevice`] (in-memory, for tests + differential harness) and, with
27/// the `linux` feature, a device over `/dev/dvb/.../ci`.
28pub trait CiDataDevice {
29    /// Write scrambled TS to the module. `ts` must be a whole number of
30    /// [`TS_PACKET_LEN`]-byte packets.
31    fn write(&mut self, ts: &[u8]) -> io::Result<()>;
32
33    /// Read descrambled TS into `buf` (sized to a multiple of [`TS_PACKET_LEN`]);
34    /// returns the byte count. `Ok(0)` means none available (non-blocking).
35    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
36
37    /// Wait up to `timeout` for descrambled TS to become readable; `Ok(true)` if
38    /// readable.
39    fn poll(&mut self, timeout: Duration) -> io::Result<bool>;
40}
41
42/// Reject a buffer that is not a whole number of TS packets.
43fn check_aligned(len: usize, what: &'static str) -> io::Result<()> {
44    if len % TS_PACKET_LEN == 0 {
45        Ok(())
46    } else {
47        Err(io::Error::new(io::ErrorKind::InvalidInput, what))
48    }
49}
50
51/// In-memory [`CiDataDevice`] for tests and the differential harness.
52///
53/// - `descrambled` is a scripted queue of TS the "module" returns; each
54///   [`read`](CiDataDevice::read) pops one entry (the "scripted descramble").
55/// - every byte the host writes is recorded in `written`, so a test (or a
56///   byte-exact comparison against an external reference) can assert the exact
57///   scrambled TS that was pushed in.
58#[derive(Debug, Default)]
59pub struct MockCiDataDevice {
60    /// Scripted descrambled-TS the module returns to the host (FIFO).
61    pub descrambled: std::collections::VecDeque<Vec<u8>>,
62    /// Scrambled TS the host wrote, in order.
63    pub written: Vec<Vec<u8>>,
64}
65
66impl MockCiDataDevice {
67    /// New mock returning the given descrambled-TS script.
68    #[must_use]
69    pub fn new(descrambled: impl IntoIterator<Item = Vec<u8>>) -> Self {
70        Self {
71            descrambled: descrambled.into_iter().collect(),
72            written: Vec::new(),
73        }
74    }
75
76    /// All scrambled TS the host wrote, concatenated.
77    #[must_use]
78    pub fn written_ts(&self) -> Vec<u8> {
79        self.written.iter().flatten().copied().collect()
80    }
81}
82
83impl CiDataDevice for MockCiDataDevice {
84    fn write(&mut self, ts: &[u8]) -> io::Result<()> {
85        check_aligned(ts.len(), "write not a multiple of 188 bytes")?;
86        self.written.push(ts.to_vec());
87        Ok(())
88    }
89
90    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
91        check_aligned(buf.len(), "read buffer not a multiple of 188 bytes")?;
92        match self.descrambled.pop_front() {
93            Some(ts) => {
94                let n = ts.len().min(buf.len());
95                buf[..n].copy_from_slice(&ts[..n]);
96                Ok(n)
97            }
98            None => Ok(0),
99        }
100    }
101
102    fn poll(&mut self, _timeout: Duration) -> io::Result<bool> {
103        Ok(!self.descrambled.is_empty())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn packet(fill: u8) -> Vec<u8> {
112        let mut p = vec![fill; TS_PACKET_LEN];
113        p[0] = 0x47; // sync byte
114        p
115    }
116
117    #[test]
118    fn mock_records_writes_and_replays_descrambled() {
119        let mut dev = MockCiDataDevice::new([packet(0xAA), packet(0xBB)]);
120        dev.write(&packet(0x11)).unwrap();
121        dev.write(&packet(0x22)).unwrap();
122
123        let mut buf = [0u8; TS_PACKET_LEN];
124        assert!(dev.poll(Duration::ZERO).unwrap());
125        assert_eq!(dev.read(&mut buf).unwrap(), TS_PACKET_LEN);
126        assert_eq!(buf[1], 0xAA);
127        assert_eq!(dev.read(&mut buf).unwrap(), TS_PACKET_LEN);
128        assert_eq!(buf[1], 0xBB);
129        assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
130        assert!(!dev.poll(Duration::ZERO).unwrap());
131
132        assert_eq!(dev.written_ts().len(), 2 * TS_PACKET_LEN);
133    }
134
135    #[test]
136    fn rejects_unaligned_io() {
137        let mut dev = MockCiDataDevice::new([]);
138        assert_eq!(
139            dev.write(&[0x47; 100]).unwrap_err().kind(),
140            io::ErrorKind::InvalidInput
141        );
142        let mut buf = [0u8; 100];
143        assert_eq!(
144            dev.read(&mut buf).unwrap_err().kind(),
145            io::ErrorKind::InvalidInput
146        );
147    }
148}