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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue