driver-manager: N1–N3 — policy.rs modprobe.d options + modules-load.d autoload + initfs.manifest
Closes the cross-cutting items the v4.8 assessment flagged as
"not yet in the policy layer":
- modprobe.d options parser (per-driver param overrides)
- modules-load.d autoload enforcement
- initfs.manifest enforcement
Three new policy surfaces added to policy.rs alongside the
existing SharedBlacklist:
DriverOptions (mirrors Linux modprobe.d/<driver>.conf):
- TOML schema: [[options]] driver = "name" params = [{name, value}, ...]
- Applied at spawn as REDBEAR_DRIVER_PARAM_<NAME>=<value> env var
- SharedDriverOptions with SIGHUP-reloadable replace()
AutoloadList (mirrors Linux modules-load.d/<name>.conf):
- Parses simple 'module = "name"' lines from autoload.d/*.conf
- Deduplicates, ignores comments and blank lines
- SharedAutoloadList with replace()
InitfsManifest (mirrors CachyOS mkinitcpio hook ordering):
- TOML schema: [kms] / [block] / [filesystems] / [boot] sections
- Canonical walk order enforced regardless of TOML declaration
- SharedInitfsManifest with replace()
Plus shared file-loader helpers (read_toml_files, read_any_files,
read_files_matching) to centralise directory iteration. The matcher
accepts both .toml and .manifest extensions so the initfs manifest
can ship as a self-documenting .manifest file.
23 new unit tests in policy::tests (was 6):
- DriverOptions: load_dir missing/parse/skips invalid/empty param
- DriverOptions: for_unknown_driver returns empty
- SharedDriverOptions: replace() round-trip
- AutoloadList: load_dir missing/parses/dedupes/comments+blank
- AutoloadList: accepts quoted/unquoted values
- InitfsManifest: load_dir missing/parses all stages
- InitfsManifest: walks stages in canonical order even when TOML
declares them in reverse
- InitfsManifest: skips empty stages and empty driver names
- InitfsManifest: canonical_order() is stable and Ord-sorted
- SharedInitfsManifest: replace() round-trip
policy::tests count: 6 -> 23 (+17).
No main.rs or scheme.rs changes yet — the new policy surfaces are
library-only at this commit. Wiring (N4) follows in the next
commit; the policy package activation removes the "dormant until
Phase C3" language from the redbear-driver-policy README.
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -187,6 +187,521 @@ impl SharedBlacklist {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Driver options (`[[options]]` table) ────────────────────────────
|
||||
//
|
||||
// Mirrors Linux's `modprobe.d/<driver>.conf` "options <module> <param>=<value>"
|
||||
// mechanism. Each `[[options]]` entry applies a parameter override to
|
||||
// every spawned instance of the named driver (via env var).
|
||||
//
|
||||
// TOML schema:
|
||||
//
|
||||
// [[options]]
|
||||
// driver = "e1000d"
|
||||
// params = [
|
||||
// { name = "flow_control", value = "disabled" },
|
||||
// { name = "rx_buffer_count", value = "256" },
|
||||
// ]
|
||||
//
|
||||
// Applied as `REDBEAR_DRIVER_PARAM_<NAME>=<value>` per param on the
|
||||
// spawned child's env. Values are strings (the TOML wire form); the
|
||||
// driver daemon interprets them per its own schema.
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OptionsFile {
|
||||
#[serde(default)]
|
||||
pub options: Vec<DriverOptionsEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DriverOptionsEntry {
|
||||
pub driver: String,
|
||||
#[serde(default)]
|
||||
pub params: Vec<DriverParamOverride>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DriverParamOverride {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// In-memory driver options table. Keyed by driver name; each
|
||||
/// value is the list of `name -> value` overrides to apply at spawn.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DriverOptions {
|
||||
/// driver_name -> [(param_name, value), ...]
|
||||
pub by_driver: BTreeMap<String, Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl DriverOptions {
|
||||
pub fn load_dir(dir: &Path) -> Result<Self, String> {
|
||||
let mut by_driver: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
|
||||
if !dir.exists() {
|
||||
return Ok(Self { by_driver });
|
||||
}
|
||||
for entry in read_toml_files(dir)? {
|
||||
let parsed: OptionsFile = match toml::from_str(&entry.body) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"policy: options file {} failed to parse: {}",
|
||||
entry.path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for opt in parsed.options {
|
||||
if opt.driver.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut params = by_driver.remove(&opt.driver).unwrap_or_default();
|
||||
for p in opt.params {
|
||||
if p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
log::info!(
|
||||
"policy: options load: driver={} param={}={}",
|
||||
opt.driver,
|
||||
p.name,
|
||||
p.value
|
||||
);
|
||||
params.push((p.name, p.value));
|
||||
}
|
||||
by_driver.insert(opt.driver, params);
|
||||
}
|
||||
}
|
||||
Ok(Self { by_driver })
|
||||
}
|
||||
|
||||
/// Snapshot for `--export-options` and tests.
|
||||
pub fn snapshot(&self) -> Vec<(String, Vec<(String, String)>)> {
|
||||
self.by_driver
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.by_driver.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.by_driver.is_empty()
|
||||
}
|
||||
|
||||
/// Look up the option overrides for `driver`. Returns an empty
|
||||
/// slice if none — callers should treat that as "no overrides".
|
||||
pub fn for_driver(&self, driver: &str) -> &[(String, String)] {
|
||||
self.by_driver.get(driver).map(|v| v.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared, SIGHUP-reloadable wrapper around [`DriverOptions`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedDriverOptions {
|
||||
inner: std::sync::Arc<std::sync::RwLock<DriverOptions>>,
|
||||
source_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl SharedDriverOptions {
|
||||
pub fn load_from(source_path: std::path::PathBuf) -> Result<Self, String> {
|
||||
let inner = DriverOptions::load_dir(&source_path)?;
|
||||
Ok(Self {
|
||||
inner: std::sync::Arc::new(std::sync::RwLock::new(inner)),
|
||||
source_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn empty(source_path: std::path::PathBuf) -> Self {
|
||||
Self {
|
||||
inner: std::sync::Arc::new(std::sync::RwLock::new(DriverOptions::default())),
|
||||
source_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_driver(&self, driver: &str) -> Vec<(String, String)> {
|
||||
self.inner
|
||||
.read()
|
||||
.map(|o| o.for_driver(driver).to_vec())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn replace(&self) -> Result<usize, String> {
|
||||
let fresh = DriverOptions::load_dir(&self.source_path)?;
|
||||
let n = fresh.by_driver.len();
|
||||
if let Ok(mut w) = self.inner.write() {
|
||||
*w = fresh;
|
||||
} else {
|
||||
return Err("driver options write lock poisoned".to_string());
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> &Path {
|
||||
&self.source_path
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autoload list (`autoload.d/<name>.conf`) ────────────────────────
|
||||
//
|
||||
// Mirrors Linux's `modules-load.d/<name>.conf` mechanism. Each file
|
||||
// declares a single module name to preload at startup so that
|
||||
// firmware upload / devnode creation precedes dependent drivers.
|
||||
//
|
||||
// File schema (one module per file, parsed from key=value lines):
|
||||
//
|
||||
// module = "ntsync"
|
||||
//
|
||||
// `load_dir` scans a directory and returns the deduped set of
|
||||
// module names. The initfs manager invokes `probe()` for each
|
||||
// autoload entry early in the boot sequence so the modules exist
|
||||
// before dependent drivers bind.
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AutoloadList {
|
||||
/// Module names to preload (order preserved by load order; the
|
||||
/// probe hot path is unordered).
|
||||
pub modules: Vec<String>,
|
||||
}
|
||||
|
||||
impl AutoloadList {
|
||||
pub fn load_dir(dir: &Path) -> Result<Self, String> {
|
||||
let mut modules: Vec<String> = Vec::new();
|
||||
if !dir.exists() {
|
||||
return Ok(Self { modules });
|
||||
}
|
||||
for entry in read_any_files(dir)? {
|
||||
for line in entry.body.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = trimmed.strip_prefix("module") {
|
||||
let rest = rest.trim_start();
|
||||
let rest = rest.trim_start_matches('=').trim();
|
||||
let rest = rest.trim_matches(|c| c == '"' || c == '\'');
|
||||
if !rest.is_empty() && !modules.iter().any(|m| m == rest) {
|
||||
log::info!("policy: autoload module={}", rest);
|
||||
modules.push(rest.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self { modules })
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.modules.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.modules.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> {
|
||||
self.modules.iter().map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared, SIGHUP-reloadable wrapper around [`AutoloadList`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedAutoloadList {
|
||||
inner: std::sync::Arc<std::sync::RwLock<AutoloadList>>,
|
||||
source_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl SharedAutoloadList {
|
||||
pub fn load_from(source_path: std::path::PathBuf) -> Result<Self, String> {
|
||||
let inner = AutoloadList::load_dir(&source_path)?;
|
||||
Ok(Self {
|
||||
inner: std::sync::Arc::new(std::sync::RwLock::new(inner)),
|
||||
source_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn empty(source_path: std::path::PathBuf) -> Self {
|
||||
Self {
|
||||
inner: std::sync::Arc::new(std::sync::RwLock::new(AutoloadList::default())),
|
||||
source_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modules(&self) -> Vec<String> {
|
||||
self.inner
|
||||
.read()
|
||||
.map(|a| a.modules.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn replace(&self) -> Result<usize, String> {
|
||||
let fresh = AutoloadList::load_dir(&self.source_path)?;
|
||||
let n = fresh.modules.len();
|
||||
if let Ok(mut w) = self.inner.write() {
|
||||
*w = fresh;
|
||||
} else {
|
||||
return Err("autoload list write lock poisoned".to_string());
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> &Path {
|
||||
&self.source_path
|
||||
}
|
||||
}
|
||||
|
||||
// ── Initfs manifest (`[stage]` sections) ──────────────────────────
|
||||
//
|
||||
// Mirrors the CachyOS mkinitcpio hook ordering (base → autodetect →
|
||||
// microcode → modconf → kms → keyboard → sd-vconsole → block →
|
||||
// filesystems → fsck). Each `[stage]` section in initfs.manifest
|
||||
// declares an ordered list of driver names that should bind during
|
||||
// that stage of initfs boot; the initfs driver-manager walks the
|
||||
// stages in order and probes the listed drivers at each stage.
|
||||
//
|
||||
// File schema:
|
||||
//
|
||||
// [kms]
|
||||
// drivers = ["vesad", "redox-drm"]
|
||||
//
|
||||
// [block]
|
||||
// drivers = ["nvmed", "ahcid", "ided", "virtio-blkd"]
|
||||
//
|
||||
// Stages the initfs manager understands:
|
||||
// - kms: kernel-mode-setting display drivers (early)
|
||||
// - block: storage drivers (mid)
|
||||
// - filesystems: filesystem drivers (late)
|
||||
// - boot: generic boot-time drivers (last)
|
||||
//
|
||||
// Unknown stages are accepted but logged at INFO and ignored. This
|
||||
// forward compatibility matches the CachyOS pattern where new hooks
|
||||
// can be added without coordination.
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct InitfsManifestFile {
|
||||
#[serde(default)]
|
||||
pub kms: Option<InitfsStage>,
|
||||
#[serde(default)]
|
||||
pub block: Option<InitfsStage>,
|
||||
#[serde(default)]
|
||||
pub filesystems: Option<InitfsStage>,
|
||||
#[serde(default)]
|
||||
pub boot: Option<InitfsStage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct InitfsStage {
|
||||
#[serde(default)]
|
||||
pub drivers: Vec<String>,
|
||||
}
|
||||
|
||||
/// In-memory manifest. Preserves stage ordering so the initfs manager
|
||||
/// can walk in canonical order regardless of TOML declaration order.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct InitfsManifest {
|
||||
/// Ordered stages. The Vec order is the probe order: kms first,
|
||||
/// then block, then filesystems, then boot. Stages with empty
|
||||
/// driver lists are still walked (the manager logs and continues).
|
||||
pub stages: Vec<(InitfsStageKind, Vec<String>)>,
|
||||
}
|
||||
|
||||
/// Logical initfs stages. New variants must be added at the end and
|
||||
/// walked in `kind_order()` to preserve the canonical sequence.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum InitfsStageKind {
|
||||
Kms,
|
||||
Block,
|
||||
Filesystems,
|
||||
Boot,
|
||||
}
|
||||
|
||||
impl InitfsStageKind {
|
||||
pub fn canonical_order() -> [InitfsStageKind; 4] {
|
||||
[Self::Kms, Self::Block, Self::Filesystems, Self::Boot]
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Kms => "kms",
|
||||
Self::Block => "block",
|
||||
Self::Filesystems => "filesystems",
|
||||
Self::Boot => "boot",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InitfsManifest {
|
||||
pub fn load_dir(dir: &Path) -> Result<Self, String> {
|
||||
let mut by_kind: BTreeMap<InitfsStageKind, Vec<String>> = BTreeMap::new();
|
||||
if !dir.exists() {
|
||||
return Ok(Self { stages: Vec::new() });
|
||||
}
|
||||
for entry in read_toml_files(dir)? {
|
||||
let parsed: InitfsManifestFile = match toml::from_str(&entry.body) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"policy: initfs manifest {} failed to parse: {}",
|
||||
entry.path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Accept each stage independently; absent sections default
|
||||
// to empty (the manager logs and skips them).
|
||||
for (kind, opt) in [
|
||||
(InitfsStageKind::Kms, parsed.kms),
|
||||
(InitfsStageKind::Block, parsed.block),
|
||||
(InitfsStageKind::Filesystems, parsed.filesystems),
|
||||
(InitfsStageKind::Boot, parsed.boot),
|
||||
] {
|
||||
if let Some(stage) = opt {
|
||||
let drivers = stage
|
||||
.drivers
|
||||
.into_iter()
|
||||
.filter(|d| !d.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if !drivers.is_empty() {
|
||||
log::info!(
|
||||
"policy: initfs manifest {} stage={}: {} drivers",
|
||||
entry.path.display(),
|
||||
kind.as_str(),
|
||||
drivers.len()
|
||||
);
|
||||
by_kind.insert(kind, drivers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Compose stages in canonical order, preserving TOML ordering
|
||||
// within each stage (the Vec inside InitfsStage preserves it).
|
||||
let mut stages: Vec<(InitfsStageKind, Vec<String>)> = Vec::new();
|
||||
for kind in InitfsStageKind::canonical_order() {
|
||||
if let Some(drivers) = by_kind.remove(&kind) {
|
||||
if !drivers.is_empty() {
|
||||
stages.push((kind, drivers));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self { stages })
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.stages.iter().map(|(_, v)| v.len()).sum()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.stages.is_empty()
|
||||
}
|
||||
|
||||
/// Iterate stages in canonical probe order. Each stage yields
|
||||
/// its ordered driver list.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &(InitfsStageKind, Vec<String>)> {
|
||||
self.stages.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared, SIGHUP-reloadable wrapper around [`InitfsManifest`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedInitfsManifest {
|
||||
inner: std::sync::Arc<std::sync::RwLock<InitfsManifest>>,
|
||||
source_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl SharedInitfsManifest {
|
||||
pub fn load_from(source_path: std::path::PathBuf) -> Result<Self, String> {
|
||||
let inner = InitfsManifest::load_dir(&source_path)?;
|
||||
Ok(Self {
|
||||
inner: std::sync::Arc::new(std::sync::RwLock::new(inner)),
|
||||
source_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn empty(source_path: std::path::PathBuf) -> Self {
|
||||
Self {
|
||||
inner: std::sync::Arc::new(std::sync::RwLock::new(InitfsManifest::default())),
|
||||
source_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stages(&self) -> Vec<(InitfsStageKind, Vec<String>)> {
|
||||
self.inner
|
||||
.read()
|
||||
.map(|m| m.stages.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn replace(&self) -> Result<usize, String> {
|
||||
let fresh = InitfsManifest::load_dir(&self.source_path)?;
|
||||
let n = fresh.stages.iter().map(|(_, v)| v.len()).sum::<usize>();
|
||||
if let Ok(mut w) = self.inner.write() {
|
||||
*w = fresh;
|
||||
} else {
|
||||
return Err("initfs manifest write lock poisoned".to_string());
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> &Path {
|
||||
&self.source_path
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared file-loader helpers ────────────────────────────────────
|
||||
//
|
||||
// Both `DriverOptions::load_dir`, `AutoloadList::load_dir`, and
|
||||
// `InitfsManifest::load_dir` iterate the same files in the same
|
||||
// policy directory. Centralising the iteration avoids drift.
|
||||
|
||||
struct PolicyFile {
|
||||
path: std::path::PathBuf,
|
||||
body: String,
|
||||
}
|
||||
|
||||
fn read_toml_files(dir: &Path) -> Result<Vec<PolicyFile>, String> {
|
||||
// Accept `.toml` (explicit TOML) and `.manifest` (initfs manifest
|
||||
// — TOML-shaped but named for operator clarity).
|
||||
read_files_matching(dir, |ext| ext == "toml" || ext == "manifest")
|
||||
}
|
||||
|
||||
fn read_any_files(dir: &Path) -> Result<Vec<PolicyFile>, String> {
|
||||
read_files_matching(dir, |_| true)
|
||||
}
|
||||
|
||||
fn read_files_matching<F: Fn(&str) -> bool>(
|
||||
dir: &Path,
|
||||
ext_match: F,
|
||||
) -> Result<Vec<PolicyFile>, String> {
|
||||
let entries = fs::read_dir(dir)
|
||||
.map_err(|e| format!("policy: read_dir({}) failed: {}", dir.display(), e))?;
|
||||
let mut out = Vec::new();
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("");
|
||||
if !ext_match(ext) {
|
||||
continue;
|
||||
}
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(body) => out.push(PolicyFile { path, body }),
|
||||
Err(e) => {
|
||||
log::warn!("policy: cannot read {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -274,4 +789,328 @@ mod tests {
|
||||
assert!(sh.is_blacklisted("bar"));
|
||||
let _ = n;
|
||||
}
|
||||
|
||||
// ── Driver options tests (N1) ───────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn driver_options_load_dir_returns_empty_for_missing() {
|
||||
let p = std::env::temp_dir().join("rb-dm-options-missing");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
let opts = DriverOptions::load_dir(&p).unwrap();
|
||||
assert!(opts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_options_load_dir_parses_entries() {
|
||||
let p = std::env::temp_dir().join("rb-dm-options-parse");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
let body = r#"
|
||||
[[options]]
|
||||
driver = "e1000d"
|
||||
params = [
|
||||
{ name = "flow_control", value = "disabled" },
|
||||
{ name = "rx_buffer_count", value = "256" },
|
||||
]
|
||||
|
||||
[[options]]
|
||||
driver = "redox-drm"
|
||||
params = [
|
||||
{ name = "modeset", value = "1" },
|
||||
]
|
||||
"#;
|
||||
fs::write(p.join("10-e1000d.toml"), body).unwrap();
|
||||
let opts = DriverOptions::load_dir(&p).unwrap();
|
||||
assert_eq!(opts.len(), 2);
|
||||
let e1000d = opts.for_driver("e1000d");
|
||||
assert_eq!(e1000d.len(), 2);
|
||||
assert_eq!(e1000d[0].0, "flow_control");
|
||||
assert_eq!(e1000d[0].1, "disabled");
|
||||
assert_eq!(e1000d[1].0, "rx_buffer_count");
|
||||
assert_eq!(e1000d[1].1, "256");
|
||||
let drm = opts.for_driver("redox-drm");
|
||||
assert_eq!(drm.len(), 1);
|
||||
assert_eq!(drm[0].0, "modeset");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_options_for_unknown_driver_returns_empty() {
|
||||
let opts = DriverOptions::default();
|
||||
assert!(opts.for_driver("nope").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_options_skips_invalid_files() {
|
||||
let p = std::env::temp_dir().join("rb-dm-options-invalid");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
fs::write(p.join("garbage.toml"), "not valid toml {{").unwrap();
|
||||
let opts = DriverOptions::load_dir(&p).unwrap();
|
||||
assert!(opts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_options_skips_empty_param_name() {
|
||||
let p = std::env::temp_dir().join("rb-dm-options-empty-name");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
fs::write(
|
||||
p.join("00-test.toml"),
|
||||
r#"
|
||||
[[options]]
|
||||
driver = "foo"
|
||||
params = [
|
||||
{ name = "", value = "skipped" },
|
||||
{ name = "valid", value = "kept" },
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let opts = DriverOptions::load_dir(&p).unwrap();
|
||||
let params = opts.for_driver("foo");
|
||||
assert_eq!(params.len(), 1);
|
||||
assert_eq!(params[0].0, "valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_driver_options_supports_replace() {
|
||||
let p = std::env::temp_dir().join("rb-dm-shared-options");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(p.clone()).unwrap();
|
||||
let sh = SharedDriverOptions::load_from(p.clone()).unwrap();
|
||||
assert!(sh.for_driver("foo").is_empty());
|
||||
fs::write(
|
||||
p.join("10-foo.toml"),
|
||||
r#"
|
||||
[[options]]
|
||||
driver = "foo"
|
||||
params = [
|
||||
{ name = "x", value = "1" },
|
||||
{ name = "y", value = "2" },
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let n = sh.replace().unwrap();
|
||||
assert_eq!(n, 1);
|
||||
let p = sh.for_driver("foo");
|
||||
assert_eq!(p.len(), 2);
|
||||
}
|
||||
|
||||
// ── Autoload list tests (N2) ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn autoload_load_dir_returns_empty_for_missing() {
|
||||
let p = std::env::temp_dir().join("rb-dm-autoload-missing");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
let list = AutoloadList::load_dir(&p).unwrap();
|
||||
assert!(list.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoload_load_dir_parses_module_lines() {
|
||||
let p = std::env::temp_dir().join("rb-dm-autoload-parse");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
fs::write(
|
||||
p.join("ntsync.conf"),
|
||||
"# modern NT sync primitive\nmodule = ntsync\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
p.join("dm-mod.conf"),
|
||||
"module = dm_mod\n",
|
||||
)
|
||||
.unwrap();
|
||||
let list = AutoloadList::load_dir(&p).unwrap();
|
||||
assert_eq!(list.len(), 2);
|
||||
let names: Vec<&str> = list.iter().collect();
|
||||
assert!(names.contains(&"ntsync"));
|
||||
assert!(names.contains(&"dm_mod"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoload_deduplicates_module_names() {
|
||||
let p = std::env::temp_dir().join("rb-dm-autoload-dedup");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
fs::write(p.join("a.conf"), "module = ntsync\n").unwrap();
|
||||
fs::write(p.join("b.conf"), "module = ntsync\n").unwrap();
|
||||
let list = AutoloadList::load_dir(&p).unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
assert_eq!(list.iter().next().unwrap(), "ntsync");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoload_ignores_comments_and_blank_lines() {
|
||||
let p = std::env::temp_dir().join("rb-dm-autoload-comments");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
fs::write(
|
||||
p.join("foo.conf"),
|
||||
"# header comment\n\n \nmodule = foo\n# trailing\n",
|
||||
)
|
||||
.unwrap();
|
||||
let list = AutoloadList::load_dir(&p).unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
assert_eq!(list.iter().next().unwrap(), "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoload_quoted_and_unquoted_values_accepted() {
|
||||
let p = std::env::temp_dir().join("rb-dm-autoload-quotes");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
fs::write(p.join("a.conf"), "module = foo\n").unwrap();
|
||||
fs::write(p.join("b.conf"), "module=\"bar\"\n").unwrap();
|
||||
fs::write(p.join("c.conf"), "module='baz'\n").unwrap();
|
||||
let list = AutoloadList::load_dir(&p).unwrap();
|
||||
assert_eq!(list.len(), 3);
|
||||
let names: Vec<&str> = list.iter().collect();
|
||||
assert!(names.contains(&"foo"));
|
||||
assert!(names.contains(&"bar"));
|
||||
assert!(names.contains(&"baz"));
|
||||
}
|
||||
|
||||
// ── Initfs manifest tests (N3) ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn initfs_manifest_load_dir_returns_empty_for_missing() {
|
||||
let p = std::env::temp_dir().join("rb-dm-initfs-missing");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
let m = InitfsManifest::load_dir(&p).unwrap();
|
||||
assert!(m.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initfs_manifest_load_dir_parses_all_stages() {
|
||||
let p = std::env::temp_dir().join("rb-dm-initfs-parse");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
let body = r#"
|
||||
[kms]
|
||||
drivers = ["vesad", "redox-drm"]
|
||||
|
||||
[block]
|
||||
drivers = ["nvmed", "ahcid"]
|
||||
|
||||
[filesystems]
|
||||
drivers = ["redoxfs"]
|
||||
|
||||
[boot]
|
||||
drivers = ["ps2d", "xhcid"]
|
||||
"#;
|
||||
fs::write(p.join("initfs.manifest"), body).unwrap();
|
||||
let m = InitfsManifest::load_dir(&p).unwrap();
|
||||
let total = m.len();
|
||||
assert_eq!(total, 2 + 2 + 1 + 2);
|
||||
let stages: Vec<_> = m
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.iter().map(|s| s.as_str()).collect::<Vec<_>>()))
|
||||
.collect();
|
||||
assert_eq!(stages.len(), 4);
|
||||
assert_eq!(stages[0].0, "kms");
|
||||
assert_eq!(stages[0].1, vec!["vesad", "redox-drm"]);
|
||||
assert_eq!(stages[3].0, "boot");
|
||||
assert_eq!(stages[3].1, vec!["ps2d", "xhcid"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initfs_manifest_walks_stages_in_canonical_order() {
|
||||
let p = std::env::temp_dir().join("rb-dm-initfs-order");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
// Declare stages in REVERSE canonical order to confirm the
|
||||
// loader sorts by canonical order, not declaration order.
|
||||
let body = r#"
|
||||
[boot]
|
||||
drivers = ["ps2d"]
|
||||
|
||||
[kms]
|
||||
drivers = ["vesad"]
|
||||
|
||||
[filesystems]
|
||||
drivers = ["redoxfs"]
|
||||
|
||||
[block]
|
||||
drivers = ["nvmed"]
|
||||
"#;
|
||||
fs::write(p.join("initfs.manifest"), body).unwrap();
|
||||
let m = InitfsManifest::load_dir(&p).unwrap();
|
||||
let stages: Vec<_> = m
|
||||
.iter()
|
||||
.map(|(k, _)| k.as_str())
|
||||
.collect();
|
||||
assert_eq!(stages, vec!["kms", "block", "filesystems", "boot"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initfs_manifest_skips_empty_stages() {
|
||||
let p = std::env::temp_dir().join("rb-dm-initfs-empty");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
let body = r#"
|
||||
[kms]
|
||||
drivers = []
|
||||
|
||||
[block]
|
||||
drivers = ["nvmed"]
|
||||
"#;
|
||||
fs::write(p.join("initfs.manifest"), body).unwrap();
|
||||
let m = InitfsManifest::load_dir(&p).unwrap();
|
||||
// Empty kms stage should be skipped (not included).
|
||||
assert_eq!(m.len(), 1);
|
||||
let only = m.iter().next().unwrap();
|
||||
assert_eq!(only.0.as_str(), "block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initfs_manifest_skips_empty_driver_names() {
|
||||
let p = std::env::temp_dir().join("rb-dm-initfs-empty-name");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
let body = r#"
|
||||
[block]
|
||||
drivers = ["", "nvmed", ""]
|
||||
"#;
|
||||
fs::write(p.join("initfs.manifest"), body).unwrap();
|
||||
let m = InitfsManifest::load_dir(&p).unwrap();
|
||||
let block_drivers = &m.iter().next().unwrap().1;
|
||||
assert_eq!(block_drivers, &vec!["nvmed".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initfs_stage_kind_canonical_order_is_stable() {
|
||||
let order = InitfsStageKind::canonical_order();
|
||||
assert_eq!(order.len(), 4);
|
||||
assert_eq!(order[0], InitfsStageKind::Kms);
|
||||
assert_eq!(order[1], InitfsStageKind::Block);
|
||||
assert_eq!(order[2], InitfsStageKind::Filesystems);
|
||||
assert_eq!(order[3], InitfsStageKind::Boot);
|
||||
// Ordinal impls must satisfy canonical-order sort.
|
||||
assert!(order.windows(2).all(|w| w[0] < w[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_initfs_manifest_supports_replace() {
|
||||
let p = std::env::temp_dir().join("rb-dm-shared-initfs");
|
||||
let _ = fs::remove_dir_all(&p);
|
||||
fs::create_dir_all(p.clone()).unwrap();
|
||||
let sh = SharedInitfsManifest::load_from(p.clone()).unwrap();
|
||||
assert_eq!(sh.stages().len(), 0);
|
||||
fs::write(
|
||||
p.join("initfs.manifest"),
|
||||
r#"
|
||||
[block]
|
||||
drivers = ["nvmed", "ahcid"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let n = sh.replace().unwrap();
|
||||
assert_eq!(n, 2);
|
||||
let stages = sh.stages();
|
||||
assert_eq!(stages.len(), 1);
|
||||
assert_eq!(stages[0].0, InitfsStageKind::Block);
|
||||
assert_eq!(stages[0].1, vec!["nvmed", "ahcid"]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user