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