feat: recipe durability guard — prevents build system from deleting local recipes

Add guard-recipes.sh with four modes:
- --verify: check all local/recipes have correct symlinks into recipes/
- --fix: repair broken symlinks (run before builds)
- --save-all: snapshot all recipe.toml into local/recipes/
- --restore: recreate all symlinks from local/recipes/ (run after sync-upstream)

Wired into apply-patches.sh (post-patch) and sync-upstream.sh (post-sync).
This prevents the build system from deleting recipe files during
cargo cook, make distclean, or upstream source refresh.
This commit is contained in:
2026-04-30 18:47:03 +01:00
parent 34360e1e4f
commit 7c7399e0a6
126 changed files with 13145 additions and 178 deletions
@@ -0,0 +1,37 @@
use alloc::vec::Vec;
use crate::device::DeviceInfo;
#[cfg(feature = "hotplug")]
use crate::hotplug::HotplugSubscription;
/// A hardware bus that can enumerate devices.
pub trait Bus: Send + Sync {
/// Returns a human-readable bus name such as `"pci"`, `"usb"`, or `"acpi"`.
fn name(&self) -> &str;
/// Enumerates all devices currently visible on this bus.
///
/// Implementations must be safe to call repeatedly so that the manager can perform
/// re-scans after topology changes or deferred-probe retries.
fn enumerate_devices(&self) -> Result<Vec<DeviceInfo>, BusError>;
/// Subscribes to bus hotplug notifications.
///
/// The returned subscription is intentionally opaque so concrete bus implementations can
/// map it to a file descriptor, channel, or other event source.
#[cfg(feature = "hotplug")]
fn subscribe_hotplug(&self) -> Result<HotplugSubscription, BusError>;
}
/// Errors produced by a [`Bus`] implementation.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BusError {
/// The bus has not finished initializing and cannot currently enumerate devices.
NotReady,
/// A transport or I/O failure occurred while talking to the bus.
IoError,
/// The requested capability is not supported by this bus implementation.
Unsupported,
/// An implementation-specific static error message.
Other(&'static str),
}
@@ -0,0 +1,58 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
/// Unique identifier for a device on a specific bus.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DeviceId {
/// The bus namespace for this device, such as `"pci"` or `"usb"`.
pub bus: String,
/// The bus-local path for the device, such as `"0000:00:02.0"` or `"1-2"`.
pub path: String,
}
/// Information about a discovered device, used for driver matching and probing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeviceInfo {
/// Stable device identifier within the manager.
pub id: DeviceId,
/// Optional vendor identifier reported by the bus or firmware.
pub vendor: Option<u16>,
/// Optional device identifier reported by the bus or firmware.
pub device: Option<u16>,
/// Optional base class code.
pub class: Option<u8>,
/// Optional subclass code.
pub subclass: Option<u8>,
/// Optional programming-interface code.
pub prog_if: Option<u8>,
/// Optional hardware revision code.
pub revision: Option<u8>,
/// Optional subsystem vendor identifier.
pub subsystem_vendor: Option<u16>,
/// Optional subsystem device identifier.
pub subsystem_device: Option<u16>,
/// Raw bus-specific device handle for detailed access.
pub raw_path: String,
/// Optional human-readable description provided by firmware or the bus layer.
pub description: Option<String>,
}
/// Generic interface for an owned device handle.
pub trait Device: Send + Sync {
/// Returns the stable identifier for this device.
fn id(&self) -> &DeviceId;
/// Returns the immutable descriptor used for matching and lifecycle actions.
fn info(&self) -> &DeviceInfo;
}
/// A device that has been successfully matched and bound to a driver.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoundDevice {
/// Static information captured at discovery time.
pub info: DeviceInfo,
/// The name of the driver that currently owns the device.
pub driver_name: String,
/// Key-value parameters associated with the active binding.
pub parameters: BTreeMap<String, String>,
}
@@ -0,0 +1,110 @@
use alloc::string::String;
use crate::device::DeviceInfo;
use crate::params::DriverParams;
use crate::r#match::DriverMatch;
/// Result of a driver probe attempt.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProbeResult {
/// The driver successfully bound to the device.
Bound,
/// The device is not supported by this driver and other drivers may still try.
NotSupported,
/// A dependency is not yet available, so the manager should retry the probe later.
Deferred {
/// Human-readable reason for the deferral.
reason: String,
},
/// The device cannot be driven successfully by this driver.
Fatal {
/// Human-readable explanation of the failure.
reason: String,
},
}
/// Errors returned by driver lifecycle operations after a device has been matched.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DriverError {
/// The operation requires a resource that is not ready yet.
NotReady,
/// The driver encountered an I/O failure while managing the device.
IoError,
/// The requested lifecycle operation is not supported by this driver.
Unsupported,
/// An implementation-specific static error message.
Other(&'static str),
}
/// A device driver that can bind to and manage devices.
pub trait Driver: Send + Sync {
/// Returns the unique driver name, such as `"nvmed"` or `"e1000d"`.
fn name(&self) -> &str;
/// Returns a human-readable description of the driver.
fn description(&self) -> &str;
/// Returns the probe priority for this driver.
///
/// Higher numbers are probed first. Storage drivers typically use higher priorities than
/// networking or peripheral drivers so boot-critical hardware claims happen early.
fn priority(&self) -> i32 {
0
}
/// Returns the driver's static match table.
fn match_table(&self) -> &[DriverMatch];
/// Probes a candidate device and decides whether the driver should take ownership.
fn probe(&self, info: &DeviceInfo) -> ProbeResult;
/// Detaches the driver from a previously bound device.
fn remove(&self, info: &DeviceInfo) -> Result<(), DriverError>;
/// Suspends a bound device.
fn suspend(&self, info: &DeviceInfo) -> Result<(), DriverError> {
let _ = info;
Ok(())
}
/// Resumes a previously suspended device.
fn resume(&self, info: &DeviceInfo) -> Result<(), DriverError> {
let _ = info;
Ok(())
}
/// Returns the driver's parameter definitions and current values.
fn params(&self) -> DriverParams {
DriverParams::default()
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use super::ProbeResult;
#[test]
fn probe_result_variants_preserve_payloads() {
let bound = ProbeResult::Bound;
let not_supported = ProbeResult::NotSupported;
let deferred = ProbeResult::Deferred {
reason: String::from("waiting for scheme"),
};
let fatal = ProbeResult::Fatal {
reason: String::from("device is wedged"),
};
assert!(matches!(bound, ProbeResult::Bound));
assert!(matches!(not_supported, ProbeResult::NotSupported));
assert!(matches!(
deferred,
ProbeResult::Deferred { reason } if reason == "waiting for scheme"
));
assert!(matches!(
fatal,
ProbeResult::Fatal { reason } if reason == "device is wedged"
));
}
}
@@ -0,0 +1,47 @@
use alloc::string::String;
use hashbrown::HashMap;
use crate::device::DeviceId;
#[cfg(feature = "hotplug")]
use crate::device::DeviceInfo;
/// A normalized action associated with a userspace device event.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UeventAction {
/// A device or logical function was added.
Add,
/// A device or logical function was removed.
Remove,
/// A device changed state or metadata.
Change,
/// A driver or subsystem bound to the device.
Bind,
/// A driver or subsystem detached from the device.
Unbind,
}
/// Bus-agnostic metadata describing a userspace device event.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Uevent {
/// Event action, normalized across bus implementations.
pub action: UeventAction,
/// Stable device identifier associated with the event.
pub device: DeviceId,
/// Bus-specific key-value metadata that accompanied the event.
pub properties: HashMap<String, String>,
}
/// Opaque subscription handle for receiving hotplug notifications.
#[cfg(feature = "hotplug")]
pub type HotplugSubscription = usize;
/// High-level hotplug event delivered by a bus implementation.
#[cfg(feature = "hotplug")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HotplugEvent {
/// A device appeared on the bus and is ready for probing.
DeviceAdded(DeviceInfo),
/// A device disappeared from the bus.
DeviceRemoved(DeviceId),
}
@@ -0,0 +1,32 @@
#![cfg_attr(not(feature = "std"), no_std)]
#![doc = "Core device-model traits and orchestration primitives for Red Bear OS drivers."]
#[cfg(not(any(feature = "std", feature = "alloc", test)))]
compile_error!("redox-driver-core requires either the `std` or `alloc` feature");
extern crate alloc;
/// Bus abstractions and related error types.
pub mod bus;
/// Device descriptors and bound-device state.
pub mod device;
/// Driver traits and probe outcomes.
pub mod driver;
/// Hotplug and uevent metadata types.
pub mod hotplug;
/// Device-manager orchestration.
pub mod manager;
/// Match-table primitives.
pub mod r#match;
/// Driver parameter definitions and runtime values.
pub mod params;
pub use bus::{Bus, BusError};
pub use device::{BoundDevice, Device, DeviceId, DeviceInfo};
pub use driver::{Driver, DriverError, ProbeResult};
pub use hotplug::{Uevent, UeventAction};
#[cfg(feature = "hotplug")]
pub use hotplug::{HotplugEvent, HotplugSubscription};
pub use manager::{DeviceManager, ManagerConfig, ProbeEvent};
pub use params::{DriverParams, ParamDef, ParamValue};
pub use r#match::{DriverMatch, MatchPriority, MatchTable};
@@ -0,0 +1,433 @@
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::bus::{Bus, BusError};
use crate::device::{BoundDevice, DeviceId, DeviceInfo};
use crate::driver::{Driver, ProbeResult};
/// Event emitted by the device manager during discovery or deferred-probe processing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProbeEvent {
/// A bus finished enumeration and reported the number of discovered devices.
BusEnumerated {
/// Bus name returned by the [`Bus`] implementation.
bus: String,
/// Number of devices returned by the bus.
device_count: usize,
},
/// A bus failed to enumerate devices.
BusEnumerationFailed {
/// Bus name returned by the [`Bus`] implementation.
bus: String,
/// Error returned by the bus.
error: BusError,
},
/// The manager skipped probing because the device is already bound.
AlreadyBound {
/// Identifier of the device that was skipped.
device: DeviceId,
/// Driver that already owns the device.
driver_name: String,
},
/// A driver completed a probe attempt for a device.
ProbeCompleted {
/// Identifier of the probed device.
device: DeviceId,
/// Driver that performed the probe.
driver_name: String,
/// Result returned by the driver's probe method.
result: ProbeResult,
},
/// No registered driver had a matching table entry for the device.
NoDriverFound {
/// Identifier of the unmatched device.
device: DeviceId,
},
/// A deferred probe referenced a driver that is no longer registered.
MissingDriver {
/// Identifier of the affected device.
device: DeviceId,
/// Driver name that could not be found.
driver_name: String,
},
}
/// Configuration for the central [`DeviceManager`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManagerConfig {
/// Maximum number of probes the manager should allow concurrently.
///
/// The current implementation probes synchronously and stores this as policy metadata for
/// future async or threaded executors.
pub max_concurrent_probes: usize,
/// Interval, in milliseconds, between deferred-probe retries.
pub deferred_retry_ms: u64,
/// Whether the manager should prefer asynchronous probing when an executor is available.
pub async_probe: bool,
}
/// Central device manager that orchestrates device discovery and driver binding.
pub struct DeviceManager {
buses: Vec<Box<dyn Bus>>,
drivers: Vec<Box<dyn Driver>>,
bound_devices: BTreeMap<DeviceId, BoundDevice>,
deferred_queue: Vec<(DeviceInfo, String)>,
config: ManagerConfig,
}
impl DeviceManager {
/// Creates a new device manager with the provided policy configuration.
pub fn new(config: ManagerConfig) -> Self {
Self {
buses: Vec::new(),
drivers: Vec::new(),
bound_devices: BTreeMap::new(),
deferred_queue: Vec::new(),
config,
}
}
/// Registers a bus that will be included in future enumeration cycles.
pub fn register_bus(&mut self, bus: Box<dyn Bus>) {
self.buses.push(bus);
}
/// Registers a driver and reorders drivers so higher-priority probes run first.
pub fn register_driver(&mut self, driver: Box<dyn Driver>) {
self.drivers.push(driver);
self.drivers
.sort_by(|left, right| right.priority().cmp(&left.priority()));
}
/// Runs a full enumeration cycle across all registered buses.
pub fn enumerate(&mut self) -> Vec<ProbeEvent> {
let _probe_budget = self.config.max_concurrent_probes.max(1);
let _async_probe = self.config.async_probe;
let mut events = Vec::new();
for bus_index in 0..self.buses.len() {
let (bus_name, enumeration) = {
let bus = &self.buses[bus_index];
(bus.name().to_string(), bus.enumerate_devices())
};
match enumeration {
Ok(devices) => {
events.push(ProbeEvent::BusEnumerated {
bus: bus_name,
device_count: devices.len(),
});
for info in devices {
if let Some(bound) = self.bound_devices.get(&info.id) {
events.push(ProbeEvent::AlreadyBound {
device: info.id.clone(),
driver_name: bound.driver_name.clone(),
});
continue;
}
self.probe_device(info, &mut events);
}
}
Err(error) => events.push(ProbeEvent::BusEnumerationFailed {
bus: bus_name,
error,
}),
}
}
events
}
/// Retries all deferred probe attempts in registration order.
pub fn retry_deferred(&mut self) -> Vec<ProbeEvent> {
let _retry_interval_ms = self.config.deferred_retry_ms;
let mut events = Vec::new();
let deferred = core::mem::take(&mut self.deferred_queue);
for (info, driver_name) in deferred {
if let Some(bound) = self.bound_devices.get(&info.id) {
events.push(ProbeEvent::AlreadyBound {
device: info.id.clone(),
driver_name: bound.driver_name.clone(),
});
continue;
}
let Some(driver_index) = self
.drivers
.iter()
.position(|driver| driver.name() == driver_name)
else {
events.push(ProbeEvent::MissingDriver {
device: info.id.clone(),
driver_name,
});
continue;
};
let (probe_driver_name, result) = {
let driver = &self.drivers[driver_index];
(driver.name().to_string(), driver.probe(&info))
};
match &result {
ProbeResult::Bound => {
self.bound_devices.insert(
info.id.clone(),
BoundDevice {
info: info.clone(),
driver_name: probe_driver_name.clone(),
parameters: BTreeMap::new(),
},
);
}
ProbeResult::Deferred { .. } => {
self.enqueue_deferred(info.clone(), probe_driver_name.clone());
}
ProbeResult::NotSupported | ProbeResult::Fatal { .. } => {}
}
events.push(ProbeEvent::ProbeCompleted {
device: info.id.clone(),
driver_name: probe_driver_name,
result,
});
}
events
}
fn probe_device(&mut self, info: DeviceInfo, events: &mut Vec<ProbeEvent>) {
let mut matched = false;
for driver_index in 0..self.drivers.len() {
let is_match = {
let driver = &self.drivers[driver_index];
driver
.match_table()
.iter()
.any(|driver_match| driver_match.matches(&info))
};
if !is_match {
continue;
}
matched = true;
let (driver_name, result) = {
let driver = &self.drivers[driver_index];
(driver.name().to_string(), driver.probe(&info))
};
match &result {
ProbeResult::Bound => {
self.bound_devices.insert(
info.id.clone(),
BoundDevice {
info: info.clone(),
driver_name: driver_name.clone(),
parameters: BTreeMap::new(),
},
);
events.push(ProbeEvent::ProbeCompleted {
device: info.id.clone(),
driver_name,
result,
});
return;
}
ProbeResult::Deferred { .. } => {
self.enqueue_deferred(info.clone(), driver_name.clone());
events.push(ProbeEvent::ProbeCompleted {
device: info.id.clone(),
driver_name,
result,
});
return;
}
ProbeResult::Fatal { .. } => {
events.push(ProbeEvent::ProbeCompleted {
device: info.id.clone(),
driver_name,
result,
});
return;
}
ProbeResult::NotSupported => {
events.push(ProbeEvent::ProbeCompleted {
device: info.id.clone(),
driver_name,
result,
});
}
}
}
if !matched {
events.push(ProbeEvent::NoDriverFound { device: info.id });
}
}
fn enqueue_deferred(&mut self, info: DeviceInfo, driver_name: String) {
let already_queued = self.deferred_queue.iter().any(|(queued_info, queued_driver)| {
queued_info.id == info.id && queued_driver == &driver_name
});
if !already_queued {
self.deferred_queue.push((info, driver_name));
}
}
}
#[cfg(test)]
mod tests {
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use super::{DeviceManager, ManagerConfig};
use crate::bus::{Bus, BusError};
use crate::device::{DeviceId, DeviceInfo};
use crate::driver::{Driver, DriverError, ProbeResult};
use crate::r#match::DriverMatch;
struct MockBus {
name: &'static str,
devices: Vec<DeviceInfo>,
}
impl Bus for MockBus {
fn name(&self) -> &str {
self.name
}
fn enumerate_devices(&self) -> Result<Vec<DeviceInfo>, BusError> {
Ok(self.devices.clone())
}
}
struct MockDriver {
name: &'static str,
description: &'static str,
priority: i32,
matches: Vec<DriverMatch>,
}
impl Driver for MockDriver {
fn name(&self) -> &str {
self.name
}
fn description(&self) -> &str {
self.description
}
fn priority(&self) -> i32 {
self.priority
}
fn match_table(&self) -> &[DriverMatch] {
self.matches.as_slice()
}
fn probe(&self, _info: &DeviceInfo) -> ProbeResult {
ProbeResult::NotSupported
}
fn remove(&self, _info: &DeviceInfo) -> Result<(), DriverError> {
Ok(())
}
}
fn config() -> ManagerConfig {
ManagerConfig {
max_concurrent_probes: 4,
deferred_retry_ms: 250,
async_probe: false,
}
}
#[test]
fn register_bus_and_driver_store_entries() {
let mut manager = DeviceManager::new(config());
manager.register_bus(Box::new(MockBus {
name: "pci",
devices: Vec::new(),
}));
manager.register_driver(Box::new(MockDriver {
name: "low",
description: "low-priority driver",
priority: 10,
matches: vec![DriverMatch {
vendor: Some(0x1234),
device: None,
class: None,
subclass: None,
prog_if: None,
subsystem_vendor: None,
subsystem_device: None,
}],
}));
manager.register_driver(Box::new(MockDriver {
name: "high",
description: "high-priority driver",
priority: 100,
matches: vec![DriverMatch {
vendor: Some(0x1234),
device: Some(0x5678),
class: None,
subclass: None,
prog_if: None,
subsystem_vendor: None,
subsystem_device: None,
}],
}));
assert_eq!(manager.buses.len(), 1);
assert_eq!(manager.drivers.len(), 2);
assert_eq!(manager.drivers[0].name(), "high");
assert_eq!(manager.drivers[1].name(), "low");
}
#[test]
fn enumerate_reports_registered_bus() {
let mut manager = DeviceManager::new(config());
manager.register_bus(Box::new(MockBus {
name: "pci",
devices: vec![DeviceInfo {
id: DeviceId {
bus: String::from("pci"),
path: String::from("0000:00:1f.2"),
},
vendor: Some(0x8086),
device: Some(0x2922),
class: Some(0x01),
subclass: Some(0x06),
prog_if: Some(0x01),
revision: Some(0x02),
subsystem_vendor: None,
subsystem_device: None,
raw_path: String::from("/scheme/pci/00.1f.2"),
description: Some(String::from("AHCI controller")),
}],
}));
let events = manager.enumerate();
assert!(events.iter().any(|event| matches!(
event,
super::ProbeEvent::BusEnumerated { bus, device_count }
if bus == "pci" && *device_count == 1
)));
}
}
@@ -0,0 +1,146 @@
use alloc::vec::Vec;
use crate::device::DeviceInfo;
/// Priority type used to order driver probes.
pub type MatchPriority = i32;
/// A single entry in a driver's match table.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct DriverMatch {
/// Optional vendor identifier match.
pub vendor: Option<u16>,
/// Optional device identifier match.
pub device: Option<u16>,
/// Optional class-code match.
pub class: Option<u8>,
/// Optional subclass-code match.
pub subclass: Option<u8>,
/// Optional programming-interface match.
pub prog_if: Option<u8>,
/// Optional subsystem vendor match.
pub subsystem_vendor: Option<u16>,
/// Optional subsystem device match.
pub subsystem_device: Option<u16>,
}
impl DriverMatch {
/// Checks whether this match entry matches the provided device information.
pub fn matches(&self, info: &DeviceInfo) -> bool {
self.vendor.map_or(true, |v| info.vendor == Some(v))
&& self.device.map_or(true, |d| info.device == Some(d))
&& self.class.map_or(true, |c| info.class == Some(c))
&& self.subclass.map_or(true, |s| info.subclass == Some(s))
&& self.prog_if.map_or(true, |p| info.prog_if == Some(p))
&& self
.subsystem_vendor
.map_or(true, |v| info.subsystem_vendor == Some(v))
&& self
.subsystem_device
.map_or(true, |d| info.subsystem_device == Some(d))
}
}
/// Collection wrapper for a driver's match entries.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct MatchTable {
entries: Vec<DriverMatch>,
}
impl MatchTable {
/// Creates a new match table from the provided entries.
pub fn new(entries: Vec<DriverMatch>) -> Self {
Self { entries }
}
/// Returns the underlying immutable slice of match entries.
pub fn entries(&self) -> &[DriverMatch] {
self.entries.as_slice()
}
/// Returns `true` if any entry in the table matches the provided device.
pub fn matches(&self, info: &DeviceInfo) -> bool {
self.entries.iter().any(|entry| entry.matches(info))
}
}
impl From<Vec<DriverMatch>> for MatchTable {
fn from(entries: Vec<DriverMatch>) -> Self {
Self::new(entries)
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use super::DriverMatch;
use crate::device::{DeviceId, DeviceInfo};
fn sample_device() -> DeviceInfo {
DeviceInfo {
id: DeviceId {
bus: String::from("pci"),
path: String::from("0000:00:02.0"),
},
vendor: Some(0x8086),
device: Some(0x1234),
class: Some(0x03),
subclass: Some(0x00),
prog_if: Some(0x00),
revision: Some(0x01),
subsystem_vendor: Some(0x8086),
subsystem_device: Some(0xabcd),
raw_path: String::from("/scheme/pci/00.02.0"),
description: Some(String::from("Display controller")),
}
}
#[test]
fn driver_match_accepts_exact_match() {
let info = sample_device();
let driver_match = DriverMatch {
vendor: Some(0x8086),
device: Some(0x1234),
class: Some(0x03),
subclass: Some(0x00),
prog_if: Some(0x00),
subsystem_vendor: Some(0x8086),
subsystem_device: Some(0xabcd),
};
assert!(driver_match.matches(&info));
}
#[test]
fn driver_match_supports_wildcards() {
let info = sample_device();
let driver_match = DriverMatch {
vendor: Some(0x8086),
device: None,
class: Some(0x03),
subclass: None,
prog_if: None,
subsystem_vendor: None,
subsystem_device: None,
};
assert!(driver_match.matches(&info));
}
#[test]
fn driver_match_rejects_mismatch() {
let info = sample_device();
let driver_match = DriverMatch {
vendor: Some(0x10ec),
device: None,
class: None,
subclass: None,
prog_if: None,
subsystem_vendor: None,
subsystem_device: None,
};
assert!(!driver_match.matches(&info));
}
}
@@ -0,0 +1,166 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use alloc::format;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParamValue {
Bool(bool),
Int(i64),
Uint(u64),
String(String),
Enum(String, Vec<String>),
}
impl ParamValue {
pub fn type_name(&self) -> &str {
match self {
ParamValue::Bool(_) => "bool",
ParamValue::Int(_) => "int",
ParamValue::Uint(_) => "uint",
ParamValue::String(_) => "string",
ParamValue::Enum(_, _) => "enum",
}
}
pub fn to_display_string(&self) -> String {
match self {
ParamValue::Bool(v) => format!("{}", v),
ParamValue::Int(v) => format!("{}", v),
ParamValue::Uint(v) => format!("{}", v),
ParamValue::String(v) => v.clone(),
ParamValue::Enum(v, _) => v.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamDef {
pub name: String,
pub description: String,
pub default: ParamValue,
pub writable: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DriverParams {
pub params: BTreeMap<String, ParamDef>,
pub values: BTreeMap<String, ParamValue>,
}
impl DriverParams {
pub fn new() -> Self {
DriverParams {
params: BTreeMap::new(),
values: BTreeMap::new(),
}
}
pub fn define(&mut self, name: &str, description: &str, default: ParamValue, writable: bool) {
self.params.insert(
String::from(name),
ParamDef {
name: String::from(name),
description: String::from(description),
default: default.clone(),
writable,
},
);
self.values.insert(String::from(name), default);
}
pub fn get(&self, name: &str) -> Option<&ParamValue> {
self.values.get(name)
}
pub fn set(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
match self.params.get(name) {
Some(def) if !def.writable => Err("parameter is read-only"),
Some(def) => {
if core::mem::discriminant(&def.default) == core::mem::discriminant(&value) {
self.values.insert(String::from(name), value);
Ok(())
} else {
Err("parameter type mismatch")
}
}
None => Err("unknown parameter"),
}
}
pub fn list(&self) -> Vec<&ParamDef> {
self.params.values().collect()
}
pub fn parse_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
pub fn parse_int(s: &str) -> Option<i64> {
s.parse().ok()
}
pub fn parse_uint(s: &str) -> Option<u64> {
s.parse().ok()
}
}
impl Default for DriverParams {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn define_and_get_parameter() {
let mut p = DriverParams::new();
p.define("debug", "Enable debug logging", ParamValue::Bool(false), true);
assert_eq!(p.get("debug"), Some(&ParamValue::Bool(false)));
}
#[test]
fn set_writable_parameter() {
let mut p = DriverParams::new();
p.define("debug", "Enable debug logging", ParamValue::Bool(false), true);
assert!(p.set("debug", ParamValue::Bool(true)).is_ok());
assert_eq!(p.get("debug"), Some(&ParamValue::Bool(true)));
}
#[test]
fn set_readonly_parameter_fails() {
let mut p = DriverParams::new();
p.define("vendor_id", "Vendor ID", ParamValue::Uint(0), false);
assert!(p.set("vendor_id", ParamValue::Uint(1)).is_err());
}
#[test]
fn set_unknown_parameter_fails() {
let mut p = DriverParams::new();
assert!(p.set("nonexistent", ParamValue::Bool(true)).is_err());
}
#[test]
fn param_value_display_strings() {
assert_eq!(ParamValue::Bool(true).to_display_string(), "true");
assert_eq!(ParamValue::Int(-42).to_display_string(), "-42");
assert_eq!(ParamValue::Uint(42).to_display_string(), "42");
assert_eq!(ParamValue::String(String::from("hello")).to_display_string(), "hello");
}
#[test]
fn parse_bool_variants() {
assert_eq!(DriverParams::parse_bool("true"), Some(true));
assert_eq!(DriverParams::parse_bool("1"), Some(true));
assert_eq!(DriverParams::parse_bool("yes"), Some(true));
assert_eq!(DriverParams::parse_bool("false"), Some(false));
assert_eq!(DriverParams::parse_bool("0"), Some(false));
}
}