Commit Graph
118 Commits
Author SHA1 Message Date
BFlat af22fd6d5b Merge pull request #6776 from dotCooCoo/fix/eval-source-takes-fuel
JavaScript: source that eval and Function compile takes fuel before it is parsed
2026-11-12 13:17:47 +07:00
BFlat 105536fff4 Merge pull request #6765 from dotCooCoo/fix/js-finally-sites-gc-test
Scripts: a finally block left early drops its saved throw site
2026-11-12 12:40:29 +07:00
BFlat df41593465 Merge pull request #6772 from dotCooCoo/fix/e4x-xml-declaration-check
Scripts: E4X checks the XML declaration and takes fuel for every run the parser scans
2026-11-12 12:40:21 +07:00
dotCooCoo 0996268f87 JavaScript: a host evaluator's whole source must fit in the heap before it is encoded
The fuel and heap checks for a host evaluator's source were split into a third
before encoding and the rest after, so a source whose whole UTF-16 text did not
fit in the heap could still be encoded and parsed. The whole encoded size is now
reserved before encoding. A source over the source limit is still refused as a
syntax error.
2026-11-11 20:54:00 -08:00
BFlat b945662f75 Merge pull request #6700 from dotCooCoo/fix/script-files-followups
Scripts: File and Folder exists checks the kind, encode keeps slashes, every call is counted
2026-11-12 11:42:26 +07:00
BFlat cd92345707 Merge pull request #6759 from dotCooCoo/fix/js-tosource-property-flags
Scripts: toSource refuses read-only, non-configurable and non-extensible data it cannot write back
2026-11-12 11:42:09 +07:00
dotCooCoo d151f3f5d0 JavaScript: source that eval and Function compile takes fuel before it is parsed
Direct and indirect eval, Function and host evaluators parsed and compiled
their source before taking any fuel or checking the heap, so a script with a
small fuel limit could make the interpreter parse megabytes of source in a
handful of instructions. Interp::prepare_source now takes one unit of fuel for
each UTF-16 code unit and checks that the source text fits in the heap before
the source is flattened, parsed or compiled. A host evaluator's string takes
the fuel for a third of its bytes before it is encoded, and the rest after.

