Skip to main content

http_uri_ext/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use http::uri::{self, Uri};
6
7pub trait HttpUriExt {
8    /// Appends `/` to `self`'s path if it does not already end with one, then appends `path`,
9    /// preserving any query parameters. Does nothing if `path` is the empty string. Note that
10    /// empty paths on URIs with authorities are implicitly normalized to start with `/` by
11    /// `http::Uri`.
12    ///
13    /// Will error if `path` contains invalid URI characters, or if the resulting URI parts are
14    /// invalid (e.g., combining an authority and path without a scheme).
15    fn extend_dir_with_path(self, path: &str) -> Result<Uri, Error>;
16
17    /// Append the given query parameter `key`=`value` to the URI, preserving existing query
18    /// parameters if any, `key` and `value` should already be URL-encoded (if necessary).
19    ///
20    /// Will only error if `key` or `value` contains invalid URI characters.
21    fn append_query_parameter(self, key: &str, value: &str) -> Result<Uri, Error>;
22
23    /// Joins a relative URI or path to this base URI.
24    /// Similar to `url::Url::join` but for `http::Uri`.
25    fn join(&self, relative: &str) -> Result<Uri, Error>;
26}
27
28impl HttpUriExt for Uri {
29    fn extend_dir_with_path(self, path: &str) -> Result<Uri, Error> {
30        if path.is_empty() {
31            return Ok(self);
32        }
33        let mut base_parts = self.into_parts();
34        let (base_path, query) = match &base_parts.path_and_query {
35            Some(path_and_query) => (path_and_query.path(), path_and_query.query()),
36            None => ("/", None),
37        };
38        let new_path_and_query = if base_path.ends_with('/') {
39            if let Some(query) = query {
40                format!("{base_path}{path}?{query}")
41            } else {
42                format!("{base_path}{path}")
43            }
44        } else {
45            if let Some(query) = query {
46                format!("{base_path}/{path}?{query}")
47            } else {
48                format!("{base_path}/{path}")
49            }
50        };
51        base_parts.path_and_query = Some(new_path_and_query.parse()?);
52        Ok(Uri::from_parts(base_parts)?)
53    }
54
55    fn append_query_parameter(self, key: &str, value: &str) -> Result<Uri, Error> {
56        let mut base_parts = self.into_parts();
57        let new_path_and_query = match &base_parts.path_and_query {
58            Some(path_and_query) => {
59                if let Some(query) = path_and_query.query() {
60                    format!("{}?{query}&{key}={value}", path_and_query.path())
61                } else {
62                    format!("{}?{key}={value}", path_and_query.path())
63                }
64            }
65            None => format!("?{key}={value}"),
66        };
67        base_parts.path_and_query = Some(new_path_and_query.parse()?);
68        Ok(Uri::from_parts(base_parts)?)
69    }
70
71    fn join(&self, relative: &str) -> Result<Uri, Error> {
72        if let Ok(rel_uri) = relative.parse::<Uri>() {
73            if rel_uri.scheme().is_some() {
74                return Ok(rel_uri);
75            }
76        }
77
78        if relative.starts_with("//") {
79            let temp_uri = format!("http:{relative}").parse::<Uri>()?;
80            let mut temp_parts = temp_uri.into_parts();
81            temp_parts.scheme = self.scheme().cloned();
82            return Ok(Uri::from_parts(temp_parts)?);
83        }
84
85        let (base_path, query) = match self.path_and_query() {
86            Some(path_and_query) => (path_and_query.path(), path_and_query.query()),
87            None => ("/", None),
88        };
89
90        let new_path = if relative.starts_with('/') {
91            relative.to_string()
92        } else {
93            if let Some((base_dir, _)) = base_path.rsplit_once('/') {
94                normalize_path(&format!("{base_dir}/{relative}"))
95            } else {
96                normalize_path(&format!("/{relative}"))
97            }
98        };
99
100        let new_path_and_query =
101            if let Some(query) = query { format!("{new_path}?{query}") } else { new_path };
102
103        let mut base_parts = self.clone().into_parts();
104        base_parts.path_and_query = Some(new_path_and_query.parse()?);
105        Ok(Uri::from_parts(base_parts)?)
106    }
107}
108
109#[derive(Debug, thiserror::Error)]
110pub enum Error {
111    #[error("invalid uri")]
112    InvalidUri(#[from] uri::InvalidUri),
113
114    #[error("invalid uri parts")]
115    InvalidUriParts(#[from] uri::InvalidUriParts),
116}
117
118fn normalize_path(path: &str) -> String {
119    let mut segments = Vec::new();
120    for segment in path.split('/') {
121        match segment {
122            "" | "." => {}
123            ".." => {
124                segments.pop();
125            }
126            _ => {
127                segments.push(segment);
128            }
129        }
130    }
131    let mut joined = segments.join("/");
132    if path.starts_with('/') {
133        joined.insert(0, '/');
134    }
135    if path.ends_with('/') {
136        joined.push('/');
137    }
138    joined
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use test_case::test_case;
145
146    fn make_uri_from_path_and_query(path_and_query: Option<&str>) -> Uri {
147        let mut parts = uri::Parts::default();
148        parts.path_and_query = path_and_query.map(|p| p.parse().unwrap());
149        Uri::from_parts(parts).unwrap()
150    }
151
152    fn assert_expected_path(base: Option<&str>, added: &str, expected: Option<&str>) {
153        let uri = make_uri_from_path_and_query(base).extend_dir_with_path(added).unwrap();
154        assert_eq!(
155            uri.into_parts().path_and_query.map(|p| p.to_string()),
156            expected.map(|s| s.to_string())
157        );
158    }
159
160    #[test]
161    fn no_query_empty_argument() {
162        assert_expected_path(None, "", None);
163        assert_expected_path(Some("/"), "", Some("/"));
164        assert_expected_path(Some("/a"), "", Some("/a"));
165        assert_expected_path(Some("/a/"), "", Some("/a/"));
166    }
167
168    #[test]
169    fn has_query_empty_argument() {
170        assert_expected_path(Some("?k=v"), "", Some("/?k=v"));
171        assert_expected_path(Some("/?k=v"), "", Some("/?k=v"));
172        assert_expected_path(Some("/a?k=v"), "", Some("/a?k=v"));
173        assert_expected_path(Some("/a/?k=v"), "", Some("/a/?k=v"));
174    }
175
176    #[test]
177    fn no_query_has_argument() {
178        assert_expected_path(None, "c", Some("/c"));
179        assert_expected_path(Some("/"), "c", Some("/c"));
180        assert_expected_path(Some("/a"), "c", Some("/a/c"));
181        assert_expected_path(Some("/a/"), "c", Some("/a/c"));
182    }
183
184    #[test]
185    fn has_query_has_argument() {
186        assert_expected_path(Some("?k=v"), "c", Some("/c?k=v"));
187        assert_expected_path(Some("/?k=v"), "c", Some("/c?k=v"));
188        assert_expected_path(Some("/a?k=v"), "c", Some("/a/c?k=v"));
189        assert_expected_path(Some("/a/?k=v"), "c", Some("/a/c?k=v"));
190    }
191
192    #[test]
193    fn extend_dir_with_authority() {
194        let uri = "http://example.com".parse::<Uri>().unwrap();
195        let extended = uri.extend_dir_with_path("foo").unwrap();
196        assert_eq!(extended.to_string(), "http://example.com/foo");
197
198        let uri = "http://example.com/".parse::<Uri>().unwrap();
199        let extended = uri.extend_dir_with_path("foo").unwrap();
200        assert_eq!(extended.to_string(), "http://example.com/foo");
201
202        let uri = "http://example.com?k=v".parse::<Uri>().unwrap();
203        let extended = uri.extend_dir_with_path("foo").unwrap();
204        assert_eq!(extended.to_string(), "http://example.com/foo?k=v");
205    }
206
207    fn assert_expected_param(base: Option<&str>, key: &str, value: &str, expected: Option<&str>) {
208        let uri = make_uri_from_path_and_query(base).append_query_parameter(key, value).unwrap();
209        assert_eq!(
210            uri.into_parts().path_and_query.map(|p| p.to_string()),
211            expected.map(|s| s.to_string())
212        );
213    }
214
215    #[test]
216    fn new_query() {
217        assert_expected_param(None, "k", "v", Some("/?k=v"));
218        assert_expected_param(Some("/"), "k", "v", Some("/?k=v"));
219        assert_expected_param(Some("/a"), "k", "v", Some("/a?k=v"));
220        assert_expected_param(Some("/a/"), "k", "v", Some("/a/?k=v"));
221    }
222
223    #[test]
224    fn append_query() {
225        assert_expected_param(Some("?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
226        assert_expected_param(Some("/?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
227        assert_expected_param(Some("/a?k=v"), "k2", "v2", Some("/a?k=v&k2=v2"));
228        assert_expected_param(Some("/a/?k=v"), "k2", "v2", Some("/a/?k=v&k2=v2"));
229    }
230
231    #[test_case("https://[fe80::1%25eth0]:8080/update/manifest", "http://example.com/foo", "http://example.com/foo"; "absolute")]
232    #[test_case("https://[fe80::1%25eth0]:8080/update/manifest", "blobs", "https://[fe80::1%25eth0]:8080/update/blobs"; "relative_path")]
233    #[test_case("https://[fe80::1%25eth0]:8080/update/manifest", "./blobs", "https://[fe80::1%25eth0]:8080/update/blobs"; "relative_starts_with_dot_slash")]
234    #[test_case("https://[fe80::1%25eth0]:8080/update/manifest", "../blobs", "https://[fe80::1%25eth0]:8080/blobs"; "relative_parent")]
235    #[test_case("https://[fe80::1%25eth0]:8080/update/manifest", "/blobs", "https://[fe80::1%25eth0]:8080/blobs"; "absolute_path")]
236    #[test_case("https://[fe80::1%25eth0]:8080/update/manifest", "//fuchsia.com/blobs/1", "https://fuchsia.com/blobs/1"; "network_path")]
237    #[test_case("https://[fe80::1%25eth0]:8080/update/", "blobs", "https://[fe80::1%25eth0]:8080/update/blobs"; "base_ends_in_slash")]
238    #[test_case("https://[fe80::1%25eth0]:8080/update/", "./blobs", "https://[fe80::1%25eth0]:8080/update/blobs"; "base_ends_in_slash_relative_starts_with_dot_slash")]
239    #[test_case("https://[fe80::1%25eth0]:8080/update/", "../blobs", "https://[fe80::1%25eth0]:8080/blobs"; "base_ends_in_slash_relative_parent")]
240    fn test_join(base: &str, relative: &str, expected: &str) {
241        let base = base.parse::<Uri>().unwrap();
242        let joined = base.join(relative).unwrap();
243        assert_eq!(joined.to_string(), expected);
244    }
245}