treewide: fix various UI bugs; optimize crypto dependencies & format

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
raf 2026-02-10 12:56:05 +03:00
commit 3ccddce7fd
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
178 changed files with 58342 additions and 54241 deletions

View file

@ -1,12 +1,16 @@
//! Pinakes Plugin API
//!
//! This crate defines the stable plugin interface for Pinakes.
//! Plugins can extend Pinakes by implementing one or more of the provided traits.
//! Plugins can extend Pinakes by implementing one or more of the provided
//! traits.
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
pub mod manifest;
@ -26,350 +30,361 @@ pub type PluginResult<T> = Result<T, PluginError>;
/// Errors that can occur in plugin operations
#[derive(Debug, Error, Serialize, Deserialize)]
pub enum PluginError {
#[error("Plugin initialization failed: {0}")]
InitializationFailed(String),
#[error("Plugin initialization failed: {0}")]
InitializationFailed(String),
#[error("Unsupported operation: {0}")]
UnsupportedOperation(String),
#[error("Unsupported operation: {0}")]
UnsupportedOperation(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("IO error: {0}")]
IoError(String),
#[error("IO error: {0}")]
IoError(String),
#[error("Metadata extraction failed: {0}")]
MetadataExtractionFailed(String),
#[error("Metadata extraction failed: {0}")]
MetadataExtractionFailed(String),
#[error("Thumbnail generation failed: {0}")]
ThumbnailGenerationFailed(String),
#[error("Thumbnail generation failed: {0}")]
ThumbnailGenerationFailed(String),
#[error("Search backend error: {0}")]
SearchBackendError(String),
#[error("Search backend error: {0}")]
SearchBackendError(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Resource limit exceeded: {0}")]
ResourceLimitExceeded(String),
#[error("Resource limit exceeded: {0}")]
ResourceLimitExceeded(String),
#[error("Plugin error: {0}")]
Other(String),
#[error("Plugin error: {0}")]
Other(String),
}
/// Context provided to plugins during initialization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginContext {
/// Plugin's data directory for persistent storage
pub data_dir: PathBuf,
/// Plugin's data directory for persistent storage
pub data_dir: PathBuf,
/// Plugin's cache directory for temporary data
pub cache_dir: PathBuf,
/// Plugin's cache directory for temporary data
pub cache_dir: PathBuf,
/// Plugin configuration from manifest
pub config: HashMap<String, serde_json::Value>,
/// Plugin configuration from manifest
pub config: HashMap<String, serde_json::Value>,
/// Capabilities granted to the plugin
pub capabilities: Capabilities,
/// Capabilities granted to the plugin
pub capabilities: Capabilities,
}
/// Capabilities that can be granted to plugins
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Capabilities {
/// Filesystem access permissions
pub filesystem: FilesystemCapability,
/// Filesystem access permissions
pub filesystem: FilesystemCapability,
/// Network access permissions
pub network: NetworkCapability,
/// Network access permissions
pub network: NetworkCapability,
/// Environment variable access
pub environment: EnvironmentCapability,
/// Environment variable access
pub environment: EnvironmentCapability,
/// Maximum memory usage in bytes
pub max_memory_bytes: Option<usize>,
/// Maximum memory usage in bytes
pub max_memory_bytes: Option<usize>,
/// Maximum CPU time in milliseconds
pub max_cpu_time_ms: Option<u64>,
/// Maximum CPU time in milliseconds
pub max_cpu_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FilesystemCapability {
/// Paths allowed for reading
pub read: Vec<PathBuf>,
/// Paths allowed for reading
pub read: Vec<PathBuf>,
/// Paths allowed for writing
pub write: Vec<PathBuf>,
/// Paths allowed for writing
pub write: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NetworkCapability {
/// Whether network access is allowed
pub enabled: bool,
/// Whether network access is allowed
pub enabled: bool,
/// Allowed domains (if None, all domains allowed when enabled)
pub allowed_domains: Option<Vec<String>>,
/// Allowed domains (if None, all domains allowed when enabled)
pub allowed_domains: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvironmentCapability {
/// Whether environment variable access is allowed
pub enabled: bool,
/// Whether environment variable access is allowed
pub enabled: bool,
/// Specific environment variables allowed (if None, all allowed when enabled)
pub allowed_vars: Option<Vec<String>>,
/// Specific environment variables allowed (if None, all allowed when
/// enabled)
pub allowed_vars: Option<Vec<String>>,
}
/// Base trait that all plugins must implement
#[async_trait]
pub trait Plugin: Send + Sync {
/// Get plugin metadata
fn metadata(&self) -> &PluginMetadata;
/// Get plugin metadata
fn metadata(&self) -> &PluginMetadata;
/// Initialize the plugin with provided context
async fn initialize(&mut self, context: PluginContext) -> PluginResult<()>;
/// Initialize the plugin with provided context
async fn initialize(&mut self, context: PluginContext) -> PluginResult<()>;
/// Shutdown the plugin gracefully
async fn shutdown(&mut self) -> PluginResult<()>;
/// Shutdown the plugin gracefully
async fn shutdown(&mut self) -> PluginResult<()>;
/// Get plugin health status
async fn health_check(&self) -> PluginResult<HealthStatus>;
/// Get plugin health status
async fn health_check(&self) -> PluginResult<HealthStatus>;
}
/// Plugin metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMetadata {
pub id: String,
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub api_version: String,
pub capabilities_required: Capabilities,
pub id: String,
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub api_version: String,
pub capabilities_required: Capabilities,
}
/// Health status of a plugin
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub healthy: bool,
pub message: Option<String>,
pub metrics: HashMap<String, f64>,
pub healthy: bool,
pub message: Option<String>,
pub metrics: HashMap<String, f64>,
}
/// Trait for plugins that provide custom media type support
#[async_trait]
pub trait MediaTypeProvider: Plugin {
/// Get the list of media types this plugin supports
fn supported_media_types(&self) -> Vec<MediaTypeDefinition>;
/// Get the list of media types this plugin supports
fn supported_media_types(&self) -> Vec<MediaTypeDefinition>;
/// Check if this plugin can handle the given file
async fn can_handle(&self, path: &Path, mime_type: Option<&str>) -> PluginResult<bool>;
/// Check if this plugin can handle the given file
async fn can_handle(
&self,
path: &Path,
mime_type: Option<&str>,
) -> PluginResult<bool>;
}
/// Definition of a custom media type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaTypeDefinition {
/// Unique identifier for this media type
pub id: String,
/// Unique identifier for this media type
pub id: String,
/// Display name
pub name: String,
/// Display name
pub name: String,
/// Category (e.g., "video", "audio", "document", "image")
pub category: String,
/// Category (e.g., "video", "audio", "document", "image")
pub category: String,
/// File extensions associated with this type
pub extensions: Vec<String>,
/// File extensions associated with this type
pub extensions: Vec<String>,
/// MIME types associated with this type
pub mime_types: Vec<String>,
/// MIME types associated with this type
pub mime_types: Vec<String>,
/// Icon name or path
pub icon: Option<String>,
/// Icon name or path
pub icon: Option<String>,
}
/// Trait for plugins that extract metadata from files
#[async_trait]
pub trait MetadataExtractor: Plugin {
/// Extract metadata from a file
async fn extract_metadata(&self, path: &Path) -> PluginResult<ExtractedMetadata>;
/// Extract metadata from a file
async fn extract_metadata(
&self,
path: &Path,
) -> PluginResult<ExtractedMetadata>;
/// Get the media types this extractor supports
fn supported_types(&self) -> Vec<String>;
/// Get the media types this extractor supports
fn supported_types(&self) -> Vec<String>;
}
/// Metadata extracted from a file
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExtractedMetadata {
pub title: Option<String>,
pub description: Option<String>,
pub author: Option<String>,
pub created_at: Option<String>,
pub duration_secs: Option<f64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub file_size_bytes: Option<u64>,
pub codec: Option<String>,
pub bitrate_kbps: Option<u32>,
pub title: Option<String>,
pub description: Option<String>,
pub author: Option<String>,
pub created_at: Option<String>,
pub duration_secs: Option<f64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub file_size_bytes: Option<u64>,
pub codec: Option<String>,
pub bitrate_kbps: Option<u32>,
/// Custom metadata fields specific to this file type
pub custom_fields: HashMap<String, serde_json::Value>,
/// Custom metadata fields specific to this file type
pub custom_fields: HashMap<String, serde_json::Value>,
/// Tags extracted from the file
pub tags: Vec<String>,
/// Tags extracted from the file
pub tags: Vec<String>,
}
/// Trait for plugins that generate thumbnails
#[async_trait]
pub trait ThumbnailGenerator: Plugin {
/// Generate a thumbnail for the given file
async fn generate_thumbnail(
&self,
path: &Path,
output_path: &Path,
options: ThumbnailOptions,
) -> PluginResult<ThumbnailInfo>;
/// Generate a thumbnail for the given file
async fn generate_thumbnail(
&self,
path: &Path,
output_path: &Path,
options: ThumbnailOptions,
) -> PluginResult<ThumbnailInfo>;
/// Get the media types this generator supports
fn supported_types(&self) -> Vec<String>;
/// Get the media types this generator supports
fn supported_types(&self) -> Vec<String>;
}
/// Options for thumbnail generation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThumbnailOptions {
pub width: u32,
pub height: u32,
pub quality: u8,
pub format: ThumbnailFormat,
pub width: u32,
pub height: u32,
pub quality: u8,
pub format: ThumbnailFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ThumbnailFormat {
Jpeg,
Png,
WebP,
Jpeg,
Png,
WebP,
}
/// Information about a generated thumbnail
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThumbnailInfo {
pub path: PathBuf,
pub width: u32,
pub height: u32,
pub file_size_bytes: u64,
pub path: PathBuf,
pub width: u32,
pub height: u32,
pub file_size_bytes: u64,
}
/// Trait for plugins that provide custom search backends
#[async_trait]
pub trait SearchBackend: Plugin {
/// Index a media item for search
async fn index_item(&self, item: &SearchIndexItem) -> PluginResult<()>;
/// Index a media item for search
async fn index_item(&self, item: &SearchIndexItem) -> PluginResult<()>;
/// Remove an item from the search index
async fn remove_item(&self, item_id: &str) -> PluginResult<()>;
/// Remove an item from the search index
async fn remove_item(&self, item_id: &str) -> PluginResult<()>;
/// Perform a search query
async fn search(&self, query: &SearchQuery) -> PluginResult<Vec<SearchResult>>;
/// Perform a search query
async fn search(
&self,
query: &SearchQuery,
) -> PluginResult<Vec<SearchResult>>;
/// Get search statistics
async fn get_stats(&self) -> PluginResult<SearchStats>;
/// Get search statistics
async fn get_stats(&self) -> PluginResult<SearchStats>;
}
/// Item to be indexed for search
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchIndexItem {
pub id: String,
pub title: Option<String>,
pub description: Option<String>,
pub content: Option<String>,
pub tags: Vec<String>,
pub media_type: String,
pub metadata: HashMap<String, serde_json::Value>,
pub id: String,
pub title: Option<String>,
pub description: Option<String>,
pub content: Option<String>,
pub tags: Vec<String>,
pub media_type: String,
pub metadata: HashMap<String, serde_json::Value>,
}
/// Search query
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
pub query_text: String,
pub filters: HashMap<String, serde_json::Value>,
pub limit: usize,
pub offset: usize,
pub query_text: String,
pub filters: HashMap<String, serde_json::Value>,
pub limit: usize,
pub offset: usize,
}
/// Search result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub id: String,
pub score: f64,
pub highlights: Vec<String>,
pub id: String,
pub score: f64,
pub highlights: Vec<String>,
}
/// Search statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchStats {
pub total_indexed: usize,
pub index_size_bytes: u64,
pub last_update: Option<String>,
pub total_indexed: usize,
pub index_size_bytes: u64,
pub last_update: Option<String>,
}
/// Trait for plugins that handle events
#[async_trait]
pub trait EventHandler: Plugin {
/// Handle an event
async fn handle_event(&self, event: &Event) -> PluginResult<()>;
/// Handle an event
async fn handle_event(&self, event: &Event) -> PluginResult<()>;
/// Get the event types this handler is interested in
fn interested_events(&self) -> Vec<EventType>;
/// Get the event types this handler is interested in
fn interested_events(&self) -> Vec<EventType>;
}
/// Event type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum EventType {
MediaImported,
MediaUpdated,
MediaDeleted,
MediaTagged,
MediaUntagged,
CollectionCreated,
CollectionUpdated,
CollectionDeleted,
ScanStarted,
ScanCompleted,
Custom(String),
MediaImported,
MediaUpdated,
MediaDeleted,
MediaTagged,
MediaUntagged,
CollectionCreated,
CollectionUpdated,
CollectionDeleted,
ScanStarted,
ScanCompleted,
Custom(String),
}
/// Event data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub event_type: EventType,
pub timestamp: String,
pub data: HashMap<String, serde_json::Value>,
pub event_type: EventType,
pub timestamp: String,
pub data: HashMap<String, serde_json::Value>,
}
/// Trait for plugins that provide UI themes
#[async_trait]
pub trait ThemeProvider: Plugin {
/// Get available themes from this provider
fn get_themes(&self) -> Vec<ThemeDefinition>;
/// Get available themes from this provider
fn get_themes(&self) -> Vec<ThemeDefinition>;
/// Load a specific theme by ID
async fn load_theme(&self, theme_id: &str) -> PluginResult<Theme>;
/// Load a specific theme by ID
async fn load_theme(&self, theme_id: &str) -> PluginResult<Theme>;
}
/// Theme definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeDefinition {
pub id: String,
pub name: String,
pub description: String,
pub author: String,
pub preview_url: Option<String>,
pub id: String,
pub name: String,
pub description: String,
pub author: String,
pub preview_url: Option<String>,
}
/// Theme data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Theme {
pub id: String,
pub colors: HashMap<String, String>,
pub fonts: HashMap<String, String>,
pub custom_css: Option<String>,
pub id: String,
pub colors: HashMap<String, String>,
pub fonts: HashMap<String, String>,
pub custom_css: Option<String>,
}

