use std::net::IpAddr;
#[allow(clippy::exhaustive_enums)]
#[derive(Debug)]
pub enum S3Path<'a> {
Root,
Bucket {
bucket: &'a str,
},
Object {
bucket: &'a str,
key: &'a str,
},
}
#[allow(missing_copy_implementations)]
#[derive(Debug, thiserror::Error)]
#[error("ParseS3PathError: {:?}",.kind)]
pub struct ParseS3PathError {
kind: S3PathErrorKind,
}
impl ParseS3PathError {
#[must_use]
pub const fn kind(&self) -> &S3PathErrorKind {
&self.kind
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum S3PathErrorKind {
InvalidPath,
InvalidBucketName,
KeyTooLong,
}
impl<'a> S3Path<'a> {
#[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.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 try_from_path(path: &'a str) -> Result<Self, ParseS3PathError> {
let path = if let Some(("", x)) = path.split_once('/') {
x
} else {
return Err(ParseS3PathError {
kind: S3PathErrorKind::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 !Self::check_bucket_name(bucket) {
return Err(ParseS3PathError {
kind: S3PathErrorKind::InvalidBucketName,
});
}
let key = match key {
None => return Ok(S3Path::Bucket { bucket }),
Some(k) => k,
};
if !Self::check_key(key) {
return Err(ParseS3PathError {
kind: S3PathErrorKind::KeyTooLong,
});
}
Ok(Self::Object { bucket, key })
}
#[must_use]
pub const fn is_root(&self) -> bool {
matches!(*self, Self::Root)
}
#[must_use]
pub const fn is_bucket(&self) -> bool {
matches!(*self, Self::Bucket { .. })
}
#[must_use]
pub const fn is_object(&self) -> bool {
matches!(*self, Self::Object { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_s3_path() {
assert!(matches!(S3Path::try_from_path("/"), Ok(S3Path::Root)));
assert!(matches!(
S3Path::try_from_path("/bucket"),
Ok(S3Path::Bucket { bucket: "bucket" })
));
assert!(matches!(
S3Path::try_from_path("/bucket/"),
Ok(S3Path::Bucket { bucket: "bucket" })
));
assert!(matches!(
S3Path::try_from_path("/bucket/dir/object"),
Ok(S3Path::Object {
bucket: "bucket",
key: "dir/object"
})
));
assert_eq!(
S3Path::try_from_path("asd").unwrap_err().kind(),
&S3PathErrorKind::InvalidPath
);
assert_eq!(
S3Path::try_from_path("a/").unwrap_err().kind(),
&S3PathErrorKind::InvalidPath
);
assert_eq!(
S3Path::try_from_path("/*").unwrap_err().kind(),
&S3PathErrorKind::InvalidBucketName
);
let too_long_path = format!("/{}/{}", "asd", "b".repeat(2048).as_str());
assert_eq!(
S3Path::try_from_path(&too_long_path).unwrap_err().kind(),
&S3PathErrorKind::KeyTooLong
);
}
}