Skip to content

Ensure long paths are handled correctly in calls to Win32 APIs #423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SWBUtil/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ add_library(SWBUtil
OutputByteStream.swift
Pair.swift
Path.swift
PathWindows.swift
PbxCp.swift
PluginManager.swift
PluginManagerCommon.swift
Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBUtil/Library.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public enum Library: Sendable {
@_alwaysEmitIntoClient
public static func open(_ path: Path) throws -> LibraryHandle {
#if os(Windows)
guard let handle = path.withPlatformString(LoadLibraryW) else {
guard let handle = try path.withPlatformString({ p in try p.withCanonicalPathRepresentation({ LoadLibraryW($0) }) }) else {
throw LibraryOpenError(message: Win32Error(GetLastError()).description)
}
return LibraryHandle(rawValue: handle)
Expand Down
89 changes: 89 additions & 0 deletions Sources/SWBUtil/PathWindows.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if os(Windows)
import WinSDK

#if canImport(System)
public import System
#else
public import SystemPackage
#endif

extension UnsafePointer where Pointee == CInterop.PlatformChar {
/// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
///
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
public func withCanonicalPathRepresentation<Result>(_ body: (Self) throws -> Result) throws -> Result {
// 1. Normalize the path first.
// Contrary to the documentation, this works on long paths independently
// of the registry or process setting to enable long paths (but it will also
// not add the \\?\ prefix required by other functions under these conditions).
let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil)
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in
guard (1..<dwLength).contains(GetFullPathNameW(self, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
throw Win32Error(GetLastError())
}

// 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
if let base = pwszFullPath.baseAddress,
base[0] == UInt8(ascii: "\\"),
base[1] == UInt8(ascii: "\\"),
base[2] == UInt8(ascii: "."),
base[3] == UInt8(ascii: "\\") {
return try body(base)
}

// 2. Canonicalize the path.
// This will add the \\?\ prefix if needed based on the path's length.
var pwszCanonicalPath: LPWSTR?
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
if let pwszCanonicalPath {
defer { LocalFree(pwszCanonicalPath) }
if result == S_OK {
// 3. Perform the operation on the normalized path.
return try body(pwszCanonicalPath)
}
}
throw Win32Error(WIN32_FROM_HRESULT(result))
}
}
}

@inline(__always)
fileprivate func HRESULT_CODE(_ hr: HRESULT) -> DWORD {
DWORD(hr) & 0xffff
}

@inline(__always)
fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD {
DWORD(hr << 16) & 0x1fff
}

@inline(__always)
fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool {
hr >= 0
}

// This is a non-standard extension to the Windows SDK that allows us to convert
// an HRESULT to a Win32 error code.
@inline(__always)
fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) }
if HRESULT_FACILITY(hr) == FACILITY_WIN32 {
return HRESULT_CODE(hr)
}
return DWORD(hr)
}
#endif
92 changes: 92 additions & 0 deletions Tests/SWBUtilTests/PathWindowsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Testing
import SWBTestSupport
import SWBUtil

@Suite(.requireHostOS(.windows))
fileprivate struct PathWindowsTests {
@Test func testCanonicalPathRepresentation_deviceFiles() throws {
#expect(try "NUL".canonicalPathRepresentation == "\\\\.\\NUL")
#expect(try Path("NUL").canonicalPathRepresentation == "\\\\.\\NUL")

#expect(try "\\\\.\\NUL".canonicalPathRepresentation == "\\\\.\\NUL")

// System.FilePath appends a trailing slash to fully qualified device file names
withKnownIssue { () throws -> () in
#expect(try Path("\\\\.\\NUL").canonicalPathRepresentation == "\\\\.\\NUL")
}
}

@Test func testCanonicalPathRepresentation_driveLetters() throws {
#expect(try Path("C:/").canonicalPathRepresentation == "C:\\")
#expect(try Path("c:/").canonicalPathRepresentation == "c:\\")

#expect(try Path("\\\\?\\C:/").canonicalPathRepresentation == "C:\\")
#expect(try Path("\\\\?\\c:/").canonicalPathRepresentation == "c:\\")
}

@Test func testCanonicalPathRepresentation_absolute() throws {
#expect(try Path("C:" + String(repeating: "/foo/bar/baz", count: 21)).canonicalPathRepresentation == "C:" + String(repeating: "\\foo\\bar\\baz", count: 21))
#expect(try Path("C:" + String(repeating: "/foo/bar/baz", count: 22)).canonicalPathRepresentation == "\\\\?\\C:" + String(repeating: "\\foo\\bar\\baz", count: 22))
}

@Test func testCanonicalPathRepresentation_relative() throws {
let root = Path.root.str.dropLast()
#expect(try Path(String(repeating: "/foo/bar/baz", count: 21)).canonicalPathRepresentation == root + String(repeating: "\\foo\\bar\\baz", count: 21))
#expect(try Path(String(repeating: "/foo/bar/baz", count: 22)).canonicalPathRepresentation == "\\\\?\\" + root + String(repeating: "\\foo\\bar\\baz", count: 22))
}

@Test func testCanonicalPathRepresentation_driveRelative() throws {
let current = Path.currentDirectory

// Ensure the output path will be < 260 characters so we can assert it's not prefixed with \\?\
let chunks = (260 - current.str.count) / "foo/bar/baz/".count
#expect(current.str.count < 248 && chunks > 0, "The current directory is too long for this test.")

#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)
#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)
}
}

fileprivate extension String {
var canonicalPathRepresentation: String {
get throws {
#if os(Windows)
return try withCString(encodedAs: UTF16.self) { platformPath in
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
return String(decodingCString: canonicalPath, as: UTF16.self)
}
}
#else
return self
#endif
}
}
}

fileprivate extension Path {
var canonicalPathRepresentation: String {
get throws {
#if os(Windows)
return try withPlatformString { platformPath in
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
return String(decodingCString: canonicalPath, as: UTF16.self)
}
}
#else
return str
#endif
}
}
}