mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-23 12:13:04 -05: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>
66 lines
3.1 KiB
Rust
66 lines
3.1 KiB
Rust
//! Script files a running script loads (`#include`, `$.evalFile`): where a name resolves, whether
|
|
//! the script may read it, and the limits that keep hostile, huge or cyclic files from running
|
|
//! away.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use effectcraft_engine::Session;
|
|
|
|
use crate::runtime::{GATE, normalize, project_dir};
|
|
|
|
/// How deep loaded files may nest (a loaded file loading another…).
|
|
pub(crate) const MAX_DEPTH: usize = 32;
|
|
/// The largest single file a script may load.
|
|
pub(crate) const MAX_FILE_BYTES: usize = 16 << 20;
|
|
|
|
/// The folder of the script file `name` runs from: `None` for the Script Console and for scripts
|
|
/// run by name or from code, which have no file.
|
|
pub(crate) fn script_dir(s: &Session, name: &str) -> Option<PathBuf> {
|
|
let parent = Path::new(name).parent().filter(|p| !p.as_os_str().is_empty())?;
|
|
s.services.exists(name).then(|| normalize(&parent.to_string_lossy()))
|
|
}
|
|
|
|
/// Resolve `name`: absolute paths as they are; relative ones against `dir` (the loading script's
|
|
/// folder), then each of `search`. The first existing file wins; when none exists, the first
|
|
/// candidate, so the read error names it.
|
|
pub(crate) fn resolve(s: &Session, name: &str, dir: Option<&Path>, search: &[PathBuf]) -> Result<PathBuf, String> {
|
|
if Path::new(name).is_absolute() {
|
|
return Ok(normalize(name));
|
|
}
|
|
let candidates: Vec<PathBuf> = dir.into_iter().chain(search.iter().map(PathBuf::as_path)).map(|d| normalize(&d.join(name).to_string_lossy())).collect();
|
|
candidates
|
|
.iter()
|
|
.find(|c| s.services.exists(&c.to_string_lossy()))
|
|
.or(candidates.first())
|
|
.cloned()
|
|
.ok_or_else(|| format!("cannot find \"{name}\": the script has no folder to look in (use an absolute path or #includepath)"))
|
|
}
|
|
|
|
/// The read gate for loaded script files: files under the running script's own folder (`root`) or
|
|
/// the project's folder; anywhere with the Allow Scripts to Write Files preference.
|
|
pub(crate) fn check_read(s: &Session, path: &Path, root: Option<&Path>) -> Result<(), String> {
|
|
if s.prefs.scripting.allow_scripts_write_files {
|
|
return Ok(());
|
|
}
|
|
let project = project_dir(s).map(|d| normalize(&d));
|
|
if root.into_iter().chain(project.as_deref()).any(|d| path.starts_with(d)) {
|
|
return Ok(());
|
|
}
|
|
Err(format!("Unable to read {}: scripts can only load files from their own or the project's folder unless {GATE} is on", path.display()))
|
|
}
|
|
|
|
/// Read a script file as text (a UTF-8 byte order mark dropped), refusing files over
|
|
/// [`MAX_FILE_BYTES`].
|
|
pub(crate) fn read(s: &Session, path: &Path) -> Result<String, String> {
|
|
let too_big = || format!("Unable to read {}: script files are limited to {} MiB", path.display(), MAX_FILE_BYTES >> 20);
|
|
if std::fs::metadata(path).is_ok_and(|m| m.len() > MAX_FILE_BYTES as u64) {
|
|
return Err(too_big());
|
|
}
|
|
let bytes = s.services.read_file(&path.to_string_lossy()).map_err(|e| format!("Unable to read {}: {e}", path.display()))?;
|
|
if bytes.len() > MAX_FILE_BYTES {
|
|
return Err(too_big());
|
|
}
|
|
let text = String::from_utf8_lossy(&bytes);
|
|
Ok(text.strip_prefix('\u{feff}').unwrap_or(&text).to_string())
|
|
}
|