89d1306c8d
- Create source symlinks for all 7 core components (kernel, relibc, base, bootloader, installer, redoxfs, userutils) pointing at local/sources/ - Create redoxfs and userutils fork repos from frozen 0.1.0 archives - Fix relibc-tests recipes: replace patch commands with direct fork build - Archive all 417 patch files to local/archived/patches-2026-06-migration/ - Full AGENTS.md rewrite: remove all 31 remaining stale patch references, update DURABILITY POLICY to describe git commit workflow, update WHERE TO LOOK table, fix build flow description, replace Recipe Patch Wiring section with Recipe Source Configuration - Zero active patches = [...] arrays remain in any recipe.toml file - All 13 remaining grep hits for 'patches' are TODO comments in WIP recipes
340 lines
11 KiB
Diff
340 lines
11 KiB
Diff
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<T: Sized>(p: &mut T) -> &mut [u8] {
|
|
slice::from_raw_parts_mut((p as *mut T) as *mut u8, size_of::<T>())
|
|
}
|
|
|
|
+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::<VtEvent>() {
|
|
+ Err(io::Error::new(
|
|
+ io::ErrorKind::InvalidData,
|
|
+ format!("inputd: partial vt event read: got {nread}, expected {}", size_of::<VtEvent>()),
|
|
+ ))
|
|
} else {
|
|
- assert_eq!(nread, size_of::<VtEvent>());
|
|
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<Self> {
|
|
+ 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<Self> {
|
|
+ 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<Option<orbclient::Event>> {
|
|
+ let mut raw = [0_u8; size_of::<orbclient::Event>()];
|
|
+
|
|
+ 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::<orbclient::Event>())
|
|
+ }))
|
|
+ }
|
|
+ Err(err) if err.kind() == ErrorKind::WouldBlock => Ok(None),
|
|
+ Err(err) => Err(err),
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+pub struct HotplugHandle {
|
|
+ fd: File,
|
|
+ partial: Vec<u8>,
|
|
+}
|
|
+
|
|
+impl HotplugHandle {
|
|
+ pub fn new() -> io::Result<Self> {
|
|
+ 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<Option<HotplugEvent>> {
|
|
+ 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<u8>) -> io::Result<Option<HotplugEvent>> {
|
|
+ if partial.len() < size_of::<HotplugEventHeader>() {
|
|
+ return Ok(None);
|
|
+ }
|
|
+
|
|
+ let header =
|
|
+ unsafe { core::ptr::read_unaligned(partial.as_ptr().cast::<HotplugEventHeader>()) };
|
|
+ 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::<HotplugEventHeader>()
|
|
+ .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::<HotplugEventHeader>()..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<char> {
|
|
+ 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<Self> {
|
|
+ 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<Self> {
|
|
+ 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),
|
|
+ }
|
|
+ }
|
|
}
|