effectcraft/crates/script/src/scriptfile.rs

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())
}