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;
10use vtcode_commons::VtCodePaths;
11use vtcode_commons::fs::write_private_file_atomic;
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 { cache_dir, cached_plugins: HashMap::new() }
27    }
28
29    /// Cache a plugin from its source path
30    pub async fn cache_plugin(&mut self, plugin_id: &str, source_path: &Path) -> PluginResult<PathBuf> {
31        super::validate_plugin_component(plugin_id)?;
32
33        // Validate source path exists
34        let source_metadata = fs::symlink_metadata(source_path).await.map_err(|error| {
35            PluginError::LoadingError(format!("Failed to inspect source path {}: {error}", source_path.display()))
36        })?;
37        if source_metadata.file_type().is_symlink() || !source_metadata.is_dir() {
38            return Err(PluginError::LoadingError(format!(
39                "Plugin source is not a directory: {}",
40                source_path.display()
41            )));
42        }
43
44        // Create cache directory if it doesn't exist
45        VtCodePaths::ensure_user_dir(&self.cache_dir)
46            .map_err(|e| PluginError::LoadingError(format!("Failed to create cache directory: {e}")))?;
47
48        // Create plugin-specific cache directory
49        let cache_path = self.cache_dir.join(plugin_id);
50
51        // Remove existing cache if it exists
52        if let Ok(metadata) = fs::symlink_metadata(&cache_path).await {
53            if metadata.file_type().is_symlink() || !metadata.is_dir() {
54                return Err(PluginError::LoadingError(format!(
55                    "Refusing to replace non-directory plugin cache path: {}",
56                    cache_path.display()
57                )));
58            }
59            fs::remove_dir_all(&cache_path)
60                .await
61                .map_err(|e| PluginError::LoadingError(format!("Failed to remove existing cache: {e}")))?;
62        }
63
64        // Copy plugin to cache directory
65        self.copy_plugin_to_cache(source_path, &cache_path).await?;
66
67        // Store in cache mapping
68        self.cached_plugins.insert(plugin_id.to_string(), cache_path.clone());
69
70        Ok(cache_path)
71    }
72
73    /// Copy plugin files to cache directory
74    async fn copy_plugin_to_cache(&self, source: &Path, destination: &Path) -> PluginResult<()> {
75        Box::pin(async {
76            VtCodePaths::ensure_user_dir(destination)
77                .map_err(|e| PluginError::LoadingError(format!("Failed to create destination directory: {e}")))?;
78
79            let mut entries = fs::read_dir(source)
80                .await
81                .map_err(|e| PluginError::LoadingError(format!("Failed to read source directory: {e}")))?;
82
83            while let Some(entry) = entries
84                .next_entry()
85                .await
86                .map_err(|e| PluginError::LoadingError(format!("Failed to read directory entry: {e}")))?
87            {
88                let src_path = entry.path();
89                let dst_path = destination.join(entry.file_name());
90
91                let source_metadata = fs::symlink_metadata(&src_path).await.map_err(|error| {
92                    PluginError::LoadingError(format!("Failed to inspect plugin source entry: {error}"))
93                })?;
94                if source_metadata.file_type().is_symlink() {
95                    return Err(PluginError::LoadingError(format!(
96                        "Refusing to copy symlinked plugin entry: {}",
97                        src_path.display()
98                    )));
99                }
100                if source_metadata.is_dir() {
101                    self.copy_plugin_to_cache(&src_path, &dst_path).await?;
102                } else if source_metadata.is_file() {
103                    if let Ok(destination_metadata) = fs::symlink_metadata(&dst_path).await
104                        && destination_metadata.file_type().is_symlink()
105                    {
106                        return Err(PluginError::LoadingError(format!(
107                            "Refusing to replace symlinked plugin cache entry: {}",
108                            dst_path.display()
109                        )));
110                    }
111                    let source = src_path.clone();
112                    let contents = tokio::task::spawn_blocking(move || VtCodePaths::read_file_no_follow(&source))
113                        .await
114                        .map_err(|error| {
115                            PluginError::LoadingError(format!("Plugin source read task panicked: {error}"))
116                        })?
117                        .map_err(|error| PluginError::LoadingError(format!("Failed to read plugin file: {error}")))?;
118                    write_private_file_atomic(&dst_path, contents)
119                        .await
120                        .map_err(|e| PluginError::LoadingError(format!("Failed to copy file: {e}")))?;
121                } else {
122                    return Err(PluginError::LoadingError(format!(
123                        "Refusing to copy special plugin entry: {}",
124                        src_path.display()
125                    )));
126                }
127            }
128
129            Ok(())
130        })
131        .await
132    }
133
134    /// Get cached plugin path
135    pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
136        self.cached_plugins.get(plugin_id)
137    }
138}