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:
@@ -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),
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
Reference in New Issue
Block a user