Skip to content

Commit 5d71c52

Browse files
committed
Foundation: various improvements to long path support
Refactor directory iteration to avoid replicating the conversion to the NT style paths everywhere. The iteration logic can be done at a single site with a callback to handle the iteration. Improve handle long file paths on Windows when performing a `removeItem` call. Furthermore, correct a few areas where we were mishandling junctions. This would result in an early termination of the loop in `removeItem` causing us to fail to clean up directories which we should have been able to. This improves the DocC test coverage on Windows. Rework the file attribute reading on Windows operation to reformulate the path to the absolute path representation and then into the NT form before performing the operation. This technically is a partial repair as drive relative paths where the current directory is deep enough, the path evaluation would fail.
1 parent 1fb0560 commit 5d71c52

File tree

1 file changed

+176
-126
lines changed

1 file changed

+176
-126
lines changed

Diff for: Sources/Foundation/FileManager+Win32.swift

+176-126
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,96 @@
1313
import let WinSDK.INVALID_FILE_ATTRIBUTES
1414
import WinSDK
1515

16+
extension URL {
17+
fileprivate var NTPath: String {
18+
"\\\\?\\\(CFURLCopyFileSystemPath(CFURLCopyAbsoluteURL(_cfObject), kCFURLWindowsPathStyle)!._swiftObject)"
19+
}
20+
21+
fileprivate func withUnsafeNTPath<Result>(_ body: (UnsafePointer<WCHAR>) throws -> Result) rethrows -> Result {
22+
return try NTPath.withCString(encodedAs: UTF16.self, body)
23+
}
24+
}
25+
26+
27+
private func withNTPathRepresentation<Result>(of path: String, _ body: (UnsafePointer<WCHAR>) throws -> Result) throws -> Result {
28+
func withNormalizedRepresentation<Result>(of path: String, _ body: (UnsafePointer<WCHAR>) throws -> Result) rethrows -> Result {
29+
var path = path
30+
31+
// Strip the leading `/` on a RFC8089 path (`/[drive-letter]:/...` ). A
32+
// leading slash indicates a rooted path on the drive for teh current
33+
// working directory.
34+
if path.count > 3 {
35+
let index0 = path.startIndex
36+
let index1 = path.index(path.startIndex, offsetBy: 1)
37+
let index2 = path.index(path.startIndex, offsetBy: 2)
38+
39+
if path[index0] == "/", path[index1].isLetter, path[index2] == ":" {
40+
path.removeFirst()
41+
}
42+
}
43+
44+
// Win32 APIs can support `/` for the arc separator. However,
45+
// symlinks created with `/` do not resolve properly, so normalize
46+
// the path.
47+
path = path.replacing("/", with: "\\")
48+
49+
// Droop trailing slashes unless it follows a drive specification. The
50+
// trailing arc separator after a drive specifier iindicates the root as
51+
// opposed to a drive relative path.
52+
while path.count > 1, path[path.index(before: path.endIndex)] == "\\",
53+
!(path.count == 3 &&
54+
path[path.index(path.endIndex, offsetBy: -2)] == ":" &&
55+
path[path.index(path.endIndex, offsetBy: -3)].isLetter) {
56+
path.removeLast()
57+
}
58+
59+
return try path.withCString(encodedAs: UTF16.self, body)
60+
}
61+
62+
guard !path.isEmpty else {
63+
throw NSError(domain: NSCocoaErrorDomain,
64+
code: CocoaError.fileReadInvalidFileName.rawValue,
65+
userInfo: [NSFilePathErrorKey:path])
66+
}
67+
68+
return try withNormalizedRepresentation(of: path) { pwszPath in
69+
guard !path.hasPrefix("\\\\"), !path.hasPrefix("//") else {
70+
return try body(pwszPath)
71+
}
72+
73+
let dwLength = GetFullPathNameW(pwszPath, 0, nil, nil)
74+
let path = withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
75+
_ = GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil)
76+
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
77+
}
78+
return try "\\\\?\\\(path)".withCString(encodedAs: UTF16.self, body)
79+
}
80+
}
81+
82+
private func walk(directory path: URL, _ body: (String, DWORD) throws -> Void) rethrows {
83+
try "\(path.NTPath)\\*".withCString(encodedAs: UTF16.self) {
84+
var ffd: WIN32_FIND_DATAW = .init()
85+
let capacity = MemoryLayout.size(ofValue: ffd.cFileName) / MemoryLayout<WCHAR>.size
86+
87+
let hFind: HANDLE = FindFirstFileW($0, &ffd)
88+
if hFind == INVALID_HANDLE_VALUE {
89+
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path.path])
90+
}
91+
92+
defer { FindClose(hFind) }
93+
94+
repeat {
95+
let entry: String = withUnsafePointer(to: ffd.cFileName) {
96+
$0.withMemoryRebound(to: WCHAR.self, capacity: capacity) {
97+
String(decodingCString: $0, as: UTF16.self)
98+
}
99+
}
100+
101+
try body(entry, ffd.dwFileAttributes)
102+
} while FindNextFileW(hFind, &ffd)
103+
}
104+
}
105+
16106
internal func joinPath(prefix: String, suffix: String) -> String {
17107
var pszPath: PWSTR?
18108

@@ -198,28 +288,15 @@ extension FileManager {
198288
}
199289

200290
internal func _contentsOfDir(atPath path: String, _ closure: (String, Int32) throws -> () ) throws {
201-
guard path != "" else {
202-
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadInvalidFileName.rawValue, userInfo: [NSFilePathErrorKey : NSString(path)])
291+
guard !path.isEmpty else {
292+
throw NSError(domain: NSCocoaErrorDomain,
293+
code: CocoaError.fileReadInvalidFileName.rawValue,
294+
userInfo: [NSFilePathErrorKey:NSString(path)])
203295
}
204-
try FileManager.default._fileSystemRepresentation(withPath: path + "\\*") {
205-
var ffd: WIN32_FIND_DATAW = WIN32_FIND_DATAW()
206-
207-
let hDirectory: HANDLE = FindFirstFileW($0, &ffd)
208-
if hDirectory == INVALID_HANDLE_VALUE {
209-
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path])
210-
}
211-
defer { FindClose(hDirectory) }
212296

213-
repeat {
214-
let path: String = withUnsafePointer(to: &ffd.cFileName) {
215-
$0.withMemoryRebound(to: UInt16.self, capacity: MemoryLayout.size(ofValue: $0) / MemoryLayout<WCHAR>.size) {
216-
String(decodingCString: $0, as: UTF16.self)
217-
}
218-
}
219-
if path != "." && path != ".." {
220-
try closure(path.standardizingPath, Int32(ffd.dwFileAttributes))
221-
}
222-
} while FindNextFileW(hDirectory, &ffd)
297+
try walk(directory: URL(fileURLWithPath: path, isDirectory: true)) { entry, attributes in
298+
if entry == "." || entry == ".." { return }
299+
try closure(entry.standardizingPath, Int32(attributes))
223300
}
224301
}
225302

