1use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fmt;
6
7#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
9pub enum Capability {
10 AudioSpeak,
12 AudioPlay,
13 AudioRecord,
14
15 DisplayChart,
17 DisplayText,
18 DisplayImage,
19 DisplayVideo,
20
21 NetworkFetch(String), NetworkWebSocket(String), FileRead(String), FileWrite(String), SystemTime,
31 SystemRandom,
32 SystemEnvironment(String), ComputeUnlimited,
36 ComputeLimited(u64), MemoryUnlimited,
40 MemoryLimited(usize), }
42
43impl fmt::Display for Capability {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Capability::AudioSpeak => write!(f, "audio.speak"),
47 Capability::AudioPlay => write!(f, "audio.play"),
48 Capability::AudioRecord => write!(f, "audio.record"),
49 Capability::DisplayChart => write!(f, "display.chart"),
50 Capability::DisplayText => write!(f, "display.text"),
51 Capability::DisplayImage => write!(f, "display.image"),
52 Capability::DisplayVideo => write!(f, "display.video"),
53 Capability::NetworkFetch(pattern) => write!(f, "network.fetch[{pattern}]"),
54 Capability::NetworkWebSocket(pattern) => write!(f, "network.websocket[{pattern}]"),
55 Capability::FileRead(pattern) => write!(f, "file.read[{pattern}]"),
56 Capability::FileWrite(pattern) => write!(f, "file.write[{pattern}]"),
57 Capability::SystemTime => write!(f, "system.time"),
58 Capability::SystemRandom => write!(f, "system.random"),
59 Capability::SystemEnvironment(var) => write!(f, "system.env[{var}]"),
60 Capability::ComputeUnlimited => write!(f, "compute.unlimited"),
61 Capability::ComputeLimited(ops) => write!(f, "compute.limited[{ops}]"),
62 Capability::MemoryUnlimited => write!(f, "memory.unlimited"),
63 Capability::MemoryLimited(bytes) => write!(f, "memory.limited[{bytes}]"),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Default)]
70pub struct CapabilitySet {
71 capabilities: HashSet<Capability>,
72}
73
74impl CapabilitySet {
75 pub fn new() -> Self {
77 CapabilitySet {
78 capabilities: HashSet::new(),
79 }
80 }
81
82 pub fn from_capabilities<I>(caps: I) -> Self
84 where
85 I: IntoIterator<Item = Capability>,
86 {
87 CapabilitySet {
88 capabilities: caps.into_iter().collect(),
89 }
90 }
91
92 pub fn grant(&mut self, cap: Capability) {
94 self.capabilities.insert(cap);
95 }
96
97 pub fn revoke(&mut self, cap: &Capability) -> bool {
99 self.capabilities.remove(cap)
100 }
101
102 pub fn has(&self, cap: &Capability) -> bool {
104 self.capabilities.contains(cap)
105 }
106
107 pub fn has_by_name(&self, name: &str) -> bool {
109 self.capabilities.iter().any(|cap| {
110 cap.to_string() == name || cap.to_string().starts_with(&format!("{name}["))
111 })
112 }
113
114 pub fn has_matching(&self, requested: &Capability) -> bool {
116 match requested {
117 Capability::NetworkFetch(url) => self.capabilities.iter().any(|cap| match cap {
118 Capability::NetworkFetch(pattern) => url_matches_pattern(url, pattern),
119 _ => false,
120 }),
121 Capability::NetworkWebSocket(url) => self.capabilities.iter().any(|cap| match cap {
122 Capability::NetworkWebSocket(pattern) => url_matches_pattern(url, pattern),
123 _ => false,
124 }),
125 Capability::FileRead(path) => self.capabilities.iter().any(|cap| match cap {
126 Capability::FileRead(pattern) => path_matches_pattern(path, pattern),
127 _ => false,
128 }),
129 Capability::FileWrite(path) => self.capabilities.iter().any(|cap| match cap {
130 Capability::FileWrite(pattern) => path_matches_pattern(path, pattern),
131 _ => false,
132 }),
133 _ => self.has(requested),
134 }
135 }
136
137 pub fn intersection(&self, other: &CapabilitySet) -> CapabilitySet {
139 CapabilitySet {
140 capabilities: self
141 .capabilities
142 .intersection(&other.capabilities)
143 .cloned()
144 .collect(),
145 }
146 }
147
148 pub fn union(&self, other: &CapabilitySet) -> CapabilitySet {
150 CapabilitySet {
151 capabilities: self
152 .capabilities
153 .union(&other.capabilities)
154 .cloned()
155 .collect(),
156 }
157 }
158
159 pub fn is_subset(&self, other: &CapabilitySet) -> bool {
161 self.capabilities.is_subset(&other.capabilities)
162 }
163
164 pub fn iter(&self) -> impl Iterator<Item = &Capability> {
166 self.capabilities.iter()
167 }
168
169 pub fn len(&self) -> usize {
171 self.capabilities.len()
172 }
173
174 pub fn is_empty(&self) -> bool {
176 self.capabilities.is_empty()
177 }
178}
179
180fn url_matches_pattern(url: &str, pattern: &str) -> bool {
182 if pattern == "*" {
183 return true;
184 }
185
186 if let Some(prefix) = pattern.strip_suffix("*") {
187 url.starts_with(prefix)
188 } else {
189 url == pattern
190 }
191}
192
193fn path_matches_pattern(path: &str, pattern: &str) -> bool {
195 if pattern == "*" {
196 return true;
197 }
198
199 if let Some(prefix) = pattern.strip_suffix("/*") {
200 path.starts_with(prefix)
201 } else if let Some(prefix) = pattern.strip_suffix("*") {
202 path.starts_with(prefix)
203 } else {
204 path == pattern
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_capability_set() {
214 let mut caps = CapabilitySet::new();
215
216 caps.grant(Capability::AudioSpeak);
217 caps.grant(Capability::DisplayText);
218
219 assert!(caps.has(&Capability::AudioSpeak));
220 assert!(caps.has(&Capability::DisplayText));
221 assert!(!caps.has(&Capability::AudioPlay));
222
223 assert!(caps.revoke(&Capability::AudioSpeak));
224 assert!(!caps.has(&Capability::AudioSpeak));
225 }
226
227 #[test]
228 fn test_pattern_matching() {
229 let mut caps = CapabilitySet::new();
230
231 caps.grant(Capability::NetworkFetch(
232 "https://api.example.com/*".to_string(),
233 ));
234 caps.grant(Capability::FileRead("/home/user/data/*".to_string()));
235
236 assert!(caps.has_matching(&Capability::NetworkFetch(
237 "https://api.example.com/users".to_string()
238 )));
239 assert!(!caps.has_matching(&Capability::NetworkFetch("https://evil.com/".to_string())));
240
241 assert!(caps.has_matching(&Capability::FileRead(
242 "/home/user/data/file.txt".to_string()
243 )));
244 assert!(!caps.has_matching(&Capability::FileRead("/etc/passwd".to_string())));
245 }
246
247 #[test]
248 fn test_set_operations() {
249 let caps1 = CapabilitySet::from_capabilities(vec![Capability::AudioSpeak, Capability::DisplayText]);
250
251 let caps2 = CapabilitySet::from_capabilities(vec![Capability::DisplayText, Capability::SystemTime]);
252
253 let intersection = caps1.intersection(&caps2);
254 assert!(intersection.has(&Capability::DisplayText));
255 assert!(!intersection.has(&Capability::AudioSpeak));
256 assert!(!intersection.has(&Capability::SystemTime));
257
258 let union = caps1.union(&caps2);
259 assert!(union.has(&Capability::AudioSpeak));
260 assert!(union.has(&Capability::DisplayText));
261 assert!(union.has(&Capability::SystemTime));
262 }
263}