use std::path::Path;
use tracing::warn;
pub(crate) fn is_safe_to_remove(workspace_path: &Path, managed_root: &Path) -> bool {
let component_count = workspace_path.components().count();
if component_count < 3 {
warn!(
path = %workspace_path.display(),
components = component_count,
"is_safe_to_remove: rejecting — too few path components"
);
return false;
}
if dirs::home_dir().is_some_and(|home| workspace_path == home) {
warn!(
path = %workspace_path.display(),
"is_safe_to_remove: rejecting — path is $HOME"
);
return false;
}
let canon_ws = match workspace_path.canonicalize() {
Ok(p) => p,
Err(e) => {
warn!(
path = %workspace_path.display(),
"is_safe_to_remove: cannot canonicalize workspace path ({e}); skipping deletion"
);
return false;
}
};
let canon_root = match managed_root.canonicalize() {
Ok(p) => p,
Err(e) => {
warn!(
root = %managed_root.display(),
"is_safe_to_remove: cannot canonicalize managed root ({e}); skipping deletion"
);
return false;
}
};
if canon_ws == canon_root {
warn!(
path = %workspace_path.display(),
"is_safe_to_remove: rejecting — path IS the managed root"
);
return false;
}
if !canon_ws.starts_with(&canon_root) {
warn!(
path = %workspace_path.display(),
root = %managed_root.display(),
"is_safe_to_remove: rejecting — path is outside the managed root"
);
return false;
}
true
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use tempfile::TempDir;
use super::is_safe_to_remove;
#[test]
fn is_safe_to_remove_accepts_valid_child() {
let root = TempDir::new().unwrap();
let child = root.path().join("owner").join("repo").join("session-abc");
std::fs::create_dir_all(&child).unwrap();
assert!(
is_safe_to_remove(&child, root.path()),
"a path strictly inside the managed root must pass"
);
}
#[test]
fn is_safe_to_remove_rejects_root_itself() {
let root = TempDir::new().unwrap();
assert!(
!is_safe_to_remove(root.path(), root.path()),
"the managed root itself must be rejected"
);
}
#[test]
fn is_safe_to_remove_rejects_outside_root() {
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap(); let outside_child = outside.path().join("some").join("path");
std::fs::create_dir_all(&outside_child).unwrap();
assert!(
!is_safe_to_remove(&outside_child, root.path()),
"a path outside the managed root must be rejected"
);
}
#[test]
fn is_safe_to_remove_rejects_shallow_path() {
let root = TempDir::new().unwrap();
let shallow = PathBuf::from("/tmp");
assert!(
!is_safe_to_remove(&shallow, root.path()),
"a shallow path (/tmp) must be rejected by the component-count guard"
);
}
#[test]
fn is_safe_to_remove_rejects_home() {
if let Some(home) = dirs::home_dir() {
assert!(
!is_safe_to_remove(&home, &home),
"$HOME must always be rejected"
);
}
}
#[test]
fn is_safe_to_remove_returns_false_for_nonexistent_path() {
let root = TempDir::new().unwrap();
let nonexistent = root.path().join("ghost").join("nope").join("absent");
assert!(
!is_safe_to_remove(&nonexistent, root.path()),
"a non-existent workspace path must return false (canonicalize fails)"
);
}
}