@@ -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,6 +777,31 @@ 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).
@@ -785,7 +814,7 @@ public struct URL: Equatable, Sendable, Hashable {
785814 return
786815 }
787816 #endif // FOUNDATION_FRAMEWORK
788- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
817+ guard let parseInfo = URL . parse ( urlString: string) else {
789818 return nil
790819 }
791820 _parseInfo = parseInfo
@@ -798,14 +827,15 @@ public struct URL: Equatable, Sendable, Hashable {
798827 ///
799828 /// 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).
800829 public init ? ( string: __shared String, relativeTo url: __shared URL? ) {
830+ guard !string. isEmpty else { return nil }
801831 #if FOUNDATION_FRAMEWORK
802832 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 }
804834 _url = URL . _converted ( from: inner)
805835 return
806836 }
807837 #endif // FOUNDATION_FRAMEWORK
808- guard let parseInfo = Parser . parse ( urlString: string, encodingInvalidCharacters : true ) else {
838+ guard let parseInfo = URL . parse ( urlString: string) else {
809839 return nil
810840 }
811841 _parseInfo = parseInfo
@@ -824,14 +854,15 @@ public struct URL: Equatable, Sendable, Hashable {
824854 /// If the URL string is still invalid after encoding, `nil` is returned.
825855 @available ( macOS 14 . 0 , iOS 17 . 0 , watchOS 10 . 0 , tvOS 17 . 0 , * )
826856 public init ? ( string: __shared String, encodingInvalidCharacters: Bool ) {
857+ guard !string. isEmpty else { return nil }
827858 #if FOUNDATION_FRAMEWORK
828859 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 }
830861 _url = URL . _converted ( from: inner)
831862 return
832863 }
833864 #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 {
835866 return nil
836867 }
837868 _parseInfo = parseInfo
@@ -858,7 +889,7 @@ public struct URL: Equatable, Sendable, Hashable {
858889 }
859890 #endif
860891 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)
862893 }
863894
864895 /// 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 {
877908 return
878909 }
879910 #endif
880- self . init ( filePath: path, directoryHint: . checkFileSystem, relativeTo: base)
911+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem, relativeTo: base)
881912 }
882913
883914 /// Initializes a newly created file URL referencing the local file or directory at path.
@@ -898,7 +929,7 @@ public struct URL: Equatable, Sendable, Hashable {
898929 }
899930 #endif
900931 let directoryHint : DirectoryHint = isDirectory ? . isDirectory : . notDirectory
901- self . init ( filePath: path, directoryHint: directoryHint)
932+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: directoryHint)
902933 }
903934
904935 /// Initializes a newly created file URL referencing the local file or directory at path.
@@ -917,7 +948,7 @@ public struct URL: Equatable, Sendable, Hashable {
917948 return
918949 }
919950 #endif
920- self . init ( filePath: path, directoryHint: . checkFileSystem)
951+ self . init ( filePath: path. isEmpty ? " . " : path , directoryHint: . checkFileSystem)
921952 }
922953
923954 // NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths
@@ -941,24 +972,24 @@ public struct URL: Equatable, Sendable, Hashable {
941972 ///
942973 /// 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.
943974 @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 ) {
945976 guard !dataRepresentation. isEmpty else { return nil }
946977 #if FOUNDATION_FRAMEWORK
947978 guard foundation_swift_url_enabled ( ) else {
948979 if isAbsolute {
949- _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url ) )
980+ _url = URL . _converted ( from: NSURL ( absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base ) )
950981 } else {
951- _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: url ) )
982+ _url = URL . _converted ( from: NSURL ( dataRepresentation: dataRepresentation, relativeTo: base ) )
952983 }
953984 return
954985 }
955986 #endif
956987 var url : URL ?
957988 if let string = String ( data: dataRepresentation, encoding: . utf8) {
958- url = URL ( string : string, relativeTo: url )
989+ url = URL ( stringOrEmpty : string, relativeTo: base )
959990 }
960991 if url == nil , let string = String ( data: dataRepresentation, encoding: . isoLatin1) {
961- url = URL ( string : string, relativeTo: url )
992+ url = URL ( stringOrEmpty : string, relativeTo: base )
962993 }
963994 guard let url else {
964995 return nil
@@ -983,7 +1014,7 @@ public struct URL: Equatable, Sendable, Hashable {
9831014 return
9841015 }
9851016 #endif
986- guard let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) else {
1017+ guard let parseInfo = URL . parse ( urlString: _url. relativeString) else {
9871018 return nil
9881019 }
9891020 _parseInfo = parseInfo
@@ -1004,7 +1035,7 @@ public struct URL: Equatable, Sendable, Hashable {
10041035 }
10051036 #endif
10061037 bookmarkDataIsStale = stale. boolValue
1007- let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) !
1038+ let parseInfo = URL . parse ( urlString: _url. relativeString) !
10081039 _parseInfo = parseInfo
10091040 if parseInfo. scheme == nil {
10101041 _baseParseInfo = url? . absoluteURL. _parseInfo
@@ -1229,6 +1260,14 @@ public struct URL: Equatable, Sendable, Hashable {
12291260 return nil
12301261 }
12311262
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+
12321271 func requestedHost( ) -> String ? {
12331272 let didPercentEncodeHost = hasAuthority ? _parseInfo. didPercentEncodeHost : _baseParseInfo? . didPercentEncodeHost ?? false
12341273 if percentEncoded {
@@ -2053,7 +2092,7 @@ public struct URL: Equatable, Sendable, Hashable {
20532092 return
20542093 }
20552094 #endif
2056- if let parseInfo = Parser . parse ( urlString: _url. relativeString, encodingInvalidCharacters : true ) {
2095+ if let parseInfo = URL . parse ( urlString: _url. relativeString) {
20572096 _parseInfo = parseInfo
20582097 } else {
20592098 // Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing)
@@ -2211,7 +2250,7 @@ extension URL {
22112250 #if !NO_FILESYSTEM
22122251 baseURL = baseURL ?? . currentDirectoryOrNil( )
22132252 #endif
2214- self . init ( string: " " , relativeTo: baseURL) !
2253+ self . init ( string: " ./ " , relativeTo: baseURL) !
22152254 return
22162255 }
22172256
@@ -2474,6 +2513,14 @@ extension URL {
24742513 #endif // NO_FILESYSTEM
24752514 }
24762515 #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+ }
24772524 return appending ( path: component, directoryHint: directoryHint, encodingSlashes: true )
24782525 }
24792526
0 commit comments