1use anyhow::Result;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::time::{Duration, SystemTime};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PathPermissions {
19 pub path: PathBuf,
21 pub exists: bool,
23 pub readable: bool,
25 pub writable: bool,
27 pub is_directory: bool,
29 pub is_file: bool,
31 pub verified_at: SystemTime,
33 pub error: Option<String>,
35}
36
37#[derive(Debug, Default)]
39pub struct PermissionCache {
40 permissions: HashMap<PathBuf, PathPermissions>,
42 cache_duration: Duration,
44}
45
46impl PermissionCache {
47 pub fn new() -> Self {
48 Self {
49 permissions: HashMap::new(),
50 cache_duration: Duration::from_secs(300), }
52 }
53
54 pub fn is_verified(&self, path: &Path) -> bool {
56 if let Some(perms) = self.permissions.get(path) {
57 if let Ok(elapsed) = perms.verified_at.elapsed() {
58 return elapsed < self.cache_duration;
59 }
60 }
61 false
62 }
63
64 pub fn get(&self, path: &Path) -> Option<&PathPermissions> {
66 self.permissions
67 .get(path)
68 .filter(|p| p.verified_at.elapsed().unwrap_or(Duration::MAX) < self.cache_duration)
69 }
70
71 pub fn verify(&mut self, path: &Path) -> Result<PathPermissions> {
73 let exists = path.exists();
75 if !exists {
76 let perms = PathPermissions {
77 path: path.to_path_buf(),
78 exists: false,
79 readable: false,
80 writable: false,
81 is_directory: false,
82 is_file: false,
83 verified_at: SystemTime::now(),
84 error: Some("Path does not exist".to_string()),
85 };
86 self.permissions.insert(path.to_path_buf(), perms.clone());
87 return Ok(perms);
88 }
89
90 let metadata = match fs::metadata(path) {
92 Ok(m) => m,
93 Err(e) => {
94 let perms = PathPermissions {
95 path: path.to_path_buf(),
96 exists: true,
97 readable: false,
98 writable: false,
99 is_directory: false,
100 is_file: false,
101 verified_at: SystemTime::now(),
102 error: Some(format!("Cannot read metadata: {}", e)),
103 };
104 self.permissions.insert(path.to_path_buf(), perms.clone());
105 return Ok(perms);
106 }
107 };
108
109 let is_directory = metadata.is_dir();
110 let is_file = metadata.is_file();
111
112 let readable = if is_directory {
114 fs::read_dir(path).is_ok()
115 } else {
116 fs::File::open(path).is_ok()
117 };
118
119 let writable = !metadata.permissions().readonly();
121
122 let perms = PathPermissions {
123 path: path.to_path_buf(),
124 exists,
125 readable,
126 writable,
127 is_directory,
128 is_file,
129 verified_at: SystemTime::now(),
130 error: None,
131 };
132
133 self.permissions.insert(path.to_path_buf(), perms.clone());
134 Ok(perms)
135 }
136
137 pub fn cleanup(&mut self) {
139 self.permissions
140 .retain(|_, p| p.verified_at.elapsed().unwrap_or(Duration::MAX) < self.cache_duration);
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ToolAvailability {
147 pub name: String,
149 pub available: bool,
151 pub reason: Option<String>,
153 pub requires: Vec<String>,
155}
156
157pub fn get_available_tools(perms: &PathPermissions) -> Vec<ToolAvailability> {
159 let mut tools = vec![];
160
161 tools.push(ToolAvailability {
163 name: "get_digest".to_string(),
164 available: true,
165 reason: None,
166 requires: vec![],
167 });
168
169 tools.push(ToolAvailability {
170 name: "server_info".to_string(),
171 available: true,
172 reason: None,
173 requires: vec![],
174 });
175
176 if perms.readable {
178 tools.extend(vec![
179 ToolAvailability {
180 name: "analyze_directory".to_string(),
181 available: perms.is_directory,
182 reason: if !perms.is_directory {
183 Some("Path is not a directory".to_string())
184 } else {
185 None
186 },
187 requires: vec!["read".to_string(), "directory".to_string()],
188 },
189 ToolAvailability {
190 name: "quick_tree".to_string(),
191 available: perms.is_directory,
192 reason: if !perms.is_directory {
193 Some("Path is not a directory".to_string())
194 } else {
195 None
196 },
197 requires: vec!["read".to_string(), "directory".to_string()],
198 },
199 ToolAvailability {
200 name: "find_files".to_string(),
201 available: perms.is_directory,
202 reason: if !perms.is_directory {
203 Some("Path is not a directory".to_string())
204 } else {
205 None
206 },
207 requires: vec!["read".to_string(), "directory".to_string()],
208 },
209 ToolAvailability {
210 name: "search_in_files".to_string(),
211 available: perms.is_directory,
212 reason: if !perms.is_directory {
213 Some("Path is not a directory".to_string())
214 } else {
215 None
216 },
217 requires: vec!["read".to_string(), "directory".to_string()],
218 },
219 ToolAvailability {
220 name: "get_statistics".to_string(),
221 available: perms.is_directory,
222 reason: if !perms.is_directory {
223 Some("Path is not a directory".to_string())
224 } else {
225 None
226 },
227 requires: vec!["read".to_string(), "directory".to_string()],
228 },
229 ToolAvailability {
230 name: "get_function_tree".to_string(),
231 available: perms.is_file,
232 reason: if !perms.is_file {
233 Some("Path is not a file".to_string())
234 } else {
235 None
236 },
237 requires: vec!["read".to_string(), "file".to_string()],
238 },
239 ]);
240 } else {
241 tools.extend(vec![
243 ToolAvailability {
244 name: "analyze_directory".to_string(),
245 available: false,
246 reason: Some("No read permission for this path".to_string()),
247 requires: vec!["read".to_string()],
248 },
249 ToolAvailability {
250 name: "quick_tree".to_string(),
251 available: false,
252 reason: Some("No read permission for this path".to_string()),
253 requires: vec!["read".to_string()],
254 },
255 ]);
256 }
257
258 if perms.writable && perms.readable {
260 tools.extend(vec![
261 ToolAvailability {
262 name: "smart_edit".to_string(),
263 available: perms.is_file,
264 reason: if !perms.is_file {
265 Some("Can only edit files, not directories".to_string())
266 } else {
267 None
268 },
269 requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
270 },
271 ToolAvailability {
272 name: "insert_function".to_string(),
273 available: perms.is_file,
274 reason: if !perms.is_file {
275 Some("Can only edit files, not directories".to_string())
276 } else {
277 None
278 },
279 requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
280 },
281 ToolAvailability {
282 name: "remove_function".to_string(),
283 available: perms.is_file,
284 reason: if !perms.is_file {
285 Some("Can only edit files, not directories".to_string())
286 } else {
287 None
288 },
289 requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
290 },
291 ToolAvailability {
292 name: "track_file_operation".to_string(),
293 available: perms.is_file,
294 reason: if !perms.is_file {
295 Some("Can only track operations on files".to_string())
296 } else {
297 None
298 },
299 requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
300 },
301 ]);
302 } else if !perms.writable && perms.readable {
303 tools.extend(vec![
305 ToolAvailability {
306 name: "smart_edit".to_string(),
307 available: false,
308 reason: Some("File is read-only - no write permission".to_string()),
309 requires: vec!["write".to_string()],
310 },
311 ToolAvailability {
312 name: "insert_function".to_string(),
313 available: false,
314 reason: Some("File is read-only - no write permission".to_string()),
315 requires: vec!["write".to_string()],
316 },
317 ToolAvailability {
318 name: "remove_function".to_string(),
319 available: false,
320 reason: Some("File is read-only - no write permission".to_string()),
321 requires: vec!["write".to_string()],
322 },
323 ]);
324 }
325
326 tools
327}
328
329pub fn _is_tool_available(tool_name: &str, perms: &PathPermissions) -> (bool, Option<String>) {
331 let tools = get_available_tools(perms);
332 for tool in tools {
333 if tool.name == tool_name {
334 return (tool.available, tool.reason);
335 }
336 }
337 (true, None)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 use tempfile::TempDir;
346
347 #[test]
348 fn test_permission_cache() {
349 let mut cache = PermissionCache::new();
350 let temp_dir = TempDir::new().unwrap();
351 let path = temp_dir.path();
352
353 let perms = cache.verify(path).unwrap();
355 assert!(perms.exists);
356 assert!(perms.readable);
357 assert!(perms.is_directory);
358 assert!(!perms.is_file);
359
360 assert!(cache.is_verified(path));
362 }
363
364 #[test]
365 fn test_tool_availability() {
366 let dir_perms = PathPermissions {
368 path: PathBuf::from("/test"),
369 exists: true,
370 readable: true,
371 writable: true,
372 is_directory: true,
373 is_file: false,
374 verified_at: SystemTime::now(),
375 error: None,
376 };
377
378 let tools = get_available_tools(&dir_perms);
379
380 let analyze = tools
382 .iter()
383 .find(|t| t.name == "analyze_directory")
384 .unwrap();
385 assert!(analyze.available);
386
387 let edit = tools.iter().find(|t| t.name == "smart_edit").unwrap();
389 assert!(!edit.available);
390 assert_eq!(
391 edit.reason,
392 Some("Can only edit files, not directories".to_string())
393 );
394
395 let ro_file_perms = PathPermissions {
397 path: PathBuf::from("/test.txt"),
398 exists: true,
399 readable: true,
400 writable: false,
401 is_directory: false,
402 is_file: true,
403 verified_at: SystemTime::now(),
404 error: None,
405 };
406
407 let tools = get_available_tools(&ro_file_perms);
408
409 let edit = tools.iter().find(|t| t.name == "smart_edit").unwrap();
411 assert!(!edit.available);
412 assert_eq!(
413 edit.reason,
414 Some("File is read-only - no write permission".to_string())
415 );
416 }
417}