Conflicts with main's batch #9880:
- psd file.rs / layer.rs: #5287 (PSB by encoded size, RLE tables widened on
promotion) against #6583's lazy reader. Main's new widening and length code
read `.data`, which #6583 leaves empty while the bytes stay in the source;
it now goes through `encoded()` / `encoded_len()` (also the auto-merged
lines in `rle_widening`, `encoded_len` and `LayerRecord::write`), and the
lazy reader sets `write_as_psb: false`. New test
`a_lazy_psd_promotes_to_the_same_psb_as_an_eager_one`: a lazily read PSD
writes the same PSB bytes as an eager one; it fails with a `.data` read.
- direct_select.rs / vector_ui.rs: #9321's Type Path and #6353's hidden
Work Path both kept.
- io tests main.rs: both modules. scorecard.md regenerated (12 of 181);
ui-parity.md and gaps.md follow it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A ZIP save encoded the thumbnail and the 1024 px composite preview as PNG
on the calling thread before any tile was compressed. The composite alone
is a ~200 ms single-threaded deflate on a 36 MP document, about 40 % of a
full save. The previews are now encoded on their own threads while the
document is collected and its new objects are compressed on all cores;
the bundle is assembled afterwards in the same order, so the bytes are
unchanged. Directory saves encode them up front as before. wasm stays
sequential.
bench_export, 36 MP, 3 layers, 16 interleaved rounds of 7 reps (median
of medians / min), Ryzen 16 threads, Linux:
.pcraft 517.9 / 471.1 ms -> 319.3 / 287.8 ms (-38 %)
.pcraft with cache 360.1 / 334.1 ms -> 277.5 / 268.3 ms (-23 %)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: resolve SonarCloud maintainability issues
* fix: format imports and fix effect_entry argument type
* fix: restore test closure and wildcard import that broke compilation
* refactor: split composite_to_surface to reduce cognitive complexity
CodeRabbit (discussion_r4370498176, two findings):
- copy_metadata copied the ACL before the creation time; an ACL that
denies its owner write access, copied onto the temporary file first,
made the creation-time step fail to open it. The time goes first now.
- AuthorizedWorkspace::write (the automation save path) copied only the
extended attributes, so an automation save reset Finder's Created date.
It now also copies the creation time through the same capability-checked
handles, with the new descriptor-based meta::copy_created_file.
Tests: keeps_the_creation_time_under_a_deny_write_acl,
write_keeps_the_replaced_files_creation_time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving writes a temporary file and renames it over the original, so on
macOS the saved file was born at the time of the save and Finder's
"Created" date moved forward. Photoshop keeps it.
`copy_metadata` now copies the replaced file's creation time onto the
temporary file before the rename, with std's `FileTimesExt::set_created`
(stable since Rust 1.75). Best effort, like the other metadata steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Autosave: a recovery listing judges each key once, so an autosave in progress isn't listed twice
* Autosave: the recovery listing builds for the browser too
On Windows the recovery sidecar was written through store::write_atomic, which replaces the
old file with ReplaceFileW: it moves the old sidecar to a backup name first and only then moves
the new one in. A recovery listing in between saw the bundle without its sidecar and listed the
entry twice, or not at all (so a recovery prompt could miss a document). The flaky
autosave::recovered_entries_survive_a_second_crash_until_saved_or_closed on Windows CI was this.
The sidecar now goes through one AtomicFs on every platform whose replace is a plain rename
(one step); Unix keeps its 0600 temp file, Windows still refuses a read-only sidecar up front.
Recovery data is private to the app, so ReplaceFileW's ACL/stream preservation isn't needed.
Test: a_listing_during_an_autosave_sees_the_entry_exactly_once lists the recovery folder while
an autosaver rewrites the entry 200 times. With the old code it failed 3 of 3 runs on Windows
(two entries, once none); with the fix it passes 3 of 3.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(format): report a failed directory flush after a save instead of ignoring it
atomic_write and the bundle writer discarded the directory fsync result once the rename had published the file, so a save whose rename might not survive a power loss reported success and its recovery data could be discarded. A real flush failure is now returned (the file is in place, the message says the save may not last), so callers keep the document unsaved and its recovery data, and a bundle save stops before removing objects the old manifest needs. A file system that can't flush directories at all (EINVAL, ENOTSUP, Unsupported) still counts as saved. Fixes#4968.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(format): a folder that can't be opened for a flush is not a failed save
A write-and-search-only folder (no read permission) can't be opened to fsync it, so no flush can be attempted there. Treating that open failure as a failed flush made saves into such folders fail (the directory_symlinks permission tests on Linux). Only a flush that is attempted and fails is reported now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(format): flush bundle folders like atomic_write, and test a failed manifest flush
The bundle writer flushed the folder with sync_all, which on Apple
platforms is F_FULLFSYNC and fails on SMB shares; it now goes through the
same RealFs::sync_dir as atomic_write (sync_file's plain-fsync fallback,
#1336). open_dir_for_sync and dir_synced are crate-private. A test-only
hook fails the manifest's folder flush: the save fails, and the objects
the previous manifest references stay until the next save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(format): keep stale previews until the manifest commits; errno 45 only where it is ENOTSUP
A directory save removed a thumbnail or composite the new document no
longer has before writing the new manifest, so a failed manifest flush
left the previous manifest naming a preview that was gone. They are now
removed after the manifest commits, best-effort like unreferenced
objects; the failed-flush test checks the thumbnail survives.
errno 45 counted as "can't flush directories" on every Unix, but it is
ENOTSUP only on Apple platforms and the BSDs (illumos: EDEADLK). Other
Unix targets now accept only EINVAL and ErrorKind::Unsupported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(format): flush the object folders before every manifest commit
When an object's folder flush failed, a retried save found the object
already written, reused it without flushing its folder, and then
committed a manifest naming it: success was reported while the object's
directory entry was still unconfirmed. Each directory save now flushes
tiles/ and blobs/ once before writing the manifest, so a reused object is
confirmed too. The flush error is shared with write_bundle_file
(confirm_flushed), and a test retries a one-tile save whose tile folder
flush failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(format): keep stale-preview removal before the manifest commit, as main requires
f77ffa3e moved stale-preview removal after the manifest commit. That
broke the hardening in 2d5a7497 (#4957), whose test
denied_stale_preview_cleanup_preserves_the_previous_manifest requires a
committed manifest never to sit beside a stale preview, so a failed
removal must fail the save. Restore main's ordering; the object-folder
flush and the failed-flush tests stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(format): flush the composite folder after removing a stale composite
The stale composite/preview.png was removed before the manifest commit,
but nothing flushed composite/, so a crash could bring it back beside
the new manifest. The folder is now flushed when a stale composite was
removed (the thumbnail lives in the bundle root, flushed with the
manifest).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Review pointed out that READ_CHUNK bounds each read but not the total
that photocraft_format::read_file returns, so a regular linked file that
keeps growing could keep a PSD export from finishing, and growth past
the stat length grew the buffer with infallible allocations. #8601 asks
for a MAX_OPEN_BYTES limit on the shared reader, not only on the PSD
call site.
read_all_with, which backs both read_file and read_all, now:
- fails with FileTooLarge once a reader has given more than
MAX_OPEN_BYTES (256 GiB), or up front when the stat length is already
over it, so a file over the limit is never embedded truncated;
- grows the buffer past the stat length only after a probe read finds
more bytes, and with try_reserve, so a file that outgrows memory is an
OutOfMemory error and a file read exactly to its length is not
over-allocated.
The limit sits far above any document memory can hold, so opening large
PSB files is unchanged. The non-regular-file rejection is unchanged.
Tests: read::tests::reads_stop_at_the_total_limit (a file over the
limit, readers over it with any hint, a hint over it, an endless reader)
and read::tests::an_exact_hint_is_not_over_allocated.
- crates/engine/src/tests.rs: kept both tests (main's restore_layer_selection test and the PR's history_states_share_large_document_payloads).
- crates/io/src/psd_import.rs: kept main's parsed Duotone inks (duotone_map) with the PR's Arc<[u8]> psd_raw; also main's unknown-adjustment-key import (#6575) now builds Arc<[u8]> raw.
- crates/io/tests/adjust_warnings.rs: moved to tests/it/adjust_warnings.rs on main; PR's .into() edits applied there, plus #6322's Color Lookup test (raw.as_slice().into()).
- docs/architecture.md: union; status line names both changes, both revision-history rows kept.
- Adapted main-new sites to the PR's types: psd_export.rs (Duotone write-back via as_deref/to_vec, Unsupported block compare by slice), inspect.rs and ui-egui unsupported_marker_tests.rs (raw .into()), io tests/it/psd_unknown_adjustment.rs (slice compare).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
crates/format/src/store.rs: the Windows tiles/blobs directory flush goes before main's manifest write (main publishes the manifest before removing stale previews), and that write takes the PR's fs argument; the PR's second manifest write is dropped.
crates/format/src/store.rs: kept both field sets in PcraftWriter::default (main's blob_hash_cache/compressed_budget and the PR's lock_wait) and both setters (set_compressed_cache_budget, set_lock_wait).
Include the native format crate in Windows platform selection so publication barriers and retry regressions execute in the unit and integration CI legs. Document the durability helpers and regression tests flagged by review.
Validated 20 platform-selection scenarios and all 130 native Windows format tests, plus formatting, Clippy, layering and the format WebAssembly check.
A layer keeps its own XMP packet (Layer::xmp), where scripts and embedding
apps store data about the layer under their own namespaces.
- PSD/PSB/layered TIFF: read from and written to shmd > cust > layerXMP
(UTF-16, NUL-terminated, after layerTime), laid out as Photoshop 2026
writes it; an unchanged Photoshop block round-trips byte for byte.
- .pcraft: optional `xmp` on each layer.
- layer.xmpMetadata {layer?, rawData?}: read, set (one undo step) or
remove with null; refuses malformed XML, DTDs, NUL and > 4 MiB.
- Duplicate Layer copies the packet; document.inspect reports hasXmp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move cached keys and compressed buffers between saves, and compare weak
allocation pointers without temporary strong-reference updates. Keep
payload budgets and archive bytes unchanged.
Allow fallible tile edits after a cached save. Move pixel storage when
only weak references remain; use a fallible copy for shared storage and
restore the original tile if the copy fails.
Cover cache key reuse, edits after saves at every depth, and weak/shared
tile ownership. Name the fixed revisions behind the existing timings.
Reserve complete bundles and directories from compressed sizes, and borrow
UTF-8 entry paths in the lookup index. Preserve lossy names and first-entry
lookup for duplicates.
Check ZIP field lengths before writing, bound metadata reservations, and
cover byte equivalence, overflow and incremental saves. ZIP entry paths
have a format limit independent of filesystem path constraints.
Flush NTFS directory publication after replacing a saved file. Report a
failed barrier as uncertain durability instead of successful Save. Flush
directory-bundle objects before the manifest, including on same-writer
retries, and stop before garbage collection when publication fails.
Add regression tests for failed barriers, retained old objects, cached
object retries, and directory handles. Keep existing Windows metadata
preservation and the separate Unix flush-error policy.
Validation: 130 format tests, 657 I/O corpus tests, clippy, layers, wasm,
12 acknowledged-save power cuts, six publication/cleanup cuts, and 1,368
native fault cases with exact archive comparisons. Existing preview-only
failure ordering remains tracked by #7548/#7549.
Fixes#7858.
Stop after one byte beyond the declared output length. This avoids expanding a forged entry up to the caller’s larger budget.
Add tests for surplus output, empty streams and truncated endings. Clarify that the backend timings do not measure the ZIP changes.
Decode archive slices directly to remove a 32 KiB input allocation and its compressed-data copies. Preserve size limits and CRC checks, with an equivalence and corruption test. Remove the unused synthetic timing test and refresh the performance notes with public x86 evidence.
The 0.11.0 release's FreeBSD job failed: copy_xattrs used rustix's
fsetxattr/flistxattr/fgetxattr, which rustix offers only on Linux,
Android and Apple systems. The real copy is now gated to those, and
other Unix systems get a no-op copy_xattrs, as Windows has. The xattr
tests are gated the same way; the Unix mode test now also checks that
no temporary file is left behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse unchanged tile and blob hashes and bound retained compressed payloads. Carry the writer through native save settings while preserving save identity and exact output.