car-server-core 0.55.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
Documentation
//! Disposable baseline inputs. Checks may produce build outputs, but cannot
//! contaminate the coding worktree or the next check's initial files.
use std::path::{Path, PathBuf};

pub(super) struct CheckWorkspace {
    workspace: car_multi::AgentWorkspace,
    _directory: tempfile::TempDir,
}
impl CheckWorkspace {
    pub fn new(source: &Path) -> Result<Self, String> {
        let directory = tempfile::tempdir().map_err(|e| e.to_string())?;
        let git = source.join(".git").exists();
        let config = if git {
            car_multi::WorkspaceConfig::git_worktree_at(source, directory.path())
        } else {
            car_multi::WorkspaceConfig::directory(directory.path())
        };
        let workspace = car_multi::AgentWorkspace::provision(&config, "baseline")?;
        let destination = workspace.path();
        if git {
            // Replace checked-out files with the exact current inputs, including
            // staged/unstaged deletions and untracked files. Keep private index.
            for entry in std::fs::read_dir(destination).map_err(|e| e.to_string())? {
                let entry = entry.map_err(|e| e.to_string())?;
                if entry.file_name() == ".git" {
                    continue;
                }
                if entry.file_type().map_err(|e| e.to_string())?.is_dir() {
                    std::fs::remove_dir_all(entry.path()).map_err(|e| e.to_string())?;
                } else {
                    std::fs::remove_file(entry.path()).map_err(|e| e.to_string())?;
                }
            }
            let output = std::process::Command::new("git")
                .arg("-C")
                .arg(source)
                .args([
                    "ls-files",
                    "--cached",
                    "--others",
                    "--exclude-standard",
                    "-z",
                ])
                .output()
                .map_err(|e| e.to_string())?;
            if !output.status.success() {
                return Err("cannot enumerate baseline inputs".into());
            }
            for raw in output.stdout.split(|b| *b == 0).filter(|p| !p.is_empty()) {
                #[cfg(unix)]
                let relative = {
                    use std::os::unix::ffi::OsStrExt;
                    PathBuf::from(std::ffi::OsStr::from_bytes(raw))
                };
                #[cfg(not(unix))]
                let relative = PathBuf::from(std::str::from_utf8(raw).map_err(|e| e.to_string())?);
                if relative.is_absolute()
                    || relative
                        .components()
                        .any(|c| matches!(c, std::path::Component::ParentDir))
                {
                    return Err("invalid baseline input path".into());
                }
                let from = source.join(&relative);
                match std::fs::symlink_metadata(&from) {
                    Ok(_) => copy(&from, &destination.join(relative), source, destination)?,
                    Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
                    Err(e) => return Err(e.to_string()),
                }
            }
            // Keep staged versus unstaged state meaningful to checks while
            // giving the copy its own index (never a hard link).
            let index = |root: &Path| -> Result<PathBuf, String> {
                let output = std::process::Command::new("git")
                    .arg("-C")
                    .arg(root)
                    .args(["rev-parse", "--git-path", "index"])
                    .output()
                    .map_err(|e| e.to_string())?;
                if !output.status.success() {
                    return Err("cannot locate baseline index".into());
                }
                let path = PathBuf::from(
                    String::from_utf8(output.stdout)
                        .map_err(|e| e.to_string())?
                        .trim(),
                );
                Ok(if path.is_absolute() {
                    path
                } else {
                    root.join(path)
                })
            };
            let from_index = index(source)?;
            let to_index = index(destination)?;
            std::fs::copy(&from_index, &to_index).map_err(|e| e.to_string())?;
            // A split index refers to sharedindex.* beside the owning index.
            for entry in std::fs::read_dir(from_index.parent().ok_or("missing index parent")?)
                .map_err(|e| e.to_string())?
            {
                let entry = entry.map_err(|e| e.to_string())?;
                if entry
                    .file_name()
                    .to_string_lossy()
                    .starts_with("sharedindex.")
                    && entry.file_type().map_err(|e| e.to_string())?.is_file()
                {
                    std::fs::copy(
                        entry.path(),
                        to_index
                            .parent()
                            .ok_or("missing snapshot index parent")?
                            .join(entry.file_name()),
                    )
                    .map_err(|e| e.to_string())?;
                }
            }
        } else {
            copy(source, destination, source, destination)?;
        }
        Ok(Self {
            workspace,
            _directory: directory,
        })
    }
    pub fn path(&self) -> &Path {
        self.workspace.path()
    }
}

