@@ -763,6 +763,10 @@ public struct URL: Equatable, Sendable, Hashable {
763763 internal var _parseInfo : URLParseInfo !
764764 private var _baseParseInfo : URLParseInfo ?
765765
766+ private static func parse( urlString: String , encodingInvalidCharacters: Bool = true ) -> URLParseInfo ? {
767+ return Parser . parse ( urlString: urlString, encodingInvalidCharacters: encodingInvalidCharacters, compatibility: . allowEmptyScheme)
768+ }
769+
766770 internal init ( parseInfo: URLParseInfo , relativeTo url: URL ? = nil ) {
767771 _parseInfo = parseInfo
768772 if parseInfo. scheme == nil {
@@ -773,26 +777,44 @@ public struct URL: Equatable, Sendable, Hashable {
773777 #endif // FOUNDATION_FRAMEWORK
774778 }
775779
780+ /// The public initializers don't allow the empty string, and we must maintain that behavior
781+ /// for compatibility. However, there are cases internally where we need to create a URL with
782+ /// an empty string, such as when `.deletingLastPathComponent()` of a single path
783+ /// component. This previously worked since `URL` just wrapped an `NSURL`, which
784+ /// allows the empty string.
785+ internal init ? ( stringOrEmpty: String , relativeTo url: URL ? = nil ) {
786+ #if FOUNDATION_FRAMEWORK
787+ guard foundation_swift_url_enabled ( ) else {
788+ guard let inner = NSURL ( string: stringOrEmpty, relativeTo: url) else { return nil }
789+ _url = URL . _converted ( from: inner)
790+ return
791+ }
792+ #endif // FOUNDATION_FRAMEWORK
793+ guard let parseInfo = URL . parse ( urlString: stringOrEmpty) else {
794+ return nil
795+ }
796+ _parseInfo = parseInfo
797+ if parseInfo. scheme == nil {
798+ _baseParseInfo = url? . absoluteURL. _parseInfo
799+ }
800+ #if FOUNDATION_FRAMEWORK
801+ _url = URL . _nsURL ( from: _parseInfo, baseParseInfo: _baseParseInfo)
802+ #endif // FOUNDATION_FRAMEWORK
803+ }
804+
776805 /// Initialize with string.
777806 ///
778807 /// Returns `nil` if a `URL` cannot be formed with the string (for example, if the string contains characters that are illegal in a URL, or is an empty string).
779808 public init ? ( string: __shared String) {
809+ guard !string. isEmpty else { return nil }
780810 #if FOUNDATION_FRAMEWORK
781811 guard foundation_swift_url_enabled ( ) else {
782- guard !string . isEmpty , let inner = NSURL ( string: string) else { return nil }
812+ guard let inner = NSURL ( string: string) else { return nil }
783813 _url = URL . _converted ( from: inner)
784814 return
785815 }
786- // Linked-on-or-after check for apps which pass an empty string.
787- // The new URL(string:) implementations allow the empty string
788- // as input since an empty path is valid and can be resolved
789- // against a base URL. This is shown in the RFC 3986 examples:
790- // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.1
791- if Self . compatibility1 && string. isEmpty {
792- return nil
793- }
794816 #endif // FOUNDATION_FRAMEWORK
795- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
817+ guard let parseInfo = URL . parse ( urlString: string) else {
796818 return nil
797819 }
798820 _parseInfo = parseInfo
@@ -805,14 +827,15 @@ public struct URL: Equatable, Sendable, Hashable {
805827 ///
806828 /// Returns `nil` if a `URL` cannot be formed with the string (for example, if the string contains characters that are illegal in a URL, or is an empty string).
807829 public init ? ( string: __shared String, relativeTo url: __shared URL? ) {
830+ guard !string. isEmpty else { return nil }
808831 #if FOUNDATION_FRAMEWORK
809832 guard foundation_swift_url_enabled ( ) else {
810- guard !string . isEmpty , let inner = NSURL ( string: string, relativeTo: url) else { return nil }
833+ guard let inner = NSURL ( string: string, relativeTo: url) else { return nil }
811834 _url = URL . _converted ( from: inner)
812835 return
813836 }
814837 #endif // FOUNDATION_FRAMEWORK
815- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
838+ guard let parseInfo = URL . parse ( urlString: string) else {
816839 return nil
817840 }
818841 _parseInfo = parseInfo
@@ -831,14 +854,15 @@ public struct URL: Equatable, Sendable, Hashable {
831854 /// If the URL string is still invalid after encoding, `nil` is returned.
832855 @available ( macOS 14 . 0 , iOS 17 . 0 , watchOS 10 . 0 , tvOS 17 . 0 , * )
833856 public init ? ( string: __shared String, encodingInvalidCharacters: Bool ) {
857+ guard !string. isEmpty else { return nil }
834858 #if FOUNDATION_FRAMEWORK
835859 guard foundation_swift_url_enabled ( ) else {
836- guard !string . isEmpty , let inner = NSURL ( string: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil }
860+ guard let inner = NSURL ( string: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil }
837861 _url = URL . _converted ( from: inner)
838862 return
839863 }
840864 #endif // FOUNDATION_FRAMEWORK
841- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else {
865+ guard let parseInfo = URL . parse ( urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else {
842866 return nil
843867 }
844868 _parseInfo = parseInfo
@@ -865,7 +889,7 @@ public struct URL: Equatable, Sendable, Hashable {
865889 }
866890 #endif
867891 let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
868- self . init ( filePath: path, directoryHint: directoryHint, relativeTo: base)
892+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint, relativeTo: base)
869893 }
870894
871895 /// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL.
@@ -884,7 +908,7 @@ public struct URL: Equatable, Sendable, Hashable {
884908 return
885909 }
886910 #endif
887- self . init ( filePath: path, directoryHint: . checkFileSystem, relativeTo: base)
911+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem, relativeTo: base)
888912 }
889913
890914 /// Initializes a newly created file URL referencing the local file or directory at path.
@@ -905,7 +929,7 @@ public struct URL: Equatable, Sendable, Hashable {
905929 }
906930 #endif
907931 let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
908- self . init ( filePath: path, directoryHint: directoryHint)
932+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint)
909933 }
910934
911935 /// Initializes a newly created file URL referencing the local file or directory at path.
@@ -924,7 +948,7 @@ public struct URL: Equatable, Sendable, Hashable {
924948 return
925949 }
926950 #endif
927- self . init ( filePath: path, directoryHint: . checkFileSystem)
951+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem)
928952 }
929953
930954 // NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths
@@ -948,24 +972,24 @@ public struct URL: Equatable, Sendable, Hashable {
948972 ///
949973 /// If the data representation is not a legal URL string as ASCII bytes, the URL object may not behave as expected. If the URL cannot be formed then this will return nil.
950974 @available ( macOS 10 . 11 , iOS 9 . 0 , watchOS 2 . 0 , tvOS 9 . 0 , * )
951- public init ? ( dataRepresentation: __shared Data, relativeTo url : __shared URL? , isAbsolute: Bool = false ) {
975+ public init ? ( dataRepresentation: __shared Data, relativeTo base : __shared URL? , isAbsolute: Bool = false ) {
952976 guard !dataRepresentation. isEmpty else { return nil }
953977 #if FOUNDATION_FRAMEWORK
954978 guard foundation_swift_url_enabled ( ) else {
955979 if isAbsolute {
956- _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url ) )
980+ _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base ) )
957981 } else {
958- _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: url ) )
982+ _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: base ) )
959983 }
960984 return
961985 }
962986 #endif
963987 var url : URL ?
964988 if let string = String ( data: dataRepresentation, encoding: . utf8) {
965- url = URL ( string : string, relativeTo: url )
989+ url = URL ( stringOrEmpty : string, relativeTo: base )
966990 }
967991 if url == nil , let string = String ( data: dataRepresentation, encoding: . isoLatin1) {
968- url = URL ( string : string, relativeTo: url )
992+ url = URL ( stringOrEmpty : string, relativeTo: base )
969993 }
970994 guard let url else {
971995 return nil
@@ -990,7 +1014,7 @@ public struct URL: Equatable, Sendable, Hashable {
9901014 return
9911015 }
9921016 #endif
993- guard let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) else {
1017+ guard let parseInfo = URL . parse ( urlString: _url. relativeString) else {
9941018 return nil
9951019 }
9961020 _parseInfo = parseInfo
@@ -1011,7 +1035,7 @@ public struct URL: Equatable, Sendable, Hashable {
10111035 }
10121036 #endif
10131037 bookmarkDataIsStale = stale. boolValue
1014- let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) !
1038+ let parseInfo = URL . parse ( urlString: _url. relativeString) !
10151039 _parseInfo = parseInfo
10161040 if parseInfo. scheme == nil {
10171041 _baseParseInfo = url? . absoluteURL. _parseInfo
@@ -1089,7 +1113,9 @@ public struct URL: Equatable, Sendable, Hashable {
10891113 }
10901114
10911115 if let baseScheme = _baseParseInfo. scheme {
1092- result. scheme = String ( baseScheme)
1116+ // Scheme might be empty, which URL allows for compatibility,
1117+ // but URLComponents does not, so we force it internally.
1118+ result. forceScheme ( String ( baseScheme) )
10931119 }
10941120
10951121 if hasAuthority {
@@ -1236,6 +1262,14 @@ public struct URL: Equatable, Sendable, Hashable {
12361262 return nil
12371263 }
12381264
1265+ // According to RFC 3986, a host always exists if there is an authority
1266+ // component, it just might be empty. However, the old implementation
1267+ // of URL.host() returned nil for URLs like "https:///", and apps rely
1268+ // on this behavior, so keep it for bincompat.
1269+ if encodedHost. isEmpty, user ( ) == nil , password ( ) == nil , port == nil {
1270+ return nil
1271+ }
1272+
12391273 func requestedHost( ) -> String ? {
12401274 let didPercentEncodeHost = hasAuthority ? _parseInfo. didPercentEncodeHost : _baseParseInfo? . didPercentEncodeHost ?? false
12411275 if percentEncoded {
@@ -1456,7 +1490,7 @@ public struct URL: Equatable, Sendable, Hashable {
14561490 }
14571491 #endif
14581492 if _baseParseInfo != nil {
1459- return absoluteURL. path ( percentEncoded: percentEncoded)
1493+ return absoluteURL. relativePath ( percentEncoded: percentEncoded)
14601494 }
14611495 if percentEncoded {
14621496 return String ( _parseInfo. path)
@@ -1844,7 +1878,7 @@ public struct URL: Equatable, Sendable, Hashable {
18441878 var components = URLComponents ( parseInfo: _parseInfo)
18451879 let newPath = components. percentEncodedPath. removingDotSegments
18461880 components. percentEncodedPath = newPath
1847- return components. url ( relativeTo: baseURL) !
1881+ return components. url ( relativeTo: baseURL) ?? self
18481882 }
18491883
18501884 /// Standardizes the path of a file URL by removing dot segments.
@@ -2060,7 +2094,7 @@ public struct URL: Equatable, Sendable, Hashable {
20602094 return
20612095 }
20622096 #endif
2063- if let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) {
2097+ if let parseInfo = URL . parse ( urlString: _url. relativeString) {
20642098 _parseInfo = parseInfo
20652099 } else {
20662100 // Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing)
@@ -2218,7 +2252,7 @@ extension URL {
22182252 #if !NO_FILESYSTEM
22192253 baseURL = baseURL ?? . currentDirectoryOrNil( )
22202254 #endif
2221- self . init ( string: " " , relativeTo: baseURL) !
2255+ self . init ( string: " ./ " , relativeTo: baseURL) !
22222256 return
22232257 }
22242258
@@ -2481,6 +2515,14 @@ extension URL {
24812515 #endif // NO_FILESYSTEM
24822516 }
24832517 #endif // FOUNDATION_FRAMEWORK
2518+
2519+ // The old .appending(component:) implementation did not actually percent-encode
2520+ // "/" for file URLs as the documentation suggests. Many apps accidentally use
2521+ // .appending(component: "path/with/slashes") instead of using .appending(path:),
2522+ // so changing this behavior would cause breakage.
2523+ if isFileURL {
2524+ return appending ( path: component, directoryHint: directoryHint, encodingSlashes: false )
2525+ }
24842526 return appending ( path: component, directoryHint: directoryHint, encodingSlashes: true )
24852527 }
24862528
0 commit comments