@@ -616,6 +616,10 @@ internal func foundation_swift_url_enabled() -> Bool {
616
616
internal func foundation_swift_url_enabled( ) -> Bool { return true }
617
617
#endif
618
618
619
+ #if canImport(os)
620
+ internal import os
621
+ #endif
622
+
619
623
/// A URL is a type that can potentially contain the location of a resource on a remote server, the path of a local file on disk, or even an arbitrary piece of encoded data.
620
624
///
621
625
/// You can construct URLs and access their parts. For URLs that represent local files, you can also manipulate properties of those files directly, such as changing the file's last modification date. Finally, you can pass URLs to other APIs to retrieve the contents of those URLs. For example, you can use the URLSession classes to access the contents of remote resources, as described in URL Session Programming Guide.
@@ -624,6 +628,12 @@ internal func foundation_swift_url_enabled() -> Bool { return true }
624
628
@available ( macOS 10 . 10 , iOS 8 . 0 , watchOS 2 . 0 , tvOS 9 . 0 , * )
625
629
public struct URL : Equatable , Sendable , Hashable {
626
630
631
+ #if canImport(os)
632
+ internal static let logger : Logger = {
633
+ Logger ( subsystem: " com.apple.foundation " , category: " url " )
634
+ } ( )
635
+ #endif
636
+
627
637
#if FOUNDATION_FRAMEWORK
628
638
629
639
private var _url : NSURL
@@ -763,6 +773,10 @@ public struct URL: Equatable, Sendable, Hashable {
763
773
internal var _parseInfo : URLParseInfo !
764
774
private var _baseParseInfo : URLParseInfo ?
765
775
776
+ private static func parse( urlString: String , encodingInvalidCharacters: Bool = true ) -> URLParseInfo ? {
777
+ return Parser . parse ( urlString: urlString, encodingInvalidCharacters: encodingInvalidCharacters, compatibility: . allowEmptyScheme)
778
+ }
779
+
766
780
internal init ( parseInfo: URLParseInfo , relativeTo url: URL ? = nil ) {
767
781
_parseInfo = parseInfo
768
782
if parseInfo. scheme == nil {
@@ -773,6 +787,31 @@ public struct URL: Equatable, Sendable, Hashable {
773
787
#endif // FOUNDATION_FRAMEWORK
774
788
}
775
789
790
+ /// The public initializers don't allow the empty string, and we must maintain that behavior
791
+ /// for compatibility. However, there are cases internally where we need to create a URL with
792
+ /// an empty string, such as when `.deletingLastPathComponent()` of a single path
793
+ /// component. This previously worked since `URL` just wrapped an `NSURL`, which
794
+ /// allows the empty string.
795
+ internal init ? ( stringOrEmpty: String , relativeTo url: URL ? = nil ) {
796
+ #if FOUNDATION_FRAMEWORK
797
+ guard foundation_swift_url_enabled ( ) else {
798
+ guard let inner = NSURL ( string: stringOrEmpty, relativeTo: url) else { return nil }
799
+ _url = URL . _converted ( from: inner)
800
+ return
801
+ }
802
+ #endif // FOUNDATION_FRAMEWORK
803
+ guard let parseInfo = URL . parse ( urlString: stringOrEmpty) else {
804
+ return nil
805
+ }
806
+ _parseInfo = parseInfo
807
+ if parseInfo. scheme == nil {
808
+ _baseParseInfo = url? . absoluteURL. _parseInfo
809
+ }
810
+ #if FOUNDATION_FRAMEWORK
811
+ _url = URL . _nsURL ( from: _parseInfo, baseParseInfo: _baseParseInfo)
812
+ #endif // FOUNDATION_FRAMEWORK
813
+ }
814
+
776
815
/// Initialize with string.
777
816
///
778
817
/// 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 +824,7 @@ public struct URL: Equatable, Sendable, Hashable {
785
824
return
786
825
}
787
826
#endif // FOUNDATION_FRAMEWORK
788
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
827
+ guard let parseInfo = URL . parse ( urlString: string) else {
789
828
return nil
790
829
}
791
830
_parseInfo = parseInfo
@@ -798,14 +837,15 @@ public struct URL: Equatable, Sendable, Hashable {
798
837
///
799
838
/// 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
839
public init ? ( string: __shared String, relativeTo url: __shared URL? ) {
840
+ guard !string. isEmpty else { return nil }
801
841
#if FOUNDATION_FRAMEWORK
802
842
guard foundation_swift_url_enabled ( ) else {
803
- guard !string . isEmpty , let inner = NSURL ( string: string, relativeTo: url) else { return nil }
843
+ guard let inner = NSURL ( string: string, relativeTo: url) else { return nil }
804
844
_url = URL . _converted ( from: inner)
805
845
return
806
846
}
807
847
#endif // FOUNDATION_FRAMEWORK
808
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
848
+ guard let parseInfo = URL . parse ( urlString: string) else {
809
849
return nil
810
850
}
811
851
_parseInfo = parseInfo
@@ -824,14 +864,15 @@ public struct URL: Equatable, Sendable, Hashable {
824
864
/// If the URL string is still invalid after encoding, `nil` is returned.
825
865
@available ( macOS 14 . 0 , iOS 17 . 0 , watchOS 10 . 0 , tvOS 17 . 0 , * )
826
866
public init ? ( string: __shared String, encodingInvalidCharacters: Bool ) {
867
+ guard !string. isEmpty else { return nil }
827
868
#if FOUNDATION_FRAMEWORK
828
869
guard foundation_swift_url_enabled ( ) else {
829
- guard !string . isEmpty , let inner = NSURL ( string: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil }
870
+ guard let inner = NSURL ( string: string, encodingInvalidCharacters: encodingInvalidCharacters) else { return nil }
830
871
_url = URL . _converted ( from: inner)
831
872
return
832
873
}
833
874
#endif // FOUNDATION_FRAMEWORK
834
- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else {
875
+ guard let parseInfo = URL . parse ( urlString: string, encodingInvalidCharacters: encodingInvalidCharacters) else {
835
876
return nil
836
877
}
837
878
_parseInfo = parseInfo
@@ -858,7 +899,7 @@ public struct URL: Equatable, Sendable, Hashable {
858
899
}
859
900
#endif
860
901
let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
861
- self . init ( filePath: path, directoryHint: directoryHint, relativeTo: base)
902
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint, relativeTo: base)
862
903
}
863
904
864
905
/// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL.
@@ -877,7 +918,7 @@ public struct URL: Equatable, Sendable, Hashable {
877
918
return
878
919
}
879
920
#endif
880
- self . init ( filePath: path, directoryHint: . checkFileSystem, relativeTo: base)
921
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem, relativeTo: base)
881
922
}
882
923
883
924
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -898,7 +939,7 @@ public struct URL: Equatable, Sendable, Hashable {
898
939
}
899
940
#endif
900
941
let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
901
- self . init ( filePath: path, directoryHint: directoryHint)
942
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint)
902
943
}
903
944
904
945
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -917,7 +958,7 @@ public struct URL: Equatable, Sendable, Hashable {
917
958
return
918
959
}
919
960
#endif
920
- self . init ( filePath: path, directoryHint: . checkFileSystem)
961
+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem)
921
962
}
922
963
923
964
// NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths
@@ -941,24 +982,24 @@ public struct URL: Equatable, Sendable, Hashable {
941
982
///
942
983
/// 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
984
@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 ) {
985
+ public init ? ( dataRepresentation: __shared Data, relativeTo base : __shared URL? , isAbsolute: Bool = false ) {
945
986
guard !dataRepresentation. isEmpty else { return nil }
946
987
#if FOUNDATION_FRAMEWORK
947
988
guard foundation_swift_url_enabled ( ) else {
948
989
if isAbsolute {
949
- _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url ) )
990
+ _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base ) )
950
991
} else {
951
- _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: url ) )
992
+ _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: base ) )
952
993
}
953
994
return
954
995
}
955
996
#endif
956
997
var url : URL ?
957
998
if let string = String ( data: dataRepresentation, encoding: . utf8) {
958
- url = URL ( string : string, relativeTo: url )
999
+ url = URL ( stringOrEmpty : string, relativeTo: base )
959
1000
}
960
1001
if url == nil , let string = String ( data: dataRepresentation, encoding: . isoLatin1) {
961
- url = URL ( string : string, relativeTo: url )
1002
+ url = URL ( stringOrEmpty : string, relativeTo: base )
962
1003
}
963
1004
guard let url else {
964
1005
return nil
@@ -983,7 +1024,7 @@ public struct URL: Equatable, Sendable, Hashable {
983
1024
return
984
1025
}
985
1026
#endif
986
- guard let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) else {
1027
+ guard let parseInfo = URL . parse ( urlString: _url. relativeString) else {
987
1028
return nil
988
1029
}
989
1030
_parseInfo = parseInfo
@@ -1004,7 +1045,7 @@ public struct URL: Equatable, Sendable, Hashable {
1004
1045
}
1005
1046
#endif
1006
1047
bookmarkDataIsStale = stale. boolValue
1007
- let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) !
1048
+ let parseInfo = URL . parse ( urlString: _url. relativeString) !
1008
1049
_parseInfo = parseInfo
1009
1050
if parseInfo. scheme == nil {
1010
1051
_baseParseInfo = url? . absoluteURL. _parseInfo
@@ -1229,6 +1270,14 @@ public struct URL: Equatable, Sendable, Hashable {
1229
1270
return nil
1230
1271
}
1231
1272
1273
+ // According to RFC 3986, a host always exists if there is an authority
1274
+ // component, it just might be empty. However, the old implementation
1275
+ // of URL.host() returned nil for URLs like "https:///", and apps rely
1276
+ // on this behavior, so keep it for bincompat.
1277
+ if encodedHost. isEmpty, user ( ) == nil , password ( ) == nil , port == nil {
1278
+ return nil
1279
+ }
1280
+
1232
1281
func requestedHost( ) -> String ? {
1233
1282
let didPercentEncodeHost = hasAuthority ? _parseInfo. didPercentEncodeHost : _baseParseInfo? . didPercentEncodeHost ?? false
1234
1283
if percentEncoded {
@@ -2053,7 +2102,7 @@ public struct URL: Equatable, Sendable, Hashable {
2053
2102
return
2054
2103
}
2055
2104
#endif
2056
- if let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) {
2105
+ if let parseInfo = URL . parse ( urlString: _url. relativeString) {
2057
2106
_parseInfo = parseInfo
2058
2107
} else {
2059
2108
// Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing)
@@ -2211,7 +2260,7 @@ extension URL {
2211
2260
#if !NO_FILESYSTEM
2212
2261
baseURL = baseURL ?? . currentDirectoryOrNil( )
2213
2262
#endif
2214
- self . init ( string: " " , relativeTo: baseURL) !
2263
+ self . init ( string: " ./ " , relativeTo: baseURL) !
2215
2264
return
2216
2265
}
2217
2266
@@ -2474,6 +2523,14 @@ extension URL {
2474
2523
#endif // NO_FILESYSTEM
2475
2524
}
2476
2525
#endif // FOUNDATION_FRAMEWORK
2526
+
2527
+ // The old .appending(component:) implementation did not actually percent-encode
2528
+ // "/" for file URLs as the documentation suggests. Many apps accidentally use
2529
+ // .appending(component: "path/with/slashes") instead of using .appending(path:),
2530
+ // so changing this behavior would cause breakage.
2531
+ if isFileURL {
2532
+ return appending ( path: component, directoryHint: directoryHint, encodingSlashes: false )
2533
+ }
2477
2534
return appending ( path: component, directoryHint: directoryHint, encodingSlashes: true )
2478
2535
}
2479
2536
0 commit comments