installer: commit collision-tracker WIP to satisfy clean-fork build gate

Pre-existing uncommitted work (materialized by the build's local-over-WIP
preflight): CollisionTracker detects config [[files]] (Layer 1) being
silently overwritten by package staging (Layer 2) — the D-Bus regression
class. Committed as-is so the fork has a clean HEAD for pkgar fingerprints;
no functional change intended by this commit beyond tracking it.
This commit is contained in:
Red Bear OS
2026-07-18 16:07:53 +09:00
parent 460d9530a5
commit bf43c0a34f
4 changed files with 780 additions and 0 deletions
+418
View File
@@ -0,0 +1,418 @@
//! Runtime file-collision detection for `install_dir()`.
//!
//! The installer writes files in four layers (see AGENTS.md § "Installer File
//! Layering"):
//!
//! ```text
//! Layer 1: Config pre-install [[files]] (postinstall = false)
//! Layer 2: Package staging (install_packages())
//! Layer 3: Config post-install [[files]] (postinstall = true)
//! Layer 4: User/group creation (passwd, shadow, group)
//! ```
//!
//! Last writer wins. Layer 2 silently overwriting Layer 1 is the D-Bus
//! regression class: a config `[[files]]` entry writes to a path, a package
//! stages the same path during `install_packages()`, the package wins, the
//! operator's customization is gone — and the build succeeds. This module
//! makes that class of bug visible at install time.
//!
//! ## Behavior
//!
//! - Every file write (regular file or symlink) is recorded with its layer
//! and source.
//! - When a later layer overwrites a path written by an earlier layer, a
//! `[COLLISION-WARN]` line is emitted to stderr. The build-time
//! `scripts/validate-collision-log.sh` greps logs for that marker.
//! - Layer 3 (`ConfigPostInstall`) is the *intentional override* layer — its
//! whole purpose is to overwrite package defaults. Collisions from Layer 3
//! are suppressed by design and never warn.
//! - Known-safe override path pairs (e.g. `/etc/init.d/` over
//! `/usr/lib/init.d/`, mirroring the init system's `config_for_dirs()`
//! priority) are also suppressed.
//! - Strict mode (`REDBEAR_INSTALLER_STRICT_COLLISIONS=1`) promotes
//! collisions to hard errors via `anyhow::bail!`. Default is warn-only.
//!
//! ## What is NOT tracked
//!
//! Directory creation. `fs::create_dir_all` is idempotent; two layers
//! creating the same directory is a no-op, not a collision. Only regular
//! files and symlinks are recorded.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
/// Installer file-write layers, in write order. The numeric value matches
/// the layer numbering in AGENTS.md § "Installer File Layering" and is used
/// in user-facing warning text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Layer {
/// `[[files]]` entries with `postinstall = false`. Written first.
ConfigPreInstall = 1,
/// Package staging via `install_packages()`. Layer 2 overwriting
/// Layer 1 is the D-Bus regression class (silent loss of config).
PackageStaging = 2,
/// `[[files]]` entries with `postinstall = true`. Intentional override
/// layer — collisions from this layer never warn by design.
ConfigPostInstall = 3,
/// Files the installer itself writes at the end of `install_dir()`
/// (`/etc/passwd`, `/etc/shadow`, `/etc/group`).
InstallerBuiltin = 4,
}
impl Layer {
/// Numeric layer identifier as documented in AGENTS.md. Stable across
/// releases — used in `[COLLISION-WARN]` output.
pub fn as_u8(self) -> u8 {
self as u8
}
}
/// Where a tracked file came from.
#[derive(Debug, Clone)]
pub enum ProvenanceSource {
/// `[[files]]` entry. The string is the config-visible path
/// (leading-slash form, e.g. `"/etc/passwd"`).
ConfigFile(String),
/// Package staging. The string is a package identifier, or `"staged"`
/// when the writing package cannot be identified (the common case —
/// `install_packages()` does not report per-file provenance).
Package(String),
/// Installer-builtin file (`/etc/passwd`, `/etc/shadow`, `/etc/group`).
InstallerBuiltin,
}
/// Provenance record for one file path.
#[derive(Debug, Clone)]
pub struct FileProvenance {
/// What wrote the file.
pub source: ProvenanceSource,
/// Which install layer wrote it.
pub layer: Layer,
}
/// One collision event recorded by the tracker.
#[derive(Debug, Clone)]
pub struct Collision {
/// Absolute filesystem path that was overwritten.
pub path: PathBuf,
/// Provenance of the earlier (overwritten) write.
pub previous: FileProvenance,
/// Provenance of the later (overwriting) write.
pub current: FileProvenance,
}
/// Marker emitted on stderr for every collision warning. The build-time
/// `scripts/validate-collision-log.sh` script greps logs for this token.
pub const WARN_MARKER: &str = "[COLLISION-WARN]";
/// Marker emitted on stderr when strict mode promotes a collision to an
/// error. Paired with `WARN_MARKER` in the same message so log-grepping
/// for either token catches every collision.
pub const ERROR_MARKER: &str = "[COLLISION-ERROR]";
/// Environment variable name that opts into strict mode (promotes
/// collisions to hard errors). Default is warn-only.
pub const STRICT_ENV_VAR: &str = "REDBEAR_INSTALLER_STRICT_COLLISIONS";
/// Pairs of `(override_dir, package_default_dir)`. When the current write
/// is under `override_dir` and the previous write was under
/// `package_default_dir`, the collision is considered intentional and is
/// suppressed. These mirror the init system's `config_for_dirs()` priority
/// directories documented in AGENTS.md § "Init Service File Ownership".
const KNOWN_SAFE_OVERRIDE_PAIRS: &[(&str, &str)] = &[
// Config overrides at /etc/init.d/ intentionally replace package
// defaults at /usr/lib/init.d/ for the same filename. The init
// system's config_for_dirs() BTreeMap gives /etc/init.d/ priority.
("/etc/init.d", "/usr/lib/init.d"),
// Likewise for environment.d — config overrides at /etc/ replace
// package defaults at /usr/lib/.
("/etc/environment.d", "/usr/lib/environment.d"),
];
/// Returns true if `(previous_config_path, current_config_path)` matches a
/// known-safe override pair: previous under a package default dir, current
/// under the matching config override dir. Both arguments are absolute
/// config-visible paths (leading `/`).
fn is_known_safe_override(previous_config_path: &str, current_config_path: &str) -> bool {
for &(override_dir, default_dir) in KNOWN_SAFE_OVERRIDE_PAIRS {
let curr_is_override =
current_config_path == override_dir || is_dir_member(current_config_path, override_dir);
let prev_is_default =
previous_config_path == default_dir || is_dir_member(previous_config_path, default_dir);
if curr_is_override && prev_is_default {
return true;
}
}
false
}
/// True if `child` is a file path strictly inside `parent` (not `parent`
/// itself). Both arguments are absolute config-visible paths with no
/// trailing slash. Uses byte comparison to stay I/O-free.
fn is_dir_member(child: &str, parent: &str) -> bool {
// We expect paths like "/etc/init.d/foo" inside "/etc/init.d".
let parent_with_slash = format!("{parent}/");
child.starts_with(&parent_with_slash)
}
/// File-write provenance tracker for `install_dir()`.
///
/// Construct with [`CollisionTracker::new`] to pick up strict mode from the
/// environment, or with [`CollisionTracker::new_with_strict`] for tests and
/// explicit control. Call [`CollisionTracker::record`] before each file
/// write, and [`CollisionTracker::scan_package_staging`] once after
/// `install_packages()` returns.
pub struct CollisionTracker {
/// Map from absolute filesystem path to the most recent provenance.
files: HashMap<PathBuf, FileProvenance>,
/// Map from absolute filesystem path back to the leading-`/` config
/// path, when known. Lets the known-safe override check compare
/// previous vs. current config-visible paths even when both resolves
/// land at the same physical file (e.g. via a symlinked /etc/init.d).
config_paths: HashMap<PathBuf, String>,
strict: bool,
collisions: Vec<Collision>,
}
impl CollisionTracker {
/// Construct a tracker, reading strict mode from the
/// `REDBEAR_INSTALLER_STRICT_COLLISIONS` environment variable.
pub fn new() -> Self {
let strict = std::env::var(STRICT_ENV_VAR)
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
Self::new_with_strict(strict)
}
/// Construct a tracker with explicit strict-mode control.
///
/// Tests should prefer this over [`Self::new`] to avoid env-var races
/// under parallel test execution.
pub fn new_with_strict(strict: bool) -> Self {
Self {
files: HashMap::new(),
config_paths: HashMap::new(),
strict,
collisions: Vec::new(),
}
}
/// Number of distinct file paths currently tracked.
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.files.len()
}
/// Whether the tracker has observed any file writes.
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
/// Number of collisions recorded (warnings emitted, or errors raised in
/// strict mode).
pub fn collision_count(&self) -> usize {
self.collisions.len()
}
/// Snapshot of recorded collisions (for test inspection and reporting).
pub fn collisions(&self) -> &[Collision] {
&self.collisions
}
/// Whether strict mode is enabled on this tracker.
#[allow(dead_code)]
pub fn is_strict(&self) -> bool {
self.strict
}
/// Record a file write at `abs_path` (absolute filesystem path) with
/// the given provenance.
///
/// `config_path` is the leading-`/` config-visible path when known
/// (used for the known-safe override check), or `None` for paths whose
/// config-visible form is not meaningful.
///
/// # Collisions
///
/// A collision is when the same `abs_path` was previously written by a
/// *different* layer. Collisions emit `[COLLISION-WARN]` on stderr and
/// are recorded. In strict mode they additionally return `Err`.
///
/// # Suppression
///
/// Warnings are suppressed when either:
/// - the current write is from `Layer::ConfigPostInstall` (the
/// intentional override layer), or
/// - the previous config path and the current config path form a
/// known-safe override pair (e.g. `/etc/init.d/X` over
/// `/usr/lib/init.d/X`).
///
/// Suppressed collisions are still recorded in the `files` map (the
/// latest provenance wins) but do not emit a warning and do not
/// increment `collision_count()`.
pub fn record(
&mut self,
abs_path: PathBuf,
provenance: FileProvenance,
config_path: Option<&str>,
) -> Result<()> {
// Capture previous state BEFORE any mutation. The known-safe
// override check compares the previous config_path against the
// current one; if we let the insert below clobber the map first,
// we'd compare the new path against itself.
let previous_provenance = self.files.get(&abs_path).cloned();
let previous_config_path = self.config_paths.get(&abs_path).cloned();
if let Some(cp) = config_path {
self.config_paths.insert(abs_path.clone(), cp.to_string());
}
let Some(previous) = previous_provenance else {
self.files.insert(abs_path, provenance);
return Ok(());
};
// Same-layer re-write is not a collision. Config merges already
// deduplicate same-path entries; package staging may legitimately
// touch a file twice (e.g. an inner archive extraction).
if previous.layer == provenance.layer {
self.files.insert(abs_path, provenance);
return Ok(());
}
// Layer 3 is the intentional override layer by design. Its whole
// purpose is to overwrite package defaults — collisions from it
// must not warn or the override mechanism would be unusable.
let is_layer3_override = provenance.layer == Layer::ConfigPostInstall;
// Known-safe override path pairs (e.g. /etc/init.d over
// /usr/lib/init.d). Only meaningful when both layers recorded a
// config-visible path for the same abs_path.
let is_known_safe = match (previous_config_path.as_deref(), config_path) {
(Some(prev_cp), Some(curr_cp)) => is_known_safe_override(prev_cp, curr_cp),
_ => false,
};
if is_layer3_override || is_known_safe {
self.files.insert(abs_path, provenance);
return Ok(());
}
// Real collision — warn (and bail in strict mode).
let collision = Collision {
path: abs_path.clone(),
previous: previous.clone(),
current: provenance.clone(),
};
self.collisions.push(collision.clone());
emit_warning(&collision);
self.files.insert(abs_path, provenance);
if self.strict {
bail!(
"{ERROR_MARKER} strict mode ({STRICT_ENV_VAR}=1): collision at {} \
— {} layer {} overwritten by {} layer {}. Aborting install. \
Fix the conflicting paths, or unset {STRICT_ENV_VAR} to demote \
this to a warning.",
collision.path.display(),
source_label(&collision.previous.source),
collision.previous.layer.as_u8(),
source_label(&collision.current.source),
collision.current.layer.as_u8(),
);
}
Ok(())
}
/// Walk `dest` after `install_packages()` returns, recording every file
/// the package staging wrote.
///
/// Files already tracked from Layer 1 trigger the Layer 2 overwrites
/// Layer 1 collision (the D-Bus regression class). Files not previously
/// tracked are recorded with `Layer::PackageStaging` provenance.
///
/// Directories are not tracked — `fs::create_dir_all` is idempotent and
/// directory paths are shared infrastructure. Only regular files and
/// symlinks are recorded.
///
/// I/O errors during the walk are tolerated (treated as "no file here")
/// so a partially-populated tree does not abort the install. The
/// collision decision is made from what we *can* read.
pub fn scan_package_staging(&mut self, dest: &Path) -> Result<()> {
if !dest.exists() {
return Ok(());
}
walk_for_files(dest, dest, self)
}
}
impl Default for CollisionTracker {
fn default() -> Self {
Self::new()
}
}
/// Recursive walker that records every file (regular or symlink) under
/// `root` as a Layer 2 (PackageStaging) write.
fn walk_for_files(root: &Path, current: &Path, tracker: &mut CollisionTracker) -> Result<()> {
let read_dir = match std::fs::read_dir(current) {
Ok(rd) => rd,
// Tolerate unreadable subdirs — they contribute no files.
Err(_) => return Ok(()),
};
for entry in read_dir {
let Ok(entry) = entry else {
continue;
};
let Ok(file_type) = entry.file_type() else {
continue;
};
let abs_path = entry.path();
if file_type.is_symlink() || file_type.is_file() {
let provenance = FileProvenance {
source: ProvenanceSource::Package("staged".to_string()),
layer: Layer::PackageStaging,
};
// For package-staged files the config-visible path is the path
// under `root` (with a leading `/`). This matches how a config
// `[[files]]` entry would address the same file.
let config_visible = match abs_path.strip_prefix(root) {
Ok(rel) => format!("/{}", rel.display()),
Err(_) => abs_path.to_string_lossy().into_owned(),
};
tracker.record(abs_path, provenance, Some(&config_visible))?;
} else if file_type.is_dir() {
walk_for_files(root, &abs_path, tracker)?;
}
}
Ok(())
}
/// Emit a single `[COLLISION-WARN]` line on stderr. The build's
/// `validate-collision-log.sh` greps for this token after the install
/// finishes.
fn emit_warning(c: &Collision) {
eprintln!(
"{WARN_MARKER} {} layer {} overwritten by layer {} ({} -> {}). \
Set {STRICT_ENV_VAR}=1 to promote this to an error.",
c.path.display(),
c.previous.layer.as_u8(),
c.current.layer.as_u8(),
source_label(&c.previous.source),
source_label(&c.current.source),
);
}
/// Short human-readable label for a provenance source, used in warning and
/// error text.
fn source_label(s: &ProvenanceSource) -> &'static str {
match s {
ProvenanceSource::ConfigFile(_) => "config",
ProvenanceSource::Package(_) => "package",
ProvenanceSource::InstallerBuiltin => "installer",
}
}
+59
View File
@@ -5,6 +5,7 @@ use rand::{rngs::OsRng, TryRngCore};
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem, BLOCK_SIZE};
use termion::input::TermRead;
use crate::collision::{CollisionTracker, FileProvenance, Layer, ProvenanceSource};
use crate::config::file::FileConfig;
use crate::config::package::PackageConfig;
use crate::config::Config;
@@ -138,16 +139,29 @@ pub fn install_dir(
let output_dir = output_dir.to_owned();
let mut tracker = CollisionTracker::new();
// Layer 1: config pre-install [[files]] (postinstall = false).
for file in &config.files {
if !file.postinstall {
record_config_file(&mut tracker, &output_dir, file, Layer::ConfigPreInstall)?;
file.create(&output_dir)?;
}
}
// Layer 2: package staging. install_packages() is opaque to per-file
// provenance, so we walk output_dir afterwards to register what it wrote
// and detect the Layer 2 overwrites Layer 1 collision (D-Bus regression
// class — config edits silently lost).
install_packages(&config, &output_dir, cookbook)?;
tracker.scan_package_staging(&output_dir)?;
// Layer 3: config post-install [[files]] (postinstall = true). This is
// the intentional override layer — CollisionTracker suppresses its
// collisions by design.
for file in &config.files {
if file.postinstall {
record_config_file(&mut tracker, &output_dir, file, Layer::ConfigPostInstall)?;
file.create(&output_dir)?;
}
}
@@ -228,11 +242,16 @@ pub fn install_dir(
groups.push((group, gid, group_config.members));
}
// Layer 4: installer-builtin files (/etc/passwd, /etc/shadow, /etc/group).
// A collision here means a package or config also wrote one of these
// core files — almost always a real misconfiguration worth surfacing.
if !passwd.is_empty() {
record_installer_builtin(&mut tracker, &output_dir, "/etc/passwd")?;
FileConfig::new_file("/etc/passwd".to_string(), passwd).create(&output_dir)?;
}
if !shadow.is_empty() {
record_installer_builtin(&mut tracker, &output_dir, "/etc/shadow")?;
FileConfig::new_file("/etc/shadow".to_string(), shadow)
.with_mod(0o0600, 0, 0)
.create(&output_dir)?;
@@ -250,6 +269,7 @@ pub fn install_dir(
println!("\tMembers: {}", members.join(", "));
}
record_installer_builtin(&mut tracker, &output_dir, "/etc/group")?;
FileConfig::new_file("/etc/group".to_string(), groups_data)
.with_mod(0o0600, 0, 0)
.create(&output_dir)?;
@@ -258,6 +278,45 @@ pub fn install_dir(
Ok(())
}
// CollisionTracker wiring helpers. Each records the provenance of one file
// write that is about to happen; the actual write (`FileConfig::create`)
// is performed by the caller. Directory creations are not recorded (see
// collision.rs module docs).
fn record_config_file(
tracker: &mut CollisionTracker,
output_dir: &Path,
file: &FileConfig,
layer: Layer,
) -> Result<()> {
if file.directory {
return Ok(());
}
let abs_path = output_dir.join(file.path.trim_start_matches('/'));
tracker.record(
abs_path,
FileProvenance {
source: ProvenanceSource::ConfigFile(file.path.clone()),
layer,
},
Some(&file.path),
)
}
fn record_installer_builtin(
tracker: &mut CollisionTracker,
output_dir: &Path,
config_path: &str,
) -> Result<()> {
tracker.record(
output_dir.join(config_path.trim_start_matches('/')),
FileProvenance {
source: ProvenanceSource::InstallerBuiltin,
layer: Layer::InstallerBuiltin,
},
Some(config_path),
)
}
fn prepare_user_home(
output_dir: &PathBuf,
uid: u32,
+8
View File
@@ -1,6 +1,10 @@
#[macro_use]
extern crate serde_derive;
// Not feature-gated: only depends on std + anyhow, so the cookbook can pull
// it in via default-features = false without the installer feature deps.
pub mod collision;
mod config;
#[cfg(feature = "installer")]
mod disk_wrapper;
@@ -9,6 +13,10 @@ mod installer;
#[cfg(feature = "installer")]
pub use crate::installer::*;
pub use crate::collision::{
Collision, CollisionTracker, FileProvenance, Layer, ProvenanceSource, ERROR_MARKER,
STRICT_ENV_VAR, WARN_MARKER,
};
pub use crate::config::file::FileConfig;
pub use crate::config::package::PackageConfig;
pub use crate::config::Config;
+295
View File
@@ -0,0 +1,295 @@
//! Integration tests for the runtime file-collision tracker.
//!
//! These exercise the public API only (`CollisionTracker` and friends,
//! re-exported at the crate root). They live in `tests/` rather than inline
//! in `src/collision.rs` to keep the production module under the 250 pure
//! LOC ceiling and to enforce the public-API-only test contract — the
//! tracker's private helpers are not touched here, so a future internal
//! rewrite stays test-compatible as long as the public surface is stable.
use redox_installer::{
CollisionTracker, FileProvenance, Layer, ProvenanceSource, ERROR_MARKER, STRICT_ENV_VAR,
};
use std::path::PathBuf;
#[test]
fn test_no_collision_when_paths_distinct() {
// Given: an empty tracker and two distinct paths.
let mut tracker = CollisionTracker::new_with_strict(false);
let path_a = PathBuf::from("/output/etc/foo.conf");
let path_b = PathBuf::from("/output/etc/bar.conf");
// When: both paths are recorded from different layers.
tracker
.record(
path_a.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/foo.conf".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/etc/foo.conf"),
)
.unwrap();
tracker
.record(
path_b.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/bar.conf".to_string()),
layer: Layer::ConfigPostInstall,
},
Some("/etc/bar.conf"),
)
.unwrap();
// Then: no collisions, both paths tracked.
assert_eq!(tracker.collision_count(), 0, "distinct paths never collide");
assert_eq!(tracker.len(), 2);
assert!(tracker.collisions().is_empty());
}
#[test]
fn test_collision_warning_when_layer2_overwrites_layer1() {
// Given: a tracker with a Layer 1 (ConfigPreInstall) write to a path.
let mut tracker = CollisionTracker::new_with_strict(false);
let abs_path = PathBuf::from("/output/usr/lib/init.d/dbus");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/usr/lib/init.d/dbus".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/usr/lib/init.d/dbus"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// When: Layer 2 (PackageStaging) writes the same path.
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/usr/lib/init.d/dbus"),
);
// Then: a collision is recorded, the call succeeds (warn-only mode),
// and the collision metadata points at the right layers.
assert!(result.is_ok(), "warn-only mode must not error");
assert_eq!(
tracker.collision_count(),
1,
"Layer 2 overwriting Layer 1 is a collision"
);
let collision = &tracker.collisions()[0];
assert_eq!(collision.path, abs_path);
assert_eq!(collision.previous.layer, Layer::ConfigPreInstall);
assert_eq!(collision.current.layer, Layer::PackageStaging);
}
#[test]
fn test_no_warning_for_postinstall_override() {
// Given: a tracker where Layer 2 (PackageStaging) has written a path.
let mut tracker = CollisionTracker::new_with_strict(false);
let abs_path = PathBuf::from("/output/etc/system.conf");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/etc/system.conf"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// When: Layer 3 (ConfigPostInstall) writes the same path — the
// documented intentional-override pattern.
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/system.conf".to_string()),
layer: Layer::ConfigPostInstall,
},
Some("/etc/system.conf"),
);
// Then: no collision is recorded. Layer 3 is the override layer.
assert!(result.is_ok());
assert_eq!(
tracker.collision_count(),
0,
"Layer 3 postinstall overrides are intentional and must not warn"
);
assert!(tracker.collisions().is_empty());
}
#[test]
fn test_known_safe_override_paths_dont_warn() {
// Given: a tracker where Layer 2 (PackageStaging) has written the
// package-default init service at /usr/lib/init.d/dbus.
//
// In a real install the config override at /etc/init.d/dbus would
// resolve to a different abs_path and no collision would fire. The
// known-safe rule only matters when both writes land at the same
// abs_path — e.g. when /etc/init.d is a symlink to /usr/lib/init.d,
// or in unit tests that exercise the rule directly.
let mut tracker = CollisionTracker::new_with_strict(false);
let abs_path = PathBuf::from("/output/etc/init.d/dbus");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/usr/lib/init.d/dbus"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// When: a config write (Layer 1 — not Layer 3, so the layer-3
// suppression rule does not apply) targets /etc/init.d/dbus, which
// resolves to the same abs_path but matches the known-safe override
// pair (/etc/init.d over /usr/lib/init.d).
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/init.d/dbus".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/etc/init.d/dbus"),
);
// Then: no collision — the known-safe override pair suppresses the
// warning, even though the layers differ and neither is Layer 3.
assert!(result.is_ok());
assert_eq!(
tracker.collision_count(),
0,
"known-safe override pair (/etc/init.d over /usr/lib/init.d) must not warn"
);
// Sanity: the opposite direction (default after override) is NOT
// suppressed — that would be a package clobbering a config override,
// which is the D-Bus regression class.
let result2 = tracker.record(
abs_path,
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/usr/lib/init.d/dbus-other-direction"),
);
assert!(result2.is_ok());
assert_eq!(
tracker.collision_count(),
1,
"default-after-override direction is NOT a known-safe override"
);
}
#[test]
fn test_strict_mode_promotes_collision_to_error() {
// Given: a strict-mode tracker with a Layer 1 write.
let mut tracker = CollisionTracker::new_with_strict(true);
let abs_path = PathBuf::from("/output/etc/dbus.conf");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/dbus.conf".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/etc/dbus.conf"),
)
.unwrap();
// When: Layer 2 writes the same path.
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/etc/dbus.conf"),
);
// Then: the call returns Err and the error message references the
// strict env var so the operator knows how to demote if needed.
assert!(
result.is_err(),
"strict mode must promote collisions to errors"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains(ERROR_MARKER),
"strict-mode error must carry the {ERROR_MARKER} marker for log grepping"
);
assert!(
err.contains(STRICT_ENV_VAR),
"strict-mode error must name the env var so the operator can demote"
);
assert!(
err.contains("layer 1"),
"strict-mode error must name the overwritten layer"
);
assert!(
err.contains("layer 2"),
"strict-mode error must name the overwriting layer"
);
// And: the collision is still counted in the tracker's history.
assert_eq!(tracker.collision_count(), 1);
}
#[test]
fn test_scan_package_staging_detects_layer2_over_layer1_on_real_disk() {
// End-to-end: write a Layer 1 file to a tempdir, then ask
// scan_package_staging to walk it. The walk must detect the file as a
// Layer 2 write that collides with the already-recorded Layer 1 entry.
//
// This is the closest unit-level proof of the D-Bus regression class:
// a config wrote to a path, then a package stages the same path, and
// the walk catches it.
let dir = std::env::temp_dir().join(format!(
"redbear_collision_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
));
std::fs::create_dir_all(&dir).unwrap();
// Layer 1: a config writes /usr/lib/init.d/dbus.
let rel = "usr/lib/init.d/dbus";
let abs_path = dir.join(rel);
std::fs::create_dir_all(abs_path.parent().unwrap()).unwrap();
std::fs::write(&abs_path, b"# config layer 1").unwrap();
let mut tracker = CollisionTracker::new_with_strict(false);
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/usr/lib/init.d/dbus".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/usr/lib/init.d/dbus"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// Layer 2: the walk sees the same file on disk and flags the collision.
tracker.scan_package_staging(&dir).unwrap();
assert_eq!(
tracker.collision_count(),
1,
"scan_package_staging must catch a Layer 2 write to a Layer 1 path"
);
let _ = std::fs::remove_dir_all(&dir);
}