Skip to content

Commit 3c2147a

Browse files
committed
FoundationEssentials: simplify path normalization
We would previously conditionally call `GetFullPathNameW` and do string manipulations for normalising the path. Instead, always call `GetFullPathNameW` to normalise the path as per Windows' rules. This more importantly will collapse runs of the arc separator, which is crucial when using extended paths as the NT path form must always use the normalised paths or the access will fail.
1 parent df5fa4e commit 3c2147a

File tree

1 file changed

+11
-39
lines changed

1 file changed

+11
-39
lines changed

Sources/FoundationEssentials/String/String+Internals.swift

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,51 +28,23 @@ extension String {
2828
throw CocoaError.errorWithFilePath(.fileReadInvalidFileName, "")
2929
}
3030

31-
// 1. Normalize the path first.
32-
33-
var path = self
31+
var iter = self.makeIterator()
32+
let bLeadingSlash = if iter.next() == "/", iter.next()?.isLetter ?? false, iter.next() == ":" { true } else { false }
3433

3534
// Strip the leading `/` on a RFC8089 path (`/[drive-letter]:/...` ). A
3635
// leading slash indicates a rooted path on the drive for the current
3736
// working directory.
38-
var iter = path.makeIterator()
39-
if iter.next() == "/", iter.next()?.isLetter ?? false, iter.next() == ":" {
40-
path.removeFirst()
41-
}
42-
43-
// Win32 APIs can support `/` for the arc separator. However,
44-
// symlinks created with `/` do not resolve properly, so normalize
45-
// the path.
46-
path.replace("/", with: "\\")
47-
48-
// Drop trailing slashes unless it follows a drive specification. The
49-
// trailing arc separator after a drive specifier indicates the root as
50-
// opposed to a drive relative path.
51-
while path.count > 1, path.last == "\\" {
52-
let first = path.startIndex
53-
let second = path.index(after: path.startIndex)
54-
if path.count == 3, path[first].isLetter, path[second] == ":" {
55-
break;
56-
}
57-
path.removeLast()
58-
}
59-
60-
// 2. Perform the operation on the normalized path.
61-
62-
return try path.withCString(encodedAs: UTF16.self) { pwszPath in
63-
guard !path.hasPrefix(#"\\"#) else { return try body(pwszPath) }
64-
65-
let dwLength = GetFullPathNameW(pwszPath, 0, nil, nil)
66-
let path = try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
67-
guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) == dwLength - 1 else {
68-
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
37+
return try self.dropFirst(bLeadingSlash ? 1 : 0).withCString(encodedAs: UTF16.self) { pwszPath in
38+
// 1. Normalize the path first.
39+
let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil)
40+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
41+
guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) > 0 else {
42+
throw CocoaError.errorWithFilePath(self, win32: GetLastError(), reading: true)
6943
}
70-
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
71-
}
72-
guard !path.hasPrefix(#"\\"#) else {
73-
return try path.withCString(encodedAs: UTF16.self, body)
44+
45+
// 2. Perform the operation on the normalized path.
46+
return try body($0.baseAddress!)
7447
}
75-
return try #"\\?\\#(path)"#.withCString(encodedAs: UTF16.self, body)
7648
}
7749
}
7850
}

0 commit comments

Comments
 (0)