fn copy(
    source: &Path,
    destination: &Path,
    source_root: &Path,
    destination_root: &Path,
) -> Result<(), String> {
    let metadata = std::fs::symlink_metadata(source).map_err(|e| e.to_string())?;
    if let Some(parent) = destination.parent() {
        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
    }
    if metadata.file_type().is_symlink() {
        let resolved = source
            .canonicalize()
            .map_err(|e| format!("cannot resolve baseline symlink {}: {e}", source.display()))?;
        let root = source_root.canonicalize().map_err(|e| e.to_string())?;
        let relative = resolved
            .strip_prefix(&root)
            .map_err(|_| format!("baseline symlink leaves the worktree: {}", source.display()))?;
        let target = destination_root.join(relative);
        #[cfg(unix)]
        std::os::unix::fs::symlink(target, destination).map_err(|e| e.to_string())?;
        #[cfg(windows)]
        {
            if source.is_dir() {
                std::os::windows::fs::symlink_dir(target, destination)
            } else {
                std::os::windows::fs::symlink_file(target, destination)
            }
            .map_err(|e| e.to_string())?;
        }
    } else if metadata.is_dir() {
        if source != source_root && source.join(".git").exists() {
            return Err(format!(
                "nested Git input needs its own baseline snapshot: {}",
                source.display()
            ));
        }
        std::fs::create_dir_all(destination).map_err(|e| e.to_string())?;
        for entry in std::fs::read_dir(source).map_err(|e| e.to_string())? {
            let entry = entry.map_err(|e| e.to_string())?;
            if entry.file_name() != ".git" {
                copy(
                    &entry.path(),
                    &destination.join(entry.file_name()),
                    source_root,
                    destination_root,
                )?;
            }
        }
    } else if metadata.is_file() {
        std::fs::copy(source, destination).map_err(|e| e.to_string())?;
    } else {
        return Err(format!("unsupported baseline input {}", source.display()));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(unix)]
    #[test]
    fn baseline_symlinks_point_into_the_copy_or_fail_preparation() {
        let source = tempfile::tempdir().unwrap();
        std::fs::write(source.path().join("input.txt"), "original").unwrap();
        std::os::unix::fs::symlink(source.path().join("input.txt"), source.path().join("alias"))
            .unwrap();
        let workspace = CheckWorkspace::new(source.path()).unwrap();
        std::fs::write(workspace.path().join("alias"), "copy mutation").unwrap();
        assert_eq!(
            std::fs::read_to_string(source.path().join("input.txt")).unwrap(),
            "original"
        );
        let external = tempfile::NamedTempFile::new().unwrap();
        std::os::unix::fs::symlink(external.path(), source.path().join("outside")).unwrap();
        assert!(CheckWorkspace::new(source.path()).is_err());
    }

    #[test]
    fn copies_current_git_inputs_without_modifying_the_source_index() {
        let source = tempfile::tempdir().unwrap();
        let git = |args: &[&str]| {
            let out = std::process::Command::new("git")
                .arg("-C")
                .arg(source.path())
                .args(args)
                .output()
                .unwrap();
            assert!(
                out.status.success(),
                "{}",
                String::from_utf8_lossy(&out.stderr)
            );
            out.stdout
        };
        git(&["init", "-q"]);
        std::fs::write(source.path().join("existing.txt"), "initial").unwrap();
        std::fs::write(source.path().join("deleted.txt"), "delete me").unwrap();
        git(&["add", "."]);
        git(&[
            "-c",
            "user.name=Test",
            "-c",
            "user.email=test@example.invalid",
            "commit",
            "-qm",
            "fixture",
        ]);
        std::fs::write(source.path().join("existing.txt"), "staged").unwrap();
        git(&["add", "existing.txt"]);
        std::fs::write(
            source.path().join("existing.txt"),
            "unstaged current content",
        )
        .unwrap();
        std::fs::remove_file(source.path().join("deleted.txt")).unwrap();
        std::fs::write(source.path().join("untracked.txt"), "unfinished work").unwrap();
        let before = git(&["diff", "--cached", "--binary"]);
        let workspace = CheckWorkspace::new(source.path()).unwrap();
        let path = workspace.path().to_path_buf();
        let copied_index = std::process::Command::new("git")
            .arg("-C")
            .arg(&path)
            .args(["diff", "--cached", "--binary"])
            .output()
            .unwrap();
        assert!(copied_index.status.success());
        assert_eq!(copied_index.stdout, before);
        assert_eq!(
            std::fs::read_to_string(path.join("existing.txt")).unwrap(),
            "unstaged current content"
        );
        assert_eq!(
            std::fs::read_to_string(path.join("untracked.txt")).unwrap(),
            "unfinished work"
        );
        assert!(!path.join("deleted.txt").exists());
        std::fs::write(path.join("existing.txt"), "check side effect").unwrap();
        drop(workspace);
        assert!(!path.exists());
        assert_eq!(git(&["diff", "--cached", "--binary"]), before);
        assert_eq!(
            std::fs::read_to_string(source.path().join("existing.txt")).unwrap(),
            "unstaged current content"
        );
    }
}