rust_web_server/url/
mod.rs1#[cfg(test)]
2mod tests;
3#[cfg(test)]
4mod example;
5pub mod path;
6
7use std::collections::HashMap;
8use url_build_parse::{build_url, UrlAuthority, UrlComponents, UrlUserInfo};
9use url_search_params::{build_url_search_params, encode_uri_component, parse_url_search_params};
10use url_search_params::decode_uri_component;
11
12pub struct URL;
14
15impl URL {
16 pub fn percent_encode(component: &str) -> String {
17 encode_uri_component(component)
18 }
19
20 pub fn percent_decode(component: &str) -> String {
21 decode_uri_component(component)
22 }
23
24 pub fn build_query(params: HashMap<String, String>) -> String {
25 build_url_search_params(params)
26 }
27
28 pub fn parse_query(component: &str) -> HashMap<String, String> {
29 parse_url_search_params(component)
30 }
31
32 pub fn build(components: UrlComponents) -> Result<String, String> {
33 build_url(components)
34 }
35
36 pub fn parse(url: &str) -> Result<UrlComponents, String> {
37 parse_url(url)
38 }
39
40 pub fn parse_request_target(target: &str) -> Result<UrlComponents, String> {
45 if !target.starts_with('/') {
46 return Err(format!("not an origin-form request target: {:?}", target));
47 }
48 parse_url(&format!("http://localhost{}", target))
49 }
50}
51
52fn parse_url(url: &str) -> Result<UrlComponents, String> {
57 let (scheme, rest) = url.split_once(':').ok_or("unable to identify scheme")?;
58 if rest.is_empty() {
59 return Err("error: remaining url is empty".to_string());
60 }
61 let mut components = UrlComponents::new();
62 components.scheme = scheme.to_string();
63
64 let mut remaining = rest;
66 if let Some(after) = rest.strip_prefix("//") {
67 let end = after.find(['/', '?', '#']).unwrap_or(after.len());
68 components.authority = Some(parse_authority(&after[..end])?);
69 remaining = &after[end..];
70 }
71
72 let path_end = remaining.find(['?', '#']).unwrap_or(remaining.len());
74 components.path = remaining[..path_end].to_string();
75 remaining = &remaining[path_end..];
76
77 if let Some(after) = remaining.strip_prefix('?') {
78 let query_end = after.find('#').unwrap_or(after.len());
79 components.query = Some(parse_url_search_params(&after[..query_end]));
80 remaining = &after[query_end..];
81 }
82 if let Some(fragment) = remaining.strip_prefix('#') {
83 components.fragment = Some(fragment.to_string());
84 }
85 Ok(components)
86}
87
88fn parse_authority(authority: &str) -> Result<UrlAuthority, String> {
90 let (user_info, host_port) = match authority.split_once('@') {
91 Some((user_info, host_port)) => {
92 let user_info = match user_info.split_once(':') {
93 Some((username, password)) => UrlUserInfo {
94 username: username.to_string(),
95 password: Some(password.to_string()),
96 },
97 None => UrlUserInfo { username: user_info.to_string(), password: None },
98 };
99 (Some(user_info), host_port)
100 }
101 None => (None, authority),
102 };
103
104 let (host, port_str) = if host_port.starts_with('[') {
105 let close = host_port.find(']').ok_or("unterminated IPv6 host")?;
106 let after = &host_port[close + 1..];
107 let port = match after.strip_prefix(':') {
108 Some(port) => Some(port),
109 None if after.is_empty() => None,
110 None => return Err(format!("unexpected characters after IPv6 host: {:?}", after)),
111 };
112 (&host_port[..=close], port)
113 } else {
114 match host_port.split_once(':') {
115 Some((host, port)) => (host, Some(port)),
116 None => (host_port, None),
117 }
118 };
119
120 let port = match port_str {
121 Some(port) => Some(port.parse::<usize>().map_err(|_| format!("invalid port: {:?}", port))?),
122 None => None,
123 };
124 Ok(UrlAuthority { user_info, host: host.to_string(), port })
125}
126