vtcode-core 0.146.1

Core library for VT Code - a Rust-based terminal coding agent
//! Plugin caching system for VT Code
//!
//! Implements the caching mechanism for plugins to ensure security and verification
//! as described in the VT Code plugin reference.

use hashbrown::HashMap;
use std::path::{Path, PathBuf};

use tokio::fs;

use crate::utils::path::resolve_workspace_path;

use super::{PluginError, PluginResult};

/// Plugin cache manager
pub struct PluginCache {
    /// Base directory for the plugin cache
    cache_dir: PathBuf,
    /// Mapping of plugin IDs to their cached paths
    cached_plugins: HashMap<String, PathBuf>,
}

impl PluginCache {
    /// Create a new plugin cache
    pub fn new(cache_dir: PathBuf) -> Self {
        Self { cache_dir, cached_plugins: HashMap::new() }
    }

    /// Cache a plugin from its source path
    pub async fn cache_plugin(&mut self, plugin_id: &str, source_path: &Path) -> PluginResult<PathBuf> {
        // Validate source path exists
        if !fs::try_exists(source_path).await.unwrap_or(false) {
            return Err(PluginError::LoadingError(format!("Source path does not exist: {}", source_path.display())));
        }

        // Create cache directory if it doesn't exist
        fs::create_dir_all(&self.cache_dir)
            .await
            .map_err(|e| PluginError::LoadingError(format!("Failed to create cache directory: {e}")))?;

        // Create plugin-specific cache directory
        let cache_path = self.cache_dir.join(plugin_id);

        // Remove existing cache if it exists
        if fs::try_exists(&cache_path).await.unwrap_or(false) {
            fs::remove_dir_all(&cache_path)
                .await
                .map_err(|e| PluginError::LoadingError(format!("Failed to remove existing cache: {e}")))?;
        }

        // Copy plugin to cache directory
        self.copy_plugin_to_cache(source_path, &cache_path).await?;

        // Store in cache mapping
        self.cached_plugins.insert(plugin_id.to_string(), cache_path.clone());

        Ok(cache_path)
    }

    /// Copy plugin files to cache directory
    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 fs::metadata(&src_path).await.map(|metadata| metadata.is_dir()).unwrap_or(false) {
                    // Skip directories that are outside the plugin root (for security)
                    if self.is_valid_plugin_subdirectory(&src_path) {
                        self.copy_plugin_to_cache(&src_path, &dst_path).await?;
                    }
                } else {
                    // Copy file to cache
                    fs::copy(&src_path, &dst_path)
                        .await
                        .map_err(|e| PluginError::LoadingError(format!("Failed to copy file: {e}")))?;
                }
            }

            Ok(())
        })
        .await
    }

    /// Check if a subdirectory is valid for caching (not traversing outside plugin root)
    fn is_valid_plugin_subdirectory(&self, path: &Path) -> bool {
        // For security, we only allow subdirectories that are within the plugin directory
        // This prevents path traversal attacks
        resolve_workspace_path(&self.cache_dir, path).is_ok()
    }

    /// Get cached plugin path
    pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
        self.cached_plugins.get(plugin_id)
    }
}