use super::{Frozen, Matcher, REGIONS, Role, absolute_other_home, current_os};
use crate::engine::facet::LocalLocus;
pub(super) fn role_is_protective(role: &Role) -> bool {
role.reads_secret
|| role.frozen != Frozen::Nothing
|| role.write_locus > LocalLocus::Worktree
|| role.read_locus > LocalLocus::WorktreeTrusted
}
pub(crate) fn keeps_absolute(abs: &str, root: &str) -> bool {
let root = comparable(root);
let home = std::env::var("HOME").ok().filter(|h| h.starts_with('/'));
let under = |inner: &str, outer: &str| outer == "/" || inner == outer || inner.strip_prefix(outer).is_some_and(|r| r.starts_with('/'));
let homes = ["/Users", "/home", "/private"].into_iter().map(str::to_string).chain(home.clone());
if homes.map(|h| comparable(&h)).any(|h| under(&h, &root)) {
return true;
}
let path = comparable(&literal_dir(abs));
REGIONS
.nodes
.iter()
.filter(|n| n.applies_here() && role_is_protective(&n.role))
.filter_map(|n| fixed_place(&n.matcher))
.any(|(place, subtree)| {
let place = match (place.strip_prefix('~'), home.as_deref()) {
(Some(rest), Some(h)) => comparable(&format!("{h}{rest}")),
(Some(_), None) => return true,
(None, _) => comparable(&place),
};
under(&place, &root) && ((subtree && under(&path, &place)) || under(&place, &path))
})
}
fn literal_dir(path: &str) -> String {
path.split('/').take_while(|c| !c.contains(['*', '?', '['])).collect::<Vec<_>>().join("/")
}
fn comparable(path: &str) -> String {
let mut parts: Vec<&str> = Vec::new();
for seg in path.split('/') {
match seg {
"" | "." => {}
".." => {
parts.pop();
}
s => parts.push(s),
}
}
let mut out = format!("/{}", parts.join("/"));
if current_os() == "macos" {
out = out.to_ascii_lowercase();
}
for firm in ["/etc", "/var", "/tmp"] {
let private = format!("/private{firm}");
if out == private || out.starts_with(&format!("{private}/")) {
out = out["/private".len()..].to_string();
}
}
out
}
fn fixed_place(matcher: &Matcher) -> Option<(String, bool)> {
match matcher {
Matcher::Exact(s) => Some((s.clone(), false)),
Matcher::Prefix(s) => Some((s.clone(), true)),
Matcher::StringPrefix(s) => Some((s.rsplit_once('/').map_or_else(String::new, |(dir, _)| dir.to_string()), true)),
Matcher::Glob(parts) => Some((parts.iter().take_while(|p| !p.contains('*')).cloned().collect::<Vec<_>>().join("/"), true)),
Matcher::Segment(_) => None,
}
}
pub(crate) fn protection_covers(path: &str) -> bool {
let path = super::super::locus::canonicalize(path);
let fold_shields = current_os() == "macos";
absolute_other_home(&path)
|| REGIONS
.nodes
.iter()
.filter(|n| n.applies_here() && role_is_protective(&n.role))
.any(|n| n.matcher.specificity(&path, n.fold && fold_shields).is_some())
}
#[cfg(test)]
pub(crate) fn anchored_protected_paths_here() -> Vec<String> {
super::declared_region_paths()
.into_iter()
.filter(|p| p.starts_with('/') || p.starts_with('~'))
.filter(|p| {
REGIONS
.nodes
.iter()
.any(|n| n.applies_here() && role_is_protective(&n.role) && n.matcher.specificity(p, false).is_some())
})
.collect()
}