Skip to content

Commit b748c5e

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 b748c5e

File tree

2 files changed

+15
-39
lines changed

2 files changed

+15
-39
lines changed

Sources/FoundationEssentials/CodableUtilities.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ extension UInt8 {
168168
guard _asciiNumbers.contains(self) else { return nil }
169169
return Int(self &- UInt8(ascii: "0"))
170170
}
171+
172+
internal var isLetter: Bool? {
173+
return (0x41 ... 0x5a) ~= self || (0x61 ... 0x7a) ~= self
174+
}
171175
}
172176

173177

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.utf8.makeIterator()
32+
let bLeadingSlash = if iter.next() == ._slash, iter.next()?.isLetter ?? false, iter.next() == ._colon { 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 Substring(self.utf8.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)