Skip to content

Commit

Permalink
Use SHCreateDirectory to create a directory with intermediate direc…
Browse files Browse the repository at this point in the history
…tories 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.
  • Loading branch information
ahoppen committed Nov 5, 2024
1 parent f77911a commit 1a22c48
Showing 1 changed file with 66 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -250,35 +250,82 @@ extension _FileManagerImpl {

try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes)
}

#if os(Windows)
/// If `path` is absolute, this is the same as `path.withNTPathRepresentation`.
/// If `path` is relative, this creates an absolute path of `path` relative to `currentDirectoryPath` and runs
/// `body` with that path.
private func withAbsoluteNTPathRepresentation<Result>(
of path: String,
_ body: (UnsafePointer<WCHAR>) throws -> Result
) throws -> Result {
try path.withNTPathRepresentation { pwszPath in
if !PathIsRelativeW(pwszPath) {
// We already have an absolute path. Nothing to do
return try body(pwszPath)
}
guard let currentDirectoryPath else {
preconditionFailure("We should always have a current directory on Windows")
}

// We have a relateive path. Make it absolute.
let absoluteUrl = URL(
filePath: path,
directoryHint: .isDirectory,
relativeTo: URL(filePath: currentDirectoryPath, directoryHint: .isDirectory)
)
return try absoluteUrl.path.withNTPathRepresentation { pwszPath in
return try body(pwszPath)
}
}
}
#endif

func createDirectory(
atPath path: String,
withIntermediateDirectories createIntermediates: Bool,
attributes: [FileAttributeKey : Any]? = nil
) throws {
#if os(Windows)
try path.withNTPathRepresentation { pwszPath in
if createIntermediates {
var isDirectory: Bool = false
if fileManager.fileExists(atPath: path, isDirectory: &isDirectory) {
guard isDirectory else {
throw CocoaError.errorWithFilePath(path, win32: ERROR_FILE_EXISTS, reading: false)
}
return
var saAttributes: SECURITY_ATTRIBUTES =
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
lpSecurityDescriptor: nil,
bInheritHandle: false)
// `SHCreateDirectoryExW` creates intermediate directories while `CreateDirectoryW` does not.
if createIntermediates {
// `SHCreateDirectoryExW` requires an absolute path while `CreateDirectoryW` works based on the current working
// directory.
try withAbsoluteNTPathRepresentation(of: path) { pwszPath in
let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes)
guard let errorCode = DWORD(exactly: errorCode) else {
// `SHCreateDirectoryExW` returns `Int` but all error codes are defined in terms of `DWORD`, aka
// `UInt`. We received an unknown error code.
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
}

let parent = path.deletingLastPathComponent()
if !parent.isEmpty {
try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes)
switch errorCode {
case ERROR_SUCCESS:
if let attributes {
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
}
case ERROR_ALREADY_EXISTS:
var isDirectory: Bool = false
if fileExists(atPath: path, isDirectory: &isDirectory), isDirectory {
// A directory already exists at this path, which is not an error if we have
// `createIntermediates == true`.
break
}
// A file (not a directory) exists at the given path or the file creation failed and the item
// at this path has been deleted before the call to `fileExists`. Throw the original error.
fallthrough
default:
throw CocoaError.errorWithFilePath(path, win32: errorCode, reading: false)
}
}

var saAttributes: SECURITY_ATTRIBUTES =
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
lpSecurityDescriptor: nil,
bInheritHandle: false)
guard CreateDirectoryW(pwszPath, &saAttributes) else {
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
} else {
try path.withNTPathRepresentation { pwszPath in
guard CreateDirectoryW(pwszPath, &saAttributes) else {
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
}
}
if let attributes {
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
Expand Down

0 comments on commit 1a22c48

Please sign in to comment.