Skip to main content

http_url/
url.rs

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/// An immutable, parsed HTTP or HTTPS URL.
18///
19/// # Example
20///
21/// ```
22/// use http_url::HttpUrl;
23///
24/// let url = HttpUrl::parse("https://example.com/path?a=1#frag").unwrap();
25/// assert_eq!(url.scheme(), "https");
26/// assert_eq!(url.host(), "example.com");
27/// assert_eq!(url.path(), "/path");
28/// assert_eq!(url.query_parameter("a"), Some("1"));
29/// assert_eq!(url.fragment(), Some("frag"));
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct HttpUrl {
33    /// The URL scheme (`http` or `https`).
34    pub(crate) scheme: Scheme,
35    /// Username for basic authentication (empty if none).
36    pub(crate) username: String,
37    /// Password for basic authentication (empty if none).
38    pub(crate) password: String,
39    /// The host (domain name, IPv4, or bracketed IPv6).
40    pub(crate) host: String,
41    /// The port number (defaults to scheme's default if not explicitly set).
42    pub(crate) port: u16,
43    /// Decoded path segments.
44    pub(crate) path_segments: Vec<String>,
45    /// Flattened query names and values (name0, value0, name1, value1, …).
46    pub(crate) query_names_and_values: Vec<String>,
47    /// The fragment component (decoded), or `None`.
48    pub(crate) fragment: Option<String>,
49}
50
51impl HttpUrl {
52    /// Create a new builder.
53    pub fn builder() -> HttpUrlBuilder {
54        HttpUrlBuilder::new()
55    }
56
57    /// Create a builder pre-populated with this URL's components.
58    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    /// Returns the URL scheme.
72    pub fn scheme(&self) -> Scheme {
73        self.scheme
74    }
75
76    /// Returns `true` if the scheme is `https`.
77    pub fn is_https(&self) -> bool {
78        self.scheme == Scheme::Https
79    }
80
81    /// Returns the username (empty string if not present).
82    pub fn username(&self) -> &str {
83        &self.username
84    }
85
86    /// Returns the password (empty string if not present).
87    pub fn password(&self) -> &str {
88        &self.password
89    }
90
91    /// Returns the host (domain, IPv4, or bracketed IPv6).
92    pub fn host(&self) -> &str {
93        &self.host
94    }
95
96    /// The explicit port, or the default port for the scheme if none was set.
97    pub fn port(&self) -> u16 {
98        self.port
99    }
100
101    /// The port only if it differs from the scheme's default.
102    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    /// The decoded path segments.
111    pub fn path_segments(&self) -> &[String] {
112        &self.path_segments
113    }
114
115    /// The encoded path (with leading `/`), e.g. `/a/b/c`.
116    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    /// The decoded path as a string, e.g. `/a/b/c`.
129    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    /// Returns the query string (percent-encoded), or `None` if there are no parameters.
142    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    /// Returns the decoded query string, or `None`.
161    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    /// Get the first value for a query parameter name.
180    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    /// Get all values for a query parameter name.
194    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    /// Returns the set of unique query parameter names.
209    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    /// Number of query parameters.
221    pub fn query_size(&self) -> usize {
222        self.query_names_and_values.len() / 2
223    }
224
225    /// The fragment (decoded), if any.
226    pub fn fragment(&self) -> Option<&str> {
227        self.fragment.as_deref()
228    }
229
230    /// The fragment (percent-encoded), if any.
231    pub fn encoded_fragment(&self) -> Option<String> {
232        self.fragment
233            .as_ref()
234            .map(|f| percent::encode_path_segment(f))
235    }
236
237    /// Resolve a relative URL against this one.
238    ///
239    /// This follows the algorithm from RFC 3986 Section 5.
240    pub fn resolve(&self, link: &str) -> Result<HttpUrl> {
241        // If the link is absolute, just parse it directly.
242        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        // Build a new URL starting from a clone of this one.
253        let mut builder = self.new_builder();
254
255        if let Some(authority_part) = link.strip_prefix("//") {
256            // Protocol-relative: keep scheme, replace rest
257            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            // Absolute path: replace path, query, fragment
272            parse::apply_path_query_fragment(&mut builder, stripped)?;
273        } else if link.starts_with('?') {
274            // Replace query and fragment
275            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            // Replace fragment only
279            let frag = percent::decode(stripped)
280                .map_err(|_| HttpUrlError::InvalidPercentEncoding(link.to_string()))?;
281            builder.fragment = Some(frag);
282        } else {
283            // Relative path: merge with existing path
284            parse::merge_path(&mut builder, link)?;
285            // Note: merge_path should handle query/fragment internally if present
286        }
287
288        builder.build()
289    }
290
291    /// Compute the top private domain (eTLD+1) of this URL's host.
292    ///
293    /// Returns `None` if the host is an IP address or does not have enough
294    /// domain labels. This is a simplified version; a full implementation
295    /// would use the Public Suffix List.
296    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        // Simplified: assume the last two labels form the private domain.
305        // A proper implementation would consult the PSL.
306        let domain = format!("{}.{}", labels[labels.len() - 2], labels[labels.len() - 1]);
307        Some(domain)
308    }
309
310    /// The full URL string (percent-encoded).
311    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}