1use std::hash::{Hash, Hasher};
2use std::{fmt::Display, fmt::Formatter};
3use url::Url;
4use uv_redacted::DisplaySafeUrl;
5use uv_small_str::SmallString;
6
7#[derive(Debug, Clone)]
27pub struct Realm {
28 scheme: SmallString,
29 host: Option<SmallString>,
30 port: Option<u16>,
31}
32
33impl From<&DisplaySafeUrl> for Realm {
34 fn from(url: &DisplaySafeUrl) -> Self {
35 Self::from(&**url)
36 }
37}
38
39impl From<&Url> for Realm {
40 fn from(url: &Url) -> Self {
41 Self {
42 scheme: SmallString::from(url.scheme()),
43 host: url.host_str().map(SmallString::from),
44 port: url.port(),
45 }
46 }
47}
48
49impl Display for Realm {
50 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
51 if let Some(port) = self.port {
52 write!(
53 f,
54 "{}://{}:{port}",
55 self.scheme,
56 self.host.as_deref().unwrap_or_default()
57 )
58 } else {
59 write!(
60 f,
61 "{}://{}",
62 self.scheme,
63 self.host.as_deref().unwrap_or_default()
64 )
65 }
66 }
67}
68
69impl PartialEq for Realm {
70 fn eq(&self, other: &Self) -> bool {
71 RealmRef::from(self) == RealmRef::from(other)
72 }
73}
74
75impl Eq for Realm {}
76
77impl Hash for Realm {
78 fn hash<H: Hasher>(&self, state: &mut H) {
79 RealmRef::from(self).hash(state);
80 }
81}
82
83#[derive(Debug, Copy, Clone)]
85pub struct RealmRef<'a> {
86 scheme: &'a str,
87 host: Option<&'a str>,
88 port: Option<u16>,
89}
90
91impl RealmRef<'_> {
92 pub(crate) fn is_subdomain_of(&self, other: Self) -> bool {
94 other.scheme == self.scheme
95 && other.port == self.port
96 && other.host.is_some_and(|other_host| {
97 self.host.is_some_and(|self_host| {
98 self_host
99 .strip_suffix(other_host)
100 .is_some_and(|prefix| prefix.ends_with('.'))
101 })
102 })
103 }
104}
105
106impl<'a> From<&'a Url> for RealmRef<'a> {
107 fn from(url: &'a Url) -> Self {
108 Self {
109 scheme: url.scheme(),
110 host: url.host_str(),
111 port: url.port(),
112 }
113 }
114}
115
116impl PartialEq for RealmRef<'_> {
117 fn eq(&self, other: &Self) -> bool {
118 self.scheme == other.scheme && self.host == other.host && self.port == other.port
119 }
120}
121
122impl Eq for RealmRef<'_> {}
123
124impl Hash for RealmRef<'_> {
125 fn hash<H: Hasher>(&self, state: &mut H) {
126 self.scheme.hash(state);
127 self.host.hash(state);
128 self.port.hash(state);
129 }
130}
131
132impl<'a> PartialEq<RealmRef<'a>> for Realm {
133 fn eq(&self, rhs: &RealmRef<'a>) -> bool {
134 RealmRef::from(self) == *rhs
135 }
136}
137
138impl PartialEq<Realm> for RealmRef<'_> {
139 fn eq(&self, rhs: &Realm) -> bool {
140 *self == RealmRef::from(rhs)
141 }
142}
143
144impl<'a> From<&'a Realm> for RealmRef<'a> {
145 fn from(realm: &'a Realm) -> Self {
146 Self {
147 scheme: &realm.scheme,
148 host: realm.host.as_deref(),
149 port: realm.port,
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use url::{ParseError, Url};
157
158 use crate::Realm;
159
160 #[test]
161 fn test_should_retain_auth() -> Result<(), ParseError> {
162 assert_eq!(
164 Realm::from(&Url::parse("https://example.com")?),
165 Realm::from(&Url::parse("https://example.com")?)
166 );
167
168 assert_eq!(
170 Realm::from(&Url::parse("https://example.com:1234")?),
171 Realm::from(&Url::parse("https://example.com:1234")?)
172 );
173
174 assert_eq!(
176 Realm::from(&Url::parse("http://example.com")?),
177 Realm::from(&Url::parse("http://example.com")?)
178 );
179
180 assert_eq!(
182 Realm::from(&Url::parse("http://example.com/foo")?),
183 Realm::from(&Url::parse("http://example.com/bar")?)
184 );
185
186 assert_eq!(
188 Realm::from(&Url::parse("https://example.com:443")?),
189 Realm::from(&Url::parse("https://example.com")?)
190 );
191
192 assert_eq!(
194 Realm::from(&Url::parse("http://example.com:80")?),
195 Realm::from(&Url::parse("http://example.com")?)
196 );
197
198 assert_ne!(
200 Realm::from(&Url::parse("https://example.com")?),
201 Realm::from(&Url::parse("http://example.com")?)
202 );
203
204 assert_ne!(
206 Realm::from(&Url::parse("http://example.com")?),
207 Realm::from(&Url::parse("https://example.com")?)
208 );
209
210 assert_ne!(
212 Realm::from(&Url::parse("https://foo.com")?),
213 Realm::from(&Url::parse("https://bar.com")?)
214 );
215
216 assert_ne!(
218 Realm::from(&Url::parse("https://example.com:1234")?),
219 Realm::from(&Url::parse("https://example.com:5678")?)
220 );
221
222 assert_ne!(
224 Realm::from(&Url::parse("https://example.com:443")?),
225 Realm::from(&Url::parse("https://example.com:5678")?)
226 );
227 assert_ne!(
228 Realm::from(&Url::parse("https://example.com:1234")?),
229 Realm::from(&Url::parse("https://example.com:443")?)
230 );
231
232 assert_ne!(
234 Realm::from(&Url::parse("https://example.com:80")?),
235 Realm::from(&Url::parse("https://example.com")?)
236 );
237
238 Ok(())
239 }
240
241 #[test]
242 fn test_is_subdomain_of() -> Result<(), ParseError> {
243 use crate::realm::RealmRef;
244
245 let subdomain_url = Url::parse("https://sub.example.com")?;
247 let domain_url = Url::parse("https://example.com")?;
248 let subdomain = RealmRef::from(&subdomain_url);
249 let domain = RealmRef::from(&domain_url);
250 assert!(subdomain.is_subdomain_of(domain));
251
252 let deep_subdomain_url = Url::parse("https://foo.bar.example.com")?;
254 let deep_subdomain = RealmRef::from(&deep_subdomain_url);
255 assert!(deep_subdomain.is_subdomain_of(domain));
256
257 let parent_subdomain_url = Url::parse("https://bar.example.com")?;
259 let parent_subdomain = RealmRef::from(&parent_subdomain_url);
260 assert!(deep_subdomain.is_subdomain_of(parent_subdomain));
261
262 assert!(!domain.is_subdomain_of(subdomain));
264
265 assert!(!domain.is_subdomain_of(domain));
267
268 let different_tld_url = Url::parse("https://example.org")?;
270 let different_tld = RealmRef::from(&different_tld_url);
271 assert!(!different_tld.is_subdomain_of(domain));
272
273 let partial_match_url = Url::parse("https://notexample.com")?;
275 let partial_match = RealmRef::from(&partial_match_url);
276 assert!(!partial_match.is_subdomain_of(domain));
277
278 let http_subdomain_url = Url::parse("http://sub.example.com")?;
280 let https_domain_url = Url::parse("https://example.com")?;
281 let http_subdomain = RealmRef::from(&http_subdomain_url);
282 let https_domain = RealmRef::from(&https_domain_url);
283 assert!(!http_subdomain.is_subdomain_of(https_domain));
284
285 let subdomain_port_8080_url = Url::parse("https://sub.example.com:8080")?;
287 let domain_port_9090_url = Url::parse("https://example.com:9090")?;
288 let subdomain_port_8080 = RealmRef::from(&subdomain_port_8080_url);
289 let domain_port_9090 = RealmRef::from(&domain_port_9090_url);
290 assert!(!subdomain_port_8080.is_subdomain_of(domain_port_9090));
291
292 let subdomain_with_port_url = Url::parse("https://sub.example.com:8080")?;
294 let domain_with_port_url = Url::parse("https://example.com:8080")?;
295 let subdomain_with_port = RealmRef::from(&subdomain_with_port_url);
296 let domain_with_port = RealmRef::from(&domain_with_port_url);
297 assert!(subdomain_with_port.is_subdomain_of(domain_with_port));
298
299 let subdomain_default_url = Url::parse("https://sub.example.com")?;
301 let domain_explicit_443_url = Url::parse("https://example.com:443")?;
302 let subdomain_default = RealmRef::from(&subdomain_default_url);
303 let domain_explicit_443 = RealmRef::from(&domain_explicit_443_url);
304 assert!(subdomain_default.is_subdomain_of(domain_explicit_443));
305
306 let file_url = Url::parse("file:///path/to/file")?;
308 let https_url = Url::parse("https://example.com")?;
309 let file_realm = RealmRef::from(&file_url);
310 let https_realm = RealmRef::from(&https_url);
311 assert!(!file_realm.is_subdomain_of(https_realm));
312 assert!(!https_realm.is_subdomain_of(file_realm));
313
314 let subdomain_with_path_url = Url::parse("https://sub.example.com/path")?;
316 let domain_with_path_url = Url::parse("https://example.com/other")?;
317 let subdomain_with_path = RealmRef::from(&subdomain_with_path_url);
318 let domain_with_path = RealmRef::from(&domain_with_path_url);
319 assert!(subdomain_with_path.is_subdomain_of(domain_with_path));
320
321 Ok(())
322 }
323}