use std::path::{Path, PathBuf};
use std::process::Command;
const ALLOWED_DEPS: &[&str] = &["bevy", "serde", "serde_json", "image"];
const FORBIDDEN_MARKERS: &[&str] = &[
"enigo",
"xdotool",
"CGEvent",
"core_graphics",
"core-graphics",
"SendInput",
"screencapture",
"osascript",
"winit",
"unsafe",
];
const EVENT_SYNTHESIS_CRATES: &[&str] = &[
"enigo",
"xdotool",
"rdev",
"device_query",
"autopilot",
"winit",
"core-graphics",
"coregraphics",
"windows",
"winapi",
"objc",
"objc2",
];
fn crate_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
}
fn rust_sources(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
rust_sources(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
fn strip_comments(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let mut chars = src.chars().peekable();
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut in_string = false;
let mut in_char = false;
while let Some(c) = chars.next() {
if in_line_comment {
if c == '\n' {
in_line_comment = false;
out.push(c);
}
continue;
}
if in_block_comment {
if c == '*' && chars.peek() == Some(&'/') {
chars.next();
in_block_comment = false;
}
continue;
}
if in_string {
out.push(c);
if c == '\\' {
if let Some(next) = chars.next() {
out.push(next);
}
continue;
}
if c == '"' {
in_string = false;
}
continue;
}
if in_char {
out.push(c);
if c == '\\' {
if let Some(next) = chars.next() {
out.push(next);
}
continue;
}
if c == '\'' {
in_char = false;
}
continue;
}
match c {
'/' if chars.peek() == Some(&'/') => {
chars.next();
in_line_comment = true;
}
'/' if chars.peek() == Some(&'*') => {
chars.next();
in_block_comment = true;
}
'"' => {
in_string = true;
out.push(c);
}
'\'' => {
in_char = true;
out.push(c);
}
_ => out.push(c),
}
}
out
}
#[test]
fn no_source_file_can_reach_the_operating_system() {
let mut files = Vec::new();
for dir in ["src/plugin", "examples"] {
rust_sources(&crate_root().join(dir), &mut files);
}
if let Ok(entries) = std::fs::read_dir(crate_root()) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|e| e == "rs") {
files.push(path);
}
}
}
files.retain(|p| p.file_name().is_some_and(|n| n != "plugin_leaf.rs"));
assert!(
!files.is_empty(),
"expected to scan the plugin's sources, found none — has the layout moved?"
);
let mut offenders = Vec::new();
for path in &files {
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
let code = strip_comments(&text);
for (n, line) in code.lines().enumerate() {
for marker in FORBIDDEN_MARKERS {
if line.contains(marker) {
offenders.push(format!(
"{}:{} — {}",
path.strip_prefix(crate_root()).unwrap_or(path).display(),
n + 1,
line.trim()
));
}
}
}
}
assert!(
offenders.is_empty(),
"the plugin must not be able to touch the OS, but {} line(s) reference something \
that can.\n {}\n\n\
Screenshots here read an offscreen `Image`, and input is written into Bevy's own in-process \
input message stream. Both guarantees are structural — they hold because nothing linked \
into the plugin is capable of synthesising an OS event. A capture that reads the window \
needs the window raised, and a synthesised keystroke lands in whatever application actually \
has focus. If you genuinely need one of these, it belongs in a different crate that a game \
opts into knowingly.",
offenders.len(),
offenders.join("\n ")
);
}
#[test]
fn the_plugin_direct_dependencies_stay_small() {
let tree = cargo_tree_for_the_plugin(Some(1));
let direct = tree
.lines()
.skip(1) .filter_map(|line| {
let name = line
.trim()
.trim_start_matches(['├', '└', '│', '─', ' '])
.split_whitespace()
.next()?;
Some(name.to_string())
})
.collect::<Vec<_>>();
assert!(
!direct.is_empty(),
"cargo tree produced no direct dependencies — has the plugin's feature set changed?"
);
for name in &direct {
assert!(
ALLOWED_DEPS.contains(&name.as_str()),
"the plugin took a new direct dependency: `{name}`.\n\n\
The allowed ones ({}) are allowed precisely because none of them can synthesise an \
operating-system event — that is what makes \"injected input cannot reach your editor\" \
a structural claim rather than a promise. Adding to this list means re-arguing it.",
ALLOWED_DEPS.join(", "),
);
}
}
#[test]
fn the_plugin_closure_contains_nothing_that_can_synthesise_an_event() {
let tree = cargo_tree_for_the_plugin(None);
let mut offenders = Vec::new();
for line in tree.lines() {
let name = line
.trim()
.trim_start_matches(['├', '└', '│', '─', ' '])
.split_whitespace()
.next()
.unwrap_or_default();
for crate_name in EVENT_SYNTHESIS_CRATES {
if name.eq_ignore_ascii_case(crate_name) {
offenders.push(name.to_string());
}
}
}
assert!(
offenders.is_empty(),
"the plugin's dependency closure contains {} crate(s) that can synthesise an operating-system \
event or read which window has focus: {}.\n\n\
This is the check that keeps \"injected input cannot reach your editor\" structural. A game \
that depends on this crate with `--no-default-features --features plugin` compiles \
everything in this closure, so anything in it that can synthesise an event is a hole in the \
guarantee. The server's dependencies are behind the `server` feature and are not in this \
closure — check that the feature split has not been undone.",
offenders.len(),
offenders.join(", ")
);
}
fn cargo_tree_for_the_plugin(depth: Option<usize>) -> String {
let depth_arg = depth.map(|d| d.to_string());
let mut args = vec![
"tree".to_string(),
"--no-default-features".to_string(),
"--features".to_string(),
"plugin".to_string(),
"--edges".to_string(),
"normal".to_string(),
];
if let Some(depth) = depth_arg {
args.push("--depth".to_string());
args.push(depth);
}
let output = Command::new(env!("CARGO"))
.args(&args)
.current_dir(crate_root())
.output()
.expect("running cargo tree");
assert!(
output.status.success(),
"cargo tree failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}