View file

@ -1,213 +1,218 @@
//! Plugin manifest parsing and validation
use std::{collections::HashMap, path::Path};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use thiserror::Error;
use crate::{Capabilities, EnvironmentCapability, FilesystemCapability, NetworkCapability};
use crate::{
Capabilities,
EnvironmentCapability,
FilesystemCapability,
NetworkCapability,
};
/// Plugin manifest file format (TOML)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub plugin: PluginInfo,
pub plugin: PluginInfo,
#[serde(default)]
pub capabilities: ManifestCapabilities,
#[serde(default)]
pub capabilities: ManifestCapabilities,
#[serde(default)]
pub config: HashMap<String, toml::Value>,
#[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>,
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>,
/// Plugin kind(s) - e.g., ["media_type", "metadata_extractor"]
pub kind: Vec<String>,
/// Binary configuration
pub binary: PluginBinary,
/// Binary configuration
pub binary: PluginBinary,
/// Dependencies on other plugins
#[serde(default)]
pub dependencies: Vec<String>,
/// 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,
/// Path to WASM binary
pub wasm: String,
/// Optional entrypoint function name (default: "_start")
pub entrypoint: Option<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 filesystem: ManifestFilesystemCapability,
#[serde(default)]
pub network: bool,
#[serde(default)]
pub network: bool,
#[serde(default)]
pub environment: Option<Vec<String>>,
#[serde(default)]
pub environment: Option<Vec<String>>,
#[serde(default)]
pub max_memory_mb: Option<usize>,
#[serde(default)]
pub max_memory_mb: Option<usize>,
#[serde(default)]
pub max_cpu_time_secs: Option<u64>,
#[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 read: Vec<String>,
#[serde(default)]
pub write: 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 read manifest file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse manifest: {0}")]
ParseError(#[from] toml::de::Error),
#[error("Failed to parse manifest: {0}")]
ParseError(#[from] toml::de::Error),
#[error("Invalid manifest: {0}")]
ValidationError(String),
#[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)
/// 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 parse_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(),
));
}
/// Parse a manifest from TOML string
pub fn parse_str(content: &str) -> Result<Self, ManifestError> {
let manifest: Self = toml::from_str(content)?;
manifest.validate()?;
Ok(manifest)
// 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(),
));
}
/// 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(())
// 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(),
));
}
/// 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)),
}
// 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(", ")
)));
}
}
/// Get plugin ID (derived from name and version)
pub fn plugin_id(&self) -> String {
format!("{}@{}", self.plugin.name, self.plugin.version)
// 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::*;
use super::*;
#[test]
fn test_parse_valid_manifest() {
let toml = r#"
#[test]
fn test_parse_valid_manifest() {
let toml = r#"
[plugin]
name = "heif-support"
version = "1.0.0"
@ -223,15 +228,15 @@ wasm = "plugin.wasm"
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);
}
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#"
#[test]
fn test_invalid_api_version() {
let toml = r#"
[plugin]
name = "test"
version = "1.0.0"
@ -242,12 +247,12 @@ kind = ["media_type"]
wasm = "plugin.wasm"
"#;
assert!(PluginManifest::parse_str(toml).is_err());
}
assert!(PluginManifest::parse_str(toml).is_err());
}
#[test]
fn test_invalid_kind() {
let toml = r#"
#[test]
fn test_invalid_kind() {
let toml = r#"
[plugin]
name = "test"
version = "1.0.0"
@ -258,6 +263,6 @@ kind = ["invalid_kind"]
wasm = "plugin.wasm"
"#;
assert!(PluginManifest::parse_str(toml).is_err());
}
assert!(PluginManifest::parse_str(toml).is_err());
}
}

