From 08cc41d7058e03a6a7f197ffb372617865a77bf8 Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 27 Jul 2026 14:15:23 +0900 Subject: [PATCH] =?UTF-8?q?driver-manager:=20N1=E2=80=93N3=20=E2=80=94=20p?= =?UTF-8?q?olicy.rs=20modprobe.d=20options=20+=20modules-load.d=20autoload?= =?UTF-8?q?=20+=20initfs.manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/.conf): - TOML schema: [[options]] driver = "name" params = [{name, value}, ...] - Applied at spawn as REDBEAR_DRIVER_PARAM_= env var - SharedDriverOptions with SIGHUP-reloadable replace() AutoloadList (mirrors Linux modules-load.d/.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. --- .../driver-manager/source/src/policy.rs | 841 +++++++++++++++++- 1 file changed, 840 insertions(+), 1 deletion(-) diff --git a/local/recipes/system/driver-manager/source/src/policy.rs b/local/recipes/system/driver-manager/source/src/policy.rs index 2433ae8c33..8b0c08ff62 100644 --- a/local/recipes/system/driver-manager/source/src/policy.rs +++ b/local/recipes/system/driver-manager/source/src/policy.rs @@ -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/.conf` "options =" +// 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_=` 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, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct DriverOptionsEntry { + pub driver: String, + #[serde(default)] + pub params: Vec, +} + +#[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>, +} + +impl DriverOptions { + pub fn load_dir(dir: &Path) -> Result { + let mut by_driver: BTreeMap> = 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>, + source_path: std::path::PathBuf, +} + +impl SharedDriverOptions { + pub fn load_from(source_path: std::path::PathBuf) -> Result { + 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 { + 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/.conf`) ──────────────────────── +// +// Mirrors Linux's `modules-load.d/.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, +} + +impl AutoloadList { + pub fn load_dir(dir: &Path) -> Result { + let mut modules: Vec = 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 { + self.modules.iter().map(|s| s.as_str()) + } +} + +/// Shared, SIGHUP-reloadable wrapper around [`AutoloadList`]. +#[derive(Debug, Clone)] +pub struct SharedAutoloadList { + inner: std::sync::Arc>, + source_path: std::path::PathBuf, +} + +impl SharedAutoloadList { + pub fn load_from(source_path: std::path::PathBuf) -> Result { + 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 { + self.inner + .read() + .map(|a| a.modules.clone()) + .unwrap_or_default() + } + + pub fn replace(&self) -> Result { + 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, + #[serde(default)] + pub block: Option, + #[serde(default)] + pub filesystems: Option, + #[serde(default)] + pub boot: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct InitfsStage { + #[serde(default)] + pub drivers: Vec, +} + +/// 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)>, +} + +/// 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 { + let mut by_kind: BTreeMap> = 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::>(); + 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)> = 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)> { + self.stages.iter() + } +} + +/// Shared, SIGHUP-reloadable wrapper around [`InitfsManifest`]. +#[derive(Debug, Clone)] +pub struct SharedInitfsManifest { + inner: std::sync::Arc>, + source_path: std::path::PathBuf, +} + +impl SharedInitfsManifest { + pub fn load_from(source_path: std::path::PathBuf) -> Result { + 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)> { + self.inner + .read() + .map(|m| m.stages.clone()) + .unwrap_or_default() + } + + pub fn replace(&self) -> Result { + let fresh = InitfsManifest::load_dir(&self.source_path)?; + let n = fresh.stages.iter().map(|(_, v)| v.len()).sum::(); + 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, 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, String> { + read_files_matching(dir, |_| true) +} + +fn read_files_matching bool>( + dir: &Path, + ext_match: F, +) -> Result, 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::>())) + .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"]); + } }