s3-wire 0.3.1

An async, streaming S3-compatible client for Rust
Documentation
use http::{HeaderMap, HeaderName, HeaderValue};

use crate::error::S3Error;

const PROTECTED_HEADERS: [&str; 7] = [
    "host",
    "authorization",
    "x-amz-date",
    "x-amz-security-token",
    "x-amz-content-sha256",
    "content-length",
    "transfer-encoding",
];

pub(in crate::client) fn request_headers(headers: HeaderMap) -> Result<HeaderMap, S3Error> {
    for name in PROTECTED_HEADERS {
        if headers.contains_key(name) {
            return Err(header_collision(name));
        }
    }
    Ok(headers)
}

pub(in crate::client) fn insert_header(
    headers: &mut HeaderMap,
    name: HeaderName,
    value: &str,
) -> Result<(), S3Error> {
    if headers.contains_key(&name) {
        return Err(header_collision(name.as_str()));
    }
    let value = HeaderValue::from_str(value)
        .map_err(|_| S3Error::configuration("request contains an invalid header value"))?;
    headers.insert(name, value);
    Ok(())
}

pub(in crate::client) fn insert_optional_header(
    headers: &mut HeaderMap,
    name: HeaderName,
    value: Option<&str>,
) -> Result<(), S3Error> {
    if let Some(value) = value {
        insert_header(headers, name, value)?;
    }
    Ok(())
}

pub(in crate::client) fn insert_named_header(
    headers: &mut HeaderMap,
    name: &str,
    value: &str,
) -> Result<(), S3Error> {
    let name = HeaderName::from_bytes(name.as_bytes())
        .map_err(|_| S3Error::configuration("request contains an invalid header name"))?;
    insert_header(headers, name, value)
}

pub(in crate::client) fn insert_optional_named_header(
    headers: &mut HeaderMap,
    name: &str,
    value: Option<&str>,
) -> Result<(), S3Error> {
    if let Some(value) = value {
        insert_named_header(headers, name, value)?;
    }
    Ok(())
}

fn header_collision(name: &str) -> S3Error {
    S3Error::configuration(format!(
        "request header `{name}` conflicts with a generated, signing-owned, or transport-owned header"
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn protected_names_are_case_insensitive_and_values_are_not_reported() {
        let mut headers = HeaderMap::new();
        headers.insert("X-Amz-Date", HeaderValue::from_static("sentinel-secret"));
        let error = request_headers(headers).unwrap_err();
        assert!(error.message().contains("x-amz-date"));
        assert!(!error.message().contains("sentinel-secret"));
    }

    #[test]
    fn custom_duplicates_are_preserved() {
        let mut headers = HeaderMap::new();
        headers.append("x-example", HeaderValue::from_static("one"));
        headers.append("x-example", HeaderValue::from_static("two"));
        let headers = request_headers(headers).unwrap();
        assert_eq!(headers.get_all("x-example").iter().count(), 2);
    }

    #[test]
    fn generated_headers_reject_custom_collisions() {
        let mut headers = HeaderMap::new();
        headers.insert("content-type", HeaderValue::from_static("sentinel-secret"));
        let error = insert_header(
            &mut headers,
            HeaderName::from_static("content-type"),
            "text/plain",
        )
        .unwrap_err();
        assert!(error.message().contains("content-type"));
        assert!(!error.message().contains("sentinel-secret"));
    }

    #[test]
    fn unset_optional_headers_do_not_reserve_their_names() {
        let mut headers = HeaderMap::new();
        headers.insert("if-match", HeaderValue::from_static("custom"));
        insert_optional_header(&mut headers, HeaderName::from_static("if-match"), None).unwrap();
        assert_eq!(headers["if-match"], "custom");
    }
}