Skip to content

Conversation

daveinglis
Copy link

@daveinglis daveinglis commented Aug 22, 2025

The //?/ prefix needs to be added to directories if the length is greater than MAX_PATH - 12, however the PATHCCH_ALLOW_LONG_PATHS to PathAllocCanonicalize would only do that if the path was greater than MAX_PATH. This change uses PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, which forces it on all the time. Just needed to remove the prefix in a few places as it was making it way out into tests as this is suppose to be transparent to the user.

This also fixes createDirectory() withIntermediateDirectories: true for long paths on Windows.

@daveinglis
Copy link
Author

@swift-ci please test

@daveinglis daveinglis force-pushed the fix_long_window_paths branch from d5ffb8a to 1bf3406 Compare August 25, 2025 16:59
@daveinglis
Copy link
Author

@swift-ci please test

The //?/ prefix needs to be added to directories if the length is
greater than MAX_PATH - 12, however the PATHCCH_ALLOW_LONG_PATHS to
PathAllocCanonicalize would only do that if the path was greater than
MAX_PATH. This change uses PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, which
forces it on all the time.  Just needed to remove the prefix in a few
places as it was making it way out into tests as this is suppose to be
transparent to the user.

This also fixes createDirectory() "withIntermediateDirectories: true"
for long paths on Windows.
@daveinglis daveinglis force-pushed the fix_long_window_paths branch from 1bf3406 to 9192a96 Compare August 27, 2025 17:54
@daveinglis
Copy link
Author

@swift-ci please test

func createDirectoryRecursively(at directoryPath: String) throws {
// Check if directory already exists
var isDirectory: Bool = false
if fileExists(atPath: directoryPath, isDirectory: &isDirectory) {
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 this could open us up to TOC-TOU issues. Since we're already handling ERROR_ALREADY_EXISTS below when we try to create the directory, can we just elide this check and let the error handling of CreateDirectoryW take care of the rest? (I know the error handling below also feels TOC-TOU-like, but given we can't know if the pre-existing result was a directory in the same call, it seems better to have a TOC-TOU issue when deciding whether to throw the error than deciding whether to create the directory to begin with)

Copy link
Author

Choose a reason for hiding this comment

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

yep, that makes sense, will update.

Copy link
Author

@daveinglis daveinglis Aug 29, 2025

Choose a reason for hiding this comment

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

so Im rethinking this.... This first check is recursively walking the parent looking for where to start creating the folders, without it, it would always walk to the root, creating the folders from there, this seems it could be quite the performance hit if this first check is removed, would it not?

@@ -139,6 +139,9 @@ extension FileManager {
fileprivate func _shouldRemoveItemAtPath(_ path: String) -> Bool {
var delegateResponse: Bool?
if let delegate = self.safeDelegate {
#if os(Windows)
let path = path.removingNTObjectNamespacePrefix()
Copy link
Contributor

Choose a reason for hiding this comment

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

This same pattern is used in a lot of delegate calls such as _shouldCopyItemAtPath, _shouldLinkItemAtPath, and _shouldMoveItemAtPath. If it's possible for the file system to contain a file that uses such a path, should we standardize all of these to remove the NT prefix before calling the delegate?

Copy link
Author

Choose a reason for hiding this comment

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

ya, I thought about this but I couldn't see how any of the other delegates would get a prefixed path since none of them passes it in, and I didn't want to add any extra unnecessary overhead. If you feel strongly Im fine with updating the other delegate calls.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I see - that's one part that I wasn't sure of. In what cases do we create paths with the new prefix? Is it just implementations that happen to call through withNTPathRepresentation directly (is that why remove file is different, since it calls the delegate with the NTPath representation instead of the result of withUnsafeFileSystemRepresentation)?

Copy link
Author

Choose a reason for hiding this comment

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

ya, looking at this again, I may be able to fix up the caller so the correct path is aways passed.

@@ -1195,8 +1193,13 @@ private struct FileManagerTests {

#expect((cwd + "/" + dirName + "/" + "lnk").resolvingSymlinksInPath == (cwd + "/" + dirName + "/" + fileName).resolvingSymlinksInPath)

#expect(throws: Never.self) { try fileManager.createDirectory(at: URL(fileURLWithPath: dirName + "/" + "subdir2"), withIntermediateDirectories: false) }
#expect(throws: Never.self) { try fileManager.createDirectory(at: URL(fileURLWithPath: dirName + "/" + "subdir2" + "/" + "subdir3"), withIntermediateDirectories: false) }
#expect(throws: Never.self) { try fileManager.createDirectory(at: URL(fileURLWithPath: dirName + "/" + "subdir1"), withIntermediateDirectories: false) }
Copy link
Contributor

Choose a reason for hiding this comment

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

This creation of <dirName>/subdir1 looks like it was moved from the above, but line 1181 still uses this directory. Was this an intentional move, or can this be moved back to ensure that the expectation on line 1181 (changing the CWD to <dirName>/subdir1) still runs when subdir1 exists?

Copy link
Author

Choose a reason for hiding this comment

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

good catch, I think I move one too many lines, will fix

@jmschonfeld jmschonfeld requested a review from jrflat August 28, 2025 21:04
/// normalize such paths to standard format.
///
/// - Returns: A string with the NT object namespace prefix removed, or the original string if no prefix is found.
package func removingNTObjectNamespacePrefix() -> String {
Copy link
Member

Choose a reason for hiding this comment

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

I think that we should call this removingNTDevicePathPrefix. The NT Object prefix would be \??\.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or maybe just "removingNTPathPrefix", more similar to the underlying API.

If we can trust Wine's implementation to match Windows, it looks like this removes the following prefixes:

There's also the device path prefix \\.\ which PathCchStripPrefix does NOT handle - I checked - and I'm not sure is relevant or would be correct to strip here.

Copy link
Author

Choose a reason for hiding this comment

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

will update

GetCurrentDirectoryW(DWORD($0.count), $0.baseAddress)
}

// Handle Windows NT object namespace prefix
// The \\??\ prefix is used by Windows NT for device paths and may appear
Copy link
Contributor

Choose a reason for hiding this comment

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

The prefix is spelled \\?\ (you have an extra question mark)

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

Successfully merging this pull request may close these issues.

4 participants