Commit Graph
107 Commits
Author SHA1 Message Date
aiongg 576ba0e02b Integration: track t12
# Conflicts:
#	contributors/people.toml
#	crates/compose/src/lib.rs
#	crates/compose/src/tests.rs
#	crates/engine/src/print_cmds.rs
#	crates/engine/src/print_cmds/tests.rs
#	docs/target-app-parity.md
2026-10-22 15:38:33 +00:00
aiongg 82cf3c828c Integration: track t01
# Conflicts:
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	docs/file-format-parity.md
#	docs/gaps.md
2026-10-22 15:38:14 +00:00
aiongg 82df3f7fe6 Merge PR #8034: fix(format): make Windows native-save publication durable
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.
2026-10-22 02:28:02 +00:00
aiongg 2c2269ff0b Merge PR #5421: fix(format): serialise directory bundle saves with a bundle-wide lock
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).
2026-10-22 02:24:47 +00:00
aiongg bf1000302d Merge PR #8367: Layers: per-layer XMP metadata (layer.xmpMetadata), stored as the reference app stores it
docs/file-format-parity.md: kept main's PSD row (45 smart filters) and added the PR's layer XMP packet phrase.
2026-10-22 02:23:45 +00:00
aiongg 45f556e677 Merge PR #5877: fix(format): ignore accidental ZIP directory signatures in comments 2026-10-22 02:23:35 +00:00
aiongg b0833a893c Merge PR #7694: perf(format): reuse .pcraft writers between document saves 2026-10-21 21:56:10 +00:00
aiongg d71ff0d34d Merge PR #7580: Preserve saved PCraft previews when manifest replacement fails 2026-10-21 21:55:49 +00:00
denstu473 c82674439f fix(ci): run native Windows tests for format changes
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.
2026-10-21 23:15:43 +02:00
Tom InglingandClaude Opus 5.5 052c1c3ab7 Layers: per-layer XMP metadata, as Photoshop's layer.xmpMetadata
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>
2026-10-21 17:03:02 -04:00
aiongg 94d28535d3 Merge PR #6464: Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215) 2026-10-21 18:15:02 +00:00
Rene Leonhardt aa715535f3 perf(format): reuse save cache ownership safely
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.
2026-10-21 17:49:09 +02:00
aiongg 77557d75f1 Merge PR #8229: Integration batch: 8 PRs on main
# Conflicts:
#	crates/engine/src/layer_style.rs
#	crates/io/src/psd_export.rs
#	crates/io/src/smart_map.rs
#	docs/file-format-parity.md
2026-10-21 15:45:07 +00:00
David Mann a492716367 Merge PR #5990: Bevel & Emboss: keep the Gloss Contour's Anti-aliased (antialiasGloss) 2026-10-21 11:29:05 +02:00
aiongg fc371711e6 Merge PR #5847: Shapes: Density and Feather on shape layers (#5846)
# Conflicts:
#	docs/scorecard.md
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-10-21 05:04:03 +00:00
0073b395d4 Integration: merge main, resolve conflicts (#5990)
docs/file-format-parity.md: main added three 2026-10-15 revision rows
(DDS save, PNG sTER, PSD guides) where the PR added its own; keep all four.

Co-authored-by: varanus-salvator <262257003+varanus-salvator@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:50:13 +02:00
David Mann 2adfe0c4e2 Merge PR #5807: Type: keep superscript and subscript when a text layer is re-rendered (#5645) 2026-10-20 22:39:12 +02:00
denstu473 851ca348a7 fix(format): persist Windows save publication before acknowledgement
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.
2026-10-20 21:24:23 +02:00
David Mann 0b39361076 Merge main into feat/tool-214-12-shape-feather 2026-10-20 17:12:29 +02:00
Brandon ThomasandClaude Opus 5.5 8ede07b096 Format: build on FreeBSD (rustix has no xattr calls there)
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>
2026-10-20 08:47:42 -04:00
00xJ0 abd3755d41 RecoveryStore::flush keeps each entry's key, so a later discard still removes it (#6464 review) 2026-10-19 12:05:01 -07:00
Rene Leonhardt 05bd004bc4 perf(format): reuse native save writers per document
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.
2026-10-19 16:58:35 +02:00
Rene Leonhardt f64a143c55 perf(format): drop unhelpful Zstandard workspace reuse 2026-10-19 13:43:20 +02:00
Rene Leonhardt 02a63c94c2 perf(format): Reuse Rust Zstandard workspaces
Keep fresh compression for partial blocks and verify independent frames against the previous encoder. Record the isolated allocation-reuse effect.
2026-10-19 11:31:59 +02:00
00xJ0 7bb7819b00 Lost window device: close only once every unsaved document is saved for recovery, retrying every 2 s; also check the device on the screen-picker frame (#6464 review) 2026-10-18 17:15:13 -07:00
skylar taylor-barrick 6cec0cab24 Save: a read-only file is refused, so its metadata test asserts that
main's 589941a5 (#6600) refuses to replace a read-only file before anything
is written. This branch's test wrote to a 0o444 file and expected the save to
succeed, so the merge of the two failed on Linux:

    cannot save .../doc.psd: the file is read-only; the original file was
    not changed

No bug on either side: the two changes disagreed about what a read-only save
should do, and main's answer is the deliberate one. The test now asserts the
refusal, that the original is untouched, and that the attribute and mode survive
— which is what it set out to check. The writable cases alongside it still cover
the metadata copy itself.

The windows-latest failure was the roll-up gate reporting this same failure, not
a second one.
2026-10-18 16:24:03 -06:00
jonathnwxh 86b4c67adf Expect post-commit best-effort preview cleanup on Unix 2026-10-19 05:37:19 +08:00
jonathnwxh 9c927993b5 Preserve omitted PCraft previews until manifest commit 2026-10-19 05:29:30 +08:00
Finn NewickandClaude Opus 5.5 e795cabc79 test(format): skip the bundle-lock test only where file locks are unsupported
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:16:37 +01:00
skylar taylor-barrick 915779b948 Merge storytold/main into save-replace-metadata
No textual conflicts, but the merge did not compile. main's 589941a5 added a
second AtomicFs test impl using copy_permissions, while this branch renames that
trait method to copy_metadata. Git merged both cleanly because they sit in
different parts of the file, giving a tree whose trait and impl disagree:
E0407, E0046 and E0599 in photocraft-format's lib test. That one error is what
failed all four CI jobs.

main's new impl is renamed to copy_metadata to match.

Verified with --features heif, which is what CI builds with and what I had
previously failed to pass locally: clippy --workspace --all-targets --locked
--features heif -- -D warnings is clean, and the workspace tests pass.
2026-10-18 15:12:41 -06:00
0807441e52 Make .pcraft reload f64 coordinates bit-exact (#7498)
Found while testing crates/format/Cargo.toml:19 (serde_json features), used for the manifest in crates/format/src/manifest.rs and store.rs

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 16:56:48 -04:00
Finn Newick 1e8dcb53b9 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock 2026-10-18 21:21:28 +01:00
00xJ0 4615a8bc26 Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215)
When the window's own wgpu device was lost (a driver reset, e.g. after sleep), egui-wgpu could
not draw another frame: a frame with shapes panicked inside it ("Failed to create staging buffer
for index data", #1215, exit 101, unsaved work lost), and a frozen window's close button opened
an unsaved-changes prompt nobody could see (#3438). The #243 fallback only covered the canvas.

- DeviceHealth::device_lost (wgpu reported the loss, not a recorded fault) and ::probe (a
  non-blocking poll that keeps running after a recorded fault).
- RecoveryStore::flush: finish queued autosaves and wait for them.
- gpu_status::check_window, at the end of every frame: once the window's device is lost the
  frame draws nothing; the first time every unsaved document is written to crash recovery
  (Services::flush_autosaves) and the app closes without asking. The next launch reopens them.
- The app watches the window device on the CPU canvas too; ui.gpu.simulateLoss {destroy: true}
  destroys the real device to drive the path.

Verified on an RX 6900 XT, Vulkan and DX12: destroying the device in the running app exited 101
with the egui-wgpu panic before, and now exits 0 with the document recovered on the next launch.
2026-10-18 12:53:48 -07:00
aiongg da96d71506 Merge PR #6863: Native format: refuse a .pcraft with out-of-range numbers when it is opened (#6862) 2026-10-18 13:22:58 +00:00
cc772ad9dc Saving onto a dangling symlink writes through to its target (#6605) (#6649)
* fix(format): saving onto a dangling symlink writes through to its target (#6605)

resolve_symlink fell back to the link path when canonicalize failed, so an
atomic save renamed over the symlink and replaced it with a regular file.
Dangling chains now resolve lexically (relative targets included, with a
depth cap), so the bytes land at the target and the link survives; a
symlink loop is a clear error instead of a silent link replacement.

* Follow dangling links without folding .. by text

A folder on the way may itself be a symlink, so leave .. for the OS to resolve.

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

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:35:35 -04:00
589941a536 Save: refuse to replace a read-only file (#6600) (#6643)
Saving writes a temporary file and renames it over the destination. A
rename only needs the folder to be writable, so on Linux and macOS a
read-only (chmod 444) file was silently replaced, while on Windows the
rename failed with a vague "locked or read-only" access error.

atomic_write now asks the AtomicFs whether the existing destination is
read-only (new `is_readonly`, false by default for other implementors)
and refuses before anything is written: "cannot save <file>: the file
is read-only; the original file was not changed". New files and
writable files save as before. Photoshop refuses the same way; the
user saves a copy with Save As.

Tests: a fake AtomicFs reporting read-only gets the error with nothing
written or renamed; a real file marked read-only keeps its bytes and
mode and gets the clear message on every platform.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 11:55:43 -06:00
skylar taylor-barrick 8c12449819 Save: keep the replaced file's metadata (#6603, #7073, #7094, #7095, #7096)
Saving writes a temporary file and renames it over the target, so everything
attached to the old file went with the file that was unlinked. Several of those
are security decisions, so a save quietly undid them: a download lost its Mark
of the Web, an explicit ACL fell back to whatever the folder hands down.

New crates/format/src/atomic/meta.rs, reached from atomic_write through two
AtomicFs methods (copy_permissions became copy_metadata; replace_existing is new
and defaults to rename, so the failure-injecting test file systems are
unchanged):

- Extended attributes (#6603): flistxattr/fgetxattr/fsetxattr through safe
  rustix, per attribute, best effort. macOS Finder tags, com.apple.quarantine
  (the Mark of the Web's macOS counterpart) and Linux user.* all ride along.
  Attributes the system owns refuse to be written and are skipped;
  com.apple.decmpfs is skipped deliberately, since it describes storage the new
  bytes do not have.
- macOS ACL (#7073): a new macOS-only exacl dependency (MIT).
  fcopyfile(COPYFILE_ACL) is an unsafe fn and fclonefileat(CLONE_ACL) is
  APFS-only and would mean writing every save as clone-then-truncate; the
  reasoning is in docs/decisions.md. A file with no ACL is left alone.
- Windows (#7094, #7096): ReplaceFileW through winsafe's kernel feature (MIT,
  no dependencies, a safe wrapper; the workspace forbids writing the FFI call
  here). One call keeps the destination's DACL, its named streams including
  Zone.Identifier, and its creation time.
- Automation write path (#7095): AuthorizedWorkspace::write copies the replaced
  file's attributes through the two cap-std descriptors, never a path, so a
  request path still cannot escape the write root. Permissions there remain
  #4963; ReplaceFileW is path-based and so is not used on that path.

The order inside copy_metadata matters: attributes and ACL first, permission
bits last. Writing an attribute needs the destination open for writing, so
copying a 0444 mode onto the temporary file first locks you out of your own
temporary file. Covered by a regression test.

Atomicity is not weakened. The metadata step runs on the temporary file before
the swap, never touches bytes, and never fails a save. Windows is the one place
the swap itself changed, and a backup file name is passed because it is a
correctness requirement, not an option: ReplaceFileW without one documents
ERROR_UNABLE_TO_MOVE_REPLACEMENT as leaving the replaced file deleted and the
replacement under its temporary name, which is a failed save that loses the
user's document. With a backup name the same failure leaves both files alone.
ERROR_UNABLE_TO_MOVE_REPLACEMENT_2, the one error that does move things, is
unwound by hand; every other error falls back to the plain rename, i.e. to the
previous behaviour.

THE WINDOWS PATHS ARE UNVERIFIED ON WINDOWS. They were written and reviewed on
a Mac. The #[cfg(windows)] tests -- Zone.Identifier, a second named stream,
icacls output, creation time -- have never executed. They do compile and pass
clippy -D warnings against x86_64-pc-windows-msvc, which is as far as a Mac
gets. CI's Windows job is the first real run, and #7094 in particular (Mark of
the Web surviving a replace) is the whole point of the change and is the part
no one has observed working.

Tested on macOS: a real chmod +a entry and a real Finder tag both survive a
save (checked with ls -le and xattr, and as unit tests), plus a 64 KiB
attribute value, a read-only file, the mode, and the automation path.

Still open: Linux POSIX ACLs are not carried over (system.posix_acl_access is
not reported by listxattr, so it needs its own step; not part of these issues),
and macOS creation time is still reset by a save (that half of #6603 needs
setattrlist, which has no safe wrapper here).
2026-10-17 10:43:09 -06:00
David MannandClaude Opus 5.5 921637322c Native format: refuse a .pcraft with out-of-range numbers when it is opened (#6862)
A number past f32's range (e.g. opacity 1e39) decoded as infinity, so the document opened but
could never be saved again; read_manifest now refuses it as corrupt, naming the field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:30:16 +02:00
Finn Newick 82aa300b51 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock 2026-10-16 21:34:43 +01:00
vangel76andClaude Opus 5.5 489feb2333 Tests: pcraft keeps text run baseline; tighten sub/superscript size check (#5645)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:57:15 +02:00
David Mann ce19e018b9 Merge remote-tracking branch 'origin/main' into feat/tool-214-12-shape-feather
# Conflicts:
#	docs/ui-parity.md
2026-10-15 23:52:29 +02:00
b24c0360f1 Dependencies: require ruzstd 0.9.1 for RUSTSEC-2026-0344 (#4952) (#5225)
ruzstd before 0.9.1 lets a crafted zstd input under 128 KB allocate
several gigabytes at once. PhotoCraft decodes zstd when it opens .pcraft
and Affinity files, so such a file could end the process with an
out-of-memory abort.

Raise the minimum in both crates that use it and update the lockfile
from 0.9.0 to 0.9.1.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:37:56 -06:00
Jason Xiang d3f6b384ac Merge upstream main into fix/zip-comment-directory
Include upstream formatting, ui.set schema, and web packaging CI fixes.
Preserve the focused feature changes and regression tests.
2026-10-15 02:15:16 -07:00
varanus-salvatorandClaude Opus 5.5 92aa5047cd Bevel & Emboss: keep the Gloss Contour's Anti-aliased (antialiasGloss)
Photoshop stores Bevel & Emboss › Gloss Contour › Anti-aliased as
`antialiasGloss` in the effect descriptor. The document model had no
field for it, so import dropped it and export regenerated the bevel
without it: once a bevel was edited (the Layer Style dialog re-applies
all its settings), a saved PSD reopened in Photoshop with the gloss
contour aliased.

Add `Bevel::gloss_anti_alias` (serde default, so older .pcraft files
load), read and write `antialiasGloss`, accept `glossAntiAlias` in
layer.layerStyle.bevelEmboss and show the Anti-aliased checkbox next to
the Gloss Contour in the dialog, as Photoshop does. Rendering is
unchanged: contour anti-aliasing is not rendered for any effect yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:31:59 +07:00
David MannandClaude Opus 5.5 6bb45c8d5b Shapes: Density and Feather on shape layers (#5846)
A shape layer's path is its vector mask in Photoshop (Properties > Masks),
with its own vector-mask density and feather. PhotoCraft's ShapeLayer had
neither, so soft shapes could not be made, and stroked soft shapes from PSDs
lost both on import.

- doc: ShapeLayer gains `density` (0..1, default 1) and `feather` (px,
  default 0).
- vector: the shape renderer applies them to the fill as a soft vector mask
  (fill shows beyond the path at 1 - density; the feather is the vector
  mask's Gaussian), so the shape's own pixels, the GPU (which samples them)
  and the stroke/fill split all follow. The stroke stays crisp. The feather
  blur moves from compose::masks to vector::feather and both share it and the
  feathered area, so a shape and an equal vector mask render the same pixels.
- compose: soft shapes' bounds reach their fill, their layer effects follow
  the soft alpha, and the split cache key includes density and feather.
- engine: `layer.vectorMask.edit` on a shape layer sets density/feather in one
  history step (path/enabled/linked/invert are refused there); density is now
  validated (finite 0..100) instead of clamped. `layer.vectorMask.info`
  reports a shape's values.
- ui: Properties shows the Vector Mask Density and Feather sliders for a
  selected shape layer in both themes (live drag, one undo per gesture).
- .pcraft: manifest and convert carry both with serde defaults (older files
  load at 100 % and 0 px).
- PSD: export writes them as the vector density/feather mask parameters;
  import reads them into the shape (re-rendered, as the stored pixels lack the
  fill beyond the path). Soft unstroked solid/gradient shapes now import as
  shapes instead of fill layers with a vector mask; pattern-filled ones stay
  fill layers.

Photoshop's 10 px feather on a 100 px black square, row 100, x = 30..72:
ours is within 1 level of every measured value (8, 16 and 32 bits);
density 50 % shows 127/128 beyond the path and 0 inside.

Scorecard: TOOL-214-12 done.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 03:07:46 +02:00
Finn Newick d770fd35f1 Merge remote-tracking branch 'origin/main' into fix/bundle-save-lock
# Conflicts:
#	crates/format/tests/roundtrip.rs
2026-10-14 23:12:45 +01:00
57d488032c fix(doc): variable definitions follow layer id remapping on copy and load (#5023)
* fix(doc): variable definitions follow layer id remapping on copy and load

A duplicated document (image.duplicate, history.newDocument) or a .pcraft
loaded with remapped ids assigned fresh layer ids but cloned VariableDef.layer
unchanged, so datasets silently drove no layer (or, after remap, a colliding
one). Remap variable targets through the same id map as layer comps and
slices; a dangling target keeps a non-colliding placeholder. A merged-only
duplicate drops the whole variable binding set, matching how it drops comps.

Closes #4969

* style: cargo fmt duplicate_variables test

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
2026-10-14 15:02:43 -07:00
Jason Xiang 3264c2fa6f fix(format): ignore accidental ZIP directory signatures in comments 2026-10-14 12:08:10 -07:00
MarekSpodBiedryandGitHub ed7df56a24 fix(format): recover autosaves with missing sidecars
Discover committed autosave bundles whose JSON sidecar was never published. Rebuild recovery metadata from the readable manifest and retain these bundles during orphan cleanup.

Fixes #4967
2026-10-14 02:02:34 -06:00
Finn NewickandClaude Opus 5.5 d658c13c4e fix(format): serialise directory bundle saves with a bundle-wide lock
A directory save lists the existing objects, writes its own, publishes its manifest and removes what that manifest doesn't reference. Two writers (two stores, or two processes) saving the same bundle could interleave, and one writer's cleanup could remove an object the other's winning manifest needs. Each save now holds an OS advisory lock on save.lock for its whole transaction; a held lock is waited for (10 s by default, PcraftWriter::set_lock_wait), then the save fails without writing. A crash releases the lock; a file system without file locks saves unlocked as before. Fixes #4957.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:12:49 +01:00