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/// A [`CaDevice`] backed by a Linux DVB CA character device.
74#[derive(Debug)]
75pub struct LinuxCaDevice {
76    file: File,
77}
78
79impl LinuxCaDevice {
80    /// Open `/dev/dvb/adapter{adapter}/ca{ca}`.
81    pub fn open(adapter: u32, ca: u32) -> io::Result<Self> {
82        let path = format!("/dev/dvb/adapter{adapter}/ca{ca}");
83        let file = OpenOptions::new().read(true).write(true).open(path)?;
84        Ok(Self { file })
85    }
86
87    /// Wrap an already-open CA device file.
88    #[must_use]
89    pub fn from_file(file: File) -> Self {
90        Self { file }
91    }
92}
93
94impl CaDevice for LinuxCaDevice {
95    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
96        // Driver calls this only after `poll` reports readable, so it will not
97        // block. A `WouldBlock` is reported as "no data".
98        match self.file.read(buf) {
99            Ok(n) => Ok(n),
100            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(0),
101            Err(e) => Err(e),
102        }
103    }
104
105    fn write(&mut self, buf: &[u8]) -> io::Result<()> {
106        self.file.write_all(buf)
107    }
108
109    fn reset(&mut self) -> io::Result<()> {
110        // SAFETY: CA_RESET takes no argument; fd is a valid open CA device.
111        let r = unsafe { libc::ioctl(self.file.as_raw_fd(), CA_RESET as libc::c_ulong) };
112        if r < 0 {
113            Err(io::Error::last_os_error())
114        } else {
115            Ok(())
116        }
117    }
118
119    fn slot_info(&mut self) -> io::Result<SlotInfo> {
120        let mut si = CaSlotInfo {
121            num: 0,
122            typ: 0,
123            flags: 0,
124        };
125        // SAFETY: CA_GET_SLOT_INFO writes a ca_slot_info; `si` is exactly that
126        // struct and outlives the call; fd is a valid open CA device.
127        let r = unsafe {
128            libc::ioctl(
129                self.file.as_raw_fd(),
130                CA_GET_SLOT_INFO as libc::c_ulong,
131                &mut si as *mut CaSlotInfo,
132            )
133        };
134        if r < 0 {
135            return Err(io::Error::last_os_error());
136        }
137        Ok(SlotInfo {
138            num: si.num as u8,
139            module_ready: si.flags & CA_CI_MODULE_READY != 0,
140        })
141    }
142
143    fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
144        poll_readable(self.file.as_raw_fd(), timeout)
145    }
146}
147
148/// A [`CiDataDevice`] backed by a Linux DVB CI TS data-plane device
149/// (`/dev/dvb/adapterN/ciM`). The host writes scrambled TS and reads the
150/// descrambled TS back; I/O is in whole 188-byte packets.
151#[derive(Debug)]
152pub struct LinuxCiDataDevice {
153    file: File,
154}
155
156impl LinuxCiDataDevice {
157    /// Open `/dev/dvb/adapter{adapter}/ci{ci}`.
158    pub fn open(adapter: u32, ci: u32) -> io::Result<Self> {
159        let path = format!("/dev/dvb/adapter{adapter}/ci{ci}");
160        let file = OpenOptions::new().read(true).write(true).open(path)?;
161        Ok(Self { file })
162    }
163
164    /// Wrap an already-open CI data-plane device file.
165    #[must_use]
166    pub fn from_file(file: File) -> Self {
167        Self { file }
168    }
169}
170
171impl CiDataDevice for LinuxCiDataDevice {
172    fn write(&mut self, ts: &[u8]) -> io::Result<()> {
173        if ts.len() % TS_PACKET_LEN != 0 {
174            return Err(io::Error::new(
175                io::ErrorKind::InvalidInput,
176                "write not a multiple of 188 bytes",
177            ));
178        }
179        self.file.write_all(ts)
180    }
181
182    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
183        if buf.len() % TS_PACKET_LEN != 0 {
184            return Err(io::Error::new(
185                io::ErrorKind::InvalidInput,
186                "read buffer not a multiple of 188 bytes",
187            ));
188        }
189        match self.file.read(buf) {
190            Ok(n) => Ok(n),
191            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(0),
192            Err(e) => Err(e),
193        }
194    }
195
196    fn poll(&mut self, timeout: Duration) -> io::Result<bool> {
197        poll_readable(self.file.as_raw_fd(), timeout)
198    }
199}