-
Notifications
You must be signed in to change notification settings - Fork 97
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.