@@ -1319,13 +1319,32 @@ public struct URL: Equatable, Sendable, Hashable {
13191319 }
13201320 }
13211321
1322- private static func fileSystemPath( for urlPath: String ) -> String {
1323- var result = urlPath
1324- if result. count > 1 && result. utf8. last == UInt8 ( ascii: " / " ) {
1325- _ = result. popLast ( )
1322+ private static func windowsPath( for posixPath: String ) -> String {
1323+ let utf8 = posixPath. utf8
1324+ guard utf8. count >= 4 else {
1325+ return posixPath
1326+ }
1327+ // "C:\" is standardized to "/C:/" on initialization
1328+ let array = Array ( utf8)
1329+ if array [ 0 ] == . _slash,
1330+ array [ 1 ] . isAlpha,
1331+ array [ 2 ] == . _colon,
1332+ array [ 3 ] == . _slash {
1333+ return String ( Substring ( utf8. dropFirst ( ) ) )
13261334 }
1335+ return posixPath
1336+ }
1337+
1338+ private static func fileSystemPath( for urlPath: String ) -> String {
13271339 let charsToLeaveEncoded : Set < UInt8 > = [ . _slash, 0 ]
1328- return Parser . percentDecode ( result, excluding: charsToLeaveEncoded) ?? " "
1340+ guard let posixPath = Parser . percentDecode ( urlPath. _droppingTrailingSlashes, excluding: charsToLeaveEncoded) else {
1341+ return " "
1342+ }
1343+ #if os(Windows)
1344+ return windowsPath ( for: posixPath)
1345+ #else
1346+ return posixPath
1347+ #endif
13291348 }
13301349
13311350 var fileSystemPath : String {
@@ -2026,55 +2045,65 @@ extension URL {
20262045
20272046#if !NO_FILESYSTEM
20282047 private static func isDirectory( _ path: String ) -> Bool {
2029- #if !FOUNDATION_FRAMEWORK
2048+ #if os(Windows)
2049+ let path = path. replacing ( . _slash, with: . _backslash)
2050+ #endif
2051+ #if !FOUNDATION_FRAMEWORK
20302052 var isDirectory : Bool = false
20312053 _ = FileManager . default. fileExists ( atPath: path, isDirectory: & isDirectory)
20322054 return isDirectory
2033- #else
2055+ #else
20342056 var isDirectory : ObjCBool = false
20352057 _ = FileManager . default. fileExists ( atPath: path, isDirectory: & isDirectory)
20362058 return isDirectory. boolValue
2037- #endif
2059+ #endif
20382060 }
20392061#endif // !NO_FILESYSTEM
20402062
20412063 /// Checks if a file path is absolute and standardizes the inputted file path on Windows
2064+ /// Assumes the path only contains `/` as the path separator
20422065 internal static func isAbsolute( standardizing filePath: inout String ) -> Bool {
2066+ if filePath. utf8. first == . _slash {
2067+ return true
2068+ }
20432069 #if os(Windows)
2044- var isAbsolute = false
20452070 let utf8 = filePath. utf8
2046- if utf8. first == . _backslash {
2047- // Either an absolute path or a UNC path
2048- isAbsolute = true
2049- } else if utf8. count >= 3 {
2050- // Check if this is a drive letter
2051- let first = utf8. first!
2052- let secondIndex = utf8. index ( after: utf8. startIndex)
2053- let second = utf8 [ secondIndex]
2054- let thirdIndex = utf8. index ( after: secondIndex)
2055- let third = utf8 [ thirdIndex]
2056- isAbsolute = (
2057- first. isAlpha
2058- && ( second == . _colon || second == . _pipe)
2059- && third == . _backslash
2060- )
2061-
2062- if isAbsolute {
2063- // Standardize to "\[drive-letter]:\..."
2064- if second == . _pipe {
2065- var filePathArray = Array ( utf8)
2066- filePathArray [ 1 ] = . _colon
2067- filePathArray. insert ( . _backslash, at: 0 )
2068- filePath = String ( decoding: filePathArray, as: UTF8 . self)
2069- } else {
2070- filePath = " \\ " + filePath
2071- }
2071+ guard utf8. count >= 3 else {
2072+ return false
2073+ }
2074+ // Check if this is a drive letter
2075+ let first = utf8. first!
2076+ let secondIndex = utf8. index ( after: utf8. startIndex)
2077+ let second = utf8 [ secondIndex]
2078+ let thirdIndex = utf8. index ( after: secondIndex)
2079+ let third = utf8 [ thirdIndex]
2080+ let isAbsolute = (
2081+ first. isAlpha
2082+ && ( second == . _colon || second == . _pipe)
2083+ && third == . _slash
2084+ )
2085+ if isAbsolute {
2086+ // Standardize to "/[drive-letter]:/..."
2087+ if second == . _pipe {
2088+ var filePathArray = Array ( utf8)
2089+ filePathArray [ 1 ] = . _colon
2090+ filePathArray. insert ( . _slash, at: 0 )
2091+ filePath = String ( decoding: filePathArray, as: UTF8 . self)
2092+ } else {
2093+ filePath = " / " + filePath
20722094 }
20732095 }
2074- #else
2075- let isAbsolute = filePath. utf8. first == UInt8 ( ascii: " / " ) || filePath. utf8. first == UInt8 ( ascii: " ~ " )
2076- #endif
20772096 return isAbsolute
2097+ #else // os(Windows)
2098+ #if !NO_FILESYSTEM
2099+ // Expand the tilde if present
2100+ if filePath. utf8. first == UInt8 ( ascii: " ~ " ) {
2101+ filePath = filePath. expandingTildeInPath
2102+ }
2103+ #endif
2104+ // Make sure the expanded path is absolute
2105+ return filePath. utf8. first == . _slash
2106+ #endif // os(Windows)
20782107 }
20792108
20802109 /// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL.
@@ -2111,10 +2140,9 @@ extension URL {
21112140 }
21122141
21132142 #if os(Windows)
2114- let slash = UInt8 ( ascii : " \\ " )
2115- var filePath = path. replacing ( UInt8 ( ascii : " / " ) , with: slash )
2143+ // Convert any "\" to "/" before storing the URL parse info
2144+ var filePath = path. replacing ( . _backslash , with: . _slash )
21162145 #else
2117- let slash = UInt8 ( ascii: " / " )
21182146 var filePath = path
21192147 #endif
21202148
@@ -2126,41 +2154,31 @@ extension URL {
21262154 }
21272155 #endif
21282156
2129- func absoluteFilePath( ) -> String {
2130- guard !isAbsolute, let baseURL else {
2131- return filePath
2132- }
2133- let basePath = baseURL. path ( )
2134- #if os(Windows)
2135- let urlPath = filePath. replacing ( UInt8 ( ascii: " \\ " ) , with: UInt8 ( ascii: " / " ) )
2136- return URL . fileSystemPath ( for: basePath. merging ( relativePath: urlPath) ) . replacing ( UInt8 ( ascii: " / " ) , with: UInt8 ( ascii: " \\ " ) )
2137- #else
2138- return URL . fileSystemPath ( for: basePath. merging ( relativePath: filePath) )
2139- #endif
2140- }
2141-
21422157 let isDirectory : Bool
21432158 switch directoryHint {
21442159 case . isDirectory:
21452160 isDirectory = true
21462161 case . notDirectory:
2162+ filePath = filePath. _droppingTrailingSlashes
21472163 isDirectory = false
21482164 case . checkFileSystem:
21492165 #if !NO_FILESYSTEM
2166+ func absoluteFilePath( ) -> String {
2167+ guard !isAbsolute, let baseURL else {
2168+ return filePath
2169+ }
2170+ let absolutePath = baseURL. path ( ) . merging ( relativePath: filePath)
2171+ return URL . fileSystemPath ( for: absolutePath)
2172+ }
21502173 isDirectory = URL . isDirectory ( absoluteFilePath ( ) )
21512174 #else
2152- isDirectory = filePath. utf8. last == slash
2175+ isDirectory = filePath. utf8. last == . _slash
21532176 #endif
21542177 case . inferFromPath:
2155- isDirectory = filePath. utf8. last == slash
2178+ isDirectory = filePath. utf8. last == . _slash
21562179 }
21572180
2158- #if os(Windows)
2159- // Convert any "\" back to "/" before storing the URL parse info
2160- filePath = filePath. replacing ( UInt8 ( ascii: " \\ " ) , with: UInt8 ( ascii: " / " ) )
2161- #endif
2162-
2163- if !filePath. isEmpty && filePath. utf8. last != UInt8 ( ascii: " / " ) && isDirectory {
2181+ if isDirectory && !filePath. isEmpty && filePath. utf8. last != . _slash {
21642182 filePath += " / "
21652183 }
21662184 var components = URLComponents ( )
@@ -2438,6 +2456,9 @@ extension URL {
24382456 guard var filePath = path else {
24392457 return nil
24402458 }
2459+ #if os(Windows)
2460+ filePath = filePath. replacing ( . _backslash, with: . _slash)
2461+ #endif
24412462 guard URL . isAbsolute ( standardizing: & filePath) else {
24422463 return nil
24432464 }
0 commit comments