* Automation: --root refuses every outside path the same way (#136)
With a root set, `resolve` canonicalized a path before checking it against
the root, so an existing outside path got "is outside the allowed
directory" while a missing one got the OS error ("No such file or
directory", "Not a directory", "invalid path"): a confined MCP client or
script could probe which files and folders exist anywhere on the disk.
Containment is now decided before anything about an outside path is
reported:
- `..` is resolved by name first (as Windows does), so a path can't pass
through an outside folder on its way back in;
- on Windows another network share or device namespace (`\\host\share`,
`\\?\UNC\...`, `\\.\...`) is refused by name, without contacting it;
- the deepest existing ancestor, links followed, must be inside the root
before anything is said about the rest;
- a broken link is refused the same way, so whether its target exists
stays hidden too.
Every outside path now gets `<path> is outside the allowed directory
<root>`, existing or not, file or folder, read or write. Filesystem errors
such as "not found" are still reported for paths inside the root.
Behaviour change: on Unix, `..` now means the parent by name, as on
Windows (`link/../x` is `x` beside `link`), and a write such as
`new/../x.pdf` that stays in the root now succeeds instead of failing with
"'..' is not allowed here".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI: script images stay inside --root (#137)
`pdfcraft-cli run --script steps.json --root DIR` wrote each step's
`"out"` PNG with `std::fs::write`, never through the root check the tools
use: an absolute `"out"` created or replaced any file the process could
write, and a relative one landed in the working directory instead of the
root.
The image is now saved through the new `Automation::write_output`, which
uses the same resolver and atomic write as a tool's own output: outside
paths get the usual "is outside the allowed directory" refusal (the step
fails, nothing is written), relative paths resolve inside the root, and
without --root the path is written as given, in place as before (so
`/dev/stdout` still works). `run <tool> --out` goes the same way.
Found while testing this, the same "." problem in two tools:
- a write path naming a folder, such as "." (the root itself), staged its
temporary file beside that folder, i.e. outside the root for ".",
overwriting and then deleting any file there called
`.root.pdfcraft-tmp`. `write_atomic` now refuses a folder before
writing anything;
- `image_save` adds an extension to a path without one, so "." became
`root.png` beside the root. It now refuses a folder first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Automation: files named after a document stay in the output folder
Found while reviewing the --root fixes for #136 and #137. Folder outputs
name their files after the document and joined that name straight onto the
resolved folder, without checking the result against the root:
- `doc_export_images` and `doc_export_all_images` used the whole document
name, so `doc_create` with `name: "../../escape"` followed by
`doc_export_images` wrote `escape_page_1.png` outside the root (an
absolute name went anywhere);
- `page_extract` (separate files), `doc_split`, `ocr_recognize_files` and
`action_run` took the last component, but on Windows a name such as
`x/C:evil.pdf` gives `C:evil`, and `folder.join("C:evil-page1.pdf")`
replaces the folder with a path relative to the current directory on C:.
Every such name now goes through `child()`, which turns separators,
colons and control characters into `_` (and prefixes a name of only dots),
so the file is always a direct child of the folder. `with_root` also
refuses a root that is not a folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>