Skip to main content

bcp_encoder/
content_store.rs

1use std::collections::HashMap;
2use std::sync::RwLock;
3
4use bcp_types::content_store::ContentStore;
5
6/// In-memory content store backed by a `HashMap`.
7///
8/// Suitable for the PoC and testing. Not persisted across runs.
9/// Uses [`RwLock`] for interior mutability so that the
10/// [`ContentStore`] trait methods (which take `&self`) can mutate
11/// the internal map safely across threads.
12///
13/// # Concurrency
14///
15/// Read operations (`get`, `contains`) acquire a read lock.
16/// Write operations (`put`) acquire a write lock. Multiple
17/// concurrent readers are allowed; writers are exclusive.
18///
19/// # Example
20///
21/// ```rust
22/// use bcp_encoder::MemoryContentStore;
23/// use bcp_types::ContentStore;
24///
25/// let store = MemoryContentStore::new();
26/// let data = b"fn main() {}";
27/// let hash = store.put(data);
28/// assert_eq!(store.get(&hash).unwrap(), data);
29/// assert!(store.contains(&hash));
30/// assert_eq!(store.len(), 1);
31/// ```
32pub struct MemoryContentStore {
33    store: RwLock<HashMap<[u8; 32], Vec<u8>>>,
34}
35
36impl MemoryContentStore {
37    /// Create an empty in-memory content store.
38    #[must_use]
39    pub fn new() -> Self {
40        Self {
41            store: RwLock::new(HashMap::new()),
42        }
43    }
44
45    /// Return the number of unique entries in the store.
46    #[must_use]
47    pub fn len(&self) -> usize {
48        self.store
49            .read()
50            .expect("content store lock poisoned")
51            .len()
52    }
53
54    /// Return `true` if the store contains no entries.
55    #[must_use]
56    pub fn is_empty(&self) -> bool {
57        self.len() == 0
58    }
59
60    /// Return the total bytes stored across all entries.
61    ///
62    /// This counts only the content bytes, not the 32-byte hash
63    /// keys or `HashMap` overhead.
64    #[must_use]
65    pub fn total_bytes(&self) -> usize {
66        self.store
67            .read()
68            .expect("content store lock poisoned")
69            .values()
70            .map(Vec::len)
71            .sum()
72    }
73}
74
75impl Default for MemoryContentStore {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl ContentStore for MemoryContentStore {
82    fn get(&self, hash: &[u8; 32]) -> Option<Vec<u8>> {
83        self.store
84            .read()
85            .expect("content store lock poisoned")
86            .get(hash)
87            .cloned()
88    }
89
90    fn put(&self, content: &[u8]) -> [u8; 32] {
91        let hash: [u8; 32] = blake3::hash(content).into();
92        let mut store = self.store.write().expect("content store lock poisoned");
93        store.entry(hash).or_insert_with(|| content.to_vec());
94        hash
95    }
96
97    fn contains(&self, hash: &[u8; 32]) -> bool {
98        self.store
99            .read()
100            .expect("content store lock poisoned")
101            .contains_key(hash)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn put_get_roundtrip() {
111        let store = MemoryContentStore::new();
112        let data = b"fn main() { println!(\"hello\"); }";
113        let hash = store.put(data);
114        let retrieved = store.get(&hash).expect("should find stored content");
115        assert_eq!(retrieved, data);
116    }
117
118    #[test]
119    fn put_returns_deterministic_hash() {
120        let store = MemoryContentStore::new();
121        let data = b"deterministic content";
122        let hash1 = store.put(data);
123        let hash2 = store.put(data);
124        assert_eq!(hash1, hash2);
125    }
126
127    #[test]
128    fn dedup_stores_only_once() {
129        let store = MemoryContentStore::new();
130        let data = b"duplicate content";
131        store.put(data);
132        store.put(data);
133        assert_eq!(store.len(), 1);
134    }
135
136    #[test]
137    fn contains_returns_true_for_stored_hash() {
138        let store = MemoryContentStore::new();
139        let data = b"some content";
140        let hash = store.put(data);
141        assert!(store.contains(&hash));
142    }
143
144    #[test]
145    fn contains_returns_false_for_unknown_hash() {
146        let store = MemoryContentStore::new();
147        let fake_hash = [0u8; 32];
148        assert!(!store.contains(&fake_hash));
149    }
150
151    #[test]
152    fn get_returns_none_for_unknown_hash() {
153        let store = MemoryContentStore::new();
154        let fake_hash = [0xFF; 32];
155        assert!(store.get(&fake_hash).is_none());
156    }
157
158    #[test]
159    fn len_and_total_bytes() {
160        let store = MemoryContentStore::new();
161        assert_eq!(store.len(), 0);
162        assert!(store.is_empty());
163        assert_eq!(store.total_bytes(), 0);
164
165        store.put(b"hello"); // 5 bytes
166        store.put(b"world"); // 5 bytes
167        assert_eq!(store.len(), 2);
168        assert!(!store.is_empty());
169        assert_eq!(store.total_bytes(), 10);
170    }
171
172    #[test]
173    fn blake3_hash_is_32_bytes() {
174        let store = MemoryContentStore::new();
175        let hash = store.put(b"test");
176        assert_eq!(hash.len(), 32);
177    }
178
179    #[test]
180    fn different_content_produces_different_hashes() {
181        let store = MemoryContentStore::new();
182        let hash1 = store.put(b"content A");
183        let hash2 = store.put(b"content B");
184        assert_ne!(hash1, hash2);
185    }
186}