Skip to content

Commit b1b2add

Browse files
authored
(142446243) Compatibility behaviors for Swift URL (#1113)
1 parent 09c2c43 commit b1b2add

File tree

4 files changed

+127
-50
lines changed

4 files changed

+127
-50
lines changed

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,10 @@ internal func foundation_swift_url_enabled() -> Bool {
616616
internal func foundation_swift_url_enabled() -> Bool { return true }
617617
#endif
618618

619+
#if canImport(os)
620+
internal import os
621+
#endif
622+
619623
/// 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.
620624
///
621625
/// 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 }
624628
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
625629
public struct URL: Equatable, Sendable, Hashable {
626630

631+
#if canImport(os)
632+
internal static let logger: Logger = {
633+
Logger(subsystem: "com.apple.foundation", category: "url")
634+
}()
635+
#endif
636+
627637
#if FOUNDATION_FRAMEWORK
628638

629639
private var _url: NSURL
@@ -763,6 +773,10 @@ public struct URL: Equatable, Sendable, Hashable {
763773
internal var _parseInfo: URLParseInfo!
764774
private var _baseParseInfo: URLParseInfo?
765775

776+
private static func parse(urlString: String, encodingInvalidCharacters: Bool = true) -> URLParseInfo? {
777+
return Parser.parse(urlString: urlString, encodingInvalidCharacters: encodingInvalidCharacters, compatibility: .allowEmptyScheme)
778+
}
779+
766780
internal init(parseInfo: URLParseInfo, relativeTo url: URL? = nil) {
767781
_parseInfo = parseInfo
768782
if parseInfo.scheme == nil {
@@ -773,6 +787,31 @@ public struct URL: Equatable, Sendable, Hashable {
773787
#endif // FOUNDATION_FRAMEWORK
774788
}
775789

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+
776815
/// Initialize with string.
777816
///
778817
/// 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 {
785824
return
786825
}
787826
#endif // FOUNDATION_FRAMEWORK
788-
guard let parseInfo = Parser.parse(urlString: string, encodingInvalidCharacters: true) else {
827+
guard let parseInfo = URL.parse(urlString: string) else {
789828
return nil
790829
}
791830
_parseInfo = parseInfo
@@ -798,14 +837,15 @@ public struct URL: Equatable, Sendable, Hashable {
798837
///
799838
/// 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).
800839
public init?(string: __shared String, relativeTo url: __shared URL?) {
840+
guard !string.isEmpty else { return nil }
801841
#if FOUNDATION_FRAMEWORK
802842
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 }
804844
_url = URL._converted(from: inner)
805845
return
806846
}
807847
#endif // FOUNDATION_FRAMEWORK
808-
guard let parseInfo = Parser.parse(urlString: string, encodingInvalidCharacters: true) else {
848+
guard let parseInfo = URL.parse(urlString: string) else {
809849
return nil
810850
}
811851
_parseInfo = parseInfo
@@ -824,14 +864,15 @@ public struct URL: Equatable, Sendable, Hashable {
824864
/// If the URL string is still invalid after encoding, `nil` is returned.
825865
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
826866
public init?(string: __shared String, encodingInvalidCharacters: Bool) {
867+
guard !string.isEmpty else { return nil }
827868
#if FOUNDATION_FRAMEWORK
828869
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 }
830871
_url = URL._converted(from: inner)
831872
return
832873
}
833874
#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 {
835876
return nil
836877
}
837878
_parseInfo = parseInfo
@@ -858,7 +899,7 @@ public struct URL: Equatable, Sendable, Hashable {
858899
}
859900
#endif
860901
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)
862903
}
863904

864905
/// 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 {
877918
return
878919
}
879920
#endif
880-
self.init(filePath: path, directoryHint: .checkFileSystem, relativeTo: base)
921+
self.init(filePath: path.isEmpty ? "." : path, directoryHint: .checkFileSystem, relativeTo: base)
881922
}
882923

883924
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -898,7 +939,7 @@ public struct URL: Equatable, Sendable, Hashable {
898939
}
899940
#endif
900941
let directoryHint: DirectoryHint = isDirectory ? .isDirectory : .notDirectory
901-
self.init(filePath: path, directoryHint: directoryHint)
942+
self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint)
902943
}
903944

