use std::fs::{self, File, OpenOptions};
use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use anyhow::{Context, bail};
pub(crate) fn safe_join(tree_root: &Path, rel: &Path) -> anyhow::Result<PathBuf> {
if rel.is_absolute() {
bail!(
"Artifact path {} must be relative to the entity tree",
rel.display()
);
}
if rel.components().any(|c| c == Component::ParentDir) {
bail!(
"Artifact path {} must not escape the entity tree",
rel.display()
);
}
Ok(tree_root.join(rel))
}
pub(crate) fn create_new_file(path: &Path) -> std::io::Result<File> {
OpenOptions::new().write(true).create_new(true).open(path)
}
pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {
static TEMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let dir = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.ok_or_else(|| anyhow::anyhow!("path has no parent dir: {}", path.display()))?;
let name = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("path has no file name: {}", path.display()))?;
let seq = TEMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let tmp = dir.join(format!(
".{}.{}.{}.tmp",
name.to_string_lossy(),
std::process::id(),
seq
));
#[expect(
clippy::disallowed_methods,
reason = "the seam itself — write_atomic's internal temp write"
)]
fs::write(&tmp, bytes).with_context(|| format!("Failed to write temp {}", tmp.display()))?;
fs::rename(&tmp, path)
.with_context(|| format!("Failed to rename {} -> {}", tmp.display(), path.display()))
}
pub(crate) fn is_real_dir(path: &Path) -> bool {
matches!(fs::symlink_metadata(path), Ok(m) if m.is_dir())
}
pub(crate) fn set_symlink(link: &Path, target: &Path) -> anyhow::Result<()> {
match fs::symlink_metadata(link) {
Ok(m) if m.file_type().is_symlink() => {
let current = fs::read_link(link)
.with_context(|| format!("Failed to read symlink {}", link.display()))?;
if current != target {
fs::remove_file(link)
.with_context(|| format!("Failed to replace symlink {}", link.display()))?;
symlink(target, link)?;
}
Ok(())
}
Ok(_) => bail!(
"Refusing to replace non-symlink {} with a symlink",
link.display()
),
Err(e) if e.kind() == ErrorKind::NotFound => symlink(target, link),
Err(e) => Err(e).with_context(|| format!("Failed to stat {}", link.display())),
}
}
fn symlink(target: &Path, link: &Path) -> anyhow::Result<()> {
std::os::unix::fs::symlink(target, link)
.with_context(|| format!("Failed to create symlink {}", link.display()))
}
pub(crate) fn copy_dir_all(src: &Path, dst: &Path) -> anyhow::Result<()> {
fs::create_dir_all(dst).with_context(|| format!("copy_dir_all: create {}", dst.display()))?;
for entry in
fs::read_dir(src).with_context(|| format!("copy_dir_all: read {}", src.display()))?
{
let entry = entry?;
let child = entry.path();
let dest = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir_all(&child, &dest)?;
} else {
fs::copy(&child, &dest)?;
}
}
Ok(())
}
#[derive(Debug)]
pub(crate) enum CopyOutcome {
Copied,
Skipped(String),
}
pub(crate) fn copy_selected(
source_root: &Path,
fork_root: &Path,
rel: &Path,
target_withheld: &dyn Fn(&Path) -> bool,
) -> anyhow::Result<CopyOutcome> {
let src = source_root.join(rel);
let meta = fs::symlink_metadata(&src).with_context(|| format!("stat {}", src.display()))?;
let real = fs::canonicalize(&src).with_context(|| format!("canonicalize {}", src.display()))?;
if !real.starts_with(source_root) {
return Ok(CopyOutcome::Skipped(format!(
"{} resolves outside the source tree",
rel.display()
)));
}
if meta.file_type().is_symlink() {
let real_rel = real
.strip_prefix(source_root)
.map_err(|e| anyhow::anyhow!("strip source prefix: {e}"))?;
if target_withheld(real_rel) {
return Ok(CopyOutcome::Skipped(format!(
"{} targets the withheld tier",
rel.display()
)));
}
}
if !real.is_file() {
return Ok(CopyOutcome::Skipped(format!(
"{} is not a regular file",
rel.display()
)));
}
let dest = fork_root.join(rel);
let parent = dest
.parent()
.ok_or_else(|| anyhow::anyhow!("dest {} has no parent", dest.display()))?;
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
let parent_canon =
fs::canonicalize(parent).with_context(|| format!("canonicalize {}", parent.display()))?;
if !parent_canon.starts_with(fork_root) {
return Ok(CopyOutcome::Skipped(format!(
"{} destination escapes the fork",
rel.display()
)));
}
let name = dest
.file_name()
.ok_or_else(|| anyhow::anyhow!("dest {} has no file name", dest.display()))?;
let final_dest = parent_canon.join(name);
fs::copy(&real, &final_dest)
.with_context(|| format!("copy {} -> {}", real.display(), final_dest.display()))?;
Ok(CopyOutcome::Copied)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn safe_join_accepts_a_tree_relative_path() {
let joined = safe_join(Path::new("/tree"), Path::new("003/x.toml")).unwrap();
assert_eq!(joined, Path::new("/tree/003/x.toml"));
}
#[test]
fn safe_join_rejects_absolute_paths() {
let err = safe_join(Path::new("/tree"), Path::new("/etc/passwd")).unwrap_err();
assert!(err.to_string().contains("must be relative"));
}
#[test]
fn safe_join_rejects_parent_escape() {
let err = safe_join(Path::new("/tree"), Path::new("../../etc/passwd")).unwrap_err();
assert!(err.to_string().contains("must not escape"));
}
#[test]
fn create_new_file_refuses_an_existing_target() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x");
assert!(create_new_file(&path).is_ok());
let err = create_new_file(&path).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
}
#[test]
fn write_atomic_creates_then_overwrites_leaving_no_temp() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.toml");
write_atomic(&path, b"v1").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "v1");
write_atomic(&path, b"v2").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "v2");
let names: Vec<String> = fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(names, ["x.toml"]);
}
#[test]
fn write_atomic_concurrent_writers_same_path_leave_no_torn_temp() {
use std::thread;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.toml");
const ITERS: usize = 200;
thread::scope(|s| {
let a = s.spawn(|| {
for _ in 0..ITERS {
write_atomic(&path, b"aaaa").unwrap();
}
});
let b = s.spawn(|| {
for _ in 0..ITERS {
write_atomic(&path, b"bbbb").unwrap();
}
});
a.join().unwrap();
b.join().unwrap();
});
let names: Vec<String> = fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(names, ["x.toml"], "no stray temps, only the target");
let got = fs::read_to_string(&path).unwrap();
assert!(
got == "aaaa" || got == "bbbb",
"content is a complete write, got {got:?}"
);
}
#[test]
fn is_real_dir_distinguishes_dirs_files_and_symlinks() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("d");
fs::create_dir(&real).unwrap();
let file = dir.path().join("f");
fs::write(&file, "x").unwrap();
let link = dir.path().join("l");
std::os::unix::fs::symlink(&real, &link).unwrap();
assert!(is_real_dir(&real));
assert!(!is_real_dir(&file));
assert!(!is_real_dir(&link));
assert!(!is_real_dir(&dir.path().join("absent")));
}
#[test]
fn set_symlink_creates_replaces_and_keeps() {
let dir = tempfile::tempdir().unwrap();
let link = dir.path().join("phases");
set_symlink(&link, Path::new("../target-a")).unwrap();
assert_eq!(fs::read_link(&link).unwrap(), Path::new("../target-a"));
set_symlink(&link, Path::new("../target-b")).unwrap();
assert_eq!(fs::read_link(&link).unwrap(), Path::new("../target-b"));
set_symlink(&link, Path::new("../target-b")).unwrap();
assert_eq!(fs::read_link(&link).unwrap(), Path::new("../target-b"));
}
#[test]
fn set_symlink_errors_on_a_real_file_squatting_the_path() {
let dir = tempfile::tempdir().unwrap();
let squat = dir.path().join("phases");
fs::write(&squat, "not a symlink").unwrap();
let err = set_symlink(&squat, Path::new("../target")).unwrap_err();
assert!(err.to_string().contains("Refusing to replace non-symlink"));
assert_eq!(fs::read_to_string(&squat).unwrap(), "not a symlink");
}
fn canon_roots() -> (tempfile::TempDir, tempfile::TempDir, PathBuf, PathBuf) {
let src = tempfile::tempdir().unwrap();
let fork = tempfile::tempdir().unwrap();
let src_canon = fs::canonicalize(src.path()).unwrap();
let fork_canon = fs::canonicalize(fork.path()).unwrap();
(src, fork, src_canon, fork_canon)
}
#[test]
fn copy_selected_copies_a_plain_nested_file() {
let (src, _fork, src_canon, fork_canon) = canon_roots();
let f = src.path().join("nested/data.txt");
fs::create_dir_all(f.parent().unwrap()).unwrap();
fs::write(&f, "hello").unwrap();
let never = |_p: &Path| false;
let out = copy_selected(
&src_canon,
&fork_canon,
Path::new("nested/data.txt"),
&never,
)
.unwrap();
assert!(matches!(out, CopyOutcome::Copied));
assert_eq!(
fs::read_to_string(fork_canon.join("nested/data.txt")).unwrap(),
"hello"
);
}
#[test]
fn copy_selected_refuses_an_out_of_tree_symlink() {
let (src, _fork, src_canon, fork_canon) = canon_roots();
let outside = tempfile::tempdir().unwrap();
let secret = outside.path().join("secret");
fs::write(&secret, "s").unwrap();
std::os::unix::fs::symlink(&secret, src.path().join("link")).unwrap();
let never = |_p: &Path| false;
let out = copy_selected(&src_canon, &fork_canon, Path::new("link"), &never).unwrap();
assert!(matches!(out, CopyOutcome::Skipped(_)));
assert!(!fork_canon.join("link").exists());
}
#[test]
fn copy_selected_refuses_a_symlink_into_the_withheld_tier() {
let (src, _fork, src_canon, fork_canon) = canon_roots();
let statefile = src.path().join(".doctrine/state/boot.md");
fs::create_dir_all(statefile.parent().unwrap()).unwrap();
fs::write(&statefile, "boot").unwrap();
std::os::unix::fs::symlink(&statefile, src.path().join("link")).unwrap();
let withheld = |p: &Path| p.starts_with(".doctrine/state");
let out = copy_selected(&src_canon, &fork_canon, Path::new("link"), &withheld).unwrap();
assert!(matches!(out, CopyOutcome::Skipped(_)));
assert!(!fork_canon.join("link").exists());
}
#[test]
fn copy_dir_all_copies_a_directory_tree() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
fs::create_dir_all(src.join("sub")).unwrap();
fs::write(src.join("a.txt"), "hello").unwrap();
fs::write(src.join("sub").join("b.txt"), "world").unwrap();
copy_dir_all(&src, &dst).unwrap();
assert!(dst.join("a.txt").is_file(), "top-level file copied");
assert!(
dst.join("sub").join("b.txt").is_file(),
"nested file copied"
);
assert_eq!(fs::read_to_string(dst.join("a.txt")).unwrap(), "hello");
assert_eq!(
fs::read_to_string(dst.join("sub").join("b.txt")).unwrap(),
"world"
);
}
}