shape_runtime/
blob_store.rs1use std::collections::HashMap;
11use std::path::PathBuf;
12
13pub trait BlobStore: Send + Sync {
15 fn get(&self, hash: &[u8; 32]) -> Option<Vec<u8>>;
17
18 fn put(&self, hash: [u8; 32], data: Vec<u8>) -> bool;
21
22 fn contains(&self, hash: &[u8; 32]) -> bool;
24}
25
26pub struct MemoryBlobStore {
32 blobs: parking_lot::RwLock<HashMap<[u8; 32], Vec<u8>>>,
33}
34
35impl MemoryBlobStore {
36 pub fn new() -> Self {
37 Self {
38 blobs: parking_lot::RwLock::new(HashMap::new()),
39 }
40 }
41}
42
43impl Default for MemoryBlobStore {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl BlobStore for MemoryBlobStore {
50 fn get(&self, hash: &[u8; 32]) -> Option<Vec<u8>> {
51 self.blobs.read().get(hash).cloned()
52 }
53
54 fn put(&self, hash: [u8; 32], data: Vec<u8>) -> bool {
55 use std::collections::hash_map::Entry;
56 match self.blobs.write().entry(hash) {
57 Entry::Occupied(_) => false,
58 Entry::Vacant(e) => {
59 e.insert(data);
60 true
61 }
62 }
63 }
64
65 fn contains(&self, hash: &[u8; 32]) -> bool {
66 self.blobs.read().contains_key(hash)
67 }
68}
69
70pub struct FsBlobStore {
79 root: PathBuf,
80}
81
82impl FsBlobStore {
83 pub fn new(root: PathBuf) -> std::io::Result<Self> {
87 std::fs::create_dir_all(&root)?;
88 Ok(Self { root })
89 }
90
91 fn blob_path(&self, hash: &[u8; 32]) -> PathBuf {
93 let hex = hex::encode(hash);
94 self.root
95 .join(&hex[..2])
96 .join(format!("{}.blob", &hex[2..]))
97 }
98}
99
100impl BlobStore for FsBlobStore {
101 fn get(&self, hash: &[u8; 32]) -> Option<Vec<u8>> {
102 let path = self.blob_path(hash);
103 std::fs::read(&path).ok()
104 }
105
106 fn put(&self, hash: [u8; 32], data: Vec<u8>) -> bool {
107 let path = self.blob_path(&hash);
108 if path.exists() {
109 return false;
110 }
111 if let Some(parent) = path.parent() {
112 if std::fs::create_dir_all(parent).is_err() {
113 return false;
114 }
115 }
116 std::fs::write(&path, &data).is_ok()
117 }
118
119 fn contains(&self, hash: &[u8; 32]) -> bool {
120 self.blob_path(hash).exists()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_memory_blob_store_put_get() {
130 let store = MemoryBlobStore::new();
131 let hash = [0xAB; 32];
132 let data = vec![1, 2, 3, 4, 5];
133
134 assert!(!store.contains(&hash));
135 assert!(store.put(hash, data.clone()));
136 assert!(store.contains(&hash));
137 assert_eq!(store.get(&hash), Some(data));
138 }
139
140 #[test]
141 fn test_memory_blob_store_duplicate_put() {
142 let store = MemoryBlobStore::new();
143 let hash = [0xCD; 32];
144
145 assert!(store.put(hash, vec![1, 2]));
146 assert!(!store.put(hash, vec![3, 4]));
147 assert_eq!(store.get(&hash), Some(vec![1, 2]));
149 }
150
151 #[test]
152 fn test_memory_blob_store_missing_key() {
153 let store = MemoryBlobStore::new();
154 assert_eq!(store.get(&[0xFF; 32]), None);
155 assert!(!store.contains(&[0xFF; 32]));
156 }
157
158 #[test]
159 fn test_fs_blob_store_put_get() {
160 let tmp = tempfile::tempdir().expect("temp dir");
161 let store = FsBlobStore::new(tmp.path().to_path_buf()).expect("create store");
162
163 let hash = [0x12; 32];
164 let data = vec![10, 20, 30];
165
166 assert!(!store.contains(&hash));
167 assert!(store.put(hash, data.clone()));
168 assert!(store.contains(&hash));
169 assert_eq!(store.get(&hash), Some(data));
170 }
171
172 #[test]
173 fn test_fs_blob_store_duplicate_put() {
174 let tmp = tempfile::tempdir().expect("temp dir");
175 let store = FsBlobStore::new(tmp.path().to_path_buf()).expect("create store");
176
177 let hash = [0x34; 32];
178 assert!(store.put(hash, vec![1]));
179 assert!(!store.put(hash, vec![2]));
180 assert_eq!(store.get(&hash), Some(vec![1]));
181 }
182
183 #[test]
184 fn test_fs_blob_store_path_layout() {
185 let tmp = tempfile::tempdir().expect("temp dir");
186 let store = FsBlobStore::new(tmp.path().to_path_buf()).expect("create store");
187
188 let mut hash = [0u8; 32];
189 hash[0] = 0xAB;
190 hash[1] = 0xCD;
191 let path = store.blob_path(&hash);
194 let path_str = path.to_string_lossy();
195
196 assert!(
198 path_str.contains("/ab/"),
199 "expected /ab/ in path, got: {}",
200 path_str
201 );
202 assert!(
203 path_str.ends_with(".blob"),
204 "expected .blob suffix, got: {}",
205 path_str
206 );
207 }
208
209 #[test]
210 fn test_fs_blob_store_missing_key() {
211 let tmp = tempfile::tempdir().expect("temp dir");
212 let store = FsBlobStore::new(tmp.path().to_path_buf()).expect("create store");
213 assert_eq!(store.get(&[0xFF; 32]), None);
214 assert!(!store.contains(&[0xFF; 32]));
215 }
216}