diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index e8cd3b904..3f6b8010e 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -1213,21 +1213,38 @@ public struct URL: Equatable, Sendable, Hashable { return nil } #endif - guard let encodedHost else { return nil } - let didPercentEncodeHost = hasAuthority ? _parseInfo.didPercentEncodeHost : _baseParseInfo?.didPercentEncodeHost ?? false - if percentEncoded { - if didPercentEncodeHost { - return String(encodedHost) - } - guard let decoded = Parser.IDNADecodeHost(encodedHost) else { + guard let encodedHost else { + return nil + } + + func requestedHost() -> String? { + let didPercentEncodeHost = hasAuthority ? _parseInfo.didPercentEncodeHost : _baseParseInfo?.didPercentEncodeHost ?? false + if percentEncoded { + if didPercentEncodeHost { + return encodedHost + } + guard let decoded = Parser.IDNADecodeHost(encodedHost) else { + return encodedHost + } + return Parser.percentEncode(decoded, component: .host) + } else { + if didPercentEncodeHost { + return Parser.percentDecode(encodedHost) + } return encodedHost } - return Parser.percentEncode(decoded, component: .host) + } + + guard let requestedHost = requestedHost() else { + return nil + } + + let isIPLiteral = hasAuthority ? _parseInfo.isIPLiteral : _baseParseInfo?.isIPLiteral ?? false + if isIPLiteral { + // Strip square brackets to be compatible with old URL.host behavior + return String(requestedHost.utf8.dropFirst().dropLast()) } else { - if didPercentEncodeHost { - return Parser.percentDecode(encodedHost) - } - return String(encodedHost) + return requestedHost } } diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 70102170f..e6834886f 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -792,6 +792,42 @@ final class URLTests : XCTestCase { XCTAssertEqual(url.host, "*.xn--poema-9qae5a.com.br") } + func testURLHostIPLiteralCompatibility() throws { + var url = URL(string: "http://[::]")! + XCTAssertEqual(url.host, "::") + XCTAssertEqual(url.host(), "::") + + url = URL(string: "https://[::1]:433/")! + XCTAssertEqual(url.host, "::1") + XCTAssertEqual(url.host(), "::1") + + url = URL(string: "https://[2001:db8::]/")! + XCTAssertEqual(url.host, "2001:db8::") + XCTAssertEqual(url.host(), "2001:db8::") + + url = URL(string: "https://[2001:db8::]:433")! + XCTAssertEqual(url.host, "2001:db8::") + XCTAssertEqual(url.host(), "2001:db8::") + + url = URL(string: "http://[fe80::a%25en1]")! + XCTAssertEqual(url.absoluteString, "http://[fe80::a%25en1]") + XCTAssertEqual(url.host, "fe80::a%en1") + XCTAssertEqual(url.host(percentEncoded: true), "fe80::a%25en1") + XCTAssertEqual(url.host(percentEncoded: false), "fe80::a%en1") + + url = URL(string: "http://[fe80::a%en1]")! + XCTAssertEqual(url.absoluteString, "http://[fe80::a%25en1]") + XCTAssertEqual(url.host, "fe80::a%en1") + XCTAssertEqual(url.host(percentEncoded: true), "fe80::a%25en1") + XCTAssertEqual(url.host(percentEncoded: false), "fe80::a%en1") + + url = URL(string: "http://[fe80::a%100%CustomZone]")! + XCTAssertEqual(url.absoluteString, "http://[fe80::a%25100%25CustomZone]") + XCTAssertEqual(url.host, "fe80::a%100%CustomZone") + XCTAssertEqual(url.host(percentEncoded: true), "fe80::a%25100%25CustomZone") + XCTAssertEqual(url.host(percentEncoded: false), "fe80::a%100%CustomZone") + } + func testURLTildeFilePath() throws { func urlIsAbsolute(_ url: URL) -> Bool { if url.relativePath.utf8.first == ._slash {