diff options
| author | Hermes Agent <hermes@localhost> | 2026-08-12 04:18:12 +0000 |
|---|---|---|
| committer | Hermes Agent <hermes@localhost> | 2026-08-12 04:18:12 +0000 |
| commit | 39a23d9035ab99191e305db3a55a8df1606f63b3 (patch) | |
| tree | ac48f8d44ce5c884b4694ea88e806e9cb08ee2c6 | |
| parent | fc4e46c41bfb769193159143f6567341cd3e3a51 (diff) | |
Make rootfs finalization repeatable
| -rw-r--r-- | src/rootfs.rs | 21 | ||||
| -rw-r--r-- | tests/rootfs_finalization.rs | 36 |
2 files changed, 54 insertions, 3 deletions
diff --git a/src/rootfs.rs b/src/rootfs.rs index 06b2557..9246aa8 100644 --- a/src/rootfs.rs +++ b/src/rootfs.rs @@ -167,7 +167,7 @@ fn copy_entry( if let Some(parent) = output.parent() { fs::create_dir_all(parent)?; } - symlink(&target, &output).with_context(|| format!("create symlink {}", output.display()))?; + replace_with_symlink(&target, &output)?; manifest.record(destination); } else if metadata.file_type().is_file() { if let Some(parent) = output.parent() { @@ -204,8 +204,23 @@ fn enable_service(rootfs: &Path, service: &ServiceName, manifest: &mut MutationM ))?; let output = rootfs.join(destination.as_path()); fs::create_dir_all(output.parent().expect("service path has a parent"))?; - symlink(format!("/usr/lib/systemd/system/{}", service.0), &output) - .with_context(|| format!("enable service {}", service.0))?; + replace_with_symlink( + Path::new(&format!("/usr/lib/systemd/system/{}", service.0)), + &output, + ) + .with_context(|| format!("enable service {}", service.0))?; manifest.record(destination.as_path()); Ok(()) } + +fn replace_with_symlink(target: &Path, output: &Path) -> Result<()> { + match fs::symlink_metadata(output) { + Ok(metadata) if metadata.file_type().is_dir() => { + bail!("cannot replace directory with symlink: {}", output.display()); + } + Ok(_) => fs::remove_file(output).with_context(|| format!("replace {}", output.display()))?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error).with_context(|| format!("inspect {}", output.display())), + } + symlink(target, output).with_context(|| format!("create symlink {}", output.display())) +} diff --git a/tests/rootfs_finalization.rs b/tests/rootfs_finalization.rs index fa0cf3b..3a98290 100644 --- a/tests/rootfs_finalization.rs +++ b/tests/rootfs_finalization.rs @@ -79,3 +79,39 @@ fn finalization_rejects_rootfs_escaping_destinations_and_symlinks() { .expect_err("unsafe symlink must be rejected"); assert!(error.to_string().contains("escapes copied tree")); } + +#[test] +fn finalization_is_idempotent_for_matching_symlinks() { + let fixture = tempdir().expect("fixture directory"); + let source = fixture.path().join("overlay"); + fs::create_dir_all(&source).expect("create source tree"); + symlink("controller.conf", source.join("controller-link")).expect("create source symlink"); + fs::write(source.join("controller.conf"), "controller\n").expect("write source file"); + let rootfs = tempdir().expect("rootfs directory"); + let finalization = RootfsFinalization::new( + vec![CopyTree::new(&source, "etc/controller").expect("valid copy destination")], + InitrdOem::new(std::iter::empty::<&str>(), std::iter::empty::<&str>()), + vec![ServiceName::new("chronyd.service").expect("valid service")], + ); + + finalization.apply(rootfs.path()).expect("first finalization succeeds"); + let second = finalization.apply(rootfs.path()).expect("repeat finalization succeeds"); + + assert_eq!( + fs::read_link(rootfs.path().join("etc/controller/controller-link")).expect("copied symlink"), + Path::new("controller.conf") + ); + assert_eq!( + fs::read_link( + rootfs + .path() + .join("etc/systemd/system/multi-user.target.wants/chronyd.service") + ) + .expect("enabled service"), + Path::new("/usr/lib/systemd/system/chronyd.service") + ); + assert!(second + .created_paths() + .iter() + .any(|path| path == "etc/controller/controller-link")); +} |