milestone: desktop path Phases 1-5

Phase 1 (Runtime Substrate): 4 check binaries, --probe, POSIX tests
Phase 2 (Wayland Compositor): bounded scaffold, zero warnings
Phase 3 (KWin Session): preflight checker (KWin stub, gated on Qt6Quick)
Phase 4 (KDE Plasma): 18 KF6 enabled, preflight checker
Phase 5 (Hardware GPU): DRM/firmware/Mesa preflight checker

Build: zero warnings, all scripts syntax-clean. Oracle-verified.
This commit is contained in:
2026-04-29 09:54:06 +01:00
parent dbbbfebfb5
commit c3a91a5c4b
508 changed files with 76526 additions and 396 deletions
@@ -8,10 +8,11 @@ DYNAMIC_INIT
mkdir -p "${COOKBOOK_STAGE}/usr/lib"
cargo build --lib --target "${TARGET}"
unset CARGO_TARGET_DIR
cargo build --manifest-path "${COOKBOOK_SOURCE}/Cargo.toml" --lib --target "${TARGET}" --release
cp "${COOKBOOK_SOURCE}/target/${TARGET}/debug/libredox_driver_sys.a" \
cp "${COOKBOOK_SOURCE}/target/${TARGET}/release/libredox_driver_sys.a" \
"${COOKBOOK_STAGE}/usr/lib/libredox_driver_sys.a"
cp "${COOKBOOK_SOURCE}/target/${TARGET}/debug/libredox_driver_sys.rlib" \
cp "${COOKBOOK_SOURCE}/target/${TARGET}/release/libredox_driver_sys.rlib" \
"${COOKBOOK_STAGE}/usr/lib/libredox_driver_sys.rlib"
"""
@@ -8,6 +8,7 @@ export SEATD_SOCK=/run/seatd.sock
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
export QT_WAYLAND_SHELL_INTEGRATION="${QT_WAYLAND_SHELL_INTEGRATION:-xdg-shell}"
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
@@ -22,7 +23,12 @@ if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2
fi
if ! command -v kwin_wayland_wrapper >/dev/null 2>&1; then
echo "redbear-greeter-compositor: kwin_wayland_wrapper not found in PATH" >&2
# Fall back to redbear-compositor (simpler Rust compositor)
if command -v /usr/bin/redbear-compositor >/dev/null 2>&1 || command -v redbear-compositor >/dev/null 2>&1; then
echo "redbear-greeter-compositor: kwin_wayland_wrapper not found, using redbear-compositor" >&2
exec /usr/bin/redbear-compositor
fi
echo "redbear-greeter-compositor: kwin_wayland_wrapper not found, and redbear-compositor not found either" >&2
exit 1
fi
@@ -39,9 +45,9 @@ if [ -z "${KWIN_DRM_DEVICES:-}" ]; then
fi
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
echo "redbear-greeter-compositor: using DRM KWin backend (KWIN_DRM_DEVICES=${KWIN_DRM_DEVICES})" >&2
echo "redbear-greeter-compositor: using DRM compositor backend (KWIN_DRM_DEVICES=${KWIN_DRM_DEVICES})" >&2
exec kwin_wayland_wrapper --drm
else
echo "redbear-greeter-compositor: using virtual KWin backend (set KWIN_DRM_DEVICES to enable DRM)" >&2
echo "redbear-greeter-compositor: using virtual compositor backend (set KWIN_DRM_DEVICES to enable DRM)" >&2
exec kwin_wayland_wrapper --virtual
fi
@@ -18,3 +18,11 @@ template = "cargo"
"/usr/bin/redbear-phase5-wifi-run" = "redbear-phase5-wifi-run"
"/usr/bin/redbear-phase5-wifi-analyze" = "redbear-phase5-wifi-analyze"
"/usr/bin/redbear-phase5-wifi-link-check" = "redbear-phase5-wifi-link-check"
"/usr/bin/redbear-phase1-evdev-check" = "redbear-phase1-evdev-check"
"/usr/bin/redbear-phase1-udev-check" = "redbear-phase1-udev-check"
"/usr/bin/redbear-phase1-firmware-check" = "redbear-phase1-firmware-check"
"/usr/bin/redbear-phase1-drm-check" = "redbear-phase1-drm-check"
"/usr/bin/redbear-phase2-wayland-check" = "redbear-phase2-wayland-check"
"/usr/bin/redbear-phase3-kwin-check" = "redbear-phase3-kwin-check"
"/usr/bin/redbear-phase4-kde-check" = "redbear-phase4-kde-check"
"/usr/bin/redbear-phase5-gpu-check" = "redbear-phase5-gpu-check"
@@ -91,10 +91,42 @@ path = "src/bin/redbear-phase-acpi-check.rs"
name = "redbear-phase-pci-irq-check"
path = "src/bin/redbear-phase-pci-irq-check.rs"
[[bin]]
name = "redbear-phase1-evdev-check"
path = "src/bin/redbear-phase1-evdev-check.rs"
[[bin]]
name = "redbear-phase1-udev-check"
path = "src/bin/redbear-phase1-udev-check.rs"
[[bin]]
name = "redbear-usb-check"
path = "src/bin/redbear-usb-check.rs"
[[bin]]
name = "redbear-phase1-firmware-check"
path = "src/bin/redbear-phase1-firmware-check.rs"
[[bin]]
name = "redbear-phase1-drm-check"
path = "src/bin/redbear-phase1-drm-check.rs"
[[bin]]
name = "redbear-phase2-wayland-check"
path = "src/bin/redbear-phase2-wayland-check.rs"
[[bin]]
name = "redbear-phase3-kwin-check"
path = "src/bin/redbear-phase3-kwin-check.rs"
[[bin]]
name = "redbear-phase4-kde-check"
path = "src/bin/redbear-phase4-kde-check.rs"
[[bin]]
name = "redbear-phase5-gpu-check"
path = "src/bin/redbear-phase5-gpu-check.rs"
[dependencies]
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
serde = { version = "1", features = ["derive"] }
@@ -3,12 +3,11 @@ use std::io::{Read, Write};
use std::process;
use std::time::{Duration, Instant};
use orbclient::{KeyEvent, K_A};
use orbclient::{K_A, KeyEvent};
use redbear_hwutils::parse_args;
const PROGRAM: &str = "redbear-input-inject";
const USAGE: &str =
"Usage: redbear-input-inject\n\nInject a synthetic 'A' key press/release through /scheme/input/producer and verify the first evdev consumer event.";
const USAGE: &str = "Usage: redbear-input-inject\n\nInject a synthetic 'A' key press/release through /scheme/input/producer and verify the first evdev consumer event.";
const EVENT_SIZE: usize = 24;
const EV_KEY: u16 = 0x01;
@@ -3,10 +3,10 @@ use std::fs;
use std::process;
use redbear_hwutils::{
lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location, PciLocation,
PciLocation, lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location,
};
use redox_driver_sys::pci::{parse_device_info_from_config_space, InterruptSupport, PciDeviceInfo};
use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags};
use redox_driver_sys::pci::{InterruptSupport, PciDeviceInfo, parse_device_info_from_config_space};
use redox_driver_sys::quirks::{PciQuirkFlags, lookup_pci_quirks};
const USAGE: &str = "Usage: lspci\nList PCI devices exposed by /scheme/pci.";
@@ -4,7 +4,7 @@ use std::process;
use std::str::FromStr;
use redbear_hwutils::{describe_usb_device, parse_args};
use redox_driver_sys::quirks::{lookup_usb_quirks, UsbQuirkFlags};
use redox_driver_sys::quirks::{UsbQuirkFlags, lookup_usb_quirks};
use serde::Deserialize;
const USAGE: &str = "Usage: lsusb\nList USB devices exposed by native usb.* schemes.";
@@ -161,7 +161,9 @@ fn run() -> Result<(), String> {
println!("BLUETOOTH_BATTERY_CHECK=pass");
println!("PASS: bounded Bluetooth Battery Level slice exercised inside target runtime");
println!("NOTE: this proves explicit-startup btusb/btctl startup, repeated packaged helper runs in one boot, daemon restart cleanup, stale-state cleanup after disconnect, and one experimental battery-sensor Battery Level read-only workload; it does not prove controller bring-up, general device traffic, generic GATT, real pairing, write support, notify support, or broad BLE maturity");
println!(
"NOTE: this proves explicit-startup btusb/btctl startup, repeated packaged helper runs in one boot, daemon restart cleanup, stale-state cleanup after disconnect, and one experimental battery-sensor Battery Level read-only workload; it does not prove controller bring-up, general device traffic, generic GATT, real pairing, write support, notify support, or broad BLE maturity"
);
Ok(())
}
@@ -278,7 +280,10 @@ fn run_cycle(label: &str, verify_info: bool) -> Result<(), String> {
)?;
require_contains(&info, &format!("workload={EXPERIMENTAL_WORKLOAD}"))?;
require_contains(&info, &format!("peripheral_class={PERIPHERAL_CLASS}"))?;
require_contains(&info, "does not prove controller bring-up, general device traffic, generic GATT, real pairing, validated reconnect semantics, write support, or notify support beyond the experimental battery-sensor read-only workload")?;
require_contains(
&info,
"does not prove controller bring-up, general device traffic, generic GATT, real pairing, validated reconnect semantics, write support, or notify support beyond the experimental battery-sensor read-only workload",
)?;
}
let disconnect_output = print_checked_command(
@@ -1,6 +1,6 @@
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::mem::{size_of, MaybeUninit};
use std::mem::{MaybeUninit, size_of};
use std::path::Path;
use std::process::{self};
@@ -162,9 +162,16 @@ fn parse_args() -> Result<(String, String, Option<String>), String> {
while let Some(arg) = args.next() {
match arg.as_str() {
"--vendor" => vendor = args.next(),
"--card" => card = args.next().ok_or_else(|| "missing value for --card".to_string())?,
"--card" => {
card = args
.next()
.ok_or_else(|| "missing value for --card".to_string())?
}
"--modeset" => {
modeset = Some(args.next().ok_or_else(|| "missing value for --modeset".to_string())?)
modeset = Some(
args.next()
.ok_or_else(|| "missing value for --modeset".to_string())?,
)
}
"-h" | "--help" => {
println!("{USAGE}");
@@ -192,7 +199,11 @@ fn decode_wire<T: Copy>(bytes: &[u8]) -> Result<T, String> {
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
out.as_mut_ptr().cast::<u8>(),
size_of::<T>(),
);
Ok(out.assume_init())
}
}
@@ -228,7 +239,9 @@ fn query_empty(file: &mut File, request: usize, payload: &[u8]) -> Result<(), St
if response == [0] || response.is_empty() {
Ok(())
} else {
Err(format!("unexpected non-empty response for ioctl {request:#x}"))
Err(format!(
"unexpected non-empty response for ioctl {request:#x}"
))
}
}
@@ -241,7 +254,9 @@ fn query_resources(file: &mut File) -> Result<ResourcesSummary, String> {
if response.len() < offset + size_of::<u32>() {
return Err("resources response missing connector id payload".to_string());
}
connector_ids.push(decode_wire::<u32>(&response[offset..offset + size_of::<u32>()])?);
connector_ids.push(decode_wire::<u32>(
&response[offset..offset + size_of::<u32>()],
)?);
offset += size_of::<u32>();
}
@@ -252,7 +267,11 @@ fn query_resources(file: &mut File) -> Result<ResourcesSummary, String> {
}
fn query_connector(file: &mut File, connector_id: u32) -> Result<DrmConnectorWire, String> {
let response = drm_query(file, DRM_IOCTL_MODE_GETCONNECTOR, &connector_id.to_le_bytes())?;
let response = drm_query(
file,
DRM_IOCTL_MODE_GETCONNECTOR,
&connector_id.to_le_bytes(),
)?;
decode_wire(&response)
}
@@ -321,7 +340,10 @@ fn query_addfb(file: &mut File, request: &DrmAddFbWire) -> Result<DrmAddFbWire,
decode_wire(&response)
}
fn query_create_dumb(file: &mut File, request: &DrmCreateDumbWire) -> Result<DrmCreateDumbWire, String> {
fn query_create_dumb(
file: &mut File,
request: &DrmCreateDumbWire,
) -> Result<DrmCreateDumbWire, String> {
let response = drm_query(file, DRM_IOCTL_MODE_CREATE_DUMB, bytes_of(request))?;
decode_wire(&response)
}
@@ -355,7 +377,12 @@ fn disable_crtc_request(crtc_id: u32) -> DrmSetCrtcWire {
}
}
fn setcrtc_request(crtc_id: u32, connector_id: u32, fb_id: u32, mode: DrmModeWire) -> DrmSetCrtcWire {
fn setcrtc_request(
crtc_id: u32,
connector_id: u32,
fb_id: u32,
mode: DrmModeWire,
) -> DrmSetCrtcWire {
let mut request = DrmSetCrtcWire {
crtc_id,
fb_handle: fb_id,
@@ -398,7 +425,9 @@ fn bounded_modeset_proof(
let encoder = query_encoder(file, connector.encoder_id)?;
let crtc_id = encoder.crtc_id;
if crtc_id == 0 {
return Err(format!("connector {connector_id} encoder did not report a usable CRTC"));
return Err(format!(
"connector {connector_id} encoder did not report a usable CRTC"
));
}
let create = query_create_dumb(
@@ -516,7 +545,11 @@ fn main() {
#[cfg(test)]
mod tests {
use super::{bytes_of, decode_wire, disable_crtc_request, find_mode, has_connector_section, has_mode_lines, parse_modeset_spec, proof_teardown_requests, setcrtc_request, DrmModeWire, DrmResourcesWire, ModeSummary};
use super::{
DrmModeWire, DrmResourcesWire, ModeSummary, bytes_of, decode_wire, disable_crtc_request,
find_mode, has_connector_section, has_mode_lines, parse_modeset_spec,
proof_teardown_requests, setcrtc_request,
};
fn owned_bytes_of<T>(value: &T) -> Vec<u8> {
unsafe {
@@ -537,7 +570,11 @@ mod tests {
#[test]
fn query_modes_accepts_empty_sentinel() {
let parsed = if vec![0] == [0] { Vec::<DrmModeWire>::new() } else { unreachable!() };
let parsed = if vec![0] == [0] {
Vec::<DrmModeWire>::new()
} else {
unreachable!()
};
assert!(parsed.is_empty());
}
@@ -3,8 +3,7 @@ use std::{
io::{BufRead, BufReader, Write},
os::unix::net::UnixStream,
path::Path,
process,
thread,
process, thread,
time::{Duration, Instant},
};
@@ -35,7 +34,10 @@ enum Mode {
Valid { username: String, password: String },
}
fn parse_credentials(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<(String, String), String> {
fn parse_credentials(
args: &mut impl Iterator<Item = String>,
flag: &str,
) -> Result<(String, String), String> {
let username = args
.next()
.ok_or_else(|| format!("missing username after {flag}"))?;
@@ -89,7 +91,8 @@ fn send_request(request: &Request) -> Result<GreeterResponse, String> {
reader
.read_line(&mut line)
.map_err(|err| format!("failed to read greeter response: {err}"))?;
serde_json::from_str(line.trim()).map_err(|err| format!("failed to parse greeter response: {err}"))
serde_json::from_str(line.trim())
.map_err(|err| format!("failed to parse greeter response: {err}"))
}
fn require_path(path: &str) -> Result<(), String> {
@@ -127,7 +130,9 @@ fn wait_for_greeter_ready(timeout: Duration) -> Result<(), String> {
thread::sleep(Duration::from_millis(250));
}
Err(String::from("timed out waiting for greeter to return to greeter_ready"))
Err(String::from(
"timed out waiting for greeter to return to greeter_ready",
))
}
fn run_status() -> Result<(), String> {
@@ -163,8 +168,12 @@ fn run_status() -> Result<(), String> {
Ok(())
}
GreeterResponse::Error { message } => Err(format!("greeter hello failed: {message}")),
GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response when greeting greeter")),
GreeterResponse::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")),
GreeterResponse::ActionResult { .. } => Err(String::from(
"unexpected power response when greeting greeter",
)),
GreeterResponse::LoginResult { .. } => Err(String::from(
"unexpected login result when greeting greeter",
)),
}
}
@@ -183,9 +192,15 @@ fn run_invalid(username: &str, password: &str) -> Result<(), String> {
Ok(())
}
}
GreeterResponse::Error { message } => Err(format!("invalid-login request failed: {message}")),
GreeterResponse::ActionResult { .. } => Err(String::from("unexpected power response for invalid login")),
GreeterResponse::HelloOk { .. } => Err(String::from("unexpected hello response for invalid login")),
GreeterResponse::Error { message } => {
Err(format!("invalid-login request failed: {message}"))
}
GreeterResponse::ActionResult { .. } => {
Err(String::from("unexpected power response for invalid login"))
}
GreeterResponse::HelloOk { .. } => {
Err(String::from("unexpected hello response for invalid login"))
}
}
}
@@ -263,7 +278,10 @@ mod tests {
#[test]
fn parse_mode_defaults_to_status() {
assert_eq!(parse_mode_from_args(Vec::<String>::new()).expect("status mode should parse"), Mode::Status);
assert_eq!(
parse_mode_from_args(Vec::<String>::new()).expect("status mode should parse"),
Mode::Status
);
}
#[test]
@@ -307,7 +325,9 @@ mod tests {
String::from("password"),
String::from("extra"),
]),
Err(String::from("unexpected extra arguments after --valid USER PASSWORD"))
Err(String::from(
"unexpected extra arguments after --valid USER PASSWORD"
))
);
}
@@ -320,7 +340,9 @@ mod tests {
String::from("wrong"),
String::from("extra"),
]),
Err(String::from("unexpected extra arguments after --invalid USER PASSWORD"))
Err(String::from(
"unexpected extra arguments after --invalid USER PASSWORD"
))
);
}
@@ -84,7 +84,11 @@ fn run() -> Result<(), String> {
println!(
"ACPI_ROOT={}",
if surface.acpi_root_present { "present" } else { "missing" }
if surface.acpi_root_present {
"present"
} else {
"missing"
}
);
println!(
"KERNEL_KSTOP={}",
@@ -94,7 +98,14 @@ fn run() -> Result<(), String> {
"missing"
}
);
println!("ACPI_DMI={}", if surface.dmi_present { "present" } else { "missing" });
println!(
"ACPI_DMI={}",
if surface.dmi_present {
"present"
} else {
"missing"
}
);
println!(
"ACPI_REBOOT={}",
if surface.reboot_present {
@@ -103,7 +103,11 @@ fn collect_irq_reports(root: &Path) -> Vec<IrqReport> {
}
}
reports.sort_by(|left, right| left.driver.cmp(&right.driver).then(left.device.cmp(&right.device)));
reports.sort_by(|left, right| {
left.driver
.cmp(&right.driver)
.then(left.device.cmp(&right.device))
});
reports
}
@@ -8,8 +8,7 @@ use syscall::O_NONBLOCK;
use redbear_hwutils::parse_args;
const PROGRAM: &str = "redbear-phase-ps2-check";
const USAGE: &str =
"Usage: redbear-phase-ps2-check\n\nRun the bounded PS/2 and serio proof check inside the guest.";
const USAGE: &str = "Usage: redbear-phase-ps2-check\n\nRun the bounded PS/2 and serio proof check inside the guest.";
fn require_path(path: &str) -> Result<(), String> {
if Path::new(path).exists()
@@ -36,7 +35,9 @@ fn run_phase3_input_check() -> Result<(), String> {
println!("phase3_input_check=ok");
Ok(())
} else {
Err(format!("redbear-phase3-input-check exited with status {status}"))
Err(format!(
"redbear-phase3-input-check exited with status {status}"
))
}
}
@@ -3,7 +3,7 @@ use std::process;
use std::thread;
use std::time::Duration;
use libredox::{flag, Fd};
use libredox::{Fd, flag};
use redbear_hwutils::parse_args;
use syscall::data::TimeSpec;
@@ -0,0 +1,411 @@
//! Phase 1 DRM/KMS smoke test.
#[cfg(target_os = "redox")]
use std::fs::{self, File};
#[cfg(target_os = "redox")]
use std::io::Read;
#[cfg(target_os = "redox")]
use std::path::Path;
use std::process;
const PROGRAM: &str = "redbear-phase1-drm-check";
const USAGE: &str = "Usage: redbear-phase1-drm-check [--json] [--verbose]\n\n\
Phase 1 DRM/KMS smoke test. Validates scheme:drm/card0 registration and\n\
bounded connector/mode queries. Lighter alternative to redbear-drm-display-check.";
#[cfg(target_os = "redox")]
const DRM_SCHEME: &str = "/scheme/drm";
#[cfg(target_os = "redox")]
const DRM_CARD: &str = "/scheme/drm/card0";
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
CheckResult::Pass => "PASS",
CheckResult::Fail => "FAIL",
CheckResult::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Check {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.to_string(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Check {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.to_string(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
verbose: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool, verbose: bool) -> Self {
Report {
checks: Vec::new(),
json_mode,
verbose,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|c| c.result == CheckResult::Fail)
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
if self.verbose || check.result != CheckResult::Skip {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
drm_scheme: bool,
card0_present: bool,
connectors: usize,
modes: usize,
checks: Vec<JsonCheck>,
}
let drm_scheme = self
.checks
.iter()
.find(|c| c.name == "DRM_SCHEME_REGISTERED")
.map_or(false, |c| c.result == CheckResult::Pass);
let card0_present = self
.checks
.iter()
.find(|c| c.name == "CARD0_NODE")
.map_or(false, |c| c.result == CheckResult::Pass);
let connectors = self
.checks
.iter()
.find(|c| c.name == "CONNECTOR_ENUM")
.and_then(|c| {
c.detail
.strip_prefix("found ")
.and_then(|s| s.split(' ').next())
.and_then(|s| s.parse::<usize>().ok())
})
.unwrap_or(0);
let modes = self
.checks
.iter()
.find(|c| c.name == "MODE_ENUM")
.and_then(|c| {
c.detail
.strip_prefix("found ")
.and_then(|s| s.split(' ').next())
.and_then(|s| s.parse::<usize>().ok())
})
.unwrap_or(0);
let checks: Vec<JsonCheck> = self
.checks
.iter()
.map(|c| JsonCheck {
name: c.name.clone(),
result: c.result.label().to_string(),
detail: c.detail.clone(),
})
.collect();
let report = JsonReport {
drm_scheme,
card0_present,
connectors,
modes,
checks,
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<(bool, bool), String> {
let mut json_mode = false;
let mut verbose = false;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => json_mode = true,
"--verbose" => verbose = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok((json_mode, verbose))
}
#[cfg(target_os = "redox")]
fn check_scheme_registered() -> Check {
match fs::read_dir(DRM_SCHEME) {
Ok(_) => Check::pass("DRM_SCHEME_REGISTERED", DRM_SCHEME),
Err(err) => Check::fail(
"DRM_SCHEME_REGISTERED",
&format!("cannot read {DRM_SCHEME}: {err}"),
),
}
}
#[cfg(target_os = "redox")]
fn check_card0_node() -> Check {
if Path::new(DRM_CARD).exists() {
Check::pass("CARD0_NODE", DRM_CARD)
} else {
Check::fail("CARD0_NODE", &format!("{DRM_CARD} not found"))
}
}
#[cfg(target_os = "redox")]
fn read_card_node() -> Result<Vec<u8>, String> {
let mut file =
File::open(DRM_CARD).map_err(|err| format!("failed to open {DRM_CARD}: {err}"))?;
let mut buf = vec![0u8; 4096];
let n = file
.read(&mut buf)
.map_err(|err| format!("failed to read {DRM_CARD}: {err}"))?;
buf.truncate(n);
Ok(buf)
}
#[cfg(target_os = "redox")]
fn check_card_responds() -> Check {
match read_card_node() {
Ok(content) if !content.is_empty() => Check::pass(
"CARD0_RESPONDS",
&format!("{} byte(s) from card node", content.len()),
),
Ok(content) => Check::fail("CARD0_RESPONDS", "card node returned empty response"),
Err(msg) => Check::fail("CARD0_RESPONDS", &msg),
}
}
#[cfg(target_os = "redox")]
fn enumerate_connectors() -> Check {
let dir_path = format!("{DRM_CARD}/connectors");
match fs::read_dir(&dir_path) {
Ok(entries) => {
let connectors: Vec<_> = entries.filter_map(|e| e.ok()).collect();
if connectors.is_empty() {
Check::fail("CONNECTOR_ENUM", "no connectors found in card0/connectors/")
} else {
let preview: Vec<String> = connectors
.iter()
.take(4)
.filter_map(|e| e.file_name().into_string().ok())
.collect();
Check::pass(
"CONNECTOR_ENUM",
&format!("found {}: {}", connectors.len(), preview.join(", ")),
)
}
}
Err(err) => Check::fail("CONNECTOR_ENUM", &format!("cannot list {dir_path}: {err}")),
}
}
#[cfg(target_os = "redox")]
fn enumerate_modes() -> Check {
let dir_path = format!("{DRM_CARD}/modes");
match fs::read_dir(&dir_path) {
Ok(entries) => {
let modes: Vec<_> = entries.filter_map(|e| e.ok()).collect();
if modes.is_empty() {
Check::fail("MODE_ENUM", "no modes found in card0/modes/")
} else {
let preview: Vec<String> = modes
.iter()
.take(4)
.filter_map(|e| e.file_name().into_string().ok())
.collect();
Check::pass(
"MODE_ENUM",
&format!("found {}: {}", modes.len(), preview.join(", ")),
)
}
}
Err(err) => Check::fail("MODE_ENUM", &format!("cannot list {dir_path}: {err}")),
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: DRM check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let (json_mode, verbose) = parse_args()?;
let mut report = Report::new(json_mode, verbose);
report.add(check_scheme_registered());
report.add(check_card0_node());
report.add(check_card_responds());
report.add(enumerate_connectors());
report.add(enumerate_modes());
report.print();
if report.any_failed() {
return Err("one or more DRM checks failed".to_string());
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(target_os = "redox")]
#[cfg(test)]
mod tests {
use super::*;
fn parse_args_with<'a>(args: &[&'a str]) -> Result<(bool, bool), String> {
let mut json_mode = false;
let mut verbose = false;
let mut args_iter = args.iter();
while let Some(arg) = args_iter.next() {
match *arg {
"--json" => json_mode = true,
"--verbose" => verbose = true,
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok((json_mode, verbose))
}
#[test]
fn parse_args_accepts_json_flag() {
let result = parse_args_with(&["--json"]);
let (json_mode, _verbose) = result.expect("parse_args should succeed");
assert!(json_mode, "json_mode should be true with --json flag");
}
#[test]
fn parse_args_accepts_verbose_flag() {
let result = parse_args_with(&["--verbose"]);
let (_json_mode, verbose) = result.expect("parse_args should succeed");
assert!(verbose, "verbose should be true with --verbose flag");
}
#[test]
fn parse_args_rejects_unknown() {
let result = parse_args_with(&["--unknown-flag"]);
assert!(result.is_err(), "parse_args should reject unknown argument");
}
#[test]
fn parse_args_default_values() {
let result = parse_args_with(&[]);
let (json_mode, verbose) = result.expect("parse_args should succeed");
assert!(!json_mode, "json_mode should be false by default");
assert!(!verbose, "verbose should be false by default");
}
#[test]
fn check_status_render_pass() {
let label = CheckResult::Pass.label();
assert_eq!(label, "PASS", "CheckResult::Pass should render as PASS");
}
#[test]
fn check_status_render_fail() {
let label = CheckResult::Fail.label();
assert_eq!(label, "FAIL", "CheckResult::Fail should render as FAIL");
}
}
@@ -0,0 +1,687 @@
use std::{process, time::Duration};
#[cfg(target_os = "redox")]
use std::{
fs::{self, File, OpenOptions},
io::{self, Read},
time::Instant,
};
#[cfg(target_os = "redox")]
use std::os::unix::fs::OpenOptionsExt;
use serde_json::json;
#[cfg(target_os = "redox")]
use syscall::O_NONBLOCK;
const PROGRAM: &str = "redbear-phase1-evdev-check";
const USAGE: &str = "Usage: redbear-phase1-evdev-check [--keyboard] [--mouse] [--timeout SECS] [--json]\n\nValidate the bounded evdevd keyboard and mouse paths inside the Red Bear guest.";
const DEFAULT_TIMEOUT_SECS: u64 = 5;
const MAX_TIMEOUT_SECS: u64 = 300;
#[cfg(target_os = "redox")]
const MAX_METADATA_BYTES: usize = 64 * 1024;
#[cfg(any(target_os = "redox", test))]
const EV_KEY: u16 = 0x01;
#[cfg(any(target_os = "redox", test))]
const EV_REL: u16 = 0x02;
#[cfg(any(target_os = "redox", test))]
const LEGACY_EVENT_SIZE: usize = 16;
#[cfg(any(target_os = "redox", test))]
const CURRENT_EVENT_SIZE: usize = 24;
#[cfg(any(target_os = "redox", test))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct InputEvent {
event_type: u16,
code: u16,
value: i32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Config {
keyboard: bool,
mouse: bool,
timeout: Duration,
json: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct Report {
evdev_scheme: bool,
keyboard_events: bool,
mouse_events: bool,
}
#[cfg(target_os = "redox")]
#[derive(Clone, Debug, Eq, PartialEq)]
enum CheckStatus {
Pass(String),
Fail(String),
Timeout(String),
Skip,
}
#[cfg(any(target_os = "redox", test))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum InputKind {
Keyboard,
Mouse,
}
#[cfg(any(target_os = "redox", test))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct EventMetadata {
keyboard: bool,
mouse: bool,
}
#[cfg(any(target_os = "redox", test))]
impl InputEvent {
fn from_legacy_bytes(bytes: &[u8]) -> Result<Self, String> {
if bytes.len() != LEGACY_EVENT_SIZE {
return Err(format!(
"expected {LEGACY_EVENT_SIZE} bytes, got {}",
bytes.len()
));
}
Ok(Self {
event_type: u16::from_le_bytes([bytes[8], bytes[9]]),
code: u16::from_le_bytes([bytes[10], bytes[11]]),
value: i32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
})
}
fn from_current_bytes(bytes: &[u8]) -> Result<Self, String> {
if bytes.len() != CURRENT_EVENT_SIZE {
return Err(format!(
"expected {CURRENT_EVENT_SIZE} bytes, got {}",
bytes.len()
));
}
Ok(Self {
event_type: u16::from_le_bytes([bytes[16], bytes[17]]),
code: u16::from_le_bytes([bytes[18], bytes[19]]),
value: i32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]),
})
}
}
#[cfg(target_os = "redox")]
impl CheckStatus {
fn is_success(&self) -> bool {
matches!(self, Self::Pass(_) | Self::Skip)
}
fn render(&self, label: &str) {
match self {
Self::Pass(detail) => println!("PASS {label}: {detail}"),
Self::Fail(detail) => println!("FAIL {label}: {detail}"),
Self::Timeout(detail) => println!("TIMEOUT {label}: {detail}"),
Self::Skip => println!("SKIP {label}: not requested"),
}
}
}
fn main() {
match parse_args(std::env::args()) {
Ok(config) => match run(&config) {
Ok(success) => process::exit(if success { 0 } else { 1 }),
Err(err) => {
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
},
Err(err) if err.is_empty() => process::exit(0),
Err(err) => {
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
}
fn parse_args(args: impl IntoIterator<Item = String>) -> Result<Config, String> {
let mut keyboard = false;
let mut mouse = false;
let mut timeout = Duration::from_secs(DEFAULT_TIMEOUT_SECS);
let mut json = false;
let mut args = args.into_iter();
let _program = args.next();
while let Some(arg) = args.next() {
match arg.as_str() {
"--keyboard" => keyboard = true,
"--mouse" => mouse = true,
"--timeout" => {
let Some(value) = args.next() else {
return Err("missing value for --timeout".to_string());
};
let secs = value
.parse::<u64>()
.map_err(|err| format!("invalid timeout '{value}': {err}"))?;
if secs > MAX_TIMEOUT_SECS {
return Err(format!(
"timeout '{value}' exceeds maximum of {MAX_TIMEOUT_SECS} seconds"
));
}
timeout = Duration::from_secs(secs);
}
"--json" => json = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
if !keyboard && !mouse {
keyboard = true;
mouse = true;
}
Ok(Config {
keyboard,
mouse,
timeout,
json,
})
}
fn run(config: &Config) -> Result<bool, String> {
#[cfg(not(target_os = "redox"))]
{
let report = Report::default();
if config.json {
let payload = serde_json::to_string(&json!({
"evdev_scheme": report.evdev_scheme,
"keyboard_events": report.keyboard_events,
"mouse_events": report.mouse_events,
}))
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
eprintln!("evdevd check requires Redox runtime");
println!("{payload}");
} else {
println!("evdevd check requires Redox runtime");
}
Ok(true)
}
#[cfg(target_os = "redox")]
{
run_redox(config)
}
}
#[cfg(target_os = "redox")]
fn run_redox(config: &Config) -> Result<bool, String> {
let evdev_scheme_present = fs::metadata("/scheme/evdev").is_ok();
let event_names = match list_event_names() {
Ok(names) => names,
Err(_) => Vec::new(),
};
let report = Report {
evdev_scheme: evdev_scheme_present,
keyboard_events: false,
mouse_events: false,
};
let metadata = load_event_metadata(&event_names);
let keyboard_name = select_event_name(&event_names, &metadata, InputKind::Keyboard, None);
let mouse_name = select_event_name(
&event_names,
&metadata,
InputKind::Mouse,
keyboard_name.as_deref(),
);
let mut report = report;
let scheme_status = if report.evdev_scheme {
CheckStatus::Pass(format!(
"enumerated {} device(s): {}",
event_names.len(),
if event_names.is_empty() {
String::from("none")
} else {
event_names.join(", ")
}
))
} else {
CheckStatus::Fail("could not enumerate any /scheme/evdev/event* nodes".to_string())
};
let keyboard_status = if config.keyboard {
run_input_check(keyboard_name.as_deref(), EV_KEY, config.timeout, "keyboard")
} else {
CheckStatus::Skip
};
report.keyboard_events = matches!(keyboard_status, CheckStatus::Pass(_));
let mouse_status = if config.mouse {
run_input_check(mouse_name.as_deref(), EV_REL, config.timeout, "mouse")
} else {
CheckStatus::Skip
};
report.mouse_events = matches!(mouse_status, CheckStatus::Pass(_));
if config.json {
let payload = serde_json::to_string(&json!({
"evdev_scheme": report.evdev_scheme,
"keyboard_events": report.keyboard_events,
"mouse_events": report.mouse_events,
}))
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
println!("{payload}");
} else {
scheme_status.render("evdev scheme");
keyboard_status.render("keyboard events");
mouse_status.render("mouse events");
}
Ok(scheme_status.is_success() && keyboard_status.is_success() && mouse_status.is_success())
}
#[cfg(target_os = "redox")]
fn list_event_names() -> Result<Vec<String>, String> {
let entries = fs::read_dir("/scheme/evdev")
.map_err(|err| format!("failed to read /scheme/evdev: {err}"))?;
let mut names = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_name().into_string().ok())
.filter(|name| event_index(name).is_some())
.collect::<Vec<_>>();
names.sort_by_key(|name| event_index(name).unwrap_or(u32::MAX));
Ok(names)
}
#[cfg(target_os = "redox")]
fn load_event_metadata(event_names: &[String]) -> Vec<(String, EventMetadata)> {
let mut metadata = Vec::new();
for event_name in event_names {
let path = format!("/scheme/udev/dev/input/{event_name}");
let info = match read_text_with_limit(&path, MAX_METADATA_BYTES) {
Ok(info) => info,
Err(_) => {
metadata.push((event_name.clone(), EventMetadata::default()));
continue;
}
};
metadata.push((event_name.clone(), parse_event_metadata(&info)));
}
metadata
}
#[cfg(target_os = "redox")]
fn read_text_with_limit(path: &str, max_bytes: usize) -> Result<String, String> {
let mut file = File::open(path).map_err(|err| format!("failed to open {path}: {err}"))?;
let mut bytes = Vec::new();
file.by_ref()
.take((max_bytes + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|err| format!("failed to read {path}: {err}"))?;
if bytes.len() > max_bytes {
return Err(format!("{path} exceeds maximum size of {max_bytes} bytes"));
}
String::from_utf8(bytes).map_err(|err| format!("{path} is not valid UTF-8: {err}"))
}
#[cfg(any(target_os = "redox", test))]
fn parse_event_metadata(info: &str) -> EventMetadata {
let mut metadata = EventMetadata::default();
for line in info.lines() {
if let Some(value) = line.strip_prefix("E=ID_INPUT_KEYBOARD=") {
metadata.keyboard = value.trim() == "1";
}
if let Some(value) = line.strip_prefix("E=ID_INPUT_MOUSE=") {
metadata.mouse = value.trim() == "1";
}
}
metadata
}
#[cfg(any(target_os = "redox", test))]
fn select_event_name(
event_names: &[String],
metadata: &[(String, EventMetadata)],
kind: InputKind,
exclude: Option<&str>,
) -> Option<String> {
let mut matching_names = metadata
.iter()
.filter_map(|(name, entry)| {
if exclude == Some(name.as_str()) {
return None;
}
let matches_kind = match kind {
InputKind::Keyboard => entry.keyboard,
InputKind::Mouse => entry.mouse,
};
matches_kind.then_some(name.clone())
})
.collect::<Vec<_>>();
matching_names.sort_by_key(|name| event_index(name).unwrap_or(u32::MAX));
if let Some(name) = matching_names.into_iter().next() {
return Some(name);
}
let preferred = match kind {
InputKind::Keyboard => "event0",
InputKind::Mouse => "event1",
};
if exclude != Some(preferred) && event_names.iter().any(|name| name == preferred) {
return Some(preferred.to_string());
}
None
}
#[cfg(target_os = "redox")]
fn run_input_check(
event_name: Option<&str>,
expected_type: u16,
timeout: Duration,
label: &str,
) -> CheckStatus {
let Some(event_name) = event_name else {
return CheckStatus::Fail(format!("no {label} event device was enumerated"));
};
let path = format!("/scheme/evdev/{event_name}");
let mut file = match open_nonblocking(&path) {
Ok(file) => file,
Err(err) => return CheckStatus::Fail(err),
};
match wait_for_event(&mut file, expected_type, timeout) {
Ok(Some(event)) => CheckStatus::Pass(format!(
"{path} produced type={} code={} value={}",
event.event_type, event.code, event.value
)),
Ok(None) => CheckStatus::Timeout(format!(
"{path} produced no matching event within {}s",
timeout.as_secs()
)),
Err(err) => CheckStatus::Fail(format!("{path}: {err}")),
}
}
#[cfg(target_os = "redox")]
fn open_nonblocking(path: &str) -> Result<File, String> {
OpenOptions::new()
.read(true)
.custom_flags(O_NONBLOCK as i32)
.open(path)
.map_err(|err| format!("failed to open {path}: {err}"))
}
#[cfg(target_os = "redox")]
fn wait_for_event(
file: &mut File,
expected_type: u16,
timeout: Duration,
) -> Result<Option<InputEvent>, String> {
let deadline = Instant::now() + timeout;
let mut raw = [0_u8; CURRENT_EVENT_SIZE * 4];
while Instant::now() < deadline {
match file.read(&mut raw) {
Ok(0) => std::thread::sleep(Duration::from_millis(25)),
Ok(len) => {
let events = parse_events_for_expected(&raw[..len], expected_type)?;
if let Some(event) = events
.into_iter()
.find(|event| event.event_type == expected_type)
{
return Ok(Some(event));
}
}
Err(err)
if matches!(
err.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
) =>
{
std::thread::sleep(Duration::from_millis(25));
}
Err(err) => return Err(format!("failed to read event data: {err}")),
}
}
Ok(None)
}
#[cfg(any(target_os = "redox", test))]
fn parse_events_for_expected(bytes: &[u8], expected_type: u16) -> Result<Vec<InputEvent>, String> {
if bytes.is_empty() {
return Ok(Vec::new());
}
let current =
parse_events_with_layout(bytes, CURRENT_EVENT_SIZE, InputEvent::from_current_bytes);
let legacy = parse_events_with_layout(bytes, LEGACY_EVENT_SIZE, InputEvent::from_legacy_bytes);
match (current, legacy) {
(Ok(current_events), Ok(legacy_events)) => {
let current_matches = current_events
.iter()
.any(|event| event.event_type == expected_type);
let legacy_matches = legacy_events
.iter()
.any(|event| event.event_type == expected_type);
match (current_matches, legacy_matches) {
(true, false) => Ok(current_events),
(false, true) => Ok(legacy_events),
(true, true) | (false, false) => Ok(current_events),
}
}
(Ok(current_events), Err(_)) => Ok(current_events),
(Err(_), Ok(legacy_events)) => Ok(legacy_events),
(Err(current_err), Err(legacy_err)) => Err(format!(
"failed to decode evdev payload as 24-byte or 16-byte events: {current_err}; {legacy_err}"
)),
}
}
#[cfg(any(target_os = "redox", test))]
fn parse_events_with_layout(
bytes: &[u8],
event_size: usize,
decode: fn(&[u8]) -> Result<InputEvent, String>,
) -> Result<Vec<InputEvent>, String> {
if bytes.len() % event_size != 0 {
return Err(format!(
"payload length {} is not divisible by event size {event_size}",
bytes.len()
));
}
bytes.chunks_exact(event_size).map(decode).collect()
}
#[cfg(any(target_os = "redox", test))]
fn event_index(name: &str) -> Option<u32> {
name.strip_prefix("event")?.parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn vec_args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn parse_args_defaults_to_keyboard_and_mouse() {
let config = parse_args(vec_args(&[PROGRAM])).unwrap();
assert!(config.keyboard);
assert!(config.mouse);
assert_eq!(config.timeout, Duration::from_secs(DEFAULT_TIMEOUT_SECS));
assert!(!config.json);
}
#[test]
fn parse_args_accepts_targeted_flags() {
let config = parse_args(vec_args(&[
PROGRAM,
"--keyboard",
"--timeout",
"9",
"--json",
]))
.unwrap();
assert!(config.keyboard);
assert!(!config.mouse);
assert_eq!(config.timeout, Duration::from_secs(9));
assert!(config.json);
}
#[test]
fn parse_args_rejects_invalid_timeout() {
let err = parse_args(vec_args(&[PROGRAM, "--timeout", "abc"])).unwrap_err();
assert!(err.contains("invalid timeout"));
}
#[test]
fn parse_args_rejects_timeout_over_limit() {
let err = parse_args(vec_args(&[PROGRAM, "--timeout", "301"])).unwrap_err();
assert!(err.contains("exceeds maximum"));
}
#[test]
fn parses_current_input_event_layout() {
let bytes = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 30, 0, 1, 0, 0, 0,
];
let event = InputEvent::from_current_bytes(&bytes).unwrap();
assert_eq!(
event,
InputEvent {
event_type: EV_KEY,
code: 30,
value: 1,
}
);
}
#[test]
fn parses_legacy_input_event_layout() {
let bytes = [0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0];
let event = InputEvent::from_legacy_bytes(&bytes).unwrap();
assert_eq!(
event,
InputEvent {
event_type: EV_REL,
code: 0,
value: 5,
}
);
}
#[test]
fn event_index_parses_numeric_suffix() {
assert_eq!(event_index("event0"), Some(0));
assert_eq!(event_index("event17"), Some(17));
assert_eq!(event_index("mouse"), None);
}
#[test]
fn parse_event_metadata_extracts_keyboard_and_mouse_flags() {
let metadata =
parse_event_metadata("E=ID_INPUT=1\nE=ID_INPUT_KEYBOARD=1\nE=ID_INPUT_MOUSE=0\n");
assert!(metadata.keyboard);
assert!(!metadata.mouse);
}
#[test]
fn select_event_name_prefers_metadata_match() {
let event_names = vec!["event0".to_string(), "event1".to_string()];
let metadata = vec![
(
"event0".to_string(),
EventMetadata {
keyboard: true,
mouse: false,
},
),
(
"event1".to_string(),
EventMetadata {
keyboard: false,
mouse: true,
},
),
];
assert_eq!(
select_event_name(&event_names, &metadata, InputKind::Mouse, None),
Some("event1".to_string())
);
}
#[test]
fn select_event_name_prefers_keyboard_metadata_match() {
let event_names = vec!["event0".to_string(), "event1".to_string()];
let metadata = vec![
(
"event0".to_string(),
EventMetadata {
keyboard: true,
mouse: false,
},
),
(
"event1".to_string(),
EventMetadata {
keyboard: false,
mouse: true,
},
),
];
assert_eq!(
select_event_name(&event_names, &metadata, InputKind::Keyboard, None),
Some("event0".to_string())
);
}
#[test]
fn select_event_name_does_not_fallback_to_arbitrary_device() {
let event_names = vec!["event2".to_string()];
let metadata = vec![("event2".to_string(), EventMetadata::default())];
assert_eq!(
select_event_name(&event_names, &metadata, InputKind::Mouse, None),
None
);
}
#[test]
fn parse_events_prefers_legacy_layout_when_only_legacy_matches_expected_type() {
let bytes = [
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 30, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let events = parse_events_for_expected(&bytes, EV_KEY).unwrap();
assert_eq!(events.len(), 3);
assert_eq!(events[0].event_type, EV_KEY);
assert_eq!(events[0].code, 30);
assert_eq!(events[0].value, 1);
}
}
@@ -0,0 +1,467 @@
//! Phase 1 firmware-loader smoke test.
#[cfg(target_os = "redox")]
use std::fs;
#[cfg(target_os = "redox")]
use std::io::Read;
#[cfg(target_os = "redox")]
use std::path::{Path, PathBuf};
use std::process;
const PROGRAM: &str = "redbear-phase1-firmware-check";
const USAGE: &str = "Usage: redbear-phase1-firmware-check [--json] [--blob KEY]\n\n\
Phase 1 firmware-loader smoke test. Validates scheme:firmware registration\n\
and at least one readable firmware blob.";
#[cfg(target_os = "redox")]
const FALLBACK_BLOBS: &[&str] = &[
"amdgpu/dce_11_0_dmcu.bin",
"amdgpu/dcn_3_2_mall.bin",
"i915/kbl_dmc_ver1_04.bin",
"r8168n.bin",
"rtl_nic/rtl8105e-1_0_0.fw",
];
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
CheckResult::Pass => "PASS",
CheckResult::Fail => "FAIL",
CheckResult::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Check {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.to_string(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Check {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.to_string(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self {
Report {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|c| c.result == CheckResult::Fail)
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
firmware_scheme: bool,
blob_read: bool,
blob_size: usize,
checks: Vec<JsonCheck>,
}
let firmware_scheme = self
.checks
.iter()
.find(|c| c.name == "FIRMWARE_SCHEME_REGISTERED")
.map_or(false, |c| c.result == CheckResult::Pass);
let blob_read = self
.checks
.iter()
.find(|c| c.name == "BLOB_READ")
.map_or(false, |c| c.result == CheckResult::Pass);
let blob_size = self
.checks
.iter()
.find(|c| c.name == "BLOB_READ")
.and_then(|c| {
c.detail
.strip_prefix("size=")
.and_then(|s| s.split(' ').next())
.and_then(|s| s.parse::<usize>().ok())
})
.unwrap_or(0);
let checks: Vec<JsonCheck> = self
.checks
.iter()
.map(|c| JsonCheck {
name: c.name.clone(),
result: c.result.label().to_string(),
detail: c.detail.clone(),
})
.collect();
let report = JsonReport {
firmware_scheme,
blob_read,
blob_size,
checks,
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<(bool, Option<String>), String> {
let mut json_mode = false;
let mut blob_key = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => json_mode = true,
"--blob" => {
blob_key = Some(
args.next()
.ok_or_else(|| "missing value for --blob".to_string())?,
);
}
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok((json_mode, blob_key))
}
#[cfg(target_os = "redox")]
fn check_scheme_registered() -> Check {
match fs::read_dir("/scheme/") {
Ok(entries) => {
let names: Vec<String> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.collect();
if names.iter().any(|n| n == "firmware") {
Check::pass(
"FIRMWARE_SCHEME_REGISTERED",
&format!("found {} scheme(s)", names.len()),
)
} else {
Check::fail(
"FIRMWARE_SCHEME_REGISTERED",
"firmware not found in /scheme/",
)
}
}
Err(err) => Check::fail(
"FIRMWARE_SCHEME_REGISTERED",
&format!("cannot read /scheme/: {err}"),
),
}
}
#[cfg(target_os = "redox")]
fn list_firmware_keys() -> Check {
match fs::read_dir("/scheme/firmware/") {
Ok(entries) => {
let keys: Vec<String> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.collect();
if keys.is_empty() {
Check::fail("FIRMWARE_KEY_LIST", "no keys found in /scheme/firmware/")
} else {
let preview = keys.iter().take(4).cloned().collect::<Vec<_>>().join(", ");
Check::pass(
"FIRMWARE_KEY_LIST",
&format!("{} key(s): {}", keys.len(), preview),
)
}
}
Err(err) => Check::fail(
"FIRMWARE_KEY_LIST",
&format!("cannot list /scheme/firmware/: {err}"),
),
}
}
#[cfg(target_os = "redox")]
fn read_firmware_blob(key: &str) -> Result<(usize, Vec<u8>), String> {
let path = format!("/scheme/firmware/{key}");
let mut file =
std::fs::File::open(&path).map_err(|err| format!("failed to open {path}: {err}"))?;
let mut buf = Vec::new();
let size = file
.read_to_end(&mut buf)
.map_err(|err| format!("failed to read {path}: {err}"))?;
Ok((size, buf))
}
#[cfg(target_os = "redox")]
fn check_blob_fstat(key: &str) -> Check {
let path = format!("/scheme/firmware/{key}");
match std::fs::File::open(&path) {
Ok(file) => match file.metadata() {
Ok(meta) => {
let size = meta.len();
if size > 0 {
Check::pass(
"BLOB_MMAP_PATH",
&format!("size={} via fstat on {}", size, key),
)
} else {
Check::fail("BLOB_MMAP_PATH", &format!("blob {key} has zero size"))
}
}
Err(err) => Check::fail("BLOB_MMAP_PATH", &format!("fstat failed for {path}: {err}")),
},
Err(err) => Check::fail("BLOB_MMAP_PATH", &format!("cannot open {path}: {err}")),
}
}
#[cfg(target_os = "redox")]
fn check_lib_firmware_dir() -> Check {
let dir = Path::new("/lib/firmware/");
match fs::read_dir(dir) {
Ok(entries) => {
let blobs: Vec<PathBuf> = entries
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map_or(false, |ft| ft.is_file()))
.map(|e| e.path())
.collect();
if blobs.is_empty() {
Check::skip("LIB_FIRMWARE_DIR", "/lib/firmware/ is empty")
} else {
let preview = blobs
.iter()
.take(3)
.filter_map(|p| p.file_name().and_then(|n| n.to_str()))
.collect::<Vec<_>>()
.join(", ");
Check::pass(
"LIB_FIRMWARE_DIR",
&format!("{} blob(s) in /lib/firmware/: {}", blobs.len(), preview),
)
}
}
Err(err) => Check::skip(
"LIB_FIRMWARE_DIR",
&format!("/lib/firmware/ not accessible: {err}"),
),
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: firmware-loader check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let (json_mode, blob_key) = parse_args()?;
let mut report = Report::new(json_mode);
report.add(check_scheme_registered());
report.add(list_firmware_keys());
report.add(check_lib_firmware_dir());
let blob_to_try = blob_key.or_else(|| {
FALLBACK_BLOBS
.iter()
.copied()
.find(|&k| Path::new(&format!("/scheme/firmware/{k}")).exists())
.map(String::from)
});
match blob_to_try {
Some(key) => {
match read_firmware_blob(&key) {
Ok((size, _content)) => {
if size > 0 {
report.add(Check::pass("BLOB_READ", &format!("size={} key={}", size, key)));
} else {
report.add(Check::fail("BLOB_READ", &format!("blob {key} has zero size")));
}
}
Err(msg) => {
report.add(Check::fail("BLOB_READ", &msg));
}
}
report.add(check_blob_fstat(&key));
}
None => {
report.add(Check::skip("BLOB_READ", "no known blob key found in /scheme/firmware/"));
report.add(Check::skip("BLOB_MMAP_PATH", "no blob to check"));
}
}
report.print();
if report.any_failed() {
return Err("one or more firmware checks failed".to_string());
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(target_os = "redox")]
#[cfg(test)]
mod tests {
use super::*;
fn parse_args_with<'a>(args: &[&'a str]) -> Result<(bool, Option<String>), String> {
let mut json_mode = false;
let mut blob_key = None;
let mut args_iter = args.iter();
while let Some(arg) = args_iter.next() {
match *arg {
"--json" => json_mode = true,
"--blob" => {
blob_key = Some(
args_iter
.next()
.ok_or_else(|| "missing value for --blob".to_string())?
.to_string(),
);
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok((json_mode, blob_key))
}
#[test]
fn parse_args_accepts_json_flag() {
let result = parse_args_with(&["--json"]);
let (json_mode, _blob_key) = result.expect("parse_args should succeed");
assert!(json_mode, "json_mode should be true with --json flag");
}
#[test]
fn parse_args_accepts_blob_flag() {
let result = parse_args_with(&["--blob", "somename"]);
let (_json_mode, blob_key) = result.expect("parse_args should succeed");
assert_eq!(blob_key, Some("somename".to_string()), "blob_key should be Some(\"somename\")");
}
#[test]
fn parse_args_rejects_unknown() {
let result = parse_args_with(&["--unknown-flag"]);
assert!(result.is_err(), "parse_args should reject unknown argument");
}
#[test]
fn parse_args_default_no_json() {
let result = parse_args_with(&[]);
let (json_mode, _blob_key) = result.expect("parse_args should succeed");
assert!(!json_mode, "json_mode should be false by default");
}
#[test]
fn check_status_render_pass() {
let label = CheckResult::Pass.label();
assert_eq!(label, "PASS", "CheckResult::Pass should render as PASS");
}
#[test]
fn check_status_render_fail() {
let label = CheckResult::Fail.label();
assert_eq!(label, "FAIL", "CheckResult::Fail should render as FAIL");
}
}
@@ -0,0 +1,336 @@
use std::process;
#[cfg(target_os = "redox")]
use std::{fs, io::Read};
use serde_json::json;
const PROGRAM: &str = "redbear-phase1-udev-check";
const USAGE: &str = "Usage: redbear-phase1-udev-check [--keyboard] [--pointer] [--drm] [--json]\n\nValidate bounded udev-shim device enumeration inside the Red Bear guest.";
#[cfg(target_os = "redox")]
const MAX_DEVICE_INFO_BYTES: usize = 64 * 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
struct Config {
keyboard: bool,
pointer: bool,
drm: bool,
json: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct Report {
udev_scheme: bool,
keyboard_count: usize,
pointer_count: usize,
drm_count: usize,
}
#[cfg(target_os = "redox")]
#[derive(Clone, Debug, Eq, PartialEq)]
enum CheckStatus {
Pass(String),
Fail(String),
Skip,
}
#[cfg(target_os = "redox")]
impl CheckStatus {
fn render(&self, label: &str) {
match self {
Self::Pass(detail) => println!("PASS {label}: {detail}"),
Self::Fail(detail) => println!("FAIL {label}: {detail}"),
Self::Skip => println!("SKIP {label}: not requested"),
}
}
}
fn main() {
match parse_args(std::env::args()) {
Ok(config) => match run(&config) {
Ok(success) => process::exit(if success { 0 } else { 1 }),
Err(err) => {
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
},
Err(err) if err.is_empty() => process::exit(0),
Err(err) => {
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
}
fn parse_args(args: impl IntoIterator<Item = String>) -> Result<Config, String> {
let mut keyboard = false;
let mut pointer = false;
let mut drm = false;
let mut json = false;
let mut args = args.into_iter();
let _program = args.next();
while let Some(arg) = args.next() {
match arg.as_str() {
"--keyboard" => keyboard = true,
"--pointer" => pointer = true,
"--drm" => drm = true,
"--json" => json = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
if !keyboard && !pointer && !drm {
keyboard = true;
pointer = true;
drm = true;
}
Ok(Config {
keyboard,
pointer,
drm,
json,
})
}
fn run(config: &Config) -> Result<bool, String> {
#[cfg(not(target_os = "redox"))]
{
let report = Report::default();
if config.json {
let payload = serde_json::to_string(&json!({
"udev_scheme": report.udev_scheme,
"keyboard_count": report.keyboard_count,
"pointer_count": report.pointer_count,
"drm_count": report.drm_count,
}))
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
eprintln!("udev-shim check requires Redox runtime");
println!("{payload}");
} else {
println!("udev-shim check requires Redox runtime");
}
Ok(true)
}
#[cfg(target_os = "redox")]
{
run_redox(config)
}
}
#[cfg(target_os = "redox")]
fn run_redox(config: &Config) -> Result<bool, String> {
let udev_scheme_present = fs::metadata("/scheme/udev").is_ok();
let device_entries = match list_dir_names("/scheme/udev/devices") {
Ok(entries) => entries,
Err(_) => Vec::new(),
};
let report = Report {
udev_scheme: udev_scheme_present,
keyboard_count: count_devices_with_property(&device_entries, "ID_INPUT_KEYBOARD", "1"),
pointer_count: count_devices_with_property(&device_entries, "ID_INPUT_MOUSE", "1"),
drm_count: count_drm_devices(),
};
let scheme_status = if report.udev_scheme {
CheckStatus::Pass(format!(
"enumerated {} /scheme/udev/devices entries",
device_entries.len()
))
} else {
CheckStatus::Fail("could not enumerate any /scheme/udev/devices entries".to_string())
};
let keyboard_status = if config.keyboard {
count_status(report.keyboard_count, "keyboard")
} else {
CheckStatus::Skip
};
let pointer_status = if config.pointer {
count_status(report.pointer_count, "pointer")
} else {
CheckStatus::Skip
};
let drm_status = if config.drm {
count_status(report.drm_count, "DRM")
} else {
CheckStatus::Skip
};
if config.json {
let payload = serde_json::to_string(&json!({
"udev_scheme": report.udev_scheme,
"keyboard_count": report.keyboard_count,
"pointer_count": report.pointer_count,
"drm_count": report.drm_count,
}))
.map_err(|err| format!("failed to serialize JSON output: {err}"))?;
println!("{payload}");
} else {
scheme_status.render("udev scheme");
keyboard_status.render("keyboard devices");
pointer_status.render("pointer devices");
drm_status.render("DRM devices");
}
Ok(overall_success(&report, &config))
}
#[cfg(any(target_os = "redox", test))]
fn overall_success(report: &Report, config: &Config) -> bool {
report.udev_scheme
&& (!config.keyboard || report.keyboard_count > 0)
&& (!config.pointer || report.pointer_count > 0)
&& (!config.drm || report.drm_count > 0)
}
#[cfg(target_os = "redox")]
fn list_dir_names(path: &str) -> Result<Vec<String>, String> {
let entries = fs::read_dir(path).map_err(|err| format!("failed to read {path}: {err}"))?;
let mut names = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_name().into_string().ok())
.collect::<Vec<_>>();
names.sort();
Ok(names)
}
#[cfg(target_os = "redox")]
fn count_devices_with_property(device_entries: &[String], key: &str, value: &str) -> usize {
device_entries
.iter()
.filter(|entry| {
let path = format!("/scheme/udev/devices/{entry}");
let Ok(info) = read_text_with_limit(&path, MAX_DEVICE_INFO_BYTES) else {
return false;
};
has_property(&info, key, value)
})
.count()
}
#[cfg(target_os = "redox")]
fn count_drm_devices() -> usize {
list_dir_names("/dev/dri")
.map(|entries| {
entries
.into_iter()
.filter(|name| name.starts_with("card"))
.count()
})
.unwrap_or(0)
}
#[cfg(target_os = "redox")]
fn read_text_with_limit(path: &str, max_bytes: usize) -> Result<String, String> {
let mut file = fs::File::open(path).map_err(|err| format!("failed to open {path}: {err}"))?;
let mut bytes = Vec::new();
file.by_ref()
.take((max_bytes + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|err| format!("failed to read {path}: {err}"))?;
if bytes.len() > max_bytes {
return Err(format!("{path} exceeds maximum size of {max_bytes} bytes"));
}
String::from_utf8(bytes).map_err(|err| format!("{path} is not valid UTF-8: {err}"))
}
#[cfg(any(target_os = "redox", test))]
fn has_property(info: &str, key: &str, expected: &str) -> bool {
let prefix = format!("E={key}=");
info.lines()
.find_map(|line| line.strip_prefix(&prefix))
.map(|value| value.trim() == expected)
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
fn vec_args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn parse_args_defaults_to_all_checks() {
let config = parse_args(vec_args(&[PROGRAM])).unwrap();
assert!(config.keyboard);
assert!(config.pointer);
assert!(config.drm);
assert!(!config.json);
}
#[test]
fn parse_args_accepts_targeted_flags() {
let config = parse_args(vec_args(&[PROGRAM, "--keyboard", "--json"])).unwrap();
assert!(config.keyboard);
assert!(!config.pointer);
assert!(!config.drm);
assert!(config.json);
}
#[test]
fn parse_args_rejects_unknown_flag() {
let err = parse_args(vec_args(&[PROGRAM, "--bogus"])).unwrap_err();
assert!(err.contains("unsupported argument"));
}
#[test]
fn has_property_matches_expected_key_and_value() {
let info = "P=/devices/platform/evdev-keyboard0\nE=ID_INPUT=1\nE=ID_INPUT_KEYBOARD=1\n";
assert!(has_property(info, "ID_INPUT_KEYBOARD", "1"));
assert!(!has_property(info, "ID_INPUT_MOUSE", "1"));
}
#[test]
fn overall_success_requires_all_requested_runtime_surfaces() {
let all_flags = Config {
keyboard: true,
pointer: true,
drm: true,
json: false,
};
let passing = Report {
udev_scheme: true,
keyboard_count: 1,
pointer_count: 1,
drm_count: 1,
};
let missing_drm = Report {
drm_count: 0,
..passing.clone()
};
assert!(overall_success(&passing, &all_flags));
assert!(!overall_success(&missing_drm, &all_flags));
}
#[test]
fn overall_success_respects_targeted_flags() {
let passing = Report {
udev_scheme: true,
keyboard_count: 1,
pointer_count: 0,
drm_count: 0,
};
let keyboard_only = Config {
keyboard: true,
pointer: false,
drm: false,
json: false,
};
assert!(overall_success(&passing, &keyboard_only));
}
}
@@ -0,0 +1,615 @@
//! Phase 2 Wayland compositor proof checker.
#[cfg(target_os = "redox")]
use std::{
env, fs,
io::{Read, Write},
os::unix::net::UnixStream,
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use std::process;
const PROGRAM: &str = "redbear-phase2-wayland-check";
const USAGE: &str = "Usage: redbear-phase2-wayland-check [--json]\n\n\
Phase 2 Wayland compositor proof checker. Validates the compositor socket,\n\
compositor process, Wayland protocol connectivity, EGL/GBM presence,\n\
software renderer evidence, and the optional qt6-wayland-smoke client.";
#[cfg(target_os = "redox")]
const DEFAULT_RUNTIME_DIR: &str = "/run/user/1000";
#[cfg(target_os = "redox")]
const DEFAULT_WAYLAND_DISPLAY: &str = "wayland-0";
#[cfg(target_os = "redox")]
const QT6_WAYLAND_SMOKE: &str = "/usr/bin/qt6-wayland-smoke";
fn parse_args_from<I>(args: I) -> Result<bool, String>
where
I: IntoIterator<Item = String>,
{
let mut json_mode = false;
let mut args = args.into_iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
fn parse_args() -> Result<bool, String> {
parse_args_from(std::env::args().skip(1))
}
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
CheckResult::Pass => "PASS",
CheckResult::Fail => "FAIL",
CheckResult::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.into(),
}
}
fn fail(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.into(),
}
}
fn skip(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.into(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
}
fn optional_check_passed(&self, name: &str) -> Option<bool> {
self.checks
.iter()
.find(|check| check.name == name)
.and_then(|check| match check.result {
CheckResult::Pass => Some(true),
CheckResult::Fail => Some(false),
CheckResult::Skip => None,
})
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
println!("=== Red Bear OS Phase 2 Wayland Compositor Check ===");
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
overall_success: bool,
compositor_socket: bool,
compositor_process: bool,
wayland_registry: bool,
egl_present: bool,
gbm_present: bool,
software_renderer: bool,
qt6_wayland_smoke_present: Option<bool>,
checks: Vec<JsonCheck>,
}
let report = JsonReport {
overall_success: !self.any_failed(),
compositor_socket: self.check_passed("WAYLAND_SOCKET"),
compositor_process: self.check_passed("COMPOSITOR_PROCESS"),
wayland_registry: self.check_passed("WAYLAND_PROTOCOL_REGISTRY"),
egl_present: self.check_passed("LIBEGL_PRESENT"),
gbm_present: self.check_passed("LIBGBM_PRESENT"),
software_renderer: self.check_passed("SOFTWARE_RENDERER"),
qt6_wayland_smoke_present: self.optional_check_passed("QT6_WAYLAND_SMOKE"),
checks: self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect(),
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[derive(Debug, Clone)]
struct WaylandEndpoint {
path: PathBuf,
display: String,
}
#[cfg(target_os = "redox")]
struct WaylandClient {
stream: UnixStream,
next_id: u32,
}
#[cfg(target_os = "redox")]
impl WaylandClient {
fn connect(path: &Path) -> Result<Self, String> {
let stream = UnixStream::connect(path)
.map_err(|err| format!("failed to connect to {}: {err}", path.display()))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|err| format!("failed to set read timeout on {}: {err}", path.display()))?;
stream
.set_write_timeout(Some(Duration::from_secs(2)))
.map_err(|err| format!("failed to set write timeout on {}: {err}", path.display()))?;
Ok(Self { stream, next_id: 2 })
}
fn alloc_id(&mut self) -> u32 {
let id = self.next_id;
self.next_id += 1;
id
}
fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> Result<(), String> {
let size = 8 + payload.len();
let mut message = Vec::with_capacity(size);
message.extend_from_slice(&object_id.to_le_bytes());
let header = ((size as u32) << 16) | u32::from(opcode);
message.extend_from_slice(&header.to_le_bytes());
message.extend_from_slice(payload);
self.stream
.write_all(&message)
.map_err(|err| format!("failed to write Wayland message: {err}"))
}
fn read_message(&mut self) -> Result<(u32, u16, Vec<u8>), String> {
let mut header = [0u8; 8];
self.stream
.read_exact(&mut header)
.map_err(|err| format!("failed to read Wayland header: {err}"))?;
let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let size = ((size_opcode >> 16) & 0xFFFF) as usize;
let opcode = (size_opcode & 0xFFFF) as u16;
if size < 8 {
return Err(format!("invalid Wayland message size {size}"));
}
let payload_len = size - 8;
let mut payload = vec![0u8; payload_len];
if payload_len > 0 {
self.stream
.read_exact(&mut payload)
.map_err(|err| format!("failed to read Wayland payload: {err}"))?;
}
Ok((object_id, opcode, payload))
}
fn get_registry(&mut self) -> Result<u32, String> {
let registry_id = self.alloc_id();
self.send_message(1, 1, &registry_id.to_le_bytes())?;
Ok(registry_id)
}
fn sync(&mut self) -> Result<u32, String> {
let callback_id = self.alloc_id();
self.send_message(1, 0, &callback_id.to_le_bytes())?;
Ok(callback_id)
}
}
#[cfg(target_os = "redox")]
fn env_value(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.trim().is_empty())
}
#[cfg(target_os = "redox")]
fn wayland_socket_candidates(runtime_dir: Option<&str>, display: Option<&str>) -> Vec<PathBuf> {
let display = display.unwrap_or(DEFAULT_WAYLAND_DISPLAY);
let mut candidates = Vec::new();
if let Some(runtime_dir) = runtime_dir {
candidates.push(PathBuf::from(runtime_dir).join(display));
}
let fallback = PathBuf::from(DEFAULT_RUNTIME_DIR).join(DEFAULT_WAYLAND_DISPLAY);
if !candidates.iter().any(|candidate| candidate == &fallback) {
candidates.push(fallback);
}
candidates
}
#[cfg(target_os = "redox")]
fn resolve_wayland_endpoint() -> Result<WaylandEndpoint, String> {
let runtime_dir = env_value("XDG_RUNTIME_DIR");
let display = env_value("WAYLAND_DISPLAY").unwrap_or_else(|| DEFAULT_WAYLAND_DISPLAY.to_string());
let candidates = wayland_socket_candidates(runtime_dir.as_deref(), Some(&display));
for candidate in candidates {
if candidate.exists() {
return Ok(WaylandEndpoint {
path: candidate,
display: display.clone(),
});
}
}
let paths = wayland_socket_candidates(runtime_dir.as_deref(), Some(&display))
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Err(format!("missing Wayland socket at any of: {paths}"))
}
#[cfg(target_os = "redox")]
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|err| format!("failed to run {label}: {err}"))?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
String::from("no output")
};
return Err(format!("{label} exited with status {}: {detail}", output.status));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(target_os = "redox")]
fn detect_compositor_process(output: &str) -> Option<&'static str> {
if output.contains("redbear-compositor") {
Some("redbear-compositor")
} else if output.contains("kwin_wayland") {
Some("kwin_wayland")
} else {
None
}
}
#[cfg(target_os = "redox")]
fn check_compositor_process() -> Check {
match run_command("ps", &[], "ps") {
Ok(output) => match detect_compositor_process(&output) {
Some(process_name) => Check::pass(
"COMPOSITOR_PROCESS",
format!("{process_name} appears in process list"),
),
None => Check::fail(
"COMPOSITOR_PROCESS",
"neither redbear-compositor nor kwin_wayland appears in ps output",
),
},
Err(err) => Check::fail("COMPOSITOR_PROCESS", err),
}
}
#[cfg(target_os = "redox")]
fn verify_registry_roundtrip(endpoint: &WaylandEndpoint) -> Result<String, String> {
let mut client = WaylandClient::connect(&endpoint.path)?;
let registry_id = client.get_registry()?;
let callback_id = client.sync()?;
for _ in 0..8 {
let (object_id, opcode, _) = client.read_message()?;
if object_id == registry_id {
return Ok(format!(
"{} responded to wl_display.get_registry with opcode {} on {}",
endpoint.display,
opcode,
endpoint.path.display()
));
}
if object_id == callback_id {
return Ok(format!(
"{} completed bounded roundtrip after wl_display.get_registry on {}",
endpoint.display,
endpoint.path.display()
));
}
}
Err(format!(
"{} did not answer wl_display.get_registry within bounded read window",
endpoint.path.display()
))
}
#[cfg(target_os = "redox")]
fn contains_software_renderer_text(output: &str) -> bool {
let lower = output.to_ascii_lowercase();
lower.contains("llvmpipe")
|| lower.contains("software rasterizer")
|| lower.contains("kms_swrast")
|| lower.contains("swrast")
}
#[cfg(target_os = "redox")]
fn is_software_driver_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
lower.contains("llvmpipe") || lower.contains("kms_swrast") || lower.contains("swrast")
}
#[cfg(target_os = "redox")]
fn software_driver_names_in_dir(dir: &Path) -> Result<Vec<String>, String> {
let entries = fs::read_dir(dir)
.map_err(|err| format!("cannot list {}: {err}", dir.display()))?
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_name().into_string().ok())
.filter(|name| is_software_driver_name(name))
.collect::<Vec<_>>();
Ok(entries)
}
#[cfg(target_os = "redox")]
fn check_software_renderer() -> Check {
let mut details = Vec::new();
if Path::new("/usr/bin/glxinfo").exists() {
match run_command("glxinfo", &[], "glxinfo") {
Ok(output) if contains_software_renderer_text(&output) => {
return Check::pass("SOFTWARE_RENDERER", "glxinfo reports llvmpipe/software renderer");
}
Ok(_) => details.push(String::from("glxinfo ran but did not report llvmpipe")),
Err(err) => details.push(err),
}
} else {
details.push(String::from("/usr/bin/glxinfo not installed"));
}
let dri_dir = Path::new("/usr/lib/dri");
match software_driver_names_in_dir(dri_dir) {
Ok(driver_names) if !driver_names.is_empty() => Check::pass(
"SOFTWARE_RENDERER",
format!(
"software DRI driver(s) present in {}: {}",
dri_dir.display(),
driver_names.join(", ")
),
),
Ok(_) => {
details.push(format!("{} has no llvmpipe/swrast-style drivers", dri_dir.display()));
Check::fail("SOFTWARE_RENDERER", details.join("; "))
}
Err(err) => {
details.push(err);
Check::fail("SOFTWARE_RENDERER", details.join("; "))
}
}
}
#[cfg(target_os = "redox")]
fn check_optional_qt_smoke() -> Check {
if Path::new(QT6_WAYLAND_SMOKE).exists() {
Check::pass("QT6_WAYLAND_SMOKE", QT6_WAYLAND_SMOKE)
} else {
Check::skip(
"QT6_WAYLAND_SMOKE",
format!("optional binary not installed at {QT6_WAYLAND_SMOKE}"),
)
}
}
fn run() -> Result<(), String> {
let json_mode = parse_args()?;
#[cfg(not(target_os = "redox"))]
{
let _ = json_mode;
println!("{PROGRAM}: Wayland compositor check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let mut report = Report::new(json_mode);
match resolve_wayland_endpoint() {
Ok(endpoint) => {
report.add(Check::pass(
"WAYLAND_SOCKET",
format!("{} ({})", endpoint.path.display(), endpoint.display),
));
report.add(check_compositor_process());
report.add(match verify_registry_roundtrip(&endpoint) {
Ok(detail) => Check::pass("WAYLAND_PROTOCOL_REGISTRY", detail),
Err(err) => Check::fail("WAYLAND_PROTOCOL_REGISTRY", err),
});
}
Err(err) => {
report.add(Check::fail("WAYLAND_SOCKET", err));
report.add(check_compositor_process());
report.add(Check::fail(
"WAYLAND_PROTOCOL_REGISTRY",
"cannot attempt wl_display.get_registry without a Wayland socket",
));
}
}
report.add(if Path::new("/usr/lib/libEGL.so").exists() {
Check::pass("LIBEGL_PRESENT", "/usr/lib/libEGL.so")
} else {
Check::fail("LIBEGL_PRESENT", "missing /usr/lib/libEGL.so")
});
report.add(if Path::new("/usr/lib/libGBM.so").exists() {
Check::pass("LIBGBM_PRESENT", "/usr/lib/libGBM.so")
} else {
Check::fail("LIBGBM_PRESENT", "missing /usr/lib/libGBM.so")
});
report.add(check_software_renderer());
report.add(check_optional_qt_smoke());
report.print();
if report.any_failed() {
return Err(String::from("one or more Phase 2 Wayland checks failed"));
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "redox")]
#[test]
fn wayland_socket_candidates_include_runtime_then_default() {
let candidates = wayland_socket_candidates(Some("/tmp/runtime"), Some("wayland-9"));
assert_eq!(candidates[0], PathBuf::from("/tmp/runtime/wayland-9"));
assert!(candidates.contains(&PathBuf::from("/run/user/1000/wayland-0")));
}
#[cfg(target_os = "redox")]
#[test]
fn detect_compositor_process_matches_kwin_wrapper_line() {
let output = "123 kwin_wayland_wrapper --virtual\n";
assert_eq!(detect_compositor_process(output), Some("kwin_wayland"));
}
#[cfg(target_os = "redox")]
#[test]
fn contains_software_renderer_text_detects_llvmpipe() {
assert!(contains_software_renderer_text(
"OpenGL renderer string: llvmpipe (LLVM 18.1, 256 bits)"
));
}
#[cfg(target_os = "redox")]
#[test]
fn is_software_driver_name_detects_swrast_variants() {
assert!(is_software_driver_name("kms_swrast_dri.so"));
assert!(is_software_driver_name("swrast_dri.so"));
assert!(!is_software_driver_name("iris_dri.so"));
}
#[test]
fn parse_args_accepts_json_flag() {
let parsed = parse_args_from([String::from("--json")]);
assert_eq!(parsed, Ok(true));
}
#[test]
fn parse_args_rejects_unknown_flag() {
let parsed = parse_args_from([String::from("--bogus")]);
assert_eq!(parsed, Err(String::from("unsupported argument: --bogus")));
}
}
@@ -0,0 +1,497 @@
//! Phase 3 desktop-session preflight checker.
//! Validates compositor binary presence, D-Bus session bus, seatd socket,
//! and WAYLAND_DISPLAY availability. Does NOT validate real KWin behavior
//! (KWin recipe currently provides cmake stubs pending Qt6Quick/QML).
#[cfg(target_os = "redox")]
use std::{
env,
io::{Read, Write},
os::unix::net::UnixStream,
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use std::process;
const PROGRAM: &str = "redbear-phase3-kwin-check";
const USAGE: &str = "Usage: redbear-phase3-kwin-check [--json]\n\n\
Phase 3 desktop-session preflight check. Validates compositor binary\n\
presence, D-Bus session bus reachability, seatd socket presence, active\n\
WAYLAND_DISPLAY state, and a bounded wl_display roundtrip.\n\
NOTE: Does NOT validate real KWin behavior (KWin is a cmake stub).";
#[cfg(target_os = "redox")]
const DEFAULT_RUNTIME_DIR: &str = "/run/user/1000";
#[cfg(target_os = "redox")]
const DBUS_SESSION_DESTINATION: &str = "org.freedesktop.DBus";
fn parse_args_from<I>(args: I) -> Result<bool, String>
where
I: IntoIterator<Item = String>,
{
let mut json_mode = false;
let mut args = args.into_iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
fn parse_args() -> Result<bool, String> {
parse_args_from(std::env::args().skip(1))
}
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
CheckResult::Pass => "PASS",
CheckResult::Fail => "FAIL",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.into(),
}
}
fn fail(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.into(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
println!("=== Red Bear OS Phase 3 Desktop Session Preflight ===");
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
overall_success: bool,
compositor_binary: bool,
dbus_session_bus_address: bool,
dbus_send_session: bool,
seatd_socket: bool,
wayland_display_active: bool,
wayland_roundtrip: bool,
checks: Vec<JsonCheck>,
}
let report = JsonReport {
overall_success: !self.any_failed(),
compositor_binary: self.check_passed("COMPOSITOR_BINARY"),
dbus_session_bus_address: self.check_passed("DBUS_SESSION_BUS_ADDRESS"),
dbus_send_session: self.check_passed("DBUS_SEND_SESSION"),
seatd_socket: self.check_passed("SEATD_SOCKET"),
wayland_display_active: self.check_passed("WAYLAND_DISPLAY_ACTIVE"),
wayland_roundtrip: self.check_passed("WAYLAND_ROUNDTRIP"),
checks: self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect(),
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[derive(Debug, Clone)]
struct WaylandEndpoint {
path: PathBuf,
display: String,
}
#[cfg(target_os = "redox")]
struct WaylandClient {
stream: UnixStream,
next_id: u32,
}
#[cfg(target_os = "redox")]
impl WaylandClient {
fn connect(path: &Path) -> Result<Self, String> {
let stream = UnixStream::connect(path)
.map_err(|err| format!("failed to connect to {}: {err}", path.display()))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|err| format!("failed to set read timeout on {}: {err}", path.display()))?;
stream
.set_write_timeout(Some(Duration::from_secs(2)))
.map_err(|err| format!("failed to set write timeout on {}: {err}", path.display()))?;
Ok(Self { stream, next_id: 2 })
}
fn alloc_id(&mut self) -> u32 {
let id = self.next_id;
self.next_id += 1;
id
}
fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> Result<(), String> {
let size = 8 + payload.len();
let mut message = Vec::with_capacity(size);
message.extend_from_slice(&object_id.to_le_bytes());
let header = ((size as u32) << 16) | u32::from(opcode);
message.extend_from_slice(&header.to_le_bytes());
message.extend_from_slice(payload);
self.stream
.write_all(&message)
.map_err(|err| format!("failed to write Wayland message: {err}"))
}
fn read_message(&mut self) -> Result<(u32, u16, Vec<u8>), String> {
let mut header = [0u8; 8];
self.stream
.read_exact(&mut header)
.map_err(|err| format!("failed to read Wayland header: {err}"))?;
let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let size = ((size_opcode >> 16) & 0xFFFF) as usize;
let opcode = (size_opcode & 0xFFFF) as u16;
if size < 8 {
return Err(format!("invalid Wayland message size {size}"));
}
let payload_len = size - 8;
let mut payload = vec![0u8; payload_len];
if payload_len > 0 {
self.stream
.read_exact(&mut payload)
.map_err(|err| format!("failed to read Wayland payload: {err}"))?;
}
Ok((object_id, opcode, payload))
}
fn sync(&mut self) -> Result<u32, String> {
let callback_id = self.alloc_id();
self.send_message(1, 0, &callback_id.to_le_bytes())?;
Ok(callback_id)
}
}
#[cfg(target_os = "redox")]
fn env_value(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.trim().is_empty())
}
#[cfg(target_os = "redox")]
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|err| format!("failed to run {label}: {err}"))?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
String::from("no output")
};
return Err(format!("{label} exited with status {}: {detail}", output.status));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(target_os = "redox")]
fn resolve_wayland_endpoint() -> Result<WaylandEndpoint, String> {
let display = env_value("WAYLAND_DISPLAY")
.ok_or_else(|| String::from("WAYLAND_DISPLAY is not set"))?;
let runtime_dir = env_value("XDG_RUNTIME_DIR").unwrap_or_else(|| DEFAULT_RUNTIME_DIR.to_string());
let path = PathBuf::from(runtime_dir).join(&display);
if path.exists() {
Ok(WaylandEndpoint { path, display })
} else {
Err(format!("WAYLAND_DISPLAY is set but socket is missing at {}", path.display()))
}
}
#[cfg(target_os = "redox")]
fn require_one_path<'a>(paths: &'a [&'a str]) -> Result<&'a str, String> {
for path in paths {
if Path::new(path).exists() {
return Ok(*path);
}
}
Err(format!("missing any of: {}", paths.join(", ")))
}
#[cfg(target_os = "redox")]
fn check_dbus_session_bus() -> (Check, Check) {
match env_value("DBUS_SESSION_BUS_ADDRESS") {
Some(address) => {
let address_check = Check::pass("DBUS_SESSION_BUS_ADDRESS", address);
if !Path::new("/usr/bin/dbus-send").exists() {
return (
address_check,
Check::fail("DBUS_SEND_SESSION", "missing /usr/bin/dbus-send"),
);
}
match run_command(
"dbus-send",
&[
"--session",
&format!("--dest={DBUS_SESSION_DESTINATION}"),
"--type=method_call",
"--print-reply",
"/org/freedesktop/DBus",
"org.freedesktop.DBus.ListNames",
],
"dbus-send --session ListNames",
) {
Ok(output) if !output.trim().is_empty() => (
address_check,
Check::pass(
"DBUS_SEND_SESSION",
"dbus-send --session returned a non-empty bus name list",
),
),
Ok(_) => (
address_check,
Check::fail(
"DBUS_SEND_SESSION",
"dbus-send --session returned empty output",
),
),
Err(err) => (address_check, Check::fail("DBUS_SEND_SESSION", err)),
}
}
None => (
Check::fail(
"DBUS_SESSION_BUS_ADDRESS",
"DBUS_SESSION_BUS_ADDRESS is not set",
),
Check::fail(
"DBUS_SEND_SESSION",
"cannot validate dbus-send without DBUS_SESSION_BUS_ADDRESS",
),
),
}
}
#[cfg(target_os = "redox")]
fn verify_wayland_roundtrip(endpoint: &WaylandEndpoint) -> Result<String, String> {
let mut client = WaylandClient::connect(&endpoint.path)?;
let callback_id = client.sync()?;
for _ in 0..8 {
let (object_id, opcode, _) = client.read_message()?;
if object_id == callback_id && opcode == 0 {
return Ok(format!(
"{} completed wl_display.sync roundtrip on {}",
endpoint.display,
endpoint.path.display()
));
}
}
Err(format!(
"{} did not emit callback.done within bounded read window",
endpoint.path.display()
))
}
fn run() -> Result<(), String> {
let json_mode = parse_args()?;
#[cfg(not(target_os = "redox"))]
{
let _ = json_mode;
println!("{PROGRAM}: desktop session preflight requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let mut report = Report::new(json_mode);
report.add(match require_one_path(&["/usr/bin/kwin_wayland", "/usr/bin/redbear-compositor"]) {
Ok(path) => Check::pass("COMPOSITOR_BINARY", path),
Err(err) => Check::fail("COMPOSITOR_BINARY", err),
});
let (dbus_address_check, dbus_send_check) = check_dbus_session_bus();
report.add(dbus_address_check);
report.add(dbus_send_check);
report.add(if Path::new("/run/seatd.sock").exists() {
Check::pass("SEATD_SOCKET", "/run/seatd.sock")
} else {
Check::fail("SEATD_SOCKET", "missing /run/seatd.sock")
});
match resolve_wayland_endpoint() {
Ok(endpoint) => {
report.add(Check::pass(
"WAYLAND_DISPLAY_ACTIVE",
format!("{} ({})", endpoint.path.display(), endpoint.display),
));
report.add(match verify_wayland_roundtrip(&endpoint) {
Ok(detail) => Check::pass("WAYLAND_ROUNDTRIP", detail),
Err(err) => Check::fail("WAYLAND_ROUNDTRIP", err),
});
}
Err(err) => {
report.add(Check::fail("WAYLAND_DISPLAY_ACTIVE", err));
report.add(Check::fail(
"WAYLAND_ROUNDTRIP",
"cannot attempt wl_display roundtrip without an active WAYLAND_DISPLAY socket",
));
}
}
report.print();
if report.any_failed() {
return Err(String::from("one or more Phase 3 preflight checks failed"));
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "redox")]
#[test]
fn require_one_path_returns_first_present_path() {
let existing = require_one_path(&["/", "/definitely/missing"]);
assert_eq!(existing, Ok("/"));
}
#[cfg(target_os = "redox")]
#[test]
fn resolve_wayland_endpoint_requires_display() {
let result = {
let display = None::<String>;
display.ok_or_else(|| String::from("WAYLAND_DISPLAY is not set"))
};
assert_eq!(result, Err(String::from("WAYLAND_DISPLAY is not set")));
}
#[test]
fn parse_args_accepts_json_flag() {
let parsed = parse_args_from([String::from("--json")]);
assert_eq!(parsed, Ok(true));
}
#[test]
fn parse_args_rejects_unknown_flag() {
let parsed = parse_args_from([String::from("--bogus")]);
assert_eq!(parsed, Err(String::from("unsupported argument: --bogus")));
}
}
@@ -0,0 +1,188 @@
// Phase 4 KDE Plasma preflight check.
// Validates KF6 library presence, plasma binaries, and session entry points.
// Does NOT validate real KDE Plasma session behavior (blocked on Qt6Quick/QML + real KWin).
use std::process;
const PROGRAM: &str = "redbear-phase4-kde-check";
const USAGE: &str = "Usage: redbear-phase4-kde-check [--json]\n\n\
Phase 4 KDE Plasma preflight check. Validates KF6 library and plasma binary\n\
presence. Does NOT validate real KDE session behavior (gated on Qt6Quick/QML).";
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
}
}
#[cfg(target_os = "redox")]
struct Check { name: String, result: CheckResult, detail: String }
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
}
fn fail(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
}
fn skip(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
}
}
#[cfg(target_os = "redox")]
struct Report { checks: Vec<Check>, json_mode: bool }
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
fn add(&mut self, check: Check) { self.checks.push(check); }
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
fn print(&self) {
if self.json_mode { self.print_json(); } else { self.print_human(); }
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck { name: String, result: String, detail: String }
#[derive(serde::Serialize)]
struct JsonReport {
kf6_libs_present: bool, plasma_binaries_present: bool,
session_entry: bool, kirigami_available: bool, checks: Vec<JsonCheck>,
}
let kf6_libs = self.checks.iter().find(|c| c.name == "KF6_LIBRARIES").map_or(false, |c| c.result == CheckResult::Pass);
let plasma_bins = self.checks.iter().find(|c| c.name == "PLASMA_BINARIES").map_or(false, |c| c.result == CheckResult::Pass);
let session_entry = self.checks.iter().find(|c| c.name == "SESSION_ENTRY").map_or(false, |c| c.result == CheckResult::Pass);
let kirigami = self.checks.iter().find(|c| c.name == "KIRIGAMI_STATUS").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { kf6_libs_present: kf6_libs, plasma_binaries_present: plasma_bins, session_entry, kirigami_available: kirigami, checks }) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn check_kf6_libraries() -> Check {
let key_libs = [
"/usr/lib/libKF6CoreAddons.so", "/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so", "/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so", "/usr/lib/libKF6Service.so",
"/usr/lib/libKF6WaylandClient.so",
];
let mut found = 0usize;
let mut missing = Vec::new();
for lib in key_libs {
if std::path::Path::new(lib).exists() {
found += 1;
} else {
missing.push(lib);
}
}
if found >= 6 {
let preview: Vec<_> = missing.iter().take(3).map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
if missing.is_empty() {
Check::pass("KF6_LIBRARIES", &format!("{}/{} key KF6 libs found", found, key_libs.len()))
} else {
Check::pass("KF6_LIBRARIES", &format!("{}/{} found, missing: {}", found, key_libs.len(), preview.join(", ")))
}
} else {
Check::fail("KF6_LIBRARIES", &format!("only {}/{} key KF6 libs found", found, key_libs.len()))
}
}
#[cfg(target_os = "redox")]
fn check_plasma_binaries() -> Check {
let bins = ["/usr/bin/plasmashell", "/usr/bin/systemsettings", "/usr/bin/kwin_wayland_wrapper"];
let mut found = 0usize;
for bin in bins {
if std::path::Path::new(bin).exists() { found += 1; }
}
if found >= 2 {
Check::pass("PLASMA_BINARIES", &format!("{}/{} plasma binaries present", found, bins.len()))
} else if found == 1 {
Check::fail("PLASMA_BINARIES", &format!("only {}/{} plasma binaries present", found, bins.len()))
} else {
Check::fail("PLASMA_BINARIES", "no plasma binaries found")
}
}
#[cfg(target_os = "redox")]
fn check_session_entry() -> Check {
let entries = ["/usr/bin/startplasma-wayland", "/usr/lib/plasma-session"];
for e in entries {
if std::path::Path::new(e).exists() {
return Check::pass("SESSION_ENTRY", e);
}
}
Check::fail("SESSION_ENTRY", "no KDE session entry point found")
}
#[cfg(target_os = "redox")]
fn check_kirigami_status() -> Check {
let kirigami_lib = "/usr/lib/libKF6Kirigami.so";
if std::path::Path::new(kirigami_lib).exists() {
Check::pass("KIRIGAMI_STATUS", "kirigami library present")
} else {
Check::skip("KIRIGAMI_STATUS", "kirigami not available (QML stub, requires Qt6Quick)")
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
println!("{PROGRAM}: KDE Plasma check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
let mut report = Report::new(json_mode);
report.add(check_kf6_libraries());
report.add(check_plasma_binaries());
report.add(check_session_entry());
report.add(check_kirigami_status());
report.print();
if report.any_failed() { return Err("one or more Phase 4 checks failed".to_string()); }
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() { process::exit(0); }
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
@@ -1,5 +1,8 @@
use std::{env, path::{Path, PathBuf}};
use std::process::{self, Command};
use std::{
env,
path::{Path, PathBuf},
};
use redbear_hwutils::parse_args;
@@ -0,0 +1,185 @@
// Phase 5 Hardware GPU preflight check.
// Validates DRM device presence, GPU firmware, and rendering infrastructure.
// Does NOT validate real hardware GPU rendering (requires hardware + CS ioctl).
use std::process;
const PROGRAM: &str = "redbear-phase5-gpu-check";
const USAGE: &str = "Usage: redbear-phase5-gpu-check [--json]\n\n\
Phase 5 hardware GPU preflight check. Validates DRM device registration,\n\
GPU firmware, and Mesa rendering infrastructure. Hardware validation\n\
requires real AMD/Intel GPU + command submission (CS ioctl).";
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
}
}
#[cfg(target_os = "redox")]
struct Check { name: String, result: CheckResult, detail: String }
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
}
fn fail(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
}
fn skip(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
}
}
#[cfg(target_os = "redox")]
struct Report { checks: Vec<Check>, json_mode: bool }
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
fn add(&mut self, check: Check) { self.checks.push(check); }
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
fn print(&self) {
if self.json_mode { self.print_json(); } else { self.print_human(); }
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck { name: String, result: String, detail: String }
#[derive(serde::Serialize)]
struct JsonReport {
drm_device: bool, gpu_firmware: bool, mesa_dri: bool,
display_modes: bool, checks: Vec<JsonCheck>,
}
let drm = self.checks.iter().find(|c| c.name == "DRM_DEVICE").map_or(false, |c| c.result == CheckResult::Pass);
let firmware = self.checks.iter().find(|c| c.name == "GPU_FIRMWARE").map_or(false, |c| c.result == CheckResult::Pass);
let mesa = self.checks.iter().find(|c| c.name == "MESA_DRI").map_or(false, |c| c.result == CheckResult::Pass);
let modes = self.checks.iter().find(|c| c.name == "DISPLAY_MODES").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { drm_device: drm, gpu_firmware: firmware, mesa_dri: mesa, display_modes: modes, checks }) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn check_drm_device() -> Check {
let paths = ["/scheme/drm/card0", "/dev/dri/card0"];
for p in paths {
if std::path::Path::new(p).exists() {
return Check::pass("DRM_DEVICE", p);
}
}
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0 or /dev/dri/card0")
}
#[cfg(target_os = "redox")]
fn check_gpu_firmware() -> Check {
let firmware_dirs = ["/lib/firmware/amdgpu", "/lib/firmware/i915"];
let mut found = false;
for dir in firmware_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
let count = entries.filter_map(|e| e.ok()).count();
if count > 0 {
found = true;
break;
}
}
}
if found {
Check::pass("GPU_FIRMWARE", "GPU firmware blobs present")
} else {
Check::skip("GPU_FIRMWARE", "no GPU firmware found (may need fetch-firmware.sh)")
}
}
#[cfg(target_os = "redox")]
fn check_mesa_dri_hardware() -> Check {
let hw_drivers = ["/usr/lib/dri/radeonsi_dri.so", "/usr/lib/dri/iris_dri.so"];
let mut found = Vec::new();
for d in hw_drivers {
if std::path::Path::new(d).exists() { found.push(d); }
}
if !found.is_empty() {
let names: Vec<_> = found.iter().map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
Check::pass("MESA_DRI", &format!("{} hardware DRI driver(s): {}", found.len(), names.join(", ")))
} else {
Check::fail("MESA_DRI", "no hardware DRI drivers found (llvmpipe software only)")
}
}
#[cfg(target_os = "redox")]
fn check_display_modes() -> Check {
let connector_dir = "/scheme/drm/card0/connectors";
match std::fs::read_dir(connector_dir) {
Ok(entries) => {
let count = entries.filter_map(|e| e.ok()).count();
if count > 0 {
Check::pass("DISPLAY_MODES", &format!("{} connector(s) found", count))
} else {
Check::fail("DISPLAY_MODES", "no connectors found")
}
}
Err(_) => Check::skip("DISPLAY_MODES", "cannot enumerate connectors (may need hardware GPU)")
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
println!("{PROGRAM}: GPU check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
let mut report = Report::new(json_mode);
report.add(check_drm_device());
report.add(check_gpu_firmware());
report.add(check_mesa_dri_hardware());
report.add(check_display_modes());
report.print();
if report.any_failed() { return Err("one or more Phase 5 checks failed".to_string()); }
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() { process::exit(0); }
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
@@ -298,7 +298,11 @@ fn validate_upower(list_names_output: &str) -> Result<(), String> {
println!("UPOWER_RUNTIME_BATTERIES={}", runtime.battery_ids.len());
println!(
"UPOWER_POWER_SURFACE={}",
if power_surface_available { "available" } else { "unavailable" }
if power_surface_available {
"available"
} else {
"unavailable"
}
);
let enumerate_output = run_command_with_retry(
@@ -5,8 +5,7 @@ use redbear_hwutils::parse_args;
use serde_json::Value;
const PROGRAM: &str = "redbear-phase5-wifi-analyze";
const USAGE: &str =
"Usage: redbear-phase5-wifi-analyze <capture.json>\n\nSummarize a Wi-Fi capture bundle into likely blocker categories.";
const USAGE: &str = "Usage: redbear-phase5-wifi-analyze <capture.json>\n\nSummarize a Wi-Fi capture bundle into likely blocker categories.";
fn read_text<'a>(value: &'a Value, path: &[&str]) -> &'a str {
let mut current = value;
@@ -114,8 +114,12 @@ fn run() -> Result<(), String> {
require_contains("redbear_info", &info, "wifi_disconnect_result")?;
println!("PASS: bounded Intel Wi-Fi runtime path exercised inside target runtime");
println!("NOTE: the packaged runtime checker currently validates the bounded open-profile path by default; WPA2-PSK is implemented and host/unit-verified elsewhere in-repo but is not yet the default packaged runtime proof");
println!("NOTE: this still does not prove real AP scan/auth/association, packet flow, DHCP success over Wi-Fi, or validated end-to-end connectivity");
println!(
"NOTE: the packaged runtime checker currently validates the bounded open-profile path by default; WPA2-PSK is implemented and host/unit-verified elsewhere in-repo but is not yet the default packaged runtime proof"
);
println!(
"NOTE: this still does not prove real AP scan/auth/association, packet flow, DHCP success over Wi-Fi, or validated end-to-end connectivity"
);
Ok(())
}
@@ -4,8 +4,7 @@ use std::process;
use redbear_hwutils::parse_args;
const PROGRAM: &str = "redbear-usb-check";
const USAGE: &str =
"Usage: redbear-usb-check\n\nCheck the USB stack inside a Red Bear guest.\n\nWalks the usb scheme tree and reports controller and device status.";
const USAGE: &str = "Usage: redbear-usb-check\n\nCheck the USB stack inside a Red Bear guest.\n\nWalks the usb scheme tree and reports controller and device status.";
fn list_scheme_dir(path: &str) -> Vec<String> {
match fs::read_dir(path) {
@@ -115,7 +115,9 @@ fn parse_pci_id_database(text: &str) -> PciIdDatabase {
let Some(vendor_id) = current_vendor else {
continue;
};
let mut parts = rest.splitn(2, char::is_whitespace).filter(|part| !part.is_empty());
let mut parts = rest
.splitn(2, char::is_whitespace)
.filter(|part| !part.is_empty());
let Some(device_hex) = parts.next() else {
continue;
};
@@ -131,7 +133,9 @@ fn parse_pci_id_database(text: &str) -> PciIdDatabase {
continue;
}
let mut parts = line.splitn(2, char::is_whitespace).filter(|part| !part.is_empty());
let mut parts = line
.splitn(2, char::is_whitespace)
.filter(|part| !part.is_empty());
let Some(vendor_hex) = parts.next() else {
continue;
};
@@ -142,7 +146,9 @@ fn parse_pci_id_database(text: &str) -> PciIdDatabase {
continue;
};
current_vendor = Some(vendor_id);
database.vendor_names.insert(vendor_id, name.trim().to_string());
database
.vendor_names
.insert(vendor_id, name.trim().to_string());
}
database
@@ -237,7 +243,10 @@ mod tests {
#[test]
fn describe_usb_device_empty_manufacturer_filtered() {
assert_eq!(describe_usb_device(Some(""), Some("USB Mouse")), "USB Mouse");
assert_eq!(
describe_usb_device(Some(""), Some("USB Mouse")),
"USB Mouse"
);
}
#[test]
@@ -254,14 +263,22 @@ mod tests {
#[test]
fn parse_args_help_flag_returns_err_empty() {
let result = parse_args("prog", "usage text", vec!["prog".to_string(), "--help".to_string()]);
let result = parse_args(
"prog",
"usage text",
vec!["prog".to_string(), "--help".to_string()],
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "");
}
#[test]
fn parse_args_h_flag_returns_err_empty() {
let result = parse_args("prog", "usage text", vec!["prog".to_string(), "-h".to_string()]);
let result = parse_args(
"prog",
"usage text",
vec!["prog".to_string(), "-h".to_string()],
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "");
}
@@ -275,7 +292,10 @@ mod tests {
);
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("unsupported arguments"), "expected 'unsupported arguments' in: {msg}");
assert!(
msg.contains("unsupported arguments"),
"expected 'unsupported arguments' in: {msg}"
);
}
// --- original pci_id_database tests ---
@@ -24,12 +24,13 @@ const VIRTIO_NET_VENDOR_ID: u16 = 0x1af4;
const VIRTIO_NET_DEVICE_ID: u16 = 0x1000;
const BLUETOOTH_STATUS_FRESHNESS_SECS: u64 = 90;
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OutputMode {
Table,
Json,
Test,
Quirks,
Probe,
Help,
}
@@ -476,6 +477,26 @@ fn run() -> Result<(), String> {
return Ok(());
}
if options.mode == OutputMode::Probe {
let result = Phase1ProbeResult {
evdev_active: probe_evdev_active(),
udev_active: probe_udev_active(),
firmware_active: probe_firmware_active(),
drm_active: probe_drm_active(),
time_active: probe_time_active(),
};
print_probe(&result);
let all_present = result.evdev_active
&& result.udev_active
&& result.firmware_active
&& result.drm_active
&& result.time_active;
if all_present {
return Ok(());
}
return Err("some Phase 1 services are not present".to_string());
}
let report = collect_report(&runtime);
match options.mode {
@@ -483,6 +504,7 @@ fn run() -> Result<(), String> {
OutputMode::Json => print_json(&report),
OutputMode::Test => print_tests(&report, options.verbose),
OutputMode::Quirks => {}
OutputMode::Probe => {}
OutputMode::Help => {}
}
@@ -1093,6 +1115,122 @@ fn collect_quirks(runtime: &Runtime) -> QuirksReport {
}
}
#[derive(Debug)]
struct Phase1ProbeResult {
evdev_active: bool,
udev_active: bool,
firmware_active: bool,
drm_active: bool,
time_active: bool,
}
#[cfg(target_os = "redox")]
fn probe_evdev_active() -> bool {
std::fs::read_dir("/scheme/")
.map(|mut entries| {
entries.any(|entry| {
entry.map_or(false, |entry| {
entry.file_name().to_string_lossy().starts_with("evdev")
})
})
})
.unwrap_or(false)
}
#[cfg(not(target_os = "redox"))]
fn probe_evdev_active() -> bool {
false
}
#[cfg(target_os = "redox")]
fn probe_udev_active() -> bool {
std::fs::read_dir("/scheme/")
.map(|mut entries| {
entries.any(|entry| {
entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "udev")
})
})
.unwrap_or(false)
}
#[cfg(not(target_os = "redox"))]
fn probe_udev_active() -> bool {
false
}
#[cfg(target_os = "redox")]
fn probe_firmware_active() -> bool {
std::fs::read_dir("/scheme/")
.map(|mut entries| {
entries.any(|entry| {
entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "firmware")
})
})
.unwrap_or(false)
}
#[cfg(not(target_os = "redox"))]
fn probe_firmware_active() -> bool {
false
}
#[cfg(target_os = "redox")]
fn probe_drm_active() -> bool {
std::fs::read_dir("/scheme/")
.map(|mut entries| {
entries.any(|entry| {
entry.map_or(false, |entry| entry.file_name().to_string_lossy() == "drm")
})
})
.unwrap_or(false)
}
#[cfg(not(target_os = "redox"))]
fn probe_drm_active() -> bool {
false
}
#[cfg(target_os = "redox")]
fn probe_time_active() -> bool {
std::path::Path::new("/scheme/time").exists()
}
#[cfg(not(target_os = "redox"))]
fn probe_time_active() -> bool {
false
}
fn print_probe(result: &Phase1ProbeResult) {
let mark = |present: bool| if present { "✓ PRESENT" } else { "✗ ABSENT" };
println!("Phase 1 Service Probes:");
println!(" evdevd {}", mark(result.evdev_active));
println!(" udev-shim {}", mark(result.udev_active));
println!(" firmware {}", mark(result.firmware_active));
println!(" drm {}", mark(result.drm_active));
println!(" time {}", mark(result.time_active));
let all = result.evdev_active
&& result.udev_active
&& result.firmware_active
&& result.drm_active
&& result.time_active;
let most = result.evdev_active as u8
+ result.udev_active as u8
+ result.firmware_active as u8
+ result.drm_active as u8
+ result.time_active as u8;
println!();
if all {
println!("ALL PHASE 1 SERVICES PRESENT");
} else if most >= 3 {
println!("MOSTLY PRESENT, SOME GAPS ({}/5)", most);
} else {
println!("SIGNIFICANT GAPS REMAIN ({}/5)", most);
}
}
fn parse_quirk_toml(name: &str, content: &str) -> Result<QuirkFile, String> {
let document: Value = content
.parse()
@@ -1286,6 +1424,9 @@ where
if mode == OutputMode::Quirks {
return Err("cannot combine --json with --quirks".to_string());
}
if mode == OutputMode::Probe {
return Err("cannot combine --json with --probe".to_string());
}
mode = OutputMode::Json;
}
"--test" => {
@@ -1295,6 +1436,9 @@ where
if mode == OutputMode::Quirks {
return Err("cannot combine --test with --quirks".to_string());
}
if mode == OutputMode::Probe {
return Err("cannot combine --test with --probe".to_string());
}
mode = OutputMode::Test;
}
"--quirks" => {
@@ -1304,8 +1448,23 @@ where
if mode == OutputMode::Test {
return Err("cannot combine --quirks with --test".to_string());
}
if mode == OutputMode::Probe {
return Err("cannot combine --quirks with --probe".to_string());
}
mode = OutputMode::Quirks;
}
"--probe" => {
if mode == OutputMode::Json {
return Err("cannot combine --probe with --json".to_string());
}
if mode == OutputMode::Test {
return Err("cannot combine --probe with --test".to_string());
}
if mode == OutputMode::Quirks {
return Err("cannot combine --probe with --quirks".to_string());
}
mode = OutputMode::Probe;
}
"-h" | "--help" => mode = OutputMode::Help,
_ => return Err(format!("unknown argument: {arg}")),
}
@@ -2071,14 +2230,14 @@ fn print_json(report: &Report<'_>) {
}
fn print_help() {
println!("Usage: redbear-info [--verbose|-v] [--json|--test|--quirks]");
println!("Usage: redbear-info [--verbose|-v] [--json|--test|--quirks|--probe]");
println!();
println!("Passive runtime integration report for Red Bear OS.");
println!();
println!("This tool distinguishes:");
println!(" present artifact or config exists");
println!(" active live runtime surface exists");
println!(" functional read-only runtime probe succeeded");
println!(" functional read-only runtime probe succeeded (table/test output; --probe mode uses PRESENT/ABSENT)");
println!();
println!("Connected means the local networking stack has a configured address.");
println!("It does not prove internet reachability.");
@@ -2088,6 +2247,7 @@ fn print_help() {
println!(" --json Print structured JSON");
println!(" --test Print suggested diagnostic commands");
println!(" --quirks Print configured hardware quirk data");
println!(" --probe Probe Phase 1 service liveness (evdevd, udev-shim, firmware-loader, drm, time)");
println!(" -h, --help Show this help message");
}
@@ -3425,6 +3585,120 @@ mod tests {
fs::remove_dir_all(root).unwrap();
}
#[test]
fn parse_args_accepts_probe_mode() {
let options = parse_args([
"redbear-info".to_string(),
"--probe".to_string(),
])
.unwrap();
assert!(matches!(options.mode, OutputMode::Probe));
}
#[test]
fn parse_args_rejects_probe_with_other_output_modes() {
// probe first, then --json: --json is the current arg, error puts current arg first
assert_eq!(
parse_args([
"redbear-info".to_string(),
"--probe".to_string(),
"--json".to_string(),
])
.err(),
Some("cannot combine --json with --probe".to_string())
);
// --test first, then --probe: --probe is the current arg
assert_eq!(
parse_args([
"redbear-info".to_string(),
"--test".to_string(),
"--probe".to_string(),
])
.err(),
Some("cannot combine --probe with --test".to_string())
);
// --quirks first, then --probe: --probe is the current arg
assert_eq!(
parse_args([
"redbear-info".to_string(),
"--quirks".to_string(),
"--probe".to_string(),
])
.err(),
Some("cannot combine --probe with --quirks".to_string())
);
// Reverse direction: --json/--test/--quirks after --probe
assert_eq!(
parse_args([
"redbear-info".to_string(),
"--json".to_string(),
"--probe".to_string(),
])
.err(),
Some("cannot combine --probe with --json".to_string())
);
assert_eq!(
parse_args([
"redbear-info".to_string(),
"--probe".to_string(),
"--test".to_string(),
])
.err(),
Some("cannot combine --test with --probe".to_string())
);
}
#[test]
fn probe_functions_return_false_on_host() {
assert!(!probe_evdev_active());
assert!(!probe_udev_active());
assert!(!probe_firmware_active());
assert!(!probe_drm_active());
assert!(!probe_time_active());
}
#[test]
fn print_probe_outputs_all_present() {
let result = Phase1ProbeResult {
evdev_active: true,
udev_active: true,
firmware_active: true,
drm_active: true,
time_active: true,
};
assert!(result.evdev_active);
assert!(result.udev_active);
assert!(result.firmware_active);
assert!(result.drm_active);
assert!(result.time_active);
let all = result.evdev_active
&& result.udev_active
&& result.firmware_active
&& result.drm_active
&& result.time_active;
assert!(all, "all five services should be present");
}
#[test]
fn print_probe_reports_gaps_count() {
let result = Phase1ProbeResult {
evdev_active: true,
udev_active: true,
firmware_active: false,
drm_active: true,
time_active: false,
};
let count = result.evdev_active as u8
+ result.udev_active as u8
+ result.firmware_active as u8
+ result.drm_active as u8
+ result.time_active as u8;
assert_eq!(count, 3);
assert!(!result.firmware_active);
assert!(!result.time_active);
}
#[test]
fn parse_args_accepts_quirks_mode() {
let options = parse_args([
@@ -0,0 +1,40 @@
#TODO: Runtime proof requires executing these binaries inside a Red Bear OS Phase 1 validation target.
[source]
path = "source"
[build]
template = "custom"
dependencies = ["relibc"]
script = """
DYNAMIC_INIT
RELIBC_STAGE="${COOKBOOK_ROOT}/recipes/core/relibc/target/${TARGET}/stage"
if [ ! -d "${RELIBC_STAGE}/usr" ]; then
RELIBC_STAGE="${COOKBOOK_ROOT}/recipes/core/relibc/target/${TARGET}/stage.tmp"
fi
mkdir -p "${COOKBOOK_SYSROOT}"
if [ -d "${RELIBC_STAGE}/usr" ]; then
rsync -av "${RELIBC_STAGE}/usr/" "${COOKBOOK_SYSROOT}/"
fi
TARGET_CC="${TARGET}-gcc"
if ! command -v "${TARGET_CC}" >/dev/null 2>&1; then
TARGET_CC="cc"
fi
rsync -av "${COOKBOOK_SOURCE}/" ./
make -j "${COOKBOOK_MAKE_JOBS}" \
CC="${CC_WRAPPER} ${TARGET_CC}" \
CPPFLAGS="-I${COOKBOOK_SYSROOT}/include" \
CFLAGS="-O2 -Wall -Wextra -Werror" \
LDFLAGS="--sysroot=${COOKBOOK_SYSROOT} -L${COOKBOOK_SYSROOT}/lib"
mkdir -p "${COOKBOOK_STAGE}/home/user/relibc-phase1-tests"
cp -v test_signalfd_wayland test_timerfd_qt6 test_eventfd_qt6 test_shm_open_qt6 \
test_sem_open_qt6 test_waitid_qt6 "${COOKBOOK_STAGE}/home/user/relibc-phase1-tests/"
"""
[package]
dependencies = ["gcc13"]
@@ -0,0 +1,22 @@
PROGRAMS = \
test_signalfd_wayland \
test_timerfd_qt6 \
test_eventfd_qt6 \
test_shm_open_qt6 \
test_sem_open_qt6 \
test_waitid_qt6
CC ?= cc
CPPFLAGS ?=
CFLAGS ?= -O2 -Wall -Wextra -Werror
LDFLAGS ?=
.PHONY: all clean
all: $(PROGRAMS)
%: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@ $(LDFLAGS)
clean:
rm -f $(PROGRAMS)
Binary file not shown.
@@ -0,0 +1,45 @@
#define _GNU_SOURCE 1
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#ifdef __redox__
#define EFD_CLOEXEC 0x80000
#define EFD_NONBLOCK 0x800
int eventfd(unsigned int initval, int flags);
#else
#include <sys/eventfd.h>
#endif
static int fail_step(const char *step) {
printf("FAIL eventfd: %s (errno=%d)\n", step, errno);
return 1;
}
int main(void) {
uint64_t expected = 42;
uint64_t observed = 0;
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (efd < 0) return fail_step("eventfd");
if (write(efd, &expected, sizeof(expected)) != (ssize_t)sizeof(expected)) return fail_step("write first");
if (read(efd, &observed, sizeof(observed)) != (ssize_t)sizeof(observed)) return fail_step("read first");
if (observed != expected) {
printf("FAIL eventfd: first read=%llu\n", (unsigned long long)observed);
return 1;
}
expected = 7;
if (write(efd, &expected, sizeof(expected)) != (ssize_t)sizeof(expected)) return fail_step("write second");
if (read(efd, &observed, sizeof(observed)) != (ssize_t)sizeof(observed)) return fail_step("read second");
if (observed != expected) {
printf("FAIL eventfd: second read=%llu\n", (unsigned long long)observed);
return 1;
}
if (close(efd) < 0) return fail_step("close");
printf("PASS eventfd\n");
return 0;
}
Binary file not shown.
@@ -0,0 +1,90 @@
#define _GNU_SOURCE 1
#include <errno.h>
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static int fail_step(const char *step) {
printf("FAIL sem_open: %s (errno=%d)\n", step, errno);
return 1;
}
int main(void) {
static const char name[] = "/rb_test_sem";
char go = 'G';
char ready = 0;
int child_value = -1;
int parent_value = -1;
int parent_to_child[2];
int value = -1;
int sync_pipe[2];
int status;
sem_t *sem;
pid_t child;
errno = 0;
if (sem_unlink(name) < 0 && errno != ENOENT) return fail_step("sem_unlink pre-clean");
sem = sem_open(name, O_CREAT, 0666, 0);
if (sem == SEM_FAILED) return fail_step("sem_open create");
if (sem_getvalue(sem, &value) < 0 || value != 0) {
printf("FAIL sem_open: initial value=%d\n", value);
return 1;
}
if (pipe(sync_pipe) < 0) return fail_step("pipe");
if (pipe(parent_to_child) < 0) return fail_step("pipe parent_to_child");
child = fork();
if (child < 0) return fail_step("fork");
if (child == 0) {
ready = 'R';
close(sync_pipe[0]);
close(parent_to_child[1]);
sem_t *child_sem = sem_open(name, 0);
if (child_sem == SEM_FAILED) _Exit(1);
if (write(sync_pipe[1], &ready, 1) != 1) _Exit(2);
if (read(parent_to_child[0], &go, 1) != 1) _Exit(3);
if (sem_wait(child_sem) < 0) _Exit(4);
if (sem_getvalue(child_sem, &child_value) < 0 || child_value != 0) _Exit(5);
if (sem_post(child_sem) < 0) _Exit(6);
if (sem_getvalue(child_sem, &child_value) < 0 || child_value != 1) _Exit(7);
if (sem_close(child_sem) < 0) _Exit(8);
close(parent_to_child[0]);
close(sync_pipe[1]);
_Exit(0);
}
close(sync_pipe[1]);
close(parent_to_child[0]);
if (read(sync_pipe[0], &ready, 1) != 1) return fail_step("child ready read");
if (sem_post(sem) < 0) return fail_step("parent sem_post");
if (sem_getvalue(sem, &parent_value) < 0 || parent_value != 1) {
printf("FAIL sem_open: post value=%d\n", parent_value);
return 1;
}
if (write(parent_to_child[1], &go, 1) != 1) return fail_step("release child");
if (close(parent_to_child[1]) < 0) return fail_step("close parent_to_child");
if (read(sync_pipe[0], &ready, 1) != 0) return fail_step("child completion pipe");
if (sem_getvalue(sem, &parent_value) < 0 || parent_value != 1) {
printf("FAIL sem_open: child post value=%d\n", parent_value);
return 1;
}
close(sync_pipe[0]);
if (sem_wait(sem) < 0) return fail_step("parent sem_wait");
if (waitpid(child, &status, 0) < 0) return fail_step("waitpid");
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
printf("FAIL sem_open: child status=%d\n", status);
return 1;
}
if (sem_getvalue(sem, &value) < 0 || value != 0) {
printf("FAIL sem_open: final value=%d\n", value);
return 1;
}
if (sem_close(sem) < 0 || sem_unlink(name) < 0) return fail_step("cleanup");
printf("PASS sem_open\n");
return 0;
}
Binary file not shown.
@@ -0,0 +1,47 @@
#define _GNU_SOURCE 1
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
static int fail_step(const char *step) {
printf("FAIL shm_open: %s (errno=%d)\n", step, errno);
return 1;
}
int main(void) {
static const char name[] = "/rb_test_shm";
uint32_t *first;
uint32_t *second;
int fd;
int second_fd;
errno = 0;
if (shm_unlink(name) < 0 && errno != ENOENT) return fail_step("shm_unlink pre-clean");
fd = shm_open(name, O_CREAT | O_RDWR, 0666);
if (fd < 0) return fail_step("shm_open");
if (ftruncate(fd, 4096) < 0) return fail_step("ftruncate");
second_fd = shm_open(name, O_RDWR, 0666);
if (second_fd < 0) return fail_step("shm_open reopen");
first = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (first == MAP_FAILED) return fail_step("mmap first");
second = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, second_fd, 0);
if (second == MAP_FAILED) return fail_step("mmap second");
*first = 0xDEADBEEFU;
if (*second != 0xDEADBEEFU) {
printf("FAIL shm_open: observed=0x%08X\n", *second);
return 1;
}
if (munmap(second, 4096) < 0) return fail_step("munmap second");
if (munmap(first, 4096) < 0) return fail_step("munmap first");
if (close(second_fd) < 0) return fail_step("close second");
if (close(fd) < 0) return fail_step("close");
if (shm_unlink(name) < 0) return fail_step("shm_unlink");
printf("PASS shm_open\n");
return 0;
}
Binary file not shown.
@@ -0,0 +1,91 @@
#define _GNU_SOURCE 1
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef __redox__
struct signalfd_siginfo {
uint32_t ssi_signo;
int32_t ssi_errno;
int32_t ssi_code;
uint32_t ssi_pid;
uint32_t ssi_uid;
int32_t ssi_fd;
uint32_t ssi_tid;
uint32_t ssi_band;
uint32_t ssi_overrun;
uint32_t ssi_trapno;
int32_t ssi_status;
int32_t ssi_int;
uint64_t ssi_ptr;
uint64_t ssi_utime;
uint64_t ssi_stime;
uint64_t ssi_addr;
uint16_t ssi_addr_lsb, __pad2;
int32_t ssi_syscall;
uint64_t ssi_call_addr;
uint32_t ssi_arch;
unsigned char __pad[28];
};
int signalfd(int fd, const sigset_t *mask, size_t masksize);
_Static_assert(sizeof(struct signalfd_siginfo) == 128, "unexpected signalfd_siginfo size");
#else
#include <sys/signalfd.h>
#endif
static int fail_step(const char *step) {
printf("FAIL signalfd: %s (errno=%d)\n", step, errno);
return 1;
}
#ifdef __redox__
#define RB_SIGNALFD(fd, mask) signalfd((fd), (mask), sizeof(*(mask)))
#else
#define RB_SIGNALFD(fd, mask) signalfd((fd), (mask), 0)
#endif
int main(void) {
sigset_t mask;
sigset_t oldmask;
struct signalfd_siginfo info;
int sfd;
int status;
pid_t child;
if (sigemptyset(&mask) < 0 || sigaddset(&mask, SIGUSR1) < 0) return fail_step("sigset setup");
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) < 0) return fail_step("sigprocmask block");
sfd = RB_SIGNALFD(-1, &mask);
if (sfd < 0) return fail_step("signalfd");
child = fork();
if (child < 0) return fail_step("fork");
if (child == 0) {
_Exit(kill(getppid(), SIGUSR1) < 0);
}
if (read(sfd, &info, sizeof(info)) != (ssize_t)sizeof(info)) return fail_step("read");
if (waitpid(child, &status, 0) < 0) return fail_step("waitpid");
if (close(sfd) < 0) return fail_step("close");
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) return fail_step("sigprocmask restore");
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
printf("FAIL signalfd: child status=%d\n", status);
return 1;
}
if (info.ssi_signo != (uint32_t)SIGUSR1) {
printf("FAIL signalfd: ssi_signo=%u\n", info.ssi_signo);
return 1;
}
if (info.ssi_code != SI_USER) {
printf("FAIL signalfd: ssi_code=%d\n", info.ssi_code);
return 1;
}
printf("PASS signalfd\n");
return 0;
}
Binary file not shown.
@@ -0,0 +1,48 @@
#define _GNU_SOURCE 1
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#ifdef __redox__
#define TFD_NONBLOCK 0x800
int timerfd_create(clockid_t clockid, int flags);
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
#else
#include <sys/timerfd.h>
#endif
static int fail_step(const char *step) {
printf("FAIL timerfd: %s (errno=%d)\n", step, errno);
return 1;
}
int main(void) {
const struct timespec pause = {.tv_sec = 0, .tv_nsec = 20000000};
struct itimerspec spec = {{0, 0}, {0, 100000000}};
uint64_t expirations = 0;
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
if (tfd < 0) return fail_step("timerfd_create");
if (timerfd_settime(tfd, 0, &spec, NULL) < 0) return fail_step("timerfd_settime");
for (int i = 0; i < 50; ++i) {
ssize_t n = read(tfd, &expirations, sizeof(expirations));
if (n == (ssize_t)sizeof(expirations)) {
if (expirations >= 1) {
if (close(tfd) < 0) return fail_step("close");
printf("PASS timerfd\n");
return 0;
}
printf("FAIL timerfd: expirations=%llu\n", (unsigned long long)expirations);
return 1;
}
if (n >= 0 || (errno != EAGAIN && errno != EWOULDBLOCK)) return fail_step("read");
nanosleep(&pause, NULL);
}
printf("FAIL timerfd: timeout waiting for expiration\n");
return 1;
}
Binary file not shown.
@@ -0,0 +1,33 @@
#define _GNU_SOURCE 1
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static int fail_step(const char *step) {
printf("FAIL waitid: %s (errno=%d)\n", step, errno);
return 1;
}
int main(void) {
siginfo_t info = {0};
pid_t child = fork();
if (child < 0) return fail_step("fork");
if (child == 0) _Exit(42);
if (waitid(P_PID, child, &info, WEXITED) < 0) return fail_step("waitid");
if (info.si_code != CLD_EXITED) {
printf("FAIL waitid: si_code=%d\n", info.si_code);
return 1;
}
if (info.si_status != 42) {
printf("FAIL waitid: si_status=%d\n", info.si_status);
return 1;
}
printf("PASS waitid\n");
return 0;
}
@@ -1,8 +1,13 @@
// Red Bear Wayland Compositor — a real Wayland display server for the Qt6 greeter UI.
// Replaces the KWin stub that previously created a placeholder socket with no actual compositing.
// Red Bear Wayland Compositor — bounded Wayland compositor proof scaffold.
// Replaces the KWin stub that previously created a placeholder socket.
//
// Architecture: creates a Wayland Unix socket, speaks the core Wayland wire protocol,
// accepts client SHM buffers, and composites them directly onto the vesad framebuffer.
// Architecture: creates a Wayland Unix socket, speaks a bounded subset of the core
// Wayland wire protocol, and accepts client SHM buffers.
//
// NOTE: This is a bounded proof scaffold, not a real compositor runtime proof.
// Known limitations: framebuffer compositing uses private heap memory (not real
// vesad), SHM fd passing uses payload bytes (not Unix SCM_RIGHTS), wire encoding
// uses NUL-terminated strings (not padded Wayland format).
//
// Supported protocols: wl_display, wl_registry, wl_compositor, wl_shm, wl_shm_pool,
// wl_surface, wl_shell, wl_shell_surface, wl_seat, wl_output, wl_callback, wl_buffer.
@@ -20,6 +25,8 @@ fn map_framebuffer(_phys: usize, size: usize) -> Vec<u8> {
const WL_DISPLAY_SYNC: u16 = 0;
const WL_DISPLAY_GET_REGISTRY: u16 = 1;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_DISPLAY_ERROR: u16 = 0;
const WL_DISPLAY_DELETE_ID: u16 = 2;
@@ -27,12 +34,16 @@ const WL_REGISTRY_BIND: u16 = 0;
const WL_REGISTRY_GLOBAL: u16 = 0;
const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_COMPOSITOR_CREATE_REGION: u16 = 1;
const WL_SHM_CREATE_POOL: u16 = 0;
const WL_SHM_FORMAT: u16 = 0;
const WL_SHM_POOL_CREATE_BUFFER: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SHM_POOL_RESIZE: u16 = 1;
const WL_BUFFER_RELEASE: u16 = 0;
@@ -40,14 +51,22 @@ const WL_BUFFER_RELEASE: u16 = 0;
const WL_SURFACE_ATTACH: u16 = 0;
const WL_SURFACE_DAMAGE: u16 = 1;
const WL_SURFACE_COMMIT: u16 = 5;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SURFACE_ENTER: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SURFACE_LEAVE: u16 = 1;
const WL_SHELL_GET_SHELL_SURFACE: u16 = 0;
const WL_SHELL_SURFACE_PONG: u16 = 0;
const WL_SHELL_SURFACE_SET_TOPLEVEL: u16 = 2;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SHELL_SURFACE_PING: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SHELL_SURFACE_CONFIGURE: u16 = 1;
const XDG_WM_BASE_DESTROY: u16 = 0;
@@ -65,9 +84,17 @@ const WL_SEAT_GET_POINTER: u16 = 0;
const WL_SEAT_GET_KEYBOARD: u16 = 1;
const WL_SEAT_CAPABILITIES: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_KEYMAP: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_ENTER: u16 = 1;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_LEAVE: u16 = 2;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_KEY: u16 = 3;
const WL_OUTPUT_GEOMETRY: u16 = 0;
@@ -78,6 +105,23 @@ const WL_CALLBACK_DONE: u16 = 0;
const WL_SHM_FORMAT_XRGB8888: u32 = 1;
const WL_SHM_FORMAT_ARGB8888: u32 = 0;
const OBJECT_TYPE_WL_DISPLAY: u32 = 1;
const OBJECT_TYPE_WL_REGISTRY: u32 = 2;
const OBJECT_TYPE_WL_COMPOSITOR: u32 = 3;
const OBJECT_TYPE_WL_SHM: u32 = 4;
const OBJECT_TYPE_WL_SHELL: u32 = 5;
const OBJECT_TYPE_WL_SEAT: u32 = 6;
const OBJECT_TYPE_WL_OUTPUT: u32 = 7;
const OBJECT_TYPE_XDG_WM_BASE: u32 = 8;
const OBJECT_TYPE_WL_SURFACE: u32 = 9;
const OBJECT_TYPE_WL_BUFFER: u32 = 10;
const OBJECT_TYPE_WL_SHELL_SURFACE: u32 = 11;
const OBJECT_TYPE_XDG_SURFACE: u32 = 12;
const OBJECT_TYPE_XDG_TOPLEVEL: u32 = 13;
const OBJECT_TYPE_WL_SHM_POOL: u32 = 14;
const OBJECT_TYPE_WL_POINTER: u32 = 15;
const OBJECT_TYPE_WL_KEYBOARD: u32 = 16;
struct Global {
name: u32,
interface: String,
@@ -86,7 +130,7 @@ struct Global {
struct ShmPool {
data: &'static mut [u8],
size: usize,
_size: usize,
}
#[derive(Clone)]
@@ -96,7 +140,7 @@ struct Buffer {
width: u32,
height: u32,
stride: u32,
format: u32,
_format: u32,
}
struct Surface {
@@ -104,8 +148,8 @@ struct Surface {
committed_buffer_id: Option<u32>,
x: u32,
y: u32,
width: u32,
height: u32,
_width: u32,
_height: u32,
}
struct ClientState {
@@ -113,7 +157,7 @@ struct ClientState {
surfaces: HashMap<u32, Surface>,
buffers: HashMap<u32, (u32, Buffer)>,
shm_pools: HashMap<u32, ShmPool>,
next_id: u32,
_next_id: u32,
}
pub struct Compositor {
@@ -181,16 +225,15 @@ impl Compositor {
);
for stream in self.listener.incoming() {
match stream {
Ok(mut stream) => {
Ok(stream) => {
let client_id = self.alloc_id();
eprintln!("redbear-compositor: client {} connected", client_id);
self.send_globals(client_id, &mut stream);
self.clients.lock().unwrap().insert(client_id, ClientState {
objects: HashMap::new(),
surfaces: HashMap::new(),
buffers: HashMap::new(),
shm_pools: HashMap::new(),
next_id: 1,
_next_id: 1,
});
self.handle_client(client_id, stream);
}
@@ -200,15 +243,14 @@ impl Compositor {
Ok(())
}
fn send_globals(&self, _client_id: u32, stream: &mut UnixStream) {
// Advertise each global interface to the client
let display_id = 1u32; // wl_display id
fn send_globals(&self, stream: &mut UnixStream, registry_id: u32) {
// Advertise each global interface on the wl_registry object after get_registry.
for global in &self.globals {
let name = global.name;
let iface = global.interface.as_bytes();
let version = global.version;
let mut msg = Vec::with_capacity(16 + iface.len() + 1);
msg.extend_from_slice(&display_id.to_ne_bytes());
msg.extend_from_slice(&registry_id.to_ne_bytes());
let size = 8 + 4 + 4 + iface.len() as u16 + 1;
let header = (size as u32) << 16 | WL_REGISTRY_GLOBAL as u32;
msg.extend_from_slice(&header.to_ne_bytes());
@@ -256,6 +298,7 @@ impl Compositor {
let mut offset = 0;
while offset + 8 <= data.len() {
let object_id = u32::from_ne_bytes([data[offset], data[offset+1], data[offset+2], data[offset+3]]);
// Wayland wire format: [object_id:u32][size:u16][opcode:u16]
let size_opcode = u32::from_ne_bytes([data[offset+4], data[offset+5], data[offset+6], data[offset+7]]);
let msg_size = ((size_opcode >> 16) & 0xFFFF) as usize;
let opcode = (size_opcode & 0xFFFF) as u16;
@@ -265,220 +308,329 @@ impl Compositor {
}
let payload = &data[offset+8..offset+msg_size];
let object_type = if object_id == 1 {
OBJECT_TYPE_WL_DISPLAY
} else {
self.clients
.lock()
.unwrap()
.get(&client_id)
.and_then(|client| client.objects.get(&object_id).copied())
.unwrap_or(0)
};
match opcode {
WL_DISPLAY_SYNC => {
let callback_id = if payload.len() >= 4 {
u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]])
} else { self.alloc_id() };
self.send_callback_done(stream, callback_id, 0);
}
WL_DISPLAY_DELETE_ID => {
if payload.len() >= 4 {
let obj_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.remove(&obj_id);
client.surfaces.remove(&obj_id);
client.buffers.remove(&obj_id);
client.shm_pools.remove(&obj_id);
match object_type {
OBJECT_TYPE_WL_DISPLAY => match opcode {
WL_DISPLAY_SYNC => {
let callback_id = if payload.len() >= 4 {
u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]])
} else { self.alloc_id() };
self.send_callback_done(stream, callback_id, 0);
}
WL_DISPLAY_DELETE_ID => {
if payload.len() >= 4 {
let obj_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.remove(&obj_id);
client.surfaces.remove(&obj_id);
client.buffers.remove(&obj_id);
client.shm_pools.remove(&obj_id);
}
}
}
}
WL_DISPLAY_GET_REGISTRY => {
if payload.len() >= 4 {
let registry_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(registry_id, 2); // wl_registry
WL_DISPLAY_GET_REGISTRY => {
if payload.len() >= 4 {
let registry_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
let mut send_globals = false;
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(registry_id, OBJECT_TYPE_WL_REGISTRY);
send_globals = true;
}
drop(clients);
if send_globals {
self.send_globals(stream, registry_id);
}
}
}
}
WL_REGISTRY_BIND => {
if payload.len() >= 12 {
let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let iface_bytes = &payload[4..];
let null_pos = iface_bytes.iter().position(|&b| b == 0).unwrap_or(iface_bytes.len());
let iface = std::str::from_utf8(&iface_bytes[..null_pos]).unwrap_or("");
let version_offset = 4 + null_pos + 1;
let new_id = if payload.len() >= version_offset + 4 {
u32::from_ne_bytes([payload[version_offset], payload[version_offset+1], payload[version_offset+2], payload[version_offset+3]])
} else { continue; };
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_REGISTRY => match opcode {
WL_REGISTRY_BIND => {
if payload.len() >= 12 {
let _name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let iface_bytes = &payload[4..];
let null_pos = iface_bytes.iter().position(|&b| b == 0).unwrap_or(iface_bytes.len());
let iface = std::str::from_utf8(&iface_bytes[..null_pos]).unwrap_or("");
let version_offset = 4 + null_pos + 1;
let new_id = if payload.len() >= version_offset + 4 {
u32::from_ne_bytes([payload[version_offset], payload[version_offset+1], payload[version_offset+2], payload[version_offset+3]])
} else { continue; };
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
let type_id = match iface {
"wl_compositor" => 3,
"wl_shm" => 4,
"wl_shell" => 5,
"wl_seat" => 6,
"wl_output" => 7,
"xdg_wm_base" => 8,
_ => 0,
};
client.objects.insert(new_id, type_id);
if iface == "wl_shm" {
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888);
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888);
}
if iface == "wl_output" {
self.send_output_info(stream, new_id);
}
if iface == "wl_seat" {
self.send_seat_capabilities(stream, new_id);
}
}
}
}
WL_COMPOSITOR_CREATE_SURFACE => {
if payload.len() >= 4 {
let surface_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(surface_id, 8);
client.surfaces.insert(surface_id, Surface {
buffer: None,
committed_buffer_id: None,
x: 0, y: 0,
width: self.fb_width,
height: self.fb_height,
});
}
}
}
WL_SHM_CREATE_POOL => {
if payload.len() >= 8 {
let pool_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let fd_val = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
let size = if payload.len() >= 12 {
u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]) as usize
} else { 0 };
if fd_val >= 0 && size > 0 {
use std::os::fd::FromRawFd;
use std::io::Seek;
let mut file = unsafe { std::fs::File::from_raw_fd(fd_val) };
let mut data = vec![0u8; size];
if file.seek(std::io::SeekFrom::Start(0)).is_ok()
&& file.read_exact(&mut data).is_ok()
{
let boxed = data.into_boxed_slice();
let leaked = Box::leak(boxed);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.shm_pools.insert(pool_id, ShmPool { data: leaked, size });
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
let type_id = match iface {
"wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR,
"wl_shm" => OBJECT_TYPE_WL_SHM,
"wl_shell" => OBJECT_TYPE_WL_SHELL,
"wl_seat" => OBJECT_TYPE_WL_SEAT,
"wl_output" => OBJECT_TYPE_WL_OUTPUT,
"xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE,
_ => 0,
};
client.objects.insert(new_id, type_id);
if iface == "wl_shm" {
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888);
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888);
}
if iface == "wl_output" {
self.send_output_info(stream, new_id);
}
if iface == "wl_seat" {
self.send_seat_capabilities(stream, new_id);
}
}
}
}
}
WL_SHM_POOL_CREATE_BUFFER => {
if payload.len() >= 20 {
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let offset = u32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
let width = u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
let height = u32::from_ne_bytes([payload[12], payload[13], payload[14], payload[15]]);
let stride = u32::from_ne_bytes([payload[16], payload[17], payload[18], payload[19]]);
let format = if payload.len() >= 24 {
u32::from_ne_bytes([payload[20], payload[21], payload[22], payload[23]])
} else { WL_SHM_FORMAT_ARGB8888 };
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(buffer_id, 9); // wl_buffer
client.buffers.insert(buffer_id, (object_id, Buffer {
pool_id: object_id,
offset, width, height, stride, format,
}));
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
}
WL_SURFACE_ATTACH => {
if payload.len() >= 12 {
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let _x = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
let _y = i32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
if let Some((pool_id, buffer)) = client.buffers.get(&buffer_id).cloned() {
if let Some(surface) = client.surfaces.get_mut(&object_id) {
surface.buffer = Some(Buffer {
pool_id,
..buffer
});
}
}
}
}
}
WL_SURFACE_COMMIT => {
let (release_id, buffer_data) = {
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
if let Some(surface) = client.surfaces.get_mut(&object_id) {
let old_buffer = surface.committed_buffer_id.take();
surface.committed_buffer_id = surface.buffer.as_ref().map(|b| {
client.buffers.iter()
.find(|(_, (_, buf))| buf.offset == b.offset && buf.width == b.width)
.map(|(id, _)| *id)
.unwrap_or(0)
},
OBJECT_TYPE_WL_COMPOSITOR => match opcode {
WL_COMPOSITOR_CREATE_SURFACE => {
if payload.len() >= 4 {
let surface_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(surface_id, OBJECT_TYPE_WL_SURFACE);
client.surfaces.insert(surface_id, Surface {
buffer: None,
committed_buffer_id: None,
x: 0, y: 0,
_width: self.fb_width,
_height: self.fb_height,
});
if let Some(ref buffer) = surface.buffer {
if let Some(pool) = client.shm_pools.get(&buffer.pool_id) {
self.composite_buffer(pool, buffer, surface);
}
}
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_SHM => match opcode {
WL_SHM_CREATE_POOL => {
if payload.len() >= 8 {
let pool_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let fd_val = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
let size = if payload.len() >= 12 {
u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]) as usize
} else { 0 };
if fd_val >= 0 && size > 0 {
use std::io::Seek;
use std::os::fd::FromRawFd;
let mut file = unsafe { std::fs::File::from_raw_fd(fd_val) };
let mut data = vec![0u8; size];
if file.seek(std::io::SeekFrom::Start(0)).is_ok()
&& file.read_exact(&mut data).is_ok()
{
let boxed = data.into_boxed_slice();
let leaked = Box::leak(boxed);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(pool_id, OBJECT_TYPE_WL_SHM_POOL);
client.shm_pools.insert(pool_id, ShmPool { data: leaked, _size: size });
}
}
(old_buffer, surface.buffer.is_some())
} else { (None, false) }
} else { (None, false) }
};
if let Some(buf_id) = release_id {
if buf_id != 0 {
self.send_buffer_release(stream, buf_id);
}
}
}
}
WL_SHELL_GET_SHELL_SURFACE | WL_SEAT_GET_KEYBOARD | WL_SEAT_GET_POINTER => {
// Ack new object creation — just register the id
if payload.len() >= 4 {
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_SHM_POOL => match opcode {
WL_SHM_POOL_CREATE_BUFFER => {
if payload.len() >= 20 {
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let offset = u32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
let width = u32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
let height = u32::from_ne_bytes([payload[12], payload[13], payload[14], payload[15]]);
let stride = u32::from_ne_bytes([payload[16], payload[17], payload[18], payload[19]]);
let format = if payload.len() >= 24 {
u32::from_ne_bytes([payload[20], payload[21], payload[22], payload[23]])
} else { WL_SHM_FORMAT_ARGB8888 };
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(buffer_id, OBJECT_TYPE_WL_BUFFER);
client.buffers.insert(buffer_id, (object_id, Buffer {
pool_id: object_id,
offset,
width,
height,
stride,
_format: format,
}));
}
}
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_SURFACE => match opcode {
WL_SURFACE_ATTACH => {
if payload.len() >= 12 {
let buffer_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let _x = i32::from_ne_bytes([payload[4], payload[5], payload[6], payload[7]]);
let _y = i32::from_ne_bytes([payload[8], payload[9], payload[10], payload[11]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
if let Some((pool_id, buffer)) = client.buffers.get(&buffer_id).cloned() {
if let Some(surface) = client.surfaces.get_mut(&object_id) {
surface.buffer = Some(Buffer {
pool_id,
..buffer
});
}
}
}
}
}
WL_SURFACE_COMMIT => {
let release_id = {
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
if let Some(surface) = client.surfaces.get_mut(&object_id) {
let old_buffer = surface.committed_buffer_id.take();
surface.committed_buffer_id = surface.buffer.as_ref().map(|b| {
client.buffers.iter()
.find(|(_, (_, buf))| buf.offset == b.offset && buf.width == b.width)
.map(|(id, _)| *id)
.unwrap_or(0)
});
if let Some(ref buffer) = surface.buffer {
if let Some(pool) = client.shm_pools.get(&buffer.pool_id) {
self.composite_buffer(pool, buffer, surface);
}
}
old_buffer
} else { None }
} else { None }
};
if let Some(buf_id) = release_id {
if buf_id != 0 {
self.send_buffer_release(stream, buf_id);
}
}
}
WL_SURFACE_DAMAGE => {
// No-op — we don't need damage tracking for a single-client greeter.
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_SHELL => match opcode {
WL_SHELL_GET_SHELL_SURFACE => {
if payload.len() >= 4 {
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, OBJECT_TYPE_WL_SHELL_SURFACE);
}
}
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_SHELL_SURFACE => match opcode {
WL_SHELL_SURFACE_SET_TOPLEVEL | WL_SHELL_SURFACE_PONG => {
// No-op — we don't need window management for a single-client greeter.
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_SEAT => match opcode {
WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => {
if payload.len() >= 4 {
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let object_type = match opcode {
WL_SEAT_GET_POINTER => OBJECT_TYPE_WL_POINTER,
WL_SEAT_GET_KEYBOARD => OBJECT_TYPE_WL_KEYBOARD,
_ => unreachable!(),
};
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, object_type);
}
}
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_XDG_WM_BASE => match opcode {
XDG_WM_BASE_GET_XDG_SURFACE => {
if payload.len() >= 4 {
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, OBJECT_TYPE_XDG_SURFACE);
}
}
}
XDG_WM_BASE_DESTROY | XDG_WM_BASE_PONG => {
// No-op — the greeter keeps the shell global alive for the client lifetime.
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_XDG_SURFACE => match opcode {
XDG_SURFACE_DESTROY => {
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, 10);
client.objects.remove(&object_id);
}
}
}
WL_SHELL_SURFACE_SET_TOPLEVEL | WL_SHELL_SURFACE_PONG | WL_SURFACE_DAMAGE => {
// No-op — we don't need window management for a single-client greeter
}
XDG_WM_BASE_GET_XDG_SURFACE | XDG_WM_BASE_DESTROY | XDG_WM_BASE_PONG => {
if payload.len() >= 4 {
let new_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, 11);
XDG_SURFACE_GET_TOPLEVEL => {
if payload.len() >= 4 {
let toplevel_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL);
}
drop(clients);
self.send_xdg_surface_configure(stream, object_id);
self.send_xdg_toplevel_configure(stream, toplevel_id);
}
}
}
XDG_SURFACE_GET_TOPLEVEL | XDG_SURFACE_DESTROY => {
if payload.len() >= 4 {
let toplevel_id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(toplevel_id, 12);
}
drop(clients);
self.send_xdg_surface_configure(stream, object_id);
self.send_xdg_toplevel_configure(stream, toplevel_id);
XDG_SURFACE_ACK_CONFIGURE => {
// Client acknowledged — ready for first commit.
}
}
XDG_SURFACE_ACK_CONFIGURE => {
// Client acknowledged — ready for first commit
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
},
OBJECT_TYPE_WL_OUTPUT
| OBJECT_TYPE_WL_BUFFER
| OBJECT_TYPE_XDG_TOPLEVEL
| OBJECT_TYPE_WL_POINTER
| OBJECT_TYPE_WL_KEYBOARD => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
}
_ => {
eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id);
eprintln!("redbear-compositor: unhandled object {} opcode {}", object_id, opcode);
}
}
@@ -621,8 +773,6 @@ fn main() {
let fb_phys = usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16)
.unwrap_or(0x80000000);
let fb_size = (fb_height as usize) * (fb_stride as usize);
eprintln!(
"redbear-compositor: fb {}x{} stride {} phys 0x{:X}",
fb_width, fb_height, fb_stride, fb_phys
@@ -109,13 +109,13 @@ fn test_compositor_globals() {
let mut client = WaylandClient::connect(socket).expect("failed to connect");
// Get registry
let registry = client.get_registry().expect("get_registry failed");
let _registry = client.get_registry().expect("get_registry failed");
// Read global events
let mut globals = Vec::new();
for _ in 0..6 {
match client.read_message() {
Ok((obj_id, opcode, payload)) => {
Ok((_obj_id, opcode, payload)) => {
assert_eq!(opcode, 0); // wl_registry.global
let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
let iface_end = payload[4..].iter().position(|&b| b == 0).unwrap_or(0);