use std::collections::BTreeMap;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Mutex;
use crate::cas::CasStore;
use crate::crypto::CryptoPolicy;
use crate::error::{Error, Result};
pub const ABI_VERSION: u32 = 1;
pub type CasCtor = Box<dyn Fn(&str) -> Result<Box<dyn CasStore>> + Send + Sync>;
pub trait Plugin: Send + Sync {
fn name(&self) -> &'static str;
fn abi_version(&self) -> u32 {
ABI_VERSION
}
fn register(&self, registry: &mut PluginRegistry);
}
#[derive(Default)]
pub struct PluginRegistry {
cas_schemes: BTreeMap<String, (&'static str, CasCtor)>,
}
impl PluginRegistry {
pub fn register_cas_scheme(&mut self, scheme: &str, ctor: CasCtor) {
self.cas_schemes.insert(scheme.to_string(), ("", ctor));
}
pub(crate) fn cas_ctor_for(&self, spec: &str) -> Option<(&'static str, &CasCtor)> {
let mut best: Option<(&String, &(&'static str, CasCtor))> = None;
for (scheme, entry) in &self.cas_schemes {
if spec.starts_with(scheme.as_str()) {
match best {
Some((s, _)) if s.len() >= scheme.len() => {}
_ => best = Some((scheme, entry)),
}
}
}
best.map(|(_, (name, ctor))| (*name, ctor))
}
pub fn registered_schemes(&self) -> Vec<&str> {
self.cas_schemes.keys().map(|s| s.as_str()).collect()
}
}
static REGISTRY: Mutex<Option<PluginRegistry>> = Mutex::new(None);
pub fn register_plugin(plugin: Box<dyn Plugin>) -> Result<()> {
let name = plugin.name();
if plugin.abi_version() != ABI_VERSION {
return Err(Error::PluginAbiMismatch {
plugin: name.to_string(),
plugin_version: plugin.abi_version(),
enprot_version: ABI_VERSION,
});
}
let mut guard = REGISTRY
.lock()
.map_err(|_| Error::PluginCrash(name.to_string()))?;
let registry = guard.get_or_insert_with(PluginRegistry::default);
catch_unwind(AssertUnwindSafe(|| plugin.register(registry)))
.map_err(|_| Error::PluginCrash(name.to_string()))?;
for entry in registry.cas_schemes.values_mut() {
if entry.0.is_empty() {
entry.0 = name;
}
}
Ok(())
}
pub(crate) fn open_plugin_cas(spec: &str) -> Option<Result<Box<dyn CasStore>>> {
let guard = REGISTRY.lock().ok()?;
let registry = guard.as_ref()?;
let (plugin_name, ctor) = registry.cas_ctor_for(spec)?;
match catch_unwind(AssertUnwindSafe(|| ctor(spec))) {
Ok(inner) => Some(inner),
Err(_) => Some(Err(Error::PluginCrash(plugin_name.to_string()))),
}
}
pub fn plugin_cas_dispatch_save(
plugin_name: &str,
store: &dyn CasStore,
blob: &[u8],
policy: &dyn CryptoPolicy,
) -> Result<String> {
catch_unwind(AssertUnwindSafe(|| store.save(blob, policy)))
.map_err(|_| Error::PluginCrash(plugin_name.to_string()))?
}
#[cfg(test)]
mod tests {
use super::*;
struct TestPlugin {
name: &'static str,
abi: u32,
}
impl Plugin for TestPlugin {
fn name(&self) -> &'static str {
self.name
}
fn abi_version(&self) -> u32 {
self.abi
}
fn register(&self, registry: &mut PluginRegistry) {
registry.register_cas_scheme(
"test-plugin://",
Box::new(|_spec| {
Err(Error::InvalidArg {
arg: "test",
reason: "constructor not exercised in this test".to_string(),
})
}),
);
}
}
fn with_registry<R>(f: impl FnOnce(&mut PluginRegistry) -> R) -> R {
let mut guard = REGISTRY.lock().unwrap();
let registry = guard.get_or_insert_with(PluginRegistry::default);
f(registry)
}
#[test]
fn register_and_lookup_scheme() {
let result = register_plugin(Box::new(TestPlugin {
name: "test-plugin",
abi: ABI_VERSION,
}));
assert!(result.is_ok());
with_registry(|r| {
assert!(r.registered_schemes().contains(&"test-plugin://"));
assert!(r.cas_ctor_for("test-plugin://bucket").is_some());
assert!(r.cas_ctor_for("other://x").is_none());
});
}
#[test]
fn abi_mismatch_is_refused() {
let err = register_plugin(Box::new(TestPlugin {
name: "old-plugin",
abi: ABI_VERSION + 1,
}))
.unwrap_err();
assert!(
matches!(err, Error::PluginAbiMismatch { .. }),
"got {err:?}"
);
}
#[test]
fn panicking_plugin_is_contained() {
struct PanicPlugin;
impl Plugin for PanicPlugin {
fn name(&self) -> &'static str {
"panic-plugin"
}
fn register(&self, _registry: &mut PluginRegistry) {
panic!("plugin registration exploded");
}
}
let err = register_plugin(Box::new(PanicPlugin)).unwrap_err();
assert!(matches!(err, Error::PluginCrash { .. }), "got {err:?}");
let err = plugin_cas_dispatch_save(
"panic-plugin",
&crate::cas::MemoryCas::new(),
b"x",
&*crate::crypto::default_policy(),
)
.err()
.or(None);
let _ = err; }
}