Skip to main content

rustack_s3_core/state/
object.rs

1//! S3 object types and metadata.
2//!
3//! This module defines the core data structures for S3 objects, delete markers,
4//! object metadata, ownership, ACL configuration, and versioning.
5
6use std::{collections::HashMap, fmt, str::FromStr};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11// ---------------------------------------------------------------------------
12// Owner
13// ---------------------------------------------------------------------------
14
15/// The owner of an S3 object or bucket.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct Owner {
19    /// The canonical user ID of the owner.
20    pub id: String,
21    /// The display name of the owner.
22    pub display_name: String,
23}
24
25impl Default for Owner {
26    fn default() -> Self {
27        Self {
28            id: "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a".to_owned(),
29            display_name: "webfile".to_owned(),
30        }
31    }
32}
33
34impl fmt::Display for Owner {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "{}({})", self.display_name, self.id)
37    }
38}
39
40// ---------------------------------------------------------------------------
41// CannedAcl
42// ---------------------------------------------------------------------------
43
44/// Predefined (canned) ACL grants for S3 buckets and objects.
45#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub enum CannedAcl {
47    /// Owner gets `FULL_CONTROL`. No one else has access rights (default).
48    #[default]
49    Private,
50    /// Owner gets `FULL_CONTROL`. The `AllUsers` group gets `READ` access.
51    PublicRead,
52    /// Owner gets `FULL_CONTROL`. The `AllUsers` group gets `READ` and `WRITE` access.
53    PublicReadWrite,
54    /// Owner gets `FULL_CONTROL`. The `AuthenticatedUsers` group gets `READ` access.
55    AuthenticatedRead,
56    /// Owner gets `FULL_CONTROL`. Amazon EC2 gets `READ` access to GET an
57    /// Amazon Machine Image (AMI) bundle from Amazon S3.
58    AwsExecRead,
59    /// Object owner gets `FULL_CONTROL`. Bucket owner gets `READ` access.
60    BucketOwnerRead,
61    /// Both the object owner and the bucket owner get `FULL_CONTROL` over the object.
62    BucketOwnerFullControl,
63    /// The `LogDelivery` group gets `WRITE` and `READ_ACP` permissions on the bucket.
64    LogDeliveryWrite,
65}
66
67impl CannedAcl {
68    /// Return the string representation of the canned ACL.
69    #[must_use]
70    pub fn as_str(&self) -> &str {
71        match self {
72            Self::Private => "private",
73            Self::PublicRead => "public-read",
74            Self::PublicReadWrite => "public-read-write",
75            Self::AuthenticatedRead => "authenticated-read",
76            Self::AwsExecRead => "aws-exec-read",
77            Self::BucketOwnerRead => "bucket-owner-read",
78            Self::BucketOwnerFullControl => "bucket-owner-full-control",
79            Self::LogDeliveryWrite => "log-delivery-write",
80        }
81    }
82}
83
84impl fmt::Display for CannedAcl {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(self.as_str())
87    }
88}
89
90/// Error returned when parsing a [`CannedAcl`] from a string fails.
91#[derive(Debug, Clone, thiserror::Error)]
92#[error("unknown canned ACL: {0}")]
93pub struct ParseCannedAclError(String);
94
95impl FromStr for CannedAcl {
96    type Err = ParseCannedAclError;
97
98    fn from_str(s: &str) -> Result<Self, Self::Err> {
99        match s {
100            "private" => Ok(Self::Private),
101            "public-read" => Ok(Self::PublicRead),
102            "public-read-write" => Ok(Self::PublicReadWrite),
103            "authenticated-read" => Ok(Self::AuthenticatedRead),
104            "aws-exec-read" => Ok(Self::AwsExecRead),
105            "bucket-owner-read" => Ok(Self::BucketOwnerRead),
106            "bucket-owner-full-control" => Ok(Self::BucketOwnerFullControl),
107            "log-delivery-write" => Ok(Self::LogDeliveryWrite),
108            _ => Err(ParseCannedAclError(s.to_owned())),
109        }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// Grant / Grantee / Permission
115// ---------------------------------------------------------------------------
116
117/// An ACL grant that pairs a grantee with a permission.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct Grant {
121    /// The entity receiving the permission.
122    pub grantee: Grantee,
123    /// The permission granted.
124    pub permission: Permission,
125}
126
127/// A grantee in an ACL grant.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase", tag = "type")]
130pub enum Grantee {
131    /// A canonical user identified by an AWS account ID.
132    CanonicalUser {
133        /// The canonical user ID.
134        id: String,
135        /// The display name for the user.
136        display_name: String,
137    },
138    /// A predefined Amazon S3 group.
139    Group {
140        /// The URI of the group (e.g.,
141        /// `http://acs.amazonaws.com/groups/global/AllUsers`).
142        uri: String,
143    },
144    /// A grantee identified by email (legacy, seldom used).
145    Email {
146        /// The email address of the grantee.
147        email: String,
148    },
149}
150
151/// A permission that can be granted to a grantee.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153pub enum Permission {
154    /// Grants full control (READ, WRITE, READ_ACP, WRITE_ACP).
155    FullControl,
156    /// Allows grantee to list objects in the bucket or read the object data.
157    Read,
158    /// Allows grantee to create objects in the bucket.
159    Write,
160    /// Allows grantee to read the bucket/object ACL.
161    ReadAcp,
162    /// Allows grantee to write the bucket/object ACL.
163    WriteAcp,
164}
165
166impl fmt::Display for Permission {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        let s = match self {
169            Self::FullControl => "FULL_CONTROL",
170            Self::Read => "READ",
171            Self::Write => "WRITE",
172            Self::ReadAcp => "READ_ACP",
173            Self::WriteAcp => "WRITE_ACP",
174        };
175        f.write_str(s)
176    }
177}
178
179// ---------------------------------------------------------------------------
180// ChecksumData
181// ---------------------------------------------------------------------------
182
183/// Checksum data attached to an S3 object or part.
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct ChecksumData {
187    /// The checksum algorithm (e.g. `CRC32`, `CRC32C`, `CRC64NVME`, `SHA1`, `SHA256`).
188    pub algorithm: String,
189    /// The base64-encoded checksum value.
190    pub value: String,
191    /// Whether this is a `FULL_OBJECT` or `COMPOSITE` checksum.
192    #[serde(default = "default_checksum_type")]
193    pub checksum_type: String,
194}
195
196/// Default checksum type for single-object uploads.
197fn default_checksum_type() -> String {
198    "FULL_OBJECT".to_owned()
199}
200
201// ---------------------------------------------------------------------------
202// ObjectMetadata
203// ---------------------------------------------------------------------------
204
205/// Metadata associated with an S3 object.
206///
207/// Includes standard HTTP headers, user-defined metadata (`x-amz-meta-*`),
208/// server-side encryption settings, tagging, ACL, and object-lock fields.
209#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct ObjectMetadata {
212    /// The MIME type of the object (e.g. `application/octet-stream`).
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub content_type: Option<String>,
215    /// Content encoding (e.g. `gzip`).
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub content_encoding: Option<String>,
218    /// Content disposition (e.g. `attachment; filename="file.txt"`).
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub content_disposition: Option<String>,
221    /// Content language (e.g. `en-US`).
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub content_language: Option<String>,
224    /// Cache control directives (e.g. `max-age=3600`).
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub cache_control: Option<String>,
227    /// Expiration date/time string.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub expires: Option<String>,
230    /// User-defined metadata headers (`x-amz-meta-*`).
231    #[serde(default)]
232    pub user_metadata: HashMap<String, String>,
233    /// Server-side encryption algorithm (e.g. `AES256`, `aws:kms`).
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub sse_algorithm: Option<String>,
236    /// KMS key ID used for server-side encryption.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub sse_kms_key_id: Option<String>,
239    /// Whether an S3 Bucket Key is enabled for SSE-KMS.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub sse_bucket_key_enabled: Option<bool>,
242    /// Customer-provided encryption algorithm for SSE-C.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub sse_customer_algorithm: Option<String>,
245    /// Base64-encoded MD5 of the customer-provided encryption key.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub sse_customer_key_md5: Option<String>,
248    /// Object tags as key-value pairs.
249    #[serde(default)]
250    pub tagging: Vec<(String, String)>,
251    /// Canned ACL applied to this object.
252    #[serde(default)]
253    pub acl: CannedAcl,
254    /// Object lock retention mode (`GOVERNANCE` or `COMPLIANCE`).
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub object_lock_mode: Option<String>,
257    /// Object lock retain-until date.
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub object_lock_retain_until: Option<DateTime<Utc>>,
260    /// Whether a legal hold is in effect for this object.
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub object_lock_legal_hold: Option<bool>,
263}
264
265// ---------------------------------------------------------------------------
266// S3Object
267// ---------------------------------------------------------------------------
268
269/// A stored S3 object (non-delete-marker).
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct S3Object {
273    /// The object key.
274    pub key: String,
275    /// The version ID (`"null"` for un-versioned objects).
276    pub version_id: String,
277    /// The entity tag (quoted hex MD5 digest, e.g. `"d41d8cd98f00b204e9800998ecf8427e"`).
278    pub etag: String,
279    /// The object size in bytes.
280    pub size: u64,
281    /// The time this version was last modified.
282    pub last_modified: DateTime<Utc>,
283    /// The storage class (default `STANDARD`).
284    pub storage_class: String,
285    /// Object metadata (headers, tags, encryption, etc.).
286    pub metadata: ObjectMetadata,
287    /// The owner of this object.
288    pub owner: Owner,
289    /// Optional checksum data.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub checksum: Option<ChecksumData>,
292    /// The number of parts if this object was created via multipart upload.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub parts_count: Option<u32>,
295    /// Individual part ETags (used for composite ETag generation in multipart uploads).
296    #[serde(default)]
297    pub part_etags: Vec<String>,
298}
299
300impl S3Object {
301    /// Returns `false` because an `S3Object` is never a delete marker.
302    #[must_use]
303    pub fn is_delete_marker(&self) -> bool {
304        false
305    }
306}
307
308// ---------------------------------------------------------------------------
309// S3DeleteMarker
310// ---------------------------------------------------------------------------
311
312/// A delete marker in a versioned bucket.
313///
314/// Delete markers are created when an object is deleted in a versioned bucket.
315/// They act as a placeholder that indicates the object has been logically deleted.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct S3DeleteMarker {
319    /// The object key.
320    pub key: String,
321    /// The version ID of this delete marker.
322    pub version_id: String,
323    /// The time this delete marker was created.
324    pub last_modified: DateTime<Utc>,
325    /// The owner of this delete marker.
326    pub owner: Owner,
327}
328
329impl S3DeleteMarker {
330    /// Returns `true` because an `S3DeleteMarker` is always a delete marker.
331    #[must_use]
332    pub fn is_delete_marker(&self) -> bool {
333        true
334    }
335}
336
337// ---------------------------------------------------------------------------
338// ObjectVersion
339// ---------------------------------------------------------------------------
340
341/// A version entry in a versioned bucket, which is either an object or a
342/// delete marker.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase", tag = "type")]
345pub enum ObjectVersion {
346    /// A real object version (boxed to reduce enum size).
347    Object(Box<S3Object>),
348    /// A delete-marker version.
349    DeleteMarker(S3DeleteMarker),
350}
351
352impl ObjectVersion {
353    /// Returns the object key.
354    #[must_use]
355    pub fn key(&self) -> &str {
356        match self {
357            Self::Object(obj) => &obj.key,
358            Self::DeleteMarker(dm) => &dm.key,
359        }
360    }
361
362    /// Returns the version ID.
363    #[must_use]
364    pub fn version_id(&self) -> &str {
365        match self {
366            Self::Object(obj) => &obj.version_id,
367            Self::DeleteMarker(dm) => &dm.version_id,
368        }
369    }
370
371    /// Returns the last-modified timestamp.
372    #[must_use]
373    pub fn last_modified(&self) -> DateTime<Utc> {
374        match self {
375            Self::Object(obj) => obj.last_modified,
376            Self::DeleteMarker(dm) => dm.last_modified,
377        }
378    }
379
380    /// Returns `true` if this version is a delete marker.
381    #[must_use]
382    pub fn is_delete_marker(&self) -> bool {
383        matches!(self, Self::DeleteMarker(_))
384    }
385
386    /// Returns the owner of this version.
387    #[must_use]
388    pub fn owner(&self) -> &Owner {
389        match self {
390            Self::Object(obj) => &obj.owner,
391            Self::DeleteMarker(dm) => &dm.owner,
392        }
393    }
394
395    /// Returns a reference to the inner `S3Object`, if this is an object version.
396    #[must_use]
397    pub fn as_object(&self) -> Option<&S3Object> {
398        match self {
399            Self::Object(obj) => Some(obj),
400            Self::DeleteMarker(_) => None,
401        }
402    }
403
404    /// Returns a mutable reference to the inner `S3Object`, if this is an object version.
405    pub fn as_object_mut(&mut self) -> Option<&mut S3Object> {
406        match self {
407            Self::Object(obj) => Some(obj),
408            Self::DeleteMarker(_) => None,
409        }
410    }
411
412    /// Returns a reference to the inner `S3DeleteMarker`, if this is a delete marker.
413    #[must_use]
414    pub fn as_delete_marker(&self) -> Option<&S3DeleteMarker> {
415        match self {
416            Self::Object(_) => None,
417            Self::DeleteMarker(dm) => Some(dm),
418        }
419    }
420}
421
422// ---------------------------------------------------------------------------
423// Tests
424// ---------------------------------------------------------------------------
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_should_use_default_owner() {
432        let owner = Owner::default();
433        assert_eq!(owner.display_name, "webfile");
434        assert!(!owner.id.is_empty());
435    }
436
437    #[test]
438    fn test_should_display_owner() {
439        let owner = Owner {
440            id: "abc123".to_owned(),
441            display_name: "alice".to_owned(),
442        };
443        assert_eq!(format!("{owner}"), "alice(abc123)");
444    }
445
446    #[test]
447    fn test_should_default_canned_acl_to_private() {
448        assert_eq!(CannedAcl::default(), CannedAcl::Private);
449        assert_eq!(CannedAcl::default().as_str(), "private");
450    }
451
452    #[test]
453    fn test_should_roundtrip_canned_acl_from_str() {
454        let cases = [
455            ("private", CannedAcl::Private),
456            ("public-read", CannedAcl::PublicRead),
457            ("public-read-write", CannedAcl::PublicReadWrite),
458            ("authenticated-read", CannedAcl::AuthenticatedRead),
459            ("aws-exec-read", CannedAcl::AwsExecRead),
460            ("bucket-owner-read", CannedAcl::BucketOwnerRead),
461            (
462                "bucket-owner-full-control",
463                CannedAcl::BucketOwnerFullControl,
464            ),
465            ("log-delivery-write", CannedAcl::LogDeliveryWrite),
466        ];
467        for (s, expected) in cases {
468            let parsed: CannedAcl = s.parse().unwrap_or_else(|_| panic!("failed to parse {s}"));
469            assert_eq!(parsed, expected);
470            assert_eq!(parsed.as_str(), s);
471        }
472    }
473
474    #[test]
475    fn test_should_reject_unknown_canned_acl() {
476        let result = "unknown-acl".parse::<CannedAcl>();
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn test_should_identify_object_as_not_delete_marker() {
482        let obj = make_test_object("test-key");
483        assert!(!obj.is_delete_marker());
484    }
485
486    #[test]
487    fn test_should_identify_delete_marker() {
488        let dm = S3DeleteMarker {
489            key: "test-key".to_owned(),
490            version_id: "v1".to_owned(),
491            last_modified: Utc::now(),
492            owner: Owner::default(),
493        };
494        assert!(dm.is_delete_marker());
495    }
496
497    #[test]
498    fn test_should_access_object_version_fields() {
499        let obj = make_test_object("my-key");
500        let version = ObjectVersion::Object(Box::new(obj));
501
502        assert_eq!(version.key(), "my-key");
503        assert_eq!(version.version_id(), "null");
504        assert!(!version.is_delete_marker());
505        assert!(version.as_object().is_some());
506        assert!(version.as_delete_marker().is_none());
507    }
508
509    #[test]
510    fn test_should_access_delete_marker_version_fields() {
511        let dm = S3DeleteMarker {
512            key: "deleted-key".to_owned(),
513            version_id: "dm-v1".to_owned(),
514            last_modified: Utc::now(),
515            owner: Owner::default(),
516        };
517        let version = ObjectVersion::DeleteMarker(dm);
518
519        assert_eq!(version.key(), "deleted-key");
520        assert_eq!(version.version_id(), "dm-v1");
521        assert!(version.is_delete_marker());
522        assert!(version.as_object().is_none());
523        assert!(version.as_delete_marker().is_some());
524    }
525
526    #[test]
527    fn test_should_default_object_metadata() {
528        let meta = ObjectMetadata::default();
529        assert!(meta.content_type.is_none());
530        assert!(meta.user_metadata.is_empty());
531        assert!(meta.tagging.is_empty());
532        assert_eq!(meta.acl, CannedAcl::Private);
533        assert!(meta.object_lock_mode.is_none());
534    }
535
536    #[test]
537    fn test_should_display_permission() {
538        assert_eq!(format!("{}", Permission::FullControl), "FULL_CONTROL");
539        assert_eq!(format!("{}", Permission::Read), "READ");
540        assert_eq!(format!("{}", Permission::Write), "WRITE");
541        assert_eq!(format!("{}", Permission::ReadAcp), "READ_ACP");
542        assert_eq!(format!("{}", Permission::WriteAcp), "WRITE_ACP");
543    }
544
545    // ---- helpers ----
546
547    fn make_test_object(key: &str) -> S3Object {
548        S3Object {
549            key: key.to_owned(),
550            version_id: "null".to_owned(),
551            etag: "\"d41d8cd98f00b204e9800998ecf8427e\"".to_owned(),
552            size: 0,
553            last_modified: Utc::now(),
554            storage_class: "STANDARD".to_owned(),
555            metadata: ObjectMetadata::default(),
556            owner: Owner::default(),
557            checksum: None,
558            parts_count: None,
559            part_etags: Vec::new(),
560        }
561    }
562}