Skip to main content

aegis_object/
lib.rs

1//! Aegis Object — object / blob store for the Aegis database.
2//!
3//! S3-style **buckets** of binary objects, each with a content type,
4//! content-addressed **ETag** (FNV-1a fingerprint of the bytes), and JSON
5//! metadata. Supports put / get / head / delete and lexical prefix listing,
6//! with snapshot persistence.
7
8pub mod engine;
9pub mod types;
10
11pub use engine::{BucketStats, EngineSnapshot, ObjectEngine};
12pub use types::{etag_of, valid_bucket_name, ObjectError, ObjectMeta, DEFAULT_CONTENT_TYPE};
13
14#[cfg(test)]
15mod tests {
16    use super::*;
17
18    fn seeded() -> ObjectEngine {
19        let e = ObjectEngine::new();
20        e.create_bucket("media").unwrap();
21        e.put(
22            "media",
23            "a.txt",
24            b"hello".to_vec(),
25            Some("text/plain".into()),
26            serde_json::json!({"k": 1}),
27        )
28        .unwrap();
29        e.put(
30            "media",
31            "img/1.png",
32            b"\x89PNG\x00".to_vec(),
33            Some("image/png".into()),
34            serde_json::Value::Null,
35        )
36        .unwrap();
37        e.put(
38            "media",
39            "img/2.png",
40            b"\x89PNG\x01".to_vec(),
41            Some("image/png".into()),
42            serde_json::Value::Null,
43        )
44        .unwrap();
45        e
46    }
47
48    #[test]
49    fn put_get_roundtrip_and_etag() {
50        let e = seeded();
51        let (data, meta) = e.get("media", "a.txt").unwrap().unwrap();
52        assert_eq!(data, b"hello");
53        assert_eq!(meta.content_type, "text/plain");
54        assert_eq!(meta.size, 5);
55        assert_eq!(meta.etag, etag_of(b"hello"));
56        assert_eq!(meta.metadata, serde_json::json!({"k": 1}));
57        // Different content => different etag; identical content => identical.
58        assert_ne!(etag_of(b"hello"), etag_of(b"world"));
59        assert_eq!(etag_of(b"hello"), etag_of(b"hello"));
60    }
61
62    #[test]
63    fn head_without_body() {
64        let e = seeded();
65        let meta = e.head("media", "img/1.png").unwrap().unwrap();
66        assert_eq!(meta.content_type, "image/png");
67        assert_eq!(meta.size, 5);
68        assert!(e.head("media", "nope").unwrap().is_none());
69    }
70
71    #[test]
72    fn overwrite_updates_etag_and_size() {
73        let e = seeded();
74        let first = e.head("media", "a.txt").unwrap().unwrap().etag;
75        e.put(
76            "media",
77            "a.txt",
78            b"hello world".to_vec(),
79            None,
80            serde_json::Value::Null,
81        )
82        .unwrap();
83        let meta = e.head("media", "a.txt").unwrap().unwrap();
84        assert_eq!(meta.size, 11);
85        assert_ne!(meta.etag, first);
86        // No content type supplied on overwrite => default.
87        assert_eq!(meta.content_type, DEFAULT_CONTENT_TYPE);
88        assert_eq!(e.bucket_stats("media").unwrap().objects, 3);
89    }
90
91    #[test]
92    fn prefix_listing_is_sorted() {
93        let e = seeded();
94        let imgs = e.list("media", "img/", None).unwrap();
95        let keys: Vec<&str> = imgs.iter().map(|m| m.key.as_str()).collect();
96        assert_eq!(keys, vec!["img/1.png", "img/2.png"]);
97
98        let all = e.list("media", "", None).unwrap();
99        assert_eq!(all.len(), 3);
100        let limited = e.list("media", "", Some(1)).unwrap();
101        assert_eq!(limited.len(), 1);
102        assert_eq!(limited[0].key, "a.txt");
103    }
104
105    #[test]
106    fn delete_and_stats() {
107        let e = seeded();
108        let stats = e.bucket_stats("media").unwrap();
109        assert_eq!(stats.objects, 3);
110        assert_eq!(stats.bytes, 5 + 5 + 5);
111        assert!(e.delete("media", "a.txt").unwrap());
112        assert!(!e.delete("media", "a.txt").unwrap());
113        assert_eq!(e.bucket_stats("media").unwrap().objects, 2);
114    }
115
116    #[test]
117    fn bucket_validation_and_errors() {
118        let e = ObjectEngine::new();
119        assert!(matches!(
120            e.create_bucket("BadName"),
121            Err(ObjectError::InvalidBucketName(_))
122        ));
123        e.create_bucket("ok").unwrap();
124        assert!(matches!(
125            e.create_bucket("ok"),
126            Err(ObjectError::BucketExists(_))
127        ));
128        // Reads on a missing bucket still error (no implicit creation).
129        assert!(matches!(
130            e.get("nope", "k"),
131            Err(ObjectError::BucketNotFound(_))
132        ));
133        assert!(matches!(
134            e.list("nope", "", None),
135            Err(ObjectError::BucketNotFound(_))
136        ));
137    }
138
139    #[test]
140    fn snapshot_roundtrip() {
141        let e = seeded();
142        let bytes = serde_json::to_vec(&e.snapshot()).unwrap();
143        let restored = ObjectEngine::new();
144        restored.load_snapshot(serde_json::from_slice(&bytes).unwrap());
145        let (data, meta) = restored.get("media", "img/2.png").unwrap().unwrap();
146        assert_eq!(data, b"\x89PNG\x01");
147        assert_eq!(meta.etag, etag_of(b"\x89PNG\x01"));
148        assert_eq!(restored.bucket_stats("media").unwrap().objects, 3);
149    }
150
151    #[test]
152    fn put_auto_creates_bucket() {
153        let e = ObjectEngine::new();
154        // No create_bucket — the first put makes the bucket.
155        e.put("auto", "k", b"hi".to_vec(), None, serde_json::Value::Null)
156            .unwrap();
157        assert_eq!(e.list_buckets(), vec!["auto"]);
158        assert_eq!(e.get("auto", "k").unwrap().unwrap().0, b"hi");
159        // An invalid bucket name is still rejected (no implicit creation).
160        assert!(matches!(
161            e.put("BAD NAME", "k", vec![], None, serde_json::Value::Null),
162            Err(ObjectError::InvalidBucketName(_))
163        ));
164        assert!(e.bucket_stats("BAD NAME").is_none());
165    }
166
167    // ---- Bucket-name validation --------------------------------------------
168
169    #[test]
170    fn bucket_name_rules() {
171        assert!(valid_bucket_name("my-bucket.1"));
172        assert!(valid_bucket_name("a"));
173        assert!(!valid_bucket_name("")); // empty
174        assert!(!valid_bucket_name("Upper")); // uppercase
175        assert!(!valid_bucket_name("has space"));
176        assert!(!valid_bucket_name("under_score")); // underscore not allowed
177        assert!(!valid_bucket_name(&"x".repeat(64))); // too long (>63)
178        assert!(valid_bucket_name(&"x".repeat(63)));
179    }
180
181    // ---- Payload edge cases -------------------------------------------------
182
183    #[test]
184    fn empty_and_binary_payloads() {
185        let e = ObjectEngine::new();
186        e.create_bucket("b").unwrap();
187        // zero-byte object
188        let m = e
189            .put("b", "empty", vec![], None, serde_json::Value::Null)
190            .unwrap();
191        assert_eq!(m.size, 0);
192        assert_eq!(m.etag, etag_of(&[]));
193        let (d, _) = e.get("b", "empty").unwrap().unwrap();
194        assert!(d.is_empty());
195        // arbitrary non-UTF8 bytes survive intact
196        let raw: Vec<u8> = (0u16..=255).map(|x| x as u8).collect();
197        e.put(
198            "b",
199            "blob",
200            raw.clone(),
201            Some("application/octet-stream".into()),
202            serde_json::Value::Null,
203        )
204        .unwrap();
205        let (back, _) = e.get("b", "blob").unwrap().unwrap();
206        assert_eq!(back, raw);
207    }
208
209    #[test]
210    fn default_content_type_when_none() {
211        let e = ObjectEngine::new();
212        e.create_bucket("b").unwrap();
213        let m = e
214            .put("b", "k", b"x".to_vec(), None, serde_json::Value::Null)
215            .unwrap();
216        assert_eq!(m.content_type, DEFAULT_CONTENT_TYPE);
217    }
218
219    #[test]
220    fn etag_is_content_addressed() {
221        // Same bytes => same etag regardless of key/bucket; different bytes differ.
222        let e = ObjectEngine::new();
223        e.create_bucket("a").unwrap();
224        e.create_bucket("b").unwrap();
225        let m1 = e
226            .put(
227                "a",
228                "k1",
229                b"identical".to_vec(),
230                None,
231                serde_json::Value::Null,
232            )
233            .unwrap();
234        let m2 = e
235            .put(
236                "b",
237                "k2",
238                b"identical".to_vec(),
239                None,
240                serde_json::Value::Null,
241            )
242            .unwrap();
243        assert_eq!(m1.etag, m2.etag);
244        let m3 = e
245            .put(
246                "a",
247                "k3",
248                b"different".to_vec(),
249                None,
250                serde_json::Value::Null,
251            )
252            .unwrap();
253        assert_ne!(m1.etag, m3.etag);
254    }
255
256    // ---- Keys, isolation, listing ------------------------------------------
257
258    #[test]
259    fn keys_with_slashes_and_bucket_isolation() {
260        let e = ObjectEngine::new();
261        e.create_bucket("one").unwrap();
262        e.create_bucket("two").unwrap();
263        e.put(
264            "one",
265            "a/b/c/deep.txt",
266            b"1".to_vec(),
267            None,
268            serde_json::Value::Null,
269        )
270        .unwrap();
271        e.put(
272            "two",
273            "a/b/c/deep.txt",
274            b"2".to_vec(),
275            None,
276            serde_json::Value::Null,
277        )
278        .unwrap();
279        // identical keys in different buckets are independent
280        assert_eq!(e.get("one", "a/b/c/deep.txt").unwrap().unwrap().0, b"1");
281        assert_eq!(e.get("two", "a/b/c/deep.txt").unwrap().unwrap().0, b"2");
282    }
283
284    #[test]
285    fn prefix_listing_variants() {
286        let e = ObjectEngine::new();
287        e.create_bucket("b").unwrap();
288        for k in ["a/1", "a/2", "a/3", "b/1", "c"] {
289            e.put("b", k, b"x".to_vec(), None, serde_json::Value::Null)
290                .unwrap();
291        }
292        assert_eq!(e.list("b", "a/", None).unwrap().len(), 3);
293        assert_eq!(e.list("b", "a/", Some(2)).unwrap().len(), 2); // limit
294        assert_eq!(e.list("b", "", None).unwrap().len(), 5); // all
295        assert!(e.list("b", "zzz", None).unwrap().is_empty()); // no match
296                                                               // results are in lexical key order
297        let keys: Vec<String> = e
298            .list("b", "", None)
299            .unwrap()
300            .into_iter()
301            .map(|m| m.key)
302            .collect();
303        let mut sorted = keys.clone();
304        sorted.sort();
305        assert_eq!(keys, sorted);
306    }
307
308    #[test]
309    fn get_and_delete_missing() {
310        let e = ObjectEngine::new();
311        e.create_bucket("b").unwrap();
312        assert!(e.get("b", "missing").unwrap().is_none());
313        assert!(e.head("b", "missing").unwrap().is_none());
314        assert!(!e.delete("b", "missing").unwrap());
315    }
316
317    #[test]
318    fn bucket_lifecycle_and_stats() {
319        let e = ObjectEngine::new();
320        assert!(matches!(
321            e.create_bucket("UP"),
322            Err(ObjectError::InvalidBucketName(_))
323        ));
324        e.create_bucket("a").unwrap();
325        e.create_bucket("b").unwrap();
326        assert!(matches!(
327            e.create_bucket("a"),
328            Err(ObjectError::BucketExists(_))
329        ));
330        assert_eq!(e.list_buckets(), vec!["a", "b"]);
331        e.put("a", "k", b"hello".to_vec(), None, serde_json::Value::Null)
332            .unwrap();
333        e.put("a", "k2", b"hi".to_vec(), None, serde_json::Value::Null)
334            .unwrap();
335        let s = e.bucket_stats("a").unwrap();
336        assert_eq!(s.objects, 2);
337        assert_eq!(s.bytes, 7); // 5 + 2
338        e.drop_bucket("a").unwrap();
339        assert!(matches!(
340            e.drop_bucket("a"),
341            Err(ObjectError::BucketNotFound(_))
342        ));
343        assert!(e.bucket_stats("a").is_none());
344        // operations on a missing bucket error rather than panic
345        assert!(matches!(
346            e.get("a", "k"),
347            Err(ObjectError::BucketNotFound(_))
348        ));
349        assert!(matches!(
350            e.list("a", "", None),
351            Err(ObjectError::BucketNotFound(_))
352        ));
353    }
354
355    #[test]
356    fn metadata_is_preserved() {
357        let e = ObjectEngine::new();
358        e.create_bucket("b").unwrap();
359        let meta = serde_json::json!({"author": "andrew", "tags": ["a", "b"]});
360        e.put(
361            "b",
362            "k",
363            b"x".to_vec(),
364            Some("text/plain".into()),
365            meta.clone(),
366        )
367        .unwrap();
368        assert_eq!(e.head("b", "k").unwrap().unwrap().metadata, meta);
369    }
370}