@@ -763,6 +763,10 @@ public struct URL: Equatable, Sendable, Hashable {
763
763
internal var _parseInfo : URLParseInfo !
764
764
private var _baseParseInfo : URLParseInfo ?
765
765
766
+ private static func parse( urlString: String , encodingInvalidCharacters: Bool = true ) -> URLParseInfo ? {
767
+ return Parser . parse ( urlString: urlString, encodingInvalidCharacters: encodingInvalidCharacters, compatibility: . allowEmptyScheme)
768
+ }
769
+
766
770
internal init ( parseInfo: URLParseInfo , relativeTo url: URL ? = nil ) {
767
771
_parseInfo = parseInfo
768
772
if parseInfo. scheme == nil {
@@ -773,26 +777,44 @@ public struct URL: Equatable, Sendable, Hashable {
773
777
#endif // FOUNDATION_FRAMEWORK
774
778
}
775
779
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
+
776
805
/// Initialize with string.
777
806
///
778
807
/// 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).
779
808
public init ? ( string: __shared String) {
809
+ guard !string. isEmpty else { return nil }
780
810
#if FOUNDATION_FRAMEWORK
781
811
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 }
783
813
_url = URL . _converted ( from: inner)
784
814
return
785
815
}
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
- }
794
816
#endif // FOUNDATION_FRAMEWORK
795
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
817
+ guard let parseInfo = URL . parse ( urlString: string) else {
796
818
return nil
797
819
}
798
820
_parseInfo = parseInfo
@@ -805,14 +827,15 @@ public struct URL: Equatable, Sendable, Hashable {
805
827
///
806
828
/// 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).
807
829
public init ? ( string: __shared String, relativeTo url: __shared URL? ) {
830
+ guard !string. isEmpty else { return nil }
808
831
#if FOUNDATION_FRAMEWORK
809
832
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 }
811
834
_url = URL . _converted ( from: inner)
812
835
return
813
836
}
814
837
#endif // FOUNDATION_FRAMEWORK
815
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
838
+ guard let parseInfo = URL . parse ( urlString: string) else {
816
839
return nil
817
840
}
818
841
_parseInfo = parseInfo
@@ -831,14 +854,15 @@ public struct URL: Equatable, Sendable, Hashable {
831
854
/// If the URL string is still invalid after encoding, `nil` is returned.
832
855
@available ( macOS 14 . 0 , iOS 17 . 0 , watchOS 10 . 0 , tvOS 17 . 0 , * )
833
856
public init ? ( string: __shared String, encodingInvalidCharacters: Bool ) {
857
+ guard !string. isEmpty else { return nil }
834
858
#if FOUNDATION_FRAMEWORK
835
859
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 }
837
861
_url = URL . _converted ( from: inner)
838
862
return
839
863
}
840
864
#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 {
842
866
return nil
843
867
}
844
868
_parseInfo = parseInfo
@@ -865,7 +889,7 @@ public struct URL: Equatable, Sendable, Hashable {
865
889
}
866
890
#endif
867
891
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)
869
893
}
870
894
871
895
/// 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 {
884
908
return
885
909
}
886
910
#endif
887
- self . init ( filePath: path, directoryHint: . checkFileSystem, relativeTo: base)
911
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem, relativeTo: base)
888
912
}
889
913
890
914
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -905,7 +929,7 @@ public struct URL: Equatable, Sendable, Hashable {
905
929
}
906
930
#endif
907
931
let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
908
- self . init ( filePath: path, directoryHint: directoryHint)
932
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint)
909
933
}
910
934
911
935
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -924,7 +948,7 @@ public struct URL: Equatable, Sendable, Hashable {
924
948
return
925
949
}
926
950
#endif
927
- self . init ( filePath: path, directoryHint: . checkFileSystem)
951
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem)
928
952
}
929
953
930
954
// NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths
@@ -948,24 +972,24 @@ public struct URL: Equatable, Sendable, Hashable {
948
972
///
949
973
/// 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.
950
974
@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 ) {
952
976
guard !dataRepresentation. isEmpty else { return nil }
953
977
#if FOUNDATION_FRAMEWORK
954
978
guard foundation_swift_url_enabled ( ) else {
955
979
if isAbsolute {
956
- _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url ) )
980
+ _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base ) )
957
981
} else {
958
- _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: url ) )
982
+ _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: base ) )
959
983
}
960
984
return
961
985
}
962
986
#endif
963
987
var url : URL ?
964
988
if let string = String ( data: dataRepresentation, encoding: . utf8) {
965
- url = URL ( string : string, relativeTo: url )
989
+ url = URL ( stringOrEmpty : string, relativeTo: base )
966
990
}
967
991
if url == nil , let string = String ( data: dataRepresentation, encoding: . isoLatin1) {
968
- url = URL ( string : string, relativeTo: url )
992
+ url = URL ( stringOrEmpty : string, relativeTo: base )
969
993
}
970
994
guard let url else {
971
995
return nil
@@ -990,7 +1014,7 @@ public struct URL: Equatable, Sendable, Hashable {
990
1014
return
991
1015
}
992
1016
#endif
993
- guard let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) else {
1017
+ guard let parseInfo = URL . parse ( urlString: _url. relativeString) else {
994
1018
return nil
995
1019
}
996
1020
_parseInfo = parseInfo
@@ -1011,7 +1035,7 @@ public struct URL: Equatable, Sendable, Hashable {
1011
1035
}
1012
1036
#endif
1013
1037
bookmarkDataIsStale = stale. boolValue
1014
- let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) !
1038
+ let parseInfo = URL . parse ( urlString: _url. relativeString) !
1015
1039
_parseInfo = parseInfo
1016
1040
if parseInfo. scheme == nil {
1017
1041
_baseParseInfo = url? . absoluteURL. _parseInfo
@@ -1089,7 +1113,9 @@ public struct URL: Equatable, Sendable, Hashable {
1089
1113
}
1090
1114
1091
1115
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) )
1093
1119
}
1094
1120
1095
1121
if hasAuthority {
@@ -1236,6 +1262,14 @@ public struct URL: Equatable, Sendable, Hashable {
1236
1262
return nil
1237
1263
}
1238
1264
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
+
1239
1273
func requestedHost( ) -> String ? {
1240
1274
let didPercentEncodeHost = hasAuthority ? _parseInfo. didPercentEncodeHost : _baseParseInfo? . didPercentEncodeHost ?? false
1241
1275
if percentEncoded {
@@ -1456,7 +1490,7 @@ public struct URL: Equatable, Sendable, Hashable {
1456
1490
}
1457
1491
#endif
1458
1492
if _baseParseInfo != nil {
1459
- return absoluteURL. path ( percentEncoded: percentEncoded)
1493
+ return absoluteURL. relativePath ( percentEncoded: percentEncoded)
1460
1494
}
1461
1495
if percentEncoded {
1462
1496
return String ( _parseInfo. path)
@@ -1844,7 +1878,7 @@ public struct URL: Equatable, Sendable, Hashable {
1844
1878
var components = URLComponents ( parseInfo: _parseInfo)
1845
1879
let newPath = components. percentEncodedPath. removingDotSegments
1846
1880
components. percentEncodedPath = newPath
1847
- return components. url ( relativeTo: baseURL) !
1881
+ return components. url ( relativeTo: baseURL) ?? self
1848
1882
}
1849
1883
1850
1884
/// Standardizes the path of a file URL by removing dot segments.
@@ -2060,7 +2094,7 @@ public struct URL: Equatable, Sendable, Hashable {
2060
2094
return
2061
2095
}
2062
2096
#endif
2063
- if let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) {
2097
+ if let parseInfo = URL . parse ( urlString: _url. relativeString) {
2064
2098
_parseInfo = parseInfo
2065
2099
} else {
2066
2100
// Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing)
@@ -2218,7 +2252,7 @@ extension URL {
2218
2252
#if !NO_FILESYSTEM
2219
2253
baseURL = baseURL ?? . currentDirectoryOrNil( )
2220
2254
#endif
2221
- self . init ( string: " " , relativeTo: baseURL) !
2255
+ self . init ( string: " ./ " , relativeTo: baseURL) !
2222
2256
return
2223
2257
}
2224
2258
@@ -2481,6 +2515,14 @@ extension URL {
2481
2515
#endif // NO_FILESYSTEM
2482
2516
}
2483
2517
#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
+ }
2484
2526
return appending ( path: component, directoryHint: directoryHint, encodingSlashes: true )
2485
2527
}
2486
2528
0 commit comments