Skip to content

Commit 47a904c

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 47a904c

File tree

1 file changed

+46
-53
lines changed

1 file changed

+46
-53
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ extension _FileManagerImpl {
4646
var homeDirectoryForCurrentUser: URL {
4747
URL(filePath: String.homeDirectoryPath(), directoryHint: .isDirectory)
4848
}
49-
49+
5050
func homeDirectory(forUser userName: String?) -> URL? {
5151
URL(filePath: String.homeDirectoryPath(forUser: userName), directoryHint: .isDirectory)
5252
}
53-
53+
5454
var temporaryDirectory: URL {
5555
URL(filePath: String.temporaryDirectoryPath, directoryHint: .isDirectory)
5656
}
57-
57+
5858
func url(
5959
for directory: FileManager.SearchPathDirectory,
6060
in domain: FileManager.SearchPathDomainMask,
@@ -84,7 +84,7 @@ extension _FileManagerImpl {
8484
guard let url = lastElement ? urls.last : urls.first else {
8585
throw CocoaError(.fileReadUnknown)
8686
}
87-
87+
8888
if shouldCreate {
8989
#if FOUNDATION_FRAMEWORK
9090
_LogSpecialFolderRecreation(fileManager, url.path)
@@ -109,35 +109,35 @@ extension _FileManagerImpl {
109109
}
110110
return url
111111
}
112-
112+
113113
func urls(
114114
for directory: FileManager.SearchPathDirectory,
115115
in domainMask: FileManager.SearchPathDomainMask
116116
) -> [URL] {
117117
Array(_SearchPathURLs(for: directory, in: domainMask, expandTilde: true))
118118
}
119-
119+
120120
#if FOUNDATION_FRAMEWORK
121121
func containerURL(forSecurityApplicationGroupIdentifier groupIdentifier: String) -> URL? {
122122
groupIdentifier.withCString {
123123
guard let path = container_create_or_lookup_app_group_path_by_app_group_identifier($0, nil) else {
124124
return nil
125125
}
126-
126+
127127
defer { path.deallocate() }
128128
return URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
129129
}
130130
}
131131
#endif
132-
132+
133133
func contentsOfDirectory(atPath path: String) throws -> [String] {
134134
#if os(macOS)
135135
// CFURLEnumerator/CarbonCore does not operate on /dev paths
136136
if !path.standardizingPath.starts(with: "/dev") {
137137
guard fileManager.fileExists(atPath: path) else {
138138
throw CocoaError.errorWithFilePath(path, osStatus: -43 /*fnfErr*/, reading: true, variant: "Folder")
139139
}
140-
140+
141141
#if FOUNDATION_FRAMEWORK
142142
// Use CFURLEnumerator in Foundation framework, otherwise fallback to POSIX sequence below
143143
var err: NSError?
@@ -163,7 +163,7 @@ extension _FileManagerImpl {
163163
}
164164
return result
165165
}
166-
166+
167167
func subpathsOfDirectory(atPath path: String) throws -> [String] {
168168
#if os(Windows)
169169
try path.withNTPathRepresentation {
@@ -203,10 +203,10 @@ extension _FileManagerImpl {
203203
guard let fileSystemRep else {
204204
throw CocoaError.errorWithFilePath(.fileNoSuchFile, path)
205205
}
206-
206+
207207
let subpaths = _FTSSequence(fileSystemRep, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT).subpaths
208208
var realFirstPath: String?
209-
209+
210210
var results: [String] = []
211211
for item in subpaths {
212212
var subpath: String
@@ -216,12 +216,12 @@ extension _FileManagerImpl {
216216
case .entry(let path):
217217
subpath = path
218218
}
219-
219+
220220
guard let realFirstPath else {
221221
realFirstPath = subpath
222222
continue
223223
}
224-
224+
225225
let trueSubpath = subpath.trimmingPrefix(realFirstPath)
226226
if trueSubpath.first == "/" {
227227
results.append(String(trueSubpath.dropFirst()))
@@ -242,43 +242,36 @@ extension _FileManagerImpl {
242242
guard url.isFileURL else {
243243
throw CocoaError.errorWithFilePath(.fileWriteUnsupportedScheme, url)
244244
}
245-
245+
246246
let path = url.path
247247
guard !path.isEmpty else {
248248
throw CocoaError.errorWithFilePath(.fileNoSuchFile, url)
249249
}
250-
250+
251251
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes)
252252
}
253-
253+
254254
func createDirectory(
255255
atPath path: String,
256256
withIntermediateDirectories createIntermediates: Bool,
257257
attributes: [FileAttributeKey : Any]? = nil
258258
) throws {
259259
#if os(Windows)
260260
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
268-
}
269-
270-
let parent = path.deletingLastPathComponent()
271-
if !parent.isEmpty {
272-
try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes)
273-
}
274-
}
275-
276261
var saAttributes: SECURITY_ATTRIBUTES =
277262
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
278263
lpSecurityDescriptor: nil,
279264
bInheritHandle: false)
280-
guard CreateDirectoryW(pwszPath, &saAttributes) else {
281-
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
265+
if createIntermediates {
266+
// `SHCreateDirectoryExW` creates intermediate directories while `CreateDirectoryW` does not
267+
let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes)
268+
guard errorCode == ERROR_SUCCESS else {
269+
throw CocoaError.errorWithFilePath(path, win32: WIN32_FROM_HRESULT(errorCode), reading: false)
270+
}
271+
} else {
272+
guard CreateDirectoryW(pwszPath, &saAttributes) else {
273+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
274+
}
282275
}
283276
if let attributes {
284277
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
@@ -289,7 +282,7 @@ extension _FileManagerImpl {
289282
guard let pathPtr else {
290283
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
291284
}
292-
285+
293286
guard createIntermediates else {
294287
guard mkdir(pathPtr, 0o777) == 0 else {
295288
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
@@ -299,12 +292,12 @@ extension _FileManagerImpl {
299292
}
300293
return
301294
}
302-
295+
303296
#if FOUNDATION_FRAMEWORK
304297
var firstDirectoryPtr: UnsafePointer<CChar>?
305298
defer { firstDirectoryPtr?.deallocate() }
306299
let result = _mkpath_np(pathPtr, S_IRWXU | S_IRWXG | S_IRWXO, &firstDirectoryPtr)
307-
300+
308301
guard result == 0 else {
309302
guard result != EEXIST else { return }
310303
var errNum = result
@@ -328,18 +321,18 @@ extension _FileManagerImpl {
328321
}
329322
throw CocoaError.errorWithFilePath(errPath, errno: errNum, reading: false)
330323
}
331-
324+
332325
guard let attributes else {
333326
return // Nothing left to do
334327
}
335-
328+
336329
// The directory was successfully created. To keep binary compatibility, we need to post-process the newly created directories and set attributes.
337330
// We're relying on the knowledge that _mkpath_np does not change any of the parent path components of firstDirectory. Otherwise, I think we'd have to canonicalize paths or check for IDs, which would probably require more file system calls than is worthwhile.
338331
var currentDirectory = firstDirectoryPtr.flatMap(String.init(cString:)) ?? path
339-
332+
340333
// Start with the first newly created directory.
341334
try? fileManager.setAttributes(attributes, ofItemAtPath: currentDirectory)// Not returning error to preserve binary compatibility.
342-
335+
343336
// Now append each subsequent path component.
344337
let fullComponents = path.pathComponents
345338
let currentComponents = currentDirectory.pathComponents
@@ -385,7 +378,7 @@ extension _FileManagerImpl {
385378
}
386379
#endif
387380
}
388-
381+
389382
#if FOUNDATION_FRAMEWORK
390383
func getRelationship(
391384
_ outRelationship: UnsafeMutablePointer<FileManager.URLRelationship>,
@@ -394,44 +387,44 @@ extension _FileManagerImpl {
394387
) throws {
395388
// Get url's resource identifier, volume identifier, and make sure it is a directory
396389
let dirValues = try directoryURL.resourceValues(forKeys: [.fileResourceIdentifierKey, .volumeIdentifierKey, .isDirectoryKey])
397-
390+
398391
guard let isDirectory = dirValues.isDirectory, isDirectory else {
399392
outRelationship.pointee = .other
400393
return
401394
}
402-
395+
403396
// Get other's resource identifier and make sure it is not the same resource as otherURL
404397
let otherValues = try otherURL.resourceValues(forKeys: [.fileIdentifierKey, .fileResourceIdentifierKey, .volumeIdentifierKey])
405398
guard !otherValues.fileResourceIdentifier!.isEqual(dirValues.fileResourceIdentifier!) else {
406399
outRelationship.pointee = .same
407400
return
408401
}
409-
402+
410403
guard otherValues.volumeIdentifier!.isEqual(dirValues.volumeIdentifier!) else {
411404
outRelationship.pointee = .other
412405
return
413406
}
414-
407+
415408
// Start looking through the parent chain up to the volume root for a parent that is equal to 'url'. Stop when the current URL reaches the volume root
416409
var currentURL = otherURL
417410
while try !currentURL.resourceValues(forKeys: [.isVolumeKey]).isVolume! {
418411
// Get url's parentURL
419412
let parentURL = try currentURL.resourceValues(forKeys: [.parentDirectoryURLKey]).parentDirectory!
420-
413+
421414
let parentResourceID = try parentURL.resourceValues(forKeys: [.fileResourceIdentifierKey]).fileResourceIdentifier!
422-
415+
423416
if parentResourceID.isEqual(dirValues.fileResourceIdentifier!) {
424417
outRelationship.pointee = .contains
425418
return
426419
}
427-
420+
428421
currentURL = parentURL
429422
}
430-
423+
431424
outRelationship.pointee = .other
432425
return
433426
}
434-
427+
435428
func getRelationship(
436429
_ outRelationship: UnsafeMutablePointer<FileManager.URLRelationship>,
437430
of directory: FileManager.SearchPathDirectory,
@@ -450,7 +443,7 @@ extension _FileManagerImpl {
450443
toItemAt: url)
451444
}
452445
#endif
453-
446+
454447
func changeCurrentDirectoryPath(_ path: String) -> Bool {
455448
#if os(Windows)
456449
return (try? path.withNTPathRepresentation {
@@ -463,7 +456,7 @@ extension _FileManagerImpl {
463456
}
464457
#endif
465458
}
466-
459+
467460
var currentDirectoryPath: String? {
468461
#if os(Windows)
469462
let dwLength: DWORD = GetCurrentDirectoryW(0, nil)

0 commit comments

Comments
 (0)