use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum S3ErrorCode { AccessDenied, BadDigest, BucketAlreadyExists, BucketAlreadyOwnedByYou, BucketNotEmpty, EntityTooLarge, EntityTooSmall, InternalError, InvalidAccessKeyId, InvalidArgument, InvalidBucketName, InvalidKey, InvalidPart, InvalidPartOrder, InvalidPolicyDocument, InvalidRange, InvalidRequest, InvalidTag, MalformedXML, MethodNotAllowed, NoSuchBucket, NoSuchBucketPolicy, NoSuchKey, NoSuchLifecycleConfiguration, NoSuchUpload, NoSuchVersion, NoSuchTagSet, ObjectCorrupted, PreconditionFailed, NotModified, QuotaExceeded, RequestTimeTooSkewed, ServerSideEncryptionConfigurationNotFoundError, SignatureDoesNotMatch, SlowDown, } impl S3ErrorCode { pub fn http_status(&self) -> u16 { match self { Self::AccessDenied => 403, Self::BadDigest => 400, Self::BucketAlreadyExists => 409, Self::BucketAlreadyOwnedByYou => 409, Self::BucketNotEmpty => 409, Self::EntityTooLarge => 413, Self::EntityTooSmall => 400, Self::InternalError => 500, Self::InvalidAccessKeyId => 403, Self::InvalidArgument => 400, Self::InvalidBucketName => 400, Self::InvalidKey => 400, Self::InvalidPart => 400, Self::InvalidPartOrder => 400, Self::InvalidPolicyDocument => 400, Self::InvalidRange => 416, Self::InvalidRequest => 400, Self::InvalidTag => 400, Self::MalformedXML => 400, Self::MethodNotAllowed => 405, Self::NoSuchBucket => 404, Self::NoSuchBucketPolicy => 404, Self::NoSuchKey => 404, Self::NoSuchLifecycleConfiguration => 404, Self::NoSuchUpload => 404, Self::NoSuchVersion => 404, Self::NoSuchTagSet => 404, Self::ObjectCorrupted => 500, Self::PreconditionFailed => 412, Self::NotModified => 304, Self::QuotaExceeded => 403, Self::RequestTimeTooSkewed => 403, Self::ServerSideEncryptionConfigurationNotFoundError => 404, Self::SignatureDoesNotMatch => 403, Self::SlowDown => 503, } } pub fn as_str(&self) -> &'static str { match self { Self::AccessDenied => "AccessDenied", Self::BadDigest => "BadDigest", Self::BucketAlreadyExists => "BucketAlreadyExists", Self::BucketAlreadyOwnedByYou => "BucketAlreadyOwnedByYou", Self::BucketNotEmpty => "BucketNotEmpty", Self::EntityTooLarge => "EntityTooLarge", Self::EntityTooSmall => "EntityTooSmall", Self::InternalError => "InternalError", Self::InvalidAccessKeyId => "InvalidAccessKeyId", Self::InvalidArgument => "InvalidArgument", Self::InvalidBucketName => "InvalidBucketName", Self::InvalidKey => "InvalidKey", Self::InvalidPart => "InvalidPart", Self::InvalidPartOrder => "InvalidPartOrder", Self::InvalidPolicyDocument => "InvalidPolicyDocument", Self::InvalidRange => "InvalidRange", Self::InvalidRequest => "InvalidRequest", Self::InvalidTag => "InvalidTag", Self::MalformedXML => "MalformedXML", Self::MethodNotAllowed => "MethodNotAllowed", Self::NoSuchBucket => "NoSuchBucket", Self::NoSuchBucketPolicy => "NoSuchBucketPolicy", Self::NoSuchKey => "NoSuchKey", Self::NoSuchLifecycleConfiguration => "NoSuchLifecycleConfiguration", Self::NoSuchUpload => "NoSuchUpload", Self::NoSuchVersion => "NoSuchVersion", Self::NoSuchTagSet => "NoSuchTagSet", Self::ObjectCorrupted => "ObjectCorrupted", Self::PreconditionFailed => "PreconditionFailed", Self::NotModified => "NotModified", Self::QuotaExceeded => "QuotaExceeded", Self::RequestTimeTooSkewed => "RequestTimeTooSkewed", Self::ServerSideEncryptionConfigurationNotFoundError => { "ServerSideEncryptionConfigurationNotFoundError" } Self::SignatureDoesNotMatch => "SignatureDoesNotMatch", Self::SlowDown => "SlowDown", } } pub fn default_message(&self) -> &'static str { match self { Self::AccessDenied => "Access Denied", Self::BadDigest => "The Content-MD5 or checksum value you specified did not match what we received", Self::BucketAlreadyExists => "The requested bucket name is not available", Self::BucketAlreadyOwnedByYou => "Your previous request to create the named bucket succeeded and you already own it", Self::BucketNotEmpty => "The bucket you tried to delete is not empty", Self::EntityTooLarge => "Your proposed upload exceeds the maximum allowed size", Self::EntityTooSmall => "Your proposed upload is smaller than the minimum allowed object size", Self::InternalError => "We encountered an internal error. Please try again.", Self::InvalidAccessKeyId => "The access key ID you provided does not exist", Self::InvalidArgument => "Invalid argument", Self::InvalidBucketName => "The specified bucket is not valid", Self::InvalidKey => "The specified key is not valid", Self::InvalidPart => "One or more of the specified parts could not be found", Self::InvalidPartOrder => "The list of parts was not in ascending order", Self::InvalidPolicyDocument => "The content of the form does not meet the conditions specified in the policy document", Self::InvalidRange => "The requested range is not satisfiable", Self::InvalidRequest => "Invalid request", Self::InvalidTag => "The Tagging header is invalid", Self::MalformedXML => "The XML you provided was not well-formed", Self::MethodNotAllowed => "The specified method is not allowed against this resource", Self::NoSuchBucket => "The specified bucket does not exist", Self::NoSuchBucketPolicy => "The bucket policy does not exist", Self::NoSuchKey => "The specified key does not exist", Self::NoSuchLifecycleConfiguration => "The lifecycle configuration does not exist", Self::NoSuchUpload => "The specified multipart upload does not exist", Self::NoSuchVersion => "The specified version does not exist", Self::NoSuchTagSet => "The TagSet does not exist", Self::ObjectCorrupted => "The stored object is corrupted and cannot be served", Self::PreconditionFailed => "At least one of the preconditions you specified did not hold", Self::NotModified => "Not Modified", Self::QuotaExceeded => "The bucket quota has been exceeded", Self::RequestTimeTooSkewed => "The difference between the request time and the server's time is too large", Self::ServerSideEncryptionConfigurationNotFoundError => "The server side encryption configuration was not found", Self::SignatureDoesNotMatch => "The request signature we calculated does not match the signature you provided", Self::SlowDown => "Please reduce your request rate", } } } impl fmt::Display for S3ErrorCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } #[derive(Debug, Clone)] pub struct S3Error { pub code: S3ErrorCode, pub message: String, pub resource: String, pub request_id: String, } impl S3Error { pub fn new(code: S3ErrorCode, message: impl Into) -> Self { Self { code, message: message.into(), resource: String::new(), request_id: String::new(), } } pub fn from_code(code: S3ErrorCode) -> Self { Self::new(code, code.default_message()) } pub fn with_resource(mut self, resource: impl Into) -> Self { self.resource = resource.into(); self } pub fn with_request_id(mut self, request_id: impl Into) -> Self { self.request_id = request_id.into(); self } pub fn http_status(&self) -> u16 { self.code.http_status() } pub fn to_xml(&self) -> String { format!( "\ \ {}\ {}\ {}\ {}\ ", self.code.as_str(), xml_escape(&self.message), xml_escape(&self.resource), xml_escape(&self.request_id), ) } } impl fmt::Display for S3Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.code, self.message) } } impl std::error::Error for S3Error {} fn xml_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_codes() { assert_eq!(S3ErrorCode::NoSuchKey.http_status(), 404); assert_eq!(S3ErrorCode::AccessDenied.http_status(), 403); assert_eq!(S3ErrorCode::NoSuchBucket.as_str(), "NoSuchBucket"); } #[test] fn test_error_to_xml() { let err = S3Error::from_code(S3ErrorCode::NoSuchKey) .with_resource("/test-bucket/test-key") .with_request_id("abc123"); let xml = err.to_xml(); assert!(xml.contains("NoSuchKey")); assert!(xml.contains("/test-bucket/test-key")); assert!(xml.contains("abc123")); } #[test] fn test_xml_escape() { let err = S3Error::new(S3ErrorCode::InvalidArgument, "key & \"value\"") .with_resource("/bucket/key&"); let xml = err.to_xml(); assert!(xml.contains("<test>")); assert!(xml.contains("&")); } }