Skip to main content

aegis_object/
types.rs

1//! Core types for the object / blob store.
2
3use serde::{Deserialize, Serialize};
4
5/// The default content type when none is supplied.
6pub const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
7
8/// Metadata for a stored object (everything but the bytes).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ObjectMeta {
11    pub key: String,
12    pub size: usize,
13    pub content_type: String,
14    /// Content fingerprint (FNV-1a 64-bit, hex) — changes iff the bytes change.
15    pub etag: String,
16    #[serde(default)]
17    pub metadata: serde_json::Value,
18}
19
20/// Errors returned by the object store.
21#[derive(Debug, thiserror::Error)]
22pub enum ObjectError {
23    #[error("bucket '{0}' not found")]
24    BucketNotFound(String),
25    #[error("bucket '{0}' already exists")]
26    BucketExists(String),
27    #[error("object '{0}' not found")]
28    ObjectNotFound(String),
29    #[error("invalid bucket name '{0}'")]
30    InvalidBucketName(String),
31}
32
33/// Content-addressed ETag: FNV-1a 64-bit over the bytes, as 16 hex digits.
34pub fn etag_of(data: &[u8]) -> String {
35    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
36    for &b in data {
37        h ^= b as u64;
38        h = h.wrapping_mul(0x0000_0100_0000_01b3);
39    }
40    format!("{h:016x}")
41}
42
43/// A bucket name must be non-empty and contain only `[a-z0-9.-]` (S3-ish).
44pub fn valid_bucket_name(name: &str) -> bool {
45    !name.is_empty()
46        && name.len() <= 63
47        && name
48            .bytes()
49            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'-')
50}