Skip to main content

winterbaume_core/protocol/
common.rs

1//! Common protocol utilities shared across all AWS protocol families.
2
3use std::collections::HashMap;
4
5/// Extract the path component from an AWS service URI.
6///
7/// Given a URI like `https://kms.us-east-1.amazonaws.com/some/path?query=1`,
8/// returns `/some/path`. Strips the query string.
9pub fn extract_path(uri: &str) -> String {
10    // Strip scheme (http:// or https://)
11    let without_scheme = if let Some(rest) = uri.strip_prefix("https://") {
12        rest
13    } else if let Some(rest) = uri.strip_prefix("http://") {
14        rest
15    } else {
16        uri
17    };
18    // Find the start of the path (first '/' after scheme was stripped)
19    if let Some(slash) = without_scheme.find('/') {
20        let path_and_query = &without_scheme[slash..];
21        // Strip query string
22        if let Some(q) = path_and_query.find('?') {
23            path_and_query[..q].to_string()
24        } else {
25            path_and_query.to_string()
26        }
27    } else {
28        // No path component — return "/"
29        "/".to_string()
30    }
31}
32
33/// Extract the query string from a URI, returning everything after '?'.
34pub fn extract_query_string(uri: &str) -> &str {
35    uri.split_once('?').map(|(_, q)| q).unwrap_or("")
36}
37
38/// Percent-decode a URI component. Also decodes '+' as space.
39pub fn percent_decode(s: &str) -> String {
40    let mut result = String::with_capacity(s.len());
41    let mut bytes = s.bytes();
42    while let Some(b) = bytes.next() {
43        match b {
44            b'%' => {
45                let hi = bytes.next().and_then(hex_val);
46                let lo = bytes.next().and_then(hex_val);
47                if let (Some(hi), Some(lo)) = (hi, lo) {
48                    result.push((hi << 4 | lo) as char);
49                }
50            }
51            b'+' => result.push(' '),
52            _ => result.push(b as char),
53        }
54    }
55    result
56}
57
58/// Decode a single hex digit.
59pub fn hex_val(b: u8) -> Option<u8> {
60    match b {
61        b'0'..=b'9' => Some(b - b'0'),
62        b'a'..=b'f' => Some(b - b'a' + 10),
63        b'A'..=b'F' => Some(b - b'A' + 10),
64        _ => None,
65    }
66}
67
68/// URL-decode a string. Alias for [`percent_decode`].
69pub fn urldecode(s: &str) -> String {
70    percent_decode(s)
71}
72
73/// Parse a query string (or form-encoded body) into key-value pairs.
74///
75/// Handles URL-decoding of both keys and values.
76pub fn parse_query_string(s: &str) -> HashMap<String, String> {
77    let mut map = HashMap::new();
78    for pair in s.split('&') {
79        if let Some((key, value)) = pair.split_once('=') {
80            let key = urldecode(key);
81            let value = urldecode(value);
82            map.insert(key, value);
83        }
84    }
85    map
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_extract_path() {
94        assert_eq!(
95            extract_path("https://kms.us-east-1.amazonaws.com/some/path"),
96            "/some/path"
97        );
98        assert_eq!(
99            extract_path("https://kms.us-east-1.amazonaws.com/some/path?query=1"),
100            "/some/path"
101        );
102        assert_eq!(extract_path("https://kms.us-east-1.amazonaws.com"), "/");
103        assert_eq!(extract_path("/local/path?q=1"), "/local/path");
104    }
105
106    #[test]
107    fn test_percent_decode() {
108        assert_eq!(percent_decode("hello%20world"), "hello world");
109        assert_eq!(percent_decode("hello+world"), "hello world");
110        assert_eq!(percent_decode("a%2Fb"), "a/b");
111        assert_eq!(percent_decode("plain"), "plain");
112    }
113
114    #[test]
115    fn test_parse_query_string() {
116        let qs = "Action=CreateUser&UserName=test%20user&Version=2010-05-08";
117        let params = parse_query_string(qs);
118        assert_eq!(params.get("Action").unwrap(), "CreateUser");
119        assert_eq!(params.get("UserName").unwrap(), "test user");
120        assert_eq!(params.get("Version").unwrap(), "2010-05-08");
121    }
122}