Files
huskies/server/src/service/shell/io.rs
T

65 lines
2.4 KiB
Rust

//! Shell I/O — the ONLY place in `service::shell/` that may perform side effects.
//!
//! Side effects here include: filesystem existence and canonicalization checks,
//! process spawning via `std::process::Command`, and reading pipe output.
//! All pure logic (pattern matching, output truncation, count parsing) lives in
//! `path_guard.rs`.
use super::Error;
use std::path::{Path, PathBuf};
/// Validate that `working_dir` is an absolute path that exists on disk and
/// lies inside the project's `.huskies/worktrees/` or `.huskies/merge_workspace/`
/// directory. Returns the canonicalized path on success.
///
/// # Errors
/// - [`Error::Validation`] if the path is relative or does not exist.
/// - [`Error::PathNotAllowed`] if the path is outside the allowed roots.
/// - [`Error::Io`] if canonicalization fails.
pub fn validate_working_dir(working_dir: &str, project_root: &Path) -> Result<PathBuf, Error> {
let wd = PathBuf::from(working_dir);
if !wd.is_absolute() {
return Err(Error::Validation(
"working_dir must be an absolute path".to_string(),
));
}
if !wd.exists() {
return Err(Error::Validation(format!(
"working_dir does not exist: {working_dir}"
)));
}
let worktrees_root = project_root.join(".huskies").join("worktrees");
let canonical_wd = wd
.canonicalize()
.map_err(|e| Error::Io(format!("Cannot canonicalize working_dir: {e}")))?;
let canonical_wt = if worktrees_root.exists() {
worktrees_root
.canonicalize()
.map_err(|e| Error::Io(format!("Cannot canonicalize worktrees root: {e}")))?
} else {
return Err(Error::PathNotAllowed(
"No worktrees directory found in project".to_string(),
));
};
// Also allow the merge workspace so mergemaster can fix conflicts.
let merge_workspace = project_root.join(".huskies").join("merge_workspace");
let canonical_mw = merge_workspace.canonicalize().unwrap_or_default();
let in_worktrees = canonical_wd.starts_with(&canonical_wt);
let in_merge_ws =
!canonical_mw.as_os_str().is_empty() && canonical_wd.starts_with(&canonical_mw);
if !in_worktrees && !in_merge_ws {
return Err(Error::PathNotAllowed(format!(
"working_dir must be inside .huskies/worktrees/ or .huskies/merge_workspace/. Got: {working_dir}"
)));
}
Ok(canonical_wd)
}