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