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