mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 18:18:17 -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>
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())
|
|
}
|