use std::path::Path;
use std::sync::Arc;
use diaryx_core::fs::{RealFileSystem, SyncToAsyncFs};
use diaryx_core::plugin::Plugin;
use extism::{Manifest as ExtismManifest, PluginBuilder, UserData, Wasm};
use thiserror::Error;
use crate::adapter::ExtismPluginAdapter;
use crate::host_fns::{self, HostContext};
use crate::protocol::{CURRENT_PROTOCOL_VERSION, GuestManifest, MIN_SUPPORTED_PROTOCOL_VERSION};
#[derive(Debug, Error)]
pub enum ExtismLoadError {
#[error("Failed to read plugins directory: {0}")]
ReadDir(#[from] std::io::Error),
#[error("Failed to create Extism plugin '{plugin_name}': {source}")]
PluginCreate {
plugin_name: String,
source: extism::Error,
},
#[error("Failed to get manifest from plugin '{plugin_name}': {source}")]
ManifestCall {
plugin_name: String,
source: extism::Error,
},
#[error("Failed to parse manifest from plugin '{plugin_name}': {source}")]
ManifestParse {
plugin_name: String,
source: serde_json::Error,
},
#[error(
"Protocol version mismatch for plugin '{plugin_name}': \
guest has v{guest_version}, host supports v{min}..=v{max}"
)]
ProtocolMismatch {
plugin_name: String,
guest_version: u32,
min: u32,
max: u32,
},
}
fn validate_protocol_version(
manifest: &GuestManifest,
plugin_name: &str,
) -> Result<(), ExtismLoadError> {
let v = manifest.protocol_version;
if v < MIN_SUPPORTED_PROTOCOL_VERSION || v > CURRENT_PROTOCOL_VERSION {
return Err(ExtismLoadError::ProtocolMismatch {
plugin_name: plugin_name.to_string(),
guest_version: v,
min: MIN_SUPPORTED_PROTOCOL_VERSION,
max: CURRENT_PROTOCOL_VERSION,
});
}
Ok(())
}
fn parse_guest_manifest(
plugin: &mut extism::Plugin,
plugin_name: &str,
) -> Result<GuestManifest, ExtismLoadError> {
let output =
plugin
.call::<&str, &[u8]>("manifest", "")
.map_err(|e| ExtismLoadError::ManifestCall {
plugin_name: plugin_name.to_string(),
source: e,
})?;
let output_str = String::from_utf8_lossy(output);
serde_json::from_str::<GuestManifest>(&output_str).map_err(|e| ExtismLoadError::ManifestParse {
plugin_name: plugin_name.to_string(),
source: e,
})
}
pub fn inspect_plugin_wasm_manifest(wasm_path: &Path) -> Result<GuestManifest, ExtismLoadError> {
let plugin_name = wasm_path
.file_stem()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".into());
let wasm = Wasm::file(wasm_path);
let extism_manifest = ExtismManifest::new([wasm]);
let fs = Arc::new(SyncToAsyncFs::new(RealFileSystem));
let user_data = UserData::new(HostContext {
plugin_id: plugin_name.clone(),
..HostContext::with_fs(fs)
});
let builder = PluginBuilder::new(extism_manifest).with_wasi(true);
let builder = host_fns::register_host_functions(builder, user_data);
let mut plugin = builder.build().map_err(|e| ExtismLoadError::PluginCreate {
plugin_name: plugin_name.clone(),
source: e,
})?;
let manifest = parse_guest_manifest(&mut plugin, &plugin_name)?;
if let Err(e) = validate_protocol_version(&manifest, &plugin_name) {
log::warn!("{e}");
}
Ok(manifest)
}
pub fn load_plugins_from_dir(
plugins_dir: &Path,
host_context: Arc<HostContext>,
) -> Result<Vec<ExtismPluginAdapter>, ExtismLoadError> {
let mut adapters = Vec::new();
let entries = std::fs::read_dir(plugins_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let wasm_path = path.join("plugin.wasm");
if !wasm_path.exists() {
continue;
}
let plugin_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".into());
match load_single_plugin(&path, &wasm_path, &plugin_name, &host_context) {
Ok(adapter) => {
log::info!(
"Loaded extism plugin: {} ({})",
adapter.manifest().name,
adapter.manifest().id
);
adapters.push(adapter);
}
Err(e) => {
log::warn!("Failed to load plugin from {}: {e}", path.display());
}
}
}
Ok(adapters)
}
pub fn load_plugin_from_wasm(
wasm_path: &Path,
host_context: Arc<HostContext>,
config_path: Option<&Path>,
) -> Result<ExtismPluginAdapter, ExtismLoadError> {
let plugin_name = wasm_path
.file_stem()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".into());
let wasm = Wasm::file(wasm_path);
let extism_manifest = ExtismManifest::new([wasm]);
let user_data = UserData::new(HostContext {
fs: host_context.fs.clone(),
storage: host_context.storage.clone(),
secret_store: host_context.secret_store.clone(),
event_emitter: host_context.event_emitter.clone(),
plugin_id: plugin_name.clone(),
permission_checker: host_context.permission_checker.clone(),
file_provider: host_context.file_provider.clone(),
ws_bridge: host_context.ws_bridge.clone(),
plugin_command_bridge: host_context.plugin_command_bridge.clone(),
runtime_context_provider: host_context.runtime_context_provider.clone(),
});
let builder = PluginBuilder::new(extism_manifest).with_wasi(true);
let builder = host_fns::register_host_functions(builder, user_data.clone());
let mut plugin = builder.build().map_err(|e| ExtismLoadError::PluginCreate {
plugin_name: plugin_name.clone(),
source: e,
})?;
let guest_manifest = parse_guest_manifest(&mut plugin, &plugin_name)?;
validate_protocol_version(&guest_manifest, &plugin_name)?;
if let Ok(ctx) = user_data.get()
&& let Ok(mut guard) = ctx.lock()
{
guard.plugin_id = guest_manifest.id.clone();
}
let manifest_path = wasm_path
.parent()
.unwrap_or(Path::new("."))
.join("manifest.json");
cache_manifest(&manifest_path, &guest_manifest);
let cfg_path = config_path.map(|p| p.to_path_buf()).unwrap_or_else(|| {
wasm_path
.parent()
.unwrap_or(Path::new("."))
.join("config.json")
});
let config = if cfg_path.exists() {
let json = std::fs::read_to_string(&cfg_path).map_err(ExtismLoadError::ReadDir)?;
serde_json::from_str(&json).unwrap_or(serde_json::Value::Object(Default::default()))
} else {
serde_json::Value::Object(Default::default())
};
Ok(ExtismPluginAdapter::new(
plugin,
guest_manifest,
config,
cfg_path,
))
}
fn load_single_plugin(
plugin_dir: &Path,
wasm_path: &Path,
plugin_name: &str,
host_context: &Arc<HostContext>,
) -> Result<ExtismPluginAdapter, ExtismLoadError> {
let wasm = Wasm::file(wasm_path);
let extism_manifest = ExtismManifest::new([wasm]);
let user_data = UserData::new(HostContext {
fs: host_context.fs.clone(),
storage: host_context.storage.clone(),
secret_store: host_context.secret_store.clone(),
event_emitter: host_context.event_emitter.clone(),
plugin_id: plugin_name.to_string(),
permission_checker: host_context.permission_checker.clone(),
file_provider: host_context.file_provider.clone(),
ws_bridge: host_context.ws_bridge.clone(),
plugin_command_bridge: host_context.plugin_command_bridge.clone(),
runtime_context_provider: host_context.runtime_context_provider.clone(),
});
let builder = PluginBuilder::new(extism_manifest).with_wasi(true);
let builder = host_fns::register_host_functions(builder, user_data.clone());
let mut plugin = builder.build().map_err(|e| ExtismLoadError::PluginCreate {
plugin_name: plugin_name.into(),
source: e,
})?;
let manifest_path = plugin_dir.join("manifest.json");
let guest_manifest = if manifest_path.exists() {
let json = std::fs::read_to_string(&manifest_path).map_err(ExtismLoadError::ReadDir)?;
serde_json::from_str::<GuestManifest>(&json).map_err(|e| {
ExtismLoadError::ManifestParse {
plugin_name: plugin_name.into(),
source: e,
}
})?
} else {
let gm = parse_guest_manifest(&mut plugin, plugin_name)?;
cache_manifest(&manifest_path, &gm);
gm
};
validate_protocol_version(&guest_manifest, plugin_name)?;
if let Ok(ctx) = user_data.get()
&& let Ok(mut guard) = ctx.lock()
{
guard.plugin_id = guest_manifest.id.clone();
}
let config_path = plugin_dir.join("config.json");
let config = if config_path.exists() {
let json = std::fs::read_to_string(&config_path).map_err(ExtismLoadError::ReadDir)?;
serde_json::from_str(&json).unwrap_or(serde_json::Value::Object(Default::default()))
} else {
serde_json::Value::Object(Default::default())
};
Ok(ExtismPluginAdapter::new(
plugin,
guest_manifest,
config,
config_path,
))
}
fn cache_manifest(path: &Path, manifest: &GuestManifest) {
match serde_json::to_string_pretty(manifest) {
Ok(json) => {
if let Err(e) = std::fs::write(path, json) {
log::debug!("Could not cache manifest to {}: {e}", path.display());
}
}
Err(e) => {
log::debug!("Could not serialize manifest for caching: {e}");
}
}
}