From 7940eefb04f3bdb915e53a2f2a468eefa2c6314d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 10 Nov 2026 10:49:06 -0800 Subject: [PATCH] Plug-ins: only an effect or command id naming a plug-in refuses a call A call was refused when any string in its arguments started with plugin., so a filter could not create text such as "plugin.example". Only the values of effect, effects, id (effect.apply's other name for effect), command and commands count now, at any depth. --- crates/engine/src/cmd/plugin_call.rs | 36 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/plugin_call.rs b/crates/engine/src/cmd/plugin_call.rs index 2e1cc689e..76786daeb 100644 --- a/crates/engine/src/cmd/plugin_call.rs +++ b/crates/engine/src/cmd/plugin_call.rs @@ -180,13 +180,24 @@ pub(crate) fn names_key(text: &str, key: &str) -> bool { }) } -/// Does `v` name a plug-in anywhere: a string that starts with `plugin.` (a plug-in's effect, a -/// command of one)? A call that does is refused, so a plug-in never runs another, or itself. +/// Param keys whose value names an effect or a command (`id` is `effect.apply`'s other name for +/// `effect`). +const PLUGIN_KEYS: &[&str] = &["effect", "effects", "id", "command", "commands"]; + +/// Does `v` name a plug-in anywhere: an effect or a command ([`PLUGIN_KEYS`]) whose id starts with +/// `plugin.`, at any depth? A call that does is refused, so a plug-in never runs another, or +/// itself. Other strings, such as text, may start with `plugin.`. fn names_plugin(v: &Value) -> bool { + names_plugin_in(v, false) +} + +/// [`names_plugin`] for `v`, the value of a key in [`PLUGIN_KEYS`] (or an item of one) when +/// `named`. +fn names_plugin_in(v: &Value, named: bool) -> bool { match v { - Value::String(s) => s.starts_with(vectorcraft_plugins::PREFIX), - Value::Array(a) => a.iter().any(names_plugin), - Value::Object(o) => o.values().any(names_plugin), + Value::String(s) => named && s.starts_with(vectorcraft_plugins::PREFIX), + Value::Array(a) => a.iter().any(|x| names_plugin_in(x, named)), + Value::Object(o) => o.iter().any(|(k, x)| names_plugin_in(x, PLUGIN_KEYS.contains(&k.as_str()))), _ => false, } } @@ -438,6 +449,21 @@ mod tests { assert!(stale.is_empty(), "reviewed commands that no command has or a rule refuses: {stale:?}"); } + /// Only an effect or a command naming a plug-in counts, at any depth; text that happens to + /// start with `plugin.` doesn't. + #[test] + fn a_plugin_is_named_only_as_an_effect_or_a_command() { + use serde_json::json; + assert!(names_plugin(&json!({"effect": "plugin.test.fx"}))); + assert!(names_plugin(&json!({"id": "plugin.test.fx"}))); + assert!(names_plugin(&json!({"effects": ["builtin.blur", "plugin.test.fx"]}))); + assert!(names_plugin(&json!({"items": [{"effect": "plugin.test.fx", "params": {}}]}))); + assert!(names_plugin(&json!({"command": "plugin.test.run"}))); + assert!(!names_plugin(&json!({"text": "plugin.example"}))); + assert!(!names_plugin(&json!({"name": "plugin.style", "effect": "drop-shadow"}))); + assert!(!names_plugin(&json!({"effect": {"name": "plugin.style"}}))); + } + #[test] fn keys_are_found_as_whole_keys() { assert!(names_key("{path: a .wasm file}", "path"));