From c7dc1a0173a14e9cb52adcb09ae566cf7eeb314a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 12 Aug 2026 06:02:57 +0000 Subject: Protect companion manifests from overwrite --- src/build.rs | 4 ++++ src/manifest.rs | 10 ++++++++-- tests/build.rs | 23 +++++++++++++++++++++++ tests/compare.rs | 15 +++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/build.rs b/src/build.rs index 42c57ef..fcacfc0 100644 --- a/src/build.rs +++ b/src/build.rs @@ -42,6 +42,10 @@ impl BuildExecutor { if artifact.exists() { bail!("output artifact already exists; refusing to overwrite it: {}", artifact.display()); } + let manifest = ArtifactManifest::path_beside(artifact)?; + if manifest.exists() { + bail!("output manifest already exists; refusing to overwrite it: {}", manifest.display()); + } let parent = artifact .parent() .filter(|parent| !parent.as_os_str().is_empty()) diff --git a/src/manifest.rs b/src/manifest.rs index 0848a98..4cd256c 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashSet}; use std::fs; -use std::io::Read; +use std::io::{Read, Write}; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; @@ -51,7 +51,13 @@ impl ArtifactManifest { pub fn write_beside(&self, artifact: impl AsRef) -> Result { let path = Self::path_beside(artifact)?; let text = toml::to_string_pretty(self).context("serialize artifact manifest")?; - fs::write(&path, text).with_context(|| format!("write artifact manifest {}", path.display()))?; + fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .with_context(|| format!("create artifact manifest {}", path.display()))? + .write_all(text.as_bytes()) + .with_context(|| format!("write artifact manifest {}", path.display()))?; Ok(path) } diff --git a/tests/build.rs b/tests/build.rs index 6613cc7..c6ed70d 100644 --- a/tests/build.rs +++ b/tests/build.rs @@ -112,6 +112,29 @@ fn rejects_an_existing_artifact_before_creating_the_workspace_or_installing() { assert!(installer.requests.borrow().is_empty()); } +#[test] +fn rejects_an_existing_companion_manifest_before_creating_the_workspace_or_installing() { + let fixture = tempdir().expect("fixture directory"); + let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) + .expect("load controller spec"); + let plan = BuildPlan::compile(spec).expect("compile build plan"); + let installer = FixtureInstaller::default(); + let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder); + let workspace = fixture.path().join("work"); + let artifact = fixture.path().join("controller.tar"); + let companion = ArtifactManifest::path_beside(&artifact).expect("companion manifest path"); + fs::write(&companion, "do not replace").expect("write pre-existing companion manifest"); + + let error = executor + .execute(&plan, &workspace, "profiles/apt.conf", &artifact) + .expect_err("pre-existing companion manifest must be rejected"); + + assert!(error.to_string().contains("output manifest already exists")); + assert_eq!(fs::read_to_string(&companion).expect("read companion manifest"), "do not replace"); + assert!(!workspace.exists()); + assert!(installer.requests.borrow().is_empty()); +} + #[test] fn rejects_a_missing_apt_configuration_before_creating_the_workspace() { let fixture = tempdir().expect("fixture directory"); diff --git a/tests/compare.rs b/tests/compare.rs index f7b3a04..4f48721 100644 --- a/tests/compare.rs +++ b/tests/compare.rs @@ -158,6 +158,21 @@ fn writes_distinct_manifests_for_artifacts_in_the_same_directory() { assert_eq!(ArtifactManifest::load(&native_path).expect("load native manifest"), native_manifest); } +#[test] +fn refuses_to_overwrite_an_existing_companion_manifest() { + let fixture = tempdir().expect("temporary directory"); + let artifact = fixture.path().join("controller.tar"); + let manifest_path = ArtifactManifest::path_beside(&artifact).expect("manifest path"); + fs::write(&manifest_path, "preserve this manifest").expect("write existing manifest"); + + let error = baseline() + .write_beside(&artifact) + .expect_err("existing companion manifests must not be overwritten"); + + assert!(error.to_string().contains("create artifact manifest")); + assert_eq!(fs::read_to_string(&manifest_path).expect("read existing manifest"), "preserve this manifest"); +} + #[test] fn rejects_duplicate_semantic_keys() { let error = ArtifactManifest::new( -- cgit v1.2.3