Skip to main content

neleus_db/
object_store.rs

1use std::path::PathBuf;
2
3use anyhow::{Result, anyhow};
4use serde::{Serialize, de::DeserializeOwned};
5
6use crate::canonical::{from_cbor, to_cbor};
7use crate::cas::CasStore;
8use crate::hash::{Hash, hash_typed};
9
10#[derive(Clone, Debug)]
11pub struct ObjectStore {
12    cas: CasStore,
13    verify_on_read: bool,
14}
15
16impl ObjectStore {
17    pub fn new(root: impl Into<PathBuf>) -> Self {
18        Self {
19            cas: CasStore::new(root),
20            verify_on_read: false,
21        }
22    }
23
24    pub fn with_options(root: impl Into<PathBuf>, verify_on_read: bool) -> Self {
25        Self {
26            cas: CasStore::new(root),
27            verify_on_read,
28        }
29    }
30
31    pub fn verify_on_read(&self) -> bool {
32        self.verify_on_read
33    }
34
35    pub fn ensure_dir(&self) -> Result<()> {
36        self.cas.ensure_dir()
37    }
38
39    pub fn put_typed_bytes(&self, tag: &[u8], bytes: &[u8]) -> Result<Hash> {
40        let hash = hash_typed(tag, bytes);
41        self.cas.put_existing_hash(hash, bytes)?;
42        Ok(hash)
43    }
44
45    pub fn get_bytes(&self, hash: Hash) -> Result<Vec<u8>> {
46        self.cas.get(hash)
47    }
48
49    pub fn get_typed_bytes(&self, tag: &[u8], hash: Hash) -> Result<Vec<u8>> {
50        let bytes = self.cas.get(hash)?;
51        if self.verify_on_read {
52            let computed = hash_typed(tag, &bytes);
53            if computed != hash {
54                return Err(anyhow!(
55                    "object hash mismatch for {} (computed {})",
56                    hash,
57                    computed
58                ));
59            }
60        }
61        Ok(bytes)
62    }
63
64    pub fn exists(&self, hash: Hash) -> bool {
65        self.cas.exists(hash)
66    }
67
68    pub fn put_serialized<T: Serialize>(&self, tag: &[u8], value: &T) -> Result<Hash> {
69        let bytes = to_cbor(value)?;
70        self.put_typed_bytes(tag, &bytes)
71    }
72
73    pub fn get_deserialized<T: DeserializeOwned>(&self, hash: Hash) -> Result<T> {
74        let bytes = self.get_bytes(hash)?;
75        from_cbor(&bytes)
76    }
77
78    pub fn get_deserialized_typed<T: DeserializeOwned>(&self, tag: &[u8], hash: Hash) -> Result<T> {
79        let bytes = self.get_typed_bytes(tag, hash)?;
80        from_cbor(&bytes)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use std::fs;
87
88    use serde::{Deserialize, Serialize};
89    use tempfile::TempDir;
90
91    use super::*;
92    use crate::hash::hash_typed;
93
94    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95    struct Obj {
96        x: u32,
97    }
98
99    #[test]
100    fn object_store_serialized_roundtrip() {
101        let dir = TempDir::new().unwrap();
102        let store = ObjectStore::new(dir.path());
103        store.ensure_dir().unwrap();
104
105        let hash = store.put_serialized(b"manifest:", &Obj { x: 7 }).unwrap();
106        let out: Obj = store.get_deserialized_typed(b"manifest:", hash).unwrap();
107        assert_eq!(out.x, 7);
108    }
109
110    #[test]
111    fn object_store_is_deterministic_for_same_object() {
112        let dir = TempDir::new().unwrap();
113        let store = ObjectStore::new(dir.path());
114        store.ensure_dir().unwrap();
115
116        let a = store.put_serialized(b"manifest:", &Obj { x: 1 }).unwrap();
117        let b = store.put_serialized(b"manifest:", &Obj { x: 1 }).unwrap();
118        assert_eq!(a, b);
119    }
120
121    #[test]
122    fn object_store_domain_separator_changes_hash() {
123        let dir = TempDir::new().unwrap();
124        let store = ObjectStore::new(dir.path());
125        store.ensure_dir().unwrap();
126
127        let bytes = crate::canonical::to_cbor(&Obj { x: 1 }).unwrap();
128        let a = store.put_typed_bytes(b"manifest:", &bytes).unwrap();
129        let b = store.put_typed_bytes(b"commit:", &bytes).unwrap();
130        assert_ne!(a, b);
131    }
132
133    #[test]
134    fn typed_read_verification_detects_corruption() {
135        let dir = TempDir::new().unwrap();
136        let store = ObjectStore::with_options(dir.path(), true);
137        store.ensure_dir().unwrap();
138
139        let hash = store.put_serialized(b"manifest:", &Obj { x: 7 }).unwrap();
140
141        let cas = CasStore::new(dir.path());
142        let path = cas.path_for(hash);
143        fs::write(path, b"tampered").unwrap();
144
145        assert!(
146            store
147                .get_deserialized_typed::<Obj>(b"manifest:", hash)
148                .is_err()
149        );
150    }
151
152    #[test]
153    fn typed_hash_matches_expected() {
154        let dir = TempDir::new().unwrap();
155        let store = ObjectStore::new(dir.path());
156        store.ensure_dir().unwrap();
157
158        let bytes = crate::canonical::to_cbor(&Obj { x: 9 }).unwrap();
159        let expected = hash_typed(b"commit:", &bytes);
160        let h = store.put_typed_bytes(b"commit:", &bytes).unwrap();
161        assert_eq!(h, expected);
162    }
163}