Skip to main content

http_url/
parse.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3use core::str::FromStr;
4
5use crate::error::Result;
6use crate::util::{host, parse, percent};
7use crate::{HttpUrl, HttpUrlError, Scheme};
8
9impl HttpUrl {
10    /// Parse a URL string into an `HttpUrl`.
11    ///
12    /// The URL must have an `http://` or `https://` scheme.
13    pub fn parse(url: &str) -> Result<Self> {
14        if url.is_empty() {
15            return Err(HttpUrlError::EmptyUrl);
16        }
17
18        let url = url.trim();
19
20        // 1. Find scheme
21        let scheme_end = url.find("://").ok_or(HttpUrlError::MissingScheme)?;
22        let scheme = Scheme::from_str(&url[..scheme_end])?;
23        let rest = &url[scheme_end + 3..];
24
25        if rest.is_empty() {
26            return Err(HttpUrlError::MissingHost);
27        }
28
29        // 2. Split authority, path, query, fragment
30        //    Find the start of path (first '/' after authority), or query '?', or fragment '#'
31        let (authority, path_query_fragment) = parse::split_authority(rest);
32
33        // 3. Parse userinfo@host:port from authority
34        let (username, password, host_str, port) = parse::parse_authority(authority, &scheme)?;
35
36        // 4. Parse host
37        let host = host::canonicalize_host(host_str)?;
38
39        // 5. Split path?query#fragment
40        let (path_str, query_str, fragment_str) =
41            parse::split_path_query_fragment(path_query_fragment);
42
43        // 6. Parse path segments
44        let path_segments = parse::parse_path_segments(path_str)?;
45
46        // 7. Parse query parameters
47        let query_names_and_values = if let Some(q) = query_str {
48            parse::parse_query_string(q)?
49        } else {
50            Vec::new()
51        };
52
53        // 8. Fragment (percent-decoded)
54        let fragment = match fragment_str {
55            Some(f) => Some(
56                percent::decode(f)
57                    .map_err(|_| HttpUrlError::InvalidPercentEncoding(f.to_string()))?,
58            ),
59            None => None,
60        };
61
62        Ok(Self {
63            scheme,
64            username,
65            password,
66            host,
67            port,
68            path_segments,
69            query_names_and_values,
70            fragment,
71        })
72    }
73}
74
75impl FromStr for HttpUrl {
76    type Err = HttpUrlError;
77
78    fn from_str(s: &str) -> Result<Self> {
79        HttpUrl::parse(s)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_parse_simple() {
89        let url = HttpUrl::parse("http://example.com").unwrap();
90        assert_eq!(url.scheme(), "http");
91        assert_eq!(url.host(), "example.com");
92        assert_eq!(url.port(), 80);
93        assert_eq!(url.path(), "/");
94    }
95
96    #[test]
97    fn test_parse_https() {
98        let url = HttpUrl::parse("https://example.com/path").unwrap();
99        assert_eq!(url.scheme(), "https");
100        assert_eq!(url.port(), 443);
101        assert_eq!(url.path(), "/path");
102    }
103
104    #[test]
105    fn test_parse_with_port() {
106        let url = HttpUrl::parse("http://example.com:8080/").unwrap();
107        assert_eq!(url.port(), 8080);
108        assert_eq!(url.explicit_port(), Some(8080));
109    }
110
111    #[test]
112    fn test_parse_default_port_explicit() {
113        let url = HttpUrl::parse("http://example.com:80/").unwrap();
114        assert_eq!(url.port(), 80);
115        assert_eq!(url.explicit_port(), None);
116    }
117
118    #[test]
119    fn test_parse_with_userinfo() {
120        let url = HttpUrl::parse("http://user:pass@example.com/").unwrap();
121        assert_eq!(url.username(), "user");
122        assert_eq!(url.password(), "pass");
123    }
124
125    #[test]
126    fn test_parse_with_username_only() {
127        let url = HttpUrl::parse("http://user@example.com/").unwrap();
128        assert_eq!(url.username(), "user");
129        assert_eq!(url.password(), "");
130    }
131
132    #[test]
133    fn test_parse_with_query() {
134        let url = HttpUrl::parse("http://example.com/?a=1&b=2").unwrap();
135        assert_eq!(url.query_parameter("a"), Some("1"));
136        assert_eq!(url.query_parameter("b"), Some("2"));
137        assert_eq!(url.query_size(), 2);
138    }
139
140    #[test]
141    fn test_parse_with_fragment() {
142        let url = HttpUrl::parse("http://example.com/#section").unwrap();
143        assert_eq!(url.fragment(), Some("section"));
144    }
145
146    #[test]
147    fn test_parse_encoded_path() {
148        let url = HttpUrl::parse("http://example.com/hello%20world").unwrap();
149        assert_eq!(url.path(), "/hello world");
150        assert_eq!(url.encoded_path(), "/hello%20world");
151    }
152
153    #[test]
154    fn test_parse_ipv6() {
155        let url = HttpUrl::parse("http://[::1]:8080/path").unwrap();
156        assert_eq!(url.host(), "[::1]");
157        assert_eq!(url.port(), 8080);
158    }
159
160    #[test]
161    fn test_parse_errors() {
162        assert!(HttpUrl::parse("").is_err());
163        assert!(HttpUrl::parse("ftp://example.com").is_err());
164        assert!(HttpUrl::parse("http://").is_err());
165    }
166
167    #[test]
168    fn test_path_canonicalization() {
169        let url = HttpUrl::parse("http://example.com/a/b/../c/./d").unwrap();
170        assert_eq!(url.path(), "/a/c/d");
171    }
172
173    #[test]
174    fn test_empty_query() {
175        let url = HttpUrl::parse("http://example.com/?").unwrap();
176        assert_eq!(url.query(), None);
177        assert_eq!(url.query_size(), 0);
178    }
179
180    #[test]
181    fn test_unicode_path() {
182        let url = HttpUrl::parse("http://example.com/%E4%B8%AD%E6%96%87").unwrap();
183        assert_eq!(url.path(), "/中文");
184    }
185
186    #[test]
187    fn test_backslash_in_path() {
188        let url = HttpUrl::parse("http://example.com/a\\b\\c").unwrap();
189        assert_eq!(url.path(), "/a/b/c");
190
191        let url = HttpUrl::parse("http://example.com/a\\b/../c").unwrap();
192        assert_eq!(url.path(), "/a/c");
193    }
194
195    #[test]
196    fn test_port_from_str() {
197        let url: HttpUrl = "http://example.com:1234/".parse().unwrap();
198        assert_eq!(url.port(), 1234);
199    }
200
201    #[test]
202    fn test_parse_roundtrip() {
203        let inputs = [
204            "http://example.com/",
205            "https://example.com/path/to?q=1&r=2#frag",
206            "http://user:pass@host.com:8080/a/b/c",
207            "http://[::1]:9090/path",
208            "http://example.com/hello%20world",
209        ];
210        for input in &inputs {
211            let url = HttpUrl::parse(input).unwrap();
212            let output = url.to_url_string();
213            let reparsed = HttpUrl::parse(&output).unwrap();
214            assert_eq!(url, reparsed, "roundtrip failed for: {}", input);
215        }
216    }
217}