View file

@ -1,156 +1,157 @@
//! Shared types used across the plugin API
use serde::{Deserialize, Serialize};
use std::fmt;
use serde::{Deserialize, Serialize};
/// Plugin identifier
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PluginId(String);
impl PluginId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PluginId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for PluginId {
fn from(s: String) -> Self {
Self(s)
}
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for PluginId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
/// Plugin lifecycle state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginState {
/// Plugin is being loaded
Loading,
/// Plugin is being loaded
Loading,
/// Plugin is initialized and ready
Ready,
/// Plugin is initialized and ready
Ready,
/// Plugin is running
Running,
/// Plugin is running
Running,
/// Plugin encountered an error
Error,
/// Plugin encountered an error
Error,
/// Plugin is being shut down
ShuttingDown,
/// Plugin is being shut down
ShuttingDown,
/// Plugin is stopped
Stopped,
/// Plugin is stopped
Stopped,
}
impl fmt::Display for PluginState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Loading => write!(f, "loading"),
Self::Ready => write!(f, "ready"),
Self::Running => write!(f, "running"),
Self::Error => write!(f, "error"),
Self::ShuttingDown => write!(f, "shutting_down"),
Self::Stopped => write!(f, "stopped"),
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Loading => write!(f, "loading"),
Self::Ready => write!(f, "ready"),
Self::Running => write!(f, "running"),
Self::Error => write!(f, "error"),
Self::ShuttingDown => write!(f, "shutting_down"),
Self::Stopped => write!(f, "stopped"),
}
}
}
/// Plugin installation status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginStatus {
pub id: PluginId,
pub name: String,
pub version: String,
pub state: PluginState,
pub enabled: bool,
pub error_message: Option<String>,
pub id: PluginId,
pub name: String,
pub version: String,
pub state: PluginState,
pub enabled: bool,
pub error_message: Option<String>,
}
/// Version information
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl Version {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
/// Parse version from string (e.g., "1.2.3")
pub fn parse(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return None;
}
/// Parse version from string (e.g., "1.2.3")
pub fn parse(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return None;
}
Some(Self {
major: parts[0].parse().ok()?,
minor: parts[1].parse().ok()?,
patch: parts[2].parse().ok()?,
})
}
Some(Self {
major: parts[0].parse().ok()?,
minor: parts[1].parse().ok()?,
patch: parts[2].parse().ok()?,
})
}
/// Check if this version is compatible with another version
/// Compatible if major version matches and minor version is >= required
pub fn is_compatible_with(&self, required: &Version) -> bool {
self.major == required.major && self.minor >= required.minor
}
/// Check if this version is compatible with another version
/// Compatible if major version matches and minor version is >= required
pub fn is_compatible_with(&self, required: &Version) -> bool {
self.major == required.major && self.minor >= required.minor
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_version_parse() {
let v = Version::parse("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
}
#[test]
fn test_version_parse() {
let v = Version::parse("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
}
#[test]
fn test_version_compatibility() {
let v1 = Version::new(1, 2, 0);
let v2 = Version::new(1, 1, 0);
let v3 = Version::new(2, 0, 0);
#[test]
fn test_version_compatibility() {
let v1 = Version::new(1, 2, 0);
let v2 = Version::new(1, 1, 0);
let v3 = Version::new(2, 0, 0);
assert!(v1.is_compatible_with(&v2)); // 1.2 >= 1.1
assert!(!v2.is_compatible_with(&v1)); // 1.1 < 1.2
assert!(!v1.is_compatible_with(&v3)); // Different major version
}
assert!(v1.is_compatible_with(&v2)); // 1.2 >= 1.1
assert!(!v2.is_compatible_with(&v1)); // 1.1 < 1.2
assert!(!v1.is_compatible_with(&v3)); // Different major version
}
#[test]
fn test_version_display() {
let v = Version::new(1, 2, 3);
assert_eq!(v.to_string(), "1.2.3");
}
#[test]
fn test_version_display() {
let v = Version::new(1, 2, 3);
assert_eq!(v.to_string(), "1.2.3");
}
}

View file

@ -1,186 +1,196 @@
//! WASM bridge types and helpers for plugin communication
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Memory allocation info for passing data between host and plugin
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmMemoryAlloc {
/// Pointer to allocated memory
pub ptr: u32,
/// Pointer to allocated memory
pub ptr: u32,
/// Size of allocation in bytes
pub len: u32,
/// Size of allocation in bytes
pub len: u32,
}
/// Request from host to plugin
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostRequest {
/// Request ID for matching with response
pub request_id: String,
/// Request ID for matching with response
pub request_id: String,
/// Method name being called
pub method: String,
/// Method name being called
pub method: String,
/// Serialized parameters
pub params: Vec<u8>,
/// Serialized parameters
pub params: Vec<u8>,
}
/// Response from plugin to host
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginResponse {
/// Request ID this response corresponds to
pub request_id: String,
/// Request ID this response corresponds to
pub request_id: String,
/// Success or error
pub result: WasmResult<Vec<u8>>,
/// Success or error
pub result: WasmResult<Vec<u8>>,
}
/// Result type for WASM operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WasmResult<T> {
Ok(T),
Err(String),
Ok(T),
Err(String),
}
impl<T> From<Result<T, String>> for WasmResult<T> {
fn from(r: Result<T, String>) -> Self {
match r {
Ok(v) => WasmResult::Ok(v),
Err(e) => WasmResult::Err(e),
}
fn from(r: Result<T, String>) -> Self {
match r {
Ok(v) => WasmResult::Ok(v),
Err(e) => WasmResult::Err(e),
}
}
}
/// Host functions available to plugins
pub mod host_functions {
/// Log a message from plugin
pub const LOG: &str = "host_log";
/// Log a message from plugin
pub const LOG: &str = "host_log";
/// Read a file (if permitted)
pub const READ_FILE: &str = "host_read_file";
/// Read a file (if permitted)
pub const READ_FILE: &str = "host_read_file";
/// Write a file (if permitted)
pub const WRITE_FILE: &str = "host_write_file";
/// Write a file (if permitted)
pub const WRITE_FILE: &str = "host_write_file";
/// Make an HTTP request (if permitted)
pub const HTTP_REQUEST: &str = "host_http_request";
/// Make an HTTP request (if permitted)
pub const HTTP_REQUEST: &str = "host_http_request";
/// Get configuration value
pub const GET_CONFIG: &str = "host_get_config";
/// Get configuration value
pub const GET_CONFIG: &str = "host_get_config";
/// Emit an event
pub const EMIT_EVENT: &str = "host_emit_event";
/// Emit an event
pub const EMIT_EVENT: &str = "host_emit_event";
}
/// Log level for plugin logging
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Trace,
Debug,
Info,
Warn,
Error,
}
/// Log message from plugin
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogMessage {
pub level: LogLevel,
pub target: String,
pub message: String,
pub fields: HashMap<String, String>,
pub level: LogLevel,
pub target: String,
pub message: String,
pub fields: HashMap<String, String>,
}
/// HTTP request parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpRequest {
pub method: String,
pub url: String,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
pub method: String,
pub url: String,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
/// HTTP response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
/// Helper functions for serializing/deserializing data across WASM boundary
pub mod helpers {
use super::*;
use super::*;
/// Serialize a value to bytes for passing to WASM
pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
serde_json::to_vec(value).map_err(|e| format!("Serialization error: {}", e))
}
/// Serialize a value to bytes for passing to WASM
pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
serde_json::to_vec(value).map_err(|e| format!("Serialization error: {}", e))
}
/// Deserialize bytes from WASM to a value
pub fn deserialize<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, String> {
serde_json::from_slice(bytes).map_err(|e| format!("Deserialization error: {}", e))
}
/// Deserialize bytes from WASM to a value
pub fn deserialize<T: for<'de> Deserialize<'de>>(
bytes: &[u8],
) -> Result<T, String> {
serde_json::from_slice(bytes)
.map_err(|e| format!("Deserialization error: {}", e))
}
/// Create a success response
pub fn ok_response<T: Serialize>(request_id: String, value: &T) -> Result<Vec<u8>, String> {
let result = WasmResult::Ok(serialize(value)?);
let response = PluginResponse { request_id, result };
serialize(&response)
}
/// Create a success response
pub fn ok_response<T: Serialize>(
request_id: String,
value: &T,
) -> Result<Vec<u8>, String> {
let result = WasmResult::Ok(serialize(value)?);
let response = PluginResponse { request_id, result };
serialize(&response)
}
/// Create an error response
pub fn error_response(request_id: String, error: String) -> Result<Vec<u8>, String> {
let result = WasmResult::<Vec<u8>>::Err(error);
let response = PluginResponse { request_id, result };
serialize(&response)
}
/// Create an error response
pub fn error_response(
request_id: String,
error: String,
) -> Result<Vec<u8>, String> {
let result = WasmResult::<Vec<u8>>::Err(error);
let response = PluginResponse { request_id, result };
serialize(&response)
}
}
#[cfg(test)]
mod tests {
use super::helpers::*;
use super::*;
use super::{helpers::*, *};
#[test]
fn test_serialize_deserialize() {
let data = vec![1u8, 2, 3, 4];
let bytes = serialize(&data).unwrap();
let recovered: Vec<u8> = deserialize(&bytes).unwrap();
assert_eq!(data, recovered);
#[test]
fn test_serialize_deserialize() {
let data = vec![1u8, 2, 3, 4];
let bytes = serialize(&data).unwrap();
let recovered: Vec<u8> = deserialize(&bytes).unwrap();
assert_eq!(data, recovered);
}
#[test]
fn test_ok_response() {
let request_id = "test-123".to_string();
let value = "success";
let response_bytes = ok_response(request_id.clone(), &value).unwrap();
let response: PluginResponse = deserialize(&response_bytes).unwrap();
assert_eq!(response.request_id, request_id);
match response.result {
WasmResult::Ok(data) => {
let recovered: String = deserialize(&data).unwrap();
assert_eq!(recovered, value);
},
WasmResult::Err(_) => panic!("Expected Ok result"),
}
}
#[test]
fn test_ok_response() {
let request_id = "test-123".to_string();
let value = "success";
let response_bytes = ok_response(request_id.clone(), &value).unwrap();
#[test]
fn test_error_response() {
let request_id = "test-456".to_string();
let error_msg = "Something went wrong";
let response_bytes =
error_response(request_id.clone(), error_msg.to_string()).unwrap();
let response: PluginResponse = deserialize(&response_bytes).unwrap();
assert_eq!(response.request_id, request_id);
let response: PluginResponse = deserialize(&response_bytes).unwrap();
assert_eq!(response.request_id, request_id);
match response.result {
WasmResult::Ok(data) => {
let recovered: String = deserialize(&data).unwrap();
assert_eq!(recovered, value);
}
WasmResult::Err(_) => panic!("Expected Ok result"),
}
}
#[test]
fn test_error_response() {
let request_id = "test-456".to_string();
let error_msg = "Something went wrong";
let response_bytes = error_response(request_id.clone(), error_msg.to_string()).unwrap();
let response: PluginResponse = deserialize(&response_bytes).unwrap();
assert_eq!(response.request_id, request_id);
match response.result {
WasmResult::Err(msg) => assert_eq!(msg, error_msg),
WasmResult::Ok(_) => panic!("Expected Err result"),
}
match response.result {
WasmResult::Err(msg) => assert_eq!(msg, error_msg),
WasmResult::Ok(_) => panic!("Expected Err result"),
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,67 +1,69 @@
use pinakes_plugin_api::PluginManifest;
use std::path::PathBuf;
use pinakes_plugin_api::PluginManifest;
#[test]
fn test_markdown_metadata_manifest() {
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("examples/plugins/markdown-metadata/plugin.toml");
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("examples/plugins/markdown-metadata/plugin.toml");
let manifest = PluginManifest::from_file(&manifest_path)
.expect("Failed to parse markdown-metadata plugin.toml");
let manifest = PluginManifest::from_file(&manifest_path)
.expect("Failed to parse markdown-metadata plugin.toml");
assert_eq!(manifest.plugin.name, "markdown-metadata");
assert_eq!(manifest.plugin.version, "1.0.0");
assert_eq!(manifest.plugin.api_version, "1.0");
assert_eq!(manifest.plugin.kind, vec!["metadata_extractor"]);
assert_eq!(manifest.plugin.binary.wasm, "markdown_metadata.wasm");
assert_eq!(manifest.plugin.name, "markdown-metadata");
assert_eq!(manifest.plugin.version, "1.0.0");
assert_eq!(manifest.plugin.api_version, "1.0");
assert_eq!(manifest.plugin.kind, vec!["metadata_extractor"]);
assert_eq!(manifest.plugin.binary.wasm, "markdown_metadata.wasm");
// Validate capabilities
let caps = manifest.to_capabilities();
assert_eq!(caps.filesystem.read.len(), 0);
assert_eq!(caps.filesystem.write.len(), 0);
assert!(!caps.network.enabled);
// Validate capabilities
let caps = manifest.to_capabilities();
assert_eq!(caps.filesystem.read.len(), 0);
assert_eq!(caps.filesystem.write.len(), 0);
assert!(!caps.network.enabled);
// Validate config
assert!(manifest.config.contains_key("extract_tags"));
assert!(manifest.config.contains_key("parse_yaml"));
assert!(manifest.config.contains_key("max_file_size"));
// Validate config
assert!(manifest.config.contains_key("extract_tags"));
assert!(manifest.config.contains_key("parse_yaml"));
assert!(manifest.config.contains_key("max_file_size"));
}
#[test]
fn test_heif_support_manifest() {
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("examples/plugins/heif-support/plugin.toml");
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("examples/plugins/heif-support/plugin.toml");
let manifest = PluginManifest::from_file(&manifest_path)
.expect("Failed to parse heif-support plugin.toml");
let manifest = PluginManifest::from_file(&manifest_path)
.expect("Failed to parse heif-support plugin.toml");
assert_eq!(manifest.plugin.name, "heif-support");
assert_eq!(manifest.plugin.version, "1.0.0");
assert_eq!(manifest.plugin.api_version, "1.0");
assert_eq!(
manifest.plugin.kind,
vec!["media_type", "metadata_extractor", "thumbnail_generator"]
);
assert_eq!(manifest.plugin.binary.wasm, "heif_support.wasm");
assert_eq!(manifest.plugin.name, "heif-support");
assert_eq!(manifest.plugin.version, "1.0.0");
assert_eq!(manifest.plugin.api_version, "1.0");
assert_eq!(manifest.plugin.kind, vec![
"media_type",
"metadata_extractor",
"thumbnail_generator"
]);
assert_eq!(manifest.plugin.binary.wasm, "heif_support.wasm");
// Validate capabilities
let caps = manifest.to_capabilities();
assert_eq!(caps.filesystem.read.len(), 1);
assert_eq!(caps.filesystem.write.len(), 1);
assert!(!caps.network.enabled);
assert_eq!(caps.max_memory_bytes, Some(256 * 1024 * 1024)); // 256MB
assert_eq!(caps.max_cpu_time_ms, Some(30 * 1000)); // 30 seconds
// Validate capabilities
let caps = manifest.to_capabilities();
assert_eq!(caps.filesystem.read.len(), 1);
assert_eq!(caps.filesystem.write.len(), 1);
assert!(!caps.network.enabled);
assert_eq!(caps.max_memory_bytes, Some(256 * 1024 * 1024)); // 256MB
assert_eq!(caps.max_cpu_time_ms, Some(30 * 1000)); // 30 seconds
// Validate config
assert!(manifest.config.contains_key("extract_exif"));
assert!(manifest.config.contains_key("generate_thumbnails"));
assert!(manifest.config.contains_key("thumbnail_quality"));
// Validate config
assert!(manifest.config.contains_key("extract_exif"));
assert!(manifest.config.contains_key("generate_thumbnails"));
assert!(manifest.config.contains_key("thumbnail_quality"));
}