Skip to content

Commit 4935298

Browse files
committed
Transparently add the \\?\ prefix to Win32 calls for extended length path handling
This simply updates the implementation of withNTPathRepresentation with what's about to be merged into Foundation via swiftlang/swift-foundation#1257
1 parent 8bec498 commit 4935298

File tree

1 file changed

+62
-5
lines changed

1 file changed

+62
-5
lines changed

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,8 +1185,12 @@ extension Optional where Wrapped == String {
11851185
}
11861186
}
11871187

1188-
// MARK: - Remove these when merging back to SwiftFoundation
11891188
extension String {
1189+
/// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
1190+
/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
1191+
///
1192+
/// - parameter relative: Returns the original path without transforming through GetFullPathNameW + PathCchCanonicalizeEx, if the path is relative.
1193+
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
11901194
internal func withNTPathRepresentation<Result>(
11911195
_ body: (UnsafePointer<WCHAR>) throws -> Result
11921196
) throws -> Result {
@@ -1209,26 +1213,79 @@ extension String {
12091213
return try Substring(self.utf8.dropFirst(bLeadingSlash ? 1 : 0)).withCString(encodedAs: UTF16.self) {
12101214
pwszPath in
12111215
// 1. Normalize the path first.
1216+
// Contrary to the documentation, this works on long paths independently
1217+
// of the registry or process setting to enable long paths (but it will also
1218+
// not add the \\?\ prefix required by other functions under these conditions).
12121219
let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil)
1213-
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
1214-
guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) > 0 else {
1220+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in
1221+
guard (1..<dwLength).contains(GetFullPathNameW(pwszPath, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
12151222
throw SubprocessError(
12161223
code: .init(.invalidWindowsPath(self)),
12171224
underlyingError: .init(rawValue: GetLastError())
12181225
)
12191226
}
12201227

1221-
// 2. Perform the operation on the normalized path.
1222-
return try body($0.baseAddress!)
1228+
// 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
1229+
if let base = pwszFullPath.baseAddress,
1230+
base[0] == UInt16(UInt8._backslash),
1231+
base[1] == UInt16(UInt8._backslash),
1232+
base[2] == UInt16(UInt8._period),
1233+
base[3] == UInt16(UInt8._backslash) {
1234+
return try body(base)
1235+
}
1236+
1237+
// 2. Canonicalize the path.
1238+
// This will add the \\?\ prefix if needed based on the path's length.
1239+
var pwszCanonicalPath: LPWSTR?
1240+
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
1241+
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
1242+
if let pwszCanonicalPath {
1243+
defer { LocalFree(pwszCanonicalPath) }
1244+
if result == S_OK {
1245+
// 3. Perform the operation on the normalized path.
1246+
return try body(pwszCanonicalPath)
1247+
}
1248+
}
1249+
throw SubprocessError(
1250+
code: .init(.invalidWindowsPath(self)),
1251+
underlyingError: .init(rawValue: WIN32_FROM_HRESULT(result))
1252+
)
12231253
}
12241254
}
12251255
}
12261256
}
12271257

1258+
@inline(__always)
1259+
fileprivate func HRESULT_CODE(_ hr: HRESULT) -> DWORD {
1260+
DWORD(hr) & 0xffff
1261+
}
1262+
1263+
@inline(__always)
1264+
fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD {
1265+
DWORD(hr << 16) & 0x1fff
1266+
}
1267+
1268+
@inline(__always)
1269+
fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool {
1270+
hr >= 0
1271+
}
1272+
1273+
// This is a non-standard extension to the Windows SDK that allows us to convert
1274+
// an HRESULT to a Win32 error code.
1275+
@inline(__always)
1276+
fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
1277+
if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) }
1278+
if HRESULT_FACILITY(hr) == FACILITY_WIN32 {
1279+
return HRESULT_CODE(hr)
1280+
}
1281+
return DWORD(hr)
1282+
}
1283+
12281284
extension UInt8 {
12291285
static var _slash: UInt8 { UInt8(ascii: "/") }
12301286
static var _backslash: UInt8 { UInt8(ascii: "\\") }
12311287
static var _colon: UInt8 { UInt8(ascii: ":") }
1288+
static var _period: UInt8 { UInt8(ascii: ".") }
12321289

12331290
var isLetter: Bool? {
12341291
return (0x41...0x5a) ~= self || (0x61...0x7a) ~= self

0 commit comments

Comments
 (0)