mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-29 12:27:38 -06:00
* photocraft-plugins: sandboxed WebAssembly filter plug-ins (wasmi host, ABI v1) A new L4 crate runs plug-ins in wasmi (pure Rust, MIT/Apache) with no host imports, a fuel budget metered in slices with a wall-clock deadline, a memory cap, a recursion cap and wasmi's strict module limits. Every band gets a fresh instance; traps, budgets, bad manifests and bad pointers are all Errors. Pixels go over as interleaved normalised f32 in tile-aligned bands, in parallel on native, blended by the selection. Includes the example Invert plug-in (no_std Rust, examples/plugins/invert-rs) built into a test fixture, hostile-module tests, and docs/plugins.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Engine: plugin.* commands (install, list, run, remove, reload) plugin.run filters the active pixel layer, a targeted channel or a smart object (recorded as a smart filter) as one undo step; Preferences > Plug-ins gains Use Additional Plug-ins Folder, loaded when prefs apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI: Filter > Plug-ins lists installed plug-ins with generated dialogs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Plug-ins: bands never outgrow the memory cap; regenerate parity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bench: example plug-in Invert vs built-in Invert on 24 MP Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.8 KiB
Rust
102 lines
3.8 KiB
Rust
//! The process-wide set of installed plug-ins.
|
|
//!
|
|
//! Plug-ins are installed per process (as in Photoshop), not per document: smart filters and
|
|
//! filter previews (which run on scratch sessions) must find the same plug-ins.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, RwLock};
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use crate::Error;
|
|
use crate::{Limits, Plugin, Result};
|
|
|
|
static PLUGINS: RwLock<BTreeMap<String, Arc<Plugin>>> = RwLock::new(BTreeMap::new());
|
|
static REVISION: AtomicU64 = AtomicU64::new(0);
|
|
|
|
fn bump() {
|
|
REVISION.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Installs `plugin`, replacing one with the same id.
|
|
pub fn install(plugin: Plugin) -> Arc<Plugin> {
|
|
let p = Arc::new(plugin);
|
|
PLUGINS.write().unwrap_or_else(|e| e.into_inner()).insert(p.id().to_string(), p.clone());
|
|
bump();
|
|
p
|
|
}
|
|
|
|
/// Loads `bytes` with the default limits and installs the plug-in.
|
|
pub fn install_bytes(bytes: &[u8]) -> Result<Arc<Plugin>> {
|
|
Ok(install(Plugin::load(bytes, Limits::default())?))
|
|
}
|
|
|
|
pub fn get(id: &str) -> Option<Arc<Plugin>> {
|
|
PLUGINS.read().unwrap_or_else(|e| e.into_inner()).get(id).cloned()
|
|
}
|
|
|
|
/// Installed plug-ins, sorted by id.
|
|
pub fn list() -> Vec<Arc<Plugin>> {
|
|
PLUGINS.read().unwrap_or_else(|e| e.into_inner()).values().cloned().collect()
|
|
}
|
|
|
|
/// Uninstalls `id`; false if it wasn't installed.
|
|
pub fn remove(id: &str) -> bool {
|
|
let gone = PLUGINS.write().unwrap_or_else(|e| e.into_inner()).remove(id).is_some();
|
|
if gone {
|
|
bump();
|
|
}
|
|
gone
|
|
}
|
|
|
|
/// Changes whenever a plug-in is installed or removed (for caches such as menus).
|
|
pub fn revision() -> u64 {
|
|
REVISION.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Reads a `.wasm` file (refusing anything that isn't a regular file or exceeds the module limit)
|
|
/// and loads it.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn load_file(path: &std::path::Path, limits: Limits) -> Result<Plugin> {
|
|
use std::io::Read;
|
|
let meta = std::fs::metadata(path).map_err(|e| Error::Io(format!("{}: {e}", path.display())))?;
|
|
if !meta.is_file() {
|
|
return Err(Error::Io(format!("{} is not a file", path.display())));
|
|
}
|
|
if meta.len() > limits.max_module_bytes as u64 {
|
|
return Err(Error::Module(format!("{} is {} bytes (limit {})", path.display(), meta.len(), limits.max_module_bytes)));
|
|
}
|
|
let f = std::fs::File::open(path).map_err(|e| Error::Io(format!("{}: {e}", path.display())))?;
|
|
let mut bytes = Vec::new();
|
|
f.take(limits.max_module_bytes as u64 + 1).read_to_end(&mut bytes).map_err(|e| Error::Io(format!("{}: {e}", path.display())))?;
|
|
Ok(Plugin::load(&bytes, limits)?.with_source(path.display().to_string()))
|
|
}
|
|
|
|
/// What [`load_folder`] did.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct FolderReport {
|
|
/// Ids of the plug-ins installed.
|
|
pub loaded: Vec<String>,
|
|
/// `(file, error)` for each `.wasm` that failed to load.
|
|
pub failed: Vec<(String, String)>,
|
|
}
|
|
|
|
/// Installs every `*.wasm` directly in `dir` (not recursive), in file-name order. A bad module is
|
|
/// reported and skipped; the others still load.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn load_folder(dir: &std::path::Path) -> Result<FolderReport> {
|
|
let rd = std::fs::read_dir(dir).map_err(|e| Error::Io(format!("{}: {e}", dir.display())))?;
|
|
let mut files: Vec<std::path::PathBuf> =
|
|
rd.filter_map(|e| e.ok().map(|e| e.path())).filter(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("wasm"))).collect();
|
|
files.sort();
|
|
files.truncate(1000);
|
|
let mut report = FolderReport::default();
|
|
for f in files {
|
|
match load_file(&f, Limits::default()) {
|
|
Ok(p) => report.loaded.push(install(p).id().to_string()),
|
|
Err(e) => report.failed.push((f.display().to_string(), e.to_string())),
|
|
}
|
|
}
|
|
Ok(report)
|
|
}
|