1use core::fmt;
2
3use alloc::format;
4use alloc::string::String;
5use alloc::string::ToString;
6use alloc::vec::Vec;
7
8#[cfg(test)]
9use alloc::vec;
10
11use crate::builder::HttpUrlBuilder;
12use crate::error::{HttpUrlError, Result};
13use crate::scheme::Scheme;
14use crate::util::{host, parse, percent};
15use core::fmt::Write;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct HttpUrl {
33 pub(crate) scheme: Scheme,
35 pub(crate) username: String,
37 pub(crate) password: String,
39 pub(crate) host: String,
41 pub(crate) port: u16,
43 pub(crate) path_segments: Vec<String>,
45 pub(crate) query_names_and_values: Vec<String>,
47 pub(crate) fragment: Option<String>,
49}
50
51impl HttpUrl {
52 pub fn builder() -> HttpUrlBuilder {
54 HttpUrlBuilder::new()
55 }
56
57 pub fn new_builder(&self) -> HttpUrlBuilder {
59 let mut b = HttpUrlBuilder::new();
60 b.scheme = Some(self.scheme);
61 b.username = self.username.clone();
62 b.password = self.password.clone();
63 b.host = Some(self.host.clone());
64 b.port = Some(self.port);
65 b.path_segments = self.path_segments.clone();
66 b.query_names_and_values = self.query_names_and_values.clone();
67 b.fragment = self.fragment.clone();
68 b
69 }
70
71 pub fn scheme(&self) -> Scheme {
73 self.scheme
74 }
75
76 pub fn is_https(&self) -> bool {
78 self.scheme == Scheme::Https
79 }
80
81 pub fn username(&self) -> &str {
83 &self.username
84 }
85
86 pub fn password(&self) -> &str {
88 &self.password
89 }
90
91 pub fn host(&self) -> &str {
93 &self.host
94 }
95
96 pub fn port(&self) -> u16 {
98 self.port
99 }
100
101 pub fn explicit_port(&self) -> Option<u16> {
103 if self.port == self.scheme.default_port() {
104 None
105 } else {
106 Some(self.port)
107 }
108 }
109
110 pub fn path_segments(&self) -> &[String] {
112 &self.path_segments
113 }
114
115 pub fn encoded_path(&self) -> String {
117 if self.path_segments.is_empty() {
118 return "/".to_string();
119 }
120 let mut out = String::new();
121 for seg in &self.path_segments {
122 out.push('/');
123 out.push_str(&percent::encode_path_segment(seg));
124 }
125 out
126 }
127
128 pub fn path(&self) -> String {
130 if self.path_segments.is_empty() {
131 return "/".to_string();
132 }
133 let mut out = String::new();
134 for seg in &self.path_segments {
135 out.push('/');
136 out.push_str(seg);
137 }
138 out
139 }
140
141 pub fn encoded_query(&self) -> Option<String> {
143 if self.query_names_and_values.is_empty() {
144 return None;
145 }
146 let mut out = String::new();
147 for i in (0..self.query_names_and_values.len()).step_by(2) {
148 if !out.is_empty() {
149 out.push('&');
150 }
151 out.push_str(&percent::encode_query(&self.query_names_and_values[i]));
152 if i + 1 < self.query_names_and_values.len() {
153 out.push('=');
154 out.push_str(&percent::encode_query(&self.query_names_and_values[i + 1]));
155 }
156 }
157 Some(out)
158 }
159
160 pub fn query(&self) -> Option<String> {
162 if self.query_names_and_values.is_empty() {
163 return None;
164 }
165 let mut out = String::new();
166 for i in (0..self.query_names_and_values.len()).step_by(2) {
167 if !out.is_empty() {
168 out.push('&');
169 }
170 out.push_str(&self.query_names_and_values[i]);
171 if i + 1 < self.query_names_and_values.len() {
172 out.push('=');
173 out.push_str(&self.query_names_and_values[i + 1]);
174 }
175 }
176 Some(out)
177 }
178
179 pub fn query_parameter(&self, name: &str) -> Option<&str> {
181 for i in (0..self.query_names_and_values.len()).step_by(2) {
182 if self.query_names_and_values[i] == name {
183 return if i + 1 < self.query_names_and_values.len() {
184 Some(&self.query_names_and_values[i + 1])
185 } else {
186 Some("")
187 };
188 }
189 }
190 None
191 }
192
193 pub fn query_parameters(&self, name: &str) -> Vec<&str> {
195 let mut result = Vec::new();
196 for i in (0..self.query_names_and_values.len()).step_by(2) {
197 if self.query_names_and_values[i] == name {
198 result.push(if i + 1 < self.query_names_and_values.len() {
199 &self.query_names_and_values[i + 1]
200 } else {
201 ""
202 });
203 }
204 }
205 result
206 }
207
208 pub fn query_parameter_names(&self) -> Vec<&str> {
210 let mut names: Vec<&str> = Vec::new();
211 for i in (0..self.query_names_and_values.len()).step_by(2) {
212 let name = &self.query_names_and_values[i];
213 if !names.contains(&name.as_str()) {
214 names.push(name);
215 }
216 }
217 names
218 }
219
220 pub fn query_size(&self) -> usize {
222 self.query_names_and_values.len() / 2
223 }
224
225 pub fn fragment(&self) -> Option<&str> {
227 self.fragment.as_deref()
228 }
229
230 pub fn encoded_fragment(&self) -> Option<String> {
232 self.fragment
233 .as_ref()
234 .map(|f| percent::encode_path_segment(f))
235 }
236
237 pub fn resolve(&self, link: &str) -> Result<HttpUrl> {
241 if link.contains("://") {
243 return HttpUrl::parse(link);
244 }
245
246 let link = link.trim();
247
248 if link.is_empty() {
249 return Ok(self.clone());
250 }
251
252 let mut builder = self.new_builder();
254
255 if let Some(authority_part) = link.strip_prefix("//") {
256 let (authority, pqf) = parse::split_authority(authority_part);
258 let (uname, pwd, host_str, port) = parse::parse_authority(authority, &self.scheme)?;
259 builder.host = Some(host::canonicalize_host(host_str)?);
260 builder.username = uname;
261 builder.password = pwd;
262 builder.port = Some(port);
263 if !pqf.is_empty() {
264 parse::apply_path_query_fragment(&mut builder, pqf)?;
265 } else {
266 builder.path_segments.clear();
267 builder.query_names_and_values.clear();
268 builder.fragment = None;
269 }
270 } else if let Some(stripped) = link.strip_prefix('/') {
271 parse::apply_path_query_fragment(&mut builder, stripped)?;
273 } else if link.starts_with('?') {
274 builder.query_names_and_values.clear();
276 parse::apply_path_query_fragment(&mut builder, link)?;
277 } else if let Some(stripped) = link.strip_prefix('#') {
278 let frag = percent::decode(stripped)
280 .map_err(|_| HttpUrlError::InvalidPercentEncoding(link.to_string()))?;
281 builder.fragment = Some(frag);
282 } else {
283 parse::merge_path(&mut builder, link)?;
285 }
287
288 builder.build()
289 }
290
291 pub fn top_private_domain(&self) -> Option<String> {
297 if !host::is_domain(&self.host) {
298 return None;
299 }
300 let labels: Vec<&str> = self.host.split('.').collect();
301 if labels.len() < 2 {
302 return None;
303 }
304 let domain = format!("{}.{}", labels[labels.len() - 2], labels[labels.len() - 1]);
307 Some(domain)
308 }
309
310 pub fn to_url_string(&self) -> String {
312 let mut out = String::new();
313 out.push_str(self.scheme.as_str());
314 out.push_str("://");
315
316 if !self.username.is_empty() || !self.password.is_empty() {
317 out.push_str(&percent::encode_user_info(&self.username));
318 if !self.password.is_empty() {
319 out.push(':');
320 out.push_str(&percent::encode_user_info(&self.password));
321 }
322 out.push('@');
323 }
324
325 out.push_str(&self.host);
326
327 if self.port() != self.scheme.default_port() {
328 out.push(':');
329 let _ = write!(out, "{}", self.port());
330 }
331
332 out.push_str(&self.encoded_path());
333
334 if let Some(q) = self.encoded_query() {
335 out.push('?');
336 out.push_str(&q);
337 }
338
339 if let Some(f) = &self.fragment {
340 out.push('#');
341 out.push_str(&percent::encode_path_segment(f));
342 }
343
344 out
345 }
346}
347
348impl fmt::Display for HttpUrl {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 write!(f, "{}", self.to_url_string())
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_encoded_query() {
360 let url = HttpUrl::builder()
361 .scheme(Scheme::Http)
362 .host("example.com")
363 .add_query_parameter("name", "hello world")
364 .build()
365 .unwrap();
366 assert_eq!(url.encoded_query().unwrap(), "name=hello%20world");
367 }
368
369 #[test]
370 fn test_multiple_query_values() {
371 let url = HttpUrl::parse("http://example.com/?a=1&a=2").unwrap();
372 assert_eq!(url.query_parameter("a"), Some("1"));
373 assert_eq!(url.query_parameters("a"), vec!["1", "2"]);
374 }
375
376 #[test]
377 fn test_multi_query_parameter_names() {
378 let url = HttpUrl::parse("http://example.com/?a=1&b=2&a=3").unwrap();
379 let names = url.query_parameter_names();
380 assert!(names.contains(&"a"));
381 assert!(names.contains(&"b"));
382 }
383
384 #[test]
385 fn test_resolve_absolute() {
386 let base = HttpUrl::parse("http://example.com/a/b").unwrap();
387 let resolved = base.resolve("http://other.com/c").unwrap();
388 assert_eq!(resolved.host(), "other.com");
389 assert_eq!(resolved.path(), "/c");
390 }
391
392 #[test]
393 fn test_resolve_relative() {
394 let base = HttpUrl::parse("http://example.com/a/b").unwrap();
395 let resolved = base.resolve("c").unwrap();
396 assert_eq!(resolved.path(), "/a/c");
397 }
398
399 #[test]
400 fn test_resolve_absolute_path() {
401 let base = HttpUrl::parse("http://example.com/a/b").unwrap();
402 let resolved = base.resolve("/c/d").unwrap();
403 assert_eq!(resolved.path(), "/c/d");
404 }
405
406 #[test]
407 fn test_resolve_protocol_relative() {
408 let base = HttpUrl::parse("http://example.com/a/b").unwrap();
409 let resolved = base.resolve("//other.com/c").unwrap();
410 assert_eq!(resolved.scheme(), "http");
411 assert_eq!(resolved.host(), "other.com");
412 assert_eq!(resolved.path(), "/c");
413 }
414
415 #[test]
416 fn test_resolve_query_only() {
417 let base = HttpUrl::parse("http://example.com/a/b?old=1").unwrap();
418 let resolved = base.resolve("?new=2").unwrap();
419 assert_eq!(resolved.query_parameter("new"), Some("2"));
420 assert_eq!(resolved.query_parameter("old"), None);
421 }
422
423 #[test]
424 fn test_resolve_fragment_only() {
425 let base = HttpUrl::parse("http://example.com/a/b#old").unwrap();
426 let resolved = base.resolve("#new").unwrap();
427 assert_eq!(resolved.fragment(), Some("new"));
428 }
429
430 #[test]
431 fn test_to_string_no_default_port() {
432 let url = HttpUrl::parse("http://example.com/").unwrap();
433 assert_eq!(url.to_string(), "http://example.com/");
434 }
435
436 #[test]
437 fn test_to_string_with_port() {
438 let url = HttpUrl::parse("http://example.com:8080/").unwrap();
439 assert_eq!(url.to_string(), "http://example.com:8080/");
440 }
441
442 #[test]
443 fn test_to_string_with_userinfo() {
444 let url = HttpUrl::parse("http://user:pass@example.com/").unwrap();
445 assert_eq!(url.to_string(), "http://user:pass@example.com/");
446 }
447
448 #[test]
449 fn test_top_private_domain() {
450 let url = HttpUrl::parse("http://www.example.com/path").unwrap();
451 assert_eq!(url.top_private_domain(), Some("example.com".to_string()));
452 }
453
454 #[test]
455 fn test_top_private_domain_ip() {
456 let url = HttpUrl::parse("http://192.168.1.1/").unwrap();
457 assert_eq!(url.top_private_domain(), None);
458 }
459
460 #[test]
461 fn test_new_builder_from_url() {
462 let url = HttpUrl::parse("http://example.com/path?q=1").unwrap();
463 let builder = url.new_builder();
464 let url2 = builder.build().unwrap();
465 assert_eq!(url, url2);
466 }
467}