Skip to main content

vtcode_core/plugins/
caching.rs

1//! Plugin caching system for VT Code
2//!
3//! Implements the caching mechanism for plugins to ensure security and verification
4//! as described in the VT Code plugin reference.
5
6use 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
15/// Plugin cache manager
16pub struct PluginCache {
17    /// Base directory for the plugin cache
18    cache_dir: PathBuf,
19    /// Mapping of plugin IDs to their cached paths
20    cached_plugins: HashMap<String, PathBuf>,
21}
22
23impl PluginCache {
24    /// Create a new plugin cache
25    pub fn new(cache_dir: PathBuf) -> Self {
26        Self {
27            cache_dir,
28            cached_plugins: HashMap::new(),
29        }
30    }
31
32    /// Cache a plugin from its source path
33    pub async fn cache_plugin(
34        &mut self,
35        plugin_id: &str,
36        source_path: &Path,
37    ) -> PluginResult<PathBuf> {
38        // Validate source path exists
39        if !source_path.exists() {
40            return Err(PluginError::LoadingError(format!(
41                "Source path does not exist: {}",
42                source_path.display()
43            )));
44        }
45
46        // Create cache directory if it doesn't exist
47        fs::create_dir_all(&self.cache_dir).await.map_err(|e| {
48            PluginError::LoadingError(format!("Failed to create cache directory: {}", e))
49        })?;
50
51        // Create plugin-specific cache directory
52        let cache_path = self.cache_dir.join(plugin_id);
53
54        // Remove existing cache if it exists
55        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        // Copy plugin to cache directory
62        self.copy_plugin_to_cache(source_path, &cache_path).await?;
63
64        // Store in cache mapping
65        self.cached_plugins
66            .insert(plugin_id.to_string(), cache_path.clone());
67
68        Ok(cache_path)
69    }
70
71    /// Copy plugin files to cache directory
72    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                    // Skip directories that are outside the plugin root (for security)
90                    if self.is_valid_plugin_subdirectory(&src_path) {
91                        self.copy_plugin_to_cache(&src_path, &dst_path).await?;
92                    }
93                } else {
94                    // Copy file to cache
95                    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    /// Check if a subdirectory is valid for caching (not traversing outside plugin root)
107    fn is_valid_plugin_subdirectory(&self, path: &Path) -> bool {
108        // For security, we only allow subdirectories that are within the plugin directory
109        // This prevents path traversal attacks
110        resolve_workspace_path(&self.cache_dir, path).is_ok()
111    }
112
113    /// Get cached plugin path
114    pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
115        self.cached_plugins.get(plugin_id)
116    }
117
118    /// Remove cached plugin
119    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    /// Clear entire cache
132    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    /// Validate plugin security by checking for path traversal
144    pub fn validate_plugin_security(&self, plugin_path: &Path) -> PluginResult<()> {
145        // Check that the plugin doesn't contain paths that traverse outside its expected directory
146        // This is a more thorough check - we scan all files in the plugin directory
147        if !plugin_path.exists() {
148            return Err(PluginError::LoadingError(
149                "Plugin path does not exist".to_string(),
150            ));
151        }
152
153        // Check the plugin path itself for traversal attempts
154        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        // Walk through all files in the plugin directory to check for traversal attempts
162        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(&current_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                    // Check for path traversal in the file/directory names
172                    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}