Skip to content

Commit 1a22c48

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 f77911a commit 1a22c48

File tree

1 file changed

+66
-19
lines changed

1 file changed

+66
-19
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -250,35 +250,82 @@ extension _FileManagerImpl {
250250

251251
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes)
252252
}
253+
254+
#if os(Windows)
255+
/// If `path` is absolute, this is the same as `path.withNTPathRepresentation`.
256+
/// If `path` is relative, this creates an absolute path of `path` relative to `currentDirectoryPath` and runs
257+
/// `body` with that path.
258+
private func withAbsoluteNTPathRepresentation<Result>(
259+
of path: String,
260+
_ body: (UnsafePointer<WCHAR>) throws -> Result
261+
) throws -> Result {
262+
try path.withNTPathRepresentation { pwszPath in
263+
if !PathIsRelativeW(pwszPath) {
264+
// We already have an absolute path. Nothing to do
265+
return try body(pwszPath)
266+
}
267+
guard let currentDirectoryPath else {
268+
preconditionFailure("We should always have a current directory on Windows")
269+
}
270+
271+
// We have a relateive path. Make it absolute.
272+
let absoluteUrl = URL(
273+
filePath: path,
274+
directoryHint: .isDirectory,
275+
relativeTo: URL(filePath: currentDirectoryPath, directoryHint: .isDirectory)
276+
)
277+
return try absoluteUrl.path.withNTPathRepresentation { pwszPath in
278+
return try body(pwszPath)
279+
}
280+
}
281+
}
282+
#endif
253283

254284
func createDirectory(
255285
atPath path: String,
256286
withIntermediateDirectories createIntermediates: Bool,
257287
attributes: [FileAttributeKey : Any]? = nil
258288
) throws {
259289
#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
290+
var saAttributes: SECURITY_ATTRIBUTES =
291+
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
292+
lpSecurityDescriptor: nil,
293+
bInheritHandle: false)
294+
// `SHCreateDirectoryExW` creates intermediate directories while `CreateDirectoryW` does not.
295+
if createIntermediates {
296+
// `SHCreateDirectoryExW` requires an absolute path while `CreateDirectoryW` works based on the current working
297+
// directory.
298+
try withAbsoluteNTPathRepresentation(of: path) { pwszPath in
299+
let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes)
300+
guard let errorCode = DWORD(exactly: errorCode) else {
301+
// `SHCreateDirectoryExW` returns `Int` but all error codes are defined in terms of `DWORD`, aka
302+
// `UInt`. We received an unknown error code.
303+
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
268304
}
269-
270-
let parent = path.deletingLastPathComponent()
271-
if !parent.isEmpty {
272-
try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes)
305+
switch errorCode {
306+
case ERROR_SUCCESS:
307+
if let attributes {
308+
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
309+
}
310+
case ERROR_ALREADY_EXISTS:
311+
var isDirectory: Bool = false
312+
if fileExists(atPath: path, isDirectory: &isDirectory), isDirectory {
313+
// A directory already exists at this path, which is not an error if we have
314+
// `createIntermediates == true`.
315+
break
316+
}
317+
// A file (not a directory) exists at the given path or the file creation failed and the item
318+
// at this path has been deleted before the call to `fileExists`. Throw the original error.
319+
fallthrough
320+
default:
321+
throw CocoaError.errorWithFilePath(path, win32: errorCode, reading: false)
273322
}
274323
}
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)
324+
} else {
325+
try path.withNTPathRepresentation { pwszPath in
326+
guard CreateDirectoryW(pwszPath, &saAttributes) else {
327+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
328+
}
282329
}
283330
if let attributes {
284331
try? fileManager.setAttributes(attributes, ofItemAtPath: path)

0 commit comments

Comments
 (0)