1use std::fs;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19use anyhow::{bail, Context, Result};
20use serde::{Deserialize, Serialize};
21use blake2::{Blake2b512, Digest};
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Node {
26 pub id: String,
28 pub coll: String,
30 pub seq: u64,
32 pub data: serde_json::Value,
34 pub prev: Option<String>,
36 pub caused_by: Vec<String>,
38 pub ts: f64,
40 pub valid_from: Option<String>,
42 pub valid_to: Option<String>,
44 #[serde(skip_serializing_if = "String::is_empty", default)]
46 pub hash: String,
47}
48
49#[derive(Clone)]
52pub struct Dek(pub [u8; 32]);
53
54impl Dek {
55 pub fn from_tmk(tmk: &[u8; 32], salt: &[u8]) -> Self {
56 use sha2::{Sha256, Digest as _};
57 let mut h = Sha256::new();
58 h.update(tmk);
59 h.update(salt);
60 let result = h.finalize();
61 let mut key = [0u8; 32];
62 key.copy_from_slice(&result[..32]);
63 Dek(key)
64 }
65}
66
67fn blake2b(data: &[u8]) -> String {
68 let mut h = Blake2b512::new();
69 h.update(data);
70 hex::encode(&h.finalize()[..32]) }
72
73fn dag_v3_enabled() -> bool {
76 std::env::var("NEDB_DAG_V3")
77 .map(|v| {
78 let v = v.trim();
79 v == "1" || v.eq_ignore_ascii_case("true")
80 || v.eq_ignore_ascii_case("on")
81 || v.eq_ignore_ascii_case("yes")
82 })
83 .unwrap_or(false)
84}
85
86fn encrypt(data: &[u8], dek: &Dek) -> Result<Vec<u8>> {
87 use aes_gcm::{Aes256Gcm, KeyInit, aead::{Aead, OsRng, rand_core::RngCore}};
88 let cipher = Aes256Gcm::new_from_slice(&dek.0)?;
89 let mut nonce_bytes = [0u8; 12];
90 OsRng.fill_bytes(&mut nonce_bytes);
91 let nonce = aes_gcm::Nonce::from(nonce_bytes);
92 let ciphertext = cipher.encrypt(&nonce, data)
93 .map_err(|e| anyhow::anyhow!("encrypt failed: {:?}", e))?;
94 let mut out = nonce_bytes.to_vec();
96 out.extend_from_slice(&ciphertext);
97 Ok(out)
98}
99
100fn decrypt(data: &[u8], dek: &Dek) -> Result<Vec<u8>> {
101 use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead};
102 if data.len() < 12 { bail!("ciphertext too short"); }
103 let (nonce_bytes, ciphertext) = data.split_at(12);
104 let cipher = Aes256Gcm::new_from_slice(&dek.0)?;
105 let nonce = aes_gcm::Nonce::from_slice(nonce_bytes);
106 cipher.decrypt(nonce, ciphertext)
107 .map_err(|e| anyhow::anyhow!("decrypt failed: {:?}", e))
108}
109
110pub struct ObjectStore {
112 root: PathBuf,
113 dek: Option<Dek>,
114 mem: Option<Arc<dashmap::DashMap<String, Vec<u8>>>>,
116 seg: Option<crate::segment::SegmentStore>,
119}
120
121impl ObjectStore {
122 pub fn new(db_root: &Path, dek: Option<Dek>) -> Result<Self> {
123 let root = db_root.join("objects");
124 fs::create_dir_all(&root)
125 .context("create objects/ dir")?;
126 let seg = if dag_v3_enabled() {
129 Some(crate::segment::SegmentStore::open(&root)?)
130 } else {
131 None
132 };
133 Ok(Self { root, dek, mem: None, seg })
134 }
135
136 pub fn in_memory() -> Self {
138 Self {
139 root: PathBuf::from(":memory:"),
140 dek: None,
141 mem: Some(Arc::new(dashmap::DashMap::new())),
142 seg: None,
143 }
144 }
145
146 pub fn write(&self, node: &mut Node) -> Result<String> {
148 node.hash = String::new();
156 let raw = serde_json::to_vec(node)?;
157 let content = match &self.dek {
158 Some(dek) => encrypt(&raw, dek)?,
159 None => raw,
160 };
161 let hash = blake2b(&content);
162
163 if let Some(ref mem) = self.mem {
164 mem.entry(hash.clone()).or_insert_with(|| content);
166 } else if let Some(ref seg) = self.seg {
167 seg.put(&hash, &content)?;
169 } else {
170 let dir = self.root.join(&hash[..2]);
172 fs::create_dir_all(&dir)?;
173 let path = dir.join(&hash[2..]);
174 if !path.exists() {
175 let tmp = path.with_extension("tmp");
176 fs::write(&tmp, &content)?;
177 fs::rename(&tmp, &path)?;
178 }
179 }
180 node.hash = hash.clone();
181 Ok(hash)
182 }
183
184 pub fn read(&self, hash: &str) -> Result<Node> {
186 if hash.len() < 3 {
187 anyhow::bail!("invalid object hash (too short): {:?}", hash);
188 }
189
190 if let Some(ref mem) = self.mem {
192 let content = mem.get(hash)
193 .map(|v| v.clone())
194 .ok_or_else(|| anyhow::anyhow!("object not found in memory: {}", hash))?;
195 return self.decode(content, hash);
196 }
197
198 if let Some(ref seg) = self.seg {
201 if let Some(content) = seg.get(hash)? {
202 return self.decode(content, hash);
203 }
204 }
206
207 let path = self.root.join(&hash[..2]).join(&hash[2..]);
209 let c = fs::read(&path).with_context(|| format!("read object {}", hash))?;
210 let actual = blake2b(&c);
212 if actual != hash {
213 bail!("object {} tampered: expected {} got {}", hash, hash, actual);
214 }
215 self.decode(c, hash)
216 }
217
218 fn decode(&self, content: Vec<u8>, hash: &str) -> Result<Node> {
222 let raw = match &self.dek {
223 Some(dek) => decrypt(&content, dek)?,
224 None => content,
225 };
226 let mut node: Node = serde_json::from_slice(&raw)
227 .context("deserialize node")?;
228 if node.hash.is_empty() {
229 node.hash = hash.to_string();
230 }
231 Ok(node)
232 }
233
234 pub fn all_hashes(&self) -> Box<dyn Iterator<Item = String> + '_> {
236 if let Some(ref mem) = self.mem {
238 let hashes: Vec<String> = mem.iter().map(|e| e.key().clone()).collect();
239 return Box::new(hashes.into_iter());
240 }
241
242 if let Some(ref seg) = self.seg {
245 let mut seen: std::collections::HashSet<String> =
246 seg.all_hashes().into_iter().collect();
247 if let Ok(rd) = fs::read_dir(&self.root) {
248 for prefix_dir in rd.flatten() {
249 if !prefix_dir.file_type().map(|t| t.is_dir()).unwrap_or(false) { continue; }
250 let prefix = prefix_dir.file_name().to_string_lossy().to_string();
251 if prefix.len() != 2 { continue; } if let Ok(rd2) = fs::read_dir(prefix_dir.path()) {
253 for e in rd2.flatten() {
254 let name = e.file_name().to_string_lossy().to_string();
255 if name.ends_with(".tmp") { continue; }
256 seen.insert(format!("{}{}", prefix, name));
257 }
258 }
259 }
260 }
261 return Box::new(seen.into_iter());
262 }
263
264 let root = self.root.clone();
266 Box::new(fs::read_dir(&root)
267 .into_iter()
268 .flatten()
269 .filter_map(|e| e.ok())
270 .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
271 .flat_map(move |prefix_dir| {
272 let prefix = prefix_dir.file_name().to_string_lossy().to_string();
273 fs::read_dir(prefix_dir.path())
274 .into_iter()
275 .flatten()
276 .filter_map(|e| e.ok())
277 .filter_map(move |e| {
278 let name = e.file_name().to_string_lossy().to_string();
279 if name.ends_with(".tmp") { return None; }
280 Some(format!("{}{}", prefix, name))
281 })
282 }))
283 }
284
285 pub fn sync(&self) -> Result<()> {
288 if let Some(ref seg) = self.seg {
289 seg.sync()?;
290 }
291 Ok(())
292 }
293
294 pub fn compact(&self, live: &std::collections::HashSet<String>) -> Result<crate::segment::CompactStats> {
298 match self.seg {
299 Some(ref seg) => seg.compact(live),
300 None => Ok(crate::segment::CompactStats::default()),
301 }
302 }
303
304 pub fn verify_all(&self) -> (usize, Vec<String>) {
306 use rayon::prelude::*;
307 let hashes: Vec<String> = self.all_hashes().collect();
308 let results: Vec<(bool, String)> = hashes.par_iter().map(|h| {
309 (self.read(h).is_ok(), h.clone())
310 }).collect();
311 let ok = results.iter().filter(|(ok, _)| *ok).count();
312 let bad: Vec<String> = results.into_iter()
313 .filter(|(ok, _)| !*ok)
314 .map(|(_, h)| h)
315 .collect();
316 (ok, bad)
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use tempfile::tempdir;
324
325 fn make_node(id: &str, coll: &str, seq: u64) -> Node {
326 Node {
327 id: id.to_string(), coll: coll.to_string(), seq,
328 data: serde_json::json!({"height": seq, "hash": "0000abc"}),
329 prev: None, caused_by: vec![], ts: 1718400000.0,
330 valid_from: None, valid_to: None, hash: String::new(),
331 }
332 }
333
334 #[test]
335 fn write_read_roundtrip() {
336 let dir = tempdir().unwrap();
337 let store = ObjectStore::new(dir.path(), None).unwrap();
338 let mut node = make_node("1", "blocks", 1);
339 let hash = store.write(&mut node).unwrap();
340 assert_eq!(hash.len(), 64);
341 let read_back = store.read(&hash).unwrap();
342 assert_eq!(read_back.id, "1");
343 assert_eq!(read_back.coll, "blocks");
344 }
345
346 #[test]
347 fn write_is_idempotent() {
348 let dir = tempdir().unwrap();
349 let store = ObjectStore::new(dir.path(), None).unwrap();
350 let mut node = make_node("1", "blocks", 1);
351 let h1 = store.write(&mut node).unwrap();
352 let h2 = store.write(&mut node).unwrap();
353 assert_eq!(h1, h2);
354 }
355
356 #[test]
357 fn tamper_detected() {
358 let dir = tempdir().unwrap();
359 let store = ObjectStore::new(dir.path(), None).unwrap();
360 let mut node = make_node("1", "blocks", 1);
361 let hash = store.write(&mut node).unwrap();
362 let path = dir.path().join("objects").join(&hash[..2]).join(&hash[2..]);
364 let mut content = fs::read(&path).unwrap();
365 content[10] ^= 0xff;
366 fs::write(&path, content).unwrap();
367 assert!(store.read(&hash).is_err());
368 }
369}