feat: atomic patch application, colored init output, XKB bridge, USB HID hardening

Build system (src/cook/fetch.rs):
- Atomic patch application: applies patches to staging directory (cp -al),
  atomically swaps on success, discards on failure — source tree is never
  left in a partially-patched state
- normalize_patch(): strips diff --git/index/new-file-mode headers that the
  build system's patch command does not recognize
- cleanup_workspace_pollution(): removes orphaned recipes/Cargo.toml and
  recipes/Cargo.lock to prevent workspace conflicts
- Added --allow-protected CLI flag to repo binary

Input stack (local/patches/base/P3-*.patch):
- P3-ps2d-led-feedback: PS/2 LED state handling + InputProducer migration
- P3-inputd-keymap-bridge: InputProducer enum, keymap bridge query
- P3-usbhidd-hardening: HID descriptor validation, static lookup table,
  8-button mouse support, transfer retry with exponential backoff
- P3-init-colored-output: ANSI-color coded init daemon output (green OK,
  red FAILED, yellow SKIP/WARN)

XKB bridge (local/recipes/system/redbear-keymapd/source/src/xkb.rs):
- Parses X11 xkb/symbols/* format, maps XKB keycodes to PS/2 scancodes,
  80+ X11 keysym names to Unicode, 4-level key support

Patch governance (local/patches/base/absorbed/README.md):
- Documents consolidation of P0-P3 patches into redox.patch
This commit is contained in:
2026-05-03 08:21:54 +01:00
parent 7b48083a14
commit aca2f2913d
13 changed files with 2920 additions and 40 deletions
@@ -0,0 +1,339 @@
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),
+ }
+ }
}