Skip to content

Transparently add the \\?\ prefix to Win32 calls for extended length path handling #226

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jakepetroules
Copy link
Member

On Windows, there is a built-in maximum path limitation of 260 characters under most conditions. This can be extended to 32767 characters under either of the following two conditions:

  • Adding the longPathAware attribute to the executable's manifest AND enabling the LongPathsEnabled system-wide registry key or group policy.
  • Ensuring fully qualified paths passed to Win32 APIs are prefixed with \?\

Unfortunately, the former is not realistic for the Swift ecosystem, since it requires developers to have awareness of this specific Windows limitation, AND set longPathAware in their apps' manifest AND expect end users of those apps to change their system configuration.

Instead, this patch transparently prefixes all eligible paths in calls to Win32 APIs with the \?\ prefix to allow them to work with paths longer than 260 characters without requiring the caller of System to manually prefix the paths.

See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more info.

@jakepetroules
Copy link
Member Author

I anticipate there might be some contention about this patch given the general spirit of SwiftSystem not incorporating higher level cross platform translations/behaviors (which should go in Foundation) but wanted to send this up to prompt some discussion of this idea.

I'd like to point out that we already convert UTF-8 strings to UTF-16 and normalize slashes in file paths, so I feel that adding the extended length prefix is still within the spirit of the level of platform translations that SwiftSystem should be doing.

Without doing this, a lot of programs will simply fail to access files whose paths are over the length limit; it's very unrealistic to assume that developers (even those relatively familiar with Windows) know about \?\ or will properly propagate it everywhere it's needed.

Do other folks agree?

@glessard
Copy link
Contributor

This approach doesn't seem unreasonable to me. It's about setting up the syscalls for success, just as we've gone to significant lengths not to clobber the errno state prematurely.

@jakepetroules
Copy link
Member Author

@swift-ci test

@jakepetroules jakepetroules force-pushed the windows-extended-length-path-handling branch from a2cd411 to f008ab0 Compare April 19, 2025 03:32
@jakepetroules
Copy link
Member Author

@swift-ci test

@jakepetroules jakepetroules force-pushed the windows-extended-length-path-handling branch from f008ab0 to eec0886 Compare April 19, 2025 04:28
@jakepetroules
Copy link
Member Author

@swift-ci test

@jakepetroules jakepetroules force-pushed the windows-extended-length-path-handling branch from eec0886 to 208a08e Compare April 19, 2025 17:28
@jakepetroules
Copy link
Member Author

@swift-ci test

// 2. Canonicalize the path.
// This will add the \\?\ prefix if needed based on the path's length.
let capacity = Int16.max
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: numericCast(capacity)) { outBuffer in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is allocating 32767 bytes, and I expect that will be executed as a heap allocation, repeated every time this is called. Usually swift-system tries to avoid doing implicit allocations like that, as clients sometimes get infuriated when they find out that a low-level syscall wrapper library engages in those.

We may have already lost those clients on Windows; I worry that this may make it harder to try winning them back.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to use PathAllocCanonicalize, which has Windows allocate the buffer and can theoretically be smaller, but it's always going to be a heap allocation.

That said, Win32 APIs are not syscalls; they are higher level than the Unix "equivalents". Windows does not have a stable syscall interface and thus it's undocumented. So I think we can get away with some heap allocations where necessary to use the APIs correctly, since many Win32 functions are already doing so internally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the length of the result of the Path*Canonicalize* functions is always less than or equal to the length of the input string plus one (plus two if you've got a trailing NUL), so we don't need to allocate 32KB here.

I also wonder whether we could have a canonicalize-in-place function that mutates the underlying SystemString as a more efficient option.

/// to ensure long paths greater than 260 characters are handled correctly.
///
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
internal func withCanonicalPathRepresentation<Result>(_ body: (Self) throws -> Result) throws -> Result {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should FilePath offer this as a public operation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tempting, but I'd rather leave that to a future enhancement, I'm not sure yet that this should be a public API or what the right shape would be for it, since we'd have to commit to supporting it if we were to make that change.

@jakepetroules
Copy link
Member Author

@swift-ci test

@jakepetroules jakepetroules force-pushed the windows-extended-length-path-handling branch from a706fe6 to f376587 Compare April 22, 2025 20:19
@jakepetroules
Copy link
Member Author

@swift-ci test

@jakepetroules jakepetroules force-pushed the windows-extended-length-path-handling branch from f376587 to 23fece6 Compare April 23, 2025 01:25
@jakepetroules
Copy link
Member Author

@swift-ci test

…path handling

On Windows, there is a built-in maximum path limitation of 260 characters under most conditions. This can be extended to 32767 characters under either of the following two conditions:

- Adding the longPathAware attribute to the executable's manifest AND enabling the LongPathsEnabled system-wide registry key or group policy.
- Ensuring fully qualified paths passed to Win32 APIs are prefixed with \\?\

Unfortunately, the former is not realistic for the Swift ecosystem, since it requires developers to have awareness of this specific Windows limitation, AND set longPathAware in their apps' manifest AND expect end users of those apps to change their system configuration.

Instead, this patch transparently prefixes all eligible paths in calls to Win32 APIs with the \\?\ prefix to allow them to work with paths longer than 260 characters without requiring the caller of System to manually prefix the paths.

See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more info.
@jakepetroules jakepetroules force-pushed the windows-extended-length-path-handling branch from 23fece6 to bc02471 Compare April 23, 2025 01:43
@jakepetroules
Copy link
Member Author

@swift-ci test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants