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