Skip to main content

aion_worker/shell/
workspace.rs

1//! Resolving a declared command's working directory against a host's
2//! workspace root.
3//!
4//! A `command` declaration states its `cwd` verbatim, `{workspace_root}`
5//! placeholder and all, because where workspaces live is a property of the box
6//! that runs the command and not of the document that declares it. Two hosts
7//! answer that question — the server, from its aion home, and `aion worker
8//! awl`, from the directory the document was pointed at — and both then have
9//! the SAME job: splice the root in, and refuse a root that cannot be spliced.
10//!
11//! This is that job, once. The two hosts differ in how they find the root and
12//! in nothing else; a second copy of the rules would be two executors
13//! disagreeing about the world a process runs in, which is exactly the defect
14//! the one-render rule exists to prevent.
15
16use std::path::{Path, PathBuf};
17
18use thiserror::Error;
19
20/// The placeholder a declared `cwd` may carry, defined by the LANGUAGE crate.
21///
22/// Restated here as a private constant rather than imported: `aion-worker`
23/// deliberately does not depend on `aion-awl` (the dependency is test-only and
24/// one-directional), and the bytes are a format constant the estate agrees on.
25/// `crates/aion-awl/src/workspace_root.rs` is where it is defined; a
26/// differential test in that crate pins the two spellings equal.
27const WORKSPACE_ROOT_PLACEHOLDER: &str = "{workspace_root}";
28
29/// Why a declared working directory could not be resolved against a root.
30///
31/// Every variant is TERMINAL: the root is a property of this box's
32/// configuration and filesystem, so a second attempt reads the same answer.
33#[derive(Clone, Debug, PartialEq, Eq, Error)]
34pub enum WorkingDirectoryError {
35    /// The root is not an absolute path.
36    ///
37    /// Refused because a relative root names a different place depending on
38    /// where the host process was launched from, which is not something a
39    /// document's author can see or predict.
40    #[error(
41        "the workspace root `{path}` is not absolute, so the directory a declared command \
42         runs in would depend on where this process was started"
43    )]
44    NotAbsolute {
45        /// The offending root, rendered lossily for the refusal.
46        path: String,
47    },
48    /// The root is not valid UTF-8, so it has no faithful spelling to splice
49    /// into the declared path.
50    #[error(
51        "the workspace root `{path}` is not valid UTF-8, so it cannot be spliced into a \
52         declared working directory"
53    )]
54    NotUnicode {
55        /// The offending root, rendered lossily for the refusal.
56        path: String,
57    },
58    /// The root contains a NUL byte, so no process can be launched in it.
59    #[error(
60        "the workspace root `{path}` contains a NUL byte, which cannot cross `execve`, so no \
61         process can be launched in it"
62    )]
63    NotSpawnable {
64        /// The offending root, rendered for the refusal.
65        path: String,
66    },
67    /// The root directory does not exist and could not be created.
68    #[error("the workspace root directory `{path}` could not be created: {error}")]
69    CreationFailed {
70        /// The directory that could not be created.
71        path: String,
72        /// The io error's own diagnosis.
73        error: String,
74    },
75}
76
77/// Whether `declared` asks where the workspace is.
78///
79/// A caller that must FIND its root before it can splice one — the server,
80/// whose root resolution may itself have failed — asks this first, so a
81/// command that never wanted the answer is not refused for it.
82#[must_use]
83pub fn needs_workspace_root(declared: &str) -> bool {
84    declared.contains(WORKSPACE_ROOT_PLACEHOLDER)
85}
86
87/// Resolve `declared` — a command's `cwd`, verbatim — against `root`.
88///
89/// A declared path with no placeholder is returned untouched and `root` is
90/// never consulted, so a command that does not ask where the workspace is
91/// cannot be refused for an answer it never wanted.
92///
93/// A path that DOES carry the placeholder gets the root validated and created
94/// before the splice, because a working directory that does not exist is a
95/// spawn failure on the first dispatch rather than a refusal at the desk.
96///
97/// # Errors
98///
99/// Returns [`WorkingDirectoryError`] when the root is relative, is not valid
100/// UTF-8, carries a NUL byte, or names a directory that cannot be created.
101pub fn resolve_working_directory(
102    declared: &str,
103    root: &Path,
104) -> Result<PathBuf, WorkingDirectoryError> {
105    if !needs_workspace_root(declared) {
106        return Ok(PathBuf::from(declared));
107    }
108    if !root.is_absolute() {
109        return Err(WorkingDirectoryError::NotAbsolute {
110            path: root.to_string_lossy().into_owned(),
111        });
112    }
113    let text = root
114        .to_str()
115        .ok_or_else(|| WorkingDirectoryError::NotUnicode {
116            path: root.to_string_lossy().into_owned(),
117        })?;
118    if text.contains('\0') {
119        return Err(WorkingDirectoryError::NotSpawnable {
120            path: text.to_owned(),
121        });
122    }
123    create_root_directory(root).map_err(|error| WorkingDirectoryError::CreationFailed {
124        path: text.to_owned(),
125        error: error.to_string(),
126    })?;
127    Ok(PathBuf::from(
128        declared.replace(WORKSPACE_ROOT_PLACEHOLDER, text),
129    ))
130}
131
132/// Create the root directory if it is missing, owner-only.
133///
134/// Idempotent: an existing directory is not an error. The mode matters because
135/// a workspace holds whatever a declared command puts there, and this crate's
136/// portability gate is target-cfg, so the unix concept is gated the same way.
137fn create_root_directory(root: &Path) -> std::io::Result<()> {
138    let mut builder = std::fs::DirBuilder::new();
139    builder.recursive(true);
140    #[cfg(unix)]
141    {
142        use std::os::unix::fs::DirBuilderExt as _;
143        builder.mode(0o700);
144    }
145    builder.create(root)
146}
147
148#[cfg(test)]
149mod tests {
150    use super::{WorkingDirectoryError, resolve_working_directory};
151    use std::path::{Path, PathBuf};
152
153    /// What a test returns. Every fallible step is carried rather than
154    /// unwrapped, because the workspace denies panicking accessors in test
155    /// code as firmly as in library code.
156    type TestResult = Result<(), Box<dyn std::error::Error>>;
157
158    #[test]
159    fn a_path_without_the_placeholder_never_consults_the_root() -> TestResult {
160        // A relative root would be refused if it were read at all, so this
161        // passing proves the root was not read.
162        assert_eq!(
163            resolve_working_directory("/srv/app", Path::new("relative"))?,
164            PathBuf::from("/srv/app")
165        );
166        Ok(())
167    }
168
169    #[test]
170    fn the_placeholder_splices_the_root_and_creates_it() -> TestResult {
171        let scratch = tempfile::tempdir()?;
172        let root = scratch.path().join("clones");
173        let resolved = resolve_working_directory("{workspace_root}/repo", &root)?;
174        assert_eq!(resolved, root.join("repo"));
175        assert!(root.is_dir(), "the root must be created before the splice");
176        Ok(())
177    }
178
179    #[test]
180    fn a_relative_root_refuses_rather_than_naming_a_place_nobody_chose() {
181        assert_eq!(
182            resolve_working_directory("{workspace_root}", Path::new("clones")),
183            Err(WorkingDirectoryError::NotAbsolute {
184                path: "clones".to_owned(),
185            })
186        );
187    }
188
189    #[test]
190    fn an_uncreatable_root_refuses_by_name() -> TestResult {
191        let scratch = tempfile::tempdir()?;
192        let file = scratch.path().join("occupied");
193        std::fs::write(&file, b"not a directory")?;
194        let Err(error) = resolve_working_directory("{workspace_root}", &file.join("clones")) else {
195            return Err("a root beneath a regular file cannot be created".into());
196        };
197        assert!(
198            matches!(error, WorkingDirectoryError::CreationFailed { .. }),
199            "{error}"
200        );
201        Ok(())
202    }
203}