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 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 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 let (authority, path_query_fragment) = parse::split_authority(rest);
32
33 let (username, password, host_str, port) = parse::parse_authority(authority, &scheme)?;
35
36 let host = host::canonicalize_host(host_str)?;
38
39 let (path_str, query_str, fragment_str) =
41 parse::split_path_query_fragment(path_query_fragment);
42
43 let path_segments = parse::parse_path_segments(path_str)?;
45
46 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 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}