Skip to content

Commit 62e9b83

Browse files
committed
(127768091) Support Windows URL paths in FoundationEssentials
1 parent c52b422 commit 62e9b83

File tree

2 files changed

+150
-23
lines changed

2 files changed

+150
-23
lines changed

Sources/FoundationEssentials/String/String+Path.swift

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ extension String {
546546
}
547547
return result
548548
}
549-
549+
550550
var standardizingPath: String {
551551
expandingTildeInPath._standardizingPath
552552
}
@@ -586,10 +586,6 @@ extension String {
586586
return userDir + self[firstSlash...]
587587
}
588588

589-
private var _isAbsolutePath: Bool {
590-
first == "~" || first == "/"
591-
}
592-
593589
private static func _resolvingSymlinksInPathUsingFullPathAttribute(_ fsRep: UnsafePointer<CChar>) -> String? {
594590
#if canImport(Darwin)
595591
var attrs = attrlist()
@@ -636,7 +632,8 @@ extension String {
636632
// If not using the cache (which may not require hitting the disk at all if it's warm), try getting the full path from getattrlist.
637633
// If it succeeds, this approach always returns an absolute path starting from the root. Since this function returns relative paths when given a relative path to a relative symlink, dont use this approach unless the path is absolute.
638634

639-
if self._isAbsolutePath, let resolved = Self._resolvingSymlinksInPathUsingFullPathAttribute(fsPtr) {
635+
var path = self
636+
if URL.isAbsolute(standardizing: &path), let resolved = Self._resolvingSymlinksInPathUsingFullPathAttribute(fsPtr) {
640637
return resolved
641638
}
642639

@@ -726,3 +723,23 @@ extension String {
726723
}
727724
#endif // !NO_FILESYSTEM
728725
}
726+
727+
extension StringProtocol {
728+
internal func replacing(_ a: UInt8, with b: UInt8) -> String {
729+
var utf8Array = Array(self.utf8)
730+
var didReplace = false
731+
// ~300x faster than Array.replace([UInt8], with: [UInt8]) for one element
732+
for i in 0..<utf8Array.count {
733+
if utf8Array[i] == a {
734+
utf8Array[i] = b
735+
didReplace = true
736+
}
737+
}
738+
guard didReplace else {
739+
return String(self)
740+
}
741+
return String(unsafeUninitializedCapacity: utf8Array.count) { buffer in
742+
buffer.initialize(fromContentsOf: utf8Array)
743+
}
744+
}
745+
}

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,50 @@ extension URL {
20032003
}
20042004
#endif // !NO_FILESYSTEM
20052005

2006+
/// Checks if a file path is absolute and standardizes the inputted file path on Windows
2007+
internal static func isAbsolute(standardizing filePath: inout String) -> Bool {
2008+
#if os(Windows)
2009+
var isAbsolute = false
2010+
let utf8 = filePath.utf8
2011+
if utf8.first == UInt8(ascii: "\\") {
2012+
// Either an absolute path or a UNC path
2013+
isAbsolute = true
2014+
} else if utf8.count >= 3 {
2015+
// Check if this is a drive letter
2016+
let first = utf8.first!
2017+
let secondIndex = utf8.index(after: utf8.startIndex)
2018+
let second = utf8[secondIndex]
2019+
let thirdIndex = utf8.index(after: secondIndex)
2020+
let third = utf8[thirdIndex]
2021+
isAbsolute = (
2022+
first.isAlpha
2023+
&& (second == UInt8(ascii: ":") || second == UInt8(ascii: "|"))
2024+
&& third == UInt8(ascii: "\\")
2025+
)
2026+
2027+
if !isAbsolute {
2028+
// Strip the drive letter so it's not mistaken as a scheme
2029+
filePath = String(filePath[thirdIndex...])
2030+
} else {
2031+
// Standardize to "\[drive-letter]:\..."
2032+
if second == UInt8(ascii: "|") {
2033+
var filePathArray = Array(utf8)
2034+
filePathArray[1] = UInt8(ascii: ":")
2035+
filePathArray.insert(UInt8(ascii: "\\"), at: 0)
2036+
filePath = String(unsafeUninitializedCapacity: filePathArray.count) { buffer in
2037+
buffer.initialize(fromContentsOf: filePathArray)
2038+
}
2039+
} else {
2040+
filePath = "\\" + filePath
2041+
}
2042+
}
2043+
}
2044+
#else
2045+
let isAbsolute = filePath.utf8.first == UInt8(ascii: "/") || filePath.utf8.first == UInt8(ascii: "~")
2046+
#endif
2047+
return isAbsolute
2048+
}
2049+
20062050
/// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL.
20072051
///
20082052
/// If an empty string is used for the path, then the path is assumed to be ".".
@@ -2027,17 +2071,59 @@ extension URL {
20272071
return
20282072
}
20292073
#endif // FOUNDATION_FRAMEWORK
2074+
var baseURL = base
20302075
guard !path.isEmpty else {
2031-
#if NO_FILESYSTEM
2032-
let baseURL = base
2033-
#else
2034-
let baseURL = base ?? .currentDirectoryOrNil()
2076+
#if !NO_FILESYSTEM
2077+
baseURL = baseURL ?? .currentDirectoryOrNil()
20352078
#endif
20362079
self.init(string: "", relativeTo: baseURL)!
20372080
return
20382081
}
20392082

2083+
#if os(Windows)
2084+
let slash = UInt8(ascii: "\\")
2085+
var hostName: String?
2086+
var hostEnd = path.startIndex
2087+
if path.utf8.starts(with: [slash, slash]) {
2088+
let hostStart = path.utf8.index(path.utf8.startIndex, offsetBy: 2)
2089+
hostEnd = path[hostStart...].utf8.firstIndex { $0 == slash || $0 = UInt8(ascii: "/") }
2090+
hostName = path[hostStart..<hostEnd]
2091+
}
2092+
var filePath = path[hostEnd...].replacing(UInt8(ascii: "/"), with: slash)
2093+
#else
20402094
let slash = UInt8(ascii: "/")
2095+
var filePath = path
2096+
#endif
2097+
2098+
let isAbsolute = URL.isAbsolute(standardizing: &filePath)
2099+
2100+
#if !NO_FILESYSTEM
2101+
if !isAbsolute {
2102+
baseURL = baseURL ?? .currentDirectoryOrNil()
2103+
}
2104+
#endif
2105+
2106+
func absoluteFilePath() -> String {
2107+
#if os(Windows)
2108+
if let hostName {
2109+
return #"\\"# + hostName + filePath
2110+
}
2111+
#endif
2112+
guard !isAbsolute, let baseURL else {
2113+
return filePath
2114+
}
2115+
#if os(Windows)
2116+
let urlPath = filePath.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/"))
2117+
var mergedPath = baseURL.mergedPath(for: urlPath).replacing(UInt8(ascii: "/"), with: UInt8(ascii: "\\"))
2118+
if let baseHost = baseURL.host(percentEncoded: false), !baseHost.isEmpty {
2119+
return #"\\"# + baseHost + mergedPath
2120+
}
2121+
return mergedPath
2122+
#else
2123+
return baseURL.mergedPath(for: filePath)
2124+
#endif
2125+
}
2126+
20412127
let isDirectory: Bool
20422128
switch directoryHint {
20432129
case .isDirectory:
@@ -2046,39 +2132,46 @@ extension URL {
20462132
isDirectory = false
20472133
case .checkFileSystem:
20482134
#if !NO_FILESYSTEM
2049-
isDirectory = URL.isDirectory(path)
2135+
isDirectory = URL.isDirectory(absoluteFilePath())
20502136
#else
2051-
isDirectory = path.utf8.last == slash
2137+
isDirectory = filePath.utf8.last == slash
20522138
#endif
20532139
case .inferFromPath:
2054-
isDirectory = path.utf8.last == slash
2140+
isDirectory = filePath.utf8.last == slash
20552141
}
20562142

2057-
var filePath = path
2058-
let isAbsolute = filePath.utf8.first == slash || filePath.utf8.first == UInt8(ascii: "~")
20592143
if !isAbsolute {
20602144
#if !NO_FILESYSTEM
20612145
filePath = filePath.standardizingPath
20622146
#else
20632147
filePath = filePath.removingDotSegments
20642148
#endif
20652149
}
2066-
if !filePath.isEmpty && filePath.utf8.last != slash && isDirectory {
2150+
2151+
#if os(Windows)
2152+
// Convert any "\" back to "/" before storing the URL parse info
2153+
filePath = filePath.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/"))
2154+
#endif
2155+
2156+
if !filePath.isEmpty && filePath.utf8.last != UInt8(ascii: "/") && isDirectory {
20672157
filePath += "/"
20682158
}
20692159
var components = URLComponents()
20702160
if isAbsolute {
20712161
components.scheme = "file"
2162+
#if os(Windows)
2163+
if let hostName {
2164+
components.host = hostName
2165+
} else {
2166+
components.encodedHost = ""
2167+
}
2168+
#else
20722169
components.encodedHost = ""
2170+
#endif
20732171
}
20742172
components.path = filePath
20752173

20762174
if !isAbsolute {
2077-
#if NO_FILESYSTEM
2078-
let baseURL = base
2079-
#else
2080-
let baseURL = base ?? .currentDirectoryOrNil()
2081-
#endif
20822175
self = components.url(relativeTo: baseURL)!
20832176
} else {
20842177
// Drop the baseURL if the URL is absolute
@@ -2087,6 +2180,9 @@ extension URL {
20872180
}
20882181

20892182
private func appending<S: StringProtocol>(path: S, directoryHint: DirectoryHint, encodingSlashes: Bool) -> URL {
2183+
#if os(Windows)
2184+
var path = path.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/"))
2185+
#endif
20902186
guard var pathToAppend = Parser.percentEncode(path, component: .path) else {
20912187
return self
20922188
}
@@ -2332,10 +2428,13 @@ extension URL {
23322428
extension URL {
23332429
private static func currentDirectoryOrNil() -> URL? {
23342430
let path: String? = FileManager.default.currentDirectoryPath
2335-
guard let path, path.utf8.first == UInt8(ascii: "/") else {
2431+
guard var filePath = path else {
23362432
return nil
23372433
}
2338-
return URL(filePath: path, directoryHint: .isDirectory)
2434+
guard URL.isAbsolute(standardizing: &filePath) else {
2435+
return nil
2436+
}
2437+
return URL(filePath: filePath, directoryHint: .isDirectory)
23392438
}
23402439

23412440
/// The working directory of the current process.
@@ -2660,3 +2759,14 @@ extension URL: _ExpressibleByFileReferenceLiteral {
26602759

26612760
public typealias _FileReferenceLiteralType = URL
26622761
#endif // FOUNDATION_FRAMEWORK
2762+
2763+
fileprivate extension UInt8 {
2764+
var isAlpha: Bool {
2765+
switch self {
2766+
case UInt8(ascii: "A")...UInt8(ascii: "Z"), UInt8(ascii: "a")...UInt8(ascii: "z"):
2767+
return true
2768+
default:
2769+
return false
2770+
}
2771+
}
2772+
}

0 commit comments

Comments
 (0)