use std::collections::BTreeMap;
use boundgraph_core::GrantAtom;
use crate::pattern::{Pattern, Seg, Tri, overlaps};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Probe {
pub(crate) hit: Option<usize>,
pub(crate) undecided: bool,
}
impl Probe {
fn merge(self, other: Self) -> Self {
Self {
hit: self.hit.or(other.hit),
undecided: self.undecided || other.undecided,
}
}
fn hit(index: usize) -> Self {
Self {
hit: Some(index),
undecided: false,
}
}
fn undecided() -> Self {
Self {
hit: None,
undecided: true,
}
}
}
#[derive(Debug, Default)]
struct Node {
literal: BTreeMap<String, Node>,
single: Option<Box<Node>>,
terminal: Vec<usize>,
recursive: Vec<usize>,
}
impl Node {
fn has_any(&self) -> Option<usize> {
self.terminal
.first()
.or_else(|| self.recursive.first())
.copied()
.or_else(|| {
self.literal
.values()
.find_map(Node::has_any)
.or_else(|| self.single.as_ref().and_then(|n| n.has_any()))
})
}
}
#[derive(Debug, Default)]
pub(crate) struct ResourceTrie {
by_domain: BTreeMap<String, Node>,
opaque: Vec<usize>,
atoms: Vec<GrantAtom>,
}
impl ResourceTrie {
pub(crate) fn build<'a>(atoms: impl IntoIterator<Item = &'a GrantAtom>) -> Self {
let mut trie = Self::default();
for atom in atoms {
trie.insert(atom.clone());
}
trie
}
pub(crate) fn atom(&self, index: usize) -> &GrantAtom {
&self.atoms[index]
}
fn insert(&mut self, atom: GrantAtom) {
let index = self.atoms.len();
let pattern = Pattern::parse(&atom.resource);
let domain = atom.domain.clone();
self.atoms.push(atom);
if !pattern.is_decidable() {
self.opaque.push(index);
return;
}
let mut node = self.by_domain.entry(domain).or_default();
for seg in pattern.segments() {
match seg {
Seg::Literal(l) => {
node = node.literal.entry(l.clone()).or_default();
}
Seg::Single => {
node = node.single.get_or_insert_with(Box::default);
}
Seg::Recursive => {
node.recursive.push(index);
return;
}
Seg::Opaque(_) => unreachable!("decidable patterns hold no opaque segments"),
}
}
node.terminal.push(index);
}
pub(crate) fn probe(&self, query: &GrantAtom) -> Probe {
let pattern = Pattern::parse(&query.resource);
let mut result = if pattern.is_decidable() {
self.by_domain
.get(&query.domain)
.map(|root| probe_node(root, pattern.segments()))
.unwrap_or_default()
} else {
self.pairwise_against_all(query)
};
for &index in &self.opaque {
let other = &self.atoms[index];
if other.domain != query.domain {
continue;
}
result = result.merge(match overlaps(&query.resource, &other.resource) {
Tri::Yes => Probe::hit(index),
Tri::Unknown => Probe::undecided(),
Tri::No => Probe::default(),
});
}
result
}
fn pairwise_against_all(&self, query: &GrantAtom) -> Probe {
self.atoms
.iter()
.enumerate()
.filter(|(i, a)| a.domain == query.domain && !self.opaque.contains(i))
.fold(Probe::default(), |acc, (index, other)| {
acc.merge(match overlaps(&query.resource, &other.resource) {
Tri::Yes => Probe::hit(index),
Tri::Unknown => Probe::undecided(),
Tri::No => Probe::default(),
})
})
}
}
fn probe_node(node: &Node, query: &[Seg]) -> Probe {
if let Some(&index) = node.recursive.first() {
return Probe::hit(index);
}
let Some(head) = query.first() else {
return node
.terminal
.first()
.map_or_else(Probe::default, |&i| Probe::hit(i));
};
let rest = &query[1..];
match head {
Seg::Recursive => node.has_any().map_or_else(Probe::default, Probe::hit),
Seg::Literal(l) => {
let via_literal = node
.literal
.get(l)
.map_or_else(Probe::default, |n| probe_node(n, rest));
let via_single = node
.single
.as_ref()
.map_or_else(Probe::default, |n| probe_node(n, rest));
via_literal.merge(via_single)
}
Seg::Single => node
.literal
.values()
.map(|n| probe_node(n, rest))
.chain(node.single.as_ref().map(|n| probe_node(n, rest)))
.fold(Probe::default(), Probe::merge),
Seg::Opaque(_) => Probe::undecided(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn atoms(list: &[&str]) -> Vec<GrantAtom> {
list.iter().map(|s| s.parse().unwrap()).collect()
}
fn probe_for(indexed: &[&str], query: &str) -> Probe {
let owned = atoms(indexed);
let trie = ResourceTrie::build(owned.iter());
trie.probe(&query.parse().unwrap())
}
#[test]
fn an_exact_match_is_found() {
let p = probe_for(&["fs:read:/src/a.rs"], "fs:read:/src/a.rs");
assert!(p.hit.is_some());
}
#[test]
fn a_sibling_path_is_not_found() {
let p = probe_for(&["fs:read:/src/a.rs"], "fs:read:/src/b.rs");
assert!(p.hit.is_none());
assert!(!p.undecided);
}
#[test]
fn an_indexed_recursive_glob_swallows_anything_beneath_it() {
let p = probe_for(&["fs:read:/src/**"], "fs:read:/src/lib/deep/a.rs");
assert!(p.hit.is_some());
assert!(
probe_for(&["fs:read:/src/**"], "fs:read:/src")
.hit
.is_some()
);
}
#[test]
fn a_query_recursive_glob_finds_anything_indexed_beneath_it() {
let p = probe_for(&["fs:read:/src/lib/a.rs"], "fs:read:/src/**");
assert!(p.hit.is_some());
}
#[test]
fn single_globs_match_on_either_side() {
assert!(
probe_for(&["fs:read:/src/*"], "fs:read:/src/a")
.hit
.is_some()
);
assert!(
probe_for(&["fs:read:/src/a"], "fs:read:/src/*")
.hit
.is_some()
);
assert!(
probe_for(&["fs:read:/src/*"], "fs:read:/src/a/b")
.hit
.is_none()
);
}
#[test]
fn different_domains_are_never_compared() {
let p = probe_for(&["fs:read:/src/**"], "net:read:/src/a");
assert!(p.hit.is_none());
assert!(!p.undecided);
}
#[test]
fn different_depths_do_not_match() {
assert!(probe_for(&["fs:read:/a/b"], "fs:read:/a").hit.is_none());
assert!(probe_for(&["fs:read:/a"], "fs:read:/a/b").hit.is_none());
}
#[test]
fn an_undecidable_indexed_atom_makes_the_probe_undecided() {
let p = probe_for(&["fs:read:/src/*.rs"], "fs:read:/src/main.rs");
assert!(p.hit.is_none());
assert!(p.undecided);
}
#[test]
fn an_undecidable_query_makes_the_probe_undecided() {
let p = probe_for(&["fs:read:/src/main.rs"], "fs:read:{path}");
assert!(p.undecided);
}
#[test]
fn a_proved_overlap_outranks_an_undecided_one() {
let p = probe_for(
&["fs:read:/src/*.rs", "fs:read:/src/main.rs"],
"fs:read:/src/main.rs",
);
assert!(p.hit.is_some(), "the definite overlap must be reported");
}
#[test]
fn the_reported_index_names_the_offending_atom() {
let indexed = atoms(&["fs:read:/etc/**", "fs:read:/src/**"]);
let trie = ResourceTrie::build(indexed.iter());
let p = trie.probe(&"fs:read:/src/lib/a.rs".parse().unwrap());
let hit = p.hit.expect("should overlap");
assert_eq!(trie.atom(hit).resource, "/src/**");
}
#[test]
fn an_empty_trie_finds_nothing() {
let p = probe_for(&[], "fs:read:/src/a");
assert!(p.hit.is_none());
assert!(!p.undecided);
}
#[test]
fn bare_resources_index_and_match() {
assert!(probe_for(&["time:now"], "time:now").hit.is_some());
assert!(probe_for(&["time:now"], "rand:bytes").hit.is_none());
}
}