Skip to content

Commit ac358da

Browse files
committed
Ensure long paths are handled correctly in calls to Win32 APIs
Use the same technique being proposed in SwiftSystem and partly-implemented in Foundation. As a follow-on change, we also need to stop using POSIX filesystem APIs, which are not Unicode or long path aware.
1 parent 2f36148 commit ac358da

File tree

4 files changed

+183
-1
lines changed

4 files changed

+183
-1
lines changed

Sources/SWBUtil/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ add_library(SWBUtil
6868
OutputByteStream.swift
6969
Pair.swift
7070
Path.swift
71+
PathWindows.swift
7172
PbxCp.swift
7273
PluginManager.swift
7374
PluginManagerCommon.swift

Sources/SWBUtil/Library.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public enum Library: Sendable {
3333
@_alwaysEmitIntoClient
3434
public static func open(_ path: Path) throws -> LibraryHandle {
3535
#if os(Windows)
36-
guard let handle = path.withPlatformString(LoadLibraryW) else {
36+
guard let handle = try path.withPlatformString({ p in try p.withCanonicalPathRepresentation({ LoadLibraryW($0) }) }) else {
3737
throw LibraryOpenError(message: Win32Error(GetLastError()).description)
3838
}
3939
return LibraryHandle(rawValue: handle)

Sources/SWBUtil/PathWindows.swift

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if os(Windows)
14+
import WinSDK
15+
16+
#if canImport(System)
17+
public import System
18+
#else
19+
public import SystemPackage
20+
#endif
21+
22+
extension UnsafePointer where Pointee == CInterop.PlatformChar {
23+
/// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
24+
/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
25+
///
26+
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
27+
public func withCanonicalPathRepresentation<Result>(_ body: (Self) throws -> Result) throws -> Result {
28+
// 1. Normalize the path first.
29+
// Contrary to the documentation, this works on long paths independently
30+
// of the registry or process setting to enable long paths (but it will also
31+
// not add the \\?\ prefix required by other functions under these conditions).
32+
let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil)
33+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in
34+
guard (1..<dwLength).contains(GetFullPathNameW(self, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
35+
throw Win32Error(GetLastError())
36+
}
37+
38+
// 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
39+
if let base = pwszFullPath.baseAddress,
40+
base[0] == UInt8(ascii: "\\"),
41+
base[1] == UInt8(ascii: "\\"),
42+
base[2] == UInt8(ascii: "."),
43+
base[3] == UInt8(ascii: "\\") {
44+
return try body(base)
45+
}
46+
47+
// 2. Canonicalize the path.
48+
// This will add the \\?\ prefix if needed based on the path's length.
49+
var pwszCanonicalPath: LPWSTR?
50+
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
51+
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
52+
if let pwszCanonicalPath {
53+
defer { LocalFree(pwszCanonicalPath) }
54+
if result == S_OK {
55+
// 3. Perform the operation on the normalized path.
56+
return try body(pwszCanonicalPath)
57+
}
58+
}
59+
throw Win32Error(WIN32_FROM_HRESULT(result))
60+
}
61+
}
62+
}
63+
64+
@inline(__always)
65+
fileprivate func HRESULT_CODE(_ hr: HRESULT) -> DWORD {
66+
DWORD(hr) & 0xffff
67+
}
68+
69+
@inline(__always)
70+
fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD {
71+
DWORD(hr << 16) & 0x1fff
72+
}
73+
74+
@inline(__always)
75+
fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool {
76+
hr >= 0
77+
}
78+
79+
// This is a non-standard extension to the Windows SDK that allows us to convert
80+
// an HRESULT to a Win32 error code.
81+
@inline(__always)
82+
fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
83+
if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) }
84+
if HRESULT_FACILITY(hr) == FACILITY_WIN32 {
85+
return HRESULT_CODE(hr)
86+
}
87+
return DWORD(hr)
88+
}
89+
#endif
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Testing
14+
import SWBTestSupport
15+
import SWBUtil
16+
17+
@Suite(.requireHostOS(.windows))
18+
fileprivate struct PathWindowsTests {
19+
@Test func testCanonicalPathRepresentation_deviceFiles() throws {
20+
#expect(try "NUL".canonicalPathRepresentation == "\\\\.\\NUL")
21+
#expect(try Path("NUL").canonicalPathRepresentation == "\\\\.\\NUL")
22+
23+
#expect(try "\\\\.\\NUL".canonicalPathRepresentation == "\\\\.\\NUL")
24+
25+
// System.FilePath appends a trailing slash to fully qualified device file names
26+
withKnownIssue { () throws -> () in
27+
#expect(try Path("\\\\.\\NUL").canonicalPathRepresentation == "\\\\.\\NUL")
28+
}
29+
}
30+
31+
@Test func testCanonicalPathRepresentation_driveLetters() throws {
32+
#expect(try Path("C:/").canonicalPathRepresentation == "C:\\")
33+
#expect(try Path("c:/").canonicalPathRepresentation == "c:\\")
34+
35+
#expect(try Path("\\\\?\\C:/").canonicalPathRepresentation == "C:\\")
36+
#expect(try Path("\\\\?\\c:/").canonicalPathRepresentation == "c:\\")
37+
}
38+
39+
@Test func testCanonicalPathRepresentation_absolute() throws {
40+
#expect(try Path("C:" + String(repeating: "/foo/bar/baz", count: 21)).canonicalPathRepresentation == "C:" + String(repeating: "\\foo\\bar\\baz", count: 21))
41+
#expect(try Path("C:" + String(repeating: "/foo/bar/baz", count: 22)).canonicalPathRepresentation == "\\\\?\\C:" + String(repeating: "\\foo\\bar\\baz", count: 22))
42+
}
43+
44+
@Test func testCanonicalPathRepresentation_relative() throws {
45+
let root = Path.root.str.dropLast()
46+
#expect(try Path(String(repeating: "/foo/bar/baz", count: 21)).canonicalPathRepresentation == root + String(repeating: "\\foo\\bar\\baz", count: 21))
47+
#expect(try Path(String(repeating: "/foo/bar/baz", count: 22)).canonicalPathRepresentation == "\\\\?\\" + root + String(repeating: "\\foo\\bar\\baz", count: 22))
48+
}
49+
50+
@Test func testCanonicalPathRepresentation_driveRelative() throws {
51+
let current = Path.currentDirectory
52+
53+
// Ensure the output path will be < 260 characters so we can assert it's not prefixed with \\?\
54+
let chunks = (260 - current.str.count) / "foo/bar/baz/".count
55+
#expect(current.str.count < 248 && chunks > 0, "The current directory is too long for this test.")
56+
57+
#expect(try Path(current.str.prefix(2) + String(repeating: "foo/bar/baz/", count: chunks)).canonicalPathRepresentation == current.join(String(repeating: "\\foo\\bar\\baz", count: chunks)).str)
58+
#expect(try Path(current.str.prefix(2) + String(repeating: "foo/bar/baz/", count: 22)).canonicalPathRepresentation == "\\\\?\\" + current.join(String(repeating: "\\foo\\bar\\baz", count: 22)).str)
59+
}
60+
}
61+
62+
fileprivate extension String {
63+
var canonicalPathRepresentation: String {
64+
get throws {
65+
#if os(Windows)
66+
return try withCString(encodedAs: UTF16.self) { platformPath in
67+
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
68+
return String(decodingCString: canonicalPath, as: UTF16.self)
69+
}
70+
}
71+
#else
72+
return self
73+
#endif
74+
}
75+
}
76+
}
77+
78+
fileprivate extension Path {
79+
var canonicalPathRepresentation: String {
80+
get throws {
81+
#if os(Windows)
82+
return try withPlatformString { platformPath in
83+
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
84+
return String(decodingCString: canonicalPath, as: UTF16.self)
85+
}
86+
}
87+
#else
88+
return str
89+
#endif
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)