s3 0.1.34

A lean, modern, unofficial S3-compatible client for Rust.
Documentation
use crate::{Error, Result};

pub(crate) fn decode_utf8_response_body(bytes: &[u8]) -> Result<String> {
    std::str::from_utf8(bytes)
        .map(str::to_owned)
        .map_err(|e| Error::decode("response body is not valid UTF-8", Some(Box::new(e))))
}

#[cfg(any(test, feature = "credentials-imds"))]
pub(crate) fn decode_single_line_response_body(name: &'static str, bytes: &[u8]) -> Result<String> {
    let text = decode_utf8_response_body(bytes)?;
    let text = strip_trailing_line_ending(&text);
    if text.is_empty() {
        return Err(Error::decode(format!("{name} response is empty"), None));
    }
    if text.bytes().any(|byte| byte.is_ascii_control()) {
        return Err(Error::decode(
            format!("{name} response must be a single line"),
            None,
        ));
    }
    Ok(text.to_string())
}

#[cfg(any(test, feature = "credentials-imds", feature = "credentials-sts"))]
pub(crate) fn strip_trailing_line_ending(value: &str) -> &str {
    if let Some(value) = value.strip_suffix("\r\n") {
        value
    } else if let Some(value) = value.strip_suffix('\n') {
        value
    } else if let Some(value) = value.strip_suffix('\r') {
        value
    } else {
        value
    }
}

pub(crate) fn truncate_snippet(body: &str, max_len: usize) -> String {
    if body.len() <= max_len {
        return body.to_string();
    }

    let cut = if body.is_char_boundary(max_len) {
        max_len
    } else {
        body.char_indices()
            .take_while(|(idx, _)| *idx < max_len)
            .last()
            .map(|(idx, _)| idx)
            .unwrap_or(0)
    };

    let mut out = body[..cut].to_string();
    out.push_str("...");
    out
}

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

    #[test]
    fn decodes_valid_utf8_response_body() {
        assert_eq!(
            decode_utf8_response_body("hello".as_bytes()).unwrap(),
            "hello"
        );
    }

    #[test]
    fn rejects_invalid_utf8_response_body() {
        let err = decode_utf8_response_body(&[0xff]).expect_err("invalid UTF-8 must fail");
        match err {
            Error::Decode { message, .. } => assert!(message.contains("UTF-8")),
            other => panic!("expected Decode error, got {other:?}"),
        }
    }

    #[test]
    fn decodes_single_line_response_body_with_one_optional_line_ending() {
        assert_eq!(
            decode_single_line_response_body("token", b"abc\r\n").unwrap(),
            "abc"
        );
        assert_eq!(
            decode_single_line_response_body("token", b"abc\n").unwrap(),
            "abc"
        );
        assert!(decode_single_line_response_body("token", b"abc\n\n").is_err());
        assert!(decode_single_line_response_body("token", b"abc\rdef").is_err());
        assert!(decode_single_line_response_body("token", b"").is_err());
    }

    #[test]
    fn truncates_ascii_without_panic() {
        let body = "a".repeat(10);
        assert_eq!(truncate_snippet(&body, 10), body);
        assert_eq!(truncate_snippet(&body, 5), "aaaaa...");
    }

    #[test]
    fn truncates_utf8_safely() {
        let body = "你好,世界".repeat(10);
        let out = truncate_snippet(&body, 5);
        assert!(out.ends_with("..."));
        assert!(out.len() > 3);
        assert!(out.len() <= 5 + 3);
    }
}