Skip to content

EPS import: EPS files from the reference app open as vectors - #331

Merged
h-a-n-a-s-h-i merged 1 commit into
storytold:mainfrom
EthanSuquet:fix/eps-real-file-prolog
Oct 7, 2026
Merged

h-a-n-a-s-h-i merged 1 commit into
storytold:mainfrom
EthanSuquet:fix/eps-real-file-prolog

Conversation

@EthanSuquet

Copy link
Copy Markdown
Contributor

Follow-up to #234 / #249. Real EPS files saved by the reference app (v30) still fell back to their preview, failing with typecheck in \a dictionary``. Working through the real prolog turned up these gaps:

  • globaldict, userdict and the other standard dictionaries were operators, not values stored in systemdict, so /globaldict get begin failed (crates/eps/src/import/interp.rs).
  • dictstack, internaldict, currentsystemparams / currentuserparams, cshow, SharedFontDirectory and errordict handleerror were missing.
  • cvx exec on a file didn't run its data.
  • SubFileDecode with 0 () didn't pass data through.
  • flushfile didn't skip a filter's data (the embedded XMP).

Fix: all of the above. The dictionaries are emptied when the interpreter finishes, since systemdict now contains itself and would otherwise leak. The EPS fuzzer gets the new tokens, and docs/mcp.md and the ROADMAP are updated.

Tests:

  • 5 new hand-written PostScript tests: all fail before, pass after.
  • The full eps suite (47) and the engine EPS fuzz tests pass.
  • On two real files (not attachable): 15 vector paths each, CMYK kept, no warnings, pixel-identical SVG and PDF round trip. Before, both opened only as their preview.

cargo fmt --check and cargo clippy -D warnings are clean. Not run locally: the full cargo xtask ci (wasm, layers).

Refs #234

🤖 Generated with Claude Code

storytold#249 made the reduced repros of storytold#234 open, but a real file saved by the
reference app (version 30) still fell back to its screen-resolution preview
with "typecheck in `a dictionary`". Running its prolog to the end needed:

- The standard dictionaries (systemdict, userdict, globaldict, statusdict,
  errordict, $error) as values in systemdict, so `/globaldict where
  { /globaldict get begin } if` gets a dictionary, not an operator.
  SharedFontDirectory names the font directory too. errordict has a
  handleerror for prologs that wrap it.
- `dictstack`, `internaldict`, `currentsystemparams` and
  `currentuserparams` (with the cache sizes programs `get`), and `cshow`.
- Executable files: `cvx` and `cvlit` set a file's attribute, `xcheck`
  reads it, and executing one runs its data as a program, as the prolog
  does with `(...) 0 () /SubFileDecode filter cvx exec`.
- SubFileDecode with a count of 0 and an empty end string passes its
  whole source through instead of nothing.
- `flushfile` on a filter over the program's data skips that data (the
  file's XMP packet). A filter chained from a pending one leaves it
  nothing to skip, so the writer's own `VCsrc flushfile` is unchanged.

The interpreter empties its standard dictionaries when it finishes, since
systemdict now holds itself. Both real files tested now import as 15
vector paths in CMYK with no warnings, and round-trip through SVG and PDF
pixel for pixel. The EPS fuzzer exercises the new operators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@h-a-n-a-s-h-i
h-a-n-a-s-h-i merged commit f020d6c into storytold:main Oct 7, 2026
bflatastic added a commit that referenced this pull request Oct 7, 2026
…ble-file tests (review of #331)

Laying out type (show, stringwidth, cshow) costs about 300 operations plus
64 a character, measured, and now counts that much against MAX_OPS: a
300-byte file running cshow over a 4 MB string took 60 s, and
{ (a) stringwidth pop pop } loop ran for over 5 minutes (already the case on
main); both now stop with "the program runs too long" within seconds.
Tests: executable filters run inside themselves stop at the nesting limit
(directly, mutually and under stopped), currentfile cvx exec runs nothing
twice, cshow over a long string is refused; the hostile PostScript fuzz
tokens gain self-recursive filter execution, currentfile filters, cvlit and
cshow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bflatastic added a commit that referenced this pull request Oct 7, 2026
@bflatastic

Copy link
Copy Markdown
Member

Thanks @EthanSuquet! This is merged. The standard dictionaries as values, dictstack, internaldict, cshow, executable files and SubFileDecode now get real EPS prologs through to vectors. A synthetic file using them fails with a typecheck on main and opens as vectors with this PR.

Since the interpreter runs untrusted files, I threw hostile PostScript at it while reviewing. Self, mutual and stopped filter-exec recursion, 100k-deep file exec, 200k unclosed braces, currentfile cvx exec loops and dict cycles are all refused with a message, with no crash. One real problem turned up, also present on main: a 300-byte file running cshow over a 4 MB string took 60 s, and a stringwidth loop ran for minutes. The fix is in #383:

  • Op budget: type layout now counts against the interpreter's op budget (Interp::spend). Those files are refused in about 3 s and 11 s.
  • Tests: a test with hostile executable files and type loops.
  • Fuzzing: the EPS fuzzer gets self-recursive filter exec, currentfile filter exec, cvlit and cshow tokens.

open_file of an EPS works over MCP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants