1use http::uri::{self, Uri};
6
7pub trait HttpUriExt {
8 fn extend_dir_with_path(self, path: &str) -> Result<Uri, Error>;
16
17 fn append_query_parameter(self, key: &str, value: &str) -> Result<Uri, Error>;
22
23 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}