Skip to content

Commit 5fff57e

Browse files
compnerdgwynne
andcommitted
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. Co-authored-by: Gwynne Raskind <gwynne@darkrainfall.org>
1 parent 1fb0560 commit 5fff57e

File tree

1 file changed

+170
-126
lines changed

1 file changed

+170
-126
lines changed

Sources/Foundation/FileManager+Win32.swift

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

16+
extension URL {
17+
fileprivate var NTPath: String {
18+
// Use a NT style, device path to avoid the 261-character path
19+
// limitation on Windows APIs. The addition of the prefix will bypass
20+
// the Win32 layer for the path handling and thus must be fully resolved
21+
// and normalised before being passed in. This allows us access to the
22+
// complete path limit as imposed by the NT kernel rather than the 260
23+
// character limit as imposed by Win32.
24+
#"\\?\\#(CFURLCopyFileSystemPath(CFURLCopyAbsoluteURL(_cfObject), kCFURLWindowsPathStyle)!._swiftObject)"#
25+
}
26+
27+
fileprivate func withUnsafeNTPath<Result>(_ body: (UnsafePointer<WCHAR>) throws -> Result) rethrows -> Result {
28+
try self.NTPath.withCString(encodedAs: UTF16.self, body)
29+
}
30+
}
31+
32+
33+
private func withNTPathRepresentation<Result>(of path: String, _ body: (UnsafePointer<WCHAR>) throws -> Result) throws -> Result {
34+
guard !path.isEmpty else {
35+
throw CocoaError.error(.fileReadInvalidFileName, userInfo: [NSFilePathErrorKey:path])
36+
}
37+
38+
// 1. Normalize the path first.
39+
40+
var path = path
41+
42+
// Strip the leading `/` on a RFC8089 path (`/[drive-letter]:/...` ). A
43+
// leading slash indicates a rooted path on the drive for teh current
44+
// working directory.
45+
var iter = path.makeIterator()
46+
if iter.next() == "/", iter.next()?.isLetter ?? false, iter.next() == ":" {
47+
path.removeFirst()
48+
}
49+
50+
// Win32 APIs can support `/` for the arc separator. However,
51+
// symlinks created with `/` do not resolve properly, so normalize
52+
// the path.
53+
path = path.replacing("/", with: "\\")
54+
55+
// Droop trailing slashes unless it follows a drive specification. The
56+
// trailing arc separator after a drive specifier iindicates the root as
57+
// opposed to a drive relative path.
58+
while path.count > 1, path[path.index(before: path.endIndex)] == "\\",
59+
!(path.count == 3 &&
60+
path[path.index(path.endIndex, offsetBy: -2)] == ":" &&
61+
path[path.index(path.endIndex, offsetBy: -3)].isLetter) {
62+
path.removeLast()
63+
}
64+
65+
// 2. Perform the operation on the normalized path.
66+
67+
return try path.withCString(encodedAs: UTF16.self) { pwszPath in
68+
guard !path.hasPrefix(#"\\"#) else { return try body(pwszPath) }
69+
70+
let dwLength = GetFullPathNameW(pwszPath, 0, nil, nil)
71+
let path = withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
72+
_ = GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil)
73+
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
74+
}
75+
return try #"\\?\\#(path)"#.withCString(encodedAs: UTF16.self, body)
76+
}
77+
}
78+
79+
private func walk(directory path: URL, _ body: (String, DWORD) throws -> Void) rethrows {
80+
try "\(path.NTPath)\\*".withCString(encodedAs: UTF16.self) {
81+
var ffd: WIN32_FIND_DATAW = .init()
82+
83+
let hFind: HANDLE = FindFirstFileW($0, &ffd)
84+
if hFind == INVALID_HANDLE_VALUE {
85+
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path.path])
86+
}
87+
88+
defer { FindClose(hFind) }
89+
90+
repeat {
91+
let entry: String = withUnsafeBytes(of: ffd.cFileName) {
92+
$0.withMemoryRebound(to: WCHAR.self) {
93+
String(decodingCString: $0.baseAddress!, as: UTF16.self)
94+
}
95+
}
96+
97+
try body(entry, ffd.dwFileAttributes)
98+
} while FindNextFileW(hFind, &ffd)
99+
}
100+
}
101+
16102
internal func joinPath(prefix: String, suffix: String) -> String {
17103
var pszPath: PWSTR?
18104

@@ -198,28 +284,13 @@ extension FileManager {
198284
}
199285

200286
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)])
287+
guard !path.isEmpty else {
288+
throw CocoaError.error(.fileReadInvalidFileName, userInfo: [NSFilePathErrorKey:path])
203289
}
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) }
212290

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)
291+
try walk(directory: URL(fileURLWithPath: path, isDirectory: true)) { entry, attributes in
292+
if entry == "." || entry == ".." { return }
293+
try closure(entry.standardizingPath, Int32(attributes))
223294
}
224295
}
225296

