Skip to main content

dvb_ci_runtime/
linux.rs

1//! Linux `/dev/dvb/adapterN/caM` [`CaDevice`] implementation (the `linux`
2//! feature).
3//!
4//! This is the one place the crate uses `unsafe` — the DVB CA ioctls
5//! (`CA_RESET`, `CA_GET_SLOT_INFO`) via `libc`. The ioctl request numbers are
6//! computed from the standard Linux `_IOC` encoding (Documentation/userspace-api
7//! + `include/uapi/linux/dvb/ca.h`), not hard-coded magic.
8//!
9//! Runtime behaviour requires a real DVB card with a CI slot; it is
10//! compile-checked in CI but exercised only on hardware.
11#![allow(unsafe_code)]
12
13use std::fs::{File, OpenOptions};
14use std::io::{self, Read, Write};
15use std::os::unix::io::AsRawFd;
16use std::time::Duration;
17
18use crate::dataplane::{CiDataDevice, TS_PACKET_LEN};
19use crate::device::{CaDevice, SlotInfo};
20
21/// Poll a file descriptor for readability up to `timeout`.
22fn poll_readable(fd: libc::c_int, timeout: Duration) -> io::Result<bool> {
23    let mut pfd = libc::pollfd {
24        fd,
25        events: libc::POLLIN,
26        revents: 0,
27    };
28    let ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
29    // SAFETY: `pfd` points at one valid pollfd for the duration of the call.
30    let r = unsafe { libc::poll(&mut pfd as *mut libc::pollfd, 1, ms) };
31    if r < 0 {
32        Err(io::Error::last_os_error())
33    } else {
34        Ok(pfd.revents & libc::POLLIN != 0)
35    }
36}
37
38// --- Linux _IOC ioctl encoding (uapi/asm-generic/ioctl.h) ------------------
39const IOC_NRBITS: u32 = 8;
40const IOC_TYPEBITS: u32 = 8;
41const IOC_SIZEBITS: u32 = 14;
42const IOC_NRSHIFT: u32 = 0;
43const IOC_TYPESHIFT: u32 = IOC_NRSHIFT + IOC_NRBITS;
44const IOC_SIZESHIFT: u32 = IOC_TYPESHIFT + IOC_TYPEBITS;
45const IOC_DIRSHIFT: u32 = IOC_SIZESHIFT + IOC_SIZEBITS;
46const IOC_NONE: u32 = 0;
47const IOC_READ: u32 = 2;
48
49const fn ioc(dir: u32, typ: u32, nr: u32, size: u32) -> u64 {
50    ((dir << IOC_DIRSHIFT) | (typ << IOC_TYPESHIFT) | (nr << IOC_NRSHIFT) | (size << IOC_SIZESHIFT))
51        as u64
52}
53
54// DVB CA device (uapi/linux/dvb/ca.h): magic 'o', ca_slot_info, flags bit.
55const DVB_CA_MAGIC: u32 = b'o' as u32;
56const CA_RESET: u64 = ioc(IOC_NONE, DVB_CA_MAGIC, 128, 0);
57const CA_GET_SLOT_INFO: u64 = ioc(
58    IOC_READ,
59    DVB_CA_MAGIC,
60    130,
61    core::mem::size_of::<CaSlotInfo>() as u32,
62);
63/// `CA_CI_MODULE_PRESENT` — a module (or card) is physically inserted in the
64/// slot (uapi `linux/dvb/ca.h` `ca_slot_info.flags`, bit 0).
65const CA_CI_MODULE_PRESENT: u32 = 1;
66/// `CA_CI_MODULE_READY` — the inserted module has completed its own init and
67/// is usable (uapi `linux/dvb/ca.h` `ca_slot_info.flags`, bit 1). Distinct
68/// from `CA_CI_MODULE_PRESENT`: a module can be present but not yet ready
69/// briefly after insertion.
70const CA_CI_MODULE_READY: u32 = 2;
71
72#[repr(C)]
73struct CaSlotInfo {
74    num: i32,
75    typ: i32,
76    flags: u32,
77}
78
79/// Settle time after `CA_RESET` before the module is usable. The DD/cxd2099
80/// (and others) only (re)initialise the slot a couple of seconds after reset;
81/// `Create_T_C` sent too early is ignored. 3s is the value validated live
82/// against a DD Octopus cxd2099 + AlphaCrypt module (2s intermittently raced the
83/// module's resource-manager open).
84const RESET_SETTLE: Duration = Duration::from_millis(3000);
85
86/// A [`CaDevice`] backed by a Linux DVB CA character device.
87///
88/// The kernel `dvb_ca_en50221` character device carries a 2-byte link header on
89/// every read/write — `[slot_id, connection_id, <TPDU>]`. This type adds/strips
90/// that header, so the sans-IO transport deals in bare TPDUs. (Writing a raw
91/// TPDU without the header is rejected `EINVAL` by the driver.)
92#[derive(Debug)]
93pub struct LinuxCaDevice {
94    file: File,
95    slot: u8,
96}
97
98impl LinuxCaDevice {
99    /// Open `/dev/dvb/adapter{adapter}/ca{ca}` (slot 0).
100    pub fn open(adapter: u32, ca: u32) -> io::Result<Self> {
101        let path = format!("/dev/dvb/adapter{adapter}/ca{ca}");
102        let file = OpenOptions::new().read(true).write(true).open(path)?;
103        Ok(Self { file, slot: 0 })
104    }
105
106    /// Wrap an already-open CA device file for `slot`.
107    #[must_use]
108    pub fn from_file(file: File, slot: u8) -> Self {
109        Self { file, slot }
110    }
111
112    /// The `connection_id` for a TPDU = its `t_c_id`, which follows the tag +
113    /// `length_field`. Falls back to 1 (the single connection) if unparseable.
114    fn connection_id(tpdu: &[u8]) -> u8 {
115        dvb_ci::length::decode(tpdu.get(1..).unwrap_or(&[]))
116            .ok()
117            .and_then(|(_, hdr)| tpdu.get(1 + hdr).copied())
118            .unwrap_or(1)
119    }
120}
121
122impl CaDevice for LinuxCaDevice {
123    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
124        // Read one kernel frame `[slot, connection_id, <TPDU>]` into a scratch
125        // buffer and hand the bare TPDU up. `poll` gates this, so it won't block;
126        // `WouldBlock` is reported as "no data".
127        let mut frame = [0u8; 4096];
128        let n = match self.file.read(&mut frame) {
129            Ok(n) => n,
130            Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(0),
131            Err(e) => return Err(e),
132        };
133        // Strip the 2-byte link header; anything shorter has no TPDU.
134        let tpdu = frame.get(2..n).unwrap_or(&[]);
135        let copy = tpdu.len().min(buf.len());
136        buf[..copy].copy_from_slice(&tpdu[..copy]);
137        Ok(copy)
138    }
139
140    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
141        // Prepend the `[slot, connection_id]` link header the driver expects.
142        let mut frame = Vec::with_capacity(buf.len() + 2);
143        frame.push(self.slot);
144        frame.push(Self::connection_id(buf));
145        frame.extend_from_slice(buf);
146        self.file.write_all(&frame)
147    }
148
149    fn reset(&mut self) -> io::Result<()> {
150        // SAFETY: CA_RESET takes no argument; fd is a valid open CA device.
151        let r = unsafe { libc::ioctl(self.file.as_raw_fd(), CA_RESET as libc::c_ulong) };
152        if r < 0 {
153            return Err(io::Error::last_os_error());
154        }
155        // The module needs a moment to re-initialise before Create_T_C.
156        std::thread::sleep(RESET_SETTLE);
157        Ok(())
158    }
159
160    fn slot_info(&mut self) -> io::Result<SlotInfo> {
161        let mut si = CaSlotInfo {
162            num: i32::from(self.slot),
163            typ: 0,
164            flags: 0,
165        };
166        // SAFETY: CA_GET_SLOT_INFO writes a ca_slot_info; `si` is exactly that
167        // struct and outlives the call; fd is a valid open CA device.
168        let r = unsafe {
169            libc::ioctl(
170                self.file.as_raw_fd(),
171                CA_GET_SLOT_INFO as libc::c_ulong,
172                &mut si as *mut CaSlotInfo,
173            )
174        };
175        if r < 0 {
176            // Some drivers (DD/cxd2099) return EINVAL for CA_GET_SLOT_INFO;
177            // presence shows via the TPDU handshake, so assume present+ready.
178            return Ok(SlotInfo {
179                num: self.slot,
180                module_ready: true,
181                module_present: true,
182            });
183        }
184        Ok(SlotInfo {
185            num: si.num as u8,
186            module_ready: si.flags & CA_CI_MODULE_READY != 0,
187            module_present: si.flags & CA_CI_MODULE_PRESENT != 0,
188        })
189    }
190
191    fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
192        poll_readable(self.file.as_raw_fd(), timeout)
193    }
194}
195
196/// A [`CiDataDevice`] backed by a Linux DVB CI TS data-plane device
197/// (`/dev/dvb/adapterN/ciM`). The host writes scrambled TS and reads the
198/// descrambled TS back; I/O is in whole 188-byte packets.
199#[derive(Debug)]
200pub struct LinuxCiDataDevice {
201    file: File,
202}
203
204impl LinuxCiDataDevice {
205    /// Open `/dev/dvb/adapter{adapter}/ci{ci}`.
206    pub fn open(adapter: u32, ci: u32) -> io::Result<Self> {
207        let path = format!("/dev/dvb/adapter{adapter}/ci{ci}");
208        let file = OpenOptions::new().read(true).write(true).open(path)?;
209        Ok(Self { file })
210    }
211
212    /// Wrap an already-open CI data-plane device file.
213    #[must_use]
214    pub fn from_file(file: File) -> Self {
215        Self { file }
216    }
217}
218
219impl CiDataDevice for LinuxCiDataDevice {
220    fn write(&mut self, ts: &[u8]) -> io::Result<()> {
221        if !ts.len().is_multiple_of(TS_PACKET_LEN) {
222            return Err(io::Error::new(
223                io::ErrorKind::InvalidInput,
224                "write not a multiple of 188 bytes",
225            ));
226        }
227        self.file.write_all(ts)
228    }
229
230    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
231        if !buf.len().is_multiple_of(TS_PACKET_LEN) {
232            return Err(io::Error::new(
233                io::ErrorKind::InvalidInput,
234                "read buffer not a multiple of 188 bytes",
235            ));
236        }
237        match self.file.read(buf) {
238            Ok(n) => Ok(n),
239            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(0),
240            Err(e) => Err(e),
241        }
242    }
243
244    fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
245        poll_readable(self.file.as_raw_fd(), timeout)
246    }
247}