Skip to content

Improve temporary file location for atomic writes #1477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions Sources/FoundationEssentials/Data/Data+Writing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,37 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL,

let pidString = String(ProcessInfo.processInfo.processIdentifier, radix: 16, uppercase: true)
let template = directoryPath + prefix + pidString + ".XXXXXX"
var count = 0
let maxCount = 7
repeat {
for _ in 0 ..< maxCount {
#if FOUNDATION_FRAMEWORK
let (sandboxResult, amkrErrno) = inPath.withFileSystemRepresentation { inPathFileSystemRep -> ((Int32, String)?, Int32?) in
guard let inPathFileSystemRep else {
return (nil, nil)
}
// First, try _amkrtemp to carry over any sandbox extensions for inPath to the temporary file (even if the application isn't sandboxed)
guard let uniqueTempFile = _amkrtemp(inPathFileSystemRep) else {
return (nil, errno)
}
defer { free(uniqueTempFile) }
let fd = openFileDescriptorProtected(path: uniqueTempFile, flags: O_CREAT | O_EXCL | O_RDWR, options: options)
if fd >= 0 {
// Got a good fd
return ((fd, String(cString: uniqueTempFile)), nil)
}
return (nil, errno)
}

// If _amkrtemp succeeded, return its result
if let sandboxResult {
return sandboxResult
}
// If _amkrtemp failed with EEXIST, just retry
if amkrErrno == EEXIST {
continue
}
// Otherwise, fall through to mktemp below
#endif

let result = try template.withMutableFileSystemRepresentation { templateFileSystemRep -> (Int32, String)? in
guard let templateFileSystemRep else {
throw CocoaError(.fileWriteInvalidFileName)
Expand Down Expand Up @@ -187,7 +215,12 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL,

// If the file exists, we repeat. Otherwise throw the error.
if errno != EEXIST {
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant)
#if FOUNDATION_FRAMEWORK
let debugDescription = "Creating a temporary file via mktemp failed. Creating the temporary file via _amkrtemp previously also failed with errno \(amkrErrno)"
#else
let debugDescription: String? = nil
#endif
throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant, debugDescription: debugDescription)
}

// Try again
Expand All @@ -196,14 +229,10 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL,

if let result {
return result
} else {
count += 1
if count > maxCount {
// Prevent an infinite loop; even if the error is obscure
throw CocoaError(.fileWriteUnknown)
}
}
} while true
}
// We hit max count, prevent an infinite loop; even if the error is obscure
throw CocoaError(.fileWriteUnknown)
#endif // os(WASI)
}

Expand Down
52 changes: 30 additions & 22 deletions Sources/FoundationEssentials/Error/CocoaError+FilePath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ import WinSDK
// MARK: - Error Creation with CocoaError.Code

