Skip to content

fix: finalize and recover recordings stored on exFAT drives - #2458

Open
AatmanAJ wants to merge 3 commits into
CapSoftware:mainfrom
AatmanAJ:fix/recovery-exfat-appledouble
Open

AatmanAJ wants to merge 3 commits into
CapSoftware:mainfrom
AatmanAJ:fix/recovery-exfat-appledouble

Conversation

@AatmanAJ

@AatmanAJ AatmanAJ commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2386

On macOS, a Studio recording saved to an exFAT drive (an SD card or most USB sticks) could not be finalized or recovered. Every attempt failed with "IO error: File exists (os error 17)", and retrying never helped.

Cause

exFAT can't store extended attributes, so macOS keeps them in a visible ._<name> AppleDouble file next to each file. Every file Cap writes has at least one attribute (com.apple.provenance), so every file gets a ._ companion.

Finalization copies the recording into a staging workspace with create_dir/create_new:

  1. The copy creates <name> in the workspace.
  2. The kernel immediately creates the workspace's ._<name> for it.
  3. The copy then reaches the source's own ._<name> and fails with EEXIST.

The same happens on every attempt. Here is the sequence on an exFAT disk image (macOS 27.0.1), copying with O_CREAT|O_EXCL in readdir order:

readdir:           ['a.txt', '._a.txt', 'b.bin', '._b.bin']
copied 'a.txt'  -> staged ['._a.txt', 'a.txt']
'._a.txt'       -> EEXIST

Fix

Skip ._ entries in these places:

  • the recovery copy
  • the integrity snapshot, so the source and its staged copy compare equal
  • the publication stamp, under a new receipt version 3. Receipts written as versions 1 and 2 keep their original rules, so a publication interrupted by an older build still reconciles. Original-media retirement now checks the receipt's own version instead of assuming version 2.
  • the preparing projection's two inventories

Cap never writes ._ names itself, so these entries are filesystem bookkeeping, not recording data. The kernel recreates them for the copies. Before this change, the preparing inventories also rejected every exFAT recording as having "unrepresented" files and fell back to the slower ordinary finalization.

Testing

  • New tests:

    • recovery_copies_and_snapshots_skip_apple_double_siblings and preparing_projection_ignores_apple_double_siblings. Both fail when is_apple_double always returns false.
    • only_v3_publication_stamps_leave_out_apple_double_entries fails if every stamp version skips ._ entries.
    • publication_receipts_reconcile_with_apple_double_entries_present covers receipt versions 1–3.
  • APFS: cargo test -p cap-recording --lib passes 566 tests and cargo test -p cap-recording --test recovery passes 50. cargo clippy -p cap-recording -- -D warnings is clean.

  • exFAT: a 2 GB disk image, with TMPDIR pointed at it so the test fixtures live there.

    • --lib recovery: 38 failures on main, 8 with this branch. Every File exists and every "Unrepresented … require ordinary finalization" is gone. The 8 remaining failures are test helpers that list or snapshot whole directories and count the sidecars, for example fs::read_dir(project).count() == 1.
    • --test recovery: 15 failures on main, 11 with this branch, and again every remaining one is a helper artifact.
      • End-to-end recoveries that failed on main and now pass include recover_after_simulated_crash_produces_playable_mp4_with_preserved_duration and recovery_and_finalize_retain_every_known_track_on_success.
      • One test goes the other way. legacy_optional_failure_does_not_bypass_retained_display_validation corrupts list_m4s_segments(..)[0], and on exFAT that entry is the sidecar ._segment_000.m4s rather than the fragment. Recovery now succeeds, correctly, on media that was never corrupted. On main the test only passed because recovery hit EEXIST first.
  • Hand-tested on macOS 27.0.1 (Apple silicon), recording to the exFAT disk image through CAP_GPUI_RECORDINGS_DIR:

Not covered

On the macOS 27 exFAT driver (FSKit), a directory created with mode 0700 reports 0700, so publication's private-ownership checks (recovery_publication_workspace, read_recovery_publication) pass. I couldn't check older macOS releases. If an older exFAT driver reports 0777 regardless, publication would fail next with "not privately owned".

RetriggerConfidence Score: 4/5

Preserve older publication hashes before merging so interrupted recordings remain recoverable after an upgrade.

Findings

  1. P1 Older recoveries get stuck ▶
  2. P2 Correct copies fail the test ▶
Fix with agent prompt
### Issue 1
crates/recording/src/recovery.rs:3150-3152
This filter changes the saved publication hash for both existing receipt versions. If an older build leaves an interrupted publication containing AppleDouble files-for example, a recording unpacked onto APFS-the new build computes different hashes and fails with “Conflicting recovery publication state; all files retained.” `RecoveryLock::acquire` then blocks recovery on every retry.

Add a new receipt version for the filtered hash. Preserve the old hash rules when reading versions 1 and 2.

### Issue 2
crates/recording/src/recovery.rs:6256-6257
These assertions require the destination to have no AppleDouble files, but macOS can create its own companions on exFAT. If the test’s temporary directory is on that volume, a correctly copied file can fail the test.

