vtcode_core/plugins/
caching.rs1use hashbrown::HashMap;
7use std::path::{Path, PathBuf};
8
9use tokio::fs;
10
11use crate::utils::path::resolve_workspace_path;
12
13use super::{PluginError, PluginResult};
14
15pub struct PluginCache {
17 cache_dir: PathBuf,
19 cached_plugins: HashMap<String, PathBuf>,
21}
22
23impl PluginCache {
24 pub fn new(cache_dir: PathBuf) -> Self {
26 Self {
27 cache_dir,
28 cached_plugins: HashMap::new(),
29 }
30 }
31
32 pub async fn cache_plugin(
34 &mut self,
35 plugin_id: &str,
36 source_path: &Path,
37 ) -> PluginResult<PathBuf> {
38 if !source_path.exists() {
40 return Err(PluginError::LoadingError(format!(
41 "Source path does not exist: {}",
42 source_path.display()
43 )));
44 }
45
46 fs::create_dir_all(&self.cache_dir).await.map_err(|e| {
48 PluginError::LoadingError(format!("Failed to create cache directory: {}", e))
49 })?;
50
51 let cache_path = self.cache_dir.join(plugin_id);
53
54 if cache_path.exists() {
56 fs::remove_dir_all(&cache_path).await.map_err(|e| {
57 PluginError::LoadingError(format!("Failed to remove existing cache: {}", e))
58 })?;
59 }
60
61 self.copy_plugin_to_cache(source_path, &cache_path).await?;
63
64 self.cached_plugins
66 .insert(plugin_id.to_string(), cache_path.clone());
67
68 Ok(cache_path)
69 }
70
71 async fn copy_plugin_to_cache(&self, source: &Path, destination: &Path) -> PluginResult<()> {
73 Box::pin(async {
74 fs::create_dir_all(destination).await.map_err(|e| {
75 PluginError::LoadingError(format!("Failed to create destination directory: {}", e))
76 })?;
77
78 let mut entries = fs::read_dir(source).await.map_err(|e| {
79 PluginError::LoadingError(format!("Failed to read source directory: {}", e))
80 })?;
81
82 while let Some(entry) = entries.next_entry().await.map_err(|e| {
83 PluginError::LoadingError(format!("Failed to read directory entry: {}", e))
84 })? {
85 let src_path = entry.path();
86 let dst_path = destination.join(entry.file_name());
87
88 if src_path.is_dir() {
89 if self.is_valid_plugin_subdirectory(&src_path) {
91 self.copy_plugin_to_cache(&src_path, &dst_path).await?;
92 }
93 } else {
94 fs::copy(&src_path, &dst_path).await.map_err(|e| {
96 PluginError::LoadingError(format!("Failed to copy file: {}", e))
97 })?;
98 }
99 }
100
101 Ok(())
102 })
103 .await
104 }
105
106 fn is_valid_plugin_subdirectory(&self, path: &Path) -> bool {
108 resolve_workspace_path(&self.cache_dir, path).is_ok()
111 }
112
113 pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
115 self.cached_plugins.get(plugin_id)
116 }
117
118 pub async fn remove_cached_plugin(&mut self, plugin_id: &str) -> PluginResult<()> {
120 if let Some(cache_path) = self.cached_plugins.get(plugin_id) {
121 if cache_path.exists() {
122 fs::remove_dir_all(cache_path).await.map_err(|e| {
123 PluginError::LoadingError(format!("Failed to remove cached plugin: {}", e))
124 })?;
125 }
126 self.cached_plugins.remove(plugin_id);
127 }
128 Ok(())
129 }
130
131 pub async fn clear_cache(&mut self) -> PluginResult<()> {
133 if self.cache_dir.exists() {
134 fs::remove_dir_all(&self.cache_dir)
135 .await
136 .map_err(|e| PluginError::LoadingError(format!("Failed to clear cache: {}", e)))?;
137 }
138
139 self.cached_plugins.clear();
140 Ok(())
141 }
142
143 pub fn validate_plugin_security(&self, plugin_path: &Path) -> PluginResult<()> {
145 if !plugin_path.exists() {
148 return Err(PluginError::LoadingError(
149 "Plugin path does not exist".to_string(),
150 ));
151 }
152
153 let plugin_str = plugin_path.to_string_lossy();
155 if plugin_str.contains("../") || plugin_str.contains("..\\") {
156 return Err(PluginError::LoadingError(
157 "Plugin path contains path traversal attempts".to_string(),
158 ));
159 }
160
161 let mut stack = vec![plugin_path.to_path_buf()];
163 while let Some(current_path) = stack.pop() {
164 if current_path.is_dir()
165 && let Ok(entries) = std::fs::read_dir(¤t_path)
166 {
167 for entry in entries.flatten() {
168 let entry_path = entry.path();
169 let entry_str = entry_path.to_string_lossy();
170
171 if entry_str.contains("../") || entry_str.contains("..\\") {
173 return Err(PluginError::LoadingError(format!(
174 "Plugin contains path traversal in file: {}",
175 entry_path.display()
176 )));
177 }
178
179 if entry_path.is_dir() {
180 stack.push(entry_path);
181 }
182 }
183 }
184 }
185
186 Ok(())
187 }
188}