use crate::s3::types::VersionId;
use crate::s3::utils::url_encode;
use std::borrow::Cow;
use std::collections::BTreeMap;
pub type Multimap = multimap::MultiMap<String, String>;
#[inline]
fn collapse_spaces(s: &str) -> Cow<'_, str> {
let trimmed = s.trim();
if !trimmed.contains(" ") {
return Cow::Borrowed(trimmed);
}
let mut result = String::with_capacity(trimmed.len());
let mut prev_space = false;
for c in trimmed.chars() {
if c == ' ' {
if !prev_space {
result.push(' ');
prev_space = true;
}
} else {
result.push(c);
prev_space = false;
}
}
Cow::Owned(result)
}
pub trait MultimapExt {
fn add<K: Into<String>, V: Into<String>>(&mut self, key: K, value: V);
fn add_multimap(&mut self, other: Multimap);
fn add_version(&mut self, version: Option<VersionId>);
#[must_use]
fn take_version(self) -> Option<String>;
fn to_query_string(&self) -> String;
fn get_canonical_query_string(&self) -> String;
fn get_canonical_headers(&self) -> (String, String);
}
impl MultimapExt for Multimap {
fn add<K: Into<String>, V: Into<String>>(&mut self, key: K, value: V) {
self.insert(key.into(), value.into());
}
fn add_multimap(&mut self, other: Multimap) {
for (key, values) in other.into_iter() {
self.insert_many(key, values);
}
}
fn add_version(&mut self, version: Option<VersionId>) {
if let Some(v) = version {
self.insert("versionId".into(), v.into_inner());
}
}
fn take_version(mut self) -> Option<String> {
self.remove("versionId").and_then(|mut v| v.pop())
}
fn to_query_string(&self) -> String {
let mut query = String::new();
for (key, values) in self.iter_all() {
for value in values {
if !query.is_empty() {
query.push('&');
}
query.push_str(&url_encode(key));
query.push('=');
query.push_str(&url_encode(value));
}
}
query
}
fn get_canonical_query_string(&self) -> String {
let mut sorted: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut total_len = 0usize;
for (key, values) in self.iter_all() {
for value in values {
total_len += key.len() + 1 + value.len() + 2; }
sorted
.entry(key.as_str())
.or_default()
.extend(values.iter().map(|s| s.as_str()));
}
let mut query = String::with_capacity(total_len + total_len / 5);
for (key, values) in sorted {
for value in values {
if !query.is_empty() {
query.push('&');
}
query.push_str(&url_encode(key));
query.push('=');
query.push_str(&url_encode(value));
}
}
query
}
fn get_canonical_headers(&self) -> (String, String) {
let mut btmap: BTreeMap<String, String> = BTreeMap::new();
let mut key_bytes = 0usize;
let mut value_bytes = 0usize;
for (k, values) in self.iter_all() {
let key = k.to_lowercase();
if key == "authorization" || key == "user-agent" {
continue;
}
let mut vs: Vec<&String> = values.iter().collect();
vs.sort();
let mut value =
String::with_capacity(vs.iter().map(|v| v.len()).sum::<usize>() + vs.len());
for v in vs {
if !value.is_empty() {
value.push(',');
}
value.push_str(&collapse_spaces(v));
}
key_bytes += key.len();
value_bytes += value.len();
btmap.insert(key, value);
}
let header_count = btmap.len();
let mut signed_headers = String::with_capacity(key_bytes + header_count);
let mut canonical_headers =
String::with_capacity(key_bytes + value_bytes + header_count * 2);
let mut add_delim = false;
for (key, value) in &btmap {
if add_delim {
signed_headers.push(';');
canonical_headers.push('\n');
}
signed_headers.push_str(key);
canonical_headers.push_str(key);
canonical_headers.push(':');
canonical_headers.push_str(value);
add_delim = true;
}
(signed_headers, canonical_headers)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collapse_spaces_no_consecutive_spaces() {
let result = collapse_spaces("hello world");
assert_eq!(result, "hello world");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_collapse_spaces_with_consecutive_spaces() {
let result = collapse_spaces("hello world");
assert_eq!(result, "hello world");
assert!(matches!(result, Cow::Owned(_)));
let result = collapse_spaces("hello world");
assert_eq!(result, "hello world");
let result = collapse_spaces("a b c d");
assert_eq!(result, "a b c d");
}
#[test]
fn test_collapse_spaces_multiple_groups() {
let result = collapse_spaces("hello world foo bar");
assert_eq!(result, "hello world foo bar");
}
#[test]
fn test_collapse_spaces_leading_trailing() {
let result = collapse_spaces(" hello world ");
assert_eq!(result, "hello world");
assert!(matches!(result, Cow::Borrowed(_)));
let result = collapse_spaces(" hello world ");
assert_eq!(result, "hello world");
assert!(matches!(result, Cow::Owned(_)));
}
#[test]
fn test_collapse_spaces_only_spaces() {
let result = collapse_spaces(" ");
assert_eq!(result, "");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_collapse_spaces_empty_string() {
let result = collapse_spaces("");
assert_eq!(result, "");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_collapse_spaces_single_space() {
let result = collapse_spaces(" ");
assert_eq!(result, "");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_collapse_spaces_no_spaces() {
let result = collapse_spaces("helloworld");
assert_eq!(result, "helloworld");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_collapse_spaces_tabs_not_collapsed() {
let result = collapse_spaces("hello\t\tworld");
assert_eq!(result, "hello\t\tworld");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_collapse_spaces_mixed_whitespace() {
let result = collapse_spaces("hello \t world");
assert_eq!(result, "hello \t world");
}
#[test]
fn test_collapse_spaces_realistic_header_value() {
let result = collapse_spaces("application/json");
assert_eq!(result, "application/json");
assert!(matches!(result, Cow::Borrowed(_)));
let result = collapse_spaces("bytes=0-1023");
assert_eq!(result, "bytes=0-1023");
assert!(matches!(result, Cow::Borrowed(_)));
}
}