use hashbrown::HashMap;
use std::path::{Path, PathBuf};
use tokio::fs;
use crate::utils::path::resolve_workspace_path;
use super::{PluginError, PluginResult};
pub struct PluginCache {
cache_dir: PathBuf,
cached_plugins: HashMap<String, PathBuf>,
}
impl PluginCache {
pub fn new(cache_dir: PathBuf) -> Self {
Self {
cache_dir,
cached_plugins: HashMap::new(),
}
}
pub async fn cache_plugin(
&mut self,
plugin_id: &str,
source_path: &Path,
) -> PluginResult<PathBuf> {
if !source_path.exists() {
return Err(PluginError::LoadingError(format!(
"Source path does not exist: {}",
source_path.display()
)));
}
fs::create_dir_all(&self.cache_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create cache directory: {}", e))
})?;
let cache_path = self.cache_dir.join(plugin_id);
if cache_path.exists() {
fs::remove_dir_all(&cache_path).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to remove existing cache: {}", e))
})?;
}
self.copy_plugin_to_cache(source_path, &cache_path).await?;
self.cached_plugins
.insert(plugin_id.to_string(), cache_path.clone());
Ok(cache_path)
}
async fn copy_plugin_to_cache(&self, source: &Path, destination: &Path) -> PluginResult<()> {
Box::pin(async {
fs::create_dir_all(destination).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create destination directory: {}", e))
})?;
let mut entries = fs::read_dir(source).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to read source directory: {}", e))
})?;
while let Some(entry) = entries.next_entry().await.map_err(|e| {
PluginError::LoadingError(format!("Failed to read directory entry: {}", e))
})? {
let src_path = entry.path();
let dst_path = destination.join(entry.file_name());
if src_path.is_dir() {
if self.is_valid_plugin_subdirectory(&src_path) {
self.copy_plugin_to_cache(&src_path, &dst_path).await?;
}
} else {
fs::copy(&src_path, &dst_path).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to copy file: {}", e))
})?;
}
}
Ok(())
})
.await
}
fn is_valid_plugin_subdirectory(&self, path: &Path) -> bool {
resolve_workspace_path(&self.cache_dir, path).is_ok()
}
pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
self.cached_plugins.get(plugin_id)
}
pub async fn remove_cached_plugin(&mut self, plugin_id: &str) -> PluginResult<()> {
if let Some(cache_path) = self.cached_plugins.get(plugin_id) {
if cache_path.exists() {
fs::remove_dir_all(cache_path).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to remove cached plugin: {}", e))
})?;
}
self.cached_plugins.remove(plugin_id);
}
Ok(())
}
pub async fn clear_cache(&mut self) -> PluginResult<()> {
if self.cache_dir.exists() {
fs::remove_dir_all(&self.cache_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to clear cache: {}", e)))?;
}
self.cached_plugins.clear();
Ok(())
}
pub fn validate_plugin_security(&self, plugin_path: &Path) -> PluginResult<()> {
if !plugin_path.exists() {
return Err(PluginError::LoadingError(
"Plugin path does not exist".to_string(),
));
}
let plugin_str = plugin_path.to_string_lossy();
if plugin_str.contains("../") || plugin_str.contains("..\\") {
return Err(PluginError::LoadingError(
"Plugin path contains path traversal attempts".to_string(),
));
}
let mut stack = vec![plugin_path.to_path_buf()];
while let Some(current_path) = stack.pop() {
if current_path.is_dir()
&& let Ok(entries) = std::fs::read_dir(¤t_path)
{
for entry in entries.flatten() {
let entry_path = entry.path();
let entry_str = entry_path.to_string_lossy();
if entry_str.contains("../") || entry_str.contains("..\\") {
return Err(PluginError::LoadingError(format!(
"Plugin contains path traversal in file: {}",
entry_path.display()
)));
}
if entry_path.is_dir() {
stack.push(entry_path);
}
}
}
}
Ok(())
}
}