Skip to content

Commit 23fece6

Browse files
committed
Transparently add the \\?\ prefix to Win32 calls for extended length path handling
On Windows, there is a built-in maximum path limitation of 260 characters under most conditions. This can be extended to 32767 characters under either of the following two conditions: - Adding the longPathAware attribute to the executable's manifest AND enabling the LongPathsEnabled system-wide registry key or group policy. - Ensuring fully qualified paths passed to Win32 APIs are prefixed with \\?\ Unfortunately, the former is not realistic for the Swift ecosystem, since it requires developers to have awareness of this specific Windows limitation, AND set longPathAware in their apps' manifest AND expect end users of those apps to change their system configuration. Instead, this patch transparently prefixes all eligible paths in calls to Win32 APIs with the \\?\ prefix to allow them to work with paths longer than 260 characters without requiring the caller of System to manually prefix the paths. See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more info.
1 parent 5691e92 commit 23fece6

File tree

3 files changed

+100
-28
lines changed

3 files changed

+100
-28
lines changed

Sources/System/FilePath/FilePathTempWindows.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fileprivate func forEachFile(
4040

4141
try searchPath.withPlatformString { szPath in
4242
var findData = WIN32_FIND_DATAW()
43-
let hFind = FindFirstFileW(szPath, &findData)
43+
let hFind = try szPath.withCanonicalPathRepresentation({ szPath in FindFirstFileW(szPath, &findData) })
4444
if hFind == INVALID_HANDLE_VALUE {
4545
throw Errno(windowsError: GetLastError())
4646
}
@@ -95,8 +95,8 @@ internal func _recursiveRemove(
9595
let subpath = path.appending(component)
9696

9797
if (findData.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) == 0 {
98-
try subpath.withPlatformString {
99-
if !DeleteFileW($0) {
98+
try subpath.withPlatformString { subpath in
99+
if try !subpath.withCanonicalPathRepresentation({ DeleteFileW($0) }) {
100100
throw Errno(windowsError: GetLastError())
101101
}
102102
}
@@ -105,7 +105,7 @@ internal func _recursiveRemove(
105105

106106
// Finally, delete the parent
107107
try path.withPlatformString {
108-
if !RemoveDirectoryW($0) {
108+
if try !$0.withCanonicalPathRepresentation({ RemoveDirectoryW($0) }) {
109109
throw Errno(windowsError: GetLastError())
110110
}
111111
}

Sources/System/FilePath/FilePathWindows.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,75 @@ extension SystemString {
461461
return lexer.current
462462
}
463463
}
464+
465+
#if os(Windows)
466+
import WinSDK
467+
468+
extension UnsafePointer where Pointee == CInterop.PlatformChar {
469+
/// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
470+
/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
471+
///
472+
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
473+
internal func withCanonicalPathRepresentation<Result>(_ body: (Self) throws -> Result) throws -> Result {
474+
// 1. Normalize the path first.
475+
// Contrary to the documentation, this works on long paths independently
476+
// of the registry or process setting to enable long paths (but it will also
477+
// not add the \\?\ prefix required by other functions under these conditions).
478+
let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil)
479+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in
480+
guard (1..<dwLength).contains(GetFullPathNameW(self, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
481+
throw Errno(rawValue: _mapWindowsErrorToErrno(GetLastError()))
482+
}
483+
484+
// 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
485+
if let base = pwszFullPath.baseAddress,
486+
base[0] == UInt8(ascii: "\\"),
487+
base[1] == UInt8(ascii: "\\"),
488+
base[2] == UInt8(ascii: "."),
489+
base[3] == UInt8(ascii: "\\") {
490+
return try body(base)
491+
}
492+
493+
// 2. Canonicalize the path.
494+
// This will add the \\?\ prefix if needed based on the path's length.
495+
var pwszCanonicalPath: LPWSTR?
496+
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
497+
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
498+
if let pwszCanonicalPath {
499+
defer { LocalFree(pwszCanonicalPath) }
500+
if result == S_OK {
501+
// 3. Perform the operation on the normalized path.
502+
return try body(pwszCanonicalPath)
503+
}
504+
}
505+
throw Errno(rawValue: _mapWindowsErrorToErrno(WIN32_FROM_HRESULT(result)))
506+
}
507+
}
508+
}
509+
510+
@inline(__always)
511+
fileprivate func HRESULT_CODE(_ hr: HRESULT) -> DWORD {
512+
DWORD(hr) & 0xffff
513+
}
514+
515+
@inline(__always)
516+
fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD {
517+
DWORD(hr << 16) & 0x1fff
518+
}
519+
520+
@inline(__always)
521+
fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool {
522+
hr >= 0
523+
}
524+
525+
// This is a non-standard extension to the Windows SDK that allows us to convert
526+
// an HRESULT to a Win32 error code.
527+
@inline(__always)
528+
fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
529+
if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) }
530+
if HRESULT_FACILITY(hr) == FACILITY_WIN32 {
531+
return HRESULT_CODE(hr)
532+
}
533+
return DWORD(hr)
534+
}
535+
#endif

Sources/System/Internals/WindowsSyscallAdapters.swift

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ internal func open(
3535
bInheritHandle: decodedFlags.bInheritHandle
3636
)
3737

38-
let hFile = CreateFileW(path,
39-
decodedFlags.dwDesiredAccess,
40-
DWORD(FILE_SHARE_DELETE
41-
| FILE_SHARE_READ
42-
| FILE_SHARE_WRITE),
43-
&saAttrs,
44-
decodedFlags.dwCreationDisposition,
45-
decodedFlags.dwFlagsAndAttributes,
46-
nil)
47-
48-
if hFile == INVALID_HANDLE_VALUE {
38+
guard let hFile = try? path.withCanonicalPathRepresentation({ path in
39+
CreateFileW(path,
40+
decodedFlags.dwDesiredAccess,
41+
DWORD(FILE_SHARE_DELETE
42+
| FILE_SHARE_READ
43+
| FILE_SHARE_WRITE),
44+
&saAttrs,
45+
decodedFlags.dwCreationDisposition,
46+
decodedFlags.dwFlagsAndAttributes,
47+
nil)
48+
}), hFile != INVALID_HANDLE_VALUE else {
4949
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
5050
return -1
5151
}
@@ -77,17 +77,17 @@ internal func open(
7777
bInheritHandle: decodedFlags.bInheritHandle
7878
)
7979

80-
let hFile = CreateFileW(path,
81-
decodedFlags.dwDesiredAccess,
82-
DWORD(FILE_SHARE_DELETE
83-
| FILE_SHARE_READ
84-
| FILE_SHARE_WRITE),
85-
&saAttrs,
86-
decodedFlags.dwCreationDisposition,
87-
decodedFlags.dwFlagsAndAttributes,
88-
nil)
89-
90-
if hFile == INVALID_HANDLE_VALUE {
80+
guard let hFile = try? path.withCanonicalPathRepresentation({ path in
81+
CreateFileW(path,
82+
decodedFlags.dwDesiredAccess,
83+
DWORD(FILE_SHARE_DELETE
84+
| FILE_SHARE_READ
85+
| FILE_SHARE_WRITE),
86+
&saAttrs,
87+
decodedFlags.dwCreationDisposition,
88+
decodedFlags.dwFlagsAndAttributes,
89+
nil)
90+
}), hFile != INVALID_HANDLE_VALUE else {
9191
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
9292
return -1
9393
}
@@ -242,7 +242,7 @@ internal func mkdir(
242242
bInheritHandle: false
243243
)
244244

245-
if !CreateDirectoryW(path, &saAttrs) {
245+
guard (try? path.withCanonicalPathRepresentation({ path in CreateDirectoryW(path, &saAttrs) })) == true else {
246246
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
247247
return -1
248248
}
@@ -254,7 +254,7 @@ internal func mkdir(
254254
internal func rmdir(
255255
_ path: UnsafePointer<CInterop.PlatformChar>
256256
) -> CInt {
257-
if !RemoveDirectoryW(path) {
257+
guard (try? path.withCanonicalPathRepresentation({ path in RemoveDirectoryW(path) })) == true else {
258258
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
259259
return -1
260260
}

0 commit comments

Comments
 (0)