Check that the source companions were not copied rather than requiring destination companions to be absent. Allow filesystem-created companions and compare the filtered snapshots.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR skips AppleDouble files when copying recordings, checking snapshots, stamping publication state, and building preparing inventories.

  • Recording recovery skips exFAT sidecars while copying and checking files.
  • Preparing projection ignores exFAT sidecars in its file inventories.

Reviews (1) · Last reviewed commit: "fix(recording): finalize and recover rec..." · Reviewed by Greptile

On exFAT/FAT volumes macOS stores every file's extended attributes (each
file Cap writes gets com.apple.provenance) in a visible ._<name>
AppleDouble sibling, and creates the destination's sibling the moment a
copy creates <name>. Finalization copies the recording into a staging
workspace with create_dir/create_new, so the source's ._<name> always
collided with the one the kernel had just made: "IO error: File exists
(os error 17)" on every finalize and every recovery retry.

Skip ._ entries in the recovery copy, the integrity snapshot, the
publication stamp, and the preparing projection's inventories. Cap never
writes ._ names, so they are filesystem bookkeeping rather than
recording data, and the kernel recreates them for the copies. The
preparing inventories previously declined every exFAT recording and fell
back to ordinary finalization.

Fixes CapSoftware#2386
@AatmanAJ
AatmanAJ marked this pull request as ready for review October 11, 2026 09:02
Comment thread crates/recording/src/recovery.rs Outdated
Comment on lines +3150 to +3152
if !is_apple_double(&entry.file_name()) {
visit(root, &entry.path(), entries, version)?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Older recoveries get stuck

This filter changes the saved publication hash for both existing receipt versions. If an older build leaves an interrupted publication containing AppleDouble files—for example, a recording unpacked onto APFS—the new build computes different hashes and fails with “Conflicting recovery publication state; all files retained.” RecoveryLock::acquire then blocks recovery on every retry.

Add a new receipt version for the filtered hash. Preserve the old hash rules when reading versions 1 and 2.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/recovery.rs
Line: 3150-3152

Comment:
**Older recoveries get stuck**

This filter changes the saved publication hash for both existing receipt versions. If an older build leaves an interrupted publication containing AppleDouble files—for example, a recording unpacked onto APFS—the new build computes different hashes and fails with “Conflicting recovery publication state; all files retained.” `RecoveryLock::acquire` then blocks recovery on every retry.

Add a new receipt version for the filtered hash. Preserve the old hash rules when reading versions 1 and 2.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d17544b. Publication stamps now carry a V3 version, and only V3 leaves out ._ entries. Receipts that older builds wrote as V1 or V2 are still checked with their original rules, so an interrupted publication from an older build reconciles as before. Covered by only_v3_publication_stamps_leave_out_apple_double_entries and publication_receipts_reconcile_with_apple_double_entries_present (receipt versions 1, 2 and 3).

Comment thread crates/recording/src/recovery.rs Outdated
Comment on lines +6256 to +6257
assert!(!staged.join("segments/segment-0/._display.mp4").exists());
assert!(!staged.join("segments/._segment-0").exists());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Correct copies fail the test

These assertions require the destination to have no AppleDouble files, but macOS can create its own companions on exFAT. If the test’s temporary directory is on that volume, a correctly copied file can fail the test.

Check that the source companions were not copied rather than requiring destination companions to be absent. Allow filesystem-created companions and compare the filtered snapshots.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/recovery.rs
Line: 6256-6257

Comment:
**Correct copies fail the test**

These assertions require the destination to have no AppleDouble files, but macOS can create its own companions on exFAT. If the test’s temporary directory is on that volume, a correctly copied file can fail the test.

Check that the source companions were not copied rather than requiring destination companions to be absent. Allow filesystem-created companions and compare the filtered snapshots.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d17544b. The test now only checks that the source's AppleDouble bytes weren't copied to the destination, so companions macOS creates itself on an exFAT temp directory don't fail it.

Skipping ._ entries in the publication segments stamp changed the hash
for receipts already written as version 1 or 2. An interrupted
publication left by an older build on a tree containing AppleDouble
files would then fail reconciliation as a conflict and block every
later recovery attempt.

Write new receipts as version 3, the only stamp version that skips ._
entries, and keep hashing them for versions 1 and 2. Original-media
retirement now validates against the receipt's own stamp version
instead of assuming version 2.

The copy test also no longer requires the destination to have no ._
files: on exFAT the kernel creates its own companions for the copies,
so it now checks that the source's companions were not copied.
…Windows

File::open on a directory fails with Access denied on Windows, which broke
only_v3_publication_stamps_leave_out_apple_double_entries in the Windows
sync-tests job. Open it with FILE_FLAG_BACKUP_SEMANTICS and
FILE_WRITE_ATTRIBUTES so set_times can restore its mtime.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MacOS] Recovery fails with IO error: File exists (os error 17) and cannot be retried

1 participant