@@ -239,13 +316,13 @@ extension FileManager {
239316
}
240317

241318
internal func windowsFileAttributes(atPath path: String) throws -> WIN32_FILE_ATTRIBUTE_DATA {
242-
return try FileManager.default._fileSystemRepresentation(withPath: path) {
243-
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = WIN32_FILE_ATTRIBUTE_DATA()
244-
if !GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) {
245-
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path])
319+
return try withNTPathRepresentation(of: path) {
320+
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
321+
if !GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) {
322+
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path])
323+
}
324+
return faAttributes
246325
}
247-
return faAttributes
248-
}
249326
}
250327

251328
internal func _attributesOfFileSystemIncludingBlockSize(forPath path: String) throws -> (attributes: [FileAttributeKey : Any], blockSize: UInt64?) {
@@ -571,94 +648,83 @@ extension FileManager {
571648
return
572649
}
573650

574-
let faAttributes: WIN32_FILE_ATTRIBUTE_DATA
575-
do {
576-
faAttributes = try windowsFileAttributes(atPath: path)
577-
} catch {
578-
// removeItem on POSIX throws fileNoSuchFile rather than
579-
// fileReadNoSuchFile that windowsFileAttributes will
580-
// throw if it doesn't find the file.
581-
if (error as NSError).code == CocoaError.fileReadNoSuchFile.rawValue {
651+
try withNTPathRepresentation(of: path) {
652+
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
653+
if !GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) {
582654
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
583-
} else {
584-
throw error
585655
}
586-
}
587-
588-
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
589-
if try !FileManager.default._fileSystemRepresentation(withPath: path, {
590-
SetFileAttributesW($0, faAttributes.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)
591-
}) {
592-
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
593-
}
594-
}
595-
596-
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
597-
if try !FileManager.default._fileSystemRepresentation(withPath: path, DeleteFileW) {
598-
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
599-
}
600-
return
601-
}
602656

