mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Problem: ExtendScript's preprocessor directives weren't understood, so a script (or ScriptUI panel, or library such as DuAEF) failed to parse at its first `#include`; `#target` / `#targetengine` / `#strict` lines broke parsing too. Fix: before a script runs, `include::expand` inlines `#include "f"` and `//@include "f"` in place of the line, resolved against the including file's folder, then the folders of the latest `#includepath "a;b"`. Each file is inlined once (which also breaks cycles), nesting is limited to 32, files are capped at 16 MiB (64 MiB in all), and reads follow the scripts' file gate (the script's own folder, the project's folder, or anywhere with Allow Scripts to Write Files). `#target`, `#targetengine`, `#strict`, `#script` and `#engine` are dropped. Errors in the expanded code are mapped back to the included file and its line. Scripts without directives run unchanged. File resolution, the gate and the limits live in `scriptfile.rs` for `$.evalFile` (#760) to share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
4.2 KiB
Rust
95 lines
4.2 KiB
Rust
//! #759: ExtendScript's `#include` / `//@include` / `#includepath` directives.
|
|
|
|
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-include-{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 includes_are_inlined_relative_to_the_script() {
|
|
let dir = tmp("ok");
|
|
write(&dir.join("lib/math.jsx"), "#include \"util.jsxinc\"\nfunction twice(x) { return 2 * x; }");
|
|
write(&dir.join("lib/util.jsxinc"), "// includes its includer back: inlined once only\n#include \"math.jsx\"\nvar unit = 'px';");
|
|
write(&dir.join("vendor/extra.jsx"), "var extra = 40;");
|
|
write(
|
|
&dir.join("main.jsx"),
|
|
"#target aftereffects\n#targetengine \"main\"\n#strict on\n//@include 'lib/math.jsx'\n#includepath \"vendor;lib\"\n#include \"extra.jsx\";\n#include \"util.jsxinc\"\n[twice(1) + extra, unit, $.fileName.split('/').pop()]",
|
|
);
|
|
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!([42, "px", "main.jsx"]));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_point_at_the_file_and_line_they_came_from() {
|
|
let dir = tmp("err");
|
|
write(&dir.join("lib.jsx"), "var a = 1;\nvar b = 2;\nnotDefined();");
|
|
write(&dir.join("main.jsx"), "// main\n#include \"lib.jsx\"\nvar c = 3;");
|
|
let mut s = session();
|
|
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
|
|
assert!(e.file.ends_with("lib.jsx") && e.line == Some(3), "{e:?}");
|
|
// After the include, lines in the script itself keep their numbers.
|
|
write(&dir.join("lib.jsx"), "var a = 1;\nvar b = 2;");
|
|
write(&dir.join("main.jsx"), "// main\n#include \"lib.jsx\"\nvar c = 3;\nalsoNotDefined();");
|
|
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
|
|
assert!(e.file.ends_with("main.jsx") && e.line == Some(4), "{e:?}");
|
|
// A missing file is a clear error on the directive's line.
|
|
write(&dir.join("main.jsx"), "var x = 1;\n#include \"missing.jsx\"");
|
|
let e = run_file(&mut s, &dir.join("main.jsx")).error.expect("an error");
|
|
assert!(e.message.contains("missing.jsx") && e.line == Some(2), "{e:?}");
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_includes_are_bounded_and_gated() {
|
|
let dir = tmp("hostile");
|
|
// A chain deeper than the limit is an error, not a stack overflow.
|
|
for i in 0..100 {
|
|
write(&dir.join(format!("chain/{i}.jsx")), &format!("#include \"{}.jsx\"", i + 1));
|
|
}
|
|
write(&dir.join("chain/100.jsx"), "1");
|
|
let mut s = session();
|
|
let e = run_file(&mut s, &dir.join("chain/0.jsx")).error.expect("an error");
|
|
assert!(e.message.contains("nested too deeply"), "{e:?}");
|
|
// Files outside the script's (and the project's) folder need the file preference.
|
|
write(&dir.join("outside.jsx"), "var secret = 7;");
|
|
write(&dir.join("app/main.jsx"), "#include \"../outside.jsx\"\nsecret");
|
|
let e = run_file(&mut s, &dir.join("app/main.jsx")).error.expect("blocked");
|
|
assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}");
|
|
// Code run without a file can't include relative to anything but the project's folder.
|
|
let e = run_code(&mut s, "#include \"outside.jsx\"", "console").error.expect("no folder");
|
|
assert!(e.message.contains("outside.jsx"), "{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);
|
|
}
|