1use celln_manifest::Hash;
11use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, thiserror::Error)]
16pub enum StoreError {
17 #[error("io error: {0}")]
18 Io(#[from] io::Error),
19 #[error("object not found: {0}")]
20 NotFound(Hash),
21 #[error("integrity failure for {expected}: bytes on disk hash to {actual}")]
22 Integrity { expected: Hash, actual: Hash },
23 #[error("hash {0} is not in the expected blake3:<hex> form")]
24 BadHash(String),
25}
26
27pub struct Store {
32 objects: PathBuf,
33}
34
35impl Store {
36 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
38 let objects = root.as_ref().join("objects");
39 fs::create_dir_all(&objects)?;
40 Ok(Store { objects })
41 }
42
43 fn hex_of(hash: &Hash) -> Result<&str, StoreError> {
44 hash.0
45 .strip_prefix("blake3:")
46 .ok_or_else(|| StoreError::BadHash(hash.0.clone()))
47 }
48
49 fn path_for(&self, hash: &Hash) -> Result<PathBuf, StoreError> {
50 let hex = Self::hex_of(hash)?;
51 let (fanout, _) = hex.split_at(2.min(hex.len()));
52 Ok(self.objects.join(fanout).join(hex))
53 }
54
55 pub fn put(&self, bytes: &[u8]) -> Result<Hash, StoreError> {
58 let hash = Hash::of(bytes);
59 let path = self.path_for(&hash)?;
60 if path.exists() {
61 return Ok(hash); }
63 if let Some(parent) = path.parent() {
64 fs::create_dir_all(parent)?;
65 }
66 let tmp = path.with_extension("tmp");
68 fs::write(&tmp, bytes)?;
69 fs::rename(&tmp, &path)?;
70 Ok(hash)
71 }
72
73 pub fn get(&self, hash: &Hash) -> Result<Vec<u8>, StoreError> {
75 let path = self.path_for(hash)?;
76 if !path.exists() {
77 return Err(StoreError::NotFound(hash.clone()));
78 }
79 let bytes = fs::read(&path)?;
80 let actual = Hash::of(&bytes);
81 if &actual != hash {
82 return Err(StoreError::Integrity {
83 expected: hash.clone(),
84 actual,
85 });
86 }
87 Ok(bytes)
88 }
89
90 pub fn has(&self, hash: &Hash) -> bool {
91 self.path_for(hash).map(|p| p.exists()).unwrap_or(false)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use tempfile::tempdir;
99
100 #[test]
101 fn put_get_roundtrip() {
102 let dir = tempdir().unwrap();
103 let store = Store::open(dir.path()).unwrap();
104 let h = store.put(b"print(1+1)").unwrap();
105 assert!(store.has(&h));
106 assert_eq!(store.get(&h).unwrap(), b"print(1+1)");
107 }
108
109 #[test]
110 fn put_dedups() {
111 let dir = tempdir().unwrap();
112 let store = Store::open(dir.path()).unwrap();
113 let a = store.put(b"same").unwrap();
114 let b = store.put(b"same").unwrap();
115 assert_eq!(a, b); }
117
118 #[test]
119 fn missing_object_is_not_found() {
120 let dir = tempdir().unwrap();
121 let store = Store::open(dir.path()).unwrap();
122 let ghost = Hash::of(b"never stored");
123 assert!(matches!(store.get(&ghost), Err(StoreError::NotFound(_))));
124 }
125
126 #[test]
127 fn corruption_is_caught_on_read() {
128 let dir = tempdir().unwrap();
129 let store = Store::open(dir.path()).unwrap();
130 let h = store.put(b"trusted tool bytes").unwrap();
131 let path = store.path_for(&h).unwrap();
133 fs::write(&path, b"trojaned bytes").unwrap();
134 assert!(matches!(store.get(&h), Err(StoreError::Integrity { .. })));
135 }
136}