s3-wire 0.3.1

An async, streaming S3-compatible client for Rust
Documentation
use std::collections::BTreeMap;
use std::time::SystemTime;

use http::{HeaderMap, HeaderName};

use crate::client::request::{insert_header, insert_named_header, insert_optional_named_header};
use crate::error::S3Error;
use crate::operation::{ChecksumAlgorithm, Conditions};
use crate::stream::PreparedBody;

pub(in crate::client::object) fn insert_upload_checksum(
    headers: &mut HeaderMap,
    algorithm: Option<ChecksumAlgorithm>,
    body: &PreparedBody,
) -> Result<(), S3Error> {
    let Some(algorithm) = algorithm else {
        return Ok(());
    };
    match algorithm {
        ChecksumAlgorithm::Sha256 => {
            insert_header(
                headers,
                HeaderName::from_static("x-amz-sdk-checksum-algorithm"),
                "SHA256",
            )?;
            insert_header(
                headers,
                HeaderName::from_static("x-amz-checksum-sha256"),
                &body.sha256_base64(),
            )
        }
        ChecksumAlgorithm::Crc32
        | ChecksumAlgorithm::Crc32c
        | ChecksumAlgorithm::Crc64Nvme
        | ChecksumAlgorithm::Sha1 => Err(S3Error::unsupported(
            "the selected upload checksum algorithm is not implemented",
        )),
    }
}

pub(in crate::client) fn insert_conditions(
    headers: &mut HeaderMap,
    conditions: &Conditions,
    prefix: &str,
) -> Result<(), S3Error> {
    insert_optional_named_header(
        headers,
        &format!("{prefix}if-match"),
        conditions.if_match.as_deref(),
    )?;
    insert_optional_named_header(
        headers,
        &format!("{prefix}if-none-match"),
        conditions.if_none_match.as_deref(),
    )?;
    if let Some(value) = conditions.if_modified_since {
        insert_named_header(
            headers,
            &format!("{prefix}if-modified-since"),
            &format_http_date(value),
        )?;
    }
    if let Some(value) = conditions.if_unmodified_since {
        insert_named_header(
            headers,
            &format!("{prefix}if-unmodified-since"),
            &format_http_date(value),
        )?;
    }
    Ok(())
}

fn format_http_date(value: time::OffsetDateTime) -> String {
    let system_time = SystemTime::from(value);
    httpdate::fmt_http_date(system_time)
}

pub(in crate::client::object) fn insert_user_metadata(
    headers: &mut HeaderMap,
    metadata: &BTreeMap<String, String>,
) -> Result<(), S3Error> {
    for (name, value) in metadata {
        insert_named_header(headers, &format!("x-amz-meta-{name}"), value)?;
    }
    Ok(())
}

pub(in crate::client::object) fn optional_query(
    name: &str,
    value: Option<&str>,
) -> Vec<(String, String)> {
    value.map_or_else(Vec::new, |value| vec![(name.to_owned(), value.to_owned())])
}

pub(in crate::client::object) fn push_optional_query(
    query: &mut Vec<(String, String)>,
    name: &str,
    value: Option<&str>,
) {
    if let Some(value) = value {
        query.push((name.to_owned(), value.to_owned()));
    }
}

pub(in crate::client) fn copy_source_header(source: &crate::operation::CopySource) -> String {
    let mut output = String::from("/");
    percent_encode(source.bucket.as_bytes(), false, &mut output);
    output.push('/');
    percent_encode(source.key.as_str().as_bytes(), true, &mut output);
    if let Some(version_id) = &source.version_id {
        output.push_str("?versionId=");
        percent_encode(version_id.as_bytes(), false, &mut output);
    }
    output
}

fn percent_encode(bytes: &[u8], preserve_slash: bool, output: &mut String) {
    const HEX: &[u8; 16] = b"0123456789ABCDEF";
    for &byte in bytes {
        if byte.is_ascii_alphanumeric()
            || matches!(byte, b'-' | b'.' | b'_' | b'~')
            || (preserve_slash && byte == b'/')
        {
            output.push(char::from(byte));
        } else {
            output.push('%');
            output.push(char::from(HEX[usize::from(byte >> 4)]));
            output.push(char::from(HEX[usize::from(byte & 0x0f)]));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::operation::{CopySource, ObjectKey};
    use time::macros::datetime;

    #[test]
    fn copy_source_encodes_path_and_version_exactly_once() {
        let source = CopySource {
            bucket: "source.bucket".to_owned(),
            key: ObjectKey::new("folder/a b%+c").unwrap(),
            version_id: Some("v +/=".to_owned()),
        };
        assert_eq!(
            copy_source_header(&source),
            "/source.bucket/folder/a%20b%25%2Bc?versionId=v%20%2B%2F%3D"
        );
    }

    #[test]
    fn dynamic_metadata_headers_reject_custom_collisions() {
        let mut headers = HeaderMap::new();
        headers.insert("x-amz-meta-owner", "custom".parse().unwrap());
        let metadata = [("owner".to_owned(), "should-not-appear".to_owned())]
            .into_iter()
            .collect();
        let error = insert_user_metadata(&mut headers, &metadata).unwrap_err();
        assert!(error.message().contains("x-amz-meta-owner"));
        assert!(!error.message().contains("custom"));
        assert!(!error.message().contains("should-not-appear"));
    }

    #[test]
    fn conditional_dates_use_http_wire_format() {
        assert_eq!(
            format_http_date(datetime!(2024-03-12 10:15:30 UTC)),
            "Tue, 12 Mar 2024 10:15:30 GMT"
        );
    }
}