pinakes-core: update remaining modules and tests
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9e0ff5ea33a5cf697473423e88f167ce6a6a6964
This commit is contained in:
parent
c8425a4c34
commit
3d9f8933d2
44 changed files with 1207 additions and 578 deletions
|
|
@ -15,12 +15,17 @@ pub struct PluginLoader {
|
|||
|
||||
impl PluginLoader {
|
||||
/// Create a new plugin loader
|
||||
pub fn new(plugin_dirs: Vec<PathBuf>) -> Self {
|
||||
#[must_use]
|
||||
pub const fn new(plugin_dirs: Vec<PathBuf>) -> Self {
|
||||
Self { plugin_dirs }
|
||||
}
|
||||
|
||||
/// Discover all plugins in configured directories
|
||||
pub async fn discover_plugins(&self) -> Result<Vec<PluginManifest>> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if a plugin directory cannot be searched.
|
||||
pub fn discover_plugins(&self) -> Result<Vec<PluginManifest>> {
|
||||
let mut manifests = Vec::new();
|
||||
|
||||
for dir in &self.plugin_dirs {
|
||||
|
|
@ -31,25 +36,16 @@ impl PluginLoader {
|
|||
|
||||
info!("Discovering plugins in: {:?}", dir);
|
||||
|
||||
match self.discover_in_directory(dir).await {
|
||||
Ok(found) => {
|
||||
info!("Found {} plugins in {:?}", found.len(), dir);
|
||||
manifests.extend(found);
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Error discovering plugins in {:?}: {}", dir, e);
|
||||
},
|
||||
}
|
||||
let found = Self::discover_in_directory(dir);
|
||||
info!("Found {} plugins in {:?}", found.len(), dir);
|
||||
manifests.extend(found);
|
||||
}
|
||||
|
||||
Ok(manifests)
|
||||
}
|
||||
|
||||
/// Discover plugins in a specific directory
|
||||
async fn discover_in_directory(
|
||||
&self,
|
||||
dir: &Path,
|
||||
) -> Result<Vec<PluginManifest>> {
|
||||
fn discover_in_directory(dir: &Path) -> Vec<PluginManifest> {
|
||||
let mut manifests = Vec::new();
|
||||
|
||||
// Walk the directory looking for plugin.toml files
|
||||
|
|
@ -83,10 +79,15 @@ impl PluginLoader {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(manifests)
|
||||
manifests
|
||||
}
|
||||
|
||||
/// Resolve the WASM binary path from a manifest
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the WASM binary is not found or its path escapes the
|
||||
/// plugin directory.
|
||||
pub fn resolve_wasm_path(
|
||||
&self,
|
||||
manifest: &PluginManifest,
|
||||
|
|
@ -114,14 +115,14 @@ impl PluginLoader {
|
|||
// traversal)
|
||||
let canonical_wasm = wasm_path
|
||||
.canonicalize()
|
||||
.map_err(|e| anyhow!("Failed to canonicalize WASM path: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to canonicalize WASM path: {e}"))?;
|
||||
let canonical_plugin_dir = plugin_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| anyhow!("Failed to canonicalize plugin dir: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to canonicalize plugin dir: {e}"))?;
|
||||
if !canonical_wasm.starts_with(&canonical_plugin_dir) {
|
||||
return Err(anyhow!(
|
||||
"WASM binary path escapes plugin directory: {:?}",
|
||||
wasm_path
|
||||
"WASM binary path escapes plugin directory: {}",
|
||||
wasm_path.display()
|
||||
));
|
||||
}
|
||||
return Ok(canonical_wasm);
|
||||
|
|
@ -135,12 +136,19 @@ impl PluginLoader {
|
|||
}
|
||||
|
||||
/// Download a plugin from a URL
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the URL is not HTTPS, no plugin directories are
|
||||
/// configured, the download fails, the archive is too large, or extraction
|
||||
/// fails.
|
||||
pub async fn download_plugin(&self, url: &str) -> Result<PathBuf> {
|
||||
const MAX_PLUGIN_SIZE: u64 = 100 * 1024 * 1024; // 100 MB
|
||||
|
||||
// Only allow HTTPS downloads
|
||||
if !url.starts_with("https://") {
|
||||
return Err(anyhow!(
|
||||
"Only HTTPS URLs are allowed for plugin downloads: {}",
|
||||
url
|
||||
"Only HTTPS URLs are allowed for plugin downloads: {url}"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -153,15 +161,15 @@ impl PluginLoader {
|
|||
|
||||
// Download the archive with timeout and size limits
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.timeout(std::time::Duration::from_mins(5))
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to build HTTP client: {e}"))?;
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download plugin: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to download plugin: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
|
|
@ -171,21 +179,19 @@ impl PluginLoader {
|
|||
}
|
||||
|
||||
// Check content-length header before downloading
|
||||
const MAX_PLUGIN_SIZE: u64 = 100 * 1024 * 1024; // 100 MB
|
||||
if let Some(content_length) = response.content_length()
|
||||
&& content_length > MAX_PLUGIN_SIZE
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Plugin archive too large: {} bytes (max {} bytes)",
|
||||
content_length,
|
||||
MAX_PLUGIN_SIZE
|
||||
"Plugin archive too large: {content_length} bytes (max \
|
||||
{MAX_PLUGIN_SIZE} bytes)"
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to read plugin response: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to read plugin response: {e}"))?;
|
||||
|
||||
// Check actual size after download
|
||||
if bytes.len() as u64 > MAX_PLUGIN_SIZE {
|
||||
|
|
@ -204,7 +210,7 @@ impl PluginLoader {
|
|||
// Extract using tar with -C to target directory
|
||||
let canonical_dest = dest_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| anyhow!("Failed to canonicalize dest dir: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to canonicalize dest dir: {e}"))?;
|
||||
let output = std::process::Command::new("tar")
|
||||
.args([
|
||||
"xzf",
|
||||
|
|
@ -213,7 +219,7 @@ impl PluginLoader {
|
|||
&canonical_dest.to_string_lossy(),
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to extract plugin archive: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to extract plugin archive: {e}"))?;
|
||||
|
||||
// Clean up the archive
|
||||
let _ = std::fs::remove_file(&temp_archive);
|
||||
|
|
@ -231,8 +237,8 @@ impl PluginLoader {
|
|||
let entry_canonical = entry.path().canonicalize()?;
|
||||
if !entry_canonical.starts_with(&canonical_dest) {
|
||||
return Err(anyhow!(
|
||||
"Extracted file escapes destination directory: {:?}",
|
||||
entry.path()
|
||||
"Extracted file escapes destination directory: {}",
|
||||
entry.path().display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -255,22 +261,26 @@ impl PluginLoader {
|
|||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"No plugin.toml found after extracting archive from: {}",
|
||||
url
|
||||
"No plugin.toml found after extracting archive from: {url}"
|
||||
))
|
||||
}
|
||||
|
||||
/// Validate a plugin package
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the path does not exist, is missing `plugin.toml`,
|
||||
/// the WASM binary is not found, or the WASM file is invalid.
|
||||
pub fn validate_plugin_package(&self, path: &Path) -> Result<()> {
|
||||
// Check that the path exists
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Plugin path does not exist: {:?}", path));
|
||||
return Err(anyhow!("Plugin path does not exist: {}", path.display()));
|
||||
}
|
||||
|
||||
// Check for plugin.toml
|
||||
let manifest_path = path.join("plugin.toml");
|
||||
if !manifest_path.exists() {
|
||||
return Err(anyhow!("Missing plugin.toml in {:?}", path));
|
||||
return Err(anyhow!("Missing plugin.toml in {}", path.display()));
|
||||
}
|
||||
|
||||
// Parse and validate manifest
|
||||
|
|
@ -291,21 +301,22 @@ impl PluginLoader {
|
|||
let canonical_path = path.canonicalize()?;
|
||||
if !canonical_wasm.starts_with(&canonical_path) {
|
||||
return Err(anyhow!(
|
||||
"WASM binary path escapes plugin directory: {:?}",
|
||||
wasm_path
|
||||
"WASM binary path escapes plugin directory: {}",
|
||||
wasm_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Validate WASM file
|
||||
let wasm_bytes = std::fs::read(&wasm_path)?;
|
||||
if wasm_bytes.len() < 4 || &wasm_bytes[0..4] != b"\0asm" {
|
||||
return Err(anyhow!("Invalid WASM file: {:?}", wasm_path));
|
||||
return Err(anyhow!("Invalid WASM file: {}", wasm_path.display()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get plugin directory path for a given plugin name
|
||||
#[must_use]
|
||||
pub fn get_plugin_dir(&self, plugin_name: &str) -> Option<PathBuf> {
|
||||
for dir in &self.plugin_dirs {
|
||||
let plugin_dir = dir.join(plugin_name);
|
||||
|
|
@ -323,17 +334,17 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_discover_plugins_empty() {
|
||||
#[test]
|
||||
fn test_discover_plugins_empty() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let loader = PluginLoader::new(vec![temp_dir.path().to_path_buf()]);
|
||||
|
||||
let manifests = loader.discover_plugins().await.unwrap();
|
||||
let manifests = loader.discover_plugins().unwrap();
|
||||
assert_eq!(manifests.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_discover_plugins_with_manifest() {
|
||||
#[test]
|
||||
fn test_discover_plugins_with_manifest() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let plugin_dir = temp_dir.path().join("test-plugin");
|
||||
std::fs::create_dir(&plugin_dir).unwrap();
|
||||
|
|
@ -356,7 +367,7 @@ wasm = "plugin.wasm"
|
|||
.unwrap();
|
||||
|
||||
let loader = PluginLoader::new(vec![temp_dir.path().to_path_buf()]);
|
||||
let manifests = loader.discover_plugins().await.unwrap();
|
||||
let manifests = loader.discover_plugins().unwrap();
|
||||
|
||||
assert_eq!(manifests.len(), 1);
|
||||
assert_eq!(manifests[0].plugin.name, "test-plugin");
|
||||
|
|
|
|||
|
|
@ -97,6 +97,11 @@ impl From<crate::config::PluginsConfig> for PluginManagerConfig {
|
|||
|
||||
impl PluginManager {
|
||||
/// Create a new plugin manager
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the data or cache directories cannot be created, or
|
||||
/// if the WASM runtime cannot be initialized.
|
||||
pub fn new(
|
||||
data_dir: PathBuf,
|
||||
cache_dir: PathBuf,
|
||||
|
|
@ -123,10 +128,14 @@ impl PluginManager {
|
|||
}
|
||||
|
||||
/// Discover and load all plugins from configured directories
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if plugin discovery fails.
|
||||
pub async fn discover_and_load_all(&self) -> Result<Vec<String>> {
|
||||
info!("Discovering plugins from {:?}", self.config.plugin_dirs);
|
||||
|
||||
let manifests = self.loader.discover_plugins().await?;
|
||||
let manifests = self.loader.discover_plugins()?;
|
||||
let mut loaded_plugins = Vec::new();
|
||||
|
||||
for manifest in manifests {
|
||||
|
|
@ -145,6 +154,12 @@ impl PluginManager {
|
|||
}
|
||||
|
||||
/// Load a plugin from a manifest file
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is invalid, capability validation
|
||||
/// fails, the WASM binary cannot be loaded, or the plugin cannot be
|
||||
/// registered.
|
||||
async fn load_plugin_from_manifest(
|
||||
&self,
|
||||
manifest: &pinakes_plugin_api::PluginManifest,
|
||||
|
|
@ -156,7 +171,7 @@ impl PluginManager {
|
|||
|| plugin_id.contains('\\')
|
||||
|| plugin_id.contains("..")
|
||||
{
|
||||
return Err(anyhow::anyhow!("Invalid plugin ID: {}", plugin_id));
|
||||
return Err(anyhow::anyhow!("Invalid plugin ID: {plugin_id}"));
|
||||
}
|
||||
|
||||
// Check if already loaded
|
||||
|
|
@ -202,7 +217,7 @@ impl PluginManager {
|
|||
|
||||
// Load WASM binary
|
||||
let wasm_path = self.loader.resolve_wasm_path(manifest)?;
|
||||
let wasm_plugin = self.runtime.load_plugin(&wasm_path, context).await?;
|
||||
let wasm_plugin = self.runtime.load_plugin(&wasm_path, context)?;
|
||||
|
||||
// Initialize plugin
|
||||
let init_succeeded = match wasm_plugin
|
||||
|
|
@ -246,13 +261,20 @@ impl PluginManager {
|
|||
enabled: init_succeeded,
|
||||
};
|
||||
|
||||
let mut registry = self.registry.write().await;
|
||||
registry.register(registered)?;
|
||||
{
|
||||
let mut registry = self.registry.write().await;
|
||||
registry.register(registered)?;
|
||||
}
|
||||
|
||||
Ok(plugin_id)
|
||||
}
|
||||
|
||||
/// Install a plugin from a file or URL
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin cannot be downloaded, the manifest cannot
|
||||
/// be read, or the plugin cannot be loaded.
|
||||
pub async fn install_plugin(&self, source: &str) -> Result<String> {
|
||||
info!("Installing plugin from: {}", source);
|
||||
|
||||
|
|
@ -276,13 +298,18 @@ impl PluginManager {
|
|||
}
|
||||
|
||||
/// Uninstall a plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is invalid, the plugin cannot be shut
|
||||
/// down, cannot be unregistered, or its data directories cannot be removed.
|
||||
pub async fn uninstall_plugin(&self, plugin_id: &str) -> Result<()> {
|
||||
// Validate plugin_id to prevent path traversal
|
||||
if plugin_id.contains('/')
|
||||
|| plugin_id.contains('\\')
|
||||
|| plugin_id.contains("..")
|
||||
{
|
||||
return Err(anyhow::anyhow!("Invalid plugin ID: {}", plugin_id));
|
||||
return Err(anyhow::anyhow!("Invalid plugin ID: {plugin_id}"));
|
||||
}
|
||||
|
||||
info!("Uninstalling plugin: {}", plugin_id);
|
||||
|
|
@ -291,8 +318,10 @@ impl PluginManager {
|
|||
self.shutdown_plugin(plugin_id).await?;
|
||||
|
||||
// Remove from registry
|
||||
let mut registry = self.registry.write().await;
|
||||
registry.unregister(plugin_id)?;
|
||||
{
|
||||
let mut registry = self.registry.write().await;
|
||||
registry.unregister(plugin_id)?;
|
||||
}
|
||||
|
||||
// Remove plugin data and cache
|
||||
let plugin_data_dir = self.data_dir.join(plugin_id);
|
||||
|
|
@ -309,37 +338,55 @@ impl PluginManager {
|
|||
}
|
||||
|
||||
/// Enable a plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is not found in the registry.
|
||||
pub async fn enable_plugin(&self, plugin_id: &str) -> Result<()> {
|
||||
let mut registry = self.registry.write().await;
|
||||
registry.enable(plugin_id)
|
||||
}
|
||||
|
||||
/// Disable a plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is not found in the registry.
|
||||
pub async fn disable_plugin(&self, plugin_id: &str) -> Result<()> {
|
||||
let mut registry = self.registry.write().await;
|
||||
registry.disable(plugin_id)
|
||||
}
|
||||
|
||||
/// Shutdown a specific plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is not found in the registry.
|
||||
pub async fn shutdown_plugin(&self, plugin_id: &str) -> Result<()> {
|
||||
debug!("Shutting down plugin: {}", plugin_id);
|
||||
|
||||
let registry = self.registry.read().await;
|
||||
if let Some(plugin) = registry.get(plugin_id) {
|
||||
plugin.wasm_plugin.call_function("shutdown", &[]).await.ok();
|
||||
let _ = plugin.wasm_plugin.call_function("shutdown", &[]).await;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Plugin not found: {}", plugin_id))
|
||||
Err(anyhow::anyhow!("Plugin not found: {plugin_id}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdown all plugins
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function always returns `Ok(())`. Individual plugin shutdown errors
|
||||
/// are logged but do not cause the overall operation to fail.
|
||||
pub async fn shutdown_all(&self) -> Result<()> {
|
||||
info!("Shutting down all plugins");
|
||||
|
||||
let registry = self.registry.read().await;
|
||||
let plugin_ids: Vec<String> =
|
||||
registry.list_all().iter().map(|p| p.id.clone()).collect();
|
||||
let plugin_ids: Vec<String> = {
|
||||
let registry = self.registry.read().await;
|
||||
registry.list_all().iter().map(|p| p.id.clone()).collect()
|
||||
};
|
||||
|
||||
for plugin_id in plugin_ids {
|
||||
if let Err(e) = self.shutdown_plugin(&plugin_id).await {
|
||||
|
|
@ -373,6 +420,11 @@ impl PluginManager {
|
|||
}
|
||||
|
||||
/// Reload a plugin (for hot-reload during development)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if hot-reload is disabled, the plugin is not found, it
|
||||
/// cannot be shut down, or the reloaded plugin cannot be registered.
|
||||
pub async fn reload_plugin(&self, plugin_id: &str) -> Result<()> {
|
||||
if !self.config.enable_hot_reload {
|
||||
return Err(anyhow::anyhow!("Hot-reload is disabled"));
|
||||
|
|
@ -387,15 +439,21 @@ impl PluginManager {
|
|||
let plugin = registry
|
||||
.get(plugin_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Plugin not found"))?;
|
||||
if let Some(ref manifest_path) = plugin.manifest_path {
|
||||
pinakes_plugin_api::PluginManifest::from_file(manifest_path)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Failed to re-read manifest from disk, using cached: {}", e);
|
||||
plugin.manifest.clone()
|
||||
})
|
||||
} else {
|
||||
plugin.manifest.clone()
|
||||
}
|
||||
let manifest = plugin.manifest_path.as_ref().map_or_else(
|
||||
|| plugin.manifest.clone(),
|
||||
|manifest_path| {
|
||||
pinakes_plugin_api::PluginManifest::from_file(manifest_path)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"Failed to re-read manifest from disk, using cached: {}",
|
||||
e
|
||||
);
|
||||
plugin.manifest.clone()
|
||||
})
|
||||
},
|
||||
);
|
||||
drop(registry);
|
||||
manifest
|
||||
};
|
||||
|
||||
// Shutdown and unload current version
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ pub struct PluginRegistry {
|
|||
|
||||
impl PluginRegistry {
|
||||
/// Create a new empty registry
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
plugins: HashMap::new(),
|
||||
|
|
@ -33,6 +34,10 @@ impl PluginRegistry {
|
|||
}
|
||||
|
||||
/// Register a new plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if a plugin with the same ID is already registered.
|
||||
pub fn register(&mut self, plugin: RegisteredPlugin) -> Result<()> {
|
||||
if self.plugins.contains_key(&plugin.id) {
|
||||
return Err(anyhow!("Plugin already registered: {}", plugin.id));
|
||||
|
|
@ -43,15 +48,20 @@ impl PluginRegistry {
|
|||
}
|
||||
|
||||
/// Unregister a plugin by ID
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is not found.
|
||||
pub fn unregister(&mut self, plugin_id: &str) -> Result<()> {
|
||||
self
|
||||
.plugins
|
||||
.remove(plugin_id)
|
||||
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
|
||||
.ok_or_else(|| anyhow!("Plugin not found: {plugin_id}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a plugin by ID
|
||||
#[must_use]
|
||||
pub fn get(&self, plugin_id: &str) -> Option<&RegisteredPlugin> {
|
||||
self.plugins.get(plugin_id)
|
||||
}
|
||||
|
|
@ -62,48 +72,61 @@ impl PluginRegistry {
|
|||
}
|
||||
|
||||
/// Check if a plugin is loaded
|
||||
#[must_use]
|
||||
pub fn is_loaded(&self, plugin_id: &str) -> bool {
|
||||
self.plugins.contains_key(plugin_id)
|
||||
}
|
||||
|
||||
/// Check if a plugin is enabled. Returns `None` if the plugin is not found.
|
||||
#[must_use]
|
||||
pub fn is_enabled(&self, plugin_id: &str) -> Option<bool> {
|
||||
self.plugins.get(plugin_id).map(|p| p.enabled)
|
||||
}
|
||||
|
||||
/// Enable a plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is not found.
|
||||
pub fn enable(&mut self, plugin_id: &str) -> Result<()> {
|
||||
let plugin = self
|
||||
.plugins
|
||||
.get_mut(plugin_id)
|
||||
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
|
||||
.ok_or_else(|| anyhow!("Plugin not found: {plugin_id}"))?;
|
||||
|
||||
plugin.enabled = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable a plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin ID is not found.
|
||||
pub fn disable(&mut self, plugin_id: &str) -> Result<()> {
|
||||
let plugin = self
|
||||
.plugins
|
||||
.get_mut(plugin_id)
|
||||
.ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
|
||||
.ok_or_else(|| anyhow!("Plugin not found: {plugin_id}"))?;
|
||||
|
||||
plugin.enabled = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all registered plugins
|
||||
#[must_use]
|
||||
pub fn list_all(&self) -> Vec<&RegisteredPlugin> {
|
||||
self.plugins.values().collect()
|
||||
}
|
||||
|
||||
/// List all enabled plugins
|
||||
#[must_use]
|
||||
pub fn list_enabled(&self) -> Vec<&RegisteredPlugin> {
|
||||
self.plugins.values().filter(|p| p.enabled).collect()
|
||||
}
|
||||
|
||||
/// Get plugins by kind (e.g., "media_type", "metadata_extractor")
|
||||
/// Get plugins by kind (e.g., "`media_type`", "`metadata_extractor`")
|
||||
#[must_use]
|
||||
pub fn get_by_kind(&self, kind: &str) -> Vec<&RegisteredPlugin> {
|
||||
self
|
||||
.plugins
|
||||
|
|
@ -113,11 +136,13 @@ impl PluginRegistry {
|
|||
}
|
||||
|
||||
/// Get count of registered plugins
|
||||
#[must_use]
|
||||
pub fn count(&self) -> usize {
|
||||
self.plugins.len()
|
||||
}
|
||||
|
||||
/// Get count of enabled plugins
|
||||
#[must_use]
|
||||
pub fn count_enabled(&self) -> usize {
|
||||
self.plugins.values().filter(|p| p.enabled).count()
|
||||
}
|
||||
|
|
@ -182,7 +207,7 @@ mod tests {
|
|||
let plugin =
|
||||
create_test_plugin("test-plugin", vec!["media_type".to_string()]);
|
||||
|
||||
registry.register(plugin.clone()).unwrap();
|
||||
registry.register(plugin).unwrap();
|
||||
|
||||
assert!(registry.is_loaded("test-plugin"));
|
||||
assert!(registry.get("test-plugin").is_some());
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::{path::Path, sync::Arc};
|
|||
|
||||
use anyhow::{Result, anyhow};
|
||||
use pinakes_plugin_api::PluginContext;
|
||||
use wasmtime::*;
|
||||
use wasmtime::{Caller, Config, Engine, Linker, Module, Store, Val, anyhow};
|
||||
|
||||
/// WASM runtime wrapper for executing plugins
|
||||
pub struct WasmRuntime {
|
||||
|
|
@ -13,6 +13,11 @@ pub struct WasmRuntime {
|
|||
|
||||
impl WasmRuntime {
|
||||
/// Create a new WASM runtime
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the WASM engine cannot be created with the given
|
||||
/// configuration.
|
||||
pub fn new() -> Result<Self> {
|
||||
let mut config = Config::new();
|
||||
config.wasm_component_model(true);
|
||||
|
|
@ -25,13 +30,18 @@ impl WasmRuntime {
|
|||
}
|
||||
|
||||
/// Load a plugin from a WASM file
|
||||
pub async fn load_plugin(
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the WASM file does not exist, cannot be read, or
|
||||
/// cannot be compiled.
|
||||
pub fn load_plugin(
|
||||
&self,
|
||||
wasm_path: &Path,
|
||||
context: PluginContext,
|
||||
) -> Result<WasmPlugin> {
|
||||
if !wasm_path.exists() {
|
||||
return Err(anyhow!("WASM file not found: {:?}", wasm_path));
|
||||
return Err(anyhow!("WASM file not found: {}", wasm_path.display()));
|
||||
}
|
||||
|
||||
let wasm_bytes = std::fs::read(wasm_path)?;
|
||||
|
|
@ -59,7 +69,8 @@ pub struct WasmPlugin {
|
|||
|
||||
impl WasmPlugin {
|
||||
/// Get the plugin context
|
||||
pub fn context(&self) -> &PluginContext {
|
||||
#[must_use]
|
||||
pub const fn context(&self) -> &PluginContext {
|
||||
&self.context
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +78,11 @@ impl WasmPlugin {
|
|||
///
|
||||
/// Creates a fresh store and instance per invocation with host functions
|
||||
/// linked, calls the requested exported function, and returns the result.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the function cannot be found, instantiation fails,
|
||||
/// or the function call returns an error.
|
||||
pub async fn call_function(
|
||||
&self,
|
||||
function_name: &str,
|
||||
|
|
@ -105,19 +121,23 @@ impl WasmPlugin {
|
|||
let offset = if let Ok(alloc) =
|
||||
instance.get_typed_func::<i32, i32>(&mut store, "alloc")
|
||||
{
|
||||
let result = alloc.call_async(&mut store, params.len() as i32).await?;
|
||||
let result = alloc
|
||||
.call_async(
|
||||
&mut store,
|
||||
i32::try_from(params.len()).unwrap_or(i32::MAX),
|
||||
)
|
||||
.await?;
|
||||
if result < 0 {
|
||||
return Err(anyhow!(
|
||||
"plugin alloc returned negative offset: {}",
|
||||
result
|
||||
"plugin alloc returned negative offset: {result}"
|
||||
));
|
||||
}
|
||||
result as usize
|
||||
u32::try_from(result).unwrap_or(0) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
alloc_offset = offset as i32;
|
||||
alloc_offset = i32::try_from(offset).unwrap_or(i32::MAX);
|
||||
let mem_data = mem.data_mut(&mut store);
|
||||
if offset + params.len() <= mem_data.len() {
|
||||
mem_data[offset..offset + params.len()].copy_from_slice(params);
|
||||
|
|
@ -128,7 +148,7 @@ impl WasmPlugin {
|
|||
instance
|
||||
.get_func(&mut store, function_name)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("exported function '{}' not found", function_name)
|
||||
anyhow!("exported function '{function_name}' not found")
|
||||
})?;
|
||||
|
||||
let func_ty = func.ty(&store);
|
||||
|
|
@ -143,7 +163,10 @@ impl WasmPlugin {
|
|||
func
|
||||
.call_async(
|
||||
&mut store,
|
||||
&[Val::I32(alloc_offset), Val::I32(params.len() as i32)],
|
||||
&[
|
||||
Val::I32(alloc_offset),
|
||||
Val::I32(i32::try_from(params.len()).unwrap_or(i32::MAX)),
|
||||
],
|
||||
&mut results,
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -152,7 +175,7 @@ impl WasmPlugin {
|
|||
} else {
|
||||
// Generic: fill with zeroes
|
||||
let params_vals: Vec<Val> =
|
||||
(0..param_count).map(|_| Val::I32(0)).collect();
|
||||
std::iter::repeat_n(Val::I32(0), param_count).collect();
|
||||
func
|
||||
.call_async(&mut store, ¶ms_vals, &mut results)
|
||||
.await?;
|
||||
|
|
@ -177,7 +200,7 @@ impl WasmPlugin {
|
|||
impl Default for WasmPlugin {
|
||||
fn default() -> Self {
|
||||
let engine = Engine::default();
|
||||
let module = Module::new(&engine, br#"(module)"#).unwrap();
|
||||
let module = Module::new(&engine, br"(module)").unwrap();
|
||||
|
||||
Self {
|
||||
module: Arc::new(module),
|
||||
|
|
@ -198,6 +221,10 @@ impl HostFunctions {
|
|||
/// Registers all host ABI functions (`host_log`, `host_read_file`,
|
||||
/// `host_write_file`, `host_http_request`, `host_get_config`,
|
||||
/// `host_get_buffer`) into the given linker.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any host function cannot be registered in the linker.
|
||||
pub fn setup_linker(linker: &mut Linker<PluginStoreData>) -> Result<()> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
|
|
@ -209,11 +236,13 @@ impl HostFunctions {
|
|||
if ptr < 0 || len < 0 {
|
||||
return;
|
||||
}
|
||||
let memory = caller.get_export("memory").and_then(|e| e.into_memory());
|
||||
let memory = caller
|
||||
.get_export("memory")
|
||||
.and_then(wasmtime::Extern::into_memory);
|
||||
if let Some(mem) = memory {
|
||||
let data = mem.data(&caller);
|
||||
let start = ptr as usize;
|
||||
let end = start + len as usize;
|
||||
let start = u32::try_from(ptr).unwrap_or(0) as usize;
|
||||
let end = start + u32::try_from(len).unwrap_or(0) as usize;
|
||||
if end <= data.len()
|
||||
&& let Ok(msg) = std::str::from_utf8(&data[start..end])
|
||||
{
|
||||
|
|
@ -238,12 +267,14 @@ impl HostFunctions {
|
|||
if path_ptr < 0 || path_len < 0 {
|
||||
return -1;
|
||||
}
|
||||
let memory = caller.get_export("memory").and_then(|e| e.into_memory());
|
||||
let memory = caller
|
||||
.get_export("memory")
|
||||
.and_then(wasmtime::Extern::into_memory);
|
||||
let Some(mem) = memory else { return -1 };
|
||||
|
||||
let data = mem.data(&caller);
|
||||
let start = path_ptr as usize;
|
||||
let end = start + path_len as usize;
|
||||
let start = u32::try_from(path_ptr).unwrap_or(0) as usize;
|
||||
let end = start + u32::try_from(path_len).unwrap_or(0) as usize;
|
||||
if end > data.len() {
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -254,9 +285,8 @@ impl HostFunctions {
|
|||
};
|
||||
|
||||
// Canonicalize path before checking permissions to prevent traversal
|
||||
let path = match std::path::Path::new(&path_str).canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return -1,
|
||||
let Ok(path) = std::path::Path::new(&path_str).canonicalize() else {
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Check read permission against canonicalized path
|
||||
|
|
@ -276,14 +306,11 @@ impl HostFunctions {
|
|||
return -2;
|
||||
}
|
||||
|
||||
match std::fs::read(&path) {
|
||||
Ok(contents) => {
|
||||
let len = contents.len() as i32;
|
||||
caller.data_mut().exchange_buffer = contents;
|
||||
len
|
||||
},
|
||||
Err(_) => -1,
|
||||
}
|
||||
std::fs::read(&path).map_or(-1, |contents| {
|
||||
let len = i32::try_from(contents.len()).unwrap_or(i32::MAX);
|
||||
caller.data_mut().exchange_buffer = contents;
|
||||
len
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
|
|
@ -299,14 +326,18 @@ impl HostFunctions {
|
|||
if path_ptr < 0 || path_len < 0 || data_ptr < 0 || data_len < 0 {
|
||||
return -1;
|
||||
}
|
||||
let memory = caller.get_export("memory").and_then(|e| e.into_memory());
|
||||
let memory = caller
|
||||
.get_export("memory")
|
||||
.and_then(wasmtime::Extern::into_memory);
|
||||
let Some(mem) = memory else { return -1 };
|
||||
|
||||
let mem_data = mem.data(&caller);
|
||||
let path_start = path_ptr as usize;
|
||||
let path_end = path_start + path_len as usize;
|
||||
let data_start = data_ptr as usize;
|
||||
let data_end = data_start + data_len as usize;
|
||||
let path_start = u32::try_from(path_ptr).unwrap_or(0) as usize;
|
||||
let path_end =
|
||||
path_start + u32::try_from(path_len).unwrap_or(0) as usize;
|
||||
let data_start = u32::try_from(data_ptr).unwrap_or(0) as usize;
|
||||
let data_end =
|
||||
data_start + u32::try_from(data_len).unwrap_or(0) as usize;
|
||||
|
||||
if path_end > mem_data.len() || data_end > mem_data.len() {
|
||||
return -1;
|
||||
|
|
@ -369,12 +400,14 @@ impl HostFunctions {
|
|||
if url_ptr < 0 || url_len < 0 {
|
||||
return -1;
|
||||
}
|
||||
let memory = caller.get_export("memory").and_then(|e| e.into_memory());
|
||||
let memory = caller
|
||||
.get_export("memory")
|
||||
.and_then(wasmtime::Extern::into_memory);
|
||||
let Some(mem) = memory else { return -1 };
|
||||
|
||||
let data = mem.data(&caller);
|
||||
let start = url_ptr as usize;
|
||||
let end = start + url_len as usize;
|
||||
let start = u32::try_from(url_ptr).unwrap_or(0) as usize;
|
||||
let end = start + u32::try_from(url_len).unwrap_or(0) as usize;
|
||||
if end > data.len() {
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -413,7 +446,7 @@ impl HostFunctions {
|
|||
|
||||
match result {
|
||||
Ok(Ok(bytes)) => {
|
||||
let len = bytes.len() as i32;
|
||||
let len = i32::try_from(bytes.len()).unwrap_or(i32::MAX);
|
||||
caller.data_mut().exchange_buffer = bytes.to_vec();
|
||||
len
|
||||
},
|
||||
|
|
@ -421,26 +454,19 @@ impl HostFunctions {
|
|||
Err(_) => {
|
||||
// block_in_place panicked (e.g. current-thread runtime);
|
||||
// fall back to blocking client with timeout
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
let Ok(client) = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return -1,
|
||||
else {
|
||||
return -1;
|
||||
};
|
||||
match client.get(&url_str).send() {
|
||||
Ok(resp) => {
|
||||
match resp.bytes() {
|
||||
Ok(bytes) => {
|
||||
let len = bytes.len() as i32;
|
||||
caller.data_mut().exchange_buffer = bytes.to_vec();
|
||||
len
|
||||
},
|
||||
Err(_) => -1,
|
||||
}
|
||||
},
|
||||
Err(_) => -1,
|
||||
}
|
||||
client.get(&url_str).send().map_or(-1, |resp| {
|
||||
resp.bytes().map_or(-1, |bytes| {
|
||||
let len = i32::try_from(bytes.len()).unwrap_or(i32::MAX);
|
||||
caller.data_mut().exchange_buffer = bytes.to_vec();
|
||||
len
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -456,12 +482,14 @@ impl HostFunctions {
|
|||
if key_ptr < 0 || key_len < 0 {
|
||||
return -1;
|
||||
}
|
||||
let memory = caller.get_export("memory").and_then(|e| e.into_memory());
|
||||
let memory = caller
|
||||
.get_export("memory")
|
||||
.and_then(wasmtime::Extern::into_memory);
|
||||
let Some(mem) = memory else { return -1 };
|
||||
|
||||
let data = mem.data(&caller);
|
||||
let start = key_ptr as usize;
|
||||
let end = start + key_len as usize;
|
||||
let start = u32::try_from(key_ptr).unwrap_or(0) as usize;
|
||||
let end = start + u32::try_from(key_len).unwrap_or(0) as usize;
|
||||
if end > data.len() {
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -471,16 +499,17 @@ impl HostFunctions {
|
|||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
match caller.data().context.config.get(&key_str) {
|
||||
Some(value) => {
|
||||
let json = value.to_string();
|
||||
let bytes = json.into_bytes();
|
||||
let len = bytes.len() as i32;
|
||||
caller.data_mut().exchange_buffer = bytes;
|
||||
len
|
||||
},
|
||||
None => -1,
|
||||
}
|
||||
let bytes = caller
|
||||
.data()
|
||||
.context
|
||||
.config
|
||||
.get(&key_str)
|
||||
.map(|value| value.to_string().into_bytes());
|
||||
bytes.map_or(-1, |b| {
|
||||
let len = i32::try_from(b.len()).unwrap_or(i32::MAX);
|
||||
caller.data_mut().exchange_buffer = b;
|
||||
len
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
|
|
@ -495,19 +524,22 @@ impl HostFunctions {
|
|||
return -1;
|
||||
}
|
||||
let buf = caller.data().exchange_buffer.clone();
|
||||
let copy_len = buf.len().min(dest_len as usize);
|
||||
let copy_len =
|
||||
buf.len().min(u32::try_from(dest_len).unwrap_or(0) as usize);
|
||||
|
||||
let memory = caller.get_export("memory").and_then(|e| e.into_memory());
|
||||
let memory = caller
|
||||
.get_export("memory")
|
||||
.and_then(wasmtime::Extern::into_memory);
|
||||
let Some(mem) = memory else { return -1 };
|
||||
|
||||
let mem_data = mem.data_mut(&mut caller);
|
||||
let start = dest_ptr as usize;
|
||||
let start = u32::try_from(dest_ptr).unwrap_or(0) as usize;
|
||||
if start + copy_len > mem_data.len() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mem_data[start..start + copy_len].copy_from_slice(&buf[..copy_len]);
|
||||
copy_len as i32
|
||||
i32::try_from(copy_len).unwrap_or(i32::MAX)
|
||||
},
|
||||
)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ pub struct CapabilityEnforcer {
|
|||
|
||||
impl CapabilityEnforcer {
|
||||
/// Create a new capability enforcer with default limits
|
||||
pub fn new() -> Self {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
max_memory_limit: 512 * 1024 * 1024, // 512 MB
|
||||
max_cpu_time_limit: 60 * 1000, // 60 seconds
|
||||
|
|
@ -36,36 +37,47 @@ impl CapabilityEnforcer {
|
|||
}
|
||||
|
||||
/// Set maximum memory limit
|
||||
pub fn with_max_memory(mut self, bytes: usize) -> Self {
|
||||
#[must_use]
|
||||
pub const fn with_max_memory(mut self, bytes: usize) -> Self {
|
||||
self.max_memory_limit = bytes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum CPU time limit
|
||||
pub fn with_max_cpu_time(mut self, milliseconds: u64) -> Self {
|
||||
#[must_use]
|
||||
pub const fn with_max_cpu_time(mut self, milliseconds: u64) -> Self {
|
||||
self.max_cpu_time_limit = milliseconds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add allowed read path
|
||||
#[must_use]
|
||||
pub fn allow_read_path(mut self, path: PathBuf) -> Self {
|
||||
self.allowed_read_paths.push(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add allowed write path
|
||||
#[must_use]
|
||||
pub fn allow_write_path(mut self, path: PathBuf) -> Self {
|
||||
self.allowed_write_paths.push(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set default network access policy
|
||||
pub fn with_network_default(mut self, allow: bool) -> Self {
|
||||
#[must_use]
|
||||
pub const fn with_network_default(mut self, allow: bool) -> Self {
|
||||
self.allow_network_default = allow;
|
||||
self
|
||||
}
|
||||
|
||||
/// Validate capabilities requested by a plugin
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the plugin requests capabilities that exceed the
|
||||
/// configured system limits, such as memory, CPU time, filesystem paths, or
|
||||
/// network access.
|
||||
pub fn validate_capabilities(
|
||||
&self,
|
||||
capabilities: &Capabilities,
|
||||
|
|
@ -115,8 +127,8 @@ impl CapabilityEnforcer {
|
|||
for path in &capabilities.filesystem.read {
|
||||
if !self.is_read_allowed(path) {
|
||||
return Err(anyhow!(
|
||||
"Plugin requests read access to {:?} which is not in allowed paths",
|
||||
path
|
||||
"Plugin requests read access to {} which is not in allowed paths",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -125,8 +137,8 @@ impl CapabilityEnforcer {
|
|||
for path in &capabilities.filesystem.write {
|
||||
if !self.is_write_allowed(path) {
|
||||
return Err(anyhow!(
|
||||
"Plugin requests write access to {:?} which is not in allowed paths",
|
||||
path
|
||||
"Plugin requests write access to {} which is not in allowed paths",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +147,7 @@ impl CapabilityEnforcer {
|
|||
}
|
||||
|
||||
/// Check if a path is allowed for reading
|
||||
#[must_use]
|
||||
pub fn is_read_allowed(&self, path: &Path) -> bool {
|
||||
if self.allowed_read_paths.is_empty() {
|
||||
return false; // deny-all when unconfigured
|
||||
|
|
@ -150,6 +163,7 @@ impl CapabilityEnforcer {
|
|||
}
|
||||
|
||||
/// Check if a path is allowed for writing
|
||||
#[must_use]
|
||||
pub fn is_write_allowed(&self, path: &Path) -> bool {
|
||||
if self.allowed_write_paths.is_empty() {
|
||||
return false; // deny-all when unconfigured
|
||||
|
|
@ -173,11 +187,13 @@ impl CapabilityEnforcer {
|
|||
}
|
||||
|
||||
/// Check if network access is allowed for a plugin
|
||||
pub fn is_network_allowed(&self, capabilities: &Capabilities) -> bool {
|
||||
#[must_use]
|
||||
pub const fn is_network_allowed(&self, capabilities: &Capabilities) -> bool {
|
||||
capabilities.network.enabled && self.allow_network_default
|
||||
}
|
||||
|
||||
/// Check if a specific domain is allowed
|
||||
#[must_use]
|
||||
pub fn is_domain_allowed(
|
||||
&self,
|
||||
capabilities: &Capabilities,
|
||||
|
|
@ -197,11 +213,13 @@ impl CapabilityEnforcer {
|
|||
.network
|
||||
.allowed_domains
|
||||
.as_ref()
|
||||
.map(|domains| domains.iter().any(|d| d.eq_ignore_ascii_case(domain)))
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|domains| {
|
||||
domains.iter().any(|d| d.eq_ignore_ascii_case(domain))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get effective memory limit for a plugin
|
||||
#[must_use]
|
||||
pub fn get_memory_limit(&self, capabilities: &Capabilities) -> usize {
|
||||
capabilities
|
||||
.max_memory_bytes
|
||||
|
|
@ -210,6 +228,7 @@ impl CapabilityEnforcer {
|
|||
}
|
||||
|
||||
/// Get effective CPU time limit for a plugin
|
||||
#[must_use]
|
||||
pub fn get_cpu_time_limit(&self, capabilities: &Capabilities) -> u64 {
|
||||
capabilities
|
||||
.max_cpu_time_ms
|
||||
|
|
@ -264,8 +283,7 @@ mod tests {
|
|||
let test_file = allowed_dir.join("test.txt");
|
||||
std::fs::write(&test_file, "test").unwrap();
|
||||
|
||||
let enforcer =
|
||||
CapabilityEnforcer::new().allow_read_path(allowed_dir.clone());
|
||||
let enforcer = CapabilityEnforcer::new().allow_read_path(allowed_dir);
|
||||
|
||||
assert!(enforcer.is_read_allowed(&test_file));
|
||||
assert!(!enforcer.is_read_allowed(Path::new("/etc/passwd")));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue