mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 12:26:52 -06:00
fix(plugin): keep the plugin directory's wire format; refuse oversized save_file data
- PluginEntry no longer carries save_file: postcard is positional and existing messages keep their fields (docs/collab-protocol.md), so mixed app/server versions keep reading the directory. Manage plugins still shows "can save files". - save_file checks the data length before reading it and refuses with status 1 and the reason, as documented, instead of failing the run with a limit error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
38034a4c4a
commit
a370211b48
@@ -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(),
|
||||
|
||||
@@ -537,8 +537,6 @@ pub struct PluginEntry {
|
||||
pub commands: Vec<String>,
|
||||
/// 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,
|
||||
|
||||
@@ -288,7 +288,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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
|
||||
),
|
||||
|
||||
@@ -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<u8>) -> i32 {
|
||||
let max = self.limits.max_message;
|
||||
@@ -595,6 +601,11 @@ fn linker(engine: &Engine) -> Result<Linker<State>, 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)))
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
+2
-2
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user