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:
2026-07-27 14:15:23 +09:00
parent 0b19fddd2c
commit 08cc41d705
@@ -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"]);
}
}