Skip to main content

rust_web_server/url/
mod.rs

1#[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
12/// URL parsing, building, and percent-encoding utilities.
13pub 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    /// Parse an origin-form request target (`/path?query`) — what
41    /// `Request::request_uri` holds. Anything not starting with `/` is an
42    /// error rather than being glued onto a placeholder authority, where a
43    /// target like `:x` would become the port of `http://localhost:x`.
44    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
52/// RFC 3986 parse of `scheme:[//authority]path[?query][#fragment]` into
53/// url-build-parse's `UrlComponents`, without panicking on malformed input.
54/// (url-build-parse's own `parse_url` unwraps on input-derived values — e.g.
55/// a non-numeric port — so a hostile URL could panic the caller.)
56fn 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    // Authority: after "//", up to the first '/', '?' or '#'.
65    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    // Path: up to the first '?' or '#'.
73    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
88/// `[userinfo@]host[:port]`, with `[...]` IPv6 hosts.
89fn 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