-
Notifications
You must be signed in to change notification settings - Fork 202
Fix a corner case where createDirectory would fail on windows #1479
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
base: main
Are you sure you want to change the base?
Conversation
f0af396
to
d5ffb8a
Compare
@swift-ci please test |
d5ffb8a
to
1bf3406
Compare
@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.
1bf3406
to
9192a96
Compare
@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) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
/// 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 { |
There was a problem hiding this comment.
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 \??\
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
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.