Scripts: File and Folder read files in the script's own folder

Scripts get the File and Folder classes as built-ins of the script
engine. A script run from a file can test whether files exist, read
their properties, list folders with getFiles (a mask or a function),
and open files to read them with read, readln, readch, seek and tell.

- Reads stay inside the script's folder and, when automation read
  roots are set, inside them too. A script given as text reads no
  files. Network and device paths, and paths outside the folder by
  their names, are refused before anything is looked up. Links and
  junctions that lead out are refused, and getFiles leaves them out.
- The encoding comes from the byte order mark, then the encoding the
  script set, then UTF-8 when the bytes are valid, then CP1252.
- Limits: 8 MiB a file, 32 MiB a run, 256 files open at once, 2,048
  getFiles entries and 65,536 names looked at, and paths of at most
  32,767 characters.
- Writing, the file dialogs and execute are not available yet: they
  return false or null and set error.

The report's hostCallCounts gains fileCalls and fileExecutes.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-11 07:46:33 -08:00
parent f242b0752f
commit b7a6196994
15 changed files with 2489 additions and 5 deletions
File diff suppressed because it is too large Load Diff
+5
View File
@@ -251,6 +251,11 @@ impl Files {
self.include_path.join(";")
}
/// The folders the run's own files are read from.
pub(crate) fn scope(&self) -> &ScriptScope {
&self.scope
}
/// The script's own text, `text`, with its preprocessor lines read.
pub(crate) fn expand_script(&mut self, text: &str) -> Result<Expanded, Failure> {
self.bytes = self.bytes.saturating_add(text.len());
+14 -1
View File
@@ -35,6 +35,8 @@
//!
//! `alert`, `confirm` and `prompt` open dialogs ([`dialogs`]): in the app the run waits for the
//! person's answer, and a run with nobody to ask takes its answers from `script.run`'s `answers`.
//!
//! `File` and `Folder` read files inside the script's own folder ([`files`]).
pub mod console;
pub mod dialogs;
@@ -56,6 +58,7 @@ use crate::{
Clipboard, DocState, DrawMode, EngineError, Journal, NodeId, PaintDefaults, Result, Session, cmd, command_specs, guard, waits_for_tool_edit,
};
pub mod files;
pub mod folder;
/// The most bytes of source a run takes, as a file or as `source`.
@@ -492,6 +495,8 @@ struct Host {
dialogs: dialogs::Dialogs,
/// The files the run reads: its includes and `$.evalFile`'s.
files: RefCell<include::Files>,
/// What `File` and `Folder` have open and have read.
disk: RefCell<files::Disk>,
/// Where the lines of the script's expanded source came from.
script_map: RefCell<include::SourceMap>,
/// The same for each source `$.evalFile` gave, in order.
@@ -511,6 +516,10 @@ struct Counts {
command_list_reads: Cell<u64>,
vc_log: Cell<u64>,
console_writes: Cell<u64>,
/// Calls of `File` and `Folder` that reach the host ([`files`]).
file_calls: Cell<u64>,
/// `execute` calls, which open nothing.
file_executes: Cell<u64>,
}
impl Counts {
@@ -521,6 +530,8 @@ impl Counts {
"commandListReads": self.command_list_reads.get(),
"vcLog": self.vc_log.get(),
"consoleWrites": self.console_writes.get(),
"fileCalls": self.file_calls.get(),
"fileExecutes": self.file_executes.get(),
})
}
}
@@ -1082,7 +1093,7 @@ fn kind_id(kind: &RunErrorKind) -> &'static str {
/// `message`, with what to do about a failure that comes from a script written for another program.
fn explained(kind: &RunErrorKind, message: &str) -> String {
const HOST_OBJECTS: &[&str] = &["app", "File", "Folder", "Window", "BridgeTalk", "activeDocument", "documents"];
const HOST_OBJECTS: &[&str] = &["app", "Window", "BridgeTalk", "activeDocument", "documents"];
let names_host_object =
message.strip_prefix("ReferenceError: ").is_some_and(|m| HOST_OBJECTS.iter().any(|o| m.split_whitespace().next() == Some(o)));
if *kind == RunErrorKind::Thrown && names_host_object {
@@ -1311,6 +1322,7 @@ impl Session {
commands: RefCell::default(),
run_nanos: Cell::new(0),
dialogs: dialogs::Dialogs::new(self.script_frames, &realm, request.answers),
disk: RefCell::new(files::Disk::new(files.scope())),
files: RefCell::new(files),
script_map: RefCell::default(),
eval_maps: RefCell::default(),
@@ -1330,6 +1342,7 @@ impl Session {
.define_host_object("vc", vc_object(&host, args))
.and_then(|()| realm.define_host_object("$", dollar_object(&host)))
.and_then(|()| dialogs::install(&mut realm, &host))
.and_then(|()| files::install(&mut realm, &host))
.and_then(|()| realm.start(&expanded.source))
.map_err(|e| located(&host, realm.error_source(), realm.error_mark(), &e))
}