Files
pdfcraft/crates/platform
FriedCerebrumandClaude Opus 5.5 78ec0e3920 fix(platform): a saved file keeps its hidden and system attributes on Windows (#4647)
* fix(platform): a saved file keeps its hidden and system attributes on Windows

Every save writes a staging file and renames it over the target, and on
Windows the renamed file keeps the staging file's attributes, not the
replaced file's: a hidden or system file came out of a save visible. The
staging file is now created with the replaced file's FILE_ATTRIBUTE_HIDDEN
and FILE_ATTRIBUTE_SYSTEM (std's OpenOptionsExt::attributes, no unsafe), so
it is hidden from its first moment when the target is, and plain when the
target is plain. A new file gets neither. Read-only targets are still
refused before staging; the archive bit is left to the system.

The test saves plain, hidden, system and hidden+system files through both
staging-name forms and checks the attributes on the staging file before the
flush and on the saved file after; it fails with the fix reverted.

Refs #4593

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(platform): on Windows a saved link takes the link's attributes, not its target's

A save replaces a symbolic link with a regular file, so the attributes it
keeps are the link's own. `metadata` followed the link, and a plain link to
a hidden file came out of a save as a hidden file. Read them with
`symlink_metadata`, as the macOS attribute copy already does. Unix keeps
reading the mode through the link, as before.

The test skips where symbolic links can't be created (no Developer Mode or
administrator rights), as it does on the machine this was written on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:00:14 -04:00
..

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 has no UI dependencies; macOS filesystem calls use the safe rustix wrapper.

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.

On macOS, write_atomically_with copies the existing regular destination's accessible extended attributes to the open staging file after writing the data and before flushing and renaming. Finder tags, comments, binary values and empty attributes therefore survive both incremental and full saves (#2658). Save As retains attributes of an existing destination; a new destination does not inherit metadata from the source PDF. Missing files and filesystems without xattr support keep working. The metadata read does not follow the destination's symlink or block on a FIFO. macOS manages the new file's com.apple.provenance; it is not copied. Kernel-hidden compression attributes are not requested.

Attribute copying is bounded: 64 KiB of names, 1024 attributes, 16 MiB per value and 64 MiB total. Each value must fill the initially queried size and retain that size in a second query after reading: a resource fork may return a partial prefix on growth rather than an error. A read/copy error or exceeded bound returns an error suggesting Save As; the original bytes and metadata stay intact and the staging file is removed. Flush errors retain the same guarantee. Tests cover exact metadata before flush and after repeated saves, empty/binary values, oversized resource forks, a simulated partial read of a growing fork and symlink isolation. Automation covers incremental/full saves, Save As and retaining an unsaved edit after metadata refusal. The staging writer also retains Unix permissions, the Windows hidden and system attributes (the staging file is created with them, so a hidden file is never published visible and a plain one never hidden, #4593) and, where supported, Windows/macOS creation dates; attribute copying runs before the creation date is restored. ACLs and hard-link identity are not preserved.