904945
/// Initializes a newly created file URL referencing the local file or directory at path.
@@ -917,7 +958,7 @@ public struct URL: Equatable, Sendable, Hashable {
917958
return
918959
}
919960
#endif
920-
self.init(filePath: path, directoryHint: .checkFileSystem)
961+
self.init(filePath: path.isEmpty ? "." : path, directoryHint: .checkFileSystem)
921962
}
922963

923964
// NSURL(fileURLWithPath:) can return nil incorrectly for some malformed paths
@@ -941,24 +982,24 @@ public struct URL: Equatable, Sendable, Hashable {
941982
///
942983
/// 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.
943984
@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) {
945986
guard !dataRepresentation.isEmpty else { return nil }
946987
#if FOUNDATION_FRAMEWORK
947988
guard foundation_swift_url_enabled() else {
948989
if isAbsolute {
949-
_url = URL._converted(from: NSURL(absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: url))
990+
_url = URL._converted(from: NSURL(absoluteURLWithDataRepresentation: dataRepresentation, relativeTo: base))
950991
} else {
951-
_url = URL._converted(from: NSURL(dataRepresentation: dataRepresentation, relativeTo: url))
992+
_url = URL._converted(from: NSURL(dataRepresentation: dataRepresentation, relativeTo: base))
952993
}
953994
return
954995
}
955996
#endif
956997
var url: URL?
957998
if let string = String(data: dataRepresentation, encoding: .utf8) {
958-
url = URL(string: string, relativeTo: url)
999+
url = URL(stringOrEmpty: string, relativeTo: base)
9591000
}
9601001
if url == nil, let string = String(data: dataRepresentation, encoding: .isoLatin1) {
961-
url = URL(string: string, relativeTo: url)
1002+
url = URL(stringOrEmpty: string, relativeTo: base)
9621003
}
9631004
guard let url else {
9641005
return nil
@@ -983,7 +1024,7 @@ public struct URL: Equatable, Sendable, Hashable {
9831024
return
9841025
}
9851026
#endif
986-
guard let parseInfo = Parser.parse(urlString: _url.relativeString, encodingInvalidCharacters: true) else {
1027+
guard let parseInfo = URL.parse(urlString: _url.relativeString) else {
9871028
return nil
9881029
}
9891030
_parseInfo = parseInfo
@@ -1004,7 +1045,7 @@ public struct URL: Equatable, Sendable, Hashable {
10041045
}
10051046
#endif
10061047
bookmarkDataIsStale = stale.boolValue
1007-
let parseInfo = Parser.parse(urlString: _url.relativeString, encodingInvalidCharacters: true)!
1048+
let parseInfo = URL.parse(urlString: _url.relativeString)!
10081049
_parseInfo = parseInfo
10091050
if parseInfo.scheme == nil {
10101051
_baseParseInfo = url?.absoluteURL._parseInfo
@@ -1229,15 +1270,13 @@ public struct URL: Equatable, Sendable, Hashable {
12291270
return nil
12301271
}
12311272

1232-
#if FOUNDATION_FRAMEWORK
1233-
// Linked-on-or-after check for apps which expect .host() to return nil
1234-
// for URLs like "https:///". The new .host() implementation returns
1235-
// an empty string because according to RFC 3986, a host always exists
1236-
// if there is an authority component, it just might be empty.
1237-
if Self.compatibility2 && encodedHost.isEmpty {
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 {
12381278
return nil
12391279
}
1240-
#endif
12411280

12421281
func requestedHost() -> String? {
12431282
let didPercentEncodeHost = hasAuthority ? _parseInfo.didPercentEncodeHost : _baseParseInfo?.didPercentEncodeHost ?? false
@@ -2063,7 +2102,7 @@ public struct URL: Equatable, Sendable, Hashable {
20632102
return
20642103
}
20652104
#endif
2066-
if let parseInfo = Parser.parse(urlString: _url.relativeString, encodingInvalidCharacters: true) {
2105+
if let parseInfo = URL.parse(urlString: _url.relativeString) {
20672106
_parseInfo = parseInfo
20682107
} else {
20692108
// Go to compatibility jail (allow `URL` as a dummy string container for `NSURL` instead of crashing)
@@ -2221,7 +2260,7 @@ extension URL {
22212260
#if !NO_FILESYSTEM
22222261
baseURL = baseURL ?? .currentDirectoryOrNil()
22232262
#endif
2224-
self.init(string: "", relativeTo: baseURL)!
2263+
self.init(string: "./", relativeTo: baseURL)!
22252264
return
22262265
}
22272266

@@ -2233,13 +2272,27 @@ extension URL {
22332272
#endif
22342273

22352274
#if FOUNDATION_FRAMEWORK
2236-
// Linked-on-or-after check for apps which incorrectly pass a full
2237-
// "file:" URL string. In the old implementation, this could work
2275+
// Linked-on-or-after check for apps which incorrectly pass a full URL
2276+
// string with a scheme. In the old implementation, this could work
22382277
// rarely if the app immediately called .appendingPathComponent(_:),
2239-
// which used to accidentally interpret a relative string starting
2240-
// with "file:" as an absolute file URL string.
2241-
if Self.compatibility3 && filePath.starts(with: "file:") {
2242-
filePath = String(filePath.dropFirst(5))
2278+
// which used to accidentally interpret a relative path starting with
2279+
// "scheme:" as an absolute "scheme:" URL string.
2280+
if Self.compatibility1 {
2281+
if filePath.utf8.starts(with: "file:".utf8) {
2282+
#if canImport(os)
2283+
URL.logger.fault("API MISUSE: URL(filePath:) called with a \"file:\" scheme. Input must only contain a path. Dropping \"file:\" scheme.")
2284+
#endif
2285+
filePath = String(filePath.dropFirst(5))._compressingSlashes()
2286+
} else if filePath.utf8.starts(with: "http:".utf8) || filePath.utf8.starts(with: "https:".utf8) {
2287+
#if canImport(os)
2288+
URL.logger.fault("API MISUSE: URL(filePath:) called with an HTTP URL string. Using URL(string:) instead.")
2289+
#endif
2290+
guard let httpURL = URL(string: filePath) else {
2291+
fatalError("API MISUSE: URL(filePath:) called with an HTTP URL string. URL(string:) returned nil.")
2292+
}
2293+
self = httpURL
2294+
return
2295+
}
22432296
}
22442297
#endif
22452298

@@ -2495,6 +2548,14 @@ extension URL {
24952548
#endif // NO_FILESYSTEM
24962549
}
24972550
#endif // FOUNDATION_FRAMEWORK
2551+
2552+
// The old .appending(component:) implementation did not actually percent-encode
2553+
// "/" for file URLs as the documentation suggests. Many apps accidentally use
2554+
// .appending(component: "path/with/slashes") instead of using .appending(path:),
2555+
// so changing this behavior would cause breakage.
2556+
if isFileURL {
2557+
return appending(path: component, directoryHint: directoryHint, encodingSlashes: false)
2558+
}
24982559
return appending(path: component, directoryHint: directoryHint, encodingSlashes: true)
24992560
}
25002561

Sources/FoundationEssentials/URL/URLComponents.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ public struct URLComponents: Hashable, Equatable, Sendable {
676676
return CFURLCreateWithString(kCFAllocatorDefault, string as CFString, nil) as URL?
677677
}
678678
#endif
679-
return URL(string: string, relativeTo: nil)
679+
return URL(stringOrEmpty: string, relativeTo: nil)
680680
}
681681

682682
/// Returns a URL created from the URLComponents relative to a base URL.
@@ -690,7 +690,7 @@ public struct URLComponents: Hashable, Equatable, Sendable {
690690
return CFURLCreateWithString(kCFAllocatorDefault, string as CFString, base as CFURL) as URL?
691691
}
692692
#endif
693-
return URL(string: string, relativeTo: base)
693+
return URL(stringOrEmpty: string, relativeTo: base)
694694
}
695695

696696
/// Returns a URL string created from the URLComponents.

0 commit comments

Comments
 (0)