diff options
| author | user <user@clank> | 2026-08-13 20:08:50 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-13 20:08:50 +0200 |
| commit | fb7076efd6d3ab5c1d36b6e554045d87b43c15b6 (patch) | |
| tree | 5ef521792cd35974cd83814dba9e68edadedcc30 | |
| parent | check: stop the live check leaking a server per run (diff) | |
| download | sl0ppi-fb7076efd6d3ab5c1d36b6e554045d87b43c15b6.tar.gz sl0ppi-fb7076efd6d3ab5c1d36b6e554045d87b43c15b6.tar.xz sl0ppi-fb7076efd6d3ab5c1d36b6e554045d87b43c15b6.zip | |
sl0p: the plugins we ship do not ask for permission
pane-finder requests three permissions as it loads, so the first press of a
keybinding we ship opened a consent screen instead of a fuzzy finder. Prompting
is right for a wasm you found on the internet and wrong for the ones that come
in our own tarball.
plugin_is_shipped_with_sl0ppi (zellij-utils/src/input/permission.rs) grants
those implicitly, through the same instruction a human "y" produces, so the
plugin's permissions are set and recorded exactly as before. "Ours" is narrow
on purpose:
- builtins, asked via the existing PluginConfig::is_builtin -- the same
predicate check_command_permission already uses to exempt them, so the two
cannot drift;
- files inside a plugin directory we install into: the config dir's plugins/
and the data dir's plugins/;
- nothing else. Remote plugins are never trusted, paths are normalised
lexically first so plugins/../../evil.wasm is not "inside" the dir, and the
directories are derived from the environment and default locations -- so
getting them wrong costs a prompt and can never grant one elsewhere.
The trade, documented in config/README.md: <config dir>/plugins/ is now a
trusted directory. Anything able to write there can already bind arbitrary
commands to a key in config.kdl, so a consent screen for the wasm beside it was
not buying anything -- but it does mean plugins/ is not a download folder.
packaging/checks/plugin-permissions-live.py drives both halves on a real
session: the shipped plugin must run without the prompt, and the SAME wasm
loaded from outside the plugin dir must still ask. Confirmed by disabling the
rule and watching the first half fail with the exact "asks permission to ...
Allow? (y/n)" screen it was written against.
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | config/README.md | 19 | ||||
| -rw-r--r-- | docs/HANDOVER.md | 19 | ||||
| -rw-r--r-- | docs/roadmap.md | 36 | ||||
| -rwxr-xr-x | packaging/checks/plugin-permissions-live.py | 199 | ||||
| -rw-r--r-- | sl0ppij/zellij-server/src/plugins/zellij_exports.rs | 14 | ||||
| -rw-r--r-- | sl0ppij/zellij-utils/src/input/permission.rs | 209 |
7 files changed, 492 insertions, 7 deletions
@@ -99,11 +99,12 @@ test: cli-test ## All tests: the CLI, plus the crates we actually modify cd $(FORK) && cargo test -p zellij-server --lib && cargo test -p zellij-utils --lib .PHONY: check-live -check-live: dev ## Drive a real session and check the mouse affordances are drawn where they are clicked +check-live: dev ## Drive real sessions and check what the UI actually does @# twice: once with just the frame settings, once with the real config, whose @# stacks and plugins are what the minimal one kept missing packaging/checks/split-button-live.py $(FORK)/target/fastdev/$(BIN_NAME) packaging/checks/split-button-live.py $(FORK)/target/fastdev/$(BIN_NAME) config/config.kdl + packaging/checks/plugin-permissions-live.py $(FORK)/target/fastdev/$(BIN_NAME) ##@ Package diff --git a/config/README.md b/config/README.md index 64e3b2d..f5a87b4 100644 --- a/config/README.md +++ b/config/README.md @@ -19,6 +19,25 @@ versioned by hand-rolled `config.kdl.pre-<thing>` backups. Edit **here**, then `make install-config` to sync into your live config dir. Do not edit the live dotfiles directly any more — they are now a build output. +## `plugins/` is a trusted directory + +A wasm plugin in `<config dir>/plugins/` (or the data dir's `plugins/`) gets the +permissions it asks for **without the consent prompt** — that is how the shipped +`pane-finder.wasm` and `zjstatus.wasm` start silently on a fresh machine +instead of opening with a three-item "Allow? (y/n)" screen the first time you +press their keybinding. + +The boundary is that directory, nothing wider: the same wasm loaded from +anywhere else still prompts, and remote (`http(s):`) plugins are never trusted. +The reasoning is that anything able to write into your config dir can already +bind arbitrary commands to a key in `config.kdl`, so a consent screen for the +wasm sitting next to it protects nothing. **Do not use `plugins/` as a download +folder** — put a plugin you are evaluating anywhere else, and it will ask. + +Rule and rationale: `plugin_is_shipped_with_sl0ppi` in +`zellij-utils/src/input/permission.rs`. Checked end to end, both halves, by +`packaging/checks/plugin-permissions-live.py`. + ## Rename status Repointed at sl0ppij's own dirs now that the rename has landed (docs/rename.md): diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 7590af8..b1dc84e 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -91,6 +91,19 @@ Not drawn on floating panes on purpose: the PIN control owns that corner and is hit-tested by its drawn position, so anything to its right moves it out from under the pointer (`test_ctrl_click_on_floating_pin_button_toggles_pin`). +## Also done: shipped plugins no longer ask for permission + +pane-finder opened a three-item consent screen the first time you pressed its +keybinding. Plugins we ship are now granted implicitly -- +`plugin_is_shipped_with_sl0ppi` in `zellij-utils/src/input/permission.rs`, +checked by `packaging/checks/plugin-permissions-live.py`. + +The part to keep in mind: **`<config dir>/plugins/` is now a trusted +directory** (documented in `config/README.md`). The same wasm from anywhere else +still prompts, and remote plugins are never trusted. If you ever need the +opposite for a specific plugin, the honest fix is to load it from outside that +directory rather than to widen the rule. + ## Where the project is Everything is committed and pushed to `ssh://git@git.sl0p.foo:39327/srv/git/sl0ppi.git`. @@ -169,7 +182,11 @@ bar. See `docs/roadmap.md` for each with its measurements. is inset from the allocation: use `Pane::frame_rect()` for anything the user can see, `position_and_size()` only for the layout solver. - **Clean up test sessions.** A background-created session I left behind is what - produced a "MY FOCUS" bug report from the user. + produced a "MY FOCUS" bug report from the user. The live checks now reap their + own servers; anything else you spawn by hand, kill by pid via `comm`. +- **`send_cli_scroll_up_action` flakes** (scrolled 1 line where the snapshot has + 2), seen once in ~10 full runs. It is the CLI-action raciness already tracked + above, not a rendering bug -- re-run before chasing it. ## Access diff --git a/docs/roadmap.md b/docs/roadmap.md index 9afed17..ef71070 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -257,6 +257,42 @@ If it ever disagrees with the code, do not theorise about the screen: put a `/tmp/sl0ppij-$UID/sl0ppij-log/`. The server's own geometry settles in one run what screen-scraping cannot. +## Fixed: the shipped plugins asked for permission on first launch + +pane-finder requests three permissions as it loads, so the first press of its +keybinding on any new machine opened a consent screen instead of a fuzzy finder +-- a nag about our own software, on a binding we ship. + +Plugins we ship are now granted implicitly: builtins (via the existing +`PluginConfig::is_builtin`, the same predicate `check_command_permission` +already uses) and any wasm inside a plugin directory we install into -- the +config dir's `plugins/` and the data dir's `plugins/`. The rule is +`plugin_is_shipped_with_sl0ppi` in `zellij-utils/src/input/permission.rs`, and +it is deliberately narrow: + +- the same wasm from any other path still prompts; +- remote plugins are never trusted; +- paths are normalised lexically first, so `plugins/../../evil.wasm` is not + "inside" the plugin dir; +- the trusted directories are derived from the environment and the default + locations, so getting them wrong costs a prompt and can never grant one + somewhere else. + +The trade is that `<config dir>/plugins/` is now a trusted directory, documented +in `config/README.md`: anything that can write there can already put arbitrary +commands on a keybinding in `config.kdl`. + +**How to re-run the check**: + +```bash +make check-live # includes packaging/checks/plugin-permissions-live.py +``` + +It launches the real pane-finder wasm from a temp config dir and asserts it +runs without the prompt, then launches the *same file* from outside that dir and +asserts it still asks. Confirmed by disabling the rule and watching the first +half fail with the exact "asks permission to ... Allow? (y/n)" screen. + ## Standing risks **`sl0ppij/target` is a symlink to `~/lol/zellij/target`.** Introduced when the diff --git a/packaging/checks/plugin-permissions-live.py b/packaging/checks/plugin-permissions-live.py new file mode 100755 index 0000000..7241504 --- /dev/null +++ b/packaging/checks/plugin-permissions-live.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""Live check: do the plugins sl0ppi ships start without a permission nag, and +does that trust stop at our own plugin directory? + +Both halves matter. The first is the feature: pane-finder asks for three +permissions the moment it loads, and answering that prompt on first launch (per +machine, per session cache) is not something a shipped keybinding should make +you do. The second is the limit: "we trust the plugins we ship" must not decay +into "we trust any wasm anyone drops on this box", so the same plugin loaded +from outside the plugin dir must still ask. + +Usage: plugin-permissions-live.py [path-to-sl0ppij] [path-to-pane-finder.wasm] +""" + +import fcntl +import os +import pty +import re +import shutil +import struct +import subprocess +import sys +import tempfile +import termios +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +BIN = os.path.abspath( + sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "../../sl0ppij/target/fastdev/sl0ppij") +) +WASM = os.path.abspath( + sys.argv[2] + if len(sys.argv) > 2 + else os.path.join(HERE, "../../sl0ppij/target/wasm32-wasip1/release/zj-pane-finder.wasm") +) +ROWS, COLS = 30, 100 +SESSION = f"sl0ptest-perms-{os.getpid()}" + +CONFIG = """ +pane_frames true +pane_frame_style "full" +default_mode "locked" +show_startup_tips false +""" + +# what the permission prompt says (plugin_pane.rs: display_request_permission_message) +NAG = ("asks permission to", "This plugin asks permission to", "Allow?") +# what the pane finder draws once it is actually running +RUNNING = "type to filter panes" + + +def screen_text(raw): + """Printable text of the stream, with escape sequences stripped. + + Coarser than the split-button check's grid, and that is on purpose: this one + asks "does this string appear", not "at which column", so a torn repaint can + only ever hide a nag we then fail to find -- and we assert the plugin's own + output too, which would go missing at the same time. + """ + text = re.sub(r"\x1b\][^\x07\x1b]*(\x07|\x1b\\)", "", raw) + text = re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]", "", text) + text = re.sub(r"\x1b[()][0-9A-B]", "", text) + return text.replace("\r", "\n") + + +def read_until(fd, predicate, seconds=25): + """Accumulate output until `predicate` is happy or the deadline passes.""" + buffered = "" + deadline = time.time() + seconds + while time.time() < deadline: + time.sleep(0.2) + try: + buffered += os.read(fd, 1 << 20).decode("utf8", "replace") + except BlockingIOError: + pass + except OSError: + break + if predicate(screen_text(buffered)): + break + return screen_text(buffered) + + +def main(): + if not os.path.isfile(WASM): + print(f"FAIL: no pane-finder wasm at {WASM} (build it with `make plugins`)") + return 1 + + tmp = tempfile.mkdtemp(prefix="sl0p-perms-") + config_dir = os.path.join(tmp, "config") + shipped = os.path.join(config_dir, "plugins") # where the installer puts ours + elsewhere = os.path.join(tmp, "elsewhere") # a plugin we do NOT ship + os.makedirs(shipped) + os.makedirs(elsewhere) + config_file = os.path.join(config_dir, "config.kdl") + with open(config_file, "w") as f: + f.write(CONFIG) + shutil.copy(WASM, os.path.join(shipped, "pane-finder.wasm")) + shutil.copy(WASM, os.path.join(elsewhere, "pane-finder.wasm")) + + env = dict(os.environ) + env.update( + { + "SL0PPIJ_SOCKET_DIR": os.path.join(tmp, "sock"), + "SL0PPIJ_CONFIG_FILE": config_file, + "SL0PPIJ_CONFIG_DIR": config_dir, + "SL0PPIJ_CACHE_DIR": os.path.join(tmp, "cache"), + "XDG_CACHE_HOME": os.path.join(tmp, "cache"), # the permission cache lives here + "TERM": "xterm-256color", + "SHELL": "/bin/sh", + } + ) + + pid, fd = pty.fork() + if pid == 0: + os.execvpe(BIN, [BIN, "-s", SESSION], env) + fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0)) + os.set_blocking(fd, False) + + failures = [] + try: + read_until(fd, lambda text: "$" in text, seconds=20) + + def launch(path): + subprocess.run( + [ + BIN, + "-s", + SESSION, + "action", + "launch-or-focus-plugin", + "--floating", + f"file:{path}", + ], + env=env, + capture_output=True, + text=True, + ) + + # 1. a plugin we ship: must run, must not ask + launch(os.path.join(shipped, "pane-finder.wasm")) + text = read_until( + fd, lambda t: RUNNING in t or any(n in t for n in NAG), seconds=25 + ) + nagged = [n for n in NAG if n in text] + running = RUNNING in text + print(f"shipped plugin: running={running} nagged={bool(nagged)}") + if nagged: + failures.append(f"the plugin we ship asked for permissions: {nagged}") + if not running: + failures.append("the plugin we ship did not start") + print("\n".join(text.splitlines()[-25:])) + + # 2. the same plugin from outside our plugin dir: must still ask, or + # "trusted" would just mean "any wasm on this machine" + subprocess.run( + [BIN, "-s", SESSION, "action", "close-pane"], env=env, capture_output=True + ) + time.sleep(1) + launch(os.path.join(elsewhere, "pane-finder.wasm")) + text = read_until(fd, lambda t: any(n in t for n in NAG), seconds=25) + nagged = [n for n in NAG if n in text] + print(f"unshipped plugin: nagged={bool(nagged)}") + if not nagged: + failures.append( + "a plugin outside the shipped plugin dir was granted permissions silently" + ) + print("\n".join(text.splitlines()[-25:])) + return failures + finally: + subprocess.run( + [BIN, "-s", SESSION, "kill-session"], env=env, capture_output=True, text=True + ) + try: + os.close(fd) + except OSError: + pass + os.waitpid(pid, os.WNOHANG) + time.sleep(1) + # never `pkill -f`: it matches this script's own command line + ps = subprocess.run(["ps", "-eo", "pid,comm,args"], capture_output=True, text=True).stdout + for line in ps.splitlines(): + fields = line.split(None, 2) + if len(fields) == 3 and fields[1] == os.path.basename(BIN) and tmp in fields[2]: + try: + os.kill(int(fields[0]), 15) + except (ProcessLookupError, ValueError, PermissionError): + pass + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + problems = main() + if problems == 1: + sys.exit(1) + for p in problems: + print(f"FAIL: {p}") + if not problems: + print("ok: shipped plugins start silently, others still ask") + sys.exit(1 if problems else 0) diff --git a/sl0ppij/zellij-server/src/plugins/zellij_exports.rs b/sl0ppij/zellij-server/src/plugins/zellij_exports.rs index 34b711d..2850136 100644 --- a/sl0ppij/zellij-server/src/plugins/zellij_exports.rs +++ b/sl0ppij/zellij-server/src/plugins/zellij_exports.rs @@ -41,7 +41,9 @@ use zellij_utils::data::{ PluginPermission, RegexHighlight, RenameLayoutResponse, SaveLayoutResponse, TabMetadata, }; use zellij_utils::home::default_layout_dir; -use zellij_utils::input::permission::PermissionCache; +use zellij_utils::input::permission::{ + plugin_is_shipped_with_sl0ppi, shipped_plugin_dirs, PermissionCache, +}; use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext}; use zellij_utils::sessions::generate_random_name as generate_random_name_impl; #[cfg(feature = "web_server_capability")] @@ -918,8 +920,14 @@ fn show_cursor(env: &PluginEnv, cursor_position: Option<(usize, usize)>) { } fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Result<()> { - if PermissionCache::from_path_or_default(None) - .check_permissions(env.plugin.location.to_string(), &permissions) + // sl0p fork: the plugins that come in our tarball are not asked about -- + // see `plugin_is_shipped_with_sl0ppi` for what that covers and why. Granted + // through the same instruction a human "y" produces, so the plugin's + // permissions are set and recorded exactly as they would otherwise be. + let is_ours = plugin_is_shipped_with_sl0ppi(&env.plugin, &shipped_plugin_dirs()); + if is_ours + || PermissionCache::from_path_or_default(None) + .check_permissions(env.plugin.location.to_string(), &permissions) { return env .senders diff --git a/sl0ppij/zellij-utils/src/input/permission.rs b/sl0ppij/zellij-utils/src/input/permission.rs index 9ef7182..46f1e60 100644 --- a/sl0ppij/zellij-utils/src/input/permission.rs +++ b/sl0ppij/zellij-utils/src/input/permission.rs @@ -2,10 +2,100 @@ use std::{ collections::HashMap, fs::{self, File}, io::Write, - path::PathBuf, + path::{Component, Path, PathBuf}, }; -use crate::{consts::ZELLIJ_PLUGIN_PERMISSIONS_CACHE, data::PermissionType}; +use crate::{ + branding, consts::ZELLIJ_PLUGIN_PERMISSIONS_CACHE, data::PermissionType, + input::layout::RunPluginLocation, input::plugins::PluginConfig, +}; + +/// sl0p fork: the plugins we ship do not ask for permission. +/// +/// Upstream prompts for every plugin on first load, which is right for a wasm +/// you found on the internet and wrong for the ones that come in the tarball: +/// pane-finder is on a keybinding we ship, and answering a three-item consent +/// screen the first time you press it is a nag about our own software. +/// +/// "Ours" is deliberately narrow, and never a blanket grant: +/// +/// - **builtin plugins** are compiled into the binary. Asked via +/// `PluginConfig::is_builtin`, the same predicate `check_command_permission` +/// already uses to exempt them from every command check, so the two cannot +/// drift into disagreeing about what "builtin" means. +/// - **files inside a plugin directory we install into** -- the config dir's +/// `plugins/`, where the installer puts `pane-finder.wasm` and +/// `zjstatus.wasm`, and the data dir's `plugins/`, where zellij extracts +/// its own assets. Anything that can write there can already edit +/// `config.kdl`, which can bind arbitrary commands to a key, so a consent +/// screen for the wasm beside it is not buying anything. +/// - **remote plugins are never trusted**, wherever they are cached. +/// +/// Everything else still prompts. This is the whole rule -- if a plugin is not +/// covered here it goes through the normal request, so the failure mode of +/// getting the directories wrong is an extra prompt, never a silent grant. +pub fn plugin_is_shipped_with_sl0ppi( + plugin: &PluginConfig, + shipped_plugin_dirs: &[PathBuf], +) -> bool { + if plugin.is_builtin() { + return true; + } + match &plugin.location { + RunPluginLocation::Zellij(_) => true, + RunPluginLocation::Remote(_) => false, + RunPluginLocation::File(path) => { + let path = resolved(path); + // a relative path cannot be compared against a directory without + // knowing the cwd it was resolved in, so we decline to trust it + if !path.is_absolute() { + return false; + } + shipped_plugin_dirs + .iter() + .map(|dir| resolved(dir)) + .any(|dir| dir.is_absolute() && path.starts_with(&dir)) + }, + } +} + +/// The directories whose plugins we ship, for `plugin_is_shipped_with_sl0ppi`. +/// +/// Derived from the environment and the default locations rather than plumbed +/// from the parsed config: a `--config-dir` given only on the command line is +/// therefore not seen here, which costs a prompt for a plugin in it and can +/// never grant one anywhere else. +pub fn shipped_plugin_dirs() -> Vec<PathBuf> { + let config_dir = branding::env_var(branding::ENV_CONFIG_DIR, branding::ENV_CONFIG_DIR_COMPAT) + .ok() + .map(PathBuf::from) + .or_else(crate::home::find_default_config_dir); + config_dir + .into_iter() + .chain(std::iter::once(crate::home::get_default_data_dir())) + .map(|dir| dir.join("plugins")) + .collect() +} + +/// Normalise a path without touching the filesystem, then resolve symlinks if +/// the file happens to be there. +/// +/// The lexical pass is what stops `<plugins>/../../anywhere.wasm` from reading +/// as "inside the plugin dir"; `canonicalize` is a best-effort tightening on +/// top, since it only works for paths that exist. +fn resolved(path: &Path) -> PathBuf { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {}, + Component::ParentDir => { + normalized.pop(); + }, + other => normalized.push(other.as_os_str()), + } + } + fs::canonicalize(&normalized).unwrap_or(normalized) +} pub type GrantedPermission = HashMap<String, Vec<PermissionType>>; @@ -65,3 +155,118 @@ impl PermissionCache { Ok(()) } } + +#[cfg(test)] +mod shipped_plugin_tests { + use super::*; + use crate::input::plugins::PluginTag; + + fn plugin_at(location: RunPluginLocation) -> PluginConfig { + PluginConfig { + path: PathBuf::new(), + _allow_exec_host_cmd: false, + location, + initial_userspace_configuration: Default::default(), + initial_cwd: None, + } + } + + fn file(path: &str) -> PluginConfig { + plugin_at(RunPluginLocation::File(PathBuf::from(path))) + } + + fn dirs() -> Vec<PathBuf> { + vec![ + PathBuf::from("/home/someone/.config/sl0ppij/plugins"), + PathBuf::from("/home/someone/.local/share/sl0ppij/plugins"), + ] + } + + #[test] + fn builtin_plugins_are_ours() { + // compiled into the binary: a prompt protects nothing, and + // check_command_permission already waves them through + let builtin = plugin_at(RunPluginLocation::Zellij(PluginTag::new("session-manager"))); + assert!(builtin.is_builtin()); + assert!(plugin_is_shipped_with_sl0ppi(&builtin, &[])); + } + + #[test] + fn plugins_in_our_plugin_dirs_are_ours() { + for path in [ + "/home/someone/.config/sl0ppij/plugins/pane-finder.wasm", + "/home/someone/.config/sl0ppij/plugins/zjstatus.wasm", + "/home/someone/.local/share/sl0ppij/plugins/tab-bar.wasm", + ] { + assert!( + plugin_is_shipped_with_sl0ppi(&file(path), &dirs()), + "{} should be trusted", + path + ); + } + } + + #[test] + fn plugins_from_anywhere_else_still_ask() { + for path in [ + "/tmp/downloaded.wasm", + "/home/someone/.config/sl0ppij/not-plugins/x.wasm", + // a sibling directory that merely starts with the same string: + // `starts_with` on a Path compares components, not bytes, so this + // must not match + "/home/someone/.config/sl0ppij/plugins-evil/x.wasm", + "relative/path.wasm", + ] { + assert!( + !plugin_is_shipped_with_sl0ppi(&file(path), &dirs()), + "{} should NOT be trusted", + path + ); + } + } + + #[test] + fn a_traversal_out_of_the_plugin_dir_is_not_ours() { + // the point of normalising lexically: this path is "inside" the plugin + // dir only as a string + assert!(!plugin_is_shipped_with_sl0ppi( + &file("/home/someone/.config/sl0ppij/plugins/../../../../tmp/evil.wasm"), + &dirs(), + )); + } + + #[test] + fn remote_plugins_are_never_ours() { + assert!(!plugin_is_shipped_with_sl0ppi( + &plugin_at(RunPluginLocation::Remote( + "https://example.com/plugin.wasm".to_owned() + )), + &dirs(), + )); + } + + #[test] + fn no_known_plugin_dirs_means_no_file_is_ours() { + // getting the directories wrong costs a prompt, never a silent grant + assert!(!plugin_is_shipped_with_sl0ppi( + &file("/home/someone/.config/sl0ppij/plugins/pane-finder.wasm"), + &[], + )); + } + + #[test] + fn the_plugin_dirs_we_look_in_are_named_plugins() { + // it does not resolve to anything real under test, but it must never + // hand back a bare config or data dir -- that would trust every file + // the user keeps beside their config + for dir in shipped_plugin_dirs() { + assert_eq!( + dir.file_name() + .map(|name| name.to_string_lossy().to_string()), + Some("plugins".to_string()), + "{:?} is not a plugin directory", + dir + ); + } + } +} |
