neleus_db/
object_store.rs1use 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}