603-
var dirStack = [path]
604-
var itemPath = ""
605-
while let currentDir = dirStack.popLast() {
606-
do {
607-
itemPath = currentDir
608-
guard alreadyConfirmed || shouldRemoveItemAtPath(itemPath, isURL: isURL) else {
609-
continue
610-
}
611-
612-
if try FileManager.default._fileSystemRepresentation(withPath: itemPath, RemoveDirectoryW) {
613-
continue
614-
}
615-
guard GetLastError() == ERROR_DIR_NOT_EMPTY else {
616-
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [itemPath])
657+
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
658+
if !SetFileAttributesW($0, faAttributes.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY) {
659+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
617660
}
618-
dirStack.append(itemPath)
619-
var ffd: WIN32_FIND_DATAW = WIN32_FIND_DATAW()
620-
let capacity = MemoryLayout.size(ofValue: ffd.cFileName)
661+
}
621662

622-
let handle: HANDLE = try FileManager.default._fileSystemRepresentation(withPath: itemPath + "\\*") {
623-
FindFirstFileW($0, &ffd)
624-
}
625-
if handle == INVALID_HANDLE_VALUE {
626-
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [itemPath])
663+
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 || faAttributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT {
664+
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY {
665+
guard RemoveDirectoryW($0) else {
666+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
667+
}
668+
} else {
669+
guard DeleteFileW($0) else {
670+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
671+
}
627672
}
628-
defer { FindClose(handle) }
673+
return
674+
}
629675

630-
repeat {
631-
let file = withUnsafePointer(to: &ffd.cFileName) {
632-
$0.withMemoryRebound(to: WCHAR.self, capacity: capacity) {
633-
String(decodingCString: $0, as: UTF16.self)
634-
}
676+
var stack = [path]
677+
while let directory = stack.popLast() {
678+
do {
679+
guard alreadyConfirmed || shouldRemoveItemAtPath(directory, isURL: isURL) else {
680+
continue
635681
}
636682

637-
itemPath = "\(currentDir)\\\(file)"
638-
if ffd.dwFileAttributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
639-
if try !FileManager.default._fileSystemRepresentation(withPath: itemPath, {
640-
SetFileAttributesW($0, ffd.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)
641-
}) {
642-
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [file])
643-
}
644-
}
645-
646-
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0) {
647-
if file != "." && file != ".." {
648-
dirStack.append(itemPath)
649-
}
650-
} else {
651-
guard alreadyConfirmed || shouldRemoveItemAtPath(itemPath, isURL: isURL) else {
652-
continue
653-
}
654-
if try !FileManager.default._fileSystemRepresentation(withPath: itemPath, DeleteFileW) {
655-
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [file])
683+
let root = URL(fileURLWithPath: directory, isDirectory: true)
684+
try root.withUnsafeNTPath {
685+
if RemoveDirectoryW($0) { return }
686+
guard GetLastError() == ERROR_DIR_NOT_EMPTY else {
687+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [directory])
688+
}
689+
stack.append(directory)
690+
691+
try walk(directory: root) { entry, attributes in
692+
if entry == "." || entry == ".." { return }
693+
694+
let isDirectory = attributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY && attributes & FILE_ATTRIBUTE_REPARSE_POINT == 0
695+
let path = root.appendingPathComponent(entry, isDirectory: isDirectory)
696+
697+
if isDirectory {
698+
stack.append(path.path)
699+
} else {
700+
guard alreadyConfirmed || shouldRemoveItemAtPath(path.path, isURL: isURL) else {
701+
return
702+
}
703+
704+
try path.withUnsafeNTPath {
705+
if attributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
706+
if !SetFileAttributesW($0, attributes & ~FILE_ATTRIBUTE_READONLY) {
707+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [entry])
708+
}
709+
}
710+
711+
if attributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY {
712+
if !RemoveDirectoryW($0) {
713+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [entry])
714+
}
715+
} else {
716+
if !DeleteFileW($0) {
717+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [entry])
718+
}
719+
}
720+
}
721+
}
722+
}
656723
}
724+
} catch {
725+
if !shouldProceedAfterError(error, removingItemAtPath: directory, isURL: isURL) {
726+
throw error
657727
}
658-
} while FindNextFileW(handle, &ffd)
659-
} catch {
660-
if !shouldProceedAfterError(error, removingItemAtPath: itemPath, isURL: isURL) {
661-
throw error
662728
}
663729
}
664730
}
@@ -970,30 +1036,14 @@ extension FileManager {
9701036
guard let _lastReturned else { return firstValidItem() }
9711037

9721038
if _lastReturned.hasDirectoryPath && (level == 0 || !_options.contains(.skipsSubdirectoryDescendants)) {
973-
var ffd = WIN32_FIND_DATAW()
974-
let capacity = MemoryLayout.size(ofValue: ffd.cFileName)
975-
976-
let handle = (try? FileManager.default._fileSystemRepresentation(withPath: _lastReturned.path + "\\*") {
977-
FindFirstFileW($0, &ffd)
978-
}) ?? INVALID_HANDLE_VALUE
979-
if handle == INVALID_HANDLE_VALUE { return firstValidItem() }
980-
defer { FindClose(handle) }
981-
982-
repeat {
983-
let file = withUnsafePointer(to: &ffd.cFileName) {
984-
$0.withMemoryRebound(to: WCHAR.self, capacity: capacity) {
985-
String(decodingCString: $0, as: UTF16.self)
986-
}
987-
}
988-
if file == "." || file == ".." { continue }
989-
if _options.contains(.skipsHiddenFiles) &&
990-
ffd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN {
991-
continue
1039+
try walk(directory: _lastReturned) { entry, attributes in
1040+
if entry == "." || entry == ".." { return }
1041+
if _options.contains(.skipsHiddenFiles) && attributes & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN {
1042+
return
9921043
}
993-
994-
let isDirectory = ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY && ffd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != FILE_ATTRIBUTE_REPARSE_POINT
995-
_stack.append(_lastReturned.appendingPathComponent(file, isDirectory: isDirectory))
996-
} while FindNextFileW(handle, &ffd)
1044+
let isDirectory = attributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY && attributes & FILE_ATTRIBUTE_REPARSE_POINT != FILE_ATTRIBUTE_REPARSE_POINT
1045+
_stack.append(_lastReturned.appendingPathComponent(entry, isDirectory: isDirectory))
1046+
}
9971047
}
9981048

9991049
return firstValidItem()

0 commit comments

Comments
 (0)