various: simplify code; work on security and performance
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
This commit is contained in:
parent
016841b200
commit
c4adc4e3e0
75 changed files with 12921 additions and 358 deletions
263
crates/pinakes-plugin-api/src/manifest.rs
Normal file
263
crates/pinakes-plugin-api/src/manifest.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! Plugin manifest parsing and validation
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{Capabilities, EnvironmentCapability, FilesystemCapability, NetworkCapability};
|
||||
|
||||
/// Plugin manifest file format (TOML)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginManifest {
|
||||
pub plugin: PluginInfo,
|
||||
|
||||
#[serde(default)]
|
||||
pub capabilities: ManifestCapabilities,
|
||||
|
||||
#[serde(default)]
|
||||
pub config: HashMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub api_version: String,
|
||||
pub author: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub license: Option<String>,
|
||||
|
||||
/// Plugin kind(s) - e.g., ["media_type", "metadata_extractor"]
|
||||
pub kind: Vec<String>,
|
||||
|
||||
/// Binary configuration
|
||||
pub binary: PluginBinary,
|
||||
|
||||
/// Dependencies on other plugins
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginBinary {
|
||||
/// Path to WASM binary
|
||||
pub wasm: String,
|
||||
|
||||
/// Optional entrypoint function name (default: "_start")
|
||||
pub entrypoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ManifestCapabilities {
|
||||
#[serde(default)]
|
||||
pub filesystem: ManifestFilesystemCapability,
|
||||
|
||||
#[serde(default)]
|
||||
pub network: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub environment: Option<Vec<String>>,
|
||||
|
||||
#[serde(default)]
|
||||
pub max_memory_mb: Option<usize>,
|
||||
|
||||
#[serde(default)]
|
||||
pub max_cpu_time_secs: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ManifestFilesystemCapability {
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ManifestError {
|
||||
#[error("Failed to read manifest file: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to parse manifest: {0}")]
|
||||
ParseError(#[from] toml::de::Error),
|
||||
|
||||
#[error("Invalid manifest: {0}")]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
impl PluginManifest {
|
||||
/// Load and parse a plugin manifest from a TOML file
|
||||
pub fn from_file(path: &Path) -> Result<Self, ManifestError> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let manifest: Self = toml::from_str(&content)?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
/// Parse a manifest from TOML string
|
||||
pub fn from_str(content: &str) -> Result<Self, ManifestError> {
|
||||
let manifest: Self = toml::from_str(content)?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
/// Validate the manifest
|
||||
pub fn validate(&self) -> Result<(), ManifestError> {
|
||||
// Check API version format
|
||||
if self.plugin.api_version.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"api_version cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check version format (basic semver check)
|
||||
if !self.plugin.version.contains('.') {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"version must be in semver format (e.g., 1.0.0)".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check that at least one kind is specified
|
||||
if self.plugin.kind.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"at least one plugin kind must be specified".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check plugin kinds are valid
|
||||
let valid_kinds = [
|
||||
"media_type",
|
||||
"metadata_extractor",
|
||||
"thumbnail_generator",
|
||||
"search_backend",
|
||||
"event_handler",
|
||||
"theme_provider",
|
||||
];
|
||||
|
||||
for kind in &self.plugin.kind {
|
||||
if !valid_kinds.contains(&kind.as_str()) {
|
||||
return Err(ManifestError::ValidationError(format!(
|
||||
"Invalid plugin kind: {}. Must be one of: {}",
|
||||
kind,
|
||||
valid_kinds.join(", ")
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Check WASM binary path is not empty
|
||||
if self.plugin.binary.wasm.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"WASM binary path cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert manifest capabilities to API capabilities
|
||||
pub fn to_capabilities(&self) -> Capabilities {
|
||||
Capabilities {
|
||||
filesystem: FilesystemCapability {
|
||||
read: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.read
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.collect(),
|
||||
write: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.write
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.collect(),
|
||||
},
|
||||
network: NetworkCapability {
|
||||
enabled: self.capabilities.network,
|
||||
allowed_domains: None,
|
||||
},
|
||||
environment: EnvironmentCapability {
|
||||
enabled: self.capabilities.environment.is_some(),
|
||||
allowed_vars: self.capabilities.environment.clone(),
|
||||
},
|
||||
max_memory_bytes: self
|
||||
.capabilities
|
||||
.max_memory_mb
|
||||
.map(|mb| mb.saturating_mul(1024).saturating_mul(1024)),
|
||||
max_cpu_time_ms: self
|
||||
.capabilities
|
||||
.max_cpu_time_secs
|
||||
.map(|secs| secs.saturating_mul(1000)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get plugin ID (derived from name and version)
|
||||
pub fn plugin_id(&self) -> String {
|
||||
format!("{}@{}", self.plugin.name, self.plugin.version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_valid_manifest() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "heif-support"
|
||||
version = "1.0.0"
|
||||
api_version = "1.0"
|
||||
author = "Test Author"
|
||||
description = "HEIF image support"
|
||||
kind = ["media_type", "metadata_extractor"]
|
||||
|
||||
[plugin.binary]
|
||||
wasm = "plugin.wasm"
|
||||
|
||||
[capabilities.filesystem]
|
||||
read = ["/tmp/pinakes-thumbnails"]
|
||||
"#;
|
||||
|
||||
let manifest = PluginManifest::from_str(toml).unwrap();
|
||||
assert_eq!(manifest.plugin.name, "heif-support");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.kind.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_api_version() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
api_version = ""
|
||||
kind = ["media_type"]
|
||||
|
||||
[plugin.binary]
|
||||
wasm = "plugin.wasm"
|
||||
"#;
|
||||
|
||||
assert!(PluginManifest::from_str(toml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_kind() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
api_version = "1.0"
|
||||
kind = ["invalid_kind"]
|
||||
|
||||
[plugin.binary]
|
||||
wasm = "plugin.wasm"
|
||||
"#;
|
||||
|
||||
assert!(PluginManifest::from_str(toml).is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue