Skip to main content

artifactize_core/
workspace.rs

1//! Workspace paths and external state/output safety checks.
2
3use std::{
4    io,
5    path::{Path, PathBuf},
6};
7
8/// Legacy markers remain safety boundaries even though discovery refuses their format.
9pub(crate) const ARTIFACT_MARKERS: [&str; 3] = [
10    crate::config::CONFIG_FILE,
11    "artifactize.json",
12    ".artifactizeignore",
13];
14
15fn has_marker(path: &Path) -> io::Result<bool> {
16    match crate::platform::marker_metadata(path) {
17        Ok(_) => Ok(true),
18        Err(error)
19            if matches!(
20                error.kind(),
21                io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
22            ) =>
23        {
24            Ok(false)
25        }
26        Err(error) => Err(error),
27    }
28}
29
30pub(crate) fn has_artifact_marker(path: &Path) -> io::Result<bool> {
31    for marker in ARTIFACT_MARKERS {
32        if has_marker(&path.join(marker))? {
33            return Ok(true);
34        }
35    }
36    let entries = match std::fs::read_dir(path) {
37        Ok(entries) => entries,
38        Err(error)
39            if matches!(
40                error.kind(),
41                io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
42            ) =>
43        {
44            return Ok(false);
45        }
46        // Search-only ancestors can be traversed but not enumerated. Retain fixed-marker
47        // stat probes, including .git, without rejecting an otherwise usable state path.
48        // Unknown sidecars cannot be detected here, but discovery also cannot enumerate
49        // this directory, so artifactize cannot run it as a discovered workspace.
50        Err(error) if error.kind() == io::ErrorKind::PermissionDenied => {
51            return has_marker(&path.join(".git"));
52        }
53        Err(error) => return Err(error),
54    };
55    for entry in entries {
56        if entry?.file_name().as_encoded_bytes().ends_with(b".artf") {
57            return Ok(true);
58        }
59    }
60    Ok(false)
61}
62
63pub fn canonical_target(path: &Path) -> io::Result<PathBuf> {
64    crate::platform::paths::canonical_target(path)
65}
66
67pub(crate) fn outside_workspace(workspace: &Path, output: &Path) -> io::Result<()> {
68    outside_workspaces(std::iter::once(workspace), output)
69}
70
71/// Validate a snapshot's repositories together: output-ancestor marker checks are identical
72/// for every saved Run and must not enumerate those directories once per Run.
73pub(crate) fn outside_workspaces<'a>(
74    workspaces: impl IntoIterator<Item = &'a Path>,
75    output: &Path,
76) -> io::Result<()> {
77    let mut workspaces = workspaces.into_iter().peekable();
78    if workspaces.peek().is_none() {
79        return Ok(());
80    }
81    if workspaces.any(|workspace| crate::platform::is_within(output, workspace)) {
82        return Err(io::Error::other(
83            "State and output directories must be outside the reviewed repository.",
84        ));
85    }
86    outside_artifact_workspaces(output)
87}
88
89pub(crate) fn outside_artifact_workspaces(output: &Path) -> io::Result<()> {
90    for ancestor in output.ancestors() {
91        if has_artifact_marker(ancestor)? {
92            return Err(io::Error::other(
93                "State and output directories must be outside artifactize workspaces.",
94            ));
95        }
96    }
97    Ok(())
98}
99
100pub(crate) fn prepare_directory(path: &Path, workspace: &Path) -> io::Result<PathBuf> {
101    let path = canonical_target(path)?;
102    outside_workspace(workspace, &path)?;
103    crate::platform::create_private_dir_all(&path)?;
104    let path = crate::platform::canonicalize(&path)?;
105    outside_workspace(workspace, &path)?;
106    Ok(path)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn state_and_output_creation_refuse_other_current_or_legacy_workspaces() {
115        for marker in [
116            "index.artf",
117            "artifactize.json",
118            ".artifactizeignore",
119            "file.txt.artf",
120        ] {
121            let root = crate::test_os::tempdir();
122            let reviewed = root.path().join("reviewed");
123            let other = root.path().join("other");
124            std::fs::create_dir(&reviewed).unwrap();
125            std::fs::create_dir(&other).unwrap();
126            std::fs::write(other.join(marker), "marker").unwrap();
127            let state = other.join("state");
128            let error = prepare_directory(&state, &reviewed).unwrap_err();
129            assert!(
130                error.to_string().contains("outside artifactize workspaces"),
131                "{marker}: {error}"
132            );
133            assert!(!state.exists());
134        }
135    }
136}