use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathPermissions {
pub path: PathBuf,
pub exists: bool,
pub readable: bool,
pub writable: bool,
pub is_directory: bool,
pub is_file: bool,
pub verified_at: SystemTime,
pub error: Option<String>,
}
#[derive(Debug, Default)]
pub struct PermissionCache {
permissions: HashMap<PathBuf, PathPermissions>,
cache_duration: Duration,
}
impl PermissionCache {
pub fn new() -> Self {
Self {
permissions: HashMap::new(),
cache_duration: Duration::from_secs(300), }
}
pub fn is_verified(&self, path: &Path) -> bool {
if let Some(perms) = self.permissions.get(path) {
if let Ok(elapsed) = perms.verified_at.elapsed() {
return elapsed < self.cache_duration;
}
}
false
}
pub fn get(&self, path: &Path) -> Option<&PathPermissions> {
self.permissions
.get(path)
.filter(|p| p.verified_at.elapsed().unwrap_or(Duration::MAX) < self.cache_duration)
}
pub fn verify(&mut self, path: &Path) -> Result<PathPermissions> {
let exists = path.exists();
if !exists {
let perms = PathPermissions {
path: path.to_path_buf(),
exists: false,
readable: false,
writable: false,
is_directory: false,
is_file: false,
verified_at: SystemTime::now(),
error: Some("Path does not exist".to_string()),
};
self.permissions.insert(path.to_path_buf(), perms.clone());
return Ok(perms);
}
let metadata = match fs::metadata(path) {
Ok(m) => m,
Err(e) => {
let perms = PathPermissions {
path: path.to_path_buf(),
exists: true,
readable: false,
writable: false,
is_directory: false,
is_file: false,
verified_at: SystemTime::now(),
error: Some(format!("Cannot read metadata: {}", e)),
};
self.permissions.insert(path.to_path_buf(), perms.clone());
return Ok(perms);
}
};
let is_directory = metadata.is_dir();
let is_file = metadata.is_file();
let readable = if is_directory {
fs::read_dir(path).is_ok()
} else {
fs::File::open(path).is_ok()
};
let writable = !metadata.permissions().readonly();
let perms = PathPermissions {
path: path.to_path_buf(),
exists,
readable,
writable,
is_directory,
is_file,
verified_at: SystemTime::now(),
error: None,
};
self.permissions.insert(path.to_path_buf(), perms.clone());
Ok(perms)
}
pub fn cleanup(&mut self) {
self.permissions
.retain(|_, p| p.verified_at.elapsed().unwrap_or(Duration::MAX) < self.cache_duration);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolAvailability {
pub name: String,
pub available: bool,
pub reason: Option<String>,
pub requires: Vec<String>,
}
pub fn get_available_tools(perms: &PathPermissions) -> Vec<ToolAvailability> {
let mut tools = vec![];
tools.push(ToolAvailability {
name: "get_digest".to_string(),
available: true,
reason: None,
requires: vec![],
});
tools.push(ToolAvailability {
name: "server_info".to_string(),
available: true,
reason: None,
requires: vec![],
});
if perms.readable {
tools.extend(vec![
ToolAvailability {
name: "analyze_directory".to_string(),
available: perms.is_directory,
reason: if !perms.is_directory {
Some("Path is not a directory".to_string())
} else {
None
},
requires: vec!["read".to_string(), "directory".to_string()],
},
ToolAvailability {
name: "quick_tree".to_string(),
available: perms.is_directory,
reason: if !perms.is_directory {
Some("Path is not a directory".to_string())
} else {
None
},
requires: vec!["read".to_string(), "directory".to_string()],
},
ToolAvailability {
name: "find_files".to_string(),
available: perms.is_directory,
reason: if !perms.is_directory {
Some("Path is not a directory".to_string())
} else {
None
},
requires: vec!["read".to_string(), "directory".to_string()],
},
ToolAvailability {
name: "search_in_files".to_string(),
available: perms.is_directory,
reason: if !perms.is_directory {
Some("Path is not a directory".to_string())
} else {
None
},
requires: vec!["read".to_string(), "directory".to_string()],
},
ToolAvailability {
name: "get_statistics".to_string(),
available: perms.is_directory,
reason: if !perms.is_directory {
Some("Path is not a directory".to_string())
} else {
None
},
requires: vec!["read".to_string(), "directory".to_string()],
},
ToolAvailability {
name: "get_function_tree".to_string(),
available: perms.is_file,
reason: if !perms.is_file {
Some("Path is not a file".to_string())
} else {
None
},
requires: vec!["read".to_string(), "file".to_string()],
},
]);
} else {
tools.extend(vec![
ToolAvailability {
name: "analyze_directory".to_string(),
available: false,
reason: Some("No read permission for this path".to_string()),
requires: vec!["read".to_string()],
},
ToolAvailability {
name: "quick_tree".to_string(),
available: false,
reason: Some("No read permission for this path".to_string()),
requires: vec!["read".to_string()],
},
]);
}
if perms.writable && perms.readable {
tools.extend(vec![
ToolAvailability {
name: "smart_edit".to_string(),
available: perms.is_file,
reason: if !perms.is_file {
Some("Can only edit files, not directories".to_string())
} else {
None
},
requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
},
ToolAvailability {
name: "insert_function".to_string(),
available: perms.is_file,
reason: if !perms.is_file {
Some("Can only edit files, not directories".to_string())
} else {
None
},
requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
},
ToolAvailability {
name: "remove_function".to_string(),
available: perms.is_file,
reason: if !perms.is_file {
Some("Can only edit files, not directories".to_string())
} else {
None
},
requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
},
ToolAvailability {
name: "track_file_operation".to_string(),
available: perms.is_file,
reason: if !perms.is_file {
Some("Can only track operations on files".to_string())
} else {
None
},
requires: vec!["read".to_string(), "write".to_string(), "file".to_string()],
},
]);
} else if !perms.writable && perms.readable {
tools.extend(vec![
ToolAvailability {
name: "smart_edit".to_string(),
available: false,
reason: Some("File is read-only - no write permission".to_string()),
requires: vec!["write".to_string()],
},
ToolAvailability {
name: "insert_function".to_string(),
available: false,
reason: Some("File is read-only - no write permission".to_string()),
requires: vec!["write".to_string()],
},
ToolAvailability {
name: "remove_function".to_string(),
available: false,
reason: Some("File is read-only - no write permission".to_string()),
requires: vec!["write".to_string()],
},
]);
}
tools
}
pub fn _is_tool_available(tool_name: &str, perms: &PathPermissions) -> (bool, Option<String>) {
let tools = get_available_tools(perms);
for tool in tools {
if tool.name == tool_name {
return (tool.available, tool.reason);
}
}
(true, None)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_permission_cache() {
let mut cache = PermissionCache::new();
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path();
let perms = cache.verify(path).unwrap();
assert!(perms.exists);
assert!(perms.readable);
assert!(perms.is_directory);
assert!(!perms.is_file);
assert!(cache.is_verified(path));
}
#[test]
fn test_tool_availability() {
let dir_perms = PathPermissions {
path: PathBuf::from("/test"),
exists: true,
readable: true,
writable: true,
is_directory: true,
is_file: false,
verified_at: SystemTime::now(),
error: None,
};
let tools = get_available_tools(&dir_perms);
let analyze = tools
.iter()
.find(|t| t.name == "analyze_directory")
.unwrap();
assert!(analyze.available);
let edit = tools.iter().find(|t| t.name == "smart_edit").unwrap();
assert!(!edit.available);
assert_eq!(
edit.reason,
Some("Can only edit files, not directories".to_string())
);
let ro_file_perms = PathPermissions {
path: PathBuf::from("/test.txt"),
exists: true,
readable: true,
writable: false,
is_directory: false,
is_file: true,
verified_at: SystemTime::now(),
error: None,
};
let tools = get_available_tools(&ro_file_perms);
let edit = tools.iter().find(|t| t.name == "smart_edit").unwrap();
assert!(!edit.available);
assert_eq!(
edit.reason,
Some("File is read-only - no write permission".to_string())
);
}
}