diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs index b68e8211..eb14de51 100644 --- a/drivers/inputd/src/lib.rs +++ b/drivers/inputd/src/lib.rs @@ -1,5 +1,5 @@ use std::fs::{File, OpenOptions}; -use std::io::{self, Read, Write}; +use std::io::{self, ErrorKind, Read, Write}; use std::mem::size_of; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, RawFd}; use std::os::unix::fs::OpenOptionsExt; @@ -31,6 +31,24 @@ unsafe fn any_as_u8_slice_mut(p: &mut T) -> &mut [u8] { slice::from_raw_parts_mut((p as *mut T) as *mut u8, size_of::()) } +fn validate_input_name(kind: &str, name: &str) -> io::Result<()> { + if name.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("input {kind} name must not be empty"), + )); + } + + if name.contains('/') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("input {kind} name must not contain '/'"), + )); + } + + Ok(()) +} + pub struct ConsumerHandle(File); pub enum ConsumerHandleEvent<'a> { @@ -64,25 +82,53 @@ impl ConsumerHandle { let fd = self.0.as_raw_fd(); let written = libredox::call::fpath(fd as usize, &mut buffer)?; - assert!(written <= buffer.len()); + if written > buffer.len() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "inputd: display path exceeded buffer size", + )); + } - let mut display_path = PathBuf::from( - std::str::from_utf8(&buffer[..written]) - .expect("init: display path UTF-8 check failed") - .to_owned(), - ); - display_path.set_file_name(format!( - "v2/{}", - display_path.file_name().unwrap().to_str().unwrap() - )); - let display_path = display_path.to_str().unwrap(); + let path_str = std::str::from_utf8(&buffer[..written]).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("inputd: display path is not valid UTF-8: {e}"), + ) + })?; + let mut display_path = PathBuf::from(path_str.to_owned()); + + let file_name = display_path + .file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "inputd: display path has no valid file name: {}", + display_path.display() + ), + ) + })?; + display_path.set_file_name(format!("v2/{file_name}")); + let display_path_str = display_path.to_str().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "inputd: constructed display path is not valid UTF-8: {}", + display_path.display() + ), + ) + })?; let display_file = - libredox::call::open(display_path, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) + libredox::call::open(display_path_str, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) .map(|socket| unsafe { File::from_raw_fd(socket as RawFd) }) - .unwrap_or_else(|err| { - panic!("failed to open display {}: {}", display_path, err); - }); + .map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("inputd: failed to open display {display_path_str}: {err}"), + ) + })?; Ok(display_file) } @@ -152,8 +198,12 @@ impl DisplayHandle { if nread == 0 { Ok(None) + } else if nread != size_of::() { + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("inputd: partial vt event read: got {nread}, expected {}", size_of::()), + )) } else { - assert_eq!(nread, size_of::()); Ok(Some(event)) } } @@ -197,6 +247,160 @@ pub struct VtEvent { pub vt: usize, } +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct HotplugEventHeader { + pub kind: u32, + pub device_id: u32, + pub name_len: u32, + pub _reserved: u32, +} + +#[derive(Debug, Clone)] +pub struct HotplugEvent { + pub kind: u32, + pub device_id: u32, + pub name: String, +} + +/// Handle for opening a named producer on the input scheme. +/// Opens /scheme/input/producer/{name} +pub struct NamedProducerHandle { + fd: File, +} + +impl NamedProducerHandle { + pub fn new(name: &str) -> io::Result { + validate_input_name("producer", name)?; + + let path = format!("/scheme/input/producer/{name}"); + File::open(path).map(|fd| Self { fd }) + } + + pub fn write_event(&mut self, event: &orbclient::Event) -> io::Result<()> { + self.fd.write(unsafe { any_as_u8_slice(event) })?; + Ok(()) + } +} + +pub struct DeviceConsumerHandle { + fd: File, +} + +impl DeviceConsumerHandle { + pub fn new(device_name: &str) -> io::Result { + validate_input_name("device", device_name)?; + + let fd = OpenOptions::new() + .read(true) + .custom_flags(O_NONBLOCK as i32) + .open(format!("/scheme/input/{device_name}"))?; + + Ok(Self { fd }) + } + + pub fn event_handle(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } + + pub fn read_event(&mut self) -> io::Result> { + let mut raw = [0_u8; size_of::()]; + + match self.fd.read(&mut raw) { + Ok(0) => Ok(None), + Ok(read) => { + assert_eq!(read, raw.len()); + Ok(Some(unsafe { + core::ptr::read_unaligned(raw.as_ptr().cast::()) + })) + } + Err(err) if err.kind() == ErrorKind::WouldBlock => Ok(None), + Err(err) => Err(err), + } + } +} + +pub struct HotplugHandle { + fd: File, + partial: Vec, +} + +impl HotplugHandle { + pub fn new() -> io::Result { + let fd = OpenOptions::new() + .read(true) + .custom_flags(O_NONBLOCK as i32) + .open("/scheme/input/events")?; + + Ok(Self { + fd, + partial: Vec::new(), + }) + } + + pub fn event_handle(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } + + pub fn read_event(&mut self) -> io::Result> { + let mut buf = [0_u8; 1024]; + + loop { + if let Some(event) = Self::try_parse_event(&mut self.partial)? { + return Ok(Some(event)); + } + + match self.fd.read(&mut buf) { + Ok(0) => return Ok(None), + Ok(read) => self.partial.extend_from_slice(&buf[..read]), + Err(err) if err.kind() == ErrorKind::WouldBlock => return Ok(None), + Err(err) => return Err(err), + } + } + } + + fn try_parse_event(partial: &mut Vec) -> io::Result> { + if partial.len() < size_of::() { + return Ok(None); + } + + let header = + unsafe { core::ptr::read_unaligned(partial.as_ptr().cast::()) }; + let name_len = usize::try_from(header.name_len).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "invalid input hotplug name length", + ) + })?; + let total_len = size_of::() + .checked_add(name_len) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "input hotplug event too large") + })?; + + if partial.len() < total_len { + return Ok(None); + } + + let name = std::str::from_utf8(&partial[size_of::()..total_len]) + .map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "input hotplug name is not UTF-8", + ) + })? + .to_owned(); + + partial.drain(..total_len); + + Ok(Some(HotplugEvent { + kind: header.kind, + device_id: header.device_id, + name, + })) + } +} + pub struct ProducerHandle(File); impl ProducerHandle { @@ -208,4 +412,58 @@ impl ProducerHandle { self.0.write(&event)?; Ok(()) } + + pub fn query_keymap_char(&self, scancode: u8, shift: bool, altgr: bool) -> Option { + use std::io::Read; + let path = format!("/scheme/keymap/translate/{}/{}/{}", scancode, shift as u8, altgr as u8); + let mut f = match std::fs::File::open(&path) { + Ok(f) => f, + Err(_) => return None, + }; + let mut buf = [0u8; 4]; + match f.read(&mut buf) { + Ok(n) if n > 0 => { + let s = std::str::from_utf8(&buf[..n]).ok()?; + s.chars().next() + } + _ => None, + } + } + + pub fn send_led_state(caps: bool, num: bool, scroll: bool) -> io::Result<()> { + let led_byte = (caps as u8) | ((num as u8) << 1) | ((scroll as u8) << 2); + let path = format!("/scheme/input/control/leds/{}", led_byte); + std::fs::write(&path, &[])?; + Ok(()) + } +} + +/// Convenience wrapper that tries a named producer first, +/// falling back to the legacy anonymous producer on failure. +pub enum InputProducer { + Named(NamedProducerHandle), + Legacy(ProducerHandle), +} + +impl InputProducer { + /// Open a named producer (`/scheme/input/producer/{name}`). + /// Falls back to the legacy anonymous producer on failure. + pub fn new_named_or_fallback(name: &str) -> io::Result { + match NamedProducerHandle::new(name) { + Ok(named) => Ok(InputProducer::Named(named)), + Err(_) => ProducerHandle::new().map(InputProducer::Legacy), + } + } + + /// Open the legacy anonymous producer directly. + pub fn new_legacy() -> io::Result { + ProducerHandle::new().map(InputProducer::Legacy) + } + + pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> { + match self { + InputProducer::Named(h) => h.write_event(&event), + InputProducer::Legacy(h) => h.write_event(event), + } + } }