//! 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, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginInfo { pub name: String, pub version: String, pub api_version: String, pub author: Option, pub description: Option, pub homepage: Option, pub license: Option, /// Plugin kind(s) - e.g., ["media_type", "metadata_extractor"] pub kind: Vec, /// Binary configuration pub binary: PluginBinary, /// Dependencies on other plugins #[serde(default)] pub dependencies: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginBinary { /// Path to WASM binary pub wasm: String, /// Optional entrypoint function name (default: "_start") pub entrypoint: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ManifestCapabilities { #[serde(default)] pub filesystem: ManifestFilesystemCapability, #[serde(default)] pub network: bool, #[serde(default)] pub environment: Option>, #[serde(default)] pub max_memory_mb: Option, #[serde(default)] pub max_cpu_time_secs: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ManifestFilesystemCapability { #[serde(default)] pub read: Vec, #[serde(default)] pub write: Vec, } #[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 { 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 parse_str(content: &str) -> Result { 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::parse_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::parse_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::parse_str(toml).is_err()); } }