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
The writer thread now asserts each take_completed() result, so an earlier failed write
can't hide behind flush()'s last result, and a drop guard sets the done flag however the
thread ends, so a writer panic is reported by join instead of the listing loop spinning.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Main now links each crate's integration tests into one tests/it binary;
the two new test files of this PR join it as modules.
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/ui-egui/src/layer_style.rs: values_of keeps the PR's method/smoothness keys and source-stop ends, and main's (#7072) unconditional From/To and geometry fields.
- crates/compose/src/cmyk_tests.rs (no textual conflict): add the PR's new `interpolation: None` field to main's CMYK test GradientOverlay literal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 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>
Main merged #5940, #6472, #7838 and #7922 directly. Kept from the batch:
the snapshot/History-follow identity at the history limit (#5940,
c7ef3676a; main still had the 3-tuple HistorySpot) and the strokeless
shape at a 0 px width step (#6472, 962539607; main still added the
stroke). Main already carries the other two fix-ups (34c9d0ea4, dff8bfd0e).
Overlaps with main resolved:
- #5842 vs main's #5568: one path, apply_color, tool-gated as #5842
has it, with #5568's float fill, kept fill opacity and lock skip;
shape_fill_ui's duplicate foreground_changed/gesture dropped; #5568's
tests run with a shape tool.
- #6230 vs main's #5180: one Type tool path click (path_target), no
second type.attachToPath; vertical type does not start type on a path,
as main has it; main's path_at/path_cursor and canvas arm dropped.
- #5452 moved the shape swatch to shape_paint_ui.rs: main's hex field
(#6201) ported there.
- type_extra_cmds::with_text takes main's dpi form, pub(crate) for
type_area_cmds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.
These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
Conflicts, both sides kept: main's .act swatches next to the batch's .asl
styles (Open filters, preset extensions), main's Render smart filters next
to the batch's Distort ones (53 mapped), parity doc status lines.
docs/scorecard.md regenerated.
Follow-ups the merge needed: the Distort smart filter readers pass their
enum type to main's stricter enum_name (the type their writers use), and
the CMYK effect tests set main's new StrokeFx overprint and Bevel
gloss_anti_alias.
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).
Integration: resolve with main for area type (#6230)
- Image Size with a new resolution (#7124, canvas_geom::set_resolution) now scales an area type's
path with the text space like a box or a type path, so the same words wrap onto the same lines;
test a_resolution_change_keeps_area_type_wrapping_where_it_was fails without it.
- The Paragraph panel's justify buttons (#7713) stay available on area type, which is paragraph
text in Photoshop; test justify_buttons_work_on_area_type.
- Test files: main's tests plus #6230's; layout.rs imports joined; docs and scorecard regenerated.
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.