Skip to content

fix: resolve the input path against the root before making it virtual - #37

Merged
evolvedlight merged 2 commits into
evolvedlight:developfrom
swisspost:fix/input-path-virtualize
Sep 6, 2026
Merged

fix: resolve the input path against the root before making it virtual#37
evolvedlight merged 2 commits into
evolvedlight:developfrom
swisspost:fix/input-path-virtualize

Conversation

@msallin

@msallin msallin commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

A Typst virtual path only accepts forward slashes. SystemWorld::new handed the caller's input path to VirtualPath::new verbatim and unwrapped the result, so on Windows an ordinary relative path such as templates\letter.typ failed to convert and panicked.

The panic happens inside create_compiler, which is extern "C" and has no unwind guard. A panic cannot unwind out of an extern "C" frame, so the process aborts rather than the call failing. Running the new tests against develop:

13: create_compiler
14: input_path::compiler_for_file
thread caused non-unwinding panic. aborting.
process didn't exit successfully (signal: 6, SIGABRT: process abort signal)

For a service on Windows, or any layout that sets an explicit root and keeps templates in a subfolder, this terminates the host process on the first compile with no managed exception. A .. component in the path does the same.

Change

Relative paths are joined onto the project root, then converted with VirtualPath::virtualize, which splits on the platform separator and performs the root containment check itself. A path that escapes the root now returns an error through the existing channel instead of panicking.

The manual to_str UTF-8 check is dropped because virtualize reports that case as VirtualizeError::Utf8. The <main> unwrap becomes an expect naming the invariant.

Every input the old code accepted resolves to the same virtual path. One input changes classification: C:foo.typ (drive-relative, which is_absolute() reports as false) used to produce a FileId whose segment was C:foo.typ, and that id could never be read back because realize rejects a segment starting with a path prefix. It is now either a clean error or, when the root is C:, a working path.

The pack job gains a cargo test step. The Rust tests call the FFI entry points directly, and that is the only place a panic crossing the boundary shows up as a failing test rather than as a crashed test runner, so without it these regression tests would never run in CI.

Known limitation

The root is matched against an absolute input path textually, so on Windows both have to use the same casing; C:\App as root with c:\app\letter.typ as input is still reported as escaping. That behaviour is unchanged by this PR and is now noted in the release notes.

This does not add an unwind guard to create_compiler, and the message built at world.rs:104 is still discarded by lib.rs:142, which returns a bare null. Both are separate concerns.

Tests

New src/typst_core/tests/input_path.rs covers a nested relative path, a literal templates\letter.typ on Windows, a path that leaves and re-enters the root, the equivalent absolute path, a relative path escaping the root, and an absolute path outside the root. Tests.cs covers the same cases through the managed API, including the common shape that passes no root at all.

The Rust tests abort the test binary against develop and pass with the change.

cargo test --release    8 passed (5 in input_path; the Windows-only case is skipped on Linux)
dotnet test            50 passed

A Typst virtual path only accepts forward slashes, and `VirtualPath::new`
was handed the caller's path verbatim. On Windows an ordinary relative path
such as `templates\letter.typ` therefore failed to convert, and the `unwrap`
turned that into a panic inside `create_compiler`. That function is
`extern "C"` and has no unwind guard, so the panic aborted the whole host
process rather than failing the single call.

Relative paths are now joined onto the project root and converted with
`VirtualPath::virtualize`, which splits on the platform separator and does
the root containment check itself. A path that genuinely escapes the root
reports an error through the existing channel instead of panicking, and the
manual UTF-8 check is no longer needed because `virtualize` covers it.

The pack job gains a `cargo test` step. The Rust tests exercise the FFI
entry points directly, which is the only place a panic crossing the boundary
surfaces as a failing test rather than as a crashed test runner.
@msallin
msallin force-pushed the fix/input-path-virtualize branch from c66c2df to 8dd77d6 Compare September 4, 2026 11:49
@evolvedlight
evolvedlight merged commit f5d457f into evolvedlight:develop Sep 6, 2026
1 check passed
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.

2 participants