Limits::fuel and the maxFuel row in docs/mcp.md say so.
2026-11-11 19:52:06 -08:00
dotCooCoo 781357921e Scripts: E4X checks the XML declaration and takes fuel for every run the parser scans
An XML declaration at the start of XML or XMLList text was skipped up to
the next "?>" without being read. It is now checked against XML 1.0's
grammar (https://www.w3.org/TR/xml/#NT-XMLDecl): a version of "1." and
digits, then an optional encoding name and an optional standalone "yes"
or "no", in that order, with optional white space before "?>". Any other
declaration gives "SyntaxError: invalid XML declaration", and a character
XML does not allow gives "invalid character in XML text". White space
before the declaration is still accepted.

The parser now takes fuel for the length of the declaration and the
white space before it, element and attribute names, white space in open
and closing tags, and the text it searched when an attribute value, CDATA
section, comment or processing instruction is not closed, or when "]]>"
or "<" stops a text run or attribute value. A long run in any of these
used the same fuel as a short one.
2026-11-11 19:35:39 -08:00
dotCooCoo dea30d6328 Scripts: a finally block left early drops its saved throw site
A finally block that break or continue leaves no longer keeps the saved
site of the exception it ran for. A Jump now removes the sites at or
above the stack's length, as a return already does, so a collection
after the stack grows back over that slot does not root and count the
old code.

New tests in crates/js/src/host_tests.rs: an error passed on by a
finally block that collects garbage under a 1 MiB heap is reported at
its throw; the saved site keeps direct eval code live through a
collection; a host abort inside a finally block leaves no saved site;
and break, continue and return out of a finally block drop the site.
2026-11-11 19:14:05 -08:00
BFlat 433a3009b7 Merge pull request #6701 from dotCooCoo/fix/xml-namespace-scope-fuel
JavaScript: XML namespace lookups take fuel for each ancestor they climb
2026-11-12 10:03:17 +07:00
dotCooCoo 19ed0889b1 Merge main into fix/script-files-followups
# Conflicts:
#	crates/engine/src/script/files.rs
2026-11-11 18:48:18 -08:00
dotCooCoo 98ad53e799 Merge main into fix/js-tosource-property-flags 2026-11-11 18:34:29 -08:00
dotCooCoo d28b72a358 Scripts: toSource refuses read-only, non-configurable and non-extensible data it cannot write back
A literal makes an extensible object whose properties are writable and
configurable, and an array whose length is writable. toSource wrote a
property defined with writable or configurable false, a sealed, frozen or
non-extensible object or array, and an array with a read-only length as an
ordinary literal, so evaluating the text gave an open value.

toSource now throws a TypeError for each of these, at any depth, as it
already does for getters, hidden properties and other prototypes.
Properties defined with every flag on are written as before.
2026-11-11 18:33:42 -08:00
BFlat 8719585cc6 Merge pull request #6663 from dotCooCoo/feat/script-target-engine
Scripts: #targetengine runs a script in a named engine that keeps its globals
2026-11-12 09:22:45 +07:00
BFlat e067daef3c Merge pull request #6662 from dotCooCoo/fix/script-finally-return-drops-throw-site
Scripts: a return from a finally block drops the discarded error's throw site
2026-11-12 09:22:36 +07:00
dotCooCoo a354503683 JavaScript: XML namespace lookups take fuel for each ancestor they climb
`inScopeNamespaces()`, `namespaceDeclarations()` and `namespace()` walk
from the node up to its root. The walk cloned each ancestor's whole node,
children and attributes included, to read its namespaces, and charged
fuel only for the namespaces it found, so a deep tree with no declared
namespaces cost no fuel. It now takes one unit of fuel per ancestor,
copies only the namespace list and parent link, and checks prefixes
already seen with a set. `namespaceDeclarations()` compares the node's
declarations with the ancestors' through a map instead of a scan.
2026-11-11 14:54:26 -08:00
dotCooCoo 5bd1bbf536 Scripts: File and Folder exists checks the kind, encode keeps slashes, every call is counted
- `File.encode` and `Folder.encode` leave `/` unescaped, as the File
  object reference lists it among the characters kept
  (https://extendscript.docsforadobe.dev/file-system-access/file-object/).
- `exists` is true for a `File` only when the path names something
  other than a folder, and for a `Folder` only when it names a folder.
- Fields of the records the app returns are read as own properties, so
  a property a script puts on `Object.prototype` does not turn a
  successful `open` into a failure.
- `File.encode`, `File.decode`, `File.isEncodingAvailable` and the
  `Folder` locations count as file calls and charge the host-call limit.
- On the web the locations are `null` and `File.tempName` names a file
  at the root, without asking the platform for a temporary folder.
2026-11-11 14:46:43 -08:00
dotCooCoo db6b55ab0a Scripts: a return looks only at the last saved finally site
The saved throw sites of running finally blocks are kept in rising slot
order, so leaving a frame or a block now removes entries from the end of
the list and stops at the first one below the slot. Before, every return
looked at every saved site, so a call made inside many nested finally
blocks took time in proportion to their count.
2026-11-11 14:03:21 -08:00
dotCooCoo 283d127e2a JavaScript: characters assigned in Unicode 17 are identifier characters
vectorcraft-js and Check Script took identifier categories from
unicode-general-category 1.1.0, which carries Unicode 16.0, so letters
and marks assigned in Unicode 17.0 (U+088F at the start of a name,
U+1ACF after its first character) were refused. Both lexers now read
general categories from unicode-properties 0.1.4 (Unicode 17.0), which
Cargo.lock already holds. The version is exact because 0.1.5 adds the
Unicode-3.0 license, which is not on the license allowlist. The
Other_ID_Start, Other_ID_Continue and Pattern_Syntax lists are the same
in PropList 17.0.

The exhaustive identifier test now expects the DerivedCoreProperties
17.0 totals (145,916 ID_Start, 149,240 ID_Continue), and new tests read
U+088F and U+1ACF written out and as escapes in both lexers.
2026-11-11 13:57:54 -08:00
dotCooCoo 8e90c833f6 Scripts: a kept engine's next run starts after the last uncaught exception is dropped
Resetting Folder.current for the next run of a named engine now drops the
last run's uncaught exception and $.error first, so memory only they held
no longer makes the reset fail with the heap limit on every later run.

A File an earlier run left open now counts as closed in a later run:
setting its encoding stores it for the next open, and reading the encoding
gives the stored one.

docs/mcp.md: engines stay on the thread that made them, and a
#targetengine line in a file $.evalFile runs makes that call throw a
SyntaxError when it runs.
2026-11-11 13:50:12 -08:00
dotCooCoo f04aa61fc1 Scripts: a return from a finally block drops the discarded error's throw site
A `return` inside a finally block discards the pending exception, but the
saved throw site for it stayed in the list of collection roots. The code it
pointed to then stayed live after the function was gone, which could stop a
script with the heap limit when that code came from `Function` or `eval`.
Returning from a frame now drops the saved sites for the slots it leaves.

Follows #6653.
2026-11-11 12:19:32 -08:00
dotCooCoo 3a7c46975c Merge main into feat/script-target-engine 2026-11-11 12:16:55 -08:00
BFlat 6e873b806c Merge pull request #6657 from dotCooCoo/fix/js-random-seed-test
Scripts: test that the OS random value seeds Math.random
2026-11-12 03:16:14 +07:00
dotCooCoo 7fbac74885 Merge main into feat/script-target-engine 2026-11-11 12:09:30 -08:00
BFlat dd6b00c228 Merge pull request #6653 from dotCooCoo/fix/script-throw-site-through-finally
Scripts: an uncaught error that passes through a finally block is reported where it was thrown
2026-11-12 02:53:22 +07:00
dotCooCoo 27c24a4f53 Scripts: test that the OS random value seeds Math.random
random_seed passes the OS random source's result to random_seed_from, so a test can
give it a known value and check the seed it makes, instead of only reading the source text.
2026-11-11 11:25:25 -08:00
BFlat 1c049233af Merge pull request #6645 from dotCooCoo/fix/js-random-seed-web
Scripts: Math.random gets a new seed on every web page load
2026-11-12 02:24:16 +07:00
dotCooCoo f1c17603bb Scripts: #targetengine runs a script in a named engine that keeps its globals
A script whose first lines name an engine with `#targetengine name` (or
`//@targetengine name`) runs in that engine, and a later script that names
it sees the globals and functions earlier runs left, until the engine is
dropped or the session ends. A script without the line, and
`#targetengine main`, still get a new engine for each run.

- A session keeps at most 8 named engines, each with a 64 MiB heap. A run
  that goes past the heap ends with the heap limit, and the engine stays
  usable. Names are 1 to 64 bytes. The line comes once, in the script
  itself, before its code and its #include lines.
- An uncaught error, a limit or script.stop puts the documents back and
  keeps the globals the run set. An internal error drops the engine. A run
  stopped at a dialog or left paused ends where it was before the engine
  is kept. An error in a function an earlier run defined is reported
  without a line and column.
- `vc`, `$`, `alert`, `confirm` and `prompt` are made once for each engine
  and act for the run that calls them, including copies an earlier run kept
  in a global. `vc.args` gives a read-only copy of the calling run's
  `args`, and what a run sets on `$` lasts for that run only.
- `script.engines` lists the named engines with their heap, and
  `script.engine.drop {name}` drops one. Scripts cannot run either.
- The engines stay on the thread that made them. A session used on
  another thread starts there with no named engines, and the old ones are
  dropped on their own thread.
- The web build has no named engines: a run that names one other than
  `main` ends with an error before any of it runs.
- `Realm::end` ends a run where it is, `Realm::error_source` counts from
  the realm's last script, and `HostItem::FrozenGetter` gives a frozen
  value each time it is read.
- The script checker lists `#targetengine` as a supported directive.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/gaps.md.
2026-11-11 11:17:42 -08:00
dotCooCoo 1f835d94c4 Merge main into fix/script-throw-site-through-finally 2026-11-11 10:44:55 -08:00
dotCooCoo 8d094fc811 Scripts: an uncaught error that passes through a finally block is reported where it was thrown
An error a finally block passed on unchanged was reported at the end of
that block, in script.run's report and in the Script Console, because the
block's end raised the error again at a new site. The interpreter now keeps
the site of the exception each finally block runs for, keyed by the stack
slot that holds it, and the block's end throws the exception on from that
site. This holds through nested finally blocks, called functions, resumed
runs and marked host errors. An error the finally block throws is reported
at that throw, and one its return, break or continue discards is not
reported.
2026-11-11 10:44:16 -08:00
BFlat 9840b65a85 Merge pull request #6626 from dotCooCoo/fix/js-xml-copy-and-insert-fuel
JavaScript: new XML copies a one-item list's node, and insertChildAfter and insertChildBefore take fuel per child scanned
2026-11-12 01:35:54 +07:00
BFlat 1e76791ae1 Merge pull request #6615 from dotCooCoo/feat/script-file-folder
Scripts: File and Folder read files in the script's own folder
2026-11-12 01:35:45 +07:00
dotCooCoo 6fbaad6c5e Scripts: Math.random gets a new seed on every web page load
Each realm seeded Math.random from std's RandomState. On
wasm32-unknown-unknown its keys come from stack and heap addresses,
which are the same on every page load, so scripts in the web build
drew the same sequence each time the page opened.

The seed now comes from getrandom (crypto.getRandomValues in the
browser, as crates/pdf/src/encrypt.rs already does), with RandomState
kept as the fallback when it fails.
2026-11-11 10:31:32 -08:00
dotCooCoo 28042c2e92 JavaScript: test that new XML copies a list item's attributes and children
The test for new XML with a one-item list now starts from an item that
has a parent, an attribute and a child. It checks that the copy has no
parent and that changing the copy's attribute and child leaves the
original unchanged, so a copy that drops or shares the children fails.
2026-11-11 09:47:33 -08:00
dotCooCoo 53bd6ecd9a JavaScript: new XML copies a one-item list's node, and insertChildAfter and insertChildBefore take fuel per child scanned
new XML(list) now returns a deep copy of the list's single item, as
ECMA-357 section 13.4.2 specifies for an XMLList argument; before, it
returned the item itself, so changes to the new value changed the list.

insertChildAfter and insertChildBefore take one unit of fuel for each
child they compare with the reference, as childIndex does.

The test for the missing xpath method now checks the XML and XMLList
prototypes; reading .xpath on a value selects child elements and
returned an empty list in every case.
2026-11-11 09:21:00 -08:00
dotCooCoo de95640d93 JavaScript: XML namespace declarations and QName strings count against the heap limit
addNamespace and setNamespace now add a declaration's bytes to the heap total when the element
gains a new prefix, and QName.prototype.toString adds the bytes of the string it returns. Before,
both only checked for room, so a loop of distinct prefixes could grow an element past the heap
limit until a collection recounted it.
2026-11-11 08:09:22 -08:00
dotCooCoo bc6c20d0c9 JavaScript: a limit during an XML path assignment or list copy keeps the tree whole
- A list copy holds each new copy as a root while the list is charged
  for it, so a collection at that point no longer frees the copy. Before,
  `x.setChildren(<c/> + <d/>)` near the heap limit could leave one child
  twice or end with an internal error.
- An assignment to a path such as `x.a.b = v` or `x.a.b[0] = v` removes
  the elements it added for the path when a later step stops at a limit
  or throws.
- Comparing XML values counts the attribute tables and the work stack
  against the heap limit.
2026-11-11 08:00:42 -08:00
dotCooCoo c4d323eb28 Merge main into feat/js-e4x-xml
# Conflicts:
#	crates/js/src/interp.rs
2026-11-11 07:48:21 -08:00
dotCooCoo ba05554c64 Merge main into feat/script-file-folder 2026-11-11 07:47:42 -08:00
dotCooCoo b7a6196994 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.
2026-11-11 07:46:33 -08:00
dotCooCoo ccf153dccc Merge main into feat/script-dollar-object 2026-11-11 04:52:37 -08:00
dotCooCoo c746841839 Scripts: $.error keeps the last error thrown, and $.sleep throws in callbacks on the web
An exception that a finally block passes on, or one that leaves a
built-in's callback, no longer becomes $.error again over an error thrown
after it; throwing a caught error again still makes it the last one.

$.sleep with more than 0 inside a getter, a conversion or a built-in's
callback now throws whether or not the platform has a clock, so the web
build throws there as the other builds do.
2026-11-11 04:48:13 -08:00
dotCooCoo af22ac8f50 Scripts: $.stack names a declared function after its declaring function returns
The compiler now keeps a function declaration's name with its code, so
$.stack no longer shows [anonymous] for a declared function that is
called after the function that declared it has returned. The member test
expects "Web" for $.os in the wasm build, as the engine gives there.
2026-11-11 04:23:32 -08:00
dotCooCoo c8475771b9 Merge main into fix/script-rethrown-error-place 2026-11-11 03:59:08 -08:00
dotCooCoo d50ea29772 Merge main into feat/script-dollar-object 2026-11-11 03:58:05 -08:00
dotCooCoo 1677170333 Scripts: a marked error a finally block leaves by return, break or continue is collected
A marked host error whose finally block was left by return, break or
continue kept its entry, and its error object, until the run ended. A
script that discarded many of them reached the heap limit. Each entry
now records the stack slot that holds its exception, and entries whose
slot no longer holds it are removed before the next exception is handled
and before each collection.
2026-11-11 03:40:02 -08:00
dotCooCoo 0bb923fc40 Scripts: $.sleep throws where the run cannot wait, and toSource checks its length as it escapes
A getter, a conversion, a callback of a built-in function or a file
$.evalFile runs is executed on native frames, so the run cannot stop at a
$.sleep there. Such a call used to only mark the run to pause, so the
code after it ran first and the wait came later. $.sleep with more than 0
milliseconds now throws an Error there, as alert, confirm and prompt do.

toSource checked the 16,777,216-unit length limit only after a whole
string was written. A long string of characters that are escaped could
reach the heap limit first, which ends the run instead of throwing a
RangeError. The limit is now checked before each part is added.
2026-11-11 03:18:55 -08:00
dotCooCoo df5d061b30 Scripts: an error $.evalFile could not run, caught and thrown again, is reported where it was thrown again
A failure $.evalFile raised for a file it could not run was placed by
matching the uncaught error's message against the last such failure. A
script that caught the failure and threw it again later, or threw a new
error whose message held its text, was reported at the failed file's line
instead of the second throw.

The failure now carries a mark (HostError::Marked) with the exception.
A catch clause drops the mark, a throw raises the value without one, and
a finally block that ends normally throws it on with its mark, nested
finally blocks included. Realm::error_mark gives the mark of the error a
run ended with, and the engine looks up the file, line and column by it.
2026-11-11 03:14:42 -08:00
dotCooCoo 0e4010d26b Merge main into feat/script-dollar-object 2026-11-11 02:35:43 -08:00
dotCooCoo 1da6e899d3 Merge main into feat/script-strict-on 2026-11-11 02:33:33 -08:00