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_READY` — the slot has a module that is ready.
64const CA_CI_MODULE_READY: u32 = 1;
65
66#[repr(C)]
67struct CaSlotInfo {
68    num: i32,
69    typ: i32,
70    flags: u32,
71}
72
73/// Settle time after `CA_RESET` before the module is usable. The DD/cxd2099
74/// (and others) only (re)initialise the slot a couple of seconds after reset;
75/// `Create_T_C` sent too early is ignored. 3s is the value validated live
76/// against a DD Octopus cxd2099 + AlphaCrypt module (2s intermittently raced the
77/// module's resource-manager open).
78const RESET_SETTLE: Duration = Duration::from_millis(3000);
79
80/// A [`CaDevice`] backed by a Linux DVB CA character device.
81///
82/// The kernel `dvb_ca_en50221` character device carries a 2-byte link header on
83/// every read/write — `[slot_id, connection_id, <TPDU>]`. This type adds/strips
84/// that header, so the sans-IO transport deals in bare TPDUs. (Writing a raw
85/// TPDU without the header is rejected `EINVAL` by the driver.)
86#[derive(Debug)]
87pub struct LinuxCaDevice {
88    file: File,
89    slot: u8,
90}
91
92impl LinuxCaDevice {
93    /// Open `/dev/dvb/adapter{adapter}/ca{ca}` (slot 0).
94    pub fn open(adapter: u32, ca: u32) -> io::Result<Self> {
95        let path = format!("/dev/dvb/adapter{adapter}/ca{ca}");
96        let file = OpenOptions::new().read(true).write(true).open(path)?;
97        Ok(Self { file, slot: 0 })
98    }
99
100    /// Wrap an already-open CA device file for `slot`.
101    #[must_use]
102    pub fn from_file(file: File, slot: u8) -> Self {
103        Self { file, slot }
104    }
105
106    /// The `connection_id` for a TPDU = its `t_c_id`, which follows the tag +
107    /// `length_field`. Falls back to 1 (the single connection) if unparseable.
108    fn connection_id(tpdu: &[u8]) -> u8 {
109        dvb_ci::length::decode(tpdu.get(1..).unwrap_or(&[]))
110            .ok()
111            .and_then(|(_, hdr)| tpdu.get(1 + hdr).copied())
112            .unwrap_or(1)
113    }
114}
115
116impl CaDevice for LinuxCaDevice {
117    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
118        // Read one kernel frame `[slot, connection_id, <TPDU>]` into a scratch
119        // buffer and hand the bare TPDU up. `poll` gates this, so it won't block;
120        // `WouldBlock` is reported as "no data".
121        let mut frame = [0u8; 4096];
122        let n = match self.file.read(&mut frame) {
123            Ok(n) => n,
124            Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(0),
125            Err(e) => return Err(e),
126        };
127        // Strip the 2-byte link header; anything shorter has no TPDU.
128        let tpdu = frame.get(2..n).unwrap_or(&[]);
129        let copy = tpdu.len().min(buf.len());
130        buf[..copy].copy_from_slice(&tpdu[..copy]);
131        Ok(copy)
132    }
133
134    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
135        // Prepend the `[slot, connection_id]` link header the driver expects.
136        let mut frame = Vec::with_capacity(buf.len() + 2);
137        frame.push(self.slot);
138        frame.push(Self::connection_id(buf));
139        frame.extend_from_slice(buf);
140        self.file.write_all(&frame)
141    }
142
143    fn reset(&mut self) -> io::Result<()> {
144        // SAFETY: CA_RESET takes no argument; fd is a valid open CA device.
145        let r = unsafe { libc::ioctl(self.file.as_raw_fd(), CA_RESET as libc::c_ulong) };
146        if r < 0 {
147            return Err(io::Error::last_os_error());
148        }
149        // The module needs a moment to re-initialise before Create_T_C.
150        std::thread::sleep(RESET_SETTLE);
151        Ok(())
152    }
153
154    fn slot_info(&mut self) -> io::Result<SlotInfo> {
155        let mut si = CaSlotInfo {
156            num: i32::from(self.slot),
157            typ: 0,
158            flags: 0,
159        };
160        // SAFETY: CA_GET_SLOT_INFO writes a ca_slot_info; `si` is exactly that
161        // struct and outlives the call; fd is a valid open CA device.
162        let r = unsafe {
163            libc::ioctl(
164                self.file.as_raw_fd(),
165                CA_GET_SLOT_INFO as libc::c_ulong,
166                &mut si as *mut CaSlotInfo,
167            )
168        };
169        if r < 0 {
170            // Some drivers (DD/cxd2099) return EINVAL for CA_GET_SLOT_INFO;
171            // presence shows via the TPDU handshake, so assume ready.
172            return Ok(SlotInfo {
173                num: self.slot,
174                module_ready: true,
175            });
176        }
177        Ok(SlotInfo {
178            num: si.num as u8,
179            module_ready: si.flags & CA_CI_MODULE_READY != 0,
180        })
181    }
182
183    fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
184        poll_readable(self.file.as_raw_fd(), timeout)
185    }
186}
187
188/// A [`CiDataDevice`] backed by a Linux DVB CI TS data-plane device
189/// (`/dev/dvb/adapterN/ciM`). The host writes scrambled TS and reads the
190/// descrambled TS back; I/O is in whole 188-byte packets.
191#[derive(Debug)]
192pub struct LinuxCiDataDevice {
193    file: File,
194}
195
196impl LinuxCiDataDevice {
197    /// Open `/dev/dvb/adapter{adapter}/ci{ci}`.
198    pub fn open(adapter: u32, ci: u32) -> io::Result<Self> {
199        let path = format!("/dev/dvb/adapter{adapter}/ci{ci}");
200        let file = OpenOptions::new().read(true).write(true).open(path)?;
201        Ok(Self { file })
202    }
203
204    /// Wrap an already-open CI data-plane device file.
205    #[must_use]
206    pub fn from_file(file: File) -> Self {
207        Self { file }
208    }
209}
210
211impl CiDataDevice for LinuxCiDataDevice {
212    fn write(&mut self, ts: &[u8]) -> io::Result<()> {
213        if ts.len() % TS_PACKET_LEN != 0 {
214            return Err(io::Error::new(
215                io::ErrorKind::InvalidInput,
216                "write not a multiple of 188 bytes",
217            ));
218        }
219        self.file.write_all(ts)
220    }
221
222    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
223        if buf.len() % TS_PACKET_LEN != 0 {
224            return Err(io::Error::new(
225                io::ErrorKind::InvalidInput,
226                "read buffer not a multiple of 188 bytes",
227            ));
228        }
229        match self.file.read(buf) {
230            Ok(n) => Ok(n),
231            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(0),
232            Err(e) => Err(e),
233        }
234    }
235
236    fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
237        poll_readable(self.file.as_raw_fd(), timeout)
238    }
239}