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 ///
32 /// An implementation must never leave a packet half written. When the
33 /// device cannot take all of `ts` (its buffer is full) it returns an
34 /// [`io::ErrorKind::WouldBlock`] error carrying a [`PartialWrite`] — read
35 /// it with [`partial_write_progress`] — whose `written` is the number of
36 /// whole-packet bytes the device did accept, so the caller can resume
37 /// after them (see [`CaDescrambler::feed_ts`](crate::descrambler::CaDescrambler::feed_ts),
38 /// which drains the device and retries the unsent tail).
39 fn write(&mut self, ts: &[u8]) -> io::Result<()>;
40
41 /// Read descrambled TS into `buf` (sized to a multiple of [`TS_PACKET_LEN`]);
42 /// returns the byte count. `Ok(0)` means none available (non-blocking).
43 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
44
45 /// Wait up to `timeout` for descrambled TS to become readable; `Ok(true)` if
46 /// readable.
47 fn poll(&mut self, timeout: Duration) -> io::Result<bool>;
48}
49
50/// Payload of the [`io::ErrorKind::WouldBlock`] error a [`CiDataDevice::write`]
51/// returns when the device buffer filled before all of the TS was accepted.
52/// Read it back with [`partial_write_progress`].
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct PartialWrite {
55 /// Bytes the device accepted — always a whole number of
56 /// [`TS_PACKET_LEN`]-byte packets (never a torn packet).
57 pub written: usize,
58 /// Bytes the caller asked to write.
59 pub requested: usize,
60 /// The device, for the error message.
61 pub device: String,
62}
63
64impl std::fmt::Display for PartialWrite {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(
67 f,
68 "{}: device buffer full, {} of {} bytes ({} packets) written",
69 self.device,
70 self.written,
71 self.requested,
72 self.written / TS_PACKET_LEN
73 )
74 }
75}
76
77impl std::error::Error for PartialWrite {}
78
79/// The whole-packet byte count a [`CiDataDevice::write`] error accepted before
80/// the device filled, or `None` when `err` is not a [`PartialWrite`].
81#[must_use]
82pub fn partial_write_progress(err: &io::Error) -> Option<usize> {
83 err.get_ref()
84 .and_then(|inner| inner.downcast_ref::<PartialWrite>())
85 .map(|p| p.written)
86}
87
88/// Reject a buffer that is not a whole number of TS packets.
89fn check_aligned(len: usize, what: &'static str) -> io::Result<()> {
90 if len.is_multiple_of(TS_PACKET_LEN) {
91 Ok(())
92 } else {
93 Err(io::Error::new(io::ErrorKind::InvalidInput, what))
94 }
95}
96
97/// In-memory [`CiDataDevice`] for tests and the differential harness.
98///
99/// - `descrambled` is a scripted queue of TS the "module" returns; each
100/// [`read`](CiDataDevice::read) pops one entry (the "scripted descramble").
101/// - every byte the host writes is recorded in `written`, so a test (or a
102/// byte-exact comparison against an external reference) can assert the exact
103/// scrambled TS that was pushed in.
104#[derive(Debug, Default)]
105pub struct MockCiDataDevice {
106 /// Scripted descrambled-TS the module returns to the host (FIFO).
107 pub descrambled: std::collections::VecDeque<Vec<u8>>,
108 /// Scrambled TS the host wrote, in order.
109 pub written: Vec<Vec<u8>>,
110}
111
112impl MockCiDataDevice {
113 /// New mock returning the given descrambled-TS script.
114 #[must_use]
115 pub fn new(descrambled: impl IntoIterator<Item = Vec<u8>>) -> Self {
116 Self {
117 descrambled: descrambled.into_iter().collect(),
118 written: Vec::new(),
119 }
120 }
121
122 /// All scrambled TS the host wrote, concatenated.
123 #[must_use]
124 pub fn written_ts(&self) -> Vec<u8> {
125 self.written.iter().flatten().copied().collect()
126 }
127}
128
129impl CiDataDevice for MockCiDataDevice {
130 fn write(&mut self, ts: &[u8]) -> io::Result<()> {
131 check_aligned(ts.len(), "write not a multiple of 188 bytes")?;
132 self.written.push(ts.to_vec());
133 Ok(())
134 }
135
136 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
137 check_aligned(buf.len(), "read buffer not a multiple of 188 bytes")?;
138 match self.descrambled.pop_front() {
139 Some(ts) => {
140 let n = ts.len().min(buf.len());
141 buf[..n].copy_from_slice(&ts[..n]);
142 Ok(n)
143 }
144 None => Ok(0),
145 }
146 }
147
148 fn poll(&mut self, _timeout: Duration) -> io::Result<bool> {
149 Ok(!self.descrambled.is_empty())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn packet(fill: u8) -> Vec<u8> {
158 let mut p = vec![fill; TS_PACKET_LEN];
159 p[0] = 0x47; // sync byte
160 p
161 }
162
163 #[test]
164 fn mock_records_writes_and_replays_descrambled() {
165 let mut dev = MockCiDataDevice::new([packet(0xAA), packet(0xBB)]);
166 dev.write(&packet(0x11)).unwrap();
167 dev.write(&packet(0x22)).unwrap();
168
169 let mut buf = [0u8; TS_PACKET_LEN];
170 assert!(dev.poll(Duration::ZERO).unwrap());
171 assert_eq!(dev.read(&mut buf).unwrap(), TS_PACKET_LEN);
172 assert_eq!(buf[1], 0xAA);
173 assert_eq!(dev.read(&mut buf).unwrap(), TS_PACKET_LEN);
174 assert_eq!(buf[1], 0xBB);
175 assert_eq!(dev.read(&mut buf).unwrap(), 0); // drained
176 assert!(!dev.poll(Duration::ZERO).unwrap());
177
178 assert_eq!(dev.written_ts().len(), 2 * TS_PACKET_LEN);
179 }
180
181 #[test]
182 fn rejects_unaligned_io() {
183 let mut dev = MockCiDataDevice::new([]);
184 assert_eq!(
185 dev.write(&[0x47; 100]).unwrap_err().kind(),
186 io::ErrorKind::InvalidInput
187 );
188 let mut buf = [0u8; 100];
189 assert_eq!(
190 dev.read(&mut buf).unwrap_err().kind(),
191 io::ErrorKind::InvalidInput
192 );
193 }
194}