@@ -239,13 +310,13 @@ extension FileManager {
239310
}
240311

241312
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])
313+
return try withNTPathRepresentation(of: path) {
314+
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
315+
if !GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) {
316+
throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [path])
317+
}
318+
return faAttributes
246319
}
247-
return faAttributes
248-
}
249320
}
250321

251322
internal func _attributesOfFileSystemIncludingBlockSize(forPath path: String) throws -> (attributes: [FileAttributeKey : Any], blockSize: UInt64?) {
@@ -571,94 +642,83 @@ extension FileManager {
571642
return
572643
}
573644

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 {
645+
try withNTPathRepresentation(of: path) {
646+
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
647+
if !GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) {
582648
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
583-
} else {
584-
throw error
585649
}
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-
}
602-
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])
617-
}
618-
dirStack.append(itemPath)
619-
var ffd: WIN32_FIND_DATAW = WIN32_FIND_DATAW()
620-
let capacity = MemoryLayout.size(ofValue: ffd.cFileName)
621650

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])
651+
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
652+
if !SetFileAttributesW($0, faAttributes.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY) {
653+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
627654
}
628-
defer { FindClose(handle) }
655+
}
629656

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-
}
657+
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 || faAttributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT {
658+
if faAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY {
659+
guard RemoveDirectoryW($0) else {
660+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
661+
}
662+
} else {
663+
guard DeleteFileW($0) else {
664+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [path])
635665
}
666+
}
667+
return
668+
}
636669

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-
}
670+
var stack = [path]
671+
while let directory = stack.popLast() {
672+
do {
673+
guard alreadyConfirmed || shouldRemoveItemAtPath(directory, isURL: isURL) else {
674+
continue
644675
}
645676

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])
677+
let root = URL(fileURLWithPath: directory, isDirectory: true)
678+
try root.withUnsafeNTPath {
679+
if RemoveDirectoryW($0) { return }
680+
guard GetLastError() == ERROR_DIR_NOT_EMPTY else {
681+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [directory])
682+
}
683+
stack.append(directory)
684+
685+
try walk(directory: root) { entry, attributes in
686+
if entry == "." || entry == ".." { return }
687+
688+
let isDirectory = attributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY && attributes & FILE_ATTRIBUTE_REPARSE_POINT == 0
689+
let path = root.appendingPathComponent(entry, isDirectory: isDirectory)
690+
691+
if isDirectory {
692+
stack.append(path.path)
693+
} else {
694+
guard alreadyConfirmed || shouldRemoveItemAtPath(path.path, isURL: isURL) else {
695+
return
696+
}
697+
698+
try path.withUnsafeNTPath {
699+
if attributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
700+
if !SetFileAttributesW($0, attributes & ~FILE_ATTRIBUTE_READONLY) {
701+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [entry])
702+
}
703+
}
704+
705+
if attributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY {
706+
if !RemoveDirectoryW($0) {
707+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [entry])
708+
}
709+
} else {
710+
if !DeleteFileW($0) {
711+
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [entry])
712+
}
713+
}
714+
}
715+
}
716+
}
656717
}
718+
} catch {
719+
if !shouldProceedAfterError(error, removingItemAtPath: directory, isURL: isURL) {
720+
throw error
657721
}
658-
} while FindNextFileW(handle, &ffd)
659-
} catch {
660-
if !shouldProceedAfterError(error, removingItemAtPath: itemPath, isURL: isURL) {
661-
throw error
662722
}
663723
}
664724
}
@@ -970,30 +1030,14 @@ extension FileManager {
9701030
guard let _lastReturned else { return firstValidItem() }
9711031

9721032
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
1033+
try walk(directory: _lastReturned) { entry, attributes in
1034+
if entry == "." || entry == ".." { return }
1035+
if _options.contains(.skipsHiddenFiles) && attributes & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN {
1036+
return
9921037
}
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)
1038+
let isDirectory = attributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY && attributes & FILE_ATTRIBUTE_REPARSE_POINT != FILE_ATTRIBUTE_REPARSE_POINT
1039+
_stack.append(_lastReturned.appendingPathComponent(entry, isDirectory: isDirectory))
1040+
}
9971041
}
9981042

9991043
return firstValidItem()

0 commit comments

Comments
 (0)