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()
}
#[cfg(test)]
mod tests {
use super::super::with_os;
use super::*;
fn role(read: LocalLocus, write: LocalLocus, reads_secret: bool, frozen: Frozen) -> Role {
Role { read_locus: read, write_locus: write, rebind_locus: write, reads_secret, frozen }
}
#[test]
fn each_stricter_face_alone_makes_a_role_protective() {
let w = LocalLocus::Worktree;
assert!(!role_is_protective(&role(w, w, false, Frozen::Nothing)), "the worktree itself");
assert!(!role_is_protective(&role(LocalLocus::WorktreeTrusted, w, false, Frozen::Nothing)), "a trusted read at the bound");
assert!(role_is_protective(&role(w, w, true, Frozen::Nothing)), "a secret alone");
assert!(role_is_protective(&role(w, w, false, Frozen::Rebind)), "a freeze alone");
assert!(role_is_protective(&role(w, LocalLocus::WorktreeTrusted, false, Frozen::Nothing)), "a write past the worktree alone");
assert!(role_is_protective(&role(LocalLocus::Machine, w, false, Frozen::Nothing)), "a read past the trusted rung alone");
}
#[test]
fn a_root_holding_only_admitted_places_still_resolves_relative() {
assert!(!keeps_absolute("/tmp/x", "/tmp"), "the scratch node admits, it does not protect");
}
#[test]
fn a_glob_node_is_anchored_at_its_literal_directory() {
assert!(keeps_absolute("/proc/1/environ", "/proc"));
assert!(keeps_absolute("/proc", "/proc"), "the root itself is above the protected place");
}
#[test]
fn protection_covers_only_protections_and_folds_case_only_on_macos() {
assert!(!protection_covers("/tmp/x"), "an admit node is not a protection");
assert!(!protection_covers("/srv/app/src"));
assert!(protection_covers("/srv/app/.ssh/id_rsa"));
assert!(with_os("macos", || protection_covers("/srv/app/.SSH/id_rsa")), "one file on a case-insensitive volume");
assert!(!with_os("linux", || protection_covers("/srv/app/.SSH/id_rsa")), "a different directory on Linux");
}
}