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}