use blake3::hash;
use spacetimedb_data_structures::map::{Entry, HashMap};
use spacetimedb_lib::{de::Deserialize, ser::Serialize};
use spacetimedb_memory_usage::MemoryUsage;
#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Serialize, Deserialize)]
pub struct BlobHash {
pub data: [u8; BlobHash::SIZE],
}
impl MemoryUsage for BlobHash {}
impl BlobHash {
pub const SIZE: usize = 32;
pub fn hash_from_bytes(bytes: &[u8]) -> Self {
let data = hash(bytes).into();
Self { data }
}
}
impl TryFrom<&[u8]> for BlobHash {
type Error = ();
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
let data: [u8; Self::SIZE] = data.try_into().map_err(drop)?;
Ok(Self { data })
}
}
#[derive(Debug)]
pub struct NoSuchBlobError;
pub type BlobsIter<'a> = Box<dyn Iterator<Item = (&'a BlobHash, usize, &'a [u8])> + 'a>;
pub trait BlobStore: Sync {
fn clone_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError>;
fn insert_blob(&mut self, bytes: &[u8]) -> BlobHash;
fn insert_with_uses(&mut self, hash: &BlobHash, uses: usize, bytes: Box<[u8]>);
fn retrieve_blob(&self, hash: &BlobHash) -> Result<&[u8], NoSuchBlobError>;
fn free_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError>;
fn iter_blobs(&self) -> BlobsIter<'_>;
fn bytes_used_by_blobs(&self) -> u64 {
self.iter_blobs()
.map(|(_, uses, data)| data.len() as u64 * uses as u64)
.sum()
}
fn num_blobs(&self) -> u64 {
self.iter_blobs().map(|(_, uses, _)| uses as u64).sum()
}
}
#[derive(Default)]
pub struct NullBlobStore;
impl BlobStore for NullBlobStore {
fn clone_blob(&mut self, _hash: &BlobHash) -> Result<(), NoSuchBlobError> {
unimplemented!("NullBlobStore doesn't do anything")
}
fn insert_blob(&mut self, _bytes: &[u8]) -> BlobHash {
unimplemented!("NullBlobStore doesn't do anything")
}
fn insert_with_uses(&mut self, _hash: &BlobHash, _uses: usize, _bytes: Box<[u8]>) {
unimplemented!("NullBlobStore doesn't do anything")
}
fn retrieve_blob(&self, _hash: &BlobHash) -> Result<&[u8], NoSuchBlobError> {
unimplemented!("NullBlobStore doesn't do anything")
}
fn free_blob(&mut self, _hash: &BlobHash) -> Result<(), NoSuchBlobError> {
unimplemented!("NullBlobStore doesn't do anything")
}
fn iter_blobs(&self) -> BlobsIter<'_> {
unimplemented!("NullBlobStore doesn't do anything")
}
}
#[derive(Default, PartialEq, Eq, Debug)]
pub struct HashMapBlobStore {
map: HashMap<BlobHash, BlobObject>,
}
impl MemoryUsage for HashMapBlobStore {
fn heap_usage(&self) -> usize {
let Self { map } = self;
map.heap_usage()
}
}
#[derive(PartialEq, Eq, Debug)]
struct BlobObject {
uses: usize,
blob: Box<[u8]>,
}
impl MemoryUsage for BlobObject {
fn heap_usage(&self) -> usize {
let Self { uses, blob } = self;
uses.heap_usage() + blob.heap_usage()
}
}
impl BlobStore for HashMapBlobStore {
fn clone_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError> {
self.map.get_mut(hash).ok_or(NoSuchBlobError)?.uses += 1;
Ok(())
}
fn insert_blob(&mut self, bytes: &[u8]) -> BlobHash {
let hash = BlobHash::hash_from_bytes(bytes);
self.map
.entry(hash)
.and_modify(|v| v.uses += 1)
.or_insert_with(|| BlobObject {
blob: bytes.into(),
uses: 1,
});
hash
}
fn insert_with_uses(&mut self, hash: &BlobHash, uses: usize, bytes: Box<[u8]>) {
debug_assert_eq!(hash, &BlobHash::hash_from_bytes(&bytes));
self.map
.entry(*hash)
.and_modify(|v| v.uses += uses)
.or_insert_with(|| BlobObject { blob: bytes, uses });
}
fn retrieve_blob(&self, hash: &BlobHash) -> Result<&[u8], NoSuchBlobError> {
self.map.get(hash).map(|obj| &*obj.blob).ok_or(NoSuchBlobError)
}
fn free_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError> {
match self.map.entry(*hash) {
Entry::Vacant(_) => return Err(NoSuchBlobError),
Entry::Occupied(entry) if entry.get().uses == 1 => drop(entry.remove()),
Entry::Occupied(mut entry) => entry.get_mut().uses -= 1,
}
Ok(())
}
fn iter_blobs(&self) -> BlobsIter<'_> {
Box::new(self.map.iter().map(|(hash, obj)| (hash, obj.uses, &obj.blob[..])))
}
}
#[cfg(test)]
impl HashMapBlobStore {
fn iter(&self) -> impl Iterator<Item = (&BlobHash, usize, &[u8])> + '_ {
self.map.iter().map(|(hash, obj)| (hash, obj.uses, &*obj.blob))
}
pub fn usage_counter(&self) -> HashMap<BlobHash, usize> {
self.iter().map(|(hash, uses, _)| (*hash, uses)).collect()
}
}