spacetimedb_table/
blob_store.rs1use blake3::hash;
15use spacetimedb_data_structures::map::{Entry, HashMap};
16use spacetimedb_lib::{de::Deserialize, ser::Serialize};
17
18use crate::MemoryUsage;
19
20#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Serialize, Deserialize)]
22pub struct BlobHash {
23 pub data: [u8; BlobHash::SIZE],
27}
28
29impl MemoryUsage for BlobHash {}
30
31impl BlobHash {
32 pub const SIZE: usize = 32;
34
35 pub fn hash_from_bytes(bytes: &[u8]) -> Self {
37 let data = hash(bytes).into();
38 Self { data }
39 }
40}
41
42impl TryFrom<&[u8]> for BlobHash {
43 type Error = ();
44
45 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
46 let data: [u8; Self::SIZE] = data.try_into().map_err(drop)?;
47 Ok(Self { data })
48 }
49}
50
51#[derive(Debug)]
53pub struct NoSuchBlobError;
54
55pub type BlobsIter<'a> = Box<dyn Iterator<Item = (&'a BlobHash, usize, &'a [u8])> + 'a>;
62
63pub trait BlobStore: Sync {
68 fn clone_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError>;
76
77 fn insert_blob(&mut self, bytes: &[u8]) -> BlobHash;
82
83 fn insert_with_uses(&mut self, hash: &BlobHash, uses: usize, bytes: Box<[u8]>);
87
88 fn retrieve_blob(&self, hash: &BlobHash) -> Result<&[u8], NoSuchBlobError>;
90
91 fn free_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError>;
97
98 fn iter_blobs(&self) -> BlobsIter<'_>;
107
108 fn bytes_used_by_blobs(&self) -> u64 {
115 self.iter_blobs()
116 .map(|(_, uses, data)| data.len() as u64 * uses as u64)
117 .sum()
118 }
119
120 fn num_blobs(&self) -> u64 {
126 self.iter_blobs().map(|(_, uses, _)| uses as u64).sum()
127 }
128}
129
130#[derive(Default)]
133pub struct NullBlobStore;
134
135impl BlobStore for NullBlobStore {
136 fn clone_blob(&mut self, _hash: &BlobHash) -> Result<(), NoSuchBlobError> {
137 unimplemented!("NullBlobStore doesn't do anything")
138 }
139
140 fn insert_blob(&mut self, _bytes: &[u8]) -> BlobHash {
141 unimplemented!("NullBlobStore doesn't do anything")
142 }
143
144 fn insert_with_uses(&mut self, _hash: &BlobHash, _uses: usize, _bytes: Box<[u8]>) {
145 unimplemented!("NullBlobStore doesn't do anything")
146 }
147
148 fn retrieve_blob(&self, _hash: &BlobHash) -> Result<&[u8], NoSuchBlobError> {
149 unimplemented!("NullBlobStore doesn't do anything")
150 }
151
152 fn free_blob(&mut self, _hash: &BlobHash) -> Result<(), NoSuchBlobError> {
153 unimplemented!("NullBlobStore doesn't do anything")
154 }
155
156 fn iter_blobs(&self) -> BlobsIter<'_> {
157 unimplemented!("NullBlobStore doesn't do anything")
158 }
159}
160
161#[derive(Default, PartialEq, Eq, Debug)]
164pub struct HashMapBlobStore {
165 map: HashMap<BlobHash, BlobObject>,
168}
169
170impl MemoryUsage for HashMapBlobStore {
171 fn heap_usage(&self) -> usize {
172 let Self { map } = self;
173 map.heap_usage()
174 }
175}
176
177#[derive(PartialEq, Eq, Debug)]
179struct BlobObject {
180 uses: usize,
182 blob: Box<[u8]>,
184}
185
186impl MemoryUsage for BlobObject {
187 fn heap_usage(&self) -> usize {
188 let Self { uses, blob } = self;
189 uses.heap_usage() + blob.heap_usage()
190 }
191}
192
193impl BlobStore for HashMapBlobStore {
194 fn clone_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError> {
195 self.map.get_mut(hash).ok_or(NoSuchBlobError)?.uses += 1;
196 Ok(())
197 }
198
199 fn insert_blob(&mut self, bytes: &[u8]) -> BlobHash {
200 let hash = BlobHash::hash_from_bytes(bytes);
201 self.map
202 .entry(hash)
203 .and_modify(|v| v.uses += 1)
204 .or_insert_with(|| BlobObject {
205 blob: bytes.into(),
206 uses: 1,
207 });
208 hash
209 }
210
211 fn insert_with_uses(&mut self, hash: &BlobHash, uses: usize, bytes: Box<[u8]>) {
212 debug_assert_eq!(hash, &BlobHash::hash_from_bytes(&bytes));
213 self.map
214 .entry(*hash)
215 .and_modify(|v| v.uses += uses)
216 .or_insert_with(|| BlobObject { blob: bytes, uses });
217 }
218
219 fn retrieve_blob(&self, hash: &BlobHash) -> Result<&[u8], NoSuchBlobError> {
220 self.map.get(hash).map(|obj| &*obj.blob).ok_or(NoSuchBlobError)
221 }
222
223 fn free_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError> {
224 match self.map.entry(*hash) {
225 Entry::Vacant(_) => return Err(NoSuchBlobError),
226 Entry::Occupied(entry) if entry.get().uses == 1 => drop(entry.remove()),
227 Entry::Occupied(mut entry) => entry.get_mut().uses -= 1,
228 }
229 Ok(())
230 }
231
232 fn iter_blobs(&self) -> BlobsIter<'_> {
233 Box::new(self.map.iter().map(|(hash, obj)| (hash, obj.uses, &obj.blob[..])))
234 }
235}
236
237#[cfg(test)]
238impl HashMapBlobStore {
239 fn iter(&self) -> impl Iterator<Item = (&BlobHash, usize, &[u8])> + '_ {
241 self.map.iter().map(|(hash, obj)| (hash, obj.uses, &*obj.blob))
242 }
243
244 pub fn usage_counter(&self) -> HashMap<BlobHash, usize> {
246 self.iter().map(|(hash, uses, _)| (*hash, uses)).collect()
247 }
248}