use anyhow::Result;
use std::path::{Path, PathBuf};
pub(crate) const SYSTEM_PROTECTED: &[&str] = &[
"/", "/home", "/etc", "/usr", "/var", "/boot", "/nix", "/run", "/sys", "/dev", "/proc",
];
pub(crate) fn check_safe_to_delete(path: &Path, user_protected: &[String]) -> Result<PathBuf> {
let canon = match path.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(path.to_path_buf());
}
Err(e) => anyhow::bail!(
"cannot canonicalize {}: {} — refusing to delete",
path.display(),
e
),
};
if let Ok(meta) = std::fs::symlink_metadata(path) {
if meta.file_type().is_symlink() {
anyhow::bail!(
"refusing to delete symlink {} — use target directly",
path.display()
);
}
}
let canon_str = canon.display().to_string();
for prot in SYSTEM_PROTECTED {
if is_protected_ancestor(&canon_str, prot) {
anyhow::bail!(
"refusing to delete protected path {} (under system root {})",
canon.display(),
prot
);
}
}
for user_prot in user_protected {
let prot_canon = match Path::new(user_prot).canonicalize() {
Ok(p) => p.display().to_string(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => anyhow::bail!(
"cannot canonicalize user-protected path {}: {} — refusing",
user_prot,
e
),
};
if is_protected_ancestor(&canon_str, &prot_canon) {
anyhow::bail!(
"refusing to delete protected path {} (under user-protected path {})",
canon.display(),
user_prot
);
}
}
Ok(canon)
}
pub(crate) fn check_safe_to_delete_guard(
path: &Path,
user_protected: &[String],
) -> Result<PathBuf> {
let canon = match path.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(path.to_path_buf());
}
Err(e) => anyhow::bail!(
"cannot canonicalize {}: {} — refusing to delete",
path.display(),
e
),
};
if let Ok(meta) = std::fs::symlink_metadata(path) {
if meta.file_type().is_symlink() {
anyhow::bail!(
"refusing to delete symlink {} — use target directly",
path.display()
);
}
}
let canon_str = canon.display().to_string();
for prot in SYSTEM_PROTECTED {
if canon_str == *prot {
anyhow::bail!(
"refusing guard cleanup of protected system path {}",
canon.display()
);
}
}
for user_prot in user_protected {
let prot_canon = match Path::new(user_prot).canonicalize() {
Ok(p) => p.display().to_string(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => anyhow::bail!(
"cannot canonicalize user-protected path {}: {} — refusing",
user_prot,
e
),
};
if is_protected_ancestor(&canon_str, &prot_canon) {
anyhow::bail!(
"refusing to delete protected path {} (under user-protected path {})",
canon.display(),
user_prot
);
}
}
Ok(canon)
}
pub(crate) fn is_protected_ancestor(path: &str, protected: &str) -> bool {
if path == protected {
return true;
}
if protected == "/" {
return path == "/";
}
let prefix = if protected.ends_with('/') {
protected.to_string()
} else {
format!("{}/", protected)
};
path.starts_with(&prefix)
}