Skip to content

Commit a706fe6

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 a706fe6

File tree

3 files changed

+122
-24
lines changed

3 files changed

+122
-24
lines changed

Sources/System/FilePath/FilePathTempWindows.swift

+4-4
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

+72
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 GetFullPathNameW(self, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil) == dwLength - 1 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

+46-20
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,23 @@ 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)
38+
let hFile: HANDLE
39+
do {
40+
hFile = try path.withCanonicalPathRepresentation({ path in
41+
CreateFileW(path,
42+
decodedFlags.dwDesiredAccess,
43+
DWORD(FILE_SHARE_DELETE
44+
| FILE_SHARE_READ
45+
| FILE_SHARE_WRITE),
46+
&saAttrs,
47+
decodedFlags.dwCreationDisposition,
48+
decodedFlags.dwFlagsAndAttributes,
49+
nil)
50+
})
51+
} catch {
52+
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
53+
return -1
54+
}
4755

4856
if hFile == INVALID_HANDLE_VALUE {
4957
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
@@ -77,15 +85,23 @@ internal func open(
7785
bInheritHandle: decodedFlags.bInheritHandle
7886
)
7987

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)
88+
let hFile: HANDLE
89+
do {
90+
hFile = try path.withCanonicalPathRepresentation({ path in
91+
CreateFileW(path,
92+
decodedFlags.dwDesiredAccess,
93+
DWORD(FILE_SHARE_DELETE
94+
| FILE_SHARE_READ
95+
| FILE_SHARE_WRITE),
96+
&saAttrs,
97+
decodedFlags.dwCreationDisposition,
98+
decodedFlags.dwFlagsAndAttributes,
99+
nil)
100+
})
101+
} catch {
102+
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
103+
return -1
104+
}
89105

90106
if hFile == INVALID_HANDLE_VALUE {
91107
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
@@ -242,7 +258,12 @@ internal func mkdir(
242258
bInheritHandle: false
243259
)
244260

245-
if !CreateDirectoryW(path, &saAttrs) {
261+
do {
262+
if try !path.withCanonicalPathRepresentation({ path in CreateDirectoryW(path, &saAttrs) }) {
263+
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
264+
return -1
265+
}
266+
} catch {
246267
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
247268
return -1
248269
}
@@ -254,7 +275,12 @@ internal func mkdir(
254275
internal func rmdir(
255276
_ path: UnsafePointer<CInterop.PlatformChar>
256277
) -> CInt {
257-
if !RemoveDirectoryW(path) {
278+
do {
279+
if try !path.withCanonicalPathRepresentation({ path in RemoveDirectoryW(path) }) {
280+
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
281+
return -1
282+
}
283+
} catch {
258284
ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError()))
259285
return -1
260286
}

0 commit comments

Comments
 (0)