use std::fs;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::{Path, PathBuf};
use super::{HidBackend, HidDeviceBackend};
use crate::descriptor::ReportDescriptor;
use crate::error::{HidError, HidResult};
use crate::{BusType, DeviceInfo, MAX_REPORT_DESCRIPTOR_SIZE};
#[cfg(not(any(
target_arch = "mips",
target_arch = "mips64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "sparc64",
)))]
mod ioc {
pub(super) const SIZEBITS: u32 = 14; pub(super) const WRITE: u32 = 1;
pub(super) const READ: u32 = 2;
}
#[cfg(any(
target_arch = "mips",
target_arch = "mips64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "sparc64",
))]
mod ioc {
pub const SIZEBITS: u32 = 13; pub const READ: u32 = 2;
pub const WRITE: u32 = 4;
}
const NRBITS: u32 = 8;
const TYPEBITS: u32 = 8;
const NRSHIFT: u32 = 0;
const TYPESHIFT: u32 = NRSHIFT + NRBITS;
const SIZESHIFT: u32 = TYPESHIFT + TYPEBITS;
const DIRSHIFT: u32 = SIZESHIFT + ioc::SIZEBITS;
const fn ioc(dir: u32, ty: u8, nr: u8, size: usize) -> libc::c_ulong {
((dir << DIRSHIFT)
| ((ty as u32) << TYPESHIFT)
| ((nr as u32) << NRSHIFT)
| ((size as u32) << SIZESHIFT)) as libc::c_ulong
}
const HIDRAW_MAGIC: u8 = b'H';
const fn hidiocgrdescsize() -> libc::c_ulong {
ioc(
ioc::READ,
HIDRAW_MAGIC,
0x01,
core::mem::size_of::<libc::c_int>(),
)
}
#[repr(C)]
struct HidrawReportDescriptor {
size: u32,
value: [u8; MAX_REPORT_DESCRIPTOR_SIZE],
}
const fn hidiocgrdesc() -> libc::c_ulong {
ioc(
ioc::READ,
HIDRAW_MAGIC,
0x02,
core::mem::size_of::<HidrawReportDescriptor>(),
)
}
const fn hidiocsfeature(len: usize) -> libc::c_ulong {
ioc(ioc::WRITE | ioc::READ, HIDRAW_MAGIC, 0x06, len)
}
const fn hidiocgfeature(len: usize) -> libc::c_ulong {
ioc(ioc::WRITE | ioc::READ, HIDRAW_MAGIC, 0x07, len)
}
const fn hidiocginput(len: usize) -> libc::c_ulong {
ioc(ioc::WRITE | ioc::READ, HIDRAW_MAGIC, 0x0A, len)
}
fn bus_type_from_id(bus: u32) -> BusType {
match bus {
0x03 => BusType::Usb,
0x05 => BusType::Bluetooth,
0x18 => BusType::I2c,
0x1C => BusType::Spi,
_ => BusType::Unknown,
}
}
#[derive(Debug, Default, PartialEq)]
struct Uevent {
bus: u32,
vendor_id: u16,
product_id: u16,
name: Option<String>,
serial: Option<String>,
}
fn parse_uevent(content: &str) -> Option<Uevent> {
let mut ev = Uevent::default();
let mut have_id = false;
for line in content.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key {
"HID_ID" => {
let mut parts = value.split(':');
let bus = u32::from_str_radix(parts.next()?, 16).ok()?;
let vid = u32::from_str_radix(parts.next()?, 16).ok()?;
let pid = u32::from_str_radix(parts.next()?, 16).ok()?;
ev.bus = bus;
ev.vendor_id = vid as u16;
ev.product_id = pid as u16;
have_id = true;
}
"HID_NAME" if !value.is_empty() => ev.name = Some(value.to_string()),
"HID_UNIQ" if !value.is_empty() => ev.serial = Some(value.to_string()),
_ => {}
}
}
have_id.then_some(ev)
}
fn read_sysfs_string(dir: &Path, file: &str) -> Option<String> {
let s = fs::read_to_string(dir.join(file)).ok()?;
let s = s.trim_end_matches('\n');
(!s.is_empty()).then(|| s.to_string())
}
fn read_sysfs_hex(dir: &Path, file: &str) -> Option<u32> {
let s = read_sysfs_string(dir, file)?;
u32::from_str_radix(s.trim(), 16).ok()
}
#[derive(Debug, Default)]
struct UsbInfo {
manufacturer: Option<String>,
product: Option<String>,
release_number: u16,
interface_number: i32,
}
fn collect_usb_info(hid_dev_dir: &Path) -> Option<UsbInfo> {
let mut info = UsbInfo {
interface_number: -1,
..Default::default()
};
let mut dir = hid_dev_dir.canonicalize().ok()?;
for _ in 0..8 {
if info.interface_number < 0 {
if let Some(n) = read_sysfs_hex(&dir, "bInterfaceNumber") {
info.interface_number = n as i32;
}
}
if dir.join("idVendor").exists() && dir.join("idProduct").exists() {
info.manufacturer = read_sysfs_string(&dir, "manufacturer");
info.product = read_sysfs_string(&dir, "product");
info.release_number = read_sysfs_hex(&dir, "bcdDevice").unwrap_or(0) as u16;
return Some(info);
}
dir = dir.parent()?.to_path_buf();
}
None
}
fn device_infos(hid_dev_dir: &Path, dev_path: &str) -> Option<Vec<DeviceInfo>> {
let uevent = fs::read_to_string(hid_dev_dir.join("uevent")).ok()?;
let uevent = parse_uevent(&uevent)?;
let bus_type = bus_type_from_id(uevent.bus);
let mut info = DeviceInfo {
path: dev_path.to_string(),
vendor_id: uevent.vendor_id,
product_id: uevent.product_id,
bus_type,
interface_number: -1,
..Default::default()
};
match bus_type {
BusType::Usb => {
info.serial_number = uevent.serial;
if let Some(usb) = collect_usb_info(hid_dev_dir) {
info.manufacturer_string = usb.manufacturer;
info.product_string = usb.product;
info.release_number = usb.release_number;
info.interface_number = usb.interface_number;
} else {
info.product_string = uevent.name;
}
}
_ => {
info.product_string = uevent.name;
info.serial_number = uevent.serial;
}
}
let usages = fs::read(hid_dev_dir.join("report_descriptor"))
.ok()
.and_then(|d| ReportDescriptor::parse(&d).ok())
.map(|d| d.top_level_usages())
.unwrap_or_default();
Some(info.per_usage(&usages))
}
#[derive(Debug)]
pub struct HidrawApi;
impl HidBackend for HidrawApi {
type Device = HidrawDevice;
fn new() -> HidResult<Self> {
Ok(HidrawApi)
}
fn enumerate(&self, vendor_id: u16, product_id: u16) -> HidResult<Vec<DeviceInfo>> {
let mut result = Vec::new();
let entries = match fs::read_dir("/sys/class/hidraw") {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(result),
Err(e) => return Err(HidError::io("reading /sys/class/hidraw", e)),
};
let mut nodes: Vec<_> = entries
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
nodes.sort();
for node in nodes {
let hid_dev_dir = PathBuf::from("/sys/class/hidraw")
.join(&node)
.join("device");
let dev_path = format!("/dev/{node}");
let Some(infos) = device_infos(&hid_dev_dir, &dev_path) else {
continue;
};
result.extend(
infos
.into_iter()
.filter(|info| info.matches(vendor_id, product_id)),
);
}
Ok(result)
}
fn open_path(&self, path: &str) -> HidResult<HidrawDevice> {
HidrawDevice::open(path)
}
}
#[derive(Debug)]
pub struct HidrawDevice {
fd: OwnedFd,
info: Option<DeviceInfo>,
}
impl HidrawDevice {
fn open(path: &str) -> HidResult<Self> {
let c_path = std::ffi::CString::new(path).map_err(|_| HidError::InvalidData {
message: "path contains NUL".into(),
})?;
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
if fd < 0 {
let err = std::io::Error::last_os_error();
return Err(match err.kind() {
std::io::ErrorKind::NotFound => HidError::DeviceNotFound,
_ => HidError::OpenFailed {
message: format!("{path}: {err}"),
},
});
}
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let sysfs_hid_dir = sysfs_dir_for_fd(fd.as_raw_fd())?;
let info = device_infos(&sysfs_hid_dir, path)
.and_then(|mut i| (!i.is_empty()).then(|| i.remove(0)));
Ok(HidrawDevice { fd, info })
}
fn raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
fn try_read_now(&self, buf: &mut [u8]) -> HidResult<Option<usize>> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "read buffer must not be empty".into(),
});
}
let mut pollfd = libc::pollfd {
fd: self.raw_fd(),
events: libc::POLLIN,
revents: 0,
};
loop {
let res = unsafe { libc::poll(&mut pollfd, 1, 0) };
if res < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(HidError::io("poll on hidraw device", err));
}
if res == 0 {
return Ok(None);
}
break;
}
if pollfd.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0 {
return Err(HidError::Disconnected);
}
let res = unsafe { libc::read(self.raw_fd(), buf.as_mut_ptr().cast(), buf.len()) };
if res < 0 {
let err = std::io::Error::last_os_error();
return match err.raw_os_error() {
Some(libc::EAGAIN) | Some(libc::EINPROGRESS) => Ok(None),
Some(libc::EIO) | Some(libc::ENODEV) => Err(HidError::Disconnected),
_ => Err(HidError::io("hidraw read", err)),
};
}
Ok((res > 0).then_some(res as usize))
}
}
impl HidDeviceBackend for HidrawDevice {
type Read<'a> = ReadAsync<'a>;
fn write(&self, data: &[u8]) -> HidResult<usize> {
if data.is_empty() {
return Err(HidError::InvalidData {
message: "write data must contain a report ID byte".into(),
});
}
let res = loop {
let r = unsafe { libc::write(self.raw_fd(), data.as_ptr().cast(), data.len()) };
if r >= 0 {
break r as usize;
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(HidError::io("hidraw write", err));
};
Ok(res)
}
fn read_async<'a>(&'a self, buf: &'a mut [u8]) -> ReadAsync<'a> {
ReadAsync { dev: self, buf }
}
fn send_feature_report(&self, data: &[u8]) -> HidResult<()> {
if data.is_empty() {
return Err(HidError::InvalidData {
message: "feature report must contain a report ID byte".into(),
});
}
let res = unsafe {
libc::ioctl(
self.raw_fd(),
hidiocsfeature(data.len()) as _,
data.as_ptr(),
)
};
if res < 0 {
return Err(HidError::last_os_error("HIDIOCSFEATURE"));
}
Ok(())
}
fn get_feature_report(&self, buf: &mut [u8]) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "buffer must contain a report ID byte".into(),
});
}
let res = unsafe {
libc::ioctl(
self.raw_fd(),
hidiocgfeature(buf.len()) as _,
buf.as_mut_ptr(),
)
};
if res < 0 {
return Err(HidError::last_os_error("HIDIOCGFEATURE"));
}
Ok(res as usize)
}
fn get_input_report(&self, buf: &mut [u8]) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "buffer must contain a report ID byte".into(),
});
}
let res = unsafe {
libc::ioctl(
self.raw_fd(),
hidiocginput(buf.len()) as _,
buf.as_mut_ptr(),
)
};
if res < 0 {
let err = std::io::Error::last_os_error();
return Err(match err.raw_os_error() {
Some(libc::EINVAL) | Some(libc::ENOTTY) => HidError::Unsupported {
message: "HIDIOCGINPUT requires Linux 5.11+".into(),
},
_ => HidError::io("HIDIOCGINPUT", err),
});
}
Ok(res as usize)
}
fn get_manufacturer_string(&self) -> HidResult<Option<String>> {
Ok(self.get_device_info()?.manufacturer_string)
}
fn get_product_string(&self) -> HidResult<Option<String>> {
Ok(self.get_device_info()?.product_string)
}
fn get_serial_number_string(&self) -> HidResult<Option<String>> {
Ok(self.get_device_info()?.serial_number)
}
fn get_indexed_string(&self, _index: u32) -> HidResult<Option<String>> {
Err(HidError::Unsupported {
message: "indexed strings are not available via hidraw; use the usb backend".into(),
})
}
fn get_report_descriptor(&self, buf: &mut [u8]) -> HidResult<usize> {
let mut size: libc::c_int = 0;
let res = unsafe { libc::ioctl(self.raw_fd(), hidiocgrdescsize() as _, &mut size) };
if res < 0 {
return Err(HidError::last_os_error("HIDIOCGRDESCSIZE"));
}
let mut desc = HidrawReportDescriptor {
size: size as u32,
value: [0; MAX_REPORT_DESCRIPTOR_SIZE],
};
let res = unsafe { libc::ioctl(self.raw_fd(), hidiocgrdesc() as _, &mut desc) };
if res < 0 {
return Err(HidError::last_os_error("HIDIOCGRDESC"));
}
let len = (desc.size as usize).min(buf.len());
buf[..len].copy_from_slice(&desc.value[..len]);
Ok(len)
}
fn get_device_info(&self) -> HidResult<DeviceInfo> {
self.info
.clone()
.ok_or_else(|| HidError::backend("failed to read device metadata from sysfs"))
}
}
impl Drop for HidrawDevice {
fn drop(&mut self) {
super::reactor::Reactor::global().deregister(self.fd.as_raw_fd());
}
}
#[derive(Debug)]
pub struct ReadAsync<'a> {
dev: &'a HidrawDevice,
buf: &'a mut [u8],
}
impl core::future::Future for ReadAsync<'_> {
type Output = HidResult<usize>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.get_mut();
match this.dev.try_read_now(this.buf) {
Ok(Some(n)) => std::task::Poll::Ready(Ok(n)),
Ok(None) => {
super::reactor::Reactor::global().register(this.dev.raw_fd(), cx.waker());
std::task::Poll::Pending
}
Err(e) => std::task::Poll::Ready(Err(e)),
}
}
}
fn sysfs_dir_for_fd(fd: RawFd) -> HidResult<PathBuf> {
let mut stat = unsafe { core::mem::zeroed::<libc::stat>() };
let res = unsafe { libc::fstat(fd, &mut stat) };
if res < 0 {
return Err(HidError::last_os_error("fstat on hidraw device"));
}
let major = libc::major(stat.st_rdev);
let minor = libc::minor(stat.st_rdev);
let class_dir = PathBuf::from(format!("/sys/dev/char/{major}:{minor}"));
class_dir
.join("device")
.canonicalize()
.map_err(|e| HidError::io("resolving sysfs device directory", e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_usb_uevent() {
let content = "DRIVER=hid-generic\n\
HID_ID=0003:0000046D:0000C216\n\
HID_NAME=Logitech Logitech Dual Action\n\
HID_PHYS=usb-0000:00:14.0-2/input0\n\
HID_UNIQ=\n\
MODALIAS=hid:b0003g0001v0000046Dp0000C216\n";
let ev = parse_uevent(content).unwrap();
assert_eq!(ev.bus, 3);
assert_eq!(ev.vendor_id, 0x046D);
assert_eq!(ev.product_id, 0xC216);
assert_eq!(ev.name.as_deref(), Some("Logitech Logitech Dual Action"));
assert_eq!(ev.serial, None);
assert_eq!(bus_type_from_id(ev.bus), BusType::Usb);
}
#[test]
fn parses_bluetooth_uevent() {
let content = "HID_ID=0005:0000054C:00000268\n\
HID_NAME=PLAYSTATION(R)3 Controller\n\
HID_UNIQ=00:19:c1:5c:f5:11\n";
let ev = parse_uevent(content).unwrap();
assert_eq!(bus_type_from_id(ev.bus), BusType::Bluetooth);
assert_eq!(ev.serial.as_deref(), Some("00:19:c1:5c:f5:11"));
}
#[test]
fn rejects_uevent_without_hid_id() {
assert_eq!(parse_uevent("DRIVER=hid-generic\n"), None);
}
#[test]
fn ioctl_numbers_match_kernel_headers() {
#[cfg(target_arch = "x86_64")]
{
assert_eq!(hidiocgrdescsize(), 0x80044801);
assert_eq!(hidiocgrdesc(), 0x90044802);
assert_eq!(hidiocsfeature(8), 0xC0084806);
assert_eq!(hidiocgfeature(8), 0xC0084807);
}
}
#[test]
fn write_preserves_leading_report_id_byte() {
let mut fds = [0i32; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let read_fd = fds[0];
let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
let dev = HidrawDevice {
fd: write_fd,
info: None,
};
let n = dev.write(&[0x00, 0xAA, 0xBB]).unwrap();
assert_eq!(n, 3, "returned length must count the report-ID byte");
let mut got = [0u8; 8];
let r = unsafe { libc::read(read_fd, got.as_mut_ptr().cast(), got.len()) };
unsafe { libc::close(read_fd) };
assert_eq!(r, 3, "all three bytes must reach the fd");
assert_eq!(&got[..3], &[0x00, 0xAA, 0xBB]);
}
#[test]
fn enumeration_does_not_panic() {
let api = HidrawApi::new().unwrap();
let devices = api.enumerate(0, 0).unwrap();
for d in &devices {
assert!(d.path().starts_with("/dev/hidraw"));
}
}
}