diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 198638cf7..79bcbfa76 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -22,8 +22,28 @@ import WinSDK internal import _FoundationCShims +extension StringProtocol { + fileprivate func _standardizingSlashes() -> String { + #if os(Windows) + // The string functions below all assume that the path separator is a forward slash + // Standardize the path to use forward slashes before processing for consistency + return self.replacing(._backslash, with: ._slash) + #else + if let str = _specializingCast(self, to: String.self) { + return str + } else { + return String(self) + } + #endif + } +} + extension String { internal func deletingLastPathComponent() -> String { + _standardizingSlashes()._deletingLastPathComponent() + } + + private func _deletingLastPathComponent() -> String { let lastSlash = self.lastIndex { $0 == "/" } guard let lastSlash else { // No slash @@ -50,6 +70,10 @@ extension String { } internal func appendingPathComponent(_ component: String) -> String { + _standardizingSlashes()._appendingPathComponent(component) + } + + private func _appendingPathComponent(_ component: String) -> String { var result = self if !component.isEmpty { var needsSlash = true @@ -103,6 +127,10 @@ extension String { } internal var lastPathComponent: String { + _standardizingSlashes()._lastPathComponent + } + + private var _lastPathComponent: String { let lastSlash = self.lastIndex { $0 == "/" } guard let lastSlash else { // No slash, just return self @@ -170,11 +198,11 @@ extension String { return false } if let lastDot = pathExtension.utf8.lastIndex(of: UInt8(ascii: ".")) { - let beforeDot = pathExtension[.. String { + _standardizingSlashes()._merging(relativePath: relativePath) + } + + private func _merging(relativePath: String) -> String { guard relativePath.utf8.first != UInt8(ascii: "/") else { return relativePath } @@ -212,6 +244,10 @@ extension String { } internal var removingDotSegments: String { + _standardizingSlashes()._removingDotSegments + } + + private var _removingDotSegments: String { let input = self.utf8 guard !input.isEmpty else { return "" @@ -440,18 +476,12 @@ extension String { // From swift-corelibs-foundation's NSTemporaryDirectory. Internal for now, pending a better public API. internal static var temporaryDirectoryPath: String { -#if os(Windows) - let validPathSeps: [Character] = ["\\", "/"] -#else - let validPathSeps: [Character] = ["/"] -#endif - func normalizedPath(with path: String) -> String { - if validPathSeps.contains(where: { path.hasSuffix(String($0)) }) { - return path - } else { - return path + String(validPathSeps.last!) + var result = path._standardizingSlashes() + guard result.utf8.last != ._slash else { + return result } + return result + "/" } #if os(Windows) let cchLength: DWORD = GetTempPathW(0, nil) @@ -547,7 +577,7 @@ extension String { static var NETWORK_PREFIX: String { #"\\"# } private var _standardizingPath: String { - var result = _transmutingCompressingSlashes()._droppingTrailingSlashes + var result = _standardizingSlashes()._transmutingCompressingSlashes()._droppingTrailingSlashes let postNetStart = if result.starts(with: String.NETWORK_PREFIX) { result.firstIndex(of: "/") ?? result.endIndex } else { @@ -558,7 +588,7 @@ extension String { result = resolved } - result = result.removingDotSegments + result = result._removingDotSegments // Automounted paths need to be stripped for various flavors of paths let exclusionList = ["/Applications", "/Library", "/System", "/Users", "/Volumes", "/bin", "/cores", "/dev", "/opt", "/private", "/sbin", "/usr"] @@ -584,6 +614,10 @@ extension String { // _NSPathComponents var pathComponents: [String] { + _standardizingSlashes()._pathComponents + } + + private var _pathComponents: [String] { var components = self.components(separatedBy: "/").filter { !$0.isEmpty } if self.first == "/" { components.insert("/", at: 0) @@ -596,6 +630,10 @@ extension String { #if !NO_FILESYSTEM var abbreviatingWithTildeInPath: String { + _standardizingSlashes()._abbreviatingWithTildeInPath + } + + private var _abbreviatingWithTildeInPath: String { guard !self.isEmpty && self != "/" else { return self } let homeDir = String.homeDirectoryPath() guard self.starts(with: homeDir) else { return self } @@ -605,6 +643,10 @@ extension String { } var expandingTildeInPath: String { + _standardizingSlashes()._expandingTildeInPath + } + + private var _expandingTildeInPath: String { guard self.first == "~" else { return self } var user: String? = nil let firstSlash = self.firstIndex(of: "/") ?? self.endIndex @@ -781,6 +823,7 @@ extension StringProtocol { } } + // Internal for testing purposes internal func _hasDotDotComponent() -> Bool { let input = self.utf8 guard input.count >= 2 else { diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 7ead66838..fc6037566 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -282,10 +282,16 @@ final class FileManagerTests : XCTestCase { try $0.createDirectory(atPath: "create_dir_test2/nested2", withIntermediateDirectories: true) XCTAssertEqual(try $0.contentsOfDirectory(atPath: "create_dir_test2").sorted(), ["nested", "nested2"]) XCTAssertNoThrow(try $0.createDirectory(atPath: "create_dir_test2/nested2", withIntermediateDirectories: true)) + + #if os(Windows) + try $0.createDirectory(atPath: "create_dir_test3\\nested", withIntermediateDirectories: true) + XCTAssertEqual(try $0.contentsOfDirectory(atPath: "create_dir_test3"), ["nested"]) + #endif + XCTAssertThrowsError(try $0.createDirectory(atPath: "create_dir_test", withIntermediateDirectories: false)) { XCTAssertEqual(($0 as? CocoaError)?.code, .fileWriteFileExists) } - XCTAssertThrowsError(try $0.createDirectory(atPath: "create_dir_test3/nested", withIntermediateDirectories: false)) { + XCTAssertThrowsError(try $0.createDirectory(atPath: "create_dir_test4/nested", withIntermediateDirectories: false)) { XCTAssertEqual(($0 as? CocoaError)?.code, .fileNoSuchFile) } XCTAssertThrowsError(try $0.createDirectory(atPath: "preexisting_file", withIntermediateDirectories: false)) {