#![deny(missing_docs)]
use crate::validation::{AwsNameValidation, NameValidation};
use std::net::IpAddr;
#[derive(Debug, PartialEq, Eq)]
pub enum S3Path {
Root,
Bucket {
bucket: Box<str>,
},
Object {
bucket: Box<str>,
key: Box<str>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseS3PathError {
#[error("The request is not a valid path-style request")]
InvalidPath,
#[error("The bucket name is invalid")]
InvalidBucketName,
#[error("The object key is too long")]
KeyTooLong,
}
impl S3Path {
#[must_use]
pub fn root() -> Self {
Self::Root
}
#[must_use]
pub fn bucket(bucket: &str) -> Self {
Self::Bucket { bucket: bucket.into() }
}
#[must_use]
pub fn object(bucket: &str, key: &str) -> Self {
Self::Object {
bucket: bucket.into(),
key: key.into(),
}
}
#[must_use]
pub fn is_root(&self) -> bool {
matches!(self, Self::Root)
}
#[must_use]
pub fn as_bucket(&self) -> Option<&str> {
match self {
Self::Bucket { bucket } => Some(bucket),
_ => None,
}
}
#[must_use]
pub fn as_object(&self) -> Option<(&str, &str)> {
match self {
Self::Object { bucket, key } => Some((bucket, key)),
_ => None,
}
}
#[must_use]
pub fn get_bucket_name(&self) -> Option<&str> {
match self {
Self::Root => None,
Self::Bucket { bucket } | Self::Object { bucket, .. } => Some(bucket),
}
}
#[must_use]
pub fn get_object_key(&self) -> Option<&str> {
match self {
Self::Root | Self::Bucket { .. } => None,
Self::Object { key, .. } => Some(key),
}
}
}
#[allow(clippy::manual_is_variant_and)] #[must_use]
pub fn check_bucket_name(name: &str) -> bool {
if !(3_usize..64).contains(&name.len()) {
return false;
}
if !name
.as_bytes()
.iter()
.all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'-')
{
return false;
}
if name.as_bytes().first().map(|&b| b.is_ascii_lowercase() || b.is_ascii_digit()) != Some(true) {
return false;
}
if name.as_bytes().last().map(|&b| b.is_ascii_lowercase() || b.is_ascii_digit()) != Some(true) {
return false;
}
if name.contains("..") {
return false;
}
if name.parse::<IpAddr>().is_ok() {
return false;
}
if name.starts_with("xn--") {
return false;
}
true
}
#[must_use]
pub const fn check_key(key: &str) -> bool {
key.len() <= 1024
}
pub fn parse_path_style(uri_path: &str) -> Result<S3Path, ParseS3PathError> {
parse_path_style_with_validation(uri_path, &AwsNameValidation::new())
}
pub fn parse_path_style_with_validation(uri_path: &str, validation: &dyn NameValidation) -> Result<S3Path, ParseS3PathError> {
let Some(path) = uri_path.strip_prefix('/') else { return Err(ParseS3PathError::InvalidPath) };
if path.is_empty() {
return Ok(S3Path::root());
}
let (bucket, key) = match path.split_once('/') {
None => (path, None),
Some((x, "")) => (x, None),
Some((bucket, key)) => (bucket, Some(key)),
};
if !validation.validate_bucket_name(bucket) {
return Err(ParseS3PathError::InvalidBucketName);
}
let Some(key) = key else { return Ok(S3Path::bucket(bucket)) };
if !check_key(key) {
return Err(ParseS3PathError::KeyTooLong);
}
Ok(S3Path::object(bucket, key))
}
pub fn parse_virtual_hosted_style(vh_bucket: Option<&str>, uri_path: &str) -> Result<S3Path, ParseS3PathError> {
parse_virtual_hosted_style_with_validation(vh_bucket, uri_path, &AwsNameValidation::new())
}
pub fn parse_virtual_hosted_style_with_validation(
vh_bucket: Option<&str>,
uri_path: &str,
validation: &dyn NameValidation,
) -> Result<S3Path, ParseS3PathError> {
let Some(bucket) = vh_bucket else { return parse_path_style_with_validation(uri_path, validation) };
let Some(key) = uri_path.strip_prefix('/') else { return Err(ParseS3PathError::InvalidPath) };
if !validation.validate_bucket_name(bucket) {
return Err(ParseS3PathError::InvalidBucketName);
}
if key.is_empty() {
return Ok(S3Path::Bucket { bucket: bucket.into() });
}
if !check_key(key) {
return Err(ParseS3PathError::KeyTooLong);
}
Ok(S3Path::Object {
bucket: bucket.into(),
key: key.into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::{S3Host, SingleDomain};
use crate::validation::AwsNameValidation;
use crate::validation::tests::RelaxedNameValidation;
#[test]
fn bucket_naming_rules() {
let cases = [
("docexamplebucket1", true),
("log-delivery-march-2020", true),
("my-hosted-content", true),
("docexamplewebsite.com", true),
("www.docexamplewebsite.com", true),
("my.example.s3.bucket", true),
("doc_example_bucket", false),
("DocExampleBucket", false),
("doc-example-bucket-", false),
];
for (input, expected) in cases {
assert_eq!(check_bucket_name(input), expected);
}
}
#[test]
fn path_style() {
let too_long_path = format!("/{}/{}", "asd", "b".repeat(2048).as_str());
let cases = [
("/", Ok(S3Path::Root)),
("/bucket", Ok(S3Path::bucket("bucket"))),
("/bucket/", Ok(S3Path::bucket("bucket"))),
("/bucket/dir/object", Ok(S3Path::object("bucket", "dir/object"))),
("asd", Err(ParseS3PathError::InvalidPath)),
("a/", Err(ParseS3PathError::InvalidPath)),
("/*", Err(ParseS3PathError::InvalidBucketName)),
(too_long_path.as_str(), Err(ParseS3PathError::KeyTooLong)),
];
for (uri_path, expected) in cases {
assert_eq!(parse_path_style(uri_path), expected);
}
}
#[test]
fn virtual_hosted_style() {
{
let s3_host = SingleDomain::new("s3.us-east-1.amazonaws.com").unwrap();
let host = "s3.us-east-1.amazonaws.com";
let uri_path = "/example.com/homepage.html";
let vh = s3_host.parse_host_header(host).unwrap();
let ans = parse_virtual_hosted_style(vh.bucket(), uri_path);
let expected = Ok(S3Path::object("example.com", "homepage.html"));
assert_eq!(ans, expected);
}
{
let s3_host = SingleDomain::new("s3.eu-west-1.amazonaws.com").unwrap();
let host = "doc-example-bucket1.eu.s3.eu-west-1.amazonaws.com";
let uri_path = "/homepage.html";
let vh = s3_host.parse_host_header(host).unwrap();
let ans = parse_virtual_hosted_style(vh.bucket(), uri_path);
let expected = Ok(S3Path::object("doc-example-bucket1.eu", "homepage.html"));
assert_eq!(ans, expected);
}
{
let s3_host = SingleDomain::new("s3.eu-west-1.amazonaws.com").unwrap();
let host = "doc-example-bucket1.eu.s3.eu-west-1.amazonaws.com";
let uri_path = "/";
let vh = s3_host.parse_host_header(host).unwrap();
let ans = parse_virtual_hosted_style(vh.bucket(), uri_path);
let expected = Ok(S3Path::bucket("doc-example-bucket1.eu"));
assert_eq!(ans, expected);
}
{
let s3_host = SingleDomain::new("s3.us-east-1.amazonaws.com").unwrap();
let host = "example.com";
let uri_path = "/homepage.html";
let vh = s3_host.parse_host_header(host).unwrap();
let ans = parse_virtual_hosted_style(vh.bucket(), uri_path);
let expected = Ok(S3Path::object("example.com", "homepage.html"));
assert_eq!(ans, expected);
}
}
#[test]
fn test_path_style_with_custom_validation() {
let invalid_names = [
"UPPERCASE", "bucket_with_underscore", "bucket..double.dots", "bucket-", "192.168.1.1", ];
for bucket_name in invalid_names {
let path = format!("/{bucket_name}/key");
let result = parse_path_style_with_validation(&path, &AwsNameValidation::new());
assert!(result.is_err(), "Expected error for bucket name: {bucket_name}");
let result = parse_path_style_with_validation(&path, &RelaxedNameValidation::new());
assert!(result.is_ok(), "Expected success for bucket name: {bucket_name}");
if let Ok(S3Path::Object { bucket, key }) = result {
assert_eq!(bucket.as_ref(), bucket_name);
assert_eq!(key.as_ref(), "key");
}
}
let result = parse_path_style_with_validation("/valid-bucket/key", &RelaxedNameValidation::new());
assert!(result.is_ok());
}
#[test]
fn test_virtual_hosted_style_with_custom_validation() {
let invalid_names = ["UPPERCASE", "bucket_with_underscore", "bucket..double.dots"];
for bucket_name in invalid_names {
let result = parse_virtual_hosted_style_with_validation(Some(bucket_name), "/key", &AwsNameValidation::new());
assert!(result.is_err(), "Expected error for bucket name: {bucket_name}");
let result = parse_virtual_hosted_style_with_validation(Some(bucket_name), "/key", &RelaxedNameValidation::new());
assert!(result.is_ok(), "Expected success for bucket name: {bucket_name}");
if let Ok(S3Path::Object { bucket, key }) = result {
assert_eq!(bucket.as_ref(), bucket_name);
assert_eq!(key.as_ref(), "key");
}
}
}
#[test]
fn test_path_style_validation_fallback() {
let result1 = parse_path_style("/UPPERCASE/key");
let result2 = parse_path_style_with_validation("/UPPERCASE/key", &AwsNameValidation::new());
assert_eq!(result1.is_err(), result2.is_err());
}
#[test]
fn test_virtual_hosted_style_validation_fallback() {
let result1 = parse_virtual_hosted_style(Some("UPPERCASE"), "/key");
let result2 = parse_virtual_hosted_style_with_validation(Some("UPPERCASE"), "/key", &AwsNameValidation::new());
assert_eq!(result1.is_err(), result2.is_err());
}
#[test]
fn s3path_root_accessors() {
let p = S3Path::root();
assert!(p.is_root());
assert!(p.as_bucket().is_none());
assert!(p.as_object().is_none());
assert!(p.get_bucket_name().is_none());
assert!(p.get_object_key().is_none());
}
#[test]
fn s3path_bucket_accessors() {
let p = S3Path::bucket("my-bucket");
assert!(!p.is_root());
assert_eq!(p.as_bucket(), Some("my-bucket"));
assert!(p.as_object().is_none());
assert_eq!(p.get_bucket_name(), Some("my-bucket"));
assert!(p.get_object_key().is_none());
}
#[test]
fn s3path_object_accessors() {
let p = S3Path::object("my-bucket", "my-key");
assert!(!p.is_root());
assert!(p.as_bucket().is_none());
assert_eq!(p.as_object(), Some(("my-bucket", "my-key")));
assert_eq!(p.get_bucket_name(), Some("my-bucket"));
assert_eq!(p.get_object_key(), Some("my-key"));
}
#[test]
fn check_key_boundary() {
let key = "a".repeat(1024);
assert!(check_key(&key));
let key = "a".repeat(1025);
assert!(!check_key(&key));
}
#[test]
fn virtual_hosted_no_bucket_fallback() {
let result = parse_virtual_hosted_style(None, "/bucket/key");
assert_eq!(result, Ok(S3Path::object("bucket", "key")));
}
#[test]
fn virtual_hosted_invalid_path() {
let result = parse_virtual_hosted_style(Some("bucket"), "no-slash");
assert_eq!(result, Err(ParseS3PathError::InvalidPath));
}
#[test]
fn virtual_hosted_key_too_long() {
let long_key = "a".repeat(1025);
let uri_path = format!("/{long_key}");
let result = parse_virtual_hosted_style(Some("bucket"), &uri_path);
assert_eq!(result, Err(ParseS3PathError::KeyTooLong));
}
#[test]
fn bucket_name_edge_cases() {
assert!(!check_bucket_name("ab"));
assert!(!check_bucket_name(&"a".repeat(64)));
assert!(check_bucket_name("abc"));
assert!(check_bucket_name(&"a".repeat(63)));
assert!(!check_bucket_name("xn--example"));
}
#[test]
fn parse_s3_path_error_display() {
let err = ParseS3PathError::InvalidPath;
assert!(!format!("{err}").is_empty());
let err = ParseS3PathError::InvalidBucketName;
assert!(!format!("{err}").is_empty());
let err = ParseS3PathError::KeyTooLong;
assert!(!format!("{err}").is_empty());
}
}