Skip to main content

omaha_client/
http_uri_ext.rs

1// Copyright 2020 The Fuchsia Authors
2//
3// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6// This file may not be copied, modified, or distributed except according to
7// those terms.
8
9use http::uri::{self, Uri};
10
11pub trait HttpUriExt {
12    /// Normalizes empty paths to `/`, appends `/` to `self`'s path if it does not end with one,
13    /// then appends `path`, preserving any query parameters. Does nothing if `path` is the empty
14    /// string.
15    ///
16    /// Will only error if asked to add a path to a `Uri` without a scheme (because `Uri` requires
17    /// a scheme if a path is present), or if `path` contains invalid URI characters.
18    fn extend_dir_with_path(self, path: &str) -> Result<Uri, Error>;
19
20    /// Append the given query parameter `key`=`value` to the URI, preserving existing query
21    /// parameters if any, `key` and `value` should already be URL-encoded (if necessary).
22    ///
23    /// Will only error if `key` or `value` contains invalid URI characters.
24    fn append_query_parameter(self, key: &str, value: &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) if !path_and_query.path().is_empty() => {
35                (path_and_query.path(), path_and_query.query())
36            }
37            Some(path_and_query) => ("/", path_and_query.query()),
38            None => ("/", None),
39        };
40        let new_path_and_query = if base_path.ends_with('/') {
41            if let Some(query) = query {
42                format!("{base_path}{path}?{query}")
43            } else {
44                format!("{base_path}{path}")
45            }
46        } else if let Some(query) = query {
47            format!("{base_path}/{path}?{query}")
48        } else {
49            format!("{base_path}/{path}")
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                let path =
60                    if path_and_query.path().is_empty() { "/" } else { path_and_query.path() };
61                if let Some(query) = path_and_query.query() {
62                    format!("{path}?{query}&{key}={value}")
63                } else {
64                    format!("{path}?{key}={value}")
65                }
66            }
67            None => format!("/?{key}={value}"),
68        };
69        base_parts.path_and_query = Some(new_path_and_query.parse()?);
70        Ok(Uri::from_parts(base_parts)?)
71    }
72}
73
74#[derive(Debug, thiserror::Error)]
75pub enum Error {
76    #[error("invalid uri: {0}")]
77    InvalidUri(#[from] uri::InvalidUri),
78    #[error("invalid uri parts: {0}")]
79    InvalidUriParts(#[from] uri::InvalidUriParts),
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    fn make_uri_from_path_and_query(path_and_query: Option<&str>) -> Uri {
87        let mut parts = uri::Parts::default();
88        let pq = match path_and_query {
89            None | Some("") => None,
90            Some(p) if p.starts_with('?') => Some(format!("/{p}").parse().unwrap()),
91            Some(p) => Some(p.parse().unwrap()),
92        };
93        parts.path_and_query = pq;
94        Uri::from_parts(parts).unwrap()
95    }
96
97    fn assert_expected_path(base: Option<&str>, added: &str, expected: Option<&str>) {
98        let uri = make_uri_from_path_and_query(base).extend_dir_with_path(added).unwrap();
99        assert_eq!(
100            uri.into_parts().path_and_query.map(|p| p.to_string()),
101            expected.map(|s| s.to_string())
102        );
103    }
104
105    #[test]
106    fn no_query_empty_argument() {
107        assert_expected_path(None, "", None);
108        assert_expected_path(Some(""), "", None);
109        assert_expected_path(Some("/"), "", Some("/"));
110        assert_expected_path(Some("/a"), "", Some("/a"));
111        assert_expected_path(Some("/a/"), "", Some("/a/"));
112    }
113
114    #[test]
115    fn has_query_empty_argument() {
116        assert_expected_path(Some("?k=v"), "", Some("/?k=v"));
117        assert_expected_path(Some("/?k=v"), "", Some("/?k=v"));
118        assert_expected_path(Some("/a?k=v"), "", Some("/a?k=v"));
119        assert_expected_path(Some("/a/?k=v"), "", Some("/a/?k=v"));
120    }
121
122    #[test]
123    fn no_query_has_argument() {
124        assert_expected_path(None, "c", Some("/c"));
125        assert_expected_path(Some(""), "c", Some("/c"));
126        assert_expected_path(Some("/"), "c", Some("/c"));
127        assert_expected_path(Some("/a"), "c", Some("/a/c"));
128        assert_expected_path(Some("/a/"), "c", Some("/a/c"));
129    }
130
131    #[test]
132    fn has_query_has_argument() {
133        assert_expected_path(Some("?k=v"), "c", Some("/c?k=v"));
134        assert_expected_path(Some("/?k=v"), "c", Some("/c?k=v"));
135        assert_expected_path(Some("/a?k=v"), "c", Some("/a/c?k=v"));
136        assert_expected_path(Some("/a/?k=v"), "c", Some("/a/c?k=v"));
137    }
138
139    fn assert_expected_param(base: Option<&str>, key: &str, value: &str, expected: Option<&str>) {
140        let uri = make_uri_from_path_and_query(base).append_query_parameter(key, value).unwrap();
141        assert_eq!(
142            uri.into_parts().path_and_query.map(|p| p.to_string()),
143            expected.map(|s| s.to_string())
144        );
145    }
146
147    #[test]
148    fn new_query() {
149        assert_expected_param(None, "k", "v", Some("/?k=v"));
150        assert_expected_param(Some(""), "k", "v", Some("/?k=v"));
151        assert_expected_param(Some("/"), "k", "v", Some("/?k=v"));
152        assert_expected_param(Some("/a"), "k", "v", Some("/a?k=v"));
153        assert_expected_param(Some("/a/"), "k", "v", Some("/a/?k=v"));
154    }
155
156    #[test]
157    fn append_query() {
158        assert_expected_param(Some("?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
159        assert_expected_param(Some("/?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
160        assert_expected_param(Some("/a?k=v"), "k2", "v2", Some("/a?k=v&k2=v2"));
161        assert_expected_param(Some("/a/?k=v"), "k2", "v2", Some("/a/?k=v&k2=v2"));
162    }
163}