Follow-up to #319, from review. create_staging, staging_suffixes and STAGING_ATTEMPTS were copied in pdfcraft-automation and pdfcraft-ui-egui, which are both L7 and can't depend on each other. They now live once in the new pdfcraft-platform crate (L7, std only), which layers.rs already reserved with both sideways edges. A pure move: the body is unchanged apart from a StagingName parameter that keeps each caller's existing file name (.{name}.{suffix}.pdfcraft-tmp for automation, .{name}.pdfcraft-{suffix}.tmp for the desktop save). The doc comment on staging_suffixes now says the suffix is SipHash keyed from the OS random source: enough that a name can't be planted in advance, but not a CSPRNG. New tests in the platform crate pin both name forms, the 128-byte stem cut, the 16-name limit and refusal when no suffix is left. The callers' planted-file, link, folder and failed-rename tests stay where they are. Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
pdfcraft-platform
Layer L7 holds operating-system services that both the desktop app (pdfcraft-ui-egui) and the automation tools (pdfcraft-automation) need. Those two crates sit on the same layer and can't depend on each other, so a helper they share lives here instead of being copied into each (xtask/src/layers.rs allows both edges). It uses only the standard library and has no UI dependencies.
staging creates the temporary file an atomic save writes before renaming it over the target. The file is opened with create_new, so anything already at the name (a file, a hard link, a symbolic link even when dangling, a folder) is skipped and never written through. On Windows a folder at the name fails with "access denied" rather than "already exists", and is skipped only when something is actually there. Names carry an unpredictable 64-bit suffix from staging_suffixes (the standard library's keyed hash, currently SipHash-1-3, keyed from the operating system's random source), which stops a name being planted in advance but is not a CSPRNG; fixed suffixes are only for tests. At most 16 names are tried, and then the save is refused. The target's name is cut to 128 bytes on a character boundary so the staging name fits every system's limit. Each caller keeps its own name form (.{name}.{suffix}.pdfcraft-tmp for automation, .{name}.pdfcraft-{suffix}.tmp for the desktop save). Writing, closing the file before the rename, renaming and removing the staging file on failure stay with the caller.
Synthetic tests pin both name forms, the 128-byte cut, the attempt limit and refusal when no name is left. The planted-file, link, folder and failed-rename tests stay with each caller, where they exercise the whole save.