use std::path::{Path, PathBuf};
use thiserror::Error;
const WORKSPACE_ROOT_PLACEHOLDER: &str = "{workspace_root}";
#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum WorkingDirectoryError {
#[error(
"the workspace root `{path}` is not absolute, so the directory a declared command \
runs in would depend on where this process was started"
)]
NotAbsolute {
path: String,
},
#[error(
"the workspace root `{path}` is not valid UTF-8, so it cannot be spliced into a \
declared working directory"
)]
NotUnicode {
path: String,
},
#[error(
"the workspace root `{path}` contains a NUL byte, which cannot cross `execve`, so no \
process can be launched in it"
)]
NotSpawnable {
path: String,
},
#[error("the workspace root directory `{path}` could not be created: {error}")]
CreationFailed {
path: String,
error: String,
},
}
#[must_use]
pub fn needs_workspace_root(declared: &str) -> bool {
declared.contains(WORKSPACE_ROOT_PLACEHOLDER)
}
pub fn resolve_working_directory(
declared: &str,
root: &Path,
) -> Result<PathBuf, WorkingDirectoryError> {
if !needs_workspace_root(declared) {
return Ok(PathBuf::from(declared));
}
if !root.is_absolute() {
return Err(WorkingDirectoryError::NotAbsolute {
path: root.to_string_lossy().into_owned(),
});
}
let text = root
.to_str()
.ok_or_else(|| WorkingDirectoryError::NotUnicode {
path: root.to_string_lossy().into_owned(),
})?;
if text.contains('\0') {
return Err(WorkingDirectoryError::NotSpawnable {
path: text.to_owned(),
});
}
create_root_directory(root).map_err(|error| WorkingDirectoryError::CreationFailed {
path: text.to_owned(),
error: error.to_string(),
})?;
Ok(PathBuf::from(
declared.replace(WORKSPACE_ROOT_PLACEHOLDER, text),
))
}
fn create_root_directory(root: &Path) -> std::io::Result<()> {
let mut builder = std::fs::DirBuilder::new();
builder.recursive(true);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt as _;
builder.mode(0o700);
}
builder.create(root)
}
#[cfg(test)]
mod tests {
use super::{WorkingDirectoryError, resolve_working_directory};
use std::path::{Path, PathBuf};
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn a_path_without_the_placeholder_never_consults_the_root() -> TestResult {
assert_eq!(
resolve_working_directory("/srv/app", Path::new("relative"))?,
PathBuf::from("/srv/app")
);
Ok(())
}
#[test]
fn the_placeholder_splices_the_root_and_creates_it() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let resolved = resolve_working_directory("{workspace_root}/repo", &root)?;
assert_eq!(resolved, root.join("repo"));
assert!(root.is_dir(), "the root must be created before the splice");
Ok(())
}
#[test]
fn a_relative_root_refuses_rather_than_naming_a_place_nobody_chose() {
assert_eq!(
resolve_working_directory("{workspace_root}", Path::new("clones")),
Err(WorkingDirectoryError::NotAbsolute {
path: "clones".to_owned(),
})
);
}
#[test]
fn an_uncreatable_root_refuses_by_name() -> TestResult {
let scratch = tempfile::tempdir()?;
let file = scratch.path().join("occupied");
std::fs::write(&file, b"not a directory")?;
let Err(error) = resolve_working_directory("{workspace_root}", &file.join("clones")) else {
return Err("a root beneath a regular file cannot be created".into());
};
assert!(
matches!(error, WorkingDirectoryError::CreationFailed { .. }),
"{error}"
);
Ok(())
}
}