extension CocoaError {
static func errorWithFilePath(_ code: CocoaError.Code, _ path: String, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError {
CocoaError(code, path: path, variant: variant, source: source, destination: destination)
static func errorWithFilePath(_ code: CocoaError.Code, _ path: String, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError {
CocoaError(code, path: path, variant: variant, source: source, destination: destination, debugDescription: debugDescription)
}

static func errorWithFilePath(_ code: CocoaError.Code, _ url: URL, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError {
CocoaError(code, url: url, variant: variant, source: source, destination: destination)
static func errorWithFilePath(_ code: CocoaError.Code, _ url: URL, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError {
CocoaError(code, url: url, variant: variant, source: source, destination: destination, debugDescription: debugDescription)
}
}

Expand Down Expand Up @@ -81,21 +81,21 @@ extension POSIXError {
}

extension CocoaError {
static func errorWithFilePath(_ pathOrURL: PathOrURL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError {
static func errorWithFilePath(_ pathOrURL: PathOrURL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError {
switch pathOrURL {
case .path(let path):
return Self.errorWithFilePath(path, errno: errno, reading: reading, variant: variant, source: source, destination: destination)
return Self.errorWithFilePath(path, errno: errno, reading: reading, variant: variant, source: source, destination: destination, debugDescription: debugDescription)
case .url(let url):
return Self.errorWithFilePath(url, errno: errno, reading: reading, variant: variant, source: source, destination: destination)
return Self.errorWithFilePath(url, errno: errno, reading: reading, variant: variant, source: source, destination: destination, debugDescription: debugDescription)
}
}

static func errorWithFilePath(_ path: String, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError {
CocoaError(Code(fileErrno: errno, reading: reading), path: path, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination)
static func errorWithFilePath(_ path: String, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError {
CocoaError(Code(fileErrno: errno, reading: reading), path: path, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination, debugDescription: debugDescription)
}

static func errorWithFilePath(_ url: URL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError {
CocoaError(Code(fileErrno: errno, reading: reading), url: url, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination)
static func errorWithFilePath(_ url: URL, errno: Int32, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError {
CocoaError(Code(fileErrno: errno, reading: reading), url: url, underlying: POSIXError(errno: errno), variant: variant, source: source, destination: destination, debugDescription: debugDescription)
}
}

Expand Down Expand Up @@ -144,18 +144,18 @@ extension CocoaError.Code {
}

extension CocoaError {
static func errorWithFilePath(_ path: PathOrURL, win32 dwError: DWORD, reading: Bool) -> CocoaError {
static func errorWithFilePath(_ path: PathOrURL, win32 dwError: DWORD, reading: Bool, debugDescription: String? = nil) -> CocoaError {
switch path {
case let .path(path):
return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path.isEmpty), path: path, underlying: Win32Error(dwError))
return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path.isEmpty), path: path, underlying: Win32Error(dwError), debugDescription: debugDescription)
case let .url(url):
let pathStr = url.withUnsafeFileSystemRepresentation { String(cString: $0!) }
return CocoaError(.init(win32: dwError, reading: reading, emptyPath: pathStr.isEmpty), path: pathStr, url: url, underlying: Win32Error(dwError))
return CocoaError(.init(win32: dwError, reading: reading, emptyPath: pathStr.isEmpty), path: pathStr, url: url, underlying: Win32Error(dwError), debugDescription: debugDescription)
}
}

static func errorWithFilePath(_ path: String? = nil, win32 dwError: DWORD, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil) -> CocoaError {
return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path?.isEmpty), path: path, underlying: Win32Error(dwError), variant: variant, source: source, destination: destination)
static func errorWithFilePath(_ path: String? = nil, win32 dwError: DWORD, reading: Bool, variant: String? = nil, source: String? = nil, destination: String? = nil, debugDescription: String? = nil) -> CocoaError {
return CocoaError(.init(win32: dwError, reading: reading, emptyPath: path?.isEmpty), path: path, underlying: Win32Error(dwError), variant: variant, source: source, destination: destination, debugDescription: debugDescription)
}
}
#endif
Expand Down Expand Up @@ -190,7 +190,8 @@ extension CocoaError {
underlying: (some Error)? = Optional<CocoaError>.none,
variant: String? = nil,
source: String? = nil,
destination: String? = nil
destination: String? = nil,
debugDescription: String? = nil
) {
self.init(
code,
Expand All @@ -199,7 +200,8 @@ extension CocoaError {
underlying: underlying,
variant: variant,
source: source,
destination: destination
destination: destination,
debugDescription: debugDescription
)
}

Expand All @@ -209,7 +211,8 @@ extension CocoaError {
underlying: (some Error)? = Optional<CocoaError>.none,
variant: String? = nil,
source: String? = nil,
destination: String? = nil
destination: String? = nil,
debugDescription: String? = nil
) {
self.init(
code,
Expand All @@ -218,7 +221,8 @@ extension CocoaError {
underlying: underlying,
variant: variant,
source: source,
destination: destination
destination: destination,
debugDescription: debugDescription
)
}

Expand All @@ -229,10 +233,11 @@ extension CocoaError {
underlying: (some Error)? = Optional<CocoaError>.none,
variant: String? = nil,
source: String? = nil,
destination: String? = nil
destination: String? = nil,
debugDescription: String? = nil
) {
#if FOUNDATION_FRAMEWORK
self.init(_uncheckedNSError: NSError._cocoaError(withCode: code.rawValue, path: path, url: url, underlying: underlying, variant: variant, source: source, destination: destination) as NSError)
self.init(_uncheckedNSError: NSError._cocoaError(withCode: code.rawValue, path: path, url: url, underlying: underlying, variant: variant, source: source, destination: destination, debugDescription: debugDescription) as NSError)
#else
var userInfo: [String : Any] = [:]
if let path {
Expand All @@ -253,6 +258,9 @@ extension CocoaError {
if let variant {
userInfo[NSUserStringVariantErrorKey] = [variant]
}
if let debugDescription {
userInfo[NSDebugDescriptionErrorKey] = debugDescription
}

self.init(code, userInfo: userInfo)
#endif
Expand Down