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,260 @@
|
||||
--- a/init/src/color.rs 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/init/src/color.rs 2026-05-03 08:14:33.424932357 +0100
|
||||
@@ -0,0 +1,27 @@
|
||||
+pub fn status_ok(msg: &str) {
|
||||
+ eprintln!("[32m[[1;32m OK [0m[32m][0m {msg}");
|
||||
+}
|
||||
+
|
||||
+pub fn status_fail(msg: &str) {
|
||||
+ eprintln!("[31m[[1;31m FAILED [0m[31m][0m {msg}");
|
||||
+}
|
||||
+
|
||||
+pub fn status_skip(msg: &str) {
|
||||
+ eprintln!("[33m[[1;33m SKIP [0m[33m][0m {msg}");
|
||||
+}
|
||||
+
|
||||
+pub fn init_error(msg: &str) {
|
||||
+ eprintln!("[31m[1minit:[0m {msg}");
|
||||
+}
|
||||
+
|
||||
+pub fn init_warn(msg: &str) {
|
||||
+ eprintln!("[33minit:[0m {msg}");
|
||||
+}
|
||||
+
|
||||
+pub fn init_info(msg: &str) {
|
||||
+ eprintln!("init: {msg}");
|
||||
+}
|
||||
+
|
||||
+pub fn init_debug(msg: &str) {
|
||||
+ eprintln!("[36minit:[0m {msg}");
|
||||
+}
|
||||
--- a/init/src/main.rs 2026-05-03 05:35:37.646633516 +0100
|
||||
+++ b/init/src/main.rs 2026-05-03 08:14:33.426007641 +0100
|
||||
@@ -8,11 +8,14 @@
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::unit::{UnitId, UnitStore};
|
||||
|
||||
+mod color;
|
||||
mod scheduler;
|
||||
mod script;
|
||||
mod service;
|
||||
mod unit;
|
||||
|
||||
+use crate::color::{init_error, init_warn, status_fail, status_ok};
|
||||
+
|
||||
fn switch_stdio(stdio: &str) -> io::Result<()> {
|
||||
let stdin = libredox::Fd::open(stdio, O_RDONLY, 0)?;
|
||||
let stdout = libredox::Fd::open(stdio, O_WRONLY, 0)?;
|
||||
@@ -49,11 +52,7 @@
|
||||
}
|
||||
|
||||
fn switch_root(unit_store: &mut UnitStore, config: &mut InitConfig, prefix: &Path, etcdir: &Path) {
|
||||
- eprintln!(
|
||||
- "init: switchroot to {} {}",
|
||||
- prefix.display(),
|
||||
- etcdir.display()
|
||||
- );
|
||||
+ status_ok(&format!("switchroot to {} {}", prefix.display(), etcdir.display()));
|
||||
|
||||
config
|
||||
.envs
|
||||
@@ -79,10 +78,7 @@
|
||||
continue;
|
||||
}
|
||||
let Some((key, value)) = env.split_once('=') else {
|
||||
- eprintln!(
|
||||
- "init: failed to parse env line from {}: {env:?}",
|
||||
- file.display(),
|
||||
- );
|
||||
+ init_warn(&format!("failed to parse env line from {}: {:?}", file.display(), env));
|
||||
continue;
|
||||
};
|
||||
config
|
||||
@@ -91,23 +87,18 @@
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
- eprintln!(
|
||||
- "init: failed to read environment from {}: {err}",
|
||||
- file.display(),
|
||||
- );
|
||||
+ init_error(&format!("failed to read environment from {}: {}", file.display(), err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
- eprintln!(
|
||||
- "init: failed to read environments from {}: {err}",
|
||||
+ init_error(&format!("failed to read environments from {}: {}",
|
||||
env_dirs
|
||||
.iter()
|
||||
.map(|dir| dir.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
- .join(", ")
|
||||
- );
|
||||
+ .join(", "), err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +120,7 @@
|
||||
.schedule_start_and_report_errors(&mut unit_store, UnitId("00_logd.service".to_owned()));
|
||||
scheduler.step(&mut unit_store, &mut init_config);
|
||||
if let Err(err) = switch_stdio("/scheme/log") {
|
||||
- eprintln!("init: failed to switch stdio to '/scheme/log': {err}");
|
||||
+ init_error(&format!("failed to switch stdio to '/scheme/log': {}", err));
|
||||
}
|
||||
|
||||
let runtime_target = UnitId("00_runtime.target".to_owned());
|
||||
@@ -153,15 +144,13 @@
|
||||
let entries = match config::config_for_dirs(&unit_store.config_dirs) {
|
||||
Ok(entries) => entries,
|
||||
Err(err) => {
|
||||
- eprintln!(
|
||||
- "init: failed to read configs from {}: {err}",
|
||||
+ init_error(&format!("failed to read configs from {}: {}",
|
||||
unit_store
|
||||
.config_dirs
|
||||
.iter()
|
||||
.map(|dir| dir.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
- .join(", ")
|
||||
- );
|
||||
+ .join(", "), err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -186,7 +175,7 @@
|
||||
let mut status = 0;
|
||||
match libredox::call::waitpid(0, &mut status, 1) {
|
||||
Ok(_pid) => {}
|
||||
- Err(err) => eprintln!("init: waitpid error: {err}"),
|
||||
+ Err(err) => init_error(&format!("waitpid error: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
--- a/init/src/scheduler.rs 2026-05-03 05:35:37.539232447 +0100
|
||||
+++ b/init/src/scheduler.rs 2026-05-03 08:14:33.426815867 +0100
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::InitConfig;
|
||||
+use crate::color::{init_debug, init_error, init_info, status_ok, status_skip};
|
||||
use crate::unit::{Unit, UnitId, UnitKind, UnitStore};
|
||||
|
||||
pub struct Scheduler {
|
||||
@@ -31,7 +32,7 @@
|
||||
let mut errors = vec![];
|
||||
self.schedule_start(unit_store, unit_id, &mut errors);
|
||||
for error in errors {
|
||||
- eprintln!("init: {error}");
|
||||
+ init_error(&format!("{}", error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,31 +86,28 @@
|
||||
UnitKind::LegacyScript { script } => {
|
||||
for cmd in script.clone() {
|
||||
if config.log_debug {
|
||||
- eprintln!("init: running: {cmd:?}");
|
||||
+ init_debug(&format!("running: {cmd:?}"));
|
||||
}
|
||||
cmd.run(config);
|
||||
}
|
||||
}
|
||||
UnitKind::Service { service } => {
|
||||
+ let desc = unit.info.description.as_ref().unwrap_or(&unit.id.0);
|
||||
if config.skip_cmd.contains(&service.cmd) {
|
||||
- eprintln!("Skipping '{} {}'", service.cmd, service.args.join(" "));
|
||||
+ status_skip(&format!("Skipping {} ({})", desc, service.cmd));
|
||||
return;
|
||||
}
|
||||
if config.log_debug {
|
||||
- eprintln!(
|
||||
- "Starting {} ({})",
|
||||
- unit.info.description.as_ref().unwrap_or(&unit.id.0),
|
||||
- service.cmd,
|
||||
- );
|
||||
+ init_info(&format!("Starting {} ({})", desc, service.cmd));
|
||||
+ } else {
|
||||
+ status_ok(&format!("Started {}", desc));
|
||||
}
|
||||
service.spawn(&config.envs);
|
||||
}
|
||||
UnitKind::Target {} => {
|
||||
if config.log_debug {
|
||||
- eprintln!(
|
||||
- "Reached target {}",
|
||||
- unit.info.description.as_ref().unwrap_or(&unit.id.0),
|
||||
- );
|
||||
+ init_info(&format!("Reached target {}",
|
||||
+ unit.info.description.as_ref().unwrap_or(&unit.id.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
--- a/init/src/service.rs 2026-05-03 05:35:37.594239723 +0100
|
||||
+++ b/init/src/service.rs 2026-05-03 08:14:33.427953738 +0100
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
+use crate::color::{init_error, init_warn, status_fail};
|
||||
use crate::script::subst_env;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -52,7 +53,7 @@
|
||||
let mut child = match command.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
- eprintln!("init: failed to execute {:?}: {}", command, err);
|
||||
+ status_fail(&format!("failed to execute {:?}: {}", command, err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -61,10 +62,10 @@
|
||||
ServiceType::Notify => match read_pipe.read_exact(&mut [0]) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
- eprintln!("init: {command:?} exited without notifying readiness");
|
||||
+ init_warn(&format!("{:?} exited without notifying readiness", command));
|
||||
}
|
||||
Err(err) => {
|
||||
- eprintln!("init: failed to wait for {command:?}: {err}");
|
||||
+ init_error(&format!("failed to wait for {:?}: {}", command, err));
|
||||
}
|
||||
},
|
||||
ServiceType::Scheme(scheme) => {
|
||||
@@ -80,16 +81,16 @@
|
||||
errno: syscall::EINTR,
|
||||
}) => continue,
|
||||
Ok(0) => {
|
||||
- eprintln!("init: {command:?} exited without notifying readiness");
|
||||
+ init_warn(&format!("{:?} exited without notifying readiness", command));
|
||||
return;
|
||||
}
|
||||
Ok(1) => break,
|
||||
Ok(n) => {
|
||||
- eprintln!("init: incorrect amount of fds {n} returned");
|
||||
+ init_error(&format!("incorrect amount of fds {} returned", n));
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
- eprintln!("init: failed to wait for {command:?}: {err}");
|
||||
+ init_error(&format!("failed to wait for {:?}: {}", command, err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -104,11 +105,11 @@
|
||||
match child.wait() {
|
||||
Ok(exit_status) => {
|
||||
if !exit_status.success() {
|
||||
- eprintln!("init: {command:?} failed with {exit_status}");
|
||||
+ status_fail(&format!("{:?} failed with {}", command, exit_status));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
- eprintln!("init: failed to wait for {:?}: {}", command, err)
|
||||
+ init_error(&format!("failed to wait for {:?}: {}", command, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
--- a/drivers/input/ps2d/src/controller.rs 21:31:04.000000000 +0100
|
||||
+++ b/drivers/input/ps2d/src/controller.rs 2026-05-02 02:50:19.907179957 +0100
|
||||
@@ -97,6 +97,14 @@
|
||||
const DEFAULT_TIMEOUT: u64 = 50_000;
|
||||
// Reset timeout in microseconds
|
||||
const RESET_TIMEOUT: u64 = 1_000_000;
|
||||
+// Maximum bytes to drain during flush (Linux: I8042_BUFFER_SIZE)
|
||||
+const FLUSH_LIMIT: usize = 4096;
|
||||
+// Controller self-test pass value (Linux: I8042_RET_CTL_TEST)
|
||||
+const SELFTEST_PASS: u8 = 0x55;
|
||||
+// Controller self-test retries (Linux: 5 attempts)
|
||||
+const SELFTEST_RETRIES: usize = 5;
|
||||
+// AUX port test pass value (Linux returns 0x00 on success)
|
||||
+const AUX_TEST_PASS: u8 = 0x00;
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
pub struct Ps2 {
|
||||
@@ -261,6 +269,30 @@
|
||||
self.write(command as u8)
|
||||
}
|
||||
|
||||
+ pub fn set_leds(&mut self, caps: bool, num: bool, scroll: bool) {
|
||||
+ let mut led_byte = 0u8;
|
||||
+ if scroll { led_byte |= 1; }
|
||||
+ if num { led_byte |= 2; }
|
||||
+ if caps { led_byte |= 4; }
|
||||
+ if let Err(err) = self.keyboard_command_inner(0xED) {
|
||||
+ warn!("ps2d: failed to send LED command 0xED: {:?}", err);
|
||||
+ return;
|
||||
+ }
|
||||
+ match self.read_timeout(DEFAULT_TIMEOUT) {
|
||||
+ Ok(0xFA) => {
|
||||
+ if let Err(err) = self.write(led_byte) {
|
||||
+ warn!("ps2d: failed to send LED byte {:02X}: {:?}", led_byte, err);
|
||||
+ }
|
||||
+ }
|
||||
+ Ok(val) => {
|
||||
+ warn!("ps2d: LED command ACK expected 0xFA, got {:02X}", val);
|
||||
+ }
|
||||
+ Err(err) => {
|
||||
+ warn!("ps2d: LED command ACK timeout: {:?}", err);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
pub fn next(&mut self) -> Option<(bool, u8)> {
|
||||
let status = self.status();
|
||||
if status.contains(StatusFlags::OUTPUT_FULL) {
|
||||
@@ -271,6 +303,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ /// Drain all pending bytes from the controller output buffer.
|
||||
+ /// Borrowed from Linux i8042_flush(): stale firmware/BIOS bytes can be
|
||||
+ /// misinterpreted as device responses during initialization.
|
||||
+ fn flush(&mut self) -> usize {
|
||||
+ let mut count = 0;
|
||||
+ while self.status().contains(StatusFlags::OUTPUT_FULL) {
|
||||
+ if count >= FLUSH_LIMIT {
|
||||
+ warn!("flush: exceeded limit, controller may be stuck");
|
||||
+ break;
|
||||
+ }
|
||||
+ let data = self.data.read();
|
||||
+ trace!("flush: discarded {:02X}", data);
|
||||
+ count += 1;
|
||||
+ }
|
||||
+ if count > 0 {
|
||||
+ debug!("flushed {} stale bytes from controller", count);
|
||||
+ }
|
||||
+ count
|
||||
+ }
|
||||
+
|
||||
+ /// Test the AUX (mouse) port via controller command 0xA9.
|
||||
+ /// Borrowed from Linux: verifies electrical connectivity before
|
||||
+ /// attempting to talk to the mouse. Returns true if the port passed.
|
||||
+ fn test_aux_port(&mut self) -> bool {
|
||||
+ if let Err(err) = self.command(Command::TestSecond) {
|
||||
+ warn!("aux port test command failed: {:?}", err);
|
||||
+ return false;
|
||||
+ }
|
||||
+ match self.read() {
|
||||
+ Ok(AUX_TEST_PASS) => {
|
||||
+ debug!("aux port test passed");
|
||||
+ true
|
||||
+ }
|
||||
+ Ok(val) => {
|
||||
+ warn!("aux port test failed: {:02X}", val);
|
||||
+ false
|
||||
+ }
|
||||
+ Err(err) => {
|
||||
+ warn!("aux port test read timeout: {:?}", err);
|
||||
+ false
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
pub fn init_keyboard(&mut self) -> Result<(), Error> {
|
||||
let mut b;
|
||||
|
||||
@@ -308,66 +384,125 @@
|
||||
}
|
||||
|
||||
pub fn init(&mut self) -> Result<(), Error> {
|
||||
+ // Linux i8042_controller_check(): verify controller is present by
|
||||
+ // flushing any stale data. A stuck output buffer means no controller.
|
||||
+ self.flush();
|
||||
+
|
||||
+ // Bare-metal controllers may be slow after firmware handoff.
|
||||
+ // Give the controller a moment to finish POST before sending commands.
|
||||
+ std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
+
|
||||
{
|
||||
- // Disable devices
|
||||
- self.command(Command::DisableFirst)?;
|
||||
- self.command(Command::DisableSecond)?;
|
||||
+ // Disable both ports first — use retry because the controller
|
||||
+ // may still be settling or temporarily unresponsive.
|
||||
+ // Failure here is non-fatal: we continue and attempt the rest
|
||||
+ // of initialization. A truly absent controller will fail later
|
||||
+ // at self-test or keyboard reset.
|
||||
+ if let Err(err) = self.retry(
|
||||
+ format_args!("disable first port"),
|
||||
+ 3,
|
||||
+ |x| x.command(Command::DisableFirst),
|
||||
+ ) {
|
||||
+ warn!("disable first port failed: {:?}", err);
|
||||
+ }
|
||||
+ if let Err(err) = self.retry(
|
||||
+ format_args!("disable second port"),
|
||||
+ 3,
|
||||
+ |x| x.command(Command::DisableSecond),
|
||||
+ ) {
|
||||
+ warn!("disable second port failed: {:?}", err);
|
||||
+ }
|
||||
}
|
||||
|
||||
- // Disable clocks, disable interrupts, and disable translate
|
||||
+ // Flush again after disabling — firmware may have queued more bytes
|
||||
+ self.flush();
|
||||
+
|
||||
+ // Linux i8042_controller_init() step 1: write a known-safe config
|
||||
+ // (interrupts off, both ports disabled) so stale config can't cause
|
||||
+ // spurious interrupts during the rest of init.
|
||||
{
|
||||
- // Since the default config may have interrupts enabled, and the kernel may eat up
|
||||
- // our data in that case, we will write a config without reading the current one
|
||||
let config = ConfigFlags::POST_PASSED
|
||||
| ConfigFlags::FIRST_DISABLED
|
||||
| ConfigFlags::SECOND_DISABLED;
|
||||
self.set_config(config)?;
|
||||
}
|
||||
|
||||
- // The keyboard seems to still collect bytes even when we disable
|
||||
- // the port, so we must disable the keyboard too
|
||||
+ // Linux i8042_controller_selftest(): retry up to 5 times with delay.
|
||||
+ // "On some really fragile systems this does not take the first time."
|
||||
+ {
|
||||
+ let mut passed = false;
|
||||
+ for attempt in 0..SELFTEST_RETRIES {
|
||||
+ if let Err(err) = self.command(Command::TestController) {
|
||||
+ warn!("self-test command failed (attempt {}): {:?}", attempt + 1, err);
|
||||
+ continue;
|
||||
+ }
|
||||
+ match self.read() {
|
||||
+ Ok(SELFTEST_PASS) => {
|
||||
+ passed = true;
|
||||
+ break;
|
||||
+ }
|
||||
+ Ok(val) => {
|
||||
+ warn!(
|
||||
+ "self-test unexpected value {:02X} (attempt {}/{})",
|
||||
+ val,
|
||||
+ attempt + 1,
|
||||
+ SELFTEST_RETRIES
|
||||
+ );
|
||||
+ }
|
||||
+ Err(err) => {
|
||||
+ warn!("self-test read timeout (attempt {}): {:?}", attempt + 1, err);
|
||||
+ }
|
||||
+ }
|
||||
+ // Linux: msleep(50) between retries
|
||||
+ std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
+ }
|
||||
+ if !passed {
|
||||
+ // Linux on x86: "giving up on controller selftest, continuing anyway"
|
||||
+ warn!("controller self-test did not pass after {} attempts, continuing", SELFTEST_RETRIES);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Flush any bytes the self-test may have left behind
|
||||
+ self.flush();
|
||||
+
|
||||
+ // Linux i8042_controller_init() step 2: set keyboard defaults
|
||||
+ // (disable scanning so keyboard doesn't send scancodes during init)
|
||||
self.retry(format_args!("keyboard defaults"), 4, |x| {
|
||||
- // Set defaults and disable scanning
|
||||
let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?;
|
||||
if b != 0xFA {
|
||||
error!("keyboard failed to set defaults: {:02X}", b);
|
||||
return Err(Error::CommandRetry);
|
||||
}
|
||||
-
|
||||
Ok(b)
|
||||
})?;
|
||||
|
||||
- {
|
||||
- // Perform the self test
|
||||
- self.command(Command::TestController)?;
|
||||
- let r = self.read()?;
|
||||
- if r != 0x55 {
|
||||
- warn!("self test unexpected value: {:02X}", r);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
// Initialize keyboard
|
||||
if let Err(err) = self.init_keyboard() {
|
||||
error!("failed to initialize keyboard: {:?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
- // Enable second device
|
||||
- let enable_mouse = match self.command(Command::EnableSecond) {
|
||||
- Ok(()) => true,
|
||||
- Err(err) => {
|
||||
- error!("failed to initialize mouse: {:?}", err);
|
||||
- false
|
||||
+ // Linux: test AUX port (command 0xA9) before enabling.
|
||||
+ // Skips mouse init entirely if the port is not electrically present.
|
||||
+ let aux_ok = self.test_aux_port();
|
||||
+
|
||||
+ // Enable second device (mouse) only if AUX port tested OK
|
||||
+ let enable_mouse = if aux_ok {
|
||||
+ match self.command(Command::EnableSecond) {
|
||||
+ Ok(()) => true,
|
||||
+ Err(err) => {
|
||||
+ warn!("failed to enable aux port after test passed: {:?}", err);
|
||||
+ false
|
||||
+ }
|
||||
}
|
||||
+ } else {
|
||||
+ info!("skipping mouse init: aux port test did not pass");
|
||||
+ false
|
||||
};
|
||||
|
||||
{
|
||||
- // Enable keyboard data reporting
|
||||
- // Use inner function to prevent retries
|
||||
- // Response is ignored since scanning is now on
|
||||
if let Err(err) = self.keyboard_command_inner(KeyboardCommand::EnableReporting as u8) {
|
||||
error!("failed to initialize keyboard reporting: {:?}", err);
|
||||
- //TODO: fix by using interrupts?
|
||||
}
|
||||
}
|
||||
|
||||
--- a/drivers/input/ps2d/src/state.rs 21:31:04.000000000 +0100
|
||||
+++ b/drivers/input/ps2d/src/state.rs 2026-05-02 04:22:27.342569199 +0100
|
||||
@@ -1,4 +1,4 @@
|
||||
-use inputd::ProducerHandle;
|
||||
+use inputd::InputProducer;
|
||||
use log::{error, warn};
|
||||
use orbclient::{ButtonEvent, KeyEvent, MouseEvent, MouseRelativeEvent, ScrollEvent};
|
||||
use std::{
|
||||
@@ -44,7 +44,8 @@
|
||||
ps2: Ps2,
|
||||
vmmouse: bool,
|
||||
vmmouse_relative: bool,
|
||||
- input: ProducerHandle,
|
||||
+ keyboard_input: InputProducer,
|
||||
+ mouse_input: InputProducer,
|
||||
time_file: File,
|
||||
extended: bool,
|
||||
mouse_x: i32,
|
||||
@@ -56,12 +57,18 @@
|
||||
mouse_timeout: Option<TimeSpec>,
|
||||
packets: [u8; 4],
|
||||
packet_i: usize,
|
||||
+ caps_lock: bool,
|
||||
+ num_lock: bool,
|
||||
+ scroll_lock: bool,
|
||||
+ leds_dirty: bool,
|
||||
}
|
||||
|
||||
impl Ps2d {
|
||||
- pub fn new(input: ProducerHandle, time_file: File) -> Self {
|
||||
+ pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self {
|
||||
let mut ps2 = Ps2::new();
|
||||
- ps2.init().expect("failed to initialize");
|
||||
+ if let Err(err) = ps2.init() {
|
||||
+ log::error!("ps2d: controller init failed: {:?}", err);
|
||||
+ }
|
||||
|
||||
// FIXME add an option for orbital to disable this when an app captures the mouse.
|
||||
let vmmouse_relative = false;
|
||||
@@ -77,7 +84,8 @@
|
||||
ps2,
|
||||
vmmouse,
|
||||
vmmouse_relative,
|
||||
- input,
|
||||
+ keyboard_input,
|
||||
+ mouse_input,
|
||||
time_file,
|
||||
extended: false,
|
||||
mouse_x: 0,
|
||||
@@ -89,6 +97,10 @@
|
||||
mouse_timeout: None,
|
||||
packets: [0; 4],
|
||||
packet_i: 0,
|
||||
+ caps_lock: false,
|
||||
+ num_lock: true,
|
||||
+ scroll_lock: false,
|
||||
+ leds_dirty: true,
|
||||
};
|
||||
|
||||
if !this.vmmouse {
|
||||
@@ -96,6 +108,12 @@
|
||||
this.handle_mouse(None);
|
||||
}
|
||||
|
||||
+ // Flush initial LED state (Num Lock on by default)
|
||||
+ if this.leds_dirty {
|
||||
+ this.leds_dirty = false;
|
||||
+ this.ps2.set_leds(this.caps_lock, this.num_lock, this.scroll_lock);
|
||||
+ }
|
||||
+
|
||||
this
|
||||
}
|
||||
|
||||
@@ -272,8 +290,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
+ if scancode != 0 && pressed {
|
||||
+ match scancode {
|
||||
+ orbclient::K_CAPS => { self.caps_lock = !self.caps_lock; self.leds_dirty = true; },
|
||||
+ orbclient::K_NUM => { self.num_lock = !self.num_lock; self.leds_dirty = true; },
|
||||
+ orbclient::K_SCROLL => { self.scroll_lock = !self.scroll_lock; self.leds_dirty = true; },
|
||||
+ _ => (),
|
||||
+ }
|
||||
+ }
|
||||
+ if self.leds_dirty {
|
||||
+ self.leds_dirty = false;
|
||||
+ self.ps2.set_leds(self.caps_lock, self.num_lock, self.scroll_lock);
|
||||
+ }
|
||||
+
|
||||
if scancode != 0 {
|
||||
- self.input
|
||||
+ self.keyboard_input
|
||||
.write_event(
|
||||
KeyEvent {
|
||||
character: '\0',
|
||||
@@ -304,7 +335,7 @@
|
||||
|
||||
if self.vmmouse_relative {
|
||||
if dx != 0 || dy != 0 {
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(
|
||||
MouseRelativeEvent {
|
||||
dx: dx as i32,
|
||||
@@ -320,14 +351,14 @@
|
||||
if x != self.mouse_x || y != self.mouse_y {
|
||||
self.mouse_x = x;
|
||||
self.mouse_y = y;
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(MouseEvent { x, y }.to_event())
|
||||
.expect("ps2d: failed to write mouse event");
|
||||
}
|
||||
};
|
||||
|
||||
if dz != 0 {
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(
|
||||
ScrollEvent {
|
||||
x: 0,
|
||||
@@ -348,7 +379,7 @@
|
||||
self.mouse_left = left;
|
||||
self.mouse_middle = middle;
|
||||
self.mouse_right = right;
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(
|
||||
ButtonEvent {
|
||||
left,
|
||||
@@ -441,13 +472,13 @@
|
||||
}
|
||||
|
||||
if dx != 0 || dy != 0 {
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(MouseRelativeEvent { dx, dy }.to_event())
|
||||
.expect("ps2d: failed to write mouse event");
|
||||
}
|
||||
|
||||
if dz != 0 {
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(ScrollEvent { x: 0, y: dz }.to_event())
|
||||
.expect("ps2d: failed to write scroll event");
|
||||
}
|
||||
@@ -462,7 +493,7 @@
|
||||
self.mouse_left = left;
|
||||
self.mouse_middle = middle;
|
||||
self.mouse_right = right;
|
||||
- self.input
|
||||
+ self.mouse_input
|
||||
.write_event(
|
||||
ButtonEvent {
|
||||
left,
|
||||
|
||||
|
||||
diff --git a/drivers/input/ps2d/src/main.rs b/drivers/input/ps2d/src/main.rs
|
||||
index db17de2a..86f903bf 100644
|
||||
|
||||
--- a/drivers/input/ps2d/src/main.rs 21:31:04.000000000 +0100
|
||||
+++ b/drivers/input/ps2d/src/main.rs
|
||||
@@ -14,4 +14,4 @@
|
||||
-use inputd::ProducerHandle;
|
||||
+use inputd::InputProducer;
|
||||
|
||||
use crate::state::Ps2d;
|
||||
|
||||
@@ -31,7 +31,8 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
|
||||
acquire_port_io_rights().expect("ps2d: failed to get I/O permission");
|
||||
|
||||
- let input = ProducerHandle::new().expect("ps2d: failed to open input producer");
|
||||
+ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard").expect("ps2d: failed to open keyboard input");
|
||||
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
|
||||
|
||||
user_data! {
|
||||
enum Source {
|
||||
@@ -93,7 +94,7 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
|
||||
daemon.ready();
|
||||
|
||||
- let mut ps2d = Ps2d::new(input, time_file);
|
||||
+ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
|
||||
|
||||
let mut data = [0; 256];
|
||||
for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) {
|
||||
+31,8 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
|
||||
acquire_port_io_rights().expect("ps2d: failed to get I/O permission");
|
||||
|
||||
- let input = ProducerHandle::new().expect("ps2d: failed to open input producer");
|
||||
+ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard").expect("ps2d: failed to open keyboard input");
|
||||
+ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse").expect("ps2d: failed to open mouse input");
|
||||
|
||||
user_data! {
|
||||
enum Source {
|
||||
@@ -93,7 +94,7 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
|
||||
daemon.ready();
|
||||
|
||||
- let mut ps2d = Ps2d::new(input, time_file);
|
||||
+ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
|
||||
|
||||
let mut data = [0; 256];
|
||||
for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) {
|
||||
@@ -0,0 +1,725 @@
|
||||
diff --git a/drivers/input/usbhidd/src/main.rs b/drivers/input/usbhidd/src/main.rs
|
||||
index 15c5b778..472ec4cf 100644
|
||||
--- a/drivers/input/usbhidd/src/main.rs
|
||||
+++ b/drivers/input/usbhidd/src/main.rs
|
||||
@@ -1,7 +1,7 @@
|
||||
-use anyhow::{Context, Result};
|
||||
+use anyhow::{bail, ensure, Context, Result};
|
||||
use std::{env, thread, time};
|
||||
|
||||
-use inputd::ProducerHandle;
|
||||
+use inputd::InputProducer;
|
||||
use orbclient::KeyEvent as OrbKeyEvent;
|
||||
use rehid::{
|
||||
report_desc::{ReportTy, REPORT_DESC_TY},
|
||||
@@ -15,161 +15,219 @@ use xhcid_interface::{
|
||||
|
||||
mod reqs;
|
||||
|
||||
-fn send_key_event(display: &mut ProducerHandle, usage_page: u16, usage: u16, pressed: bool) {
|
||||
- let scancode = match usage_page {
|
||||
- 0x07 => match usage {
|
||||
- 0x04 => orbclient::K_A,
|
||||
- 0x05 => orbclient::K_B,
|
||||
- 0x06 => orbclient::K_C,
|
||||
- 0x07 => orbclient::K_D,
|
||||
- 0x08 => orbclient::K_E,
|
||||
- 0x09 => orbclient::K_F,
|
||||
- 0x0A => orbclient::K_G,
|
||||
- 0x0B => orbclient::K_H,
|
||||
- 0x0C => orbclient::K_I,
|
||||
- 0x0D => orbclient::K_J,
|
||||
- 0x0E => orbclient::K_K,
|
||||
- 0x0F => orbclient::K_L,
|
||||
- 0x10 => orbclient::K_M,
|
||||
- 0x11 => orbclient::K_N,
|
||||
- 0x12 => orbclient::K_O,
|
||||
- 0x13 => orbclient::K_P,
|
||||
- 0x14 => orbclient::K_Q,
|
||||
- 0x15 => orbclient::K_R,
|
||||
- 0x16 => orbclient::K_S,
|
||||
- 0x17 => orbclient::K_T,
|
||||
- 0x18 => orbclient::K_U,
|
||||
- 0x19 => orbclient::K_V,
|
||||
- 0x1A => orbclient::K_W,
|
||||
- 0x1B => orbclient::K_X,
|
||||
- 0x1C => orbclient::K_Y,
|
||||
- 0x1D => orbclient::K_Z,
|
||||
- 0x1E => orbclient::K_1,
|
||||
- 0x1F => orbclient::K_2,
|
||||
- 0x20 => orbclient::K_3,
|
||||
- 0x21 => orbclient::K_4,
|
||||
- 0x22 => orbclient::K_5,
|
||||
- 0x23 => orbclient::K_6,
|
||||
- 0x24 => orbclient::K_7,
|
||||
- 0x25 => orbclient::K_8,
|
||||
- 0x26 => orbclient::K_9,
|
||||
- 0x27 => orbclient::K_0,
|
||||
- 0x28 => orbclient::K_ENTER,
|
||||
- 0x29 => orbclient::K_ESC,
|
||||
- 0x2A => orbclient::K_BKSP,
|
||||
- 0x2B => orbclient::K_TAB,
|
||||
- 0x2C => orbclient::K_SPACE,
|
||||
- 0x2D => orbclient::K_MINUS,
|
||||
- 0x2E => orbclient::K_EQUALS,
|
||||
- 0x2F => orbclient::K_BRACE_OPEN,
|
||||
- 0x30 => orbclient::K_BRACE_CLOSE,
|
||||
- 0x31 => orbclient::K_BACKSLASH,
|
||||
- // 0x32 non-us # and ~
|
||||
- 0x32 => 0x56,
|
||||
- 0x33 => orbclient::K_SEMICOLON,
|
||||
- 0x34 => orbclient::K_QUOTE,
|
||||
- 0x35 => orbclient::K_TICK,
|
||||
- 0x36 => orbclient::K_COMMA,
|
||||
- 0x37 => orbclient::K_PERIOD,
|
||||
- 0x38 => orbclient::K_SLASH,
|
||||
- 0x39 => orbclient::K_CAPS,
|
||||
- 0x3A => orbclient::K_F1,
|
||||
- 0x3B => orbclient::K_F2,
|
||||
- 0x3C => orbclient::K_F3,
|
||||
- 0x3D => orbclient::K_F4,
|
||||
- 0x3E => orbclient::K_F5,
|
||||
- 0x3F => orbclient::K_F6,
|
||||
- 0x40 => orbclient::K_F7,
|
||||
- 0x41 => orbclient::K_F8,
|
||||
- 0x42 => orbclient::K_F9,
|
||||
- 0x43 => orbclient::K_F10,
|
||||
- 0x44 => orbclient::K_F11,
|
||||
- 0x45 => orbclient::K_F12,
|
||||
- 0x46 => orbclient::K_PRTSC,
|
||||
- 0x47 => orbclient::K_SCROLL,
|
||||
- // 0x48 pause
|
||||
- 0x49 => orbclient::K_INS,
|
||||
- 0x4A => orbclient::K_HOME,
|
||||
- 0x4B => orbclient::K_PGUP,
|
||||
- 0x4C => orbclient::K_DEL,
|
||||
- 0x4D => orbclient::K_END,
|
||||
- 0x4E => orbclient::K_PGDN,
|
||||
- 0x4F => orbclient::K_RIGHT,
|
||||
- 0x50 => orbclient::K_LEFT,
|
||||
- 0x51 => orbclient::K_DOWN,
|
||||
- 0x52 => orbclient::K_UP,
|
||||
- 0x53 => orbclient::K_NUM,
|
||||
- 0x54 => orbclient::K_NUM_SLASH,
|
||||
- 0x55 => orbclient::K_NUM_ASTERISK,
|
||||
- 0x56 => orbclient::K_NUM_MINUS,
|
||||
- 0x57 => orbclient::K_NUM_PLUS,
|
||||
- 0x58 => orbclient::K_NUM_ENTER,
|
||||
- 0x59 => orbclient::K_NUM_1,
|
||||
- 0x5A => orbclient::K_NUM_2,
|
||||
- 0x5B => orbclient::K_NUM_3,
|
||||
- 0x5C => orbclient::K_NUM_4,
|
||||
- 0x5D => orbclient::K_NUM_5,
|
||||
- 0x5E => orbclient::K_NUM_6,
|
||||
- 0x5F => orbclient::K_NUM_7,
|
||||
- 0x60 => orbclient::K_NUM_8,
|
||||
- 0x61 => orbclient::K_NUM_9,
|
||||
- 0x62 => orbclient::K_NUM_0,
|
||||
- // 0x62 num .
|
||||
- // 0x64 non-us \ and |
|
||||
- 0x64 => orbclient::K_APP,
|
||||
- 0x66 => orbclient::K_POWER,
|
||||
- // 0x67 num =
|
||||
- // unmapped values
|
||||
- 0xE0 => orbclient::K_LEFT_CTRL,
|
||||
- 0xE1 => orbclient::K_LEFT_SHIFT,
|
||||
- 0xE2 => orbclient::K_ALT,
|
||||
- 0xE3 => orbclient::K_LEFT_SUPER,
|
||||
- 0xE4 => orbclient::K_RIGHT_CTRL,
|
||||
- 0xE5 => orbclient::K_RIGHT_SHIFT,
|
||||
- 0xE6 => orbclient::K_ALT_GR,
|
||||
- 0xE7 => orbclient::K_RIGHT_SUPER,
|
||||
- // reserved values
|
||||
- _ => {
|
||||
- log::warn!("unknown usage_page {:#x} usage {:#x}", usage_page, usage);
|
||||
- return;
|
||||
- }
|
||||
- },
|
||||
- _ => {
|
||||
- log::warn!("unknown usage_page {:#x}", usage_page);
|
||||
+const MAX_MOUSE_BUTTONS: usize = 8;
|
||||
+const MAX_RETRIES: u32 = 3;
|
||||
+const RETRY_BASE_DELAY_MS: u64 = 2;
|
||||
+const REPORT_DESC_MAX_SIZE: usize = 4096;
|
||||
+const REPORT_DESC_MIN_SIZE: usize = 3;
|
||||
+
|
||||
+/// USB HID Usage (Keyboard page 0x07) → orbclient scancode lookup table.
|
||||
+/// Covers usage codes 0x04–0xE7 per USB HID Usage Tables spec v1.12.
|
||||
+/// Entries default to 0 (unmapped); only defined usages are populated.
|
||||
+static HID_KEYBOARD_TO_SCANCODE: [u8; 0xE8] = {
|
||||
+ let mut table = [0u8; 0xE8];
|
||||
+ table[0x04] = orbclient::K_A;
|
||||
+ table[0x05] = orbclient::K_B;
|
||||
+ table[0x06] = orbclient::K_C;
|
||||
+ table[0x07] = orbclient::K_D;
|
||||
+ table[0x08] = orbclient::K_E;
|
||||
+ table[0x09] = orbclient::K_F;
|
||||
+ table[0x0A] = orbclient::K_G;
|
||||
+ table[0x0B] = orbclient::K_H;
|
||||
+ table[0x0C] = orbclient::K_I;
|
||||
+ table[0x0D] = orbclient::K_J;
|
||||
+ table[0x0E] = orbclient::K_K;
|
||||
+ table[0x0F] = orbclient::K_L;
|
||||
+ table[0x10] = orbclient::K_M;
|
||||
+ table[0x11] = orbclient::K_N;
|
||||
+ table[0x12] = orbclient::K_O;
|
||||
+ table[0x13] = orbclient::K_P;
|
||||
+ table[0x14] = orbclient::K_Q;
|
||||
+ table[0x15] = orbclient::K_R;
|
||||
+ table[0x16] = orbclient::K_S;
|
||||
+ table[0x17] = orbclient::K_T;
|
||||
+ table[0x18] = orbclient::K_U;
|
||||
+ table[0x19] = orbclient::K_V;
|
||||
+ table[0x1A] = orbclient::K_W;
|
||||
+ table[0x1B] = orbclient::K_X;
|
||||
+ table[0x1C] = orbclient::K_Y;
|
||||
+ table[0x1D] = orbclient::K_Z;
|
||||
+ table[0x1E] = orbclient::K_1;
|
||||
+ table[0x1F] = orbclient::K_2;
|
||||
+ table[0x20] = orbclient::K_3;
|
||||
+ table[0x21] = orbclient::K_4;
|
||||
+ table[0x22] = orbclient::K_5;
|
||||
+ table[0x23] = orbclient::K_6;
|
||||
+ table[0x24] = orbclient::K_7;
|
||||
+ table[0x25] = orbclient::K_8;
|
||||
+ table[0x26] = orbclient::K_9;
|
||||
+ table[0x27] = orbclient::K_0;
|
||||
+ table[0x28] = orbclient::K_ENTER;
|
||||
+ table[0x29] = orbclient::K_ESC;
|
||||
+ table[0x2A] = orbclient::K_BKSP;
|
||||
+ table[0x2B] = orbclient::K_TAB;
|
||||
+ table[0x2C] = orbclient::K_SPACE;
|
||||
+ table[0x2D] = orbclient::K_MINUS;
|
||||
+ table[0x2E] = orbclient::K_EQUALS;
|
||||
+ table[0x2F] = orbclient::K_BRACE_OPEN;
|
||||
+ table[0x30] = orbclient::K_BRACE_CLOSE;
|
||||
+ table[0x31] = orbclient::K_BACKSLASH;
|
||||
+ table[0x32] = 0x56;
|
||||
+ table[0x33] = orbclient::K_SEMICOLON;
|
||||
+ table[0x34] = orbclient::K_QUOTE;
|
||||
+ table[0x35] = orbclient::K_TICK;
|
||||
+ table[0x36] = orbclient::K_COMMA;
|
||||
+ table[0x37] = orbclient::K_PERIOD;
|
||||
+ table[0x38] = orbclient::K_SLASH;
|
||||
+ table[0x39] = orbclient::K_CAPS;
|
||||
+ table[0x3A] = orbclient::K_F1;
|
||||
+ table[0x3B] = orbclient::K_F2;
|
||||
+ table[0x3C] = orbclient::K_F3;
|
||||
+ table[0x3D] = orbclient::K_F4;
|
||||
+ table[0x3E] = orbclient::K_F5;
|
||||
+ table[0x3F] = orbclient::K_F6;
|
||||
+ table[0x40] = orbclient::K_F7;
|
||||
+ table[0x41] = orbclient::K_F8;
|
||||
+ table[0x42] = orbclient::K_F9;
|
||||
+ table[0x43] = orbclient::K_F10;
|
||||
+ table[0x44] = orbclient::K_F11;
|
||||
+ table[0x45] = orbclient::K_F12;
|
||||
+ table[0x46] = orbclient::K_PRTSC;
|
||||
+ table[0x47] = orbclient::K_SCROLL;
|
||||
+ table[0x49] = orbclient::K_INS;
|
||||
+ table[0x4A] = orbclient::K_HOME;
|
||||
+ table[0x4B] = orbclient::K_PGUP;
|
||||
+ table[0x4C] = orbclient::K_DEL;
|
||||
+ table[0x4D] = orbclient::K_END;
|
||||
+ table[0x4E] = orbclient::K_PGDN;
|
||||
+ table[0x4F] = orbclient::K_RIGHT;
|
||||
+ table[0x50] = orbclient::K_LEFT;
|
||||
+ table[0x51] = orbclient::K_DOWN;
|
||||
+ table[0x52] = orbclient::K_UP;
|
||||
+ table[0x53] = orbclient::K_NUM;
|
||||
+ table[0x54] = orbclient::K_NUM_SLASH;
|
||||
+ table[0x55] = orbclient::K_NUM_ASTERISK;
|
||||
+ table[0x56] = orbclient::K_NUM_MINUS;
|
||||
+ table[0x57] = orbclient::K_NUM_PLUS;
|
||||
+ table[0x58] = orbclient::K_NUM_ENTER;
|
||||
+ table[0x59] = orbclient::K_NUM_1;
|
||||
+ table[0x5A] = orbclient::K_NUM_2;
|
||||
+ table[0x5B] = orbclient::K_NUM_3;
|
||||
+ table[0x5C] = orbclient::K_NUM_4;
|
||||
+ table[0x5D] = orbclient::K_NUM_5;
|
||||
+ table[0x5E] = orbclient::K_NUM_6;
|
||||
+ table[0x5F] = orbclient::K_NUM_7;
|
||||
+ table[0x60] = orbclient::K_NUM_8;
|
||||
+ table[0x61] = orbclient::K_NUM_9;
|
||||
+ table[0x62] = orbclient::K_NUM_0;
|
||||
+ table[0x64] = orbclient::K_APP;
|
||||
+ table[0x66] = orbclient::K_POWER;
|
||||
+ table[0xE0] = orbclient::K_LEFT_CTRL;
|
||||
+ table[0xE1] = orbclient::K_LEFT_SHIFT;
|
||||
+ table[0xE2] = orbclient::K_ALT;
|
||||
+ table[0xE3] = orbclient::K_LEFT_SUPER;
|
||||
+ table[0xE4] = orbclient::K_RIGHT_CTRL;
|
||||
+ table[0xE5] = orbclient::K_RIGHT_SHIFT;
|
||||
+ table[0xE6] = orbclient::K_ALT_GR;
|
||||
+ table[0xE7] = orbclient::K_RIGHT_SUPER;
|
||||
+ table
|
||||
+};
|
||||
+
|
||||
+fn hid_usage_to_scancode(usage: u16) -> Option<u8> {
|
||||
+ let idx = usage as usize;
|
||||
+ if idx < HID_KEYBOARD_TO_SCANCODE.len() {
|
||||
+ let sc = HID_KEYBOARD_TO_SCANCODE[idx];
|
||||
+ if sc != 0 {
|
||||
+ return Some(sc);
|
||||
+ }
|
||||
+ }
|
||||
+ None
|
||||
+}
|
||||
+
|
||||
+fn send_key_event(display: &mut InputProducer, usage_page: u16, usage: u16, pressed: bool) {
|
||||
+ if usage_page != 0x07 {
|
||||
+ log::warn!("send_key_event: unexpected usage_page {:#x}", usage_page);
|
||||
+ return;
|
||||
+ }
|
||||
+ let scancode = match hid_usage_to_scancode(usage) {
|
||||
+ Some(sc) => sc,
|
||||
+ None => {
|
||||
+ log::warn!("unmapped HID keyboard usage {:#x}", usage);
|
||||
return;
|
||||
}
|
||||
};
|
||||
-
|
||||
let key_event = OrbKeyEvent {
|
||||
character: '\0',
|
||||
scancode,
|
||||
pressed,
|
||||
};
|
||||
+ if let Err(err) = display.write_event(key_event.to_event()) {
|
||||
+ log::warn!("failed to send key event: {}", err);
|
||||
+ }
|
||||
+}
|
||||
|
||||
- match display.write_event(key_event.to_event()) {
|
||||
- Ok(_) => (),
|
||||
- Err(err) => {
|
||||
- log::warn!("failed to send key event to orbital: {}", err);
|
||||
+fn validate_report_descriptor(bytes: &[u8]) -> Result<()> {
|
||||
+ ensure!(
|
||||
+ bytes.len() >= REPORT_DESC_MIN_SIZE,
|
||||
+ "report descriptor too short: {} bytes (minimum {})",
|
||||
+ bytes.len(),
|
||||
+ REPORT_DESC_MIN_SIZE
|
||||
+ );
|
||||
+ ensure!(
|
||||
+ bytes.len() <= REPORT_DESC_MAX_SIZE,
|
||||
+ "report descriptor too large: {} bytes (maximum {})",
|
||||
+ bytes.len(),
|
||||
+ REPORT_DESC_MAX_SIZE
|
||||
+ );
|
||||
+ let mut depth = 0u32;
|
||||
+ let mut i = 0;
|
||||
+ while i < bytes.len() {
|
||||
+ let b = bytes[i];
|
||||
+ let size = match b & 0x03 {
|
||||
+ 0 => 0,
|
||||
+ 1 => 1,
|
||||
+ 2 => 2,
|
||||
+ 3 => 4,
|
||||
+ _ => unreachable!(),
|
||||
+ };
|
||||
+ let item_type = (b >> 2) & 0x03;
|
||||
+ if item_type == 0x0A {
|
||||
+ depth += 1;
|
||||
+ } else if item_type == 0x0C {
|
||||
+ if depth == 0 {
|
||||
+ bail!(
|
||||
+ "unbalanced Collection/EndCollection in report descriptor at offset {}",
|
||||
+ i
|
||||
+ );
|
||||
+ }
|
||||
+ depth -= 1;
|
||||
}
|
||||
+ i += 1 + size as usize;
|
||||
}
|
||||
+ ensure!(
|
||||
+ depth == 0,
|
||||
+ "unbalanced Collection/EndCollection (depth {} at end)",
|
||||
+ depth
|
||||
+ );
|
||||
+ Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
- const USAGE: &'static str = "usbhidd <scheme> <port> <interface>";
|
||||
+ const USAGE: &str = "usbhidd <scheme> <port> <interface>";
|
||||
|
||||
- let scheme = args.next().expect(USAGE);
|
||||
- let port = args
|
||||
+ let scheme = args.next().context(USAGE)?;
|
||||
+ let port: PortId = args
|
||||
.next()
|
||||
- .expect(USAGE)
|
||||
- .parse::<PortId>()
|
||||
- .expect("Expected port ID");
|
||||
- let interface_num = args
|
||||
+ .context(USAGE)?
|
||||
+ .parse()
|
||||
+ .map_err(|e| anyhow::anyhow!("Expected port ID: {}", e))?;
|
||||
+ let interface_num: u8 = args
|
||||
.next()
|
||||
- .expect(USAGE)
|
||||
- .parse::<u8>()
|
||||
- .expect("Expected integer as input of interface");
|
||||
+ .context(USAGE)?
|
||||
+ .parse()
|
||||
+ .map_err(|e| anyhow::anyhow!("Expected interface number: {}", e))?;
|
||||
|
||||
let name = format!("{}_{}_{}_hid", scheme, port, interface_num);
|
||||
common::setup_logging(
|
||||
@@ -218,7 +276,6 @@ fn main() -> Result<()> {
|
||||
}
|
||||
});
|
||||
let hid_desc = if_desc.hid_descs.iter().find_map(|hid_desc| {
|
||||
- //TODO: should we do any filtering?
|
||||
Some(hid_desc)
|
||||
})?;
|
||||
Some((if_desc.clone(), endp_desc_opt, hid_desc))
|
||||
@@ -240,31 +297,39 @@ fn main() -> Result<()> {
|
||||
})
|
||||
.context("Failed to configure endpoints")?;
|
||||
|
||||
- //TODO: do we need to set protocol to report? It fails for mice.
|
||||
-
|
||||
- //TODO: dynamically create good values, fix xhcid so it does not block on each request
|
||||
- // This sets all reports to a duration of 4ms
|
||||
reqs::set_idle(&handle, 1, 0, interface_num as u16).context("Failed to set idle")?;
|
||||
|
||||
- let report_desc_len = hid_desc.desc_len;
|
||||
- assert_eq!(hid_desc.desc_ty, REPORT_DESC_TY);
|
||||
+ let report_desc_len = hid_desc.desc_len as usize;
|
||||
+ ensure!(
|
||||
+ hid_desc.desc_ty == REPORT_DESC_TY,
|
||||
+ "unexpected HID descriptor type: expected {}, got {}",
|
||||
+ REPORT_DESC_TY,
|
||||
+ hid_desc.desc_ty
|
||||
+ );
|
||||
+ ensure!(
|
||||
+ report_desc_len >= REPORT_DESC_MIN_SIZE && report_desc_len <= REPORT_DESC_MAX_SIZE,
|
||||
+ "suspicious report descriptor length: {}",
|
||||
+ report_desc_len
|
||||
+ );
|
||||
|
||||
- let mut report_desc_bytes = vec![0u8; report_desc_len as usize];
|
||||
+ let mut report_desc_bytes = vec![0u8; report_desc_len];
|
||||
handle
|
||||
.get_descriptor(
|
||||
PortReqRecipient::Interface,
|
||||
REPORT_DESC_TY,
|
||||
0,
|
||||
- //TODO: should this be an index into interface_descs?
|
||||
interface_num as u16,
|
||||
&mut report_desc_bytes,
|
||||
)
|
||||
.context("Failed to retrieve report descriptor")?;
|
||||
|
||||
+ validate_report_descriptor(&report_desc_bytes)
|
||||
+ .context("HID report descriptor validation failed")?;
|
||||
+
|
||||
let mut handler =
|
||||
- ReportHandler::new(&report_desc_bytes).expect("failed to parse report descriptor");
|
||||
+ ReportHandler::new(&report_desc_bytes).map_err(|e| anyhow::anyhow!("failed to parse report descriptor: {}", e))?;
|
||||
|
||||
- let report_len = match endp_desc_opt {
|
||||
+ let report_len = match &endp_desc_opt {
|
||||
Some((_endp_num, endp_desc)) => endp_desc.max_packet_size as usize,
|
||||
None => handler.total_byte_length as usize,
|
||||
};
|
||||
@@ -272,7 +337,9 @@ fn main() -> Result<()> {
|
||||
let report_ty = ReportTy::Input;
|
||||
let report_id = 0;
|
||||
|
||||
- let mut display = ProducerHandle::new().context("Failed to open input socket")?;
|
||||
+ let producer_name = format!("usb-{}-if{}", port, interface_num);
|
||||
+ let mut display = InputProducer::new_named_or_fallback(&producer_name)
|
||||
+ .context("Failed to open input socket")?;
|
||||
let mut endpoint_opt = match endp_desc_opt {
|
||||
Some((endp_num, _endp_desc)) => match handle.open_endpoint(endp_num as u8) {
|
||||
Ok(ok) => Some(ok),
|
||||
@@ -286,172 +353,168 @@ fn main() -> Result<()> {
|
||||
let mut left_shift = false;
|
||||
let mut right_shift = false;
|
||||
let mut last_mouse_pos = (0, 0);
|
||||
- let mut last_buttons = [false, false, false];
|
||||
+ let mut last_buttons = [false; MAX_MOUSE_BUTTONS];
|
||||
+ let mut consecutive_errors: u32 = 0;
|
||||
+
|
||||
loop {
|
||||
- //TODO: get frequency from device
|
||||
- //TODO: use sleeps when accuracy is better: thread::sleep(time::Duration::from_millis(10));
|
||||
- let timer = time::Instant::now();
|
||||
- while timer.elapsed() < time::Duration::from_millis(1) {
|
||||
- thread::yield_now();
|
||||
- }
|
||||
+ thread::sleep(time::Duration::from_millis(1));
|
||||
|
||||
- if let Some(endpoint) = &mut endpoint_opt {
|
||||
- // interrupt transfer
|
||||
- endpoint
|
||||
- .transfer_read(&mut report_buffer)
|
||||
- .context("failed to get report")?;
|
||||
+ let transfer_result: Result<(), anyhow::Error> = if let Some(endpoint) = &mut endpoint_opt {
|
||||
+ endpoint.transfer_read(&mut report_buffer)
|
||||
+ .map(|_| ())
|
||||
+ .context("interrupt transfer failed")
|
||||
} else {
|
||||
- // control transfer
|
||||
- reqs::get_report(
|
||||
- &handle,
|
||||
- report_ty,
|
||||
- report_id,
|
||||
- //TODO: should this be an index into interface_descs?
|
||||
- interface_num as u16,
|
||||
- &mut report_buffer,
|
||||
- )
|
||||
- .context("failed to get report")?;
|
||||
+ reqs::get_report(&handle, report_ty, report_id, interface_num as u16, &mut report_buffer)
|
||||
+ .map(|_| ())
|
||||
+ .context("control transfer failed")
|
||||
+ };
|
||||
+
|
||||
+ if let Err(err) = transfer_result {
|
||||
+ consecutive_errors += 1;
|
||||
+ if consecutive_errors >= MAX_RETRIES {
|
||||
+ bail!(
|
||||
+ "transfer failed {} times consecutively: {}",
|
||||
+ consecutive_errors,
|
||||
+ err
|
||||
+ );
|
||||
+ }
|
||||
+ let delay = RETRY_BASE_DELAY_MS * (1 << consecutive_errors.min(4));
|
||||
+ log::warn!(
|
||||
+ "transfer failed ({}/{}), retry in {}ms: {}",
|
||||
+ consecutive_errors,
|
||||
+ MAX_RETRIES,
|
||||
+ delay,
|
||||
+ err
|
||||
+ );
|
||||
+ thread::sleep(time::Duration::from_millis(delay));
|
||||
+ continue;
|
||||
}
|
||||
+ consecutive_errors = 0;
|
||||
|
||||
let mut mouse_pos = last_mouse_pos;
|
||||
let mut mouse_dx = 0i32;
|
||||
let mut mouse_dy = 0i32;
|
||||
let mut scroll_y = 0i32;
|
||||
let mut buttons = last_buttons;
|
||||
- for event in handler
|
||||
- .handle(&report_buffer)
|
||||
- .expect("failed to parse report")
|
||||
- {
|
||||
- log::debug!("{}", event);
|
||||
- if event.usage_page == UsagePage::GenericDesktop as u16 {
|
||||
- if event.usage == GenericDesktopUsage::X as u16 {
|
||||
- if event.relative {
|
||||
- mouse_dx += event.value as i32;
|
||||
- } else {
|
||||
- mouse_pos.0 = event.value as i32;
|
||||
- }
|
||||
- } else if event.usage == GenericDesktopUsage::Y as u16 {
|
||||
- if event.relative {
|
||||
- mouse_dy += event.value as i32;
|
||||
- } else {
|
||||
- mouse_pos.1 = event.value as i32;
|
||||
- }
|
||||
- } else if event.usage == GenericDesktopUsage::Wheel as u16 {
|
||||
- //TODO: what is X scroll?
|
||||
- if event.relative {
|
||||
- scroll_y += event.value as i32;
|
||||
- } else {
|
||||
- log::warn!("absolute mouse wheel not supported");
|
||||
+
|
||||
+ match handler.handle(&report_buffer) {
|
||||
+ Ok(events) => {
|
||||
+ for event in events {
|
||||
+ log::debug!("{}", event);
|
||||
+ if event.usage_page == UsagePage::GenericDesktop as u16 {
|
||||
+ if event.usage == GenericDesktopUsage::X as u16 {
|
||||
+ if event.relative {
|
||||
+ mouse_dx += event.value as i32;
|
||||
+ } else {
|
||||
+ mouse_pos.0 = event.value as i32;
|
||||
+ }
|
||||
+ } else if event.usage == GenericDesktopUsage::Y as u16 {
|
||||
+ if event.relative {
|
||||
+ mouse_dy += event.value as i32;
|
||||
+ } else {
|
||||
+ mouse_pos.1 = event.value as i32;
|
||||
+ }
|
||||
+ } else if event.usage == GenericDesktopUsage::Wheel as u16 {
|
||||
+ if event.relative {
|
||||
+ scroll_y += event.value as i32;
|
||||
+ } else {
|
||||
+ log::warn!("absolute mouse wheel not supported");
|
||||
+ }
|
||||
+ } else if event.usage == GenericDesktopUsage::Z as u16 {
|
||||
+ if event.relative && event.value != 0 {
|
||||
+ let scroll_event = orbclient::event::ScrollEvent {
|
||||
+ x: event.value as i32,
|
||||
+ y: 0,
|
||||
+ };
|
||||
+ if let Err(err) = display.write_event(scroll_event.to_event()) {
|
||||
+ log::warn!("failed to send hscroll: {}", err);
|
||||
+ }
|
||||
+ }
|
||||
+ } else {
|
||||
+ log::info!(
|
||||
+ "unsupported generic desktop usage 0x{:X}:0x{:X} value {}",
|
||||
+ event.usage_page,
|
||||
+ event.usage,
|
||||
+ event.value
|
||||
+ );
|
||||
+ }
|
||||
+ } else if event.usage_page == UsagePage::KeyboardOrKeypad as u16 {
|
||||
+ let pressed = event.value != 0;
|
||||
+ if event.usage == 0xE1 {
|
||||
+ left_shift = pressed;
|
||||
+ } else if event.usage == 0xE5 {
|
||||
+ right_shift = pressed;
|
||||
+ }
|
||||
+ send_key_event(&mut display, event.usage_page, event.usage, pressed);
|
||||
+ } else if event.usage_page == UsagePage::Button as u16 {
|
||||
+ let btn_idx = event.usage as usize;
|
||||
+ if btn_idx > 0 && btn_idx <= MAX_MOUSE_BUTTONS {
|
||||
+ buttons[btn_idx - 1] = event.value != 0;
|
||||
+ } else if btn_idx > MAX_MOUSE_BUTTONS {
|
||||
+ log::debug!(
|
||||
+ "ignoring button {} (max {})",
|
||||
+ btn_idx,
|
||||
+ MAX_MOUSE_BUTTONS
|
||||
+ );
|
||||
+ }
|
||||
+ } else if event.usage_page < 0xFF00 {
|
||||
+ log::info!(
|
||||
+ "unsupported usage 0x{:X}:0x{:X} value {}",
|
||||
+ event.usage_page,
|
||||
+ event.usage,
|
||||
+ event.value
|
||||
+ );
|
||||
}
|
||||
- } else {
|
||||
- log::info!(
|
||||
- "unsupported generic desktop usage 0x{:X}:0x{:X} value {}",
|
||||
- event.usage_page,
|
||||
- event.usage,
|
||||
- event.value
|
||||
- );
|
||||
}
|
||||
- } else if event.usage_page == UsagePage::KeyboardOrKeypad as u16 {
|
||||
- let (pressed, shift_opt) = if event.value != 0 {
|
||||
- (true, Some(left_shift | right_shift))
|
||||
- } else {
|
||||
- (false, None)
|
||||
- };
|
||||
- if event.usage == 0xE1 {
|
||||
- left_shift = pressed;
|
||||
- } else if event.usage == 0xE5 {
|
||||
- right_shift = pressed;
|
||||
- }
|
||||
- send_key_event(&mut display, event.usage_page, event.usage, pressed);
|
||||
- } else if event.usage_page == UsagePage::Button as u16 {
|
||||
- if event.usage > 0 && event.usage as usize <= buttons.len() {
|
||||
- buttons[event.usage as usize - 1] = event.value != 0;
|
||||
- } else {
|
||||
- log::info!(
|
||||
- "unsupported buttons usage 0x{:X}:0x{:X} value {}",
|
||||
- event.usage_page,
|
||||
- event.usage,
|
||||
- event.value
|
||||
- );
|
||||
- }
|
||||
- } else if event.usage_page >= 0xFF00 {
|
||||
- // Ignore vendor defined event
|
||||
- } else {
|
||||
- log::info!(
|
||||
- "unsupported usage 0x{:X}:0x{:X} value {}",
|
||||
- event.usage_page,
|
||||
- event.usage,
|
||||
- event.value
|
||||
- );
|
||||
+ }
|
||||
+ Err(err) => {
|
||||
+ log::warn!("failed to parse HID report: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if mouse_pos != last_mouse_pos {
|
||||
last_mouse_pos = mouse_pos;
|
||||
-
|
||||
- // TODO
|
||||
// ps2d uses 0..=65535 as range, while usb uses 0..=32767. orbital
|
||||
// expects the former range, so multiply by two here to temporarily
|
||||
- // align with orbital expectation. This workaround will make cursor
|
||||
- // looks out of sync in QEMU using virtio-vga with usb-tablet.
|
||||
+ // align with orbital expectation.
|
||||
let mouse_event = orbclient::event::MouseEvent {
|
||||
x: mouse_pos.0 * 2,
|
||||
y: mouse_pos.1 * 2,
|
||||
};
|
||||
-
|
||||
- match display.write_event(mouse_event.to_event()) {
|
||||
- Ok(_) => (),
|
||||
- Err(err) => {
|
||||
- log::warn!("failed to send mouse event to orbital: {}", err);
|
||||
- }
|
||||
+ if let Err(err) = display.write_event(mouse_event.to_event()) {
|
||||
+ log::warn!("failed to send mouse event: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if mouse_dx != 0 || mouse_dy != 0 {
|
||||
- // TODO: This is a filter to prevent random mouse jumps
|
||||
if mouse_dx > -127 && mouse_dx < 127 {
|
||||
let mouse_event = orbclient::event::MouseRelativeEvent {
|
||||
dx: mouse_dx,
|
||||
dy: mouse_dy,
|
||||
};
|
||||
-
|
||||
- match display.write_event(mouse_event.to_event()) {
|
||||
- Ok(_) => (),
|
||||
- Err(err) => {
|
||||
- log::warn!("failed to send mouse event to orbital: {}", err);
|
||||
- }
|
||||
+ if let Err(err) = display.write_event(mouse_event.to_event()) {
|
||||
+ log::warn!("failed to send relative mouse event: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if scroll_y != 0 {
|
||||
let scroll_event = orbclient::event::ScrollEvent { x: 0, y: scroll_y };
|
||||
-
|
||||
- match display.write_event(scroll_event.to_event()) {
|
||||
- Ok(_) => (),
|
||||
- Err(err) => {
|
||||
- log::warn!("failed to send scroll event to orbital: {}", err);
|
||||
- }
|
||||
+ if let Err(err) = display.write_event(scroll_event.to_event()) {
|
||||
+ log::warn!("failed to send scroll event: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if buttons != last_buttons {
|
||||
last_buttons = buttons;
|
||||
-
|
||||
let button_event = orbclient::event::ButtonEvent {
|
||||
left: buttons[0],
|
||||
right: buttons[1],
|
||||
middle: buttons[2],
|
||||
};
|
||||
-
|
||||
- match display.write_event(button_event.to_event()) {
|
||||
- Ok(_) => (),
|
||||
- Err(err) => {
|
||||
- log::warn!("failed to send button event to orbital: {}", err);
|
||||
- }
|
||||
+ if let Err(err) = display.write_event(button_event.to_event()) {
|
||||
+ log::warn!("failed to send button event: {}", err);
|
||||
}
|
||||
}
|
||||
-
|
||||
- // log::trace!("took {}ms", timer.elapsed().as_millis())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Absorbed Patches
|
||||
|
||||
These patches have been **consolidated into `local/patches/base/redox.patch`** (the
|
||||
mega-patch applied automatically to the base recipe source tree).
|
||||
|
||||
**Do not wire these patches into `recipes/core/base/recipe.toml`.** They are kept here
|
||||
for reference and git history only. If a patch referenced by a recipe.toml symlink
|
||||
breaks, that symlink should be updated to point to a current, active patch in
|
||||
`local/patches/base/` (NOT one in this directory).
|
||||
|
||||
## Consolidation timeline
|
||||
|
||||
| Date | Action |
|
||||
|------|--------|
|
||||
| 2026-04-30 | P0 + P2 patches consolidated into redox.patch |
|
||||
| 2026-04-30 | P1-P2 driver/ACPI patches consolidated |
|
||||
| 2026-04-30 | P3 ACPI/PCI patches absorbed |
|
||||
|
||||
## Active patches (NOT in this directory)
|
||||
|
||||
The active patches applied on top of `redox.patch` live in `local/patches/base/`:
|
||||
|
||||
- `P3-ps2d-led-feedback.patch` — PS/2 LED state + InputProducer migration
|
||||
- `P3-inputd-keymap-bridge.patch` — InputProducer enum + keymap bridge
|
||||
- `P3-usbhidd-hardening.patch` — USB HID descriptor validation, retry, lookup table
|
||||
- `P3-init-colored-output.patch` — ANSI-colored init daemon output
|
||||
- `P9-fix-so-pecred.patch` — shared-object credential fix
|
||||
- `redox.patch` — cumulative mega-patch (applied first, automatically)
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod keymap;
|
||||
pub mod scheme;
|
||||
pub mod xkb;
|
||||
@@ -0,0 +1,59 @@
|
||||
mod keymap;
|
||||
mod scheme;
|
||||
mod xkb;
|
||||
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::process;
|
||||
|
||||
use scheme::KeymapScheme;
|
||||
|
||||
fn log_msg(level: &str, msg: &str) {
|
||||
let _ = writeln!(std::io::stderr(), "[keymapd] {} {}", level, msg);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut scheme = KeymapScheme::new();
|
||||
|
||||
let builtins = keymap::BuiltinKeymaps::new();
|
||||
scheme.load_builtin(&builtins);
|
||||
|
||||
let keymap_dir = match env::var("KEYMAP_DIR") {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => "/etc/keymaps".to_string(),
|
||||
};
|
||||
if let Err(e) = scheme.load_from_dir(&keymap_dir) {
|
||||
log_msg("ERROR", &format!("failed to load keymaps from {}: {}", keymap_dir, e));
|
||||
}
|
||||
|
||||
log_msg("INFO", &format!("loaded {} keymap(s)", scheme.keymap_count()));
|
||||
|
||||
let socket = redox_scheme::Socket::nonblock("keymap")
|
||||
.expect("keymapd: failed to register scheme:keymap");
|
||||
log_msg("INFO", "registered scheme:keymap");
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(redox_scheme::SignalBehavior::Restart) {
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => {
|
||||
log_msg("INFO", "scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log_msg("ERROR", &format!("failed to read request: {}", e));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match request.handle_scheme_block_mut(&mut scheme) {
|
||||
Ok(response) => {
|
||||
if let Err(e) = socket.write_response(response, redox_scheme::SignalBehavior::Restart) {
|
||||
log_msg("ERROR", &format!("failed to write response: {}", e));
|
||||
}
|
||||
}
|
||||
Err(_request) => {
|
||||
log_msg("ERROR", "unhandled scheme request");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
use syscall::data::Stat;
|
||||
use syscall::error::{Error, Result, EBADF, EINVAL, ENOENT};
|
||||
use syscall::flag::{MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
|
||||
|
||||
use crate::keymap::Keymap;
|
||||
|
||||
enum HandleKind {
|
||||
Root,
|
||||
Active,
|
||||
List,
|
||||
Keymap { name: String },
|
||||
}
|
||||
|
||||
struct Handle {
|
||||
kind: HandleKind,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
pub struct KeymapScheme {
|
||||
next_id: usize,
|
||||
handles: HashMap<usize, Handle>,
|
||||
keymaps: HashMap<String, Keymap>,
|
||||
active_keymap: String,
|
||||
}
|
||||
|
||||
impl KeymapScheme {
|
||||
pub fn new() -> Self {
|
||||
KeymapScheme {
|
||||
next_id: 0,
|
||||
handles: HashMap::new(),
|
||||
keymaps: HashMap::new(),
|
||||
active_keymap: "us".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_builtin(&mut self, builtins: &crate::keymap::BuiltinKeymaps) {
|
||||
for (name, km) in [
|
||||
("us", &builtins.us),
|
||||
("gb", &builtins.gb),
|
||||
("dvorak", &builtins.dvorak),
|
||||
("azerty", &builtins.azerty),
|
||||
("bepo", &builtins.bepo),
|
||||
("it", &builtins.it),
|
||||
] {
|
||||
self.keymaps.insert(name.to_string(), km.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_dir(&mut self, dir: &str) -> io::Result<()> {
|
||||
let entries = std::fs::read_dir(dir)?;
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().map_or(false, |e| e == "json") {
|
||||
let name = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let json_str = std::fs::read_to_string(&path)?;
|
||||
if let Ok(km) = Keymap::from_json(&name, &json_str) {
|
||||
self.keymaps.insert(name, km);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_xkb(&mut self, xkb_dir: &str, layout: &str, variant: Option<&str>) -> io::Result<()> {
|
||||
let km = crate::xkb::load_xkb_keymap(xkb_dir, layout, variant)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
let name = match variant {
|
||||
Some(v) => format!("{}({})", layout, v),
|
||||
None => layout.to_string(),
|
||||
};
|
||||
self.keymaps.insert(name, km);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn keymap_count(&self) -> usize {
|
||||
self.keymaps.len()
|
||||
}
|
||||
|
||||
fn active_keymap(&self) -> &Keymap {
|
||||
self.keymaps
|
||||
.get(&self.active_keymap)
|
||||
.or_else(|| self.keymaps.get("us"))
|
||||
.expect("at least one keymap must be loaded")
|
||||
}
|
||||
|
||||
pub fn translate(&self, scancode: u8, shift: bool, altgr: bool) -> char {
|
||||
self.active_keymap().get_char(scancode, shift, altgr)
|
||||
}
|
||||
}
|
||||
|
||||
impl redox_scheme::SchemeBlockMut for KeymapScheme {
|
||||
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
|
||||
let cleaned = path.trim_matches('/');
|
||||
|
||||
let kind = if cleaned.is_empty() {
|
||||
HandleKind::Root
|
||||
} else if cleaned == "active" {
|
||||
HandleKind::Active
|
||||
} else if cleaned == "list" {
|
||||
HandleKind::List
|
||||
} else if let Some(name) = cleaned.strip_prefix("keymap/") {
|
||||
let name = name.trim_end_matches('/').to_string();
|
||||
if !self.keymaps.contains_key(&name) {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
HandleKind::Keymap { name }
|
||||
} else if self.keymaps.contains_key(cleaned) {
|
||||
let name = cleaned.to_string();
|
||||
if let Some(km) = self.keymaps.get(&name) {
|
||||
let _ = km;
|
||||
}
|
||||
HandleKind::Keymap { name }
|
||||
} else if cleaned.starts_with("set/") {
|
||||
let requested = &cleaned[4..];
|
||||
if !self.keymaps.contains_key(requested) {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
self.active_keymap = requested.to_string();
|
||||
HandleKind::Active
|
||||
} else {
|
||||
return Err(Error::new(ENOENT));
|
||||
};
|
||||
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.handles.insert(id, Handle { kind, offset: 0 });
|
||||
Ok(Some(id))
|
||||
}
|
||||
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
|
||||
let content: Vec<u8> = match &handle.kind {
|
||||
HandleKind::Root => {
|
||||
let mut listing = String::new();
|
||||
listing.push_str("active\nlist\n");
|
||||
for name in self.keymaps.keys() {
|
||||
listing.push_str(&format!("keymap/{}\n", name));
|
||||
}
|
||||
listing.into_bytes()
|
||||
}
|
||||
HandleKind::Active => self.active_keymap.clone().into_bytes(),
|
||||
HandleKind::List => {
|
||||
let mut listing = String::new();
|
||||
for (i, name) in self.keymaps.keys().enumerate() {
|
||||
if i > 0 {
|
||||
listing.push('\n');
|
||||
}
|
||||
listing.push_str(name);
|
||||
}
|
||||
listing.push('\n');
|
||||
listing.into_bytes()
|
||||
}
|
||||
HandleKind::Keymap { name } => {
|
||||
let km = self.keymaps.get(name).ok_or(Error::new(ENOENT))?;
|
||||
format!(
|
||||
"name={}\nentries={}\ncompose={}\ndead_keys={}\n",
|
||||
km.name,
|
||||
km.entries.len(),
|
||||
km.compose.len(),
|
||||
km.dead_keys.len()
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
};
|
||||
|
||||
if handle.offset >= content.len() {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
let remaining = &content[handle.offset..];
|
||||
let to_copy = remaining.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
|
||||
handle.offset += to_copy;
|
||||
Ok(Some(to_copy))
|
||||
}
|
||||
|
||||
fn write(&mut self, id: usize, buf: &[u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
match &handle.kind {
|
||||
HandleKind::Active => {
|
||||
let name = String::from_utf8_lossy(buf);
|
||||
let name = name.trim();
|
||||
if self.keymaps.contains_key(name) {
|
||||
self.active_keymap = name.to_string();
|
||||
Ok(Some(buf.len()))
|
||||
} else {
|
||||
Err(Error::new(ENOENT))
|
||||
}
|
||||
}
|
||||
_ => Err(Error::new(EINVAL)),
|
||||
}
|
||||
}
|
||||
|
||||
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let new_offset = match whence {
|
||||
SEEK_SET => pos as isize,
|
||||
SEEK_CUR => handle.offset as isize + pos,
|
||||
SEEK_END => pos,
|
||||
_ => return Err(Error::new(EINVAL)),
|
||||
};
|
||||
if new_offset < 0 {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
handle.offset = new_offset as usize;
|
||||
Ok(Some(new_offset))
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
match &handle.kind {
|
||||
HandleKind::Root => {
|
||||
stat.st_mode = MODE_DIR | 0o555;
|
||||
}
|
||||
_ => {
|
||||
stat.st_mode = MODE_FILE | 0o644;
|
||||
}
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
self.handles.remove(&id);
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
let path = match &handle.kind {
|
||||
HandleKind::Root => "keymap:".to_string(),
|
||||
HandleKind::Active => "keymap:active".to_string(),
|
||||
HandleKind::List => "keymap:list".to_string(),
|
||||
HandleKind::Keymap { name } => format!("keymap:keymap/{}", name),
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let to_copy = bytes.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&bytes[..to_copy]);
|
||||
Ok(Some(to_copy))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::keymap::{Keymap, KeymapEntry};
|
||||
|
||||
fn xkb_keycode_to_scancode(code: &str) -> Option<u8> {
|
||||
match code {
|
||||
"TLDE" => Some(0x29),
|
||||
"AE01" => Some(0x02),
|
||||
"AE02" => Some(0x03),
|
||||
"AE03" => Some(0x04),
|
||||
"AE04" => Some(0x05),
|
||||
"AE05" => Some(0x06),
|
||||
"AE06" => Some(0x07),
|
||||
"AE07" => Some(0x08),
|
||||
"AE08" => Some(0x09),
|
||||
"AE09" => Some(0x0A),
|
||||
"AE10" => Some(0x0B),
|
||||
"AE11" => Some(0x0C),
|
||||
"AE12" => Some(0x0D),
|
||||
"AD01" => Some(0x10),
|
||||
"AD02" => Some(0x11),
|
||||
"AD03" => Some(0x12),
|
||||
"AD04" => Some(0x13),
|
||||
"AD05" => Some(0x14),
|
||||
"AD06" => Some(0x15),
|
||||
"AD07" => Some(0x16),
|
||||
"AD08" => Some(0x17),
|
||||
"AD09" => Some(0x18),
|
||||
"AD10" => Some(0x19),
|
||||
"AD11" => Some(0x1A),
|
||||
"AD12" => Some(0x1B),
|
||||
"AC01" => Some(0x1E),
|
||||
"AC02" => Some(0x1F),
|
||||
"AC03" => Some(0x20),
|
||||
"AC04" => Some(0x21),
|
||||
"AC05" => Some(0x22),
|
||||
"AC06" => Some(0x23),
|
||||
"AC07" => Some(0x24),
|
||||
"AC08" => Some(0x25),
|
||||
"AC09" => Some(0x26),
|
||||
"AC10" => Some(0x27),
|
||||
"AC11" => Some(0x28),
|
||||
"BKSL" => Some(0x2B),
|
||||
"AB01" => Some(0x2C),
|
||||
"AB02" => Some(0x2D),
|
||||
"AB03" => Some(0x2E),
|
||||
"AB04" => Some(0x2F),
|
||||
"AB05" => Some(0x30),
|
||||
"AB06" => Some(0x31),
|
||||
"AB07" => Some(0x32),
|
||||
"AB08" => Some(0x33),
|
||||
"AB09" => Some(0x34),
|
||||
"AB10" => Some(0x35),
|
||||
"SPCE" => Some(0x39),
|
||||
"LSGT" => Some(0x56),
|
||||
"BKSP" => Some(0x0E),
|
||||
"TAB" => Some(0x0F),
|
||||
"RTRN" => Some(0x1C),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn keysym_to_char(sym: &str) -> char {
|
||||
if sym.len() == 1 {
|
||||
return sym.chars().next().unwrap_or('\0');
|
||||
}
|
||||
match sym {
|
||||
"space" => ' ',
|
||||
"exclam" => '!',
|
||||
"quotedbl" => '"',
|
||||
"numbersign" => '#',
|
||||
"dollar" => '$',
|
||||
"percent" => '%',
|
||||
"ampersand" => '&',
|
||||
"apostrophe" => '\'',
|
||||
"quoteright" => '\'',
|
||||
"parenleft" => '(',
|
||||
"parenright" => ')',
|
||||
"asterisk" => '*',
|
||||
"plus" => '+',
|
||||
"comma" => ',',
|
||||
"minus" => '-',
|
||||
"period" => '.',
|
||||
"slash" => '/',
|
||||
"colon" => ':',
|
||||
"semicolon" => ';',
|
||||
"less" => '<',
|
||||
"equal" => '=',
|
||||
"greater" => '>',
|
||||
"question" => '?',
|
||||
"at" => '@',
|
||||
"bracketleft" => '[',
|
||||
"backslash" => '\\',
|
||||
"bracketright" => ']',
|
||||
"asciicircum" => '^',
|
||||
"underscore" => '_',
|
||||
"grave" => '`',
|
||||
"braceleft" => '{',
|
||||
"bar" => '|',
|
||||
"braceright" => '}',
|
||||
"asciitilde" => '~',
|
||||
"nobreakspace" => '\u{00A0}',
|
||||
"exclamdown" => '¡',
|
||||
"cent" => '¢',
|
||||
"sterling" => '£',
|
||||
"currency" => '¤',
|
||||
"yen" => '¥',
|
||||
"brokenbar" => '¦',
|
||||
"section" => '§',
|
||||
"diaeresis" => '¨',
|
||||
"copyright" => '©',
|
||||
"ordfeminine" => 'ª',
|
||||
"guillemotleft" => '«',
|
||||
"notsign" => '¬',
|
||||
"hyphen" => '\u{00AD}',
|
||||
"registered" => '®',
|
||||
"macron" => '¯',
|
||||
"degree" => '°',
|
||||
"plusminus" => '±',
|
||||
"twosuperior" => '²',
|
||||
"threesuperior" => '³',
|
||||
"acute" => '´',
|
||||
"mu" => 'µ',
|
||||
"paragraph" => '¶',
|
||||
"periodcentered" => '·',
|
||||
"cedilla" => '¸',
|
||||
"onesuperior" => '¹',
|
||||
"masculine" => 'º',
|
||||
"guillemotright" => '»',
|
||||
"onequarter" => '¼',
|
||||
"onehalf" => '½',
|
||||
"threequarters" => '¾',
|
||||
"questiondown" => '¿',
|
||||
"Agrave" => 'À',
|
||||
"Aacute" => 'Á',
|
||||
"Acircumflex" => 'Â',
|
||||
"Atilde" => 'Ã',
|
||||
"Adiaeresis" => 'Ä',
|
||||
"Aring" => 'Å',
|
||||
"AE" => 'Æ',
|
||||
"Ccedilla" => 'Ç',
|
||||
"Egrave" => 'È',
|
||||
"Eacute" => 'É',
|
||||
"Ecircumflex" => 'Ê',
|
||||
"Ediaeresis" => 'Ë',
|
||||
"Igrave" => 'Ì',
|
||||
"Iacute" => 'Í',
|
||||
"Icircumflex" => 'Î',
|
||||
"Idiaeresis" => 'Ï',
|
||||
"ETH" => 'Ð',
|
||||
"Ntilde" => 'Ñ',
|
||||
"Ograve" => 'Ò',
|
||||
"Oacute" => 'Ó',
|
||||
"Ocircumflex" => 'Ô',
|
||||
"Otilde" => 'Õ',
|
||||
"Odiaeresis" => 'Ö',
|
||||
"multiply" => '×',
|
||||
"Ooblique" => 'Ø',
|
||||
"Ugrave" => 'Ù',
|
||||
"Uacute" => 'Ú',
|
||||
"Ucircumflex" => 'Û',
|
||||
"Udiaeresis" => 'Ü',
|
||||
"Yacute" => 'Ý',
|
||||
"THORN" => 'Þ',
|
||||
"ssharp" => 'ß',
|
||||
"agrave" => 'à',
|
||||
"aacute" => 'á',
|
||||
"acircumflex" => 'â',
|
||||
"atilde" => 'ã',
|
||||
"adiaeresis" => 'ä',
|
||||
"aring" => 'å',
|
||||
"ae" => 'æ',
|
||||
"ccedilla" => 'ç',
|
||||
"egrave" => 'è',
|
||||
"eacute" => 'é',
|
||||
"ecircumflex" => 'ê',
|
||||
"ediaeresis" => 'ë',
|
||||
"igrave" => 'ì',
|
||||
"iacute" => 'í',
|
||||
"icircumflex" => 'î',
|
||||
"idiaeresis" => 'ï',
|
||||
"eth" => 'ð',
|
||||
"ntilde" => 'ñ',
|
||||
"ograve" => 'ò',
|
||||
"oacute" => 'ó',
|
||||
"ocircumflex" => 'ô',
|
||||
"otilde" => 'õ',
|
||||
"odiaeresis" => 'ö',
|
||||
"division" => '÷',
|
||||
"oslash" => 'ø',
|
||||
"ugrave" => 'ù',
|
||||
"uacute" => 'ú',
|
||||
"ucircumflex" => 'û',
|
||||
"udiaeresis" => 'ü',
|
||||
"yacute" => 'ý',
|
||||
"thorn" => 'þ',
|
||||
"ydiaeresis" => 'ÿ',
|
||||
"EuroSign" => '€',
|
||||
"NoSymbol" => '\0',
|
||||
_ => '\0',
|
||||
}
|
||||
}
|
||||
|
||||
struct XkbKeyEntry {
|
||||
scancode: u8,
|
||||
normal: char,
|
||||
shifted: char,
|
||||
altgr: char,
|
||||
altgr_shifted: char,
|
||||
}
|
||||
|
||||
fn parse_keysyms(syms: &[&str]) -> (char, char, char, char) {
|
||||
let normal = syms.get(0).map(|s| keysym_to_char(s.trim())).unwrap_or('\0');
|
||||
let shifted = syms.get(1).map(|s| keysym_to_char(s.trim())).unwrap_or('\0');
|
||||
let altgr = syms.get(2).map(|s| keysym_to_char(s.trim())).unwrap_or('\0');
|
||||
let altgr_shifted = syms.get(3).map(|s| keysym_to_char(s.trim())).unwrap_or('\0');
|
||||
(normal, shifted, altgr, altgr_shifted)
|
||||
}
|
||||
|
||||
pub fn parse_xkb_symbols(content: &str, variant: Option<&str>) -> Result<Keymap, String> {
|
||||
let target_variant = variant.unwrap_or("basic");
|
||||
let mut entries: HashMap<u8, KeymapEntry> = HashMap::new();
|
||||
let mut found_variant = false;
|
||||
|
||||
let mut i = 0;
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
while i < lines.len() {
|
||||
let line = lines[i].trim();
|
||||
|
||||
if line.starts_with("xkb_symbols") {
|
||||
let name = extract_variant_name(line).unwrap_or("basic");
|
||||
if name != target_variant {
|
||||
i = skip_brace_block(&lines, i + 1);
|
||||
continue;
|
||||
}
|
||||
found_variant = true;
|
||||
i += 1;
|
||||
while i < lines.len() {
|
||||
let inner = lines[i].trim();
|
||||
if inner.starts_with('}') {
|
||||
break;
|
||||
}
|
||||
if inner.starts_with("key <") {
|
||||
if let Some(entry) = parse_key_line(inner) {
|
||||
entries.entry(entry.scancode).or_insert_with(|| {
|
||||
KeymapEntry {
|
||||
scancode: entry.scancode,
|
||||
normal: entry.normal,
|
||||
shifted: entry.shifted,
|
||||
altgr: entry.altgr,
|
||||
altgr_shifted: entry.altgr_shifted,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if !found_variant {
|
||||
return Err(format!("variant '{}' not found in XKB symbols file", target_variant));
|
||||
}
|
||||
|
||||
Ok(Keymap {
|
||||
name: variant.unwrap_or("basic").to_string(),
|
||||
entries,
|
||||
compose: Vec::new(),
|
||||
dead_keys: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_variant_name(line: &str) -> Option<&str> {
|
||||
let start = line.find('"')?;
|
||||
let rest = &line[start + 1..];
|
||||
let end = rest.find('"')?;
|
||||
Some(&rest[..end])
|
||||
}
|
||||
|
||||
fn skip_brace_block(lines: &[&str], start: usize) -> usize {
|
||||
let mut depth = 1;
|
||||
let mut i = start;
|
||||
while i < lines.len() && depth > 0 {
|
||||
for ch in lines[i].chars() {
|
||||
match ch {
|
||||
'{' => depth += 1,
|
||||
'}' => depth -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn parse_key_line(line: &str) -> Option<XkbKeyEntry> {
|
||||
let key_start = line.find('<')?;
|
||||
let key_end = line[key_start + 1..].find('>')?;
|
||||
let keycode = &line[key_start + 1..key_start + 1 + key_end];
|
||||
let scancode = xkb_keycode_to_scancode(keycode)?;
|
||||
|
||||
let syms_start = line.find('[')?;
|
||||
let syms_end = line.rfind(']')?;
|
||||
let syms_content = &line[syms_start + 1..syms_end];
|
||||
let syms: Vec<&str> = syms_content.split(',').collect();
|
||||
|
||||
let (normal, shifted, altgr, altgr_shifted) = parse_keysyms(&syms);
|
||||
|
||||
Some(XkbKeyEntry {
|
||||
scancode,
|
||||
normal,
|
||||
shifted,
|
||||
altgr,
|
||||
altgr_shifted,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_xkb_keymap(xkb_dir: &str, layout: &str, variant: Option<&str>) -> Result<Keymap, String> {
|
||||
let file_path = format!("{}/symbols/{}", xkb_dir, layout);
|
||||
let content = std::fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("failed to read XKB symbols file '{}': {}", file_path, e))?;
|
||||
parse_xkb_symbols(&content, variant)
|
||||
}
|
||||
@@ -4,17 +4,13 @@ rev = "463f76b9608a896e6f6c9f63457f57f6409873c7"
|
||||
patches = [
|
||||
"P0-daemon-fix-init-notify-unwrap.patch",
|
||||
"P0-workspace-add-bootstrap.patch",
|
||||
"P0-bootstrap-workspace-fix.patch",
|
||||
"P2-daemon-ready-graceful.patch",
|
||||
"P0-init-continuous-scheduling.patch",
|
||||
"P2-i2c-gpio-ucsi-drivers.patch",
|
||||
"P3-pcid-bind-scheme.patch",
|
||||
"P3-pcid-aer-scheme.patch",
|
||||
"P3-pcid-uevent-format-fix.patch",
|
||||
"P3-acpi-wave12-hardening.patch",
|
||||
"P3-acpi-power-dmi.patch",
|
||||
"P3-xhci-device-hardening.patch",
|
||||
"P5-init-supervisor-restart.patch",
|
||||
"P5-init-daemon-panic-hardening.patch",
|
||||
"P9-fix-so-pecred.patch",
|
||||
"P3-inputd-keymap-bridge.patch",
|
||||
"P3-ps2d-led-feedback.patch",
|
||||
"P3-usbhidd-hardening.patch",
|
||||
"P3-init-colored-output.patch",
|
||||
]
|
||||
|
||||
[build]
|
||||
|
||||
@@ -80,6 +80,8 @@ const REPO_HELP_STR: &str = r#"
|
||||
--category=<category> apply to all recipes in <cookbook_dir>/<category>
|
||||
--filesystem=<filesystem> override recipes config using installer file
|
||||
--repo-binary override recipes config to use repo_binary
|
||||
--allow-protected allow re-fetching of protected recipes
|
||||
(equivalent to REDBEAR_ALLOW_PROTECTED_FETCH=1)
|
||||
|
||||
cook env and their defaults:
|
||||
CI= set to any value to disable TUI
|
||||
@@ -465,6 +467,10 @@ fn parse_args(args: Vec<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
|
||||
"--repo-binary" => override_filesystem_repo_binary = true,
|
||||
"--with-package-deps" => config.with_package_deps = true,
|
||||
"--all" => config.all = true,
|
||||
"--allow-protected" => {
|
||||
// SAFETY: set once at startup, before any threading
|
||||
unsafe { env::set_var("REDBEAR_ALLOW_PROTECTED_FETCH", "1"); }
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Error: Unknown flag: {}", arg);
|
||||
process::exit(1);
|
||||
|
||||
@@ -200,6 +200,9 @@ pub fn build(
|
||||
) -> Result<BuildResult, String> {
|
||||
let recipe = &cook_recipe.recipe;
|
||||
let name = &cook_recipe.name;
|
||||
|
||||
crate::cook::fetch::cleanup_workspace_pollution(recipe_dir, logger);
|
||||
|
||||
let check_source = !cook_recipe.is_deps;
|
||||
let sysroot_dir = get_sub_target_dir(target_dir, "sysroot");
|
||||
let toolchain_dir = get_sub_target_dir(target_dir, "toolchain");
|
||||
|
||||
+458
-30
@@ -35,6 +35,20 @@ pub struct FetchResult {
|
||||
pub cached: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_workspace_pollution(recipe_dir: &Path, logger: &PtyOut) {
|
||||
let recipes_root = recipe_dir.join("../..");
|
||||
for file in &["Cargo.toml", "Cargo.lock"] {
|
||||
let path = recipes_root.join(file);
|
||||
if path.is_file() && !path.is_symlink() {
|
||||
if let Err(e) = fs::remove_file(&path) {
|
||||
log_to_pty!(logger, "[WARN] failed to remove workspace pollution {}: {e}", path.display());
|
||||
} else {
|
||||
log_to_pty!(logger, "[CLEAN] removed workspace pollution {}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn redbear_protected_recipe(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
@@ -158,6 +172,100 @@ fn redbear_allow_protected_fetch() -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn redbear_release() -> Option<String> {
|
||||
env::var("REDBEAR_RELEASE")
|
||||
.ok()
|
||||
.map(|value| value.trim().trim_start_matches('=').to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn redbear_project_root(recipe_dir: &Path) -> Option<PathBuf> {
|
||||
let absolute_recipe_dir = if recipe_dir.is_absolute() {
|
||||
recipe_dir.to_path_buf()
|
||||
} else {
|
||||
env::current_dir().ok()?.join(recipe_dir)
|
||||
};
|
||||
for ancestor in absolute_recipe_dir.ancestors() {
|
||||
if ancestor.file_name().is_some_and(|name| name == "recipes") {
|
||||
return ancestor.parent().map(Path::to_path_buf);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn redbear_recipe_restore_path(recipe_dir: &Path) -> Option<String> {
|
||||
let mut saw_recipes = false;
|
||||
let mut parts = Vec::new();
|
||||
for component in recipe_dir.components() {
|
||||
let value = component.as_os_str().to_string_lossy();
|
||||
if saw_recipes {
|
||||
parts.push(value.to_string());
|
||||
} else if value == "recipes" {
|
||||
saw_recipes = true;
|
||||
}
|
||||
}
|
||||
if saw_recipes && !parts.is_empty() {
|
||||
Some(parts.join("/"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn redbear_try_restore_source(recipe_dir: &Path, logger: &PtyOut, force: bool) -> Result<()> {
|
||||
let Some(release) = redbear_release() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(project_root) = redbear_project_root(recipe_dir) else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(recipe_path) = redbear_recipe_restore_path(recipe_dir) else {
|
||||
return Ok(());
|
||||
};
|
||||
let restore_script = project_root.join("local/scripts/restore-sources.sh");
|
||||
if !restore_script.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut command = Command::new("python3");
|
||||
command.current_dir(&project_root);
|
||||
command.arg(&restore_script);
|
||||
command.arg(format!("--release={release}"));
|
||||
if force {
|
||||
command.arg("--force");
|
||||
}
|
||||
command.arg(recipe_path);
|
||||
run_command(command, logger)
|
||||
}
|
||||
|
||||
fn redbear_source_dir_is_effectively_empty(source_dir: &Path) -> bool {
|
||||
let Ok(entries) = fs::read_dir(source_dir) else {
|
||||
return true;
|
||||
};
|
||||
let visible_entries = entries
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.file_name() != ".gitkeep")
|
||||
.count();
|
||||
visible_entries == 0
|
||||
}
|
||||
|
||||
fn redbear_ensure_offline_source(recipe_dir: &Path, source_dir: &PathBuf, logger: &PtyOut) -> Result<()> {
|
||||
if !source_dir.exists() || redbear_source_dir_is_effectively_empty(source_dir) {
|
||||
redbear_try_restore_source(recipe_dir, logger, true)?;
|
||||
}
|
||||
offline_check_exists(source_dir)
|
||||
}
|
||||
|
||||
fn redbear_ensure_offline_git_source(
|
||||
recipe_dir: &Path,
|
||||
source_dir: &PathBuf,
|
||||
logger: &PtyOut,
|
||||
) -> Result<()> {
|
||||
let git_head = source_dir.join(".git/HEAD");
|
||||
if !source_dir.exists() || !git_head.is_file() {
|
||||
redbear_try_restore_source(recipe_dir, logger, true)?;
|
||||
}
|
||||
offline_check_exists(source_dir)
|
||||
}
|
||||
|
||||
/// Check if a recipe directory is a local Red Bear overlay (symlink into local/).
|
||||
fn is_local_overlay(recipe_dir: &Path) -> bool {
|
||||
if let Ok(resolved) = recipe_dir.canonicalize() {
|
||||
@@ -197,6 +305,11 @@ pub(crate) fn get_blake3(path: &PathBuf) -> Result<String> {
|
||||
pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result<FetchResult> {
|
||||
let recipe_dir = &recipe.dir;
|
||||
let source_dir = recipe_dir.join("source");
|
||||
|
||||
// Clean up workspace pollution that may have been left by previous
|
||||
// builds (e.g. Cargo workspace files leaked into the recipes/ root).
|
||||
cleanup_workspace_pollution(recipe_dir, logger);
|
||||
|
||||
match recipe.recipe.build.kind {
|
||||
BuildKind::None => {
|
||||
// the build function doesn't need source dir exists
|
||||
@@ -210,8 +323,12 @@ pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result<FetchResult
|
||||
}
|
||||
|
||||
let result = match &recipe.recipe.source {
|
||||
Some(SourceRecipe::Path { path: _ }) | None => {
|
||||
offline_check_exists(&source_dir)?;
|
||||
Some(SourceRecipe::Path { path: _ }) => {
|
||||
redbear_ensure_offline_source(recipe_dir, &source_dir, logger)?;
|
||||
let ident = fetch_apply_source_info(recipe, "".to_string())?;
|
||||
FetchResult::cached(source_dir, ident)
|
||||
}
|
||||
None => {
|
||||
let ident = fetch_apply_source_info(recipe, "".to_string())?;
|
||||
FetchResult::cached(source_dir, ident)
|
||||
}
|
||||
@@ -227,22 +344,61 @@ pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result<FetchResult
|
||||
upstream: _,
|
||||
branch: _,
|
||||
rev,
|
||||
patches: _,
|
||||
script: _,
|
||||
patches,
|
||||
script,
|
||||
shallow_clone: _,
|
||||
}) => {
|
||||
offline_check_exists(&source_dir)?;
|
||||
redbear_ensure_offline_git_source(recipe_dir, &source_dir, logger)?;
|
||||
let git_head = source_dir.join(".git/HEAD");
|
||||
if !git_head.is_file() {
|
||||
let source_ident = rev.clone().unwrap_or_else(|| {
|
||||
format!("release-archive:{}", recipe.name.name())
|
||||
});
|
||||
FetchResult::cached(source_dir, source_ident)
|
||||
} else {
|
||||
let (head_rev, _) = get_git_head_rev(&source_dir)?;
|
||||
if let Some(expected_rev) = rev {
|
||||
if head_rev != *expected_rev {
|
||||
let head_short = &head_rev[..head_rev.len().min(7)];
|
||||
let expected_short = &expected_rev[..expected_rev.len().min(7)];
|
||||
if !head_rev.starts_with(expected_rev.as_str())
|
||||
&& head_short != expected_short
|
||||
{
|
||||
bail_other_err!(
|
||||
"source at {} has revision {} but recipe expects {}. \
|
||||
Source archives may be corrupted. Restore from release archives.",
|
||||
source_dir.display(), head_rev, expected_rev
|
||||
source_dir.display(), head_short, expected_rev
|
||||
);
|
||||
}
|
||||
}
|
||||
// Validate all patch symlinks resolve before touching source.
|
||||
fetch_validate_patch_symlinks(recipe_dir, patches)?;
|
||||
|
||||
if (!patches.is_empty() || script.is_some())
|
||||
&& fetch_patches_state_stale(recipe_dir, patches, script, &source_dir)
|
||||
{
|
||||
log_to_pty!(logger, "[INFO] patches state stale or missing — re-applying");
|
||||
// Reset source to clean state, including submodules.
|
||||
let mut clean_cmd = Command::new("git");
|
||||
clean_cmd.arg("-C").arg(&source_dir);
|
||||
clean_cmd.arg("clean").arg("-ffdx");
|
||||
let _ = run_command(clean_cmd, logger);
|
||||
let mut reset_cmd = Command::new("git");
|
||||
reset_cmd.arg("-C").arg(&source_dir);
|
||||
reset_cmd.arg("reset").arg("--hard");
|
||||
run_command(reset_cmd, logger)?;
|
||||
// Recursively reset submodules if any exist.
|
||||
if source_dir.join(".gitmodules").exists() {
|
||||
let mut sub_cmd = Command::new("git");
|
||||
sub_cmd.arg("-C").arg(&source_dir);
|
||||
sub_cmd.arg("submodule").arg("foreach");
|
||||
sub_cmd.arg("--recursive");
|
||||
sub_cmd.arg("git reset --hard && git clean -ffdx");
|
||||
run_command(sub_cmd, logger)?;
|
||||
}
|
||||
fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?;
|
||||
}
|
||||
FetchResult::cached(source_dir, head_rev)
|
||||
}
|
||||
}
|
||||
Some(SourceRecipe::Tar {
|
||||
tar: _,
|
||||
@@ -274,7 +430,7 @@ pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result<FetchResult
|
||||
}
|
||||
}
|
||||
}
|
||||
offline_check_exists(&source_dir)?;
|
||||
redbear_ensure_offline_source(recipe_dir, &source_dir, logger)?;
|
||||
FetchResult::new(source_dir, ident, cached)
|
||||
}
|
||||
};
|
||||
@@ -288,7 +444,7 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
|
||||
if redbear_protected_recipe(recipe.name.name()) && !redbear_allow_protected_fetch() {
|
||||
log_to_pty!(
|
||||
logger,
|
||||
"[INFO]: protected recipe {} uses local source (fetch disabled; set REDBEAR_ALLOW_PROTECTED_FETCH=1 to override)",
|
||||
"[INFO]: protected recipe {} uses local source (fetch disabled; use --allow-protected flag or set REDBEAR_ALLOW_PROTECTED_FETCH=1 to override)",
|
||||
recipe.name.name()
|
||||
);
|
||||
return fetch_offline(recipe, logger);
|
||||
@@ -528,6 +684,7 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
|
||||
manual_git_recursive_submodule(logger, &source_dir, cmds)?;
|
||||
}
|
||||
|
||||
fetch_validate_patch_symlinks(recipe_dir, patches)?;
|
||||
fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?;
|
||||
}
|
||||
|
||||
@@ -967,36 +1124,307 @@ pub(crate) fn fetch_apply_patches(
|
||||
source_dir_tmp: &PathBuf,
|
||||
logger: &PtyOut,
|
||||
) -> Result<()> {
|
||||
if patches.is_empty() && script.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Read and normalize all patch files.
|
||||
let mut patch_contents: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for patch_name in patches {
|
||||
let patch_file = recipe_dir.join(patch_name);
|
||||
if !patch_file.is_file() {
|
||||
bail_other_err!("Failed to find patch file {:?}", patch_file.display());
|
||||
}
|
||||
|
||||
let patch = fs::read_to_string(&patch_file).map_err(|err| {
|
||||
let raw = fs::read(&patch_file).map_err(|err| {
|
||||
format!(
|
||||
"failed to read patch file '{}': {}\n{:#?}",
|
||||
patch_file.display(),
|
||||
err,
|
||||
err
|
||||
"failed to read patch file '{}': {err}",
|
||||
patch_file.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut command = Command::new("patch");
|
||||
command.arg("--directory").arg(source_dir_tmp);
|
||||
command.arg("--strip=1");
|
||||
run_command_stdin(command, patch.as_bytes(), logger)?;
|
||||
let normalized = normalize_patch(&raw);
|
||||
patch_contents.push((patch_name.clone(), normalized));
|
||||
}
|
||||
Ok(if let Some(script) = script {
|
||||
let mut command = Command::new("bash");
|
||||
command.arg("-ex");
|
||||
command.current_dir(source_dir_tmp);
|
||||
run_command_stdin(
|
||||
command,
|
||||
format!("{SHARED_PRESCRIPT}\n{script}").as_bytes(),
|
||||
logger,
|
||||
)?;
|
||||
})
|
||||
|
||||
// Apply all patches atomically to a staging directory.
|
||||
// If any patch fails, the staging directory is discarded and the
|
||||
// original source tree is left untouched.
|
||||
// Uses cp -al (hard links) for zero-copy staging.
|
||||
let staging_dir = source_dir_tmp.with_extension("staging");
|
||||
let _ = fs::remove_dir_all(&staging_dir);
|
||||
Command::new("cp")
|
||||
.arg("-al")
|
||||
.arg(source_dir_tmp)
|
||||
.arg(&staging_dir)
|
||||
.status()
|
||||
.map_err(|e| format!("failed to create staging copy via cp -al: {e}"))?;
|
||||
|
||||
let result = (|| -> Result<Vec<String>> {
|
||||
let mut applied = Vec::new();
|
||||
for (patch_name, patch_data) in &patch_contents {
|
||||
let mut command = Command::new("patch");
|
||||
command.arg("--directory").arg(&staging_dir);
|
||||
command.arg("--strip=1");
|
||||
command.arg("--batch");
|
||||
command.arg("--fuzz=0");
|
||||
run_command_stdin(command, patch_data.as_slice(), logger)
|
||||
.map_err(|e| format!("patch {patch_name} FAILED: {e}"))?;
|
||||
|
||||
for ext in &["rej", "orig"] {
|
||||
let rej_check = Command::new("find")
|
||||
.arg(&staging_dir)
|
||||
.arg("-name")
|
||||
.arg(format!("*.{ext}"))
|
||||
.arg("-print")
|
||||
.arg("-quit")
|
||||
.output();
|
||||
if let Ok(out) = rej_check {
|
||||
if !out.stdout.is_empty() {
|
||||
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
bail_other_err!(
|
||||
"patch {patch_name} left .{ext} file (hunks failed to apply): {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
applied.push(patch_name.clone());
|
||||
}
|
||||
Ok(applied)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(applied) => {
|
||||
let backup_dir = source_dir_tmp.with_extension("backup");
|
||||
let _ = fs::remove_dir_all(&backup_dir);
|
||||
fs::rename(source_dir_tmp, &backup_dir)
|
||||
.map_err(|e| format!("failed to rename source to backup: {e}"))?;
|
||||
fs::rename(&staging_dir, source_dir_tmp)
|
||||
.map_err(|e| format!("failed to promote staging to source: {e}"))?;
|
||||
let _ = fs::remove_dir_all(&backup_dir);
|
||||
|
||||
fetch_write_patches_state(recipe_dir, &applied, source_dir_tmp, script, logger)?;
|
||||
|
||||
if let Some(script) = script {
|
||||
let mut command = Command::new("bash");
|
||||
command.arg("-ex");
|
||||
command.current_dir(source_dir_tmp);
|
||||
run_command_stdin(
|
||||
command,
|
||||
format!("{SHARED_PRESCRIPT}\n{script}").as_bytes(),
|
||||
logger,
|
||||
)?;
|
||||
}
|
||||
log_to_pty!(logger, "[ATOMIC] {n}/{n} patches applied", n = applied.len());
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = fs::remove_dir_all(&staging_dir);
|
||||
log_to_pty!(logger, "[ATOMIC] patch application rolled back — source tree unchanged");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizes a patch for compatibility with the `patch` command by stripping
|
||||
/// git-specific headers (`diff --git`, `index`, `new file mode`, etc.) that
|
||||
/// `patch` does not recognize.
|
||||
fn normalize_patch(raw: &[u8]) -> Vec<u8> {
|
||||
let text = String::from_utf8_lossy(raw);
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut prev_empty = true;
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("diff --git ")
|
||||
|| trimmed.starts_with("index ")
|
||||
|| trimmed.starts_with("new file mode ")
|
||||
|| trimmed.starts_with("deleted file mode ")
|
||||
|| trimmed.starts_with("rename from ")
|
||||
|| trimmed.starts_with("rename to ")
|
||||
|| trimmed.starts_with("similarity index ")
|
||||
|| trimmed.starts_with("dissimilarity index ")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !prev_empty || !line.is_empty() {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
prev_empty = line.is_empty();
|
||||
}
|
||||
}
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
out.into_bytes()
|
||||
}
|
||||
|
||||
/// Computes a BLAKE3 hash over all patch file contents (in order).
|
||||
fn fetch_compute_patches_hash(
|
||||
recipe_dir: &Path,
|
||||
patches: &[String],
|
||||
) -> Result<String> {
|
||||
// BLAKE3 is already a project dependency (used for source verification).
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
for patch_name in patches {
|
||||
let patch_file = recipe_dir.join(patch_name);
|
||||
let content = fs::read(&patch_file).map_err(|err| {
|
||||
format!("failed to read patch for hashing '{}': {err}", patch_file.display())
|
||||
})?;
|
||||
hasher.update(&content);
|
||||
}
|
||||
Ok(hasher.finalize().to_hex().to_string())
|
||||
}
|
||||
|
||||
/// Writes a .patches-state file into the recipe's *target* directory
|
||||
/// (NOT the source checkout — git clean would delete it otherwise).
|
||||
/// Contains: upstream commit, ordered patch list, composite hash, script hash,
|
||||
/// and state schema version for forward-compatibility.
|
||||
/// Computes a BLAKE3 hash over all tracked files in the source directory,
|
||||
/// so that manual source edits (outside the patch system) are detected
|
||||
/// and trigger re-patching on the next build.
|
||||
fn fetch_compute_source_hash(source_dir: &Path) -> String {
|
||||
let output = Command::new("git")
|
||||
.arg("-C").arg(source_dir)
|
||||
.args(["ls-files", "-z"])
|
||||
.output();
|
||||
match output {
|
||||
Ok(out) if !out.stdout.is_empty() => {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
// Hash file paths in sorted order for stability.
|
||||
let mut files: Vec<&str> = out.stdout
|
||||
.split(|&b| b == 0)
|
||||
.filter_map(|s| std::str::from_utf8(s).ok())
|
||||
.collect();
|
||||
files.sort();
|
||||
for path in &files {
|
||||
hasher.update(path.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
// Hash file contents for integrity.
|
||||
if let Ok(content) = fs::read(source_dir.join(path)) {
|
||||
hasher.update(&content);
|
||||
}
|
||||
hasher.update(b"\0");
|
||||
}
|
||||
hasher.finalize().to_hex().to_string()
|
||||
}
|
||||
_ => "no-git".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_write_patches_state(
|
||||
recipe_dir: &Path,
|
||||
applied: &[String],
|
||||
source_dir: &Path,
|
||||
script: &Option<String>,
|
||||
logger: &PtyOut,
|
||||
) -> Result<()> {
|
||||
let head_rev = get_git_head_rev(&source_dir.to_path_buf())
|
||||
.map(|(r, _)| r)
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
let hash = fetch_compute_patches_hash(recipe_dir, applied)
|
||||
.unwrap_or_else(|_| "hash-error".to_string());
|
||||
let script_hash = script.as_ref().map(|s| {
|
||||
blake3::hash(s.as_bytes()).to_hex().to_string()
|
||||
}).unwrap_or_else(|| "none".to_string());
|
||||
|
||||
// State goes in target/ so git clean/reset won't delete it.
|
||||
let state_dir = recipe_dir.join("target");
|
||||
let _ = fs::create_dir_all(&state_dir);
|
||||
let state_file = state_dir.join(".patches-state");
|
||||
|
||||
let source_hash = fetch_compute_source_hash(source_dir);
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str("schema: 1\n");
|
||||
content.push_str(&format!("upstream-rev: {head_rev}\n"));
|
||||
content.push_str(&format!("patches-hash: {hash}\n"));
|
||||
content.push_str(&format!("script-hash: {script_hash}\n"));
|
||||
content.push_str(&format!("source-hash: {source_hash}\n"));
|
||||
for (i, name) in applied.iter().enumerate() {
|
||||
content.push_str(&format!("patch[{}]: {name}\n", i + 1));
|
||||
}
|
||||
fs::write(&state_file, &content).map_err(|err| {
|
||||
format!("failed to write .patches-state: {err}")
|
||||
})?;
|
||||
log_to_pty!(logger, "[OK] wrote .patches-state ({}/{} patches)", applied.len(), applied.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates that every patch file path resolves to a real file before we
|
||||
/// touch the source tree. Fails early with a clear message if any symlink
|
||||
/// is broken or file is missing.
|
||||
fn fetch_validate_patch_symlinks(
|
||||
recipe_dir: &Path,
|
||||
patches: &[String],
|
||||
) -> Result<()> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for patch_name in patches {
|
||||
let patch_file = recipe_dir.join(patch_name);
|
||||
if !patch_file.is_file() {
|
||||
bail_other_err!(
|
||||
"patch file not found: {:?} (broken symlink or missing file in {})",
|
||||
patch_file.display(),
|
||||
recipe_dir.display()
|
||||
);
|
||||
}
|
||||
// Canonicalize to catch symlink chains
|
||||
let canonical = patch_file.canonicalize().map_err(|e| {
|
||||
format!(
|
||||
"cannot resolve patch path {:?}: {e} (broken symlink?)",
|
||||
patch_file.display()
|
||||
)
|
||||
})?;
|
||||
if !seen.insert(canonical) {
|
||||
bail_other_err!(
|
||||
"duplicate patch after canonicalization: {:?} (listed twice in recipe?)",
|
||||
patch_name
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks whether the source directory's .patches-state matches the
|
||||
/// recipe's current patch list. Returns true if patches should be
|
||||
/// (re-)applied.
|
||||
fn fetch_patches_state_stale(
|
||||
recipe_dir: &Path,
|
||||
patches: &[String],
|
||||
script: &Option<String>,
|
||||
source_dir: &Path,
|
||||
) -> bool {
|
||||
let state_file = recipe_dir.join("target/.patches-state");
|
||||
let state_content = match fs::read_to_string(&state_file) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return true,
|
||||
};
|
||||
|
||||
let expected_hash = match fetch_compute_patches_hash(recipe_dir, patches) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return true,
|
||||
};
|
||||
let expected_script_hash = script.as_ref().map(|s| {
|
||||
blake3::hash(s.as_bytes()).to_hex().to_string()
|
||||
}).unwrap_or_else(|| "none".to_string());
|
||||
let current_source_hash = fetch_compute_source_hash(source_dir);
|
||||
|
||||
let mut found_hash = false;
|
||||
let mut found_script = false;
|
||||
let mut found_source = false;
|
||||
for line in state_content.lines() {
|
||||
if let Some(stored) = line.strip_prefix("patches-hash: ") {
|
||||
if stored.trim() != expected_hash { return true; }
|
||||
found_hash = true;
|
||||
}
|
||||
if let Some(stored) = line.strip_prefix("script-hash: ") {
|
||||
if stored.trim() != expected_script_hash { return true; }
|
||||
found_script = true;
|
||||
}
|
||||
if let Some(stored) = line.strip_prefix("source-hash: ") {
|
||||
if stored.trim() != current_source_hash { return true; }
|
||||
found_source = true;
|
||||
}
|
||||
}
|
||||
|
||||
!found_hash || !found_script || !found_source
|
||||
}
|
||||
|
||||
pub(crate) fn fetch_apply_source_info(
|
||||
|
||||
Reference in New Issue
Block a user