mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 02:27:00 -06:00
Problem: `$.evalFile` always threw, and `typeof BridgeTalk` was `undefined`, so frameworks such as DuAEF failed during initialisation when they load their modules or ask which application they run in. Fix: `$.evalFile(file[, timeout])` evaluates a script file in the global scope and returns its last value. Relative paths resolve against the folder of the file being evaluated (the running script's for the outermost call, else the project's); reads follow the same file gate, 16 MiB size cap and 32-deep nesting limit as `#include` (#759, shared `scriptfile.rs`), and the bracket-depth check runs before parsing. `BridgeTalk` names the host honestly (`appName` "effectcraft", its version, `appSpecifier`, `isRunning` / `getSpecifier` / `getStatus` / `getTargets` for ourselves only); `send()` throws a clear "not supported" error instead of messaging other applications. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
4.3 KiB
Rust
100 lines
4.3 KiB
Rust
//! #760: `$.evalFile` and the `BridgeTalk` host identification.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use effectcraft_engine::Session;
|
|
use serde_json::json;
|
|
|
|
use crate::{Outcome, run_code};
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::default();
|
|
crate::install(&mut s);
|
|
s
|
|
}
|
|
|
|
fn tmp(tag: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("ec-evalfile-{tag}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
dir
|
|
}
|
|
|
|
fn write(path: &Path, text: &str) {
|
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
std::fs::write(path, text).unwrap();
|
|
}
|
|
|
|
/// Run the script file `main` (its code read from disk, as File ▸ Scripts ▸ Run Script File does).
|
|
fn run_file(s: &mut Session, main: &Path) -> Outcome {
|
|
let code = std::fs::read_to_string(main).unwrap();
|
|
run_code(s, &code, &main.to_string_lossy())
|
|
}
|
|
|
|
#[test]
|
|
fn eval_file_evaluates_relative_to_the_calling_script() {
|
|
let dir = tmp("ok");
|
|
// lib/a.jsx loads b.jsx next to itself, not next to main.jsx.
|
|
write(&dir.join("lib/a.jsx"), "var fromA = $.evalFile('b.jsx') + 1;\nfunction twice(x) { return 2 * x; }\nfromA");
|
|
write(&dir.join("lib/b.jsx"), "var fromB = 20;\nfromB");
|
|
write(
|
|
&dir.join("main.jsx"),
|
|
"var r = $.evalFile(new File($.fileName).path + '/lib/a.jsx');\nvar s = $.evalFile('lib/b.jsx');\n[r, s, twice(fromB), typeof fromA]",
|
|
);
|
|
let mut s = session();
|
|
let o = run_file(&mut s, &dir.join("main.jsx"));
|
|
assert!(o.error.is_none(), "{:?}", o.error);
|
|
assert_eq!(o.result, json!([21, 20, 40, "number"]));
|
|
// Errors in the evaluated file reach the caller, which can catch them.
|
|
write(&dir.join("bad.jsx"), "throw new Error('bad file');");
|
|
write(&dir.join("main.jsx"), "var m; try { $.evalFile('bad.jsx'); } catch (e) { m = e.message; } m");
|
|
assert_eq!(run_file(&mut s, &dir.join("main.jsx")).result, json!("bad file"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_file_is_bounded_and_gated() {
|
|
let dir = tmp("hostile");
|
|
// A file that evaluates itself forever stops at the depth limit.
|
|
write(&dir.join("loop.jsx"), "$.evalFile('loop.jsx');");
|
|
let mut s = session();
|
|
let e = run_file(&mut s, &dir.join("loop.jsx")).error.expect("an error");
|
|
assert!(e.message.contains("nested too deeply"), "{e:?}");
|
|
// Deeply bracketed files are refused before they reach the parser.
|
|
write(&dir.join("deep.jsx"), &"[".repeat(100_000));
|
|
write(&dir.join("main.jsx"), "$.evalFile('deep.jsx')");
|
|
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
|
|
assert!(e.message.contains("nested too deeply"), "{e:?}");
|
|
// Missing files are a clear error.
|
|
write(&dir.join("main.jsx"), "$.evalFile('missing.jsx')");
|
|
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
|
|
assert!(e.message.contains("missing.jsx"), "{e:?}");
|
|
// Files outside the script's (and the project's) folder need the file preference.
|
|
write(&dir.join("outside.jsx"), "7");
|
|
write(&dir.join("app/main.jsx"), "$.evalFile('../outside.jsx')");
|
|
let e = run_file(&mut s, &dir.join("app/main.jsx")).error.expect("blocked");
|
|
assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}");
|
|
s.prefs.scripting.allow_scripts_write_files = true;
|
|
let o = run_file(&mut s, &dir.join("app/main.jsx"));
|
|
assert_eq!(o.result, json!(7), "{:?}", o.error);
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn bridgetalk_identifies_effectcraft() {
|
|
let mut s = session();
|
|
let o = run_code(
|
|
&mut s,
|
|
"[typeof BridgeTalk, BridgeTalk.appName, BridgeTalk.appVersion, BridgeTalk.appSpecifier, BridgeTalk.isRunning('aftereffects'), \
|
|
BridgeTalk.getSpecifier('effectcraft'), BridgeTalk.getStatus('photoshop'), new BridgeTalk().sender]",
|
|
"bt.jsx",
|
|
);
|
|
assert!(o.error.is_none(), "{:?}", o.error);
|
|
let v = env!("CARGO_PKG_VERSION");
|
|
let spec = format!("effectcraft-{v}");
|
|
assert_eq!(o.result, json!(["function", "effectcraft", v, spec, false, spec, "ISNOTINSTALLED", spec]));
|
|
// Messages to other applications are unsupported, with a clear error.
|
|
let e = run_code(&mut s, "var bt = new BridgeTalk(); bt.target = 'photoshop'; bt.body = '1'; bt.send();", "bt.jsx").error.expect("unsupported");
|
|
assert!(e.message.contains("not supported"), "{e:?}");
|
|
}
|