Skip to main content

trillium_client/
into_url.rs

1use crate::{Error, Method, Result};
2use std::{
3    borrow::Cow,
4    net::{IpAddr, SocketAddr},
5    str::FromStr,
6};
7use trillium_server_common::url::{ParseError, Url};
8
9/// attempt to construct a url, with base if present
10pub trait IntoUrl {
11    /// attempt to construct a url, with base if present
12    fn into_url(self, base: Option<&Url>) -> Result<Url>;
13
14    /// Returns an explicit request target override for the given method, if this input represents
15    /// a special-form target like `*` (for OPTIONS) or `host:port` (for CONNECT).
16    fn request_target(&self, _method: Method) -> Option<Cow<'static, str>> {
17        None
18    }
19}
20
21impl IntoUrl for Url {
22    fn into_url(self, base: Option<&Url>) -> Result<Url> {
23        if self.cannot_be_a_base() {
24            return Err(Error::UnexpectedUriFormat);
25        }
26
27        if base.is_some_and(|base| !self.as_str().starts_with(base.as_str())) {
28            Err(Error::UnexpectedUriFormat)
29        } else {
30            Ok(self)
31        }
32    }
33}
34
35impl IntoUrl for &str {
36    fn into_url(self, base: Option<&Url>) -> Result<Url> {
37        match (Url::from_str(self), base) {
38            (Ok(url), base) => url.into_url(base),
39            (Err(ParseError::RelativeUrlWithoutBase), Some(base)) => base
40                .join(self.trim_start_matches('/'))
41                .map_err(|_| Error::UnexpectedUriFormat),
42            _ => Err(Error::UnexpectedUriFormat),
43        }
44    }
45
46    fn request_target(&self, method: Method) -> Option<Cow<'static, str>> {
47        match method {
48            Method::Connect if !self.contains('/') => Url::from_str(&format!("http://{self}"))
49                .ok()
50                .map(|_| Cow::Owned(self.to_string())),
51
52            Method::Options if *self == "*" => Some(Cow::Borrowed("*")),
53            _ => None,
54        }
55    }
56}
57
58impl IntoUrl for String {
59    #[inline(always)]
60    fn into_url(self, base: Option<&Url>) -> Result<Url> {
61        self.as_str().into_url(base)
62    }
63
64    fn request_target(&self, method: Method) -> Option<Cow<'static, str>> {
65        self.as_str().request_target(method)
66    }
67}
68
69impl<S: AsRef<str>> IntoUrl for &[S] {
70    fn into_url(self, base: Option<&Url>) -> Result<Url> {
71        let Some(mut url) = base.cloned() else {
72            return Err(Error::UnexpectedUriFormat);
73        };
74        url.path_segments_mut()
75            .map_err(|_| Error::UnexpectedUriFormat)?
76            .pop_if_empty()
77            .extend(self);
78        Ok(url)
79    }
80}
81
82impl<S: AsRef<str>, const N: usize> IntoUrl for [S; N] {
83    fn into_url(self, base: Option<&Url>) -> Result<Url> {
84        self.as_slice().into_url(base)
85    }
86}
87
88impl<S: AsRef<str>> IntoUrl for Vec<S> {
89    fn into_url(self, base: Option<&Url>) -> Result<Url> {
90        self.as_slice().into_url(base)
91    }
92}
93
94impl IntoUrl for SocketAddr {
95    fn into_url(self, base: Option<&Url>) -> Result<Url> {
96        let scheme = if self.port() == 443 { "https" } else { "http" };
97        format!("{scheme}://{self}").into_url(base)
98    }
99
100    fn request_target(&self, method: Method) -> Option<Cow<'static, str>> {
101        (method == Method::Connect).then(|| self.to_string().into())
102    }
103}
104
105impl IntoUrl for IpAddr {
106    /// note that http is assumed regardless of port
107    fn into_url(self, base: Option<&Url>) -> Result<Url> {
108        match self {
109            IpAddr::V4(v4) => format!("http://{v4}"),
110            IpAddr::V6(v6) => format!("http://[{v6}]"),
111        }
112        .into_url(base)
113    }
114}