Skip to content

Commit 5ef6689

Browse files
authored
Merge pull request #19 from jakepetroules/long-path-support
Transparently add the \\?\ prefix to Win32 calls for extended length path handling
2 parents 87444c8 + 4935298 commit 5ef6689

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)