Skip to content

Commit d72791a

Browse files
committed
Standardize backslashes before string path processing on Windows
1 parent 7f9238e commit d72791a

File tree

2 files changed

+64
-15
lines changed

2 files changed

+64
-15
lines changed

Sources/FoundationEssentials/String/String+Path.swift

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,24 @@ import WinSDK
2222

2323
internal import _FoundationCShims
2424

25+
extension StringProtocol {
26+
fileprivate func _standardizingSlashes() -> String {
27+
#if os(Windows)
28+
// The string functions below all assume that the path separator is a forward slash
29+
// Standardize the path to use forward slashes before processing for consistency
30+
return self.replacing(._backslash, with: ._slash)
31+
#else
32+
return String(self)
33+
#endif
34+
}
35+
}
36+
2537
extension String {
2638
internal func deletingLastPathComponent() -> String {
39+
_standardizingSlashes()._deletingLastPathComponent()
40+
}
41+
42+
private func _deletingLastPathComponent() -> String {
2743
let lastSlash = self.lastIndex { $0 == "/" }
2844
guard let lastSlash else {
2945
// No slash
@@ -50,6 +66,10 @@ extension String {
5066
}
5167

5268
internal func appendingPathComponent(_ component: String) -> String {
69+
_standardizingSlashes()._appendingPathComponent(component)
70+
}
71+
72+
private func _appendingPathComponent(_ component: String) -> String {
5373
var result = self
5474
if !component.isEmpty {
5575
var needsSlash = true
@@ -103,6 +123,10 @@ extension String {
103123
}
104124

105125
internal var lastPathComponent: String {
126+
_standardizingSlashes()._lastPathComponent
127+
}
128+
129+
private var _lastPathComponent: String {
106130
let lastSlash = self.lastIndex { $0 == "/" }
107131
guard let lastSlash else {
108132
// No slash, just return self
@@ -170,11 +194,11 @@ extension String {
170194
return false
171195
}
172196
if let lastDot = pathExtension.utf8.lastIndex(of: UInt8(ascii: ".")) {
173-
let beforeDot = pathExtension[..<lastDot].unicodeScalars
174-
let afterDot = pathExtension[pathExtension.index(after: lastDot)...].unicodeScalars
197+
let beforeDot = pathExtension[..<lastDot]._standardizingSlashes().unicodeScalars
198+
let afterDot = pathExtension[pathExtension.index(after: lastDot)...]._standardizingSlashes().unicodeScalars
175199
return beforeDot.allSatisfy { $0 != "/" } && afterDot.allSatisfy { !String.invalidExtensionScalars.contains($0) }
176200
} else {
177-
return pathExtension.unicodeScalars.allSatisfy { !String.invalidExtensionScalars.contains($0) }
201+
return pathExtension._standardizingSlashes().unicodeScalars.allSatisfy { !String.invalidExtensionScalars.contains($0) }
178202
}
179203
}
180204

@@ -202,6 +226,10 @@ extension String {
202226
}
203227

204228
internal func merging(relativePath: String) -> String {
229+
_standardizingSlashes()._merging(relativePath: relativePath)
230+
}
231+
232+
private func _merging(relativePath: String) -> String {
205233
guard relativePath.utf8.first != UInt8(ascii: "/") else {
206234
return relativePath
207235
}
@@ -212,6 +240,10 @@ extension String {
212240
}
213241

214242
internal var removingDotSegments: String {
243+
_standardizingSlashes()._removingDotSegments
244+
}
245+
246+
private var _removingDotSegments: String {
215247
let input = self.utf8
216248
guard !input.isEmpty else {
217249
return ""
@@ -440,18 +472,16 @@ extension String {
440472

441473
// From swift-corelibs-foundation's NSTemporaryDirectory. Internal for now, pending a better public API.
442474
internal static var temporaryDirectoryPath: String {
443-
#if os(Windows)
444-
let validPathSeps: [Character] = ["\\", "/"]
445-
#else
446-
let validPathSeps: [Character] = ["/"]
447-
#endif
448-
449475
func normalizedPath(with path: String) -> String {
450-
if validPathSeps.contains(where: { path.hasSuffix(String($0)) }) {
476+
guard path.utf8.last != ._slash else {
477+
return path
478+
}
479+
#if os(Windows)
480+
guard path.utf8.last != ._backslash else {
451481
return path
452-
} else {
453-
return path + String(validPathSeps.last!)
454482
}
483+
#endif
484+
return path + "/"
455485
}
456486
#if os(Windows)
457487
let cchLength: DWORD = GetTempPathW(0, nil)
@@ -547,7 +577,7 @@ extension String {
547577
static var NETWORK_PREFIX: String { #"\\"# }
548578

549579
private var _standardizingPath: String {
550-
var result = _transmutingCompressingSlashes()._droppingTrailingSlashes
580+
var result = _standardizingSlashes()._transmutingCompressingSlashes()._droppingTrailingSlashes
551581
let postNetStart = if result.starts(with: String.NETWORK_PREFIX) {
552582
result.firstIndex(of: "/") ?? result.endIndex
553583
} else {
@@ -558,7 +588,7 @@ extension String {
558588
result = resolved
559589
}
560590

561-
result = result.removingDotSegments
591+
result = result._removingDotSegments
562592

563593
// Automounted paths need to be stripped for various flavors of paths
564594
let exclusionList = ["/Applications", "/Library", "/System", "/Users", "/Volumes", "/bin", "/cores", "/dev", "/opt", "/private", "/sbin", "/usr"]
@@ -584,6 +614,10 @@ extension String {
584614

585615
// _NSPathComponents
586616
var pathComponents: [String] {
617+
_standardizingSlashes()._pathComponents
618+
}
619+
620+
private var _pathComponents: [String] {
587621
var components = self.components(separatedBy: "/").filter { !$0.isEmpty }
588622
if self.first == "/" {
589623
components.insert("/", at: 0)
@@ -596,6 +630,10 @@ extension String {
596630

597631
#if !NO_FILESYSTEM
598632
var abbreviatingWithTildeInPath: String {
633+
_standardizingSlashes()._abbreviatingWithTildeInPath
634+
}
635+
636+
private var _abbreviatingWithTildeInPath: String {
599637
guard !self.isEmpty && self != "/" else { return self }
600638
let homeDir = String.homeDirectoryPath()
601639
guard self.starts(with: homeDir) else { return self }
@@ -605,6 +643,10 @@ extension String {
605643
}
606644

607645
var expandingTildeInPath: String {
646+
_standardizingSlashes()._expandingTildeInPath
647+
}
648+
649+
private var _expandingTildeInPath: String {
608650
guard self.first == "~" else { return self }
609651
var user: String? = nil
610652
let firstSlash = self.firstIndex(of: "/") ?? self.endIndex
@@ -781,6 +823,7 @@ extension StringProtocol {
781823
}
782824
}
783825

826+
// Internal for testing purposes
784827
internal func _hasDotDotComponent() -> Bool {
785828
let input = self.utf8
786829
guard input.count >= 2 else {

Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,16 @@ final class FileManagerTests : XCTestCase {
282282
try $0.createDirectory(atPath: "create_dir_test2/nested2", withIntermediateDirectories: true)
283283
XCTAssertEqual(try $0.contentsOfDirectory(atPath: "create_dir_test2").sorted(), ["nested", "nested2"])
284284
XCTAssertNoThrow(try $0.createDirectory(atPath: "create_dir_test2/nested2", withIntermediateDirectories: true))
285+
286+
#if os(Windows)
287+
try $0.createDirectory(atPath: "create_dir_test3\\nested", withIntermediateDirectories: true)
288+
XCTAssertEqual(try $0.contentsOfDirectory(atPath: "create_dir_test3"), ["nested"])
289+
#endif
290+
285291
XCTAssertThrowsError(try $0.createDirectory(atPath: "create_dir_test", withIntermediateDirectories: false)) {
286292
XCTAssertEqual(($0 as? CocoaError)?.code, .fileWriteFileExists)
287293
}
288-
XCTAssertThrowsError(try $0.createDirectory(atPath: "create_dir_test3/nested", withIntermediateDirectories: false)) {
294+
XCTAssertThrowsError(try $0.createDirectory(atPath: "create_dir_test4/nested", withIntermediateDirectories: false)) {
289295
XCTAssertEqual(($0 as? CocoaError)?.code, .fileNoSuchFile)
290296
}
291297
XCTAssertThrowsError(try $0.createDirectory(atPath: "preexisting_file", withIntermediateDirectories: false)) {

0 commit comments

Comments
 (0)