mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 17:02:03 -06:00
Merge main into fix/plugin-grant-module-hash
This commit is contained in:
+271
-34
@@ -1,20 +1,28 @@
|
||||
//! What a plug-in may import from the host ([`IMPORTS`]), from plug-in API level 2 on:
|
||||
//! `vc_progress` reports how far a run has got, `vc_cancelled` tells it the user or a host limit
|
||||
//! asked it to stop, and `vc_log` writes a log line. A module may import them only when its
|
||||
//! manifest gives `api` 2 and lists the `progress` permission, and the host checks each import when
|
||||
//! the module is loaded ([`check_imports`], [`check_granted`]): one it doesn't offer, one with
|
||||
//! another signature and one the manifest doesn't allow are refused by name.
|
||||
//! asked it to stop, and `vc_log` writes a log line (the `progress` permission); `vc_settings_*`
|
||||
//! read and write its settings (`settings`) and `vc_cache_*` its cache (`cache`, [`crate::storage`]).
|
||||
//! A module may import one only when its manifest gives `api` 2 and lists the import's permission,
|
||||
//! and the host checks each import when the module is loaded ([`check_imports`], [`check_granted`]):
|
||||
//! one it doesn't offer, one with another signature and one the manifest doesn't allow are refused
|
||||
//! by name.
|
||||
//!
|
||||
//! Each call's arguments are checked: progress is clamped and passed on only when it changes (with
|
||||
//! a known total, to another tenth of a percent; past [`MAX_PROGRESS_REPORTS`], once per
|
||||
//! [`PROGRESS_STRIDE`] changes), log text must lie inside the module's memory and be UTF-8, and
|
||||
//! each line ([`MAX_LOG_LINE`]) and each run ([`MAX_LOG_LINES`], [`MAX_LOG_BYTES`],
|
||||
//! [`MAX_LOG_READ`]) are capped. Nothing a module passes can panic the host.
|
||||
//! [`MAX_LOG_READ`]) are capped. Keys and values must lie inside the module's memory, keys must be
|
||||
//! [`crate::storage::valid_name`]s, values are capped, and so are each run's storage calls
|
||||
//! ([`MAX_STORAGE_CALLS`]) and the bytes they move ([`MAX_STORAGE_BYTES`]). Nothing a module passes
|
||||
//! can panic the host.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use wasmi::{Caller, Engine, Extern, ExternType, Linker, Module, StoreLimits, ValType};
|
||||
|
||||
use crate::manifest::Manifest;
|
||||
use crate::permissions::{self, PROGRESS_API, Permission};
|
||||
use crate::storage::{self, Meter, Storage, StorageError};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The module a plug-in imports host functions from (the name a Rust or C toolchain gives
|
||||
@@ -60,16 +68,51 @@ pub const LOG_NOT_UTF8: i32 = -2;
|
||||
/// What `vc_log` returns: the level is not 0 to 3.
|
||||
pub const LOG_BAD_LEVEL: i32 = -3;
|
||||
|
||||
/// One host import: its name and its type.
|
||||
/// What a storage import that writes or removes returns when it did.
|
||||
pub const STORAGE_DONE: i32 = 0;
|
||||
/// What a storage import returns: the key or entry has no value.
|
||||
pub const STORAGE_NOT_FOUND: i32 = -1;
|
||||
/// What a storage import returns: a key, value or buffer doesn't lie inside the module's memory.
|
||||
pub const STORAGE_OUTSIDE_MEMORY: i32 = -2;
|
||||
/// What a storage import returns: the key is not a [`crate::storage::valid_name`].
|
||||
pub const STORAGE_BAD_NAME: i32 = -3;
|
||||
/// What a storage import returns: the write would pass a cap ([`crate::storage`]); nothing changed.
|
||||
pub const STORAGE_FULL: i32 = -4;
|
||||
/// What a storage import returns: there is nowhere to keep anything (no plug-in store, as on the
|
||||
/// web, or a module that isn't installed).
|
||||
pub const STORAGE_UNAVAILABLE: i32 = -5;
|
||||
/// What a storage import returns: reading or writing the file failed.
|
||||
pub const STORAGE_FAILED: i32 = -6;
|
||||
/// What a storage import returns: the run used up its storage calls or bytes
|
||||
/// ([`MAX_STORAGE_CALLS`], [`MAX_STORAGE_BYTES`]).
|
||||
pub const STORAGE_BUDGET: i32 = -7;
|
||||
|
||||
/// Most storage calls one run makes; later ones return [`STORAGE_BUDGET`].
|
||||
pub const MAX_STORAGE_CALLS: usize = 10_000;
|
||||
|
||||
/// Most bytes one run's storage calls read and write, all together, counting the whole settings
|
||||
/// file each settings call reads and writes and each cache entry read or written; a call that would
|
||||
/// pass it returns [`STORAGE_BUDGET`].
|
||||
pub const MAX_STORAGE_BYTES: usize = 1 << 30;
|
||||
|
||||
/// One host import: its name, its type and the permission it needs.
|
||||
pub struct Import {
|
||||
pub name: &'static str,
|
||||
params: &'static [ValType],
|
||||
results: &'static [ValType],
|
||||
/// Its type as WebAssembly text writes it.
|
||||
pub signature: &'static str,
|
||||
pub permission: Permission,
|
||||
}
|
||||
|
||||
/// The host imports of API level 2, in module [`MODULE`]:
|
||||
const I32: ValType = ValType::I32;
|
||||
|
||||
/// An import that takes `params` i32 arguments and returns an i32.
|
||||
const fn storage_import(name: &'static str, params: &'static [ValType], signature: &'static str, permission: Permission) -> Import {
|
||||
Import { name, params, results: &[I32], signature, permission }
|
||||
}
|
||||
|
||||
/// The host imports of API level 2, in module [`MODULE`]. With `progress`:
|
||||
///
|
||||
/// - `vc_progress(done: i64, total: i64)`: `done` of `total` steps are finished (`total` 0 or less:
|
||||
/// not known). Negative values count as 0 and `done` is clamped to `total`;
|
||||
@@ -77,17 +120,71 @@ pub struct Import {
|
||||
/// - `vc_log(level: i32, ptr: i32, len: i32) -> i32`: writes the UTF-8 text at `ptr` as a log line
|
||||
/// of `level` (0 error, 1 warning, 2 information, 3 debug) → [`LOG_WRITTEN`], [`LOG_DROPPED`],
|
||||
/// [`LOG_OUTSIDE_MEMORY`], [`LOG_NOT_UTF8`] or [`LOG_BAD_LEVEL`].
|
||||
pub const IMPORTS: [Import; 3] = [
|
||||
Import { name: "vc_progress", params: &[ValType::I64, ValType::I64], results: &[], signature: "(param i64 i64)" },
|
||||
Import { name: "vc_cancelled", params: &[], results: &[ValType::I32], signature: "(result i32)" },
|
||||
///
|
||||
/// With `settings`, for the key of `key_len` bytes at `key`:
|
||||
///
|
||||
/// - `vc_settings_get(key, key_len, out, out_cap) -> i32`: the value's length, its bytes copied to
|
||||
/// `out` when they fit in `out_cap` (nothing is copied otherwise);
|
||||
/// - `vc_settings_set(key, key_len, value, value_len) -> i32`: sets it → [`STORAGE_DONE`];
|
||||
/// - `vc_settings_delete(key, key_len) -> i32`: removes it → [`STORAGE_DONE`].
|
||||
///
|
||||
/// With `cache`, the same for the entry named at `name`: `vc_cache_read`, `vc_cache_write` and
|
||||
/// `vc_cache_clear(name, name_len)`, which removes the entry, or every entry when `name_len` is 0.
|
||||
///
|
||||
/// A storage call that fails returns [`STORAGE_NOT_FOUND`], [`STORAGE_OUTSIDE_MEMORY`],
|
||||
/// [`STORAGE_BAD_NAME`], [`STORAGE_FULL`], [`STORAGE_UNAVAILABLE`], [`STORAGE_FAILED`] or
|
||||
/// [`STORAGE_BUDGET`].
|
||||
pub const IMPORTS: [Import; 9] = [
|
||||
Import {
|
||||
name: "vc_progress",
|
||||
params: &[ValType::I64, ValType::I64],
|
||||
results: &[],
|
||||
signature: "(param i64 i64)",
|
||||
permission: Permission::Progress,
|
||||
},
|
||||
Import { name: "vc_cancelled", params: &[], results: &[I32], signature: "(result i32)", permission: Permission::Progress },
|
||||
Import {
|
||||
name: "vc_log",
|
||||
params: &[ValType::I32, ValType::I32, ValType::I32],
|
||||
results: &[ValType::I32],
|
||||
params: &[I32, I32, I32],
|
||||
results: &[I32],
|
||||
signature: "(param i32 i32 i32) (result i32)",
|
||||
permission: Permission::Progress,
|
||||
},
|
||||
storage_import("vc_settings_get", &[I32, I32, I32, I32], "(param i32 i32 i32 i32) (result i32)", Permission::Settings),
|
||||
storage_import("vc_settings_set", &[I32, I32, I32, I32], "(param i32 i32 i32 i32) (result i32)", Permission::Settings),
|
||||
storage_import("vc_settings_delete", &[I32, I32], "(param i32 i32) (result i32)", Permission::Settings),
|
||||
storage_import("vc_cache_read", &[I32, I32, I32, I32], "(param i32 i32 i32 i32) (result i32)", Permission::Cache),
|
||||
storage_import("vc_cache_write", &[I32, I32, I32, I32], "(param i32 i32 i32 i32) (result i32)", Permission::Cache),
|
||||
storage_import("vc_cache_clear", &[I32, I32], "(param i32 i32) (result i32)", Permission::Cache),
|
||||
];
|
||||
|
||||
/// Which of a plug-in's stores a storage import reads or writes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Area {
|
||||
Settings,
|
||||
Cache,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
/// The largest value one key or entry holds.
|
||||
fn max_value(self) -> usize {
|
||||
match self {
|
||||
Area::Settings => storage::MAX_SETTING_BYTES,
|
||||
Area::Cache => storage::MAX_CACHE_ENTRY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a storage import returns for `e`.
|
||||
fn storage_code(e: &StorageError) -> i32 {
|
||||
match e {
|
||||
StorageError::Full => STORAGE_FULL,
|
||||
StorageError::Unavailable => STORAGE_UNAVAILABLE,
|
||||
StorageError::Io(_) => STORAGE_FAILED,
|
||||
StorageError::Budget => STORAGE_BUDGET,
|
||||
}
|
||||
}
|
||||
|
||||
/// A log line's level.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
@@ -189,6 +286,11 @@ pub(crate) struct Run {
|
||||
/// Bytes of log text written (with their escapes) and read.
|
||||
log_bytes: usize,
|
||||
log_read: usize,
|
||||
/// Where the plug-in keeps its settings and cache (none while the module is being loaded), and
|
||||
/// how many storage calls the run made and bytes they moved.
|
||||
storage: Option<Arc<dyn Storage>>,
|
||||
storage_calls: usize,
|
||||
storage_bytes: usize,
|
||||
pub(crate) report: Report,
|
||||
}
|
||||
|
||||
@@ -204,10 +306,111 @@ impl Run {
|
||||
skipped: 0,
|
||||
log_bytes: 0,
|
||||
log_read: 0,
|
||||
storage: None,
|
||||
storage_calls: 0,
|
||||
storage_bytes: 0,
|
||||
report: Report::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The run, with the plug-in's settings and cache kept in `storage`.
|
||||
pub(crate) fn with_storage(mut self, storage: Option<Arc<dyn Storage>>) -> Self {
|
||||
self.storage = storage;
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes one storage call with `call`, on the plug-in's storage and a [`Meter`] holding the
|
||||
/// run's storage bytes left, and counts the call and the bytes it read and wrote → the code for
|
||||
/// a run that has used up its calls or has no storage, else what `call` returns.
|
||||
fn metered<T>(&mut self, call: impl FnOnce(&dyn Storage, &mut Meter) -> std::result::Result<T, StorageError>) -> std::result::Result<T, i32> {
|
||||
if self.storage_calls >= MAX_STORAGE_CALLS {
|
||||
return Err(STORAGE_BUDGET);
|
||||
}
|
||||
self.storage_calls += 1;
|
||||
let Some(store) = self.storage.clone() else { return Err(STORAGE_UNAVAILABLE) };
|
||||
let mut meter = Meter::new(MAX_STORAGE_BYTES.saturating_sub(self.storage_bytes) as u64);
|
||||
let done = call(store.as_ref(), &mut meter);
|
||||
// What the call read before it failed counts too.
|
||||
self.storage_bytes = self.storage_bytes.saturating_add(usize::try_from(meter.used()).unwrap_or(usize::MAX));
|
||||
done.map_err(|e| storage_code(&e))
|
||||
}
|
||||
|
||||
/// The key or entry name of `len` bytes at `ptr` in `memory`, checked, or the code a storage
|
||||
/// import returns for it.
|
||||
fn storage_name(memory: &[u8], ptr: usize, len: usize) -> std::result::Result<String, i32> {
|
||||
let bytes = ptr.checked_add(len).and_then(|end| memory.get(ptr..end)).ok_or(STORAGE_OUTSIDE_MEMORY)?;
|
||||
let name = std::str::from_utf8(bytes).ok().filter(|n| storage::valid_name(n)).ok_or(STORAGE_BAD_NAME)?;
|
||||
Ok(name.to_string())
|
||||
}
|
||||
|
||||
/// `vc_settings_get` and `vc_cache_read`: copies the value of the key at `name` into the
|
||||
/// `out_cap` bytes at `out` when it fits → its length.
|
||||
fn storage_read(&mut self, area: Area, memory: &mut [u8], name: (usize, usize), out: (usize, usize)) -> i32 {
|
||||
let key = match Self::storage_name(memory, name.0, name.1) {
|
||||
Ok(key) => key,
|
||||
Err(code) => return code,
|
||||
};
|
||||
if out.1 > 0 && out.0.checked_add(out.1).is_none_or(|end| end > memory.len()) {
|
||||
return STORAGE_OUTSIDE_MEMORY;
|
||||
}
|
||||
// The storage counts what it read whether or not the value fits the buffer.
|
||||
let got = self.metered(|store, meter| match area {
|
||||
Area::Settings => store.setting(&key, meter),
|
||||
Area::Cache => store.cache_entry(&key, meter),
|
||||
});
|
||||
let value = match got {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => return STORAGE_NOT_FOUND,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let Ok(len) = i32::try_from(value.len()) else { return STORAGE_FAILED };
|
||||
if value.len() <= out.1
|
||||
&& let Some(dest) = memory.get_mut(out.0..out.0 + value.len())
|
||||
{
|
||||
dest.copy_from_slice(&value);
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
/// `vc_settings_set` and `vc_cache_write`: sets the key at `name` to the `value` bytes.
|
||||
fn storage_write(&mut self, area: Area, memory: &[u8], name: (usize, usize), value: (usize, usize)) -> i32 {
|
||||
let key = match Self::storage_name(memory, name.0, name.1) {
|
||||
Ok(key) => key,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let Some(bytes) = value.0.checked_add(value.1).and_then(|end| memory.get(value.0..end)) else { return STORAGE_OUTSIDE_MEMORY };
|
||||
if bytes.len() > area.max_value() {
|
||||
return STORAGE_FULL;
|
||||
}
|
||||
let written = self.metered(|store, meter| match area {
|
||||
Area::Settings => store.set_setting(&key, bytes, meter),
|
||||
Area::Cache => store.set_cache_entry(&key, bytes, meter),
|
||||
});
|
||||
written.map_or_else(|code| code, |()| STORAGE_DONE)
|
||||
}
|
||||
|
||||
/// `vc_settings_delete` and `vc_cache_clear`: removes the key at `name`, or with `vc_cache_clear`
|
||||
/// and a `name` of no bytes, every cache entry.
|
||||
fn storage_remove(&mut self, area: Area, memory: &[u8], name: (usize, usize)) -> i32 {
|
||||
let key = match (area, name.1) {
|
||||
(Area::Cache, 0) => None,
|
||||
_ => match Self::storage_name(memory, name.0, name.1) {
|
||||
Ok(key) => Some(key),
|
||||
Err(code) => return code,
|
||||
},
|
||||
};
|
||||
let removed = self.metered(|store, meter| match (area, key) {
|
||||
(Area::Settings, Some(key)) => store.delete_setting(&key, meter),
|
||||
(Area::Cache, Some(key)) => store.remove_cache_entry(&key),
|
||||
(_, None) => store.clear_cache().map(|_| true),
|
||||
});
|
||||
match removed {
|
||||
Ok(true) => STORAGE_DONE,
|
||||
Ok(false) => STORAGE_NOT_FOUND,
|
||||
Err(code) => code,
|
||||
}
|
||||
}
|
||||
|
||||
fn progress(&mut self, done: i64, total: i64) {
|
||||
let total = u64::try_from(total).unwrap_or(0);
|
||||
let done = u64::try_from(done).unwrap_or(0);
|
||||
@@ -286,55 +489,89 @@ pub(crate) struct Host {
|
||||
pub(crate) run: Run,
|
||||
}
|
||||
|
||||
/// A linker that offers the host imports when `granted`, and no import otherwise.
|
||||
pub(crate) fn linker(engine: &Engine, granted: bool) -> Linker<Host> {
|
||||
/// The module's memory and the store's state, for an import that reads or writes the memory.
|
||||
fn memory_and_host<'a>(caller: &'a mut Caller<'_, Host>) -> Option<(&'a mut [u8], &'a mut Host)> {
|
||||
let Some(Extern::Memory(memory)) = caller.get_export("memory") else { return None };
|
||||
Some(memory.data_and_store_mut(caller))
|
||||
}
|
||||
|
||||
/// An i32 argument as the unsigned offset or length it stands for.
|
||||
fn offset(v: i32) -> usize {
|
||||
v as u32 as usize
|
||||
}
|
||||
|
||||
/// A linker that offers the host imports whose permission is in `granted`, and no other import.
|
||||
pub(crate) fn linker(engine: &Engine, granted: &[Permission]) -> Linker<Host> {
|
||||
let mut linker = Linker::<Host>::new(engine);
|
||||
if granted {
|
||||
// Each name is defined once, so none of these fails.
|
||||
// Each name is defined once, so none of these fails.
|
||||
if granted.contains(&Permission::Progress) {
|
||||
let _ = linker.func_wrap(MODULE, "vc_progress", |mut caller: Caller<'_, Host>, done: i64, total: i64| {
|
||||
caller.data_mut().run.progress(done, total);
|
||||
});
|
||||
let _ = linker.func_wrap(MODULE, "vc_cancelled", |caller: Caller<'_, Host>| -> i32 { caller.data().run.cancelled() });
|
||||
let _ = linker.func_wrap(MODULE, "vc_log", |mut caller: Caller<'_, Host>, level: i32, ptr: i32, len: i32| -> i32 {
|
||||
let Some(Extern::Memory(memory)) = caller.get_export("memory") else { return LOG_OUTSIDE_MEMORY };
|
||||
let (data, host) = memory.data_and_store_mut(&mut caller);
|
||||
host.run.log(level, data, ptr as u32 as usize, len as u32 as usize)
|
||||
let Some((data, host)) = memory_and_host(&mut caller) else { return LOG_OUTSIDE_MEMORY };
|
||||
host.run.log(level, data, offset(ptr), offset(len))
|
||||
});
|
||||
}
|
||||
for (area, permission, get, set, remove) in [
|
||||
(Area::Settings, Permission::Settings, "vc_settings_get", "vc_settings_set", "vc_settings_delete"),
|
||||
(Area::Cache, Permission::Cache, "vc_cache_read", "vc_cache_write", "vc_cache_clear"),
|
||||
] {
|
||||
if !granted.contains(&permission) {
|
||||
continue;
|
||||
}
|
||||
let _ = linker.func_wrap(MODULE, get, move |mut caller: Caller<'_, Host>, key: i32, key_len: i32, out: i32, out_cap: i32| -> i32 {
|
||||
let Some((data, host)) = memory_and_host(&mut caller) else { return STORAGE_OUTSIDE_MEMORY };
|
||||
host.run.storage_read(area, data, (offset(key), offset(key_len)), (offset(out), offset(out_cap)))
|
||||
});
|
||||
let _ = linker.func_wrap(MODULE, set, move |mut caller: Caller<'_, Host>, key: i32, key_len: i32, value: i32, value_len: i32| -> i32 {
|
||||
let Some((data, host)) = memory_and_host(&mut caller) else { return STORAGE_OUTSIDE_MEMORY };
|
||||
host.run.storage_write(area, data, (offset(key), offset(key_len)), (offset(value), offset(value_len)))
|
||||
});
|
||||
let _ = linker.func_wrap(MODULE, remove, move |mut caller: Caller<'_, Host>, key: i32, key_len: i32| -> i32 {
|
||||
let Some((data, host)) = memory_and_host(&mut caller) else { return STORAGE_OUTSIDE_MEMORY };
|
||||
host.run.storage_remove(area, data, (offset(key), offset(key_len)))
|
||||
});
|
||||
}
|
||||
linker
|
||||
}
|
||||
|
||||
/// Checks that every import of `module` is a host import with the host's signature → their names.
|
||||
/// Any other import is refused, and the error names it.
|
||||
pub(crate) fn check_imports(module: &Module) -> Result<Vec<&'static str>> {
|
||||
let mut names = Vec::new();
|
||||
/// Checks that every import of `module` is a host import with the host's signature → them. Any
|
||||
/// other import is refused, and the error names it.
|
||||
pub(crate) fn check_imports(module: &Module) -> Result<Vec<&'static Import>> {
|
||||
let mut found = Vec::new();
|
||||
for imp in module.imports() {
|
||||
let offered = if imp.module() == MODULE { IMPORTS.iter().find(|i| i.name == imp.name()) } else { None };
|
||||
let (from, name) = (permissions::display_text(imp.module(), 64), permissions::display_text(imp.name(), 64));
|
||||
let Some(host) = offered else {
|
||||
return Err(Error::Abi(format!(
|
||||
"the module imports `{from}.{name}`, which the host doesn't offer: a plug-in may import only `env.vc_progress`, `env.vc_cancelled` and `env.vc_log` (plug-in API level 2), and no WASI, files or network"
|
||||
"the module imports `{from}.{name}`, which the host doesn't offer: a plug-in may import only the `env.vc_*` functions that docs/plugins.md lists (plug-in API level 2), and no WASI, files or network"
|
||||
)));
|
||||
};
|
||||
match imp.ty() {
|
||||
ExternType::Func(f) if f.params() == host.params && f.results() == host.results => names.push(host.name),
|
||||
ExternType::Func(f) if f.params() == host.params && f.results() == host.results => found.push(host),
|
||||
_ => {
|
||||
let signature = host.signature;
|
||||
return Err(Error::Abi(format!("the module imports `env.{name}` with another type than the host's: it is {signature}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(names)
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
/// Checks that `manifest` allows the host imports `imports`: `api` 2 and the `progress`
|
||||
/// permission. The error names the first import it doesn't allow.
|
||||
pub(crate) fn check_granted(manifest: &Manifest, imports: &[&str]) -> Result<()> {
|
||||
match imports.first() {
|
||||
Some(name) if manifest.api < PROGRESS_API || !manifest.permissions().contains(&Permission::Progress) => Err(Error::Abi(format!(
|
||||
"the module imports `env.{name}`, which a plug-in may use only at plug-in API level {PROGRESS_API} (`api` {PROGRESS_API}) and with the permission `progress`"
|
||||
))),
|
||||
_ => Ok(()),
|
||||
/// Checks that `manifest` allows the host imports `imports`: `api` 2 and each import's permission.
|
||||
/// The error names the first import it doesn't allow.
|
||||
pub(crate) fn check_granted(manifest: &Manifest, imports: &[&Import]) -> Result<()> {
|
||||
let granted = manifest.permissions();
|
||||
match imports.iter().find(|i| manifest.api < PROGRESS_API || !granted.contains(&i.permission)) {
|
||||
Some(import) => {
|
||||
let (name, permission) = (import.name, import.permission.name());
|
||||
Err(Error::Abi(format!(
|
||||
"the module imports `env.{name}`, which a plug-in may use only at plug-in API level {PROGRESS_API} (`api` {PROGRESS_API}) and with the permission `{permission}`"
|
||||
)))
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
//! A plug-in is a WebAssembly module that implements the VectorCraft plug-in ABI (v1, documented
|
||||
//! in `docs/plugins.md`). It runs in [`wasmi`], a pure-Rust interpreter, with:
|
||||
//!
|
||||
//! - **no host imports but the three of API level 2** ([`host`]: progress, Cancel and log lines,
|
||||
//! with the `progress` permission): no WASI, file system, network, clock or randomness. A module
|
||||
//! that imports anything else is rejected;
|
||||
//! - **no host imports but those of API level 2** ([`host`]: progress, Cancel and log lines with
|
||||
//! the `progress` permission, and settings and a cache of its own with the `settings` and `cache`
|
||||
//! permissions, [`storage`]): no WASI, file system, network, clock or randomness. A module that
|
||||
//! imports anything else is rejected;
|
||||
//! - an **instruction budget** (fuel) per call, metered in slices so a wall-clock deadline is
|
||||
//! also enforced (native builds), so infinite loops end with an error. A filter may ask for a
|
||||
//! larger one ([`Budget::Heavy`]), which the host lets the user allow before installing it;
|
||||
//! - a **linear-memory cap**, a **recursion-depth cap** and wasmi's strict module limits;
|
||||
//! - a **fresh instance per run**, so no state leaks between calls.
|
||||
//! - a **fresh instance per run**, so no state leaks between calls; what a plug-in keeps is only
|
||||
//! what it writes to its settings and cache.
|
||||
//!
|
||||
//! Every failure (malformed module, trap, exhausted budget, bad output) is an [`Error`]; nothing
|
||||
//! a module does can panic or hang the host.
|
||||
@@ -36,6 +38,7 @@ pub mod permissions;
|
||||
pub mod registry;
|
||||
mod runtime;
|
||||
pub mod safe_mode;
|
||||
pub mod storage;
|
||||
pub mod wat;
|
||||
|
||||
pub use dialog::ParamUi;
|
||||
|
||||
@@ -308,10 +308,11 @@ impl Manifest {
|
||||
return Err(Error::Manifest(format!("`api` is {}; this host implements plug-in API levels 1 to {API_LEVEL}", self.api)));
|
||||
}
|
||||
if let Some(names) = &self.permissions
|
||||
&& permissions::validate(self.kind, names)?.contains(&Permission::Progress)
|
||||
&& let Some(host) = permissions::validate(self.kind, names)?.into_iter().find(|p| permissions::HOST.contains(p))
|
||||
&& self.api < PROGRESS_API
|
||||
{
|
||||
return Err(Error::Manifest(format!("permission `progress` needs plug-in API level {PROGRESS_API}: give `api` {PROGRESS_API}")));
|
||||
let permission = host.name();
|
||||
return Err(Error::Manifest(format!("permission `{permission}` needs plug-in API level {PROGRESS_API}: give `api` {PROGRESS_API}")));
|
||||
}
|
||||
self.validate_extensions()?;
|
||||
match (self.budget, self.kind) {
|
||||
@@ -658,6 +659,9 @@ mod tests {
|
||||
assert_eq!(serde_json::to_value(&level2).unwrap()["api"], json!(2));
|
||||
let plain2 = Manifest::parse(br#"{"id":"a","name":"x","kind":"filter","api":2}"#).unwrap();
|
||||
assert!(!plain2.permissions().contains(&Permission::Progress), "never implied");
|
||||
let storing = Manifest::parse(br#"{"id":"a","name":"x","kind":"filter","api":2,"permissions":["cache","settings"]}"#).unwrap();
|
||||
assert_eq!(storing.permissions(), [Permission::Settings, Permission::Cache]);
|
||||
assert!(!plain2.permissions().iter().any(|p| permissions::HOST.contains(p)), "never implied");
|
||||
for (bad, says) in [
|
||||
(r#"{"id":"a","name":"x","kind":"filter","permissions":["doc.read","net.fetch"]}"#, "unknown permission `net.fetch`"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","permissions":["ui.panel"]}"#, "reserved"),
|
||||
@@ -665,6 +669,8 @@ mod tests {
|
||||
(r#"{"id":"a","name":"x","kind":"filter","permissions":"doc.read"}"#, "manifest"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","permissions":["progress"]}"#, "`progress` needs plug-in API level 2"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","api":1,"permissions":["progress"]}"#, "`progress` needs plug-in API level 2"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","permissions":["settings"]}"#, "`settings` needs plug-in API level 2"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","api":1,"permissions":["doc.read","cache"]}"#, "`cache` needs plug-in API level 2"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","api":3}"#, "`api` is 3"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","api":0}"#, "`api` is 0"),
|
||||
(r#"{"id":"a","name":"x","kind":"filter","api":-1}"#, "manifest"),
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
//! `doc.read`, `doc.write`, `state` and `progress` are the names of the plug-in capability model in
|
||||
//! storytold/photocraft#662; the other names are VectorCraft's own. A manifest without
|
||||
//! `permissions` gets the ones its kind implies ([`Permission::implied`]): what every plug-in of
|
||||
//! that kind could do before manifests listed them. `progress`, which lets a plug-in use the host
|
||||
//! imports of API level 2 ([`crate::host`]), is never implied: a manifest lists it.
|
||||
//! that kind could do before manifests listed them. `progress`, `settings` and `cache`, which let a
|
||||
//! plug-in use the host imports of API level 2 ([`crate::host`]), are never implied: a manifest
|
||||
//! lists them.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -16,8 +17,8 @@ use crate::{Error, Kind, Result};
|
||||
/// this.
|
||||
pub const API_LEVEL: u32 = 2;
|
||||
|
||||
/// The API level that adds the host imports ([`crate::host::IMPORTS`]) and the `progress`
|
||||
/// permission they need.
|
||||
/// The API level that adds the host imports ([`crate::host::IMPORTS`]) and the `progress`,
|
||||
/// `settings` and `cache` permissions they need.
|
||||
pub const PROGRESS_API: u32 = 2;
|
||||
|
||||
/// Most permissions a manifest may list.
|
||||
@@ -56,11 +57,29 @@ pub enum Permission {
|
||||
/// host imports of API level 2 ([`crate::host::IMPORTS`]).
|
||||
#[serde(rename = "progress")]
|
||||
Progress,
|
||||
/// Keep settings of its own between runs and sessions ([`crate::storage`]).
|
||||
#[serde(rename = "settings")]
|
||||
Settings,
|
||||
/// Keep a cache of its own, which the user can clear ([`crate::storage`]).
|
||||
#[serde(rename = "cache")]
|
||||
Cache,
|
||||
}
|
||||
|
||||
/// Every permission this host grants, in the order prompts list them.
|
||||
pub const ALL: [Permission; 6] =
|
||||
[Permission::DocRead, Permission::DocWrite, Permission::State, Permission::EffectRender, Permission::ExportWrite, Permission::Progress];
|
||||
pub const ALL: [Permission; 8] = [
|
||||
Permission::DocRead,
|
||||
Permission::DocWrite,
|
||||
Permission::State,
|
||||
Permission::EffectRender,
|
||||
Permission::ExportWrite,
|
||||
Permission::Progress,
|
||||
Permission::Settings,
|
||||
Permission::Cache,
|
||||
];
|
||||
|
||||
/// The permissions of the host imports, which any kind may list and none implies; each needs plug-in
|
||||
/// API level [`PROGRESS_API`].
|
||||
pub const HOST: [Permission; 3] = [Permission::Progress, Permission::Settings, Permission::Cache];
|
||||
|
||||
impl Permission {
|
||||
/// Its name in a manifest and in `plugin.install`'s `grant`.
|
||||
@@ -72,6 +91,8 @@ impl Permission {
|
||||
Permission::EffectRender => "effect.render",
|
||||
Permission::ExportWrite => "export.write",
|
||||
Permission::Progress => "progress",
|
||||
Permission::Settings => "settings",
|
||||
Permission::Cache => "cache",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,17 +110,19 @@ impl Permission {
|
||||
Permission::EffectRender => "Change how the objects its effect is applied to are drawn",
|
||||
Permission::ExportWrite => "Write the document as a file in its own format",
|
||||
Permission::Progress => "Show its progress, see Cancel and write log lines",
|
||||
Permission::Settings => "Keep settings of its own",
|
||||
Permission::Cache => "Keep a cache of its own, which you can clear",
|
||||
}
|
||||
}
|
||||
|
||||
/// May a plug-in of `kind` list it: one its kind implies, or `progress`, which any kind may
|
||||
/// list.
|
||||
/// May a plug-in of `kind` list it: one its kind implies, or one of the host imports' ([`HOST`]),
|
||||
/// which any kind may list.
|
||||
pub fn applies(self, kind: Kind) -> bool {
|
||||
self == Permission::Progress || Permission::implied(kind).contains(&self)
|
||||
HOST.contains(&self) || Permission::implied(kind).contains(&self)
|
||||
}
|
||||
|
||||
/// The permissions a manifest of `kind` without `permissions` gets. A manifest may also list
|
||||
/// `progress` ([`Permission::applies`]).
|
||||
/// the host imports' ([`HOST`], [`Permission::applies`]).
|
||||
pub fn implied(kind: Kind) -> &'static [Permission] {
|
||||
match kind {
|
||||
Kind::Filter => &[Permission::DocRead, Permission::DocWrite, Permission::State],
|
||||
@@ -247,10 +270,22 @@ mod tests {
|
||||
assert!(validate(Kind::Effect, &list(&["doc.read", "effect.render", "progress"])).is_ok());
|
||||
assert!(validate(Kind::Exporter, &list(&["progress", "export.write", "doc.read"])).is_ok());
|
||||
assert!([Kind::Filter, Kind::Effect, Kind::Exporter].iter().all(|k| !Permission::implied(*k).contains(&Permission::Progress)));
|
||||
// So may `settings` and `cache`, which no kind implies either.
|
||||
assert_eq!(
|
||||
validate(Kind::Effect, &list(&["cache", "settings", "doc.read", "effect.render"])).unwrap(),
|
||||
[Permission::DocRead, Permission::EffectRender, Permission::Settings, Permission::Cache]
|
||||
);
|
||||
assert!(validate(Kind::Exporter, &list(&["settings", "export.write", "doc.read"])).is_ok());
|
||||
assert_eq!(validate(Kind::Filter, &list(&["cache"])).unwrap(), [Permission::Cache]);
|
||||
for kind in [Kind::Filter, Kind::Effect, Kind::Exporter] {
|
||||
assert!(HOST.iter().all(|p| p.applies(kind) && !Permission::implied(kind).contains(p)));
|
||||
}
|
||||
for (kind, names, says) in [
|
||||
(Kind::Filter, &["doc.read", "clipboard"][..], "unknown permission `clipboard`"),
|
||||
(Kind::Filter, &["ui.panel"], "`ui.panel` is reserved"),
|
||||
(Kind::Filter, &["progress", "progress"], "`progress` is listed twice"),
|
||||
(Kind::Filter, &["cache", "cache"], "`cache` is listed twice"),
|
||||
(Kind::Filter, &["storage"], "unknown permission `storage`"),
|
||||
(Kind::Filter, &["export.write"], "doesn't apply to a plug-in of kind `filter`"),
|
||||
(Kind::Exporter, &["doc.read", "export.write", "doc.read"], "`doc.read` is listed twice"),
|
||||
(Kind::Effect, &["doc.read"], "needs the permission `effect.render`"),
|
||||
|
||||
@@ -10,12 +10,30 @@ use std::sync::{Arc, Mutex, RwLock};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::Error;
|
||||
use crate::permissions::Grant;
|
||||
use crate::storage::Storage;
|
||||
use crate::{Limits, Plugin, Result};
|
||||
|
||||
static PLUGINS: RwLock<BTreeMap<String, Arc<Plugin>>> = RwLock::new(BTreeMap::new());
|
||||
static REVISION: AtomicU64 = AtomicU64::new(0);
|
||||
/// What the user approved, by plug-in id.
|
||||
static GRANTS: RwLock<BTreeMap<String, Grant>> = RwLock::new(BTreeMap::new());
|
||||
/// Where each plug-in keeps its settings and cache, by plug-in id.
|
||||
static STORAGE: RwLock<BTreeMap<String, Arc<dyn Storage>>> = RwLock::new(BTreeMap::new());
|
||||
|
||||
/// Keeps the settings and cache of plug-in `id` in `storage` from now on (`None`: nowhere, so its
|
||||
/// storage calls fail as unavailable). A run of the plug-in reaches only its own id's storage.
|
||||
pub fn set_storage(id: &str, storage: Option<Arc<dyn Storage>>) {
|
||||
let mut all = STORAGE.write().unwrap_or_else(|e| e.into_inner());
|
||||
match storage {
|
||||
Some(s) => all.insert(id.to_string(), s),
|
||||
None => all.remove(id),
|
||||
};
|
||||
}
|
||||
|
||||
/// Where plug-in `id` keeps its settings and cache, if anywhere.
|
||||
pub fn storage(id: &str) -> Option<Arc<dyn Storage>> {
|
||||
STORAGE.read().unwrap_or_else(|e| e.into_inner()).get(id).cloned()
|
||||
}
|
||||
|
||||
/// Records what the user approved for a plug-in, replacing an earlier grant for its id.
|
||||
pub fn grant(g: Grant) {
|
||||
@@ -47,9 +65,12 @@ pub fn approved(plugin: &Plugin) -> bool {
|
||||
|
||||
/// Installs `plugin`, replacing one with the same id.
|
||||
pub fn install(mut plugin: Plugin) -> Arc<Plugin> {
|
||||
// The revision moves while the write lock is held, so a cache that reads the new revision and
|
||||
// then the plug-ins finds the new plug-in among them.
|
||||
let mut plugins = PLUGINS.write().unwrap_or_else(|e| e.into_inner());
|
||||
plugin.set_serial(REVISION.fetch_add(1, Ordering::Relaxed) + 1);
|
||||
let p = Arc::new(plugin);
|
||||
PLUGINS.write().unwrap_or_else(|e| e.into_inner()).insert(p.id().to_string(), p.clone());
|
||||
plugins.insert(p.id().to_string(), p.clone());
|
||||
p
|
||||
}
|
||||
|
||||
|
||||
@@ -154,8 +154,9 @@ impl Plugin {
|
||||
let hash = crate::permissions::sha256_hex(bytes);
|
||||
let mut p = Plugin { manifest: Manifest::placeholder(), engine, module, limits, size: bytes.len(), hash, source: None, serial: 0 };
|
||||
let deadline = Deadline::new(p.limits.wall_time_ms);
|
||||
// The manifest isn't read yet: every host import is linked, and what it reports is dropped.
|
||||
let mut live = p.instantiate(Run::new("", false, false, deadline), true, &mut Report::default())?;
|
||||
// The manifest isn't read yet: every host import is linked, what it reports is dropped and
|
||||
// there is no storage to reach.
|
||||
let mut live = p.instantiate(Run::new("", false, false, deadline), &crate::permissions::ALL, &mut Report::default())?;
|
||||
let version: TypedFunc<(), i32> = live.func("vc_abi_version")?;
|
||||
let v = p.call(&mut live, &version, (), p.limits.fuel_base, &deadline)?;
|
||||
if v != ABI_VERSION {
|
||||
@@ -215,9 +216,9 @@ impl Plugin {
|
||||
self.serial = serial;
|
||||
}
|
||||
|
||||
/// A new instance for `run`, with the host imports linked when `granted`. When the module's
|
||||
/// start function fails, what it reported through the host imports goes to `report`.
|
||||
fn instantiate(&self, run: Run, granted: bool, report: &mut Report) -> Result<Live> {
|
||||
/// A new instance for `run`, with the host imports of the permissions `granted` linked. When the
|
||||
/// module's start function fails, what it reported through the host imports goes to `report`.
|
||||
fn instantiate(&self, run: Run, granted: &[Permission], report: &mut Report) -> Result<Live> {
|
||||
let limits =
|
||||
StoreLimitsBuilder::new().memory_size(self.limits.max_memory_bytes).memories(1).tables(4).table_elements(100_000).instances(1).build();
|
||||
let mut store = Store::new(&self.engine, Host { limits, run });
|
||||
@@ -287,7 +288,8 @@ impl Plugin {
|
||||
}
|
||||
|
||||
/// [`Plugin::run_limited`], and what the run reported through the host imports: its last
|
||||
/// progress report and its log lines, which also go to the app's log under its id. Inside a
|
||||
/// progress report and its log lines, which also go to the app's log under its id. Its storage
|
||||
/// imports reach the storage of its id ([`crate::registry::storage`]). Inside a
|
||||
/// [`vectorcraft_doc::cancel::scope`] its progress goes to the scope's sink (a live effect's
|
||||
/// doesn't) and `vc_cancelled` sees the scope's Cancel.
|
||||
pub fn run_reporting(&self, input: &[u8], params: &[u8], limits: &Limits) -> (Result<Vec<u8>>, Report) {
|
||||
@@ -306,8 +308,8 @@ impl Plugin {
|
||||
total.ok_or_else(|| Error::Limit("memory budget (the selection is too large to hand over)".into()))?;
|
||||
let deadline = Deadline::new(limits.wall_time_ms);
|
||||
let m = &self.manifest;
|
||||
let run = Run::new(&m.id, m.kind != Kind::Effect, true, deadline);
|
||||
let mut live = self.instantiate(run, m.permissions().contains(&Permission::Progress), report)?;
|
||||
let run = Run::new(&m.id, m.kind != Kind::Effect, true, deadline).with_storage(crate::registry::storage(&m.id));
|
||||
let mut live = self.instantiate(run, &m.permissions(), report)?;
|
||||
let out = self.run_live(&mut live, input, params, limits, &deadline);
|
||||
*report = std::mem::take(&mut live.store.data_mut().run.report);
|
||||
out
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! What a plug-in keeps between runs, from plug-in API level 2 on: **settings**, a few named values
|
||||
//! (the `settings` permission), and a **cache** of named entries the user can clear (the `cache`
|
||||
//! permission). A plug-in reads and writes them through the host imports ([`crate::host::IMPORTS`]),
|
||||
//! naming each by a key ([`valid_name`]), never by a path; the host keeps them per plug-in id
|
||||
//! ([`Storage`], given with [`crate::registry::set_storage`]), so one plug-in can't reach another's.
|
||||
//!
|
||||
//! The caps are VectorCraft's own values: [`MAX_SETTINGS`] keys of at most [`MAX_SETTING_BYTES`]
|
||||
//! each in a settings file of at most [`MAX_SETTINGS_BYTES`], and cache entries of at most
|
||||
//! [`MAX_CACHE_ENTRY`] bytes, [`MAX_CACHE_BYTES`] and [`MAX_CACHE_ENTRIES`] in all. A write that
|
||||
//! would pass one fails with [`StorageError::Full`] and changes nothing.
|
||||
|
||||
/// Longest key or cache entry name, in bytes.
|
||||
pub const MAX_NAME: usize = 64;
|
||||
|
||||
/// Most settings a plug-in keeps.
|
||||
pub const MAX_SETTINGS: usize = 256;
|
||||
|
||||
/// Largest value of one setting, in bytes.
|
||||
pub const MAX_SETTING_BYTES: usize = 64 << 10;
|
||||
|
||||
/// Largest settings file, in bytes: the keys and the values in base64, as JSON.
|
||||
pub const MAX_SETTINGS_BYTES: usize = 1 << 20;
|
||||
|
||||
/// Largest cache entry, in bytes.
|
||||
pub const MAX_CACHE_ENTRY: usize = 64 << 20;
|
||||
|
||||
/// Most bytes a plug-in's cache holds, all entries together.
|
||||
pub const MAX_CACHE_BYTES: u64 = 512 << 20;
|
||||
|
||||
/// Most entries a plug-in's cache holds.
|
||||
pub const MAX_CACHE_ENTRIES: usize = 4096;
|
||||
|
||||
/// Why a storage call failed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum StorageError {
|
||||
/// The write would pass a cap; nothing was changed.
|
||||
Full,
|
||||
/// There is nowhere to keep anything: no plug-in store is attached, as in the web build.
|
||||
Unavailable,
|
||||
/// Reading or writing the file failed, with the system's reason.
|
||||
Io(String),
|
||||
/// The call would read or write more bytes than its [`Meter`] has left; nothing was changed.
|
||||
Budget,
|
||||
}
|
||||
|
||||
/// The bytes a storage call may still read and write, and those it has. The storage charges each
|
||||
/// file it reads or writes, at its size, before it does.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Meter {
|
||||
left: u64,
|
||||
used: u64,
|
||||
}
|
||||
|
||||
impl Meter {
|
||||
/// A meter that lets a call read and write `left` bytes.
|
||||
pub fn new(left: u64) -> Self {
|
||||
Meter { left, used: 0 }
|
||||
}
|
||||
|
||||
/// A meter with no limit, for callers that don't count.
|
||||
pub fn unlimited() -> Self {
|
||||
Meter::new(u64::MAX)
|
||||
}
|
||||
|
||||
/// Counts `bytes` about to be read or written → [`StorageError::Budget`], counting nothing,
|
||||
/// when that is more than is left.
|
||||
pub fn take(&mut self, bytes: u64) -> Result<(), StorageError> {
|
||||
if bytes > self.left {
|
||||
return Err(StorageError::Budget);
|
||||
}
|
||||
self.left -= bytes;
|
||||
self.used = self.used.saturating_add(bytes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The bytes counted so far.
|
||||
pub fn used(&self) -> u64 {
|
||||
self.used
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StorageError::Full => write!(f, "the plug-in's storage is full"),
|
||||
StorageError::Unavailable => write!(f, "no plug-in storage is available"),
|
||||
StorageError::Io(why) => write!(f, "{why}"),
|
||||
StorageError::Budget => write!(f, "the run has used up its storage bytes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where one plug-in's settings and cache are kept. Every key and name the host passes is a
|
||||
/// [`valid_name`], and every value is within its cap ([`MAX_SETTING_BYTES`], [`MAX_CACHE_ENTRY`]);
|
||||
/// the storage enforces the totals. The calls that read or write a file charge `meter` the bytes
|
||||
/// they read and write ([`Meter::take`]) before they do: a settings call the whole settings file it
|
||||
/// reads and the whole file it writes, a cache call the entry it reads or writes.
|
||||
pub trait Storage: Send + Sync {
|
||||
/// The value of setting `key`, if it has one.
|
||||
fn setting(&self, key: &str, meter: &mut Meter) -> Result<Option<Vec<u8>>, StorageError>;
|
||||
/// Sets `key` to `value`.
|
||||
fn set_setting(&self, key: &str, value: &[u8], meter: &mut Meter) -> Result<(), StorageError>;
|
||||
/// Removes setting `key` → whether it had a value.
|
||||
fn delete_setting(&self, key: &str, meter: &mut Meter) -> Result<bool, StorageError>;
|
||||
/// The cache entry `name`, if there is one.
|
||||
fn cache_entry(&self, name: &str, meter: &mut Meter) -> Result<Option<Vec<u8>>, StorageError>;
|
||||
/// Writes the cache entry `name`.
|
||||
fn set_cache_entry(&self, name: &str, value: &[u8], meter: &mut Meter) -> Result<(), StorageError>;
|
||||
/// Removes the cache entry `name` → whether there was one.
|
||||
fn remove_cache_entry(&self, name: &str) -> Result<bool, StorageError>;
|
||||
/// Removes every cache entry → the bytes they held.
|
||||
fn clear_cache(&self) -> Result<u64, StorageError>;
|
||||
/// The bytes the cache holds now.
|
||||
fn cache_bytes(&self) -> u64;
|
||||
}
|
||||
|
||||
/// Is `name` a key or cache entry name a plug-in may use: 1 to [`MAX_NAME`] characters from
|
||||
/// `A-Z a-z 0-9 _ -`, and not a name Windows keeps for a device (`CON`, `PRN`, `AUX`, `NUL`,
|
||||
/// `COM0` to `COM9`, `LPT0` to `LPT9`, in any case)? Keys differ in case: `a` and `A` are two.
|
||||
pub fn valid_name(name: &str) -> bool {
|
||||
(1..=MAX_NAME).contains(&name.len()) && name.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-')) && !device_name(name)
|
||||
}
|
||||
|
||||
/// Is `name` one of the device names Windows keeps?
|
||||
fn device_name(name: &str) -> bool {
|
||||
let upper = name.to_ascii_uppercase();
|
||||
match upper.as_bytes() {
|
||||
[a, b, c] => matches!(&[*a, *b, *c], b"CON" | b"PRN" | b"AUX" | b"NUL"),
|
||||
[a, b, c, d] => matches!(&[*a, *b, *c], b"COM" | b"LPT") && d.is_ascii_digit(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn names_are_short_plain_and_never_a_device() {
|
||||
for ok in ["a", "Z", "zoom-level", "tile_12", &"x".repeat(MAX_NAME), "CONSOLE", "COM10", "LPTX", "con1"] {
|
||||
assert!(valid_name(ok), "{ok}");
|
||||
}
|
||||
for bad in ["", "..", ".", "a/b", "a\\b", "a.b", "a b", "a\u{0}", "\u{7}", "é", &"x".repeat(MAX_NAME + 1), "CON", "con", "Nul", "aux", "prn"]
|
||||
{
|
||||
assert!(!valid_name(bad), "{bad:?}");
|
||||
}
|
||||
for n in 0..10 {
|
||||
assert!(!valid_name(&format!("COM{n}")) && !valid_name(&format!("lpt{n}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,16 @@ pub const HOST_IMPORTS: &str = r#"(import "env" "vc_progress" (func $vc_progress
|
||||
(import "env" "vc_cancelled" (func $vc_cancelled (result i32)))
|
||||
(import "env" "vc_log" (func $vc_log (param i32 i32 i32) (result i32)))"#;
|
||||
|
||||
/// The storage imports of plug-in API level 2 ([`crate::host::IMPORTS`]), as `$vc_settings_get`,
|
||||
/// `$vc_settings_set`, `$vc_settings_delete`, `$vc_cache_read`, `$vc_cache_write` and
|
||||
/// `$vc_cache_clear`; a manifest that uses them lists `settings` and `cache`.
|
||||
pub const STORAGE_IMPORTS: &str = r#"(import "env" "vc_settings_get" (func $vc_settings_get (param i32 i32 i32 i32) (result i32)))
|
||||
(import "env" "vc_settings_set" (func $vc_settings_set (param i32 i32 i32 i32) (result i32)))
|
||||
(import "env" "vc_settings_delete" (func $vc_settings_delete (param i32 i32) (result i32)))
|
||||
(import "env" "vc_cache_read" (func $vc_cache_read (param i32 i32 i32 i32) (result i32)))
|
||||
(import "env" "vc_cache_write" (func $vc_cache_write (param i32 i32 i32 i32) (result i32)))
|
||||
(import "env" "vc_cache_clear" (func $vc_cache_clear (param i32 i32) (result i32)))"#;
|
||||
|
||||
/// A plug-in of API level 2 (its manifest gives `api` 2 and lists `progress`) that imports the
|
||||
/// host functions ([`HOST_IMPORTS`]) and whose `vc_run` runs `first`, which may call them, then
|
||||
/// returns `output`.
|
||||
|
||||
Reference in New Issue
Block a user