diff --git a/apps/figcraft-server/src/directory.rs b/apps/figcraft-server/src/directory.rs index cdf18364..0443c0f4 100644 --- a/apps/figcraft-server/src/directory.rs +++ b/apps/figcraft-server/src/directory.rs @@ -245,7 +245,6 @@ impl Directory { }, commands: m.permissions.commands.clone(), widget: m.widget, - save_file: m.permissions.save_file, bytes: bytes.len() as u64, sha256: sha.clone(), published: now(), diff --git a/crates/collab/src/message.rs b/crates/collab/src/message.rs index 734e4076..d035042b 100644 --- a/crates/collab/src/message.rs +++ b/crates/collab/src/message.rs @@ -537,8 +537,6 @@ pub struct PluginEntry { pub commands: Vec, /// It draws a widget. pub widget: bool, - /// It may offer files to save (`permissions.saveFile`). - pub save_file: bool, /// The bundle's size in bytes and its SHA-256 (hex). pub bytes: u64, pub sha256: String, diff --git a/crates/engine/src/plugin_directory.rs b/crates/engine/src/plugin_directory.rs index 1d33178f..2f721fea 100644 --- a/crates/engine/src/plugin_directory.rs +++ b/crates/engine/src/plugin_directory.rs @@ -288,7 +288,7 @@ pub fn specs() -> Vec { spec!( "plugin.directory.list", "Browse plugin directory", - "{server? (ws://host:port/; default: the one browsed last), query?} → {server, plugins: [{id, name, version, description, author, api, access, commands, widget, save_file, bytes, sha256, published, versions, installed, update}]}", + "{server? (ws://host:port/; default: the one browsed last), query?} → {server, plugins: [{id, name, version, description, author, api, access, commands, widget, bytes, sha256, published, versions, installed, update}]}", false, list ), diff --git a/crates/plugin/src/sandbox.rs b/crates/plugin/src/sandbox.rs index 59d66118..c7337dca 100644 --- a/crates/plugin/src/sandbox.rs +++ b/crates/plugin/src/sandbox.rs @@ -195,6 +195,12 @@ struct State { } impl State { + /// Make `message` the reply and return status 1 (refused). + fn refuse(&mut self, message: String) -> i32 { + self.set_reply(message.into_bytes()); + 1 + } + /// Make `bytes` the reply; returns its length. fn set_reply(&mut self, bytes: Vec) -> i32 { let max = self.limits.max_message; @@ -595,6 +601,11 @@ fn linker(engine: &Engine) -> Result, PluginError> { charge(&mut c)?; let name = read_str(&mut c, np, nl)?; let mime = read_str(&mut c, mp, ml)?; + // Too much data is a refusal the plugin can handle, not a limit that fails the run. + let (len, max) = (dl as u32 as usize, c.data().limits.max_message); + if len > max { + return Ok(c.data_mut().refuse(format!("a file to save is limited to {max} bytes, this one has {len}"))); + } let data = read_bytes(&mut c, dp, dl)?; Err(ask(&mut c, Request::SaveFile(name, mime, data))) }) diff --git a/crates/plugin/src/save_file_tests.rs b/crates/plugin/src/save_file_tests.rs index 34241bb7..7915323f 100644 --- a/crates/plugin/src/save_file_tests.rs +++ b/crates/plugin/src/save_file_tests.rs @@ -98,12 +98,14 @@ fn a_host_without_saving_refuses_by_default() { } #[test] -fn too_much_data_is_a_limit_error_and_bad_utf8_a_trap() { - let limits = Limits { max_message: 4, ..Limits::default() }; - let wasm = module("a.txt", "text/plain", b"12345", &call("a.txt", "text/plain", b"12345")); +fn too_much_data_is_refused_and_bad_utf8_a_trap() { + let limits = Limits { max_message: 64, ..Limits::default() }; + let data = [b'x'; 65]; + let wasm = module("a.txt", "text/plain", &data, &call("a.txt", "text/plain", &data)); let mut host = Saves::default(); + // Status 1 with the reason, which the module passes on as its own failure. let err = Instance::new(&wasm, &limits).unwrap().call("{}", &mut host).unwrap_err(); - assert!(matches!(err, PluginError::Limit(_)), "{err:?}"); + assert_eq!(err, PluginError::Failed("a file to save is limited to 64 bytes, this one has 65".into())); assert!(host.0.is_empty()); // The name points at a lone 0xff byte: not UTF-8. diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index 08aa5c1a..03de53af 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -1533,7 +1533,6 @@ status = "draft — needs native review" "read and edit the document" = "das Dokument lesen und bearbeiten" "read the document" = "das Dokument lesen" "reads the document" = "liest das Dokument" -"save files you choose" = "Dateien an einem von Ihnen gewählten Ort speichern" "set-variable actions on variables that aren't in the variables JSON were skipped" = "Aktionen „Variable festlegen“ für Variablen, die nicht im Variablen-JSON sind, wurden übersprungen" "set-variable actions with values that can't be evaluated were skipped" = "Aktionen „Variable festlegen“ mit nicht auswertbaren Werten wurden übersprungen" "set-variable actions with values that don't fit the variable were skipped" = "Aktionen „Variable festlegen“ mit Werten, die nicht zur Variable passen, wurden übersprungen" diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index 58890934..8a31028e 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -1621,7 +1621,6 @@ name = "English" "read and edit the document" = "read and edit the document" "read the document" = "read the document" "reads the document" = "reads the document" -"save files you choose" = "save files you choose" "set-variable actions on variables that aren't in the variables JSON were skipped" = "set-variable actions on variables that aren't in the variables JSON were skipped" "set-variable actions with values that can't be evaluated were skipped" = "set-variable actions with values that can't be evaluated were skipped" "set-variable actions with values that don't fit the variable were skipped" = "set-variable actions with values that don't fit the variable were skipped" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index 1f92bf91..08e47492 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -1622,7 +1622,6 @@ status = "draft — needs native review" "read and edit the document" = "leer y editar el documento" "read the document" = "leer el documento" "reads the document" = "lee el documento" -"save files you choose" = "guardar archivos donde tú elijas" "set-variable actions on variables that aren't in the variables JSON were skipped" = "se omitieron las acciones «Establecer variable» sobre variables que no están en el JSON de variables" "set-variable actions with values that can't be evaluated were skipped" = "se omitieron las acciones «Establecer variable» con valores que no se pueden evaluar" "set-variable actions with values that don't fit the variable were skipped" = "se omitieron las acciones «Establecer variable» con valores que no encajan con la variable" diff --git a/crates/ui-egui/src/plugins_ui.rs b/crates/ui-egui/src/plugins_ui.rs index 919fd484..7933fa7b 100644 --- a/crates/ui-egui/src/plugins_ui.rs +++ b/crates/ui-egui/src/plugins_ui.rs @@ -477,9 +477,6 @@ fn confirm_window(app: &mut FigApp, ctx: &Context, t: &Tokens) { if e.widget { asks.push(tr("put a widget on the canvas").to_string()); } - if e.save_file { - asks.push(tr("save files you choose").to_string()); - } for a in asks { ui.add(egui::Label::new(RichText::new(format!("• {a}")).font(regular(11.0)).color(t.text)).wrap()); } diff --git a/docs/plugins.md b/docs/plugins.md index ed4b9927..a14dd7a4 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -135,7 +135,7 @@ plugins: import them in the script. | `widget.insert` | `{plugin, x?, y?, fuel?}` | Puts widget plugin `plugin`'s widget on the current page, its top-left at `x`, `y` (default: centred in the view), and selects it; the plugin draws it. One undo step. Returns `{id, plugin, state, targets, output, logs, calls, fuel}`: `targets` are the names of its clickable parts. | | `widget.click` | `{id, target, fuel?}` | Clicks part `target` of widget `id` (or of the widget a layer `id` is in), as a click on the canvas would: the plugin is called back and redraws it. One undo step; returns like `widget.insert`. | | `widget.list` | `{}` | `{widgets: [{id, plugin, name, page, state, targets}]}`: every widget in the file. | -| `plugin.directory.list` | `{server?, query?}` | The plugin directory at `server` (`ws://host:port/`; default: the one browsed last): `{server, plugins: [{id, name, version, description, author, api, access, commands, widget, save_file, bytes, sha256, published, versions, installed, update}]}`, the latest version of each plugin whose id, name, description or author contains `query`. `installed` is the installed version (or null), `update` whether the listed one is newer. | +| `plugin.directory.list` | `{server?, query?}` | The plugin directory at `server` (`ws://host:port/`; default: the one browsed last): `{server, plugins: [{id, name, version, description, author, api, access, commands, widget, bytes, sha256, published, versions, installed, update}]}`, the latest version of each plugin whose id, name, description or author contains `query`. `installed` is the installed version (or null), `update` whether the listed one is newer. | | `plugin.directory.install` | `{id, version?, confirm, server?, sha256?}` | Downloads version `version` (default: the latest) and installs it, or updates the installed one. `confirm` must be `true` (installing runs the plugin's code: show its permissions first); without it nothing is downloaded. The download must match the SHA-256 the directory lists (and `sha256`, the one shown, when given). Returns `{installed, name, version, permissions, sha256, path, source, replaced}`. | | `plugin.directory.publish` | `{path \| plugin, server}` | Publishes a plugin folder (or its `figcraft-plugin.json`), or an installed plugin, as a new version. `server` carries the server's key: `ws://host:port/?key=…`. Returns `{published, server}`. | | `plugin.directory.unpublish` | `{id, version?, server}` | Removes one version (default: every version) from the directory; needs the key. Returns `{id, removed}`. | @@ -225,7 +225,7 @@ plugin directory requests (`plugin.directory.*` says so). Import plugins with `p | `author` | Optional: who made it, as you type it (64 characters at most); plugin directories show it. | | `main` | Required. The module, a `.wasm` path relative to the manifest, inside its folder. | | `api` | The plugin API version the plugin was written for: `1` (the default), `2` (panels and settings), `3` (widgets) or `4` (saving files). A manifest asking for a newer one than FigCraft runs is refused, so a plugin that needs panels, widgets or `saveFile` fails clearly on an older FigCraft. | -| `permissions.saveFile` | API 4: `true` lets the plugin offer a file to save (an exporter's output). FigCraft asks the user where it goes with its own Save dialog (a download in the browser); the plugin never gets a path. The install confirmation lists it. | +| `permissions.saveFile` | API 4: `true` lets the plugin offer a file to save (an exporter's output). FigCraft asks the user where it goes with its own Save dialog (a download in the browser); the plugin never gets a path. Manage plugins lists it ("can save files"); a plugin directory's entries don't carry it, so their wire format stays as it is. | | `widget` | API 3: `true` when the plugin draws a widget (it's listed in Plugins ▸ Widgets). | | `permissions.access` | `read` (the default): commands that leave the document as it is; such plugins also run from Dev Mode's Inspect tab. `write`: edits too. | | `permissions.commands` | Optional allowlist: exact command ids or prefixes ending in `*` (`node.*`). Empty: any command the access allows. |