Skip to content

Commit f008ab0

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 1b8167d commit f008ab0

File tree

3 files changed

+86
-24
lines changed

3 files changed

+86
-24
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,39 @@ extension SystemString {
461461
return lexer.current
462462
}
463463
}
464+
465+
#if os(Windows)
466+
extension UnsafePointer where Pointee == CInterop.PlatformChar {
467+
/// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
468+
/// to ensure long paths greater than 260 characters are handled correctly.
469+
///
470+
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
471+
internal func withCanonicalPathRepresentation<Result>(_ body: (Self) throws -> Result) throws -> Result {
472+
// 1. Normalize the path first.
473+
// Contrary to the documentation, this works on long paths independently
474+
// of the registry or process setting to enable long paths (but it will also
475+
// not add the \\?\ prefix required by other functions under these conditions).
476+
let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil)
477+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { fullPath in
478+
guard GetFullPathNameW(self, DWORD(fullPath.count), fullPath.baseAddress, nil) > 0 else {
479+
throw Errno(rawValue: _mapWindowsErrorToErrno(GetLastError()))
480+
}
481+
482+
// 2. Canonicalize the path.
483+
// This will add the \\?\ prefix if needed based on the path's length.
484+
let capacity = Int16.max
485+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: numericCast(capacity)) { outBuffer in
486+
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
487+
let result = PathCchCanonicalizeEx(outBuffer.baseAddress, numericCast(capacity), fullPath.baseAddress, flags)
488+
switch result {
489+
case S_OK:
490+
// 3. Perform the operation on the normalized path.
491+
return try body(outBuffer.baseAddress!)
492+
default:
493+
throw Errno(rawValue: _mapWindowsErrorToErrno(GetLastError()))
494+
}
495+
}
496+
}
497+
}
498+
}
499+
#endif

Sources/System/Internals/WindowsSyscallAdapters.swift

Lines changed: 46 additions & 20 deletions
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 !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)