atd_runtime/
capability.rs1use std::collections::BTreeSet;
11
12#[derive(Debug, Clone, Default)]
16pub struct CapabilitySet {
17 granted: BTreeSet<String>,
18}
19
20impl CapabilitySet {
21 pub fn empty() -> Self {
22 Self::default()
23 }
24
25 pub fn contains(&self, cap: &str) -> bool {
26 self.granted.contains(cap)
27 }
28
29 pub fn granted(&self) -> Vec<String> {
31 self.granted.iter().cloned().collect()
32 }
33
34 pub fn intersect(&self, requested: &[String]) -> (Vec<String>, Vec<String>) {
38 let mut granted: Vec<String> = Vec::new();
39 let mut denied: Vec<String> = Vec::new();
40 let req_sorted: BTreeSet<&String> = requested.iter().collect();
41 for r in req_sorted {
42 if self.granted.contains(r) {
43 granted.push(r.clone());
44 } else {
45 denied.push(r.clone());
46 }
47 }
48 (granted, denied)
49 }
50
51 pub fn union(&self, other: &Self) -> Self {
56 Self {
57 granted: self.granted.union(&other.granted).cloned().collect(),
58 }
59 }
60}
61
62impl FromIterator<String> for CapabilitySet {
63 fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
64 Self {
65 granted: iter.into_iter().collect(),
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn empty_contains_nothing() {
76 let s = CapabilitySet::empty();
77 assert!(!s.contains("anything"));
78 assert!(s.granted().is_empty());
79 }
80
81 #[test]
82 fn from_iter_builds_set() {
83 let s = CapabilitySet::from_iter(["read".to_string(), "exec".to_string()]);
84 assert!(s.contains("read"));
85 assert!(s.contains("exec"));
86 assert!(!s.contains("admin"));
87 }
88
89 #[test]
90 fn granted_returns_sorted_deterministic_order() {
91 let s =
93 CapabilitySet::from_iter(["zeta".to_string(), "alpha".to_string(), "mu".to_string()]);
94 assert_eq!(s.granted(), vec!["alpha", "mu", "zeta"]);
95 }
96
97 #[test]
98 fn intersect_with_empty_granted_denies_all() {
99 let s = CapabilitySet::empty();
100 let (granted, denied) = s.intersect(&["read".into(), "exec".into()]);
101 assert!(granted.is_empty());
102 assert_eq!(denied, vec!["exec", "read"]);
103 }
104
105 #[test]
106 fn intersect_partial() {
107 let s = CapabilitySet::from_iter(["read".to_string(), "exec".to_string()]);
108 let (granted, denied) = s.intersect(&["read".into(), "admin".into(), "exec".into()]);
109 assert_eq!(granted, vec!["exec", "read"]);
110 assert_eq!(denied, vec!["admin"]);
111 }
112
113 #[test]
114 fn intersect_full_grants_all_requested() {
115 let s = CapabilitySet::from_iter(["a".to_string(), "b".to_string(), "c".to_string()]);
116 let (granted, denied) = s.intersect(&["a".into(), "b".into()]);
117 assert_eq!(granted, vec!["a", "b"]);
118 assert!(denied.is_empty());
119 }
120
121 #[test]
122 fn intersect_dedupes_via_sorted_input() {
123 let s = CapabilitySet::from_iter(["read".to_string()]);
125 let (granted, denied) = s.intersect(&["read".into(), "read".into(), "admin".into()]);
126 assert_eq!(granted, vec!["read"]);
127 assert_eq!(denied, vec!["admin"]);
128 }
129}