Skip to content

Commit d5ffb8a

Browse files
committed
Fix a corner case where createDirectory would fail on windows
The //?/ prefix needs to be added to directories if the length is > MAX_PATH - 12, however the PATHCCH_ALLOW_LONG_PATHS to PathAllocCanonicalize would only do that if the path was > MAX_PATH. This change uses PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, which forces it on all the time. Just needed to remove the prefix in a few places as it was making it way out into tests as this is suppose to be transparent to the user.
1 parent 574d608 commit d5ffb8a

File tree

6 files changed

+45
-12
lines changed

6 files changed

+45
-12
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,9 +497,14 @@ extension _FileManagerImpl {
497497
// This is solely to minimize the number of allocations and number of bytes allocated versus starting with a hardcoded value like MAX_PATH.
498498
// We should NOT early-return if this returns 0, in order to avoid TOCTOU issues.
499499
let dwSize = GetCurrentDirectoryW(0, nil)
500-
return try? FillNullTerminatedWideStringBuffer(initialSize: dwSize >= 0 ? dwSize : DWORD(MAX_PATH), maxSize: DWORD(Int16.max)) {
500+
let cwd = try? FillNullTerminatedWideStringBuffer(initialSize: dwSize >= 0 ? dwSize : DWORD(MAX_PATH), maxSize: DWORD(Int16.max)) {
501501
GetCurrentDirectoryW(DWORD($0.count), $0.baseAddress)
502502
}
503+
504+
// Handle Windows NT object namespace prefix
505+
// The \\??\ prefix is used by Windows NT for device paths and may appear
506+
// in current working directory paths. We strip it to return a standard path.
507+
return cwd?.removingNTObjectNamespacePrefix()
503508
#else
504509
withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in
505510
guard getcwd(buffer.baseAddress!, FileManager.MAX_PATH_SIZE) != nil else {

Sources/FoundationEssentials/FileManager/FileOperations.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ extension FileManager {
139139
fileprivate func _shouldRemoveItemAtPath(_ path: String) -> Bool {
140140
var delegateResponse: Bool?
141141
if let delegate = self.safeDelegate {
142+
#if os(Windows)
143+
let path = path.removingNTObjectNamespacePrefix()
144+
#endif
142145
#if FOUNDATION_FRAMEWORK
143146
delegateResponse = delegate.fileManager?(self, shouldRemoveItemAt: URL(fileURLWithPath: path))
144147

@@ -393,7 +396,7 @@ enum _FileOperations {
393396
} else {
394397
if entry.dwFileAttributes & FILE_ATTRIBUTE_READONLY == FILE_ATTRIBUTE_READONLY {
395398
guard SetFileAttributesW($0, entry.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY) else {
396-
throw CocoaError.removeFileError(GetLastError(), ntpath)
399+
throw CocoaError.removeFileError(GetLastError(), entry.fileName)
397400
}
398401
}
399402
if entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY {

Sources/FoundationEssentials/String/String+Internals.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ extension String {
6666
// 2. Canonicalize the path.
6767
// This will add the \\?\ prefix if needed based on the path's length.
6868
var pwszCanonicalPath: LPWSTR?
69-
let flags: ULONG = PATHCCH_ALLOW_LONG_PATHS
69+
// Alway add the long path prefix since we don't know if this is a directory.
70+
let flags: ULONG = PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH
7071
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
7172
if let pwszCanonicalPath {
7273
defer { LocalFree(pwszCanonicalPath) }
@@ -79,6 +80,28 @@ extension String {
7980
}
8081
}
8182
}
83+
/// Removes the Windows NT object namespace prefix if present.
84+
/// The \\??\ prefix is used by Windows NT for device paths and may appear
85+
/// in paths returned by system APIs. This method provides a clean way to
86+
/// normalize such paths to standard format.
87+
///
88+
/// - Returns: A string with the NT object namespace prefix removed, or the original string if no prefix is found.
89+
package func removingNTObjectNamespacePrefix() -> String {
90+
// Define the NT object namespace prefix as a constant for maintainability
91+
let ntObjectPrefix = "\\\\?\\"
92+
93+
// Use early return for better readability and performance
94+
guard hasPrefix(ntObjectPrefix) else {
95+
return self
96+
}
97+
98+
// Validate that we have content after the prefix to avoid returning empty strings
99+
guard count > ntObjectPrefix.count else {
100+
return self
101+
}
102+
103+
return String(dropFirst(ntObjectPrefix.count))
104+
}
82105
}
83106
#endif
84107

Sources/FoundationEssentials/String/String+Path.swift

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -741,15 +741,7 @@ extension String {
741741
guard GetFinalPathNameByHandleW(hFile, $0.baseAddress, dwLength, VOLUME_NAME_DOS) == dwLength - 1 else {
742742
return nil
743743
}
744-
745-
let pathBaseAddress: UnsafePointer<WCHAR>
746-
if Array($0.prefix(4)) == Array(#"\\?\"#.utf16) {
747-
// When using `VOLUME_NAME_DOS`, the returned path uses `\\?\`.
748-
pathBaseAddress = UnsafePointer($0.baseAddress!.advanced(by: 4))
749-
} else {
750-
pathBaseAddress = UnsafePointer($0.baseAddress!)
751-
}
752-
return String(decodingCString: pathBaseAddress, as: UTF16.self)
744+
return String(decodingCString: UnsafePointer($0.baseAddress!), as: UTF16.self).removingNTObjectNamespacePrefix()
753745
}
754746
}
755747
#else // os(Windows)

Sources/FoundationEssentials/WinSDK+Extensions.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ package var PATHCCH_ALLOW_LONG_PATHS: ULONG {
229229
ULONG(WinSDK.PATHCCH_ALLOW_LONG_PATHS.rawValue)
230230
}
231231

232+
package var PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH: ULONG {
233+
ULONG(WinSDK.PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH.rawValue)
234+
}
235+
232236
package var RRF_RT_REG_SZ: DWORD {
233237
DWORD(WinSDK.RRF_RT_REG_SZ)
234238
}

Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,12 @@ private struct FileManagerTests {
11331133
let fileName = UUID().uuidString
11341134
let cwd = fileManager.currentDirectoryPath
11351135

1136+
#expect(fileManager.changeCurrentDirectoryPath(cwd))
1137+
#expect(cwd == fileManager.currentDirectoryPath)
1138+
1139+
let nearLimitDir = cwd + "/" + String(repeating: "A", count: 255 - cwd.count)
1140+
#expect(throws: Never.self) { try fileManager.createDirectory(at: URL(fileURLWithPath: nearLimitDir), withIntermediateDirectories: false) }
1141+
11361142
#expect(fileManager.createFile(atPath: dirName + "/" + fileName, contents: nil))
11371143

11381144
let dirURL = URL(filePath: dirName, directoryHint: .checkFileSystem)

0 commit comments

Comments
 (0)