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) => (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 if let Some(query) = query {
44            format!("{}/{}?{}", base_path, path, query)
45        } else {
46            format!("{}/{}", base_path, path)
47        };
48        base_parts.path_and_query = Some(new_path_and_query.parse()?);
49        Ok(Uri::from_parts(base_parts)?)
50    }
51
52    fn append_query_parameter(self, key: &str, value: &str) -> Result<Uri, Error> {
53        let mut base_parts = self.into_parts();
54        let new_path_and_query = match &base_parts.path_and_query {
55            Some(path_and_query) => {
56                if let Some(query) = path_and_query.query() {
57                    format!("{}?{query}&{key}={value}", path_and_query.path())
58                } else {
59                    format!("{}?{key}={value}", path_and_query.path())
60                }
61            }
62            None => format!("?{key}={value}"),
63        };
64        base_parts.path_and_query = Some(new_path_and_query.parse()?);
65        Ok(Uri::from_parts(base_parts)?)
66    }
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum Error {
71    #[error("invalid uri: {0}")]
72    InvalidUri(#[from] uri::InvalidUri),
73    #[error("invalid uri parts: {0}")]
74    InvalidUriParts(#[from] uri::InvalidUriParts),
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn make_uri_from_path_and_query(path_and_query: Option<&str>) -> Uri {
82        let mut parts = uri::Parts::default();
83        parts.path_and_query = path_and_query.map(|p| p.parse().unwrap());
84        Uri::from_parts(parts).unwrap()
85    }
86
87    fn assert_expected_path(base: Option<&str>, added: &str, expected: Option<&str>) {
88        let uri = make_uri_from_path_and_query(base)
89            .extend_dir_with_path(added)
90            .unwrap();
91        assert_eq!(
92            uri.into_parts().path_and_query.map(|p| p.to_string()),
93            expected.map(|s| s.to_string())
94        );
95    }
96
97    #[test]
98    fn no_query_empty_argument() {
99        assert_expected_path(None, "", None);
100        assert_expected_path(Some(""), "", None);
101        assert_expected_path(Some("/"), "", Some("/"));
102        assert_expected_path(Some("/a"), "", Some("/a"));
103        assert_expected_path(Some("/a/"), "", Some("/a/"));
104    }
105
106    #[test]
107    fn has_query_empty_argument() {
108        assert_expected_path(Some("?k=v"), "", Some("/?k=v"));
109        assert_expected_path(Some("/?k=v"), "", Some("/?k=v"));
110        assert_expected_path(Some("/a?k=v"), "", Some("/a?k=v"));
111        assert_expected_path(Some("/a/?k=v"), "", Some("/a/?k=v"));
112    }
113
114    #[test]
115    fn no_query_has_argument() {
116        assert_expected_path(None, "c", Some("/c"));
117        assert_expected_path(Some(""), "c", Some("/c"));
118        assert_expected_path(Some("/"), "c", Some("/c"));
119        assert_expected_path(Some("/a"), "c", Some("/a/c"));
120        assert_expected_path(Some("/a/"), "c", Some("/a/c"));
121    }
122
123    #[test]
124    fn has_query_has_argument() {
125        assert_expected_path(Some("?k=v"), "c", Some("/c?k=v"));
126        assert_expected_path(Some("/?k=v"), "c", Some("/c?k=v"));
127        assert_expected_path(Some("/a?k=v"), "c", Some("/a/c?k=v"));
128        assert_expected_path(Some("/a/?k=v"), "c", Some("/a/c?k=v"));
129    }
130
131    fn assert_expected_param(base: Option<&str>, key: &str, value: &str, expected: Option<&str>) {
132        let uri = make_uri_from_path_and_query(base)
133            .append_query_parameter(key, value)
134            .unwrap();
135        assert_eq!(
136            uri.into_parts().path_and_query.map(|p| p.to_string()),
137            expected.map(|s| s.to_string())
138        );
139    }
140
141    #[test]
142    fn new_query() {
143        assert_expected_param(None, "k", "v", Some("/?k=v"));
144        assert_expected_param(Some(""), "k", "v", Some("/?k=v"));
145        assert_expected_param(Some("/"), "k", "v", Some("/?k=v"));
146        assert_expected_param(Some("/a"), "k", "v", Some("/a?k=v"));
147        assert_expected_param(Some("/a/"), "k", "v", Some("/a/?k=v"));
148    }
149
150    #[test]
151    fn append_query() {
152        assert_expected_param(Some("?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
153        assert_expected_param(Some("/?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
154        assert_expected_param(Some("/a?k=v"), "k2", "v2", Some("/a?k=v&k2=v2"));
155        assert_expected_param(Some("/a/?k=v"), "k2", "v2", Some("/a/?k=v&k2=v2"));
156    }
157}