Skip to content

Commit 91bc159

Browse files
authored
Improve URL equality (#453)
1 parent 989440b commit 91bc159

File tree

8 files changed

+282
-29
lines changed

8 files changed

+282
-29
lines changed

Sources/Shared/Toolkit/URL/Absolute URL/FileURL.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ public struct FileURL: AbsoluteURL, Hashable, Sendable {
6666
public func isDirectory() throws -> Bool {
6767
try (url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
6868
}
69+
70+
public func hash(into hasher: inout Hasher) {
71+
hasher.combine(path)
72+
hasher.combine(url.user)
73+
}
74+
75+
public static func == (lhs: Self, rhs: Self) -> Bool {
76+
lhs.path == rhs.path
77+
&& lhs.url.user == rhs.url.user
78+
}
6979
}
7080

7181
public extension URLConvertible {

Sources/Shared/Toolkit/URL/Absolute URL/HTTPURL.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ public struct HTTPURL: AbsoluteURL, Hashable, Sendable {
3535
}
3636
return o
3737
}
38+
39+
public func hash(into hasher: inout Hasher) {
40+
hasher.combine(origin)
41+
hasher.combine(path)
42+
hasher.combine(query)
43+
hasher.combine(fragment)
44+
hasher.combine(url.user)
45+
}
46+
47+
public static func == (lhs: Self, rhs: Self) -> Bool {
48+
lhs.origin == rhs.origin
49+
&& lhs.path == rhs.path
50+
&& lhs.query == rhs.query
51+
&& lhs.fragment == rhs.fragment
52+
&& lhs.url.user == rhs.url.user
53+
}
3854
}
3955

4056
public extension URLConvertible {

Sources/Shared/Toolkit/URL/Absolute URL/UnknownAbsoluteURL.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,24 @@ struct UnknownAbsoluteURL: AbsoluteURL, Hashable {
2323
let url: URL
2424
let scheme: URLScheme
2525
let origin: String? = nil
26+
27+
public func hash(into hasher: inout Hasher) {
28+
hasher.combine(scheme)
29+
hasher.combine(host)
30+
hasher.combine(url.port)
31+
hasher.combine(path)
32+
hasher.combine(query)
33+
hasher.combine(fragment)
34+
hasher.combine(url.user)
35+
}
36+
37+
public static func == (lhs: Self, rhs: Self) -> Bool {
38+
lhs.scheme == rhs.scheme
39+
&& lhs.host == rhs.host
40+
&& lhs.url.port == rhs.url.port
41+
&& lhs.path == rhs.path
42+
&& lhs.query == rhs.query
43+
&& lhs.fragment == rhs.fragment
44+
&& lhs.url.user == rhs.url.user
45+
}
2646
}

Sources/Shared/Toolkit/URL/RelativeURL.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ public struct RelativeURL: URLProtocol, Hashable {
9696
.removingPrefix("/")
9797
)
9898
}
99+
100+
public func hash(into hasher: inout Hasher) {
101+
hasher.combine(path)
102+
hasher.combine(query)
103+
hasher.combine(fragment)
104+
}
105+
106+
public static func == (lhs: Self, rhs: Self) -> Bool {
107+
lhs.path == rhs.path
108+
&& lhs.query == rhs.query
109+
&& lhs.fragment == rhs.fragment
110+
}
99111
}
100112

101113
/// Implements `URLConvertible`.

Tests/SharedTests/Toolkit/URL/Absolute URL/FileURLTests.swift

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,60 @@ import XCTest
1010

1111
class FileURLTests: XCTestCase {
1212
func testEquality() {
13+
// Paths must be equal.
1314
XCTAssertEqual(
1415
FileURL(string: "file:///foo/bar")!,
15-
FileURL(string: "file:///foo/bar")!
16+
FileURL(string: "file:///foo/bar")
1617
)
17-
// Fragments are ignored.
18+
XCTAssertNotEqual(
19+
FileURL(string: "file:///foo/baz")!,
20+
FileURL(string: "file:///foo/bar")
21+
)
22+
23+
// Paths is compared percent and entity-decoded.
1824
XCTAssertEqual(
19-
FileURL(string: "file:///foo/bar")!,
20-
FileURL(string: "file:///foo/bar#fragment")!
25+
FileURL(string: "file:///c%27est%20valide")!,
26+
FileURL(string: "file:///c%27est%20valide")
2127
)
22-
XCTAssertNotEqual(
23-
FileURL(string: "file:///foo/bar")!,
24-
FileURL(string: "file:///foo/baz")!
28+
XCTAssertEqual(
29+
FileURL(string: "file:///c'est%20valide")!,
30+
FileURL(string: "file:///c%27est%20valide")
31+
)
32+
33+
// Authority must be equal.
34+
XCTAssertEqual(
35+
FileURL(string: "file://user:password@host/foo")!,
36+
FileURL(string: "file://user:password@host/foo")
2537
)
2638
XCTAssertNotEqual(
27-
FileURL(string: "file:///foo/bar")!,
28-
FileURL(string: "file:///foo/bar/")!
39+
FileURL(string: "file://foo"),
40+
FileURL(string: "file://host/foo")
41+
)
42+
43+
// Query parameters are ignored.
44+
XCTAssertEqual(
45+
FileURL(string: "file:///foo/bar?b=b&a=a")!,
46+
FileURL(string: "file:///foo/bar?a=a&b=b")
47+
)
48+
XCTAssertEqual(
49+
FileURL(string: "file:///foo/bar?b=b")!,
50+
FileURL(string: "file:///foo/bar?a=a")
51+
)
52+
53+
// Scheme is case insensitive.
54+
XCTAssertEqual(
55+
FileURL(string: "FILE:///foo")!,
56+
FileURL(string: "file:///foo")
57+
)
58+
59+
// Fragment is ignored.
60+
XCTAssertEqual(
61+
FileURL(string: "file:///foo")!,
62+
FileURL(string: "file:///foo#fragment")
63+
)
64+
XCTAssertEqual(
65+
FileURL(string: "file:///foo#other")!,
66+
FileURL(string: "file:///foo#fragment")
2967
)
3068
}
3169

Tests/SharedTests/Toolkit/URL/Absolute URL/HTTPURLTests.swift

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,90 @@ import XCTest
1010

1111
class HTTPURLTests: XCTestCase {
1212
func testEquality() {
13+
// Paths must be equal.
1314
XCTAssertEqual(
14-
HTTPURL(string: "http://domain.com")!,
15-
HTTPURL(string: "http://domain.com")!
15+
HTTPURL(string: "http://example.com/foo/bar")!,
16+
HTTPURL(string: "http://example.com/foo/bar")
1617
)
1718
XCTAssertNotEqual(
18-
HTTPURL(string: "http://domain.com")!,
19-
HTTPURL(string: "http://domain.com#fragment")!
19+
HTTPURL(string: "http://example.com/foo/baz")!,
20+
HTTPURL(string: "http://example.com/foo/bar")
21+
)
22+
23+
// Paths is compared percent and entity-decoded.
24+
XCTAssertEqual(
25+
HTTPURL(string: "http://example.com/c%27est%20valide")!,
26+
HTTPURL(string: "http://example.com/c%27est%20valide")
27+
)
28+
XCTAssertEqual(
29+
HTTPURL(string: "http://example.com/c'est%20valide")!,
30+
HTTPURL(string: "http://example.com/c%27est%20valide")
31+
)
32+
33+
// Authority must be equal.
34+
XCTAssertEqual(
35+
HTTPURL(string: "http://example.com/foo")!,
36+
HTTPURL(string: "http://example.com/foo")
37+
)
38+
XCTAssertNotEqual(
39+
HTTPURL(string: "http://example.com:80/foo")!,
40+
HTTPURL(string: "http://example.com/foo")
41+
)
42+
XCTAssertNotEqual(
43+
HTTPURL(string: "http://example.com:80/foo")!,
44+
HTTPURL(string: "http://example.com:443/foo")
45+
)
46+
XCTAssertNotEqual(
47+
HTTPURL(string: "http://example.com:80/foo")!,
48+
HTTPURL(string: "http://example.com/foo")
49+
)
50+
XCTAssertNotEqual(
51+
HTTPURL(string: "http://domain.com/foo")!,
52+
HTTPURL(string: "http://example.com/foo")
53+
)
54+
XCTAssertNotEqual(
55+
HTTPURL(string: "http://user:[email protected]/foo")!,
56+
HTTPURL(string: "http://example.com/foo")
57+
)
58+
XCTAssertNotEqual(
59+
HTTPURL(string: "http://user:[email protected]/foo")!,
60+
HTTPURL(string: "http://other:[email protected]/foo")
61+
)
62+
63+
// Order of query parameters is important.
64+
XCTAssertNotEqual(
65+
HTTPURL(string: "http://example.com/foo/bar?b=b&a=a")!,
66+
HTTPURL(string: "http://example.com/foo/bar?a=a&b=b")
67+
)
68+
69+
// Content of parameters is important.
70+
XCTAssertEqual(
71+
HTTPURL(string: "http://example.com/foo/bar?a=a&b=b")!,
72+
HTTPURL(string: "http://example.com/foo/bar?a=a&b=b")
73+
)
74+
XCTAssertNotEqual(
75+
HTTPURL(string: "http://example.com/foo/bar?b=b")!,
76+
HTTPURL(string: "http://example.com/foo/bar?a=a")
77+
)
78+
79+
// Scheme is case insensitive.
80+
XCTAssertEqual(
81+
HTTPURL(string: "HTTP://example.com/foo")!,
82+
HTTPURL(string: "http://example.com/foo")
83+
)
84+
XCTAssertNotEqual(
85+
HTTPURL(string: "https://example.com/foo")!,
86+
HTTPURL(string: "http://example.com/foo")
87+
)
88+
89+
// Fragment is relevant.
90+
XCTAssertEqual(
91+
HTTPURL(string: "http://example.com/foo#fragment")!,
92+
HTTPURL(string: "http://example.com/foo#fragment")
93+
)
94+
XCTAssertNotEqual(
95+
HTTPURL(string: "http://example.com/foo#other")!,
96+
HTTPURL(string: "http://example.com/foo#fragment")
2097
)
2198
}
2299

Tests/SharedTests/Toolkit/URL/Absolute URL/UnknownAbsoluteURLTests.swift

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,86 @@ import XCTest
1010

1111
class UnknownAbsoluteURLTests: XCTestCase {
1212
func testEquality() {
13+
// Paths must be equal.
1314
XCTAssertEqual(
14-
UnknownAbsoluteURL(string: "opds://domain.com")!,
15-
UnknownAbsoluteURL(string: "opds://domain.com")!
15+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar")!,
16+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar")
1617
)
1718
XCTAssertNotEqual(
18-
UnknownAbsoluteURL(string: "opds://domain.com")!,
19-
UnknownAbsoluteURL(string: "opds://domain.com#fragment")!
19+
UnknownAbsoluteURL(string: "opds://example.com/foo/baz")!,
20+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar")
21+
)
22+
23+
// Paths is compared percent and entity-decoded.
24+
XCTAssertEqual(
25+
UnknownAbsoluteURL(string: "opds://example.com/c%27est%20valide")!,
26+
UnknownAbsoluteURL(string: "opds://example.com/c%27est%20valide")
27+
)
28+
XCTAssertEqual(
29+
UnknownAbsoluteURL(string: "opds://example.com/c'est%20valide")!,
30+
UnknownAbsoluteURL(string: "opds://example.com/c%27est%20valide")
31+
)
32+
33+
// Authority must be equal.
34+
XCTAssertEqual(
35+
UnknownAbsoluteURL(string: "opds://example.com/foo")!,
36+
UnknownAbsoluteURL(string: "opds://example.com/foo")
37+
)
38+
XCTAssertNotEqual(
39+
UnknownAbsoluteURL(string: "opds://example.com:80/foo")!,
40+
UnknownAbsoluteURL(string: "opds://example.com/foo")
41+
)
42+
XCTAssertNotEqual(
43+
UnknownAbsoluteURL(string: "opds://example.com:80/foo")!,
44+
UnknownAbsoluteURL(string: "opds://example.com:443/foo")
45+
)
46+
XCTAssertNotEqual(
47+
UnknownAbsoluteURL(string: "opds://example.com:80/foo")!,
48+
UnknownAbsoluteURL(string: "opds://example.com/foo")
49+
)
50+
XCTAssertNotEqual(
51+
UnknownAbsoluteURL(string: "opds://domain.com/foo")!,
52+
UnknownAbsoluteURL(string: "opds://example.com/foo")
53+
)
54+
XCTAssertNotEqual(
55+
UnknownAbsoluteURL(string: "opds://user:[email protected]/foo")!,
56+
UnknownAbsoluteURL(string: "opds://example.com/foo")
57+
)
58+
XCTAssertNotEqual(
59+
UnknownAbsoluteURL(string: "opds://user:[email protected]/foo")!,
60+
UnknownAbsoluteURL(string: "opds://other:[email protected]/foo")
61+
)
62+
63+
// Order of query parameters is important.
64+
XCTAssertNotEqual(
65+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar?b=b&a=a")!,
66+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar?a=a&b=b")
67+
)
68+
69+
// Content of parameters is important.
70+
XCTAssertEqual(
71+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar?a=a&b=b")!,
72+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar?a=a&b=b")
73+
)
74+
XCTAssertNotEqual(
75+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar?b=b")!,
76+
UnknownAbsoluteURL(string: "opds://example.com/foo/bar?a=a")
77+
)
78+
79+
// Scheme is case insensitive.
80+
XCTAssertEqual(
81+
UnknownAbsoluteURL(string: "OPDS://example.com/foo")!,
82+
UnknownAbsoluteURL(string: "opds://example.com/foo")
83+
)
84+
85+
// Fragment is relevant.
86+
XCTAssertEqual(
87+
UnknownAbsoluteURL(string: "opds://example.com/foo#fragment")!,
88+
UnknownAbsoluteURL(string: "opds://example.com/foo#fragment")
89+
)
90+
XCTAssertNotEqual(
91+
UnknownAbsoluteURL(string: "opds://example.com/foo#other")!,
92+
UnknownAbsoluteURL(string: "opds://example.com/foo#fragment")
2093
)
2194
}
2295

Tests/SharedTests/Toolkit/URL/RelativeURLTests.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,25 @@ import XCTest
1212

1313
class RelativeURLTests: XCTestCase {
1414
func testEquality() {
15-
XCTAssertEqual(
16-
RelativeURL(string: "dir/file")!,
17-
RelativeURL(string: "dir/file")!
18-
)
19-
XCTAssertNotEqual(
20-
RelativeURL(string: "dir/file/")!,
21-
RelativeURL(string: "dir/file")!
22-
)
23-
XCTAssertNotEqual(
24-
RelativeURL(string: "dir")!,
25-
RelativeURL(string: "dir/file")!
26-
)
15+
// Paths must be equal.
16+
XCTAssertEqual(RelativeURL(string: "foo/bar")!, RelativeURL(string: "foo/bar"))
17+
XCTAssertNotEqual(RelativeURL(string: "foo/bar")!, RelativeURL(string: "foo/bar/"))
18+
XCTAssertNotEqual(RelativeURL(string: "foo/baz")!, RelativeURL(string: "foo/bar"))
19+
20+
// Paths is compared percent and entity-decoded.
21+
XCTAssertEqual(RelativeURL(string: "c%27est%20valide")!, RelativeURL(string: "c%27est%20valide"))
22+
XCTAssertEqual(RelativeURL(string: "c'est%20valide")!, RelativeURL(string: "c%27est%20valide"))
23+
24+
// Order of query parameters is important.
25+
XCTAssertNotEqual(RelativeURL(string: "foo/bar?b=b&a=a")!, RelativeURL(string: "foo/bar?a=a&b=b"))
26+
27+
// Content of parameters is important.
28+
XCTAssertEqual(RelativeURL(string: "foo/bar?a=a&b=b")!, RelativeURL(string: "foo/bar?a=a&b=b"))
29+
XCTAssertNotEqual(RelativeURL(string: "foo/bar?b=b")!, RelativeURL(string: "foo/bar?a=a"))
30+
31+
// Fragment is relevant.
32+
XCTAssertEqual(RelativeURL(string: "foo/bar#fragment")!, RelativeURL(string: "foo/bar#fragment"))
33+
XCTAssertNotEqual(RelativeURL(string: "foo/bar#other")!, RelativeURL(string: "foo/bar#fragment"))
2734
}
2835

2936
// MARK: - URLProtocol

0 commit comments

Comments
 (0)