Skip to content

Data.write: adds support for the combination of .atomic & .withoutOverwriting, instead of crashing (#1098)#2011

Open
wadetregaskis wants to merge 1 commit into
swiftlang:mainfrom
wadetregaskis:atomicWithoutOverwriting
Open

Data.write: adds support for the combination of .atomic & .withoutOverwriting, instead of crashing (#1098)#2011
wadetregaskis wants to merge 1 commit into
swiftlang:mainfrom
wadetregaskis:atomicWithoutOverwriting

Conversation

@wadetregaskis

Copy link
Copy Markdown
Contributor

Motivation:

Prior to this patch, Data.write(to:options:) called fatalError when both .atomic and .withoutOverwriting were passed together. That combination worked in Swift 5.10 and is documented as supported (Apple's Foundation maps it to "write to aux file with O_EXCL, then exchange"), so trapping is not just innately bad but also a source-breaking regression.

Modifications:

  • POSIX: after writing the auxiliary file, link is used to map it under the final name too. link fails with EEXIST if the destination already exists, which is exactly the contract of .withoutOverwriting. And whether successful or not, the original name is unlinked (which leaves only the intended final name referring to the file, or deletes the file if the link failed).
  • Windows: in SetFileInformationByHandle, FILE_RENAME_FLAG_REPLACE_IF_EXISTS / MOVEFILE_REPLACE_EXISTING are dropped if .withoutOverwriting is specified, and the ERROR_FILE_EXISTS & ERROR_ALREADY_EXISTS errors are mapped to Cocoa's fileWriteFileExists.

Result:

Writing atomically without overwriting now works [again].

Testing:

New unit test added.

…Overwriting`, instead of crashing (swiftlang#1098).

Prior to this patch, `Data.write(to:options:)` called `fatalError` when both `.atomic` and `.withoutOverwriting` were passed together.  That combination worked in Swift 5.10 and is documented as supported (Apple's Foundation maps it to "write to aux file with O_EXCL, then exchange"), so trapping is a source-breaking regression.

The implementation details depend on platform:

  - POSIX: after writing the auxiliary file, `link` is used to map it under the final name too.  `link` fails with `EEXIST` if the destination already exists, which is exactly the contract of `.withoutOverwriting`.  And whether successful or not, the original name is unlinked (which leaves only the intended final name referring to the file, or deletes the file if the `link` failed).
  - Windows: in `SetFileInformationByHandle`, `FILE_RENAME_FLAG_REPLACE_IF_EXISTS` / `MOVEFILE_REPLACE_EXISTING` are dropped if `.withoutOverwriting` is specified, and the `ERROR_FILE_EXISTS` & `ERROR_ALREADY_EXISTS` errors are mapped to Cocoa's `fileWriteFileExists`.
@wadetregaskis wadetregaskis requested a review from a team as a code owner May 31, 2026 16:22
@jmschonfeld

Copy link
Copy Markdown
Contributor

That combination worked in Swift 5.10 and is documented as supported (Apple's Foundation maps it to "write to aux file with O_EXCL, then exchange"), so trapping is not just innately bad but also a source-breaking regression.

Can you please elaborate on this statement? It seems mostly incorrect to me. This combination of options is explicitly documented as unsupported:

You can’t combine this constant with atomic because atomic allows the system to overwrite the original file.

https://developer.apple.com/documentation/foundation/nsdata/writingoptions/withoutoverwriting

Additionally, this source code is shared with Foundation.framework on Apple platforms and it terminates the same way on Apple platforms as it does on Linux currently. I did confirm it did work with 5.10 before the swift-foundation recoring, but it looks like Apple platforms have always performed this way and were documented as such. Can you please share what platform + OS version you tested on when validating this?

@kperryua

kperryua commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

The semantics of WithoutOverwriting referring that of the auxiliary file was never intended by the original API design. The option is specifically documented to affect the destination file only.

When originally implemented, we had no silver bullet syscall that could support an atomic rename() that had the same semantics as O_EXCL. We have that now on Darwin for APFS in the form of RENAME_EXCL. It's also available for Linux in terms of RENAME_NOREPLACE. And per your description, sounds like we have it for Windows. In order to adopt however, we'd have to decide whether we're willing to start dynamically throwing "feature unsupported" errors on file systems where the behavior is not supported.

Furthermore, without any kind of availability declaration, we'd probably have to predicate this behavior on deployment target so that it doesn't surprisingly behave differently on current OS's and then crash immediately when running on older OS's.

RE: link(). The solution is clever, but there is known friction with Sandbox on Darwin when using link() on a aux file that exists in a temporary directory and certain destination directories. I'd rather avoid that headache if we can.

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.

3 participants