@@ -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,6 +777,31 @@ 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).
@@ -785,7 +814,7 @@ public struct URL: Equatable, Sendable, Hashable {
785
814
return
786
815
}
787
816
#endif // FOUNDATION_FRAMEWORK
788
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
817
+ guard let parseInfo = URL . parse ( urlString: string) else {
789
818
return nil
790
819
}
791
820
_parseInfo = parseInfo
@@ -798,14 +827,15 @@ public struct URL: Equatable, Sendable, Hashable {
798
827
///
799
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).
800
829
public init ? ( string: __shared String, relativeTo url: __shared URL? ) {
830
+ guard !string. isEmpty else { return nil }
801
831
#if FOUNDATION_FRAMEWORK
802
832
guard foundation_swift_url_enabled ( ) else {
803
- 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 }
804
834
_url = URL . _converted ( from: inner)
805
835
return
806
836
}
807
837
#endif // FOUNDATION_FRAMEWORK
808
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
838
+ guard let parseInfo = URL . parse ( urlString: string) else {
809
839
return nil
810
840
}
811
841
_parseInfo = parseInfo
@@ -824,14 +854,15 @@ public struct URL: Equatable, Sendable, Hashable {
824
854
/// If the URL string is still invalid after encoding, `nil` is returned.
825
855
@available ( macOS 14 . 0 , iOS 17 . 0 , watchOS 10 . 0 , tvOS 17 . 0 , * )
826
856
public init ? ( string: __shared String, encodingInvalidCharacters: Bool ) {
857
+ guard !string. isEmpty else { return nil }
827
858
#if FOUNDATION_FRAMEWORK
828
859
guard foundation_swift_url_enabled ( ) else {
829
- 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 }
830
861
_url = URL . _converted ( from: inner)
831
862
return
832
863
}
833
864
#endif // FOUNDATION_FRAMEWORK
834
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else {
865
+ guard let parseInfo = URL . parse ( urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else {
835
866
return nil
836
867
}
837
868
_parseInfo = parseInfo
@@ -858,7 +889,7 @@ public struct URL: Equatable, Sendable, Hashable {
858
889
}
859
890
#endif
860
891
let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
861
- self . init ( filePath: path, directoryHint: directoryHint, relativeTo: base)
892
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint, relativeTo: base)
862
893
}
863
894
864
895
/// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL.
@@ -877,7 +908,7 @@ public struct URL: Equatable, Sendable, Hashable {
877
908
return
878
909
}
879
910
#endif
880
- self . init ( filePath: path, directoryHint: . checkFileSystem, relativeTo: base)
911
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem, relativeTo: base)
881
912
}
882
913
883
914
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -898,7 +929,7 @@ public struct URL: Equatable, Sendable, Hashable {
898
929
}
899
930
#endif
900
931
let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
901
- self . init ( filePath: path, directoryHint: directoryHint)
932
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint)
902
933
}
903
934
904
935
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -917,7 +948,7 @@ public struct URL: Equatable, Sendable, Hashable {
917
948
return
918
949
}
919
950
#endif
920
- self . init ( filePath: path, directoryHint: . checkFileSystem)
951
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem)
921
952
}
922
953
923
954
// NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths
@@ -941,24 +972,24 @@ public struct URL: Equatable, Sendable, Hashable {
941
972
///
942
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.
943
974
@available ( macOS 10 . 11 , iOS 9 . 0 , watchOS 2 . 0 , tvOS 9 . 0 , * )
944
- 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 ) {
945
976
guard !dataRepresentation. isEmpty else { return nil }
946
977
#if FOUNDATION_FRAMEWORK
947
978
guard foundation_swift_url_enabled ( ) else {
948
979
if isAbsolute {
949
- _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url ) )
980
+ _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base ) )
950
981
} else {
951
- _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: url ) )
982
+ _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: base ) )
952
983
}
953
984
return
954
985
}
955
986
#endif
956
987
var url : URL ?
957
988
if let string = String ( data: dataRepresentation, encoding: . utf8) {
958
- url = URL ( string : string, relativeTo: url )
989
+ url = URL ( stringOrEmpty : string, relativeTo: base )
959
990
}
960
991
if url == nil , let string = String ( data: dataRepresentation, encoding: . isoLatin1) {
961
- url = URL ( string : string, relativeTo: url )
992
+ url = URL ( stringOrEmpty : string, relativeTo: base )
962
993
}
963
994
guard let url else {
964
995
return nil
@@ -983,7 +1014,7 @@ public struct URL: Equatable, Sendable, Hashable {
983
1014
return
984
1015
}
985
1016
#endif
986
- guard let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) else {
1017
+ guard let parseInfo = URL . parse ( urlString: _url. relativeString) else {
987
1018
return nil
988
1019
}
989
1020
_parseInfo = parseInfo
@@ -1004,7 +1035,7 @@ public struct URL: Equatable, Sendable, Hashable {
1004
1035
}
1005
1036
#endif
1006
1037
bookmarkDataIsStale = stale. boolValue
1007
- let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) !
1038
+ let parseInfo = URL . parse ( urlString: _url. relativeString) !
1008
1039
_parseInfo = parseInfo
1009
1040
if parseInfo. scheme == nil {
1010
1041
_baseParseInfo = url? . absoluteURL. _parseInfo
@@ -1229,6 +1260,14 @@ public struct URL: Equatable, Sendable, Hashable {
1229
1260
return nil
1230
1261
}
1231
1262
1263
+ // According to RFC 3986, a host always exists if there is an authority
1264
+ // component, it just might be empty. However, the old implementation
1265
+ // of URL.host() returned nil for URLs like "https:///", and apps rely
1266
+ // on this behavior, so keep it for bincompat.
1267
+ if encodedHost. isEmpty, user ( ) == nil , password ( ) == nil , port == nil {
1268
+ return nil
1269
+ }
1270
+
1232
1271
func requestedHost( ) -> String ? {
1233
1272
let didPercentEncodeHost = hasAuthority ? _parseInfo. didPercentEncodeHost : _baseParseInfo? . didPercentEncodeHost ?? false
1234
1273
if percentEncoded {
@@ -2053,7 +2092,7 @@ public struct URL: Equatable, Sendable, Hashable {
2053
2092
return
2054
2093
}
2055
2094
#endif
2056
- if let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) {
2095
+ if let parseInfo = URL . parse ( urlString: _url. relativeString) {
2057
2096
_parseInfo = parseInfo
2058
2097
} else {
2059
2098
// Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing)
@@ -2211,7 +2250,7 @@ extension URL {
2211
2250
#if !NO_FILESYSTEM
2212
2251
baseURL = baseURL ?? . currentDirectoryOrNil( )
2213
2252
#endif
2214
- self . init ( string: " " , relativeTo: baseURL) !
2253
+ self . init ( string: " ./ " , relativeTo: baseURL) !
2215
2254
return
2216
2255
}
2217
2256
@@ -2474,6 +2513,14 @@ extension URL {
2474
2513
#endif // NO_FILESYSTEM
2475
2514
}
2476
2515
#endif // FOUNDATION_FRAMEWORK
2516
+
2517
+ // The old .appending(component:) implementation did not actually percent-encode
2518
+ // "/" for file URLs as the documentation suggests. Many apps accidentally use
2519
+ // .appending(component: "path/with/slashes") instead of using .appending(path:),
2520
+ // so changing this behavior would cause breakage.
2521
+ if isFileURL {
2522
+ return appending ( path: component, directoryHint: directoryHint, encodingSlashes: false )
2523
+ }
2477
2524
return appending ( path: component, directoryHint: directoryHint, encodingSlashes: true )
2478
2525
}
2479
2526
0 commit comments