ferro_blob_store/
memory.rs1use std::collections::HashMap;
5use std::sync::{Arc, RwLock};
6
7use async_trait::async_trait;
8use bytes::Bytes;
9
10use crate::{BlobStore, BlobStoreError, Digest, DigestAlgo, Result};
11
12#[derive(Debug, Clone, Default)]
18pub struct InMemoryBlobStore {
19 inner: Arc<RwLock<HashMap<Digest, Bytes>>>,
20}
21
22impl InMemoryBlobStore {
23 #[must_use]
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 #[must_use]
31 pub fn len(&self) -> usize {
32 self.inner.read().expect("poisoned").len()
33 }
34
35 #[must_use]
37 pub fn is_empty(&self) -> bool {
38 self.inner.read().expect("poisoned").is_empty()
39 }
40
41 pub fn clear(&self) {
43 self.inner.write().expect("poisoned").clear();
44 }
45}
46
47#[async_trait]
48impl BlobStore for InMemoryBlobStore {
49 async fn put(&self, digest: &Digest, bytes: Bytes) -> Result<()> {
50 let computed = match digest.algo() {
51 DigestAlgo::Sha256 => Digest::sha256_of(&bytes),
52 DigestAlgo::Sha512 => Digest::sha512_of(&bytes),
53 };
54 if &computed != digest {
55 return Err(BlobStoreError::DigestMismatch {
56 expected: digest.to_string(),
57 computed: computed.to_string(),
58 });
59 }
60 self.inner
61 .write()
62 .expect("poisoned")
63 .insert(digest.clone(), bytes);
64 Ok(())
65 }
66
67 async fn get(&self, digest: &Digest) -> Result<Bytes> {
68 self.inner
69 .read()
70 .expect("poisoned")
71 .get(digest)
72 .cloned()
73 .ok_or_else(|| BlobStoreError::NotFound(digest.to_string()))
74 }
75
76 async fn contains(&self, digest: &Digest) -> Result<bool> {
77 Ok(self.inner.read().expect("poisoned").contains_key(digest))
78 }
79
80 async fn delete(&self, digest: &Digest) -> Result<()> {
81 self.inner.write().expect("poisoned").remove(digest);
82 Ok(())
83 }
84
85 async fn list(&self) -> Result<Vec<Digest>> {
86 Ok(self
87 .inner
88 .read()
89 .expect("poisoned")
90 .keys()
91 .cloned()
92 .collect())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[tokio::test]
101 async fn put_get_round_trip() {
102 let store = InMemoryBlobStore::new();
103 let body = Bytes::from_static(b"hello");
104 let d = Digest::sha256_of(&body);
105 store.put(&d, body.clone()).await.unwrap();
106 assert_eq!(store.get(&d).await.unwrap(), body);
107 }
108
109 #[tokio::test]
110 async fn put_rejects_digest_mismatch() {
111 let store = InMemoryBlobStore::new();
112 let real_body = Bytes::from_static(b"hello");
113 let lying_digest = Digest::sha256_of(b"goodbye");
114 let err = store.put(&lying_digest, real_body).await.unwrap_err();
115 assert!(matches!(err, BlobStoreError::DigestMismatch { .. }));
116 }
117
118 #[tokio::test]
119 async fn contains_and_list() {
120 let store = InMemoryBlobStore::new();
121 let body = Bytes::from_static(b"x");
122 let d = Digest::sha256_of(&body);
123 assert!(!store.contains(&d).await.unwrap());
124 store.put(&d, body).await.unwrap();
125 assert!(store.contains(&d).await.unwrap());
126 let listed = store.list().await.unwrap();
127 assert_eq!(listed, vec![d]);
128 }
129
130 #[tokio::test]
131 async fn get_returns_not_found() {
132 let store = InMemoryBlobStore::new();
133 let d = Digest::sha256_of(b"missing");
134 let err = store.get(&d).await.unwrap_err();
135 assert!(matches!(err, BlobStoreError::NotFound(_)));
136 }
137
138 #[tokio::test]
139 async fn delete_missing_is_ok() {
140 let store = InMemoryBlobStore::new();
141 let d = Digest::sha256_of(b"never-stored");
142 store.delete(&d).await.unwrap();
143 }
144
145 #[tokio::test]
146 async fn clone_shares_storage() {
147 let a = InMemoryBlobStore::new();
148 let b = a.clone();
149 let body = Bytes::from_static(b"shared");
150 let d = Digest::sha256_of(&body);
151 a.put(&d, body.clone()).await.unwrap();
152 assert_eq!(b.get(&d).await.unwrap(), body);
153 assert_eq!(b.len(), 1);
154 b.clear();
155 assert!(a.is_empty());
156 }
157}