From d72791ac7c773208eee2e472bc2796246abfb0dc Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Fri, 12 Jul 2024 14:27:36 -0700 Subject: [PATCH 1/3] Standardize backslashes before string path processing on Windows --- .../String/String+Path.swift | 71 +++++++++++++++---- .../FileManager/FileManagerTests.swift | 8 ++- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 198638cf7..7d2299932 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -22,8 +22,24 @@ 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 + 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 +66,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 +123,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 +194,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 +240,10 @@ extension String { } internal var removingDotSegments: String { + _standardizingSlashes()._removingDotSegments + } + + private var _removingDotSegments: String { let input = self.utf8 guard !input.isEmpty else { return "" @@ -440,18 +472,16 @@ 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)) }) { + guard path.utf8.last != ._slash else { + return path + } + #if os(Windows) + guard path.utf8.last != ._backslash else { return path - } else { - return path + String(validPathSeps.last!) } + #endif + return path + "/" } #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)) { From a04873a95ad0bbbeb390f60f77a398d2b0bc981f Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Sun, 14 Jul 2024 18:10:23 -0700 Subject: [PATCH 2/3] Call _standardizingSlashes() from normalizedPath(with:) --- .../FoundationEssentials/String/String+Path.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index 7d2299932..d9d1b38fc 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -473,15 +473,11 @@ extension String { // From swift-corelibs-foundation's NSTemporaryDirectory. Internal for now, pending a better public API. internal static var temporaryDirectoryPath: String { func normalizedPath(with path: String) -> String { - guard path.utf8.last != ._slash else { - return path - } - #if os(Windows) - guard path.utf8.last != ._backslash else { - return path + var result = path._standardizingSlashes() + guard result.utf8.last != ._slash else { + return result } - #endif - return path + "/" + return result + "/" } #if os(Windows) let cchLength: DWORD = GetTempPathW(0, nil) From 75e39c5c6b9d62bcd6f98a7e9fa576515a0d775f Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 15 Jul 2024 09:04:25 -0700 Subject: [PATCH 3/3] Return self when possible on non-Windows --- Sources/FoundationEssentials/String/String+Path.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/String/String+Path.swift b/Sources/FoundationEssentials/String/String+Path.swift index d9d1b38fc..79bcbfa76 100644 --- a/Sources/FoundationEssentials/String/String+Path.swift +++ b/Sources/FoundationEssentials/String/String+Path.swift @@ -29,7 +29,11 @@ extension StringProtocol { // Standardize the path to use forward slashes before processing for consistency return self.replacing(._backslash, with: ._slash) #else - return String(self) + if let str = _specializingCast(self, to: String.self) { + return str + } else { + return String(self) + } #endif } }