rust_web_server/url/
mod.rs1#[cfg(test)]
10mod tests;
11#[cfg(test)]
12mod example;
13pub mod path;
14
15use std::collections::HashMap;
16use url_build_parse::{build_url, UrlAuthority, UrlComponents, UrlUserInfo};
17use url_search_params::{build_url_search_params, encode_uri_component, parse_url_search_params};
18use url_search_params::decode_uri_component;
19
20pub struct URL;
22
23impl URL {
24 pub fn percent_encode(component: &str) -> String {
25 encode_uri_component(component)
26 }
27
28 pub fn percent_decode(component: &str) -> String {
29 decode_uri_component(component)
30 }
31
32 pub fn form_decode(component: &str) -> String {
36 let b = component.as_bytes();
37 let mut out = Vec::with_capacity(b.len());
38 let mut i = 0;
39 while i < b.len() {
40 let hex = |c: u8| (c as char).to_digit(16);
41 match b[i] {
42 b'+' => out.push(b' '),
43 b'%' => match (b.get(i + 1).and_then(|c| hex(*c)), b.get(i + 2).and_then(|c| hex(*c))) {
44 (Some(h), Some(l)) => {
45 out.push((h * 16 + l) as u8);
46 i += 2;
47 }
48 _ => out.push(b'%'),
49 },
50 c => out.push(c),
51 }
52 i += 1;
53 }
54 String::from_utf8_lossy(&out).into_owned()
55 }
56
57 pub fn build_query(params: HashMap<String, String>) -> String {
58 build_url_search_params(params)
59 }
60
61 pub fn parse_query(component: &str) -> HashMap<String, String> {
62 parse_url_search_params(component)
63 }
64
65 pub fn build(components: UrlComponents) -> Result<String, String> {
66 build_url(components)
67 }
68
69 pub fn parse(url: &str) -> Result<UrlComponents, String> {
70 parse_url(url)
71 }
72
73 pub fn parse_request_target(target: &str) -> Result<UrlComponents, String> {
78 if !target.starts_with('/') {
79 return Err(format!("not an origin-form request target: {:?}", target));
80 }
81 parse_url(&format!("http://localhost{}", target))
82 }
83
84 pub(crate) fn request_target_path(target: &str) -> Option<&str> {
89 if !target.starts_with('/') {
90 return None;
91 }
92 Some(&target[..target.find(['?', '#']).unwrap_or(target.len())])
93 }
94}
95
96fn parse_url(url: &str) -> Result<UrlComponents, String> {
101 let (scheme, rest) = url.split_once(':').ok_or("unable to identify scheme")?;
102 if rest.is_empty() {
103 return Err("error: remaining url is empty".to_string());
104 }
105 let mut components = UrlComponents::new();
106 components.scheme = scheme.to_string();
107
108 let mut remaining = rest;
110 if let Some(after) = rest.strip_prefix("//") {
111 let end = after.find(['/', '?', '#']).unwrap_or(after.len());
112 components.authority = Some(parse_authority(&after[..end])?);
113 remaining = &after[end..];
114 }
115
116 let path_end = remaining.find(['?', '#']).unwrap_or(remaining.len());
118 components.path = remaining[..path_end].to_string();
119 remaining = &remaining[path_end..];
120
121 if let Some(after) = remaining.strip_prefix('?') {
122 let query_end = after.find('#').unwrap_or(after.len());
123 components.query = Some(parse_url_search_params(&after[..query_end]));
124 remaining = &after[query_end..];
125 }
126 if let Some(fragment) = remaining.strip_prefix('#') {
127 components.fragment = Some(fragment.to_string());
128 }
129 Ok(components)
130}
131
132fn parse_authority(authority: &str) -> Result<UrlAuthority, String> {
134 let (user_info, host_port) = match authority.split_once('@') {
135 Some((user_info, host_port)) => {
136 let user_info = match user_info.split_once(':') {
137 Some((username, password)) => UrlUserInfo {
138 username: username.to_string(),
139 password: Some(password.to_string()),
140 },
141 None => UrlUserInfo { username: user_info.to_string(), password: None },
142 };
143 (Some(user_info), host_port)
144 }
145 None => (None, authority),
146 };
147
148 let (host, port_str) = if host_port.starts_with('[') {
149 let close = host_port.find(']').ok_or("unterminated IPv6 host")?;
150 let after = &host_port[close + 1..];
151 let port = match after.strip_prefix(':') {
152 Some(port) => Some(port),
153 None if after.is_empty() => None,
154 None => return Err(format!("unexpected characters after IPv6 host: {:?}", after)),
155 };
156 (&host_port[..=close], port)
157 } else {
158 match host_port.split_once(':') {
159 Some((host, port)) => (host, Some(port)),
160 None => (host_port, None),
161 }
162 };
163
164 let port = match port_str {
165 Some(port) => Some(port.parse::<usize>().map_err(|_| format!("invalid port: {:?}", port))?),
166 None => None,
167 };
168 Ok(UrlAuthority { user_info, host: host.to_string(), port })
169}
170