Skip to content

Commit 241368a

Browse files
committed
Use SHCreateDirectory to create a directory with intermediate directories on Windows
Instead of recursively creating all the parent directories, which is racy if the directory is created between the check if the parent directory exists and the actual directory creation, use `SHCreateDirectoryExW`, which also creates intermediate directories.
1 parent 33a49e5 commit 241368a

File tree

1 file changed

+43
-22
lines changed

1 file changed

+43
-22
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -257,33 +257,54 @@ extension _FileManagerImpl {
257257
attributes: [FileAttributeKey : Any]? = nil
258258
) throws {
259259
#if os(Windows)
260-
try path.withNTPathRepresentation { pwszPath in
261-
if createIntermediates {
262-
var isDirectory: Bool = false
263-
if fileManager.fileExists(atPath: path, isDirectory: &isDirectory) {
264-
guard isDirectory else {
265-
throw CocoaError.errorWithFilePath(path, win32: ERROR_FILE_EXISTS, reading: false)
266-
}
267-
return
260+
var saAttributes: SECURITY_ATTRIBUTES =
261+
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
262+
lpSecurityDescriptor: nil,
263+
bInheritHandle: false)
264+
// `SHCreateDirectoryExW` creates intermediate directories while `CreateDirectoryW` does not.
265+
if createIntermediates {
266+
// `SHCreateDirectoryExW` requires an absolute path while `CreateDirectoryW` works based on the current
267+
// working directory.
268+
let fullyQualifiedPath = if let currentDirectoryPath {
269+
URL(filePath: path, directoryHint: .isDirectory, relativeTo: URL(filePath: currentDirectoryPath, directoryHint: .isDirectory)).path
270+
} else {
271+
path
272+
}
273+
try fullyQualifiedPath.withNTPathRepresentation { pwszPath in
274+
let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes)
275+
guard let errorCode = DWORD(exactly: errorCode) else {
276+
// `SHCreateDirectoryExW` returns `int` but all error codes are defined in terms of `DWORD`. We
277+
// received an unknown error code.
278+
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
268279
}
269-
270-
let parent = path.deletingLastPathComponent()
271-
if !parent.isEmpty {
272-
try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes)
280+
switch errorCode {
281+
case ERROR_SUCCESS:
282+
break
283+
case ERROR_ALREADY_EXISTS:
284+
var isDirectory: Bool = false
285+
if fileExists(atPath: path, isDirectory: &isDirectory), isDirectory {
286+
// A directory already exists at this path, which is not an error if we have
287+
// `createIntermediates == true`.
288+
break
289+
} else {
290+
// A file (not a directory) exists at the given path or the file creation failed and the item
291+
// at this path has been deleted before the call to `fileExists`. Throw the original error.
292+
fallthrough
293+
}
294+
default:
295+
throw CocoaError.errorWithFilePath(path, win32: errorCode, reading: false)
273296
}
274297
}
275-
276-
var saAttributes: SECURITY_ATTRIBUTES =
277-
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
278-
lpSecurityDescriptor: nil,
279-
bInheritHandle: false)
280-
guard CreateDirectoryW(pwszPath, &saAttributes) else {
281-
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
282-
}
283-
if let attributes {
284-
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
298+
} else {
299+
try path.withNTPathRepresentation { pwszPath in
300+
guard CreateDirectoryW(pwszPath, &saAttributes) else {
301+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
302+
}
285303
}
286304
}
305+
if let attributes {
306+
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
307+
}
287308
#else
288309
try fileManager.withFileSystemRepresentation(for: path) { pathPtr in
289310
guard let pathPtr else {

0 commit comments

Comments
 (0)