Skip to content

Commit 86cc5aa

Browse files
committed
Add Support for '\' in Windows Paths
While URLs get converted to forward slashes on Windows, strings are handled as is and thus need to be able to handle \ separators. Since many Windows apis also support /, we'll handle that as well, but default to \ if required.
1 parent 95b8e5c commit 86cc5aa

File tree

2 files changed

+55
-49
lines changed

2 files changed

+55
-49
lines changed

Foundation/NSPathUtilities.swift

+34-28
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99

1010
import CoreFoundation
1111

12+
#if os(Windows)
13+
let defaultPathSep = "\\"
14+
let validPathSeps: [Character] = ["\\", "/"]
15+
#else
16+
let defaultPathSep = "/"
17+
let validPathSeps: [Character] = ["/"]
18+
#endif
19+
1220
public func NSTemporaryDirectory() -> String {
1321
#if os(Windows)
1422
let cchLength: DWORD = GetTempPathW(0, nil)
@@ -51,8 +59,8 @@ public func NSTemporaryDirectory() -> String {
5159
}
5260
#endif
5361
if let tmpdir = ProcessInfo.processInfo.environment["TMPDIR"] {
54-
if !tmpdir.hasSuffix("/") {
55-
return tmpdir + "/"
62+
if !validPathSeps.contains(where: { tmpdir.hasSuffix(String($0)) }) {
63+
return tmpdir + defaultPathSep
5664
} else {
5765
return tmpdir
5866
}
@@ -70,15 +78,15 @@ public func NSTemporaryDirectory() -> String {
7078
extension String {
7179

7280
internal var _startOfLastPathComponent : String.Index {
73-
precondition(!hasSuffix("/") && length > 1)
81+
precondition(!validPathSeps.contains(where: { hasSuffix(String($0)) }) && length > 1)
7482

7583
let startPos = startIndex
7684
var curPos = endIndex
7785

7886
// Find the beginning of the component
7987
while curPos > startPos {
8088
let prevPos = index(before: curPos)
81-
if self[prevPos] == "/" {
89+
if validPathSeps.contains(self[prevPos]) {
8290
break
8391
}
8492
curPos = prevPos
@@ -88,7 +96,7 @@ extension String {
8896
}
8997

9098
internal var _startOfPathExtension : String.Index? {
91-
precondition(!hasSuffix("/"))
99+
precondition(!validPathSeps.contains(where: { hasSuffix(String($0)) }))
92100

93101
var currentPosition = endIndex
94102
let startOfLastPathComponent = _startOfLastPathComponent
@@ -97,7 +105,7 @@ extension String {
97105
while currentPosition > startOfLastPathComponent {
98106
let previousPosition = index(before: currentPosition)
99107
let character = self[previousPosition]
100-
if character == "/" {
108+
if validPathSeps.contains(character) {
101109
return nil
102110
} else if character == "." {
103111
if startOfLastPathComponent == previousPosition {
@@ -128,10 +136,10 @@ extension String {
128136
if isEmpty {
129137
return str
130138
}
131-
if hasSuffix("/") {
139+
if validPathSeps.contains(where: { hasSuffix(String($0)) }) {
132140
return self + str
133141
}
134-
return self + "/" + str
142+
return self + defaultPathSep + str
135143
}
136144

137145
internal func _stringByFixingSlashes(compress : Bool = true, stripTrailing: Bool = true) -> String {
@@ -142,13 +150,13 @@ extension String {
142150
var curPos = startPos
143151

144152
while curPos < endPos {
145-
if result[curPos] == "/" {
153+
if validPathSeps.contains(result[curPos]) {
146154
var afterLastSlashPos = curPos
147-
while afterLastSlashPos < endPos && result[afterLastSlashPos] == "/" {
155+
while afterLastSlashPos < endPos && validPathSeps.contains(result[afterLastSlashPos]) {
148156
afterLastSlashPos = result.index(after: afterLastSlashPos)
149157
}
150158
if afterLastSlashPos != result.index(after: curPos) {
151-
result.replaceSubrange(curPos ..< afterLastSlashPos, with: ["/"])
159+
result.replaceSubrange(curPos ..< afterLastSlashPos, with: [Character(defaultPathSep)])
152160
endPos = result.endIndex
153161
}
154162
curPos = afterLastSlashPos
@@ -157,7 +165,7 @@ extension String {
157165
}
158166
}
159167
}
160-
if stripTrailing && result.length > 1 && result.hasSuffix("/") {
168+
if stripTrailing && result.length > 1 && validPathSeps.contains(where: {result.hasSuffix(String($0))}) {
161169
result.remove(at: result.index(before: result.endIndex))
162170
}
163171
return result
@@ -223,7 +231,7 @@ extension NSString {
223231

224232
public var deletingLastPathComponent : String {
225233
let fixedSelf = _stringByFixingSlashes()
226-
if fixedSelf == "/" || fixedSelf == "" {
234+
if validPathSeps.contains(where: { String($0) == fixedSelf}) || fixedSelf == "" {
227235
return fixedSelf
228236
}
229237

@@ -235,7 +243,7 @@ extension NSString {
235243

236244
// absolute path, single component
237245
case fixedSelf.index(after: fixedSelf.startIndex):
238-
return "/"
246+
return defaultPathSep
239247

240248
// all common cases
241249
case let startOfLast:
@@ -244,7 +252,7 @@ extension NSString {
244252
}
245253

246254
internal func _stringByFixingSlashes(compress : Bool = true, stripTrailing: Bool = true) -> String {
247-
if _swiftObject == "/" {
255+
if validPathSeps.contains(where: { String($0) == _swiftObject }) {
248256
return _swiftObject
249257
}
250258

@@ -255,13 +263,13 @@ extension NSString {
255263
var curPos = startPos
256264

257265
while curPos < endPos {
258-
if result[curPos] == "/" {
266+
if validPathSeps.contains(result[curPos]) {
259267
var afterLastSlashPos = curPos
260-
while afterLastSlashPos < endPos && result[afterLastSlashPos] == "/" {
268+
while afterLastSlashPos < endPos && validPathSeps.contains(result[afterLastSlashPos]) {
261269
afterLastSlashPos = result.index(after: afterLastSlashPos)
262270
}
263271
if afterLastSlashPos != result.index(after: curPos) {
264-
result.replaceSubrange(curPos ..< afterLastSlashPos, with: ["/"])
272+
result.replaceSubrange(curPos ..< afterLastSlashPos, with: [Character(defaultPathSep)])
265273
endPos = result.endIndex
266274
}
267275
curPos = afterLastSlashPos
@@ -270,7 +278,7 @@ extension NSString {
270278
}
271279
}
272280
}
273-
if stripTrailing && result.hasSuffix("/") {
281+
if stripTrailing && validPathSeps.contains(where: { result.hasSuffix(String($0)) }) {
274282
result.remove(at: result.index(before: result.endIndex))
275283
}
276284
return result
@@ -310,7 +318,7 @@ extension NSString {
310318
}
311319

312320
public func appendingPathExtension(_ str: String) -> String? {
313-
if str.hasPrefix("/") || self == "" || self == "/" {
321+
if validPathSeps.contains(where: { str.hasPrefix(String($0)) }) || self == "" || validPathSeps.contains(where: { String($0)._nsObject == self }) {
314322
print("Cannot append extension \(str) to path \(self)")
315323
return nil
316324
}
@@ -323,7 +331,7 @@ extension NSString {
323331
return _swiftObject
324332
}
325333

326-
let endOfUserName = _swiftObject.firstIndex(of: "/") ?? _swiftObject.endIndex
334+
let endOfUserName = _swiftObject.firstIndex(where : { validPathSeps.contains($0) }) ?? _swiftObject.endIndex
327335
let startOfUserName = _swiftObject.index(after: _swiftObject.startIndex)
328336
let userName = String(_swiftObject[startOfUserName..<endOfUserName])
329337
let optUserName: String? = userName.isEmpty ? nil : userName
@@ -355,12 +363,10 @@ extension NSString {
355363
}
356364

357365
// TODO: pathComponents keeps final path separator if any. Check that logic.
358-
if components.last == "/" && components.count > 1 {
366+
if validPathSeps.contains(where: { String($0) == components.last }) && components.count > 1 {
359367
components.removeLast()
360368
}
361369

362-
let isAbsolutePath = components.first == "/"
363-
364370
var resolvedPath = components.removeFirst()
365371
for component in components {
366372
switch component {
@@ -422,7 +428,7 @@ extension NSString {
422428
let commonPath = searchAllFilesInDirectory ? path : _ensureLastPathSeparator(deletingLastPathComponent)
423429

424430
if searchAllFilesInDirectory {
425-
outputName = "/"
431+
outputName = defaultPathSep
426432
} else {
427433
if let lcp = _longestCommonPrefix(matches, caseSensitive: flag) {
428434
outputName = (commonPath + lcp)
@@ -435,7 +441,7 @@ extension NSString {
435441
}
436442

437443
internal func _stringIsPathToDirectory(_ path: String) -> Bool {
438-
if !path.hasSuffix("/") {
444+
if !validPathSeps.contains(where: { path.hasSuffix(String($0)) }) {
439445
return false
440446
}
441447

@@ -534,11 +540,11 @@ extension NSString {
534540
}
535541

536542
internal func _ensureLastPathSeparator(_ path: String) -> String {
537-
if path.hasSuffix("/") || path.isEmpty {
543+
if validPathSeps.contains(where: { path.hasSuffix(String($0)) }) || path.isEmpty {
538544
return path
539545
}
540546

541-
return path + "/"
547+
return path + defaultPathSep
542548
}
543549

544550
public var fileSystemRepresentation: UnsafePointer<Int8> {

Foundation/NSURL.swift

+21-21
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,27 @@ internal func _pathComponents(_ path: String?) -> [String]? {
4747
let characterView = p
4848
var curPos = characterView.startIndex
4949
let endPos = characterView.endIndex
50-
if characterView[curPos] == "/" {
51-
result.append("/")
50+
if validPathSeps.contains(characterView[curPos]) {
51+
result.append(defaultPathSep)
5252
}
5353

5454
while curPos < endPos {
55-
while curPos < endPos && characterView[curPos] == "/" {
55+
while curPos < endPos && validPathSeps.contains(characterView[curPos]) {
5656
curPos = characterView.index(after: curPos)
5757
}
5858
if curPos == endPos {
5959
break
6060
}
6161
var curEnd = curPos
62-
while curEnd < endPos && characterView[curEnd] != "/" {
62+
while curEnd < endPos && !validPathSeps.contains(characterView[curEnd]) {
6363
curEnd = characterView.index(after: curEnd)
6464
}
6565
result.append(String(characterView[curPos ..< curEnd]))
6666
curPos = curEnd
6767
}
6868
}
69-
if p.length > 1 && p.hasSuffix("/") {
70-
result.append("/")
69+
if p.length > 1 && validPathSeps.contains(where: { p.hasSuffix(String($0)) }) {
70+
result.append(defaultPathSep)
7171
}
7272
return result
7373
}
@@ -337,7 +337,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
337337
let thePath = _standardizedPath(path)
338338

339339
var isDir: ObjCBool = false
340-
if thePath.hasSuffix("/") {
340+
if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) {
341341
isDir = true
342342
} else {
343343
let absolutePath: String
@@ -367,7 +367,7 @@ open class NSURL : NSObject, NSSecureCoding, NSCopying {
367367
}
368368

369369
var isDir: ObjCBool = false
370-
if thePath.hasSuffix("/") {
370+
if validPathSeps.contains(where: { thePath.hasSuffix(String($0)) }) {
371371
isDir = true
372372
} else {
373373
if !FileManager.default.fileExists(atPath: path, isDirectory: &isDir) {
@@ -860,7 +860,7 @@ extension NSURL {
860860
*/
861861
open class func fileURL(withPathComponents components: [String]) -> URL? {
862862
let path = NSString.pathWithComponents(components)
863-
if components.last == "/" {
863+
if validPathSeps.contains(where: { String($0) == components.last }) {
864864
return URL(fileURLWithPath: path, isDirectory: true)
865865
} else {
866866
return URL(fileURLWithPath: path)
@@ -872,7 +872,7 @@ extension NSURL {
872872
return nil
873873
}
874874

875-
if p == "/" {
875+
if validPathSeps.contains(where: { String($0) == p }) {
876876
return p
877877
}
878878

@@ -883,13 +883,13 @@ extension NSURL {
883883
var curPos = startPos
884884

885885
while curPos < endPos {
886-
if result[curPos] == "/" {
886+
if validPathSeps.contains(result[curPos]) {
887887
var afterLastSlashPos = curPos
888-
while afterLastSlashPos < endPos && result[afterLastSlashPos] == "/" {
888+
while afterLastSlashPos < endPos && validPathSeps.contains(result[afterLastSlashPos]) {
889889
afterLastSlashPos = result.index(after: afterLastSlashPos)
890890
}
891891
if afterLastSlashPos != result.index(after: curPos) {
892-
result.replaceSubrange(curPos ..< afterLastSlashPos, with: ["/"])
892+
result.replaceSubrange(curPos ..< afterLastSlashPos, with: [Character(defaultPathSep)])
893893
endPos = result.endIndex
894894
}
895895
curPos = afterLastSlashPos
@@ -898,7 +898,7 @@ extension NSURL {
898898
}
899899
}
900900
}
901-
if stripTrailing && result.hasSuffix("/") {
901+
if stripTrailing && validPathSeps.contains(where: { result.hasSuffix(String($0)) }) {
902902
result.remove(at: result.index(before: result.endIndex))
903903
}
904904
return result
@@ -936,7 +936,7 @@ extension NSURL {
936936

937937
open func appendingPathComponent(_ pathComponent: String) -> URL? {
938938
var result : URL? = appendingPathComponent(pathComponent, isDirectory: false)
939-
if !pathComponent.hasSuffix("/") && isFileURL {
939+
if !validPathSeps.contains(where: { pathComponent.hasSuffix(String($0)) }) && isFileURL {
940940
if let urlWithoutDirectory = result {
941941
var isDir: ObjCBool = false
942942
if FileManager.default.fileExists(atPath: urlWithoutDirectory.path, isDirectory: &isDir) && isDir.boolValue {
@@ -1025,8 +1025,8 @@ extension NSURL {
10251025
resolvedPath = resolvedPath._tryToRemovePathPrefix("/private") ?? resolvedPath
10261026
}
10271027

1028-
if isExistingDirectory.boolValue && !resolvedPath.hasSuffix("/") {
1029-
resolvedPath += "/"
1028+
if isExistingDirectory.boolValue && !validPathSeps.contains(where: { resolvedPath.hasSuffix(String($0)) }) {
1029+
resolvedPath += defaultPathSep
10301030
}
10311031

10321032
return URL(fileURLWithPath: resolvedPath)
@@ -1035,15 +1035,15 @@ extension NSURL {
10351035
fileprivate func _pathByRemovingDots(_ comps: [String]) -> String {
10361036
var components = comps
10371037

1038-
if(components.last == "/") {
1038+
if validPathSeps.contains(where: { String($0) == components.last }) {
10391039
components.removeLast()
10401040
}
10411041

10421042
guard !components.isEmpty else {
10431043
return self.path!
10441044
}
10451045

1046-
let isAbsolutePath = components.first == "/"
1046+
let isAbsolutePath = components.first?.isAbsolutePath ?? false
10471047
var result : String = components.removeFirst()
10481048

10491049
for component in components {
@@ -1057,8 +1057,8 @@ extension NSURL {
10571057
}
10581058
}
10591059

1060-
if(self.path!.hasSuffix("/")) {
1061-
result += "/"
1060+
if validPathSeps.contains(where: { self.path!.hasSuffix(String($0)) }) {
1061+
result += defaultPathSep
10621062
}
10631063

10641064
return result

0 commit comments

Comments
 (0)