eidetica/backend/database/in_memory/mod.rs
1//! In-memory database backend implementation
2//!
3//! This module provides an in-memory implementation of the Database trait,
4//! suitable for testing, development, or scenarios where data persistence
5//! is not strictly required or is handled externally.
6
7mod cache;
8mod persistence;
9mod storage;
10mod traversal;
11
12use std::{
13 any::Any,
14 collections::{HashMap, HashSet},
15 path::Path,
16 sync::{Mutex, RwLock},
17};
18
19use async_trait::async_trait;
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 Result,
24 backend::{
25 BackendImpl, CacheScope, InstanceMetadata, InstanceSecrets, VerificationStatus,
26 errors::BackendError,
27 },
28 entry::{Entry, ID},
29 snapshot::Snapshot,
30};
31
32use cache::InMemoryCrdtCache;
33
34/// Grouped tree tips cache: (tree_tips, subtree_name -> subtree_tips)
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub(crate) struct TreeTipsCache {
37 pub(crate) tree_tips: HashSet<ID>,
38 pub(crate) subtree_tips: HashMap<String, HashSet<ID>>,
39}
40
41/// Core data protected by a single lock.
42///
43/// All fields that participate in entry storage and tip tracking are grouped
44/// together to eliminate lock ordering concerns. A single `RwLock` on the
45/// outer `InMemory` struct protects all fields atomically.
46#[derive(Debug)]
47pub(crate) struct InMemoryInner {
48 pub(crate) entries: HashMap<ID, Entry>,
49 pub(crate) verification_status: HashMap<ID, VerificationStatus>,
50 /// Instance metadata containing device public key and system database IDs.
51 ///
52 /// When `None`, the backend is uninitialized. When `Some`, contains the
53 /// device public key and root IDs for system databases.
54 pub(crate) instance_metadata: Option<InstanceMetadata>,
55 /// Instance secrets containing the device signing key.
56 ///
57 /// **Security Warning**: The signing key is stored in memory without encryption.
58 /// This is suitable for development/testing only. Production systems should use
59 /// proper key management with encryption at rest.
60 pub(crate) instance_secrets: Option<InstanceSecrets>,
61 /// Cached tips grouped by tree: tree_id -> (tree_tips, subtree_name -> subtree_tips)
62 pub(crate) tips: HashMap<ID, TreeTipsCache>,
63}
64
65/// A simple in-memory database implementation using a `HashMap` for storage.
66///
67/// This database is suitable for testing, development, or scenarios where
68/// data persistence is not strictly required or is handled externally
69/// (e.g., by saving/loading the entire state to/from a file).
70///
71/// It provides basic persistence capabilities via `save_to_file` and
72/// `load_from_file`, serializing the `HashMap` to JSON.
73///
74/// **Security Note**: The device key is stored in memory in plaintext in this implementation.
75/// This is acceptable for development and testing but should not be used in production
76/// without proper encryption or hardware security module integration.
77#[derive(Debug)]
78pub struct InMemory {
79 /// Core data protected by a single lock for atomic access and
80 /// to eliminate lock ordering concerns between entries, verification
81 /// status, and tips.
82 pub(crate) inner: RwLock<InMemoryInner>,
83 /// Scope-keyed, byte-bounded LRU cache for materialized CRDT state.
84 /// `Mutex` (not `RwLock`) because `LruCache::get` mutates ordering, so
85 /// every read is effectively a write under LRU semantics. Hosts both
86 /// `Shared` (daemon-trusted) and `User` (client-attested) entries.
87 pub(crate) crdt_cache: Mutex<InMemoryCrdtCache>,
88}
89
90impl InMemory {
91 /// Creates a new, empty `InMemory` database.
92 pub fn new() -> Self {
93 Self {
94 inner: RwLock::new(InMemoryInner {
95 entries: HashMap::new(),
96 verification_status: HashMap::new(),
97 instance_metadata: None,
98 instance_secrets: None,
99 tips: HashMap::new(),
100 }),
101 crdt_cache: Mutex::new(InMemoryCrdtCache::new()),
102 }
103 }
104
105 /// Returns a vector containing the IDs of all entries currently stored in the database.
106 pub async fn all_ids(&self) -> Vec<ID> {
107 let inner = self.inner.read().unwrap();
108 inner.entries.keys().cloned().collect()
109 }
110
111 /// Saves the entire database state (all entries) to a specified file as JSON.
112 ///
113 /// The write is atomic on POSIX (writes to `<path>.tmp` then renames
114 /// into place). On Windows the final rename is not atomic when the
115 /// destination already exists.
116 ///
117 /// This is synchronous — the body is just `std::fs::write` + `rename`
118 /// with no await points — so it's safe to call from `Drop` impls and
119 /// other non-async contexts. Callers on a tokio runtime should be
120 /// aware that the write briefly blocks the calling worker thread.
121 ///
122 /// # Arguments
123 /// * `path` - The path to the file where the state should be saved.
124 ///
125 /// # Returns
126 /// A `Result` indicating success or an I/O or serialization error.
127 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
128 persistence::save_to_file(self, path)
129 }
130
131 /// Loads the database state from a specified JSON file.
132 ///
133 /// If the file does not exist, a new, empty `InMemory` database is returned.
134 /// Callers that need to tell "missing" apart from "loaded empty" should
135 /// use [`Self::try_load_from_file`].
136 ///
137 /// # Arguments
138 /// * `path` - The path to the file from which to load the state.
139 ///
140 /// # Returns
141 /// A `Result` containing the loaded `InMemory` database or an I/O or deserialization error.
142 pub async fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
143 persistence::load_from_file(path)
144 }
145
146 /// Like [`Self::load_from_file`], but returns `Ok(None)` when the file
147 /// does not exist instead of falling back to an empty backend. Lets
148 /// callers distinguish "no snapshot yet" from "snapshot loaded as
149 /// empty" without a separate `path.exists()` round-trip (and the TOCTOU
150 /// window that comes with it).
151 pub async fn try_load_from_file<P: AsRef<Path>>(path: P) -> Result<Option<Self>> {
152 persistence::try_load_from_file(path)
153 }
154
155 /// Sort entries by their height within a tree (exposed for testing)
156 ///
157 /// Heights are stored directly in entries, so this just reads and sorts.
158 ///
159 /// # Arguments
160 /// * `_tree` - The ID of the tree context (unused, kept for API compatibility)
161 /// * `entries` - The vector of entries to be sorted in place
162 pub fn sort_entries_by_height(&self, _tree: &ID, entries: &mut [Entry]) {
163 cache::sort_entries_by_height(entries)
164 }
165
166 /// Sort entries by their height within a subtree (exposed for testing)
167 ///
168 /// Heights are stored directly in entries, so this just reads and sorts.
169 ///
170 /// # Arguments
171 /// * `_tree` - The ID of the tree context (unused, kept for API compatibility)
172 /// * `subtree` - The name of the subtree context
173 /// * `entries` - The vector of entries to be sorted in place
174 pub fn sort_entries_by_subtree_height(&self, _tree: &ID, subtree: &str, entries: &mut [Entry]) {
175 cache::sort_entries_by_subtree_height(subtree, entries)
176 }
177
178 /// Check if an entry is a tip within its tree (exposed for benchmarks)
179 ///
180 /// An entry is a tip if no other entry in the same tree lists it as a parent.
181 ///
182 /// # Arguments
183 /// * `tree` - The ID of the tree to check within
184 /// * `entry_id` - The ID of the entry to check
185 ///
186 /// # Returns
187 /// `true` if the entry is a tip, `false` otherwise
188 pub async fn is_tip(&self, tree: &ID, entry_id: &ID) -> bool {
189 let inner = self.inner.read().unwrap();
190 storage::is_tip(&inner.entries, tree, entry_id)
191 }
192}
193
194impl Default for InMemory {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200#[async_trait]
201impl BackendImpl for InMemory {
202 /// Retrieves an entry by its unique content-addressable ID.
203 ///
204 /// # Arguments
205 /// * `id` - The ID of the entry to retrieve.
206 ///
207 /// # Returns
208 /// A `Result` containing the `Entry` if found, or a `DatabaseError::EntryNotFound` otherwise.
209 /// Returns an owned copy to support concurrent access with internal synchronization.
210 async fn get(&self, id: &ID) -> Result<Entry> {
211 let inner = self.inner.read().unwrap();
212 storage::get(&inner, id)
213 }
214
215 /// Gets the verification status of an entry.
216 ///
217 /// # Arguments
218 /// * `id` - The ID of the entry to check.
219 ///
220 /// # Returns
221 /// A `Result` containing the `VerificationStatus` if the entry exists, or a `DatabaseError::VerificationStatusNotFound` otherwise.
222 async fn get_verification_status(&self, id: &ID) -> Result<VerificationStatus> {
223 let inner = self.inner.read().unwrap();
224 inner
225 .verification_status
226 .get(id)
227 .copied()
228 .ok_or_else(|| BackendError::VerificationStatusNotFound { id: id.clone() }.into())
229 }
230
231 async fn put(&self, entry: Entry) -> Result<()> {
232 // Validate before acquiring write lock to fail fast
233 entry.validate()?;
234 let mut inner = self.inner.write().unwrap();
235 storage::put(&mut inner, entry)
236 }
237
238 /// Updates the verification status of an existing entry.
239 ///
240 /// This allows the authentication system to mark entries as verified or failed
241 /// after they have been stored. Useful for batch verification operations.
242 ///
243 /// # Arguments
244 /// * `id` - The ID of the entry to update
245 /// * `verification_status` - The new verification status
246 ///
247 /// # Returns
248 /// A `Result` indicating success or `DatabaseError::EntryNotFound` if the entry doesn't exist.
249 async fn update_verification_status(
250 &self,
251 id: &ID,
252 verification_status: VerificationStatus,
253 ) -> Result<()> {
254 let mut inner = self.inner.write().unwrap();
255 if inner.verification_status.contains_key(id) {
256 inner
257 .verification_status
258 .insert(id.clone(), verification_status);
259 Ok(())
260 } else {
261 Err(BackendError::EntryNotFound { id: id.clone() }.into())
262 }
263 }
264
265 /// Gets all entries with a specific verification status.
266 ///
267 /// This is useful for finding unverified entries that need authentication
268 /// or for security audits.
269 ///
270 /// # Arguments
271 /// * `status` - The verification status to filter by
272 ///
273 /// # Returns
274 /// A `Result` containing a vector of entry IDs with the specified status.
275 async fn get_entries_by_verification_status(
276 &self,
277 status: VerificationStatus,
278 ) -> Result<Vec<ID>> {
279 let inner = self.inner.read().unwrap();
280 let ids = inner
281 .verification_status
282 .iter()
283 .filter(|&(_, entry_status)| *entry_status == status)
284 .map(|(id, _)| id.clone())
285 .collect();
286 Ok(ids)
287 }
288
289 async fn snapshot(&self, tree: &ID) -> Result<Snapshot> {
290 // Fast path: check cache with read lock
291 {
292 let inner = self.inner.read().unwrap();
293 if let Some(cache) = inner.tips.get(tree) {
294 return Ok(Snapshot::new(cache.tree_tips.iter().cloned().collect()));
295 }
296 }
297 // Slow path: compute and cache with write lock
298 let mut inner = self.inner.write().unwrap();
299 traversal::snapshot(&mut inner, tree).map(Snapshot::new)
300 }
301
302 async fn store_snapshot(&self, tree: &ID, subtree: &str) -> Result<Snapshot> {
303 // Fast path: check cache with read lock
304 {
305 let inner = self.inner.read().unwrap();
306 if let Some(cache) = inner.tips.get(tree)
307 && let Some(subtree_tips) = cache.subtree_tips.get(subtree)
308 {
309 return Ok(Snapshot::new(subtree_tips.iter().cloned().collect()));
310 }
311 }
312 // Slow path: compute and cache with write lock
313 let mut inner = self.inner.write().unwrap();
314 traversal::store_snapshot(&mut inner, tree, subtree).map(Snapshot::new)
315 }
316
317 async fn store_snapshot_at(
318 &self,
319 tree: &ID,
320 subtree: &str,
321 main_snapshot: &Snapshot,
322 ) -> Result<Snapshot> {
323 let mut inner = self.inner.write().unwrap();
324 traversal::store_snapshot_at(&mut inner, tree, subtree, main_snapshot.tips())
325 .map(Snapshot::new)
326 }
327
328 /// Retrieves the IDs of all top-level root entries stored in the database.
329 ///
330 /// Top-level roots are entries that are themselves roots of a tree
331 /// (i.e., `entry.is_root()` is true) and are not part of a larger tree structure
332 /// tracked by the backend. These represent the starting points
333 /// of distinct trees managed by the database.
334 ///
335 /// # Returns
336 /// A `Result` containing a vector of top-level root entry IDs or an error.
337 async fn all_roots(&self) -> Result<Vec<ID>> {
338 let inner = self.inner.read().unwrap();
339 let roots: Vec<ID> = inner
340 .entries
341 .values()
342 .filter(|entry| entry.is_root())
343 .map(|entry| entry.id())
344 .collect();
345 Ok(roots)
346 }
347
348 async fn find_merge_base(&self, tree: &ID, subtree: &str, entry_ids: &[ID]) -> Result<ID> {
349 let inner = self.inner.read().unwrap();
350 traversal::find_merge_base(&inner, tree, subtree, entry_ids)
351 }
352
353 async fn collect_root_to_target(
354 &self,
355 tree: &ID,
356 subtree: &str,
357 target_entry: &ID,
358 ) -> Result<Vec<ID>> {
359 let inner = self.inner.read().unwrap();
360 traversal::collect_root_to_target(&inner, tree, subtree, target_entry)
361 }
362
363 fn as_any(&self) -> &dyn Any {
364 self
365 }
366
367 async fn get_tree(&self, tree: &ID) -> Result<Vec<Entry>> {
368 let inner = self.inner.read().unwrap();
369 storage::get_tree(&inner, tree)
370 }
371
372 async fn get_store(&self, tree: &ID, subtree: &str) -> Result<Vec<Entry>> {
373 let inner = self.inner.read().unwrap();
374 storage::get_store(&inner, tree, subtree)
375 }
376
377 async fn get_tree_from_tips(&self, tree: &ID, tips: &[ID]) -> Result<Vec<Entry>> {
378 let inner = self.inner.read().unwrap();
379 storage::get_tree_from_tips(&inner, tree, tips)
380 }
381
382 async fn store_at(&self, tree: &ID, subtree: &str, snapshot: &Snapshot) -> Result<Vec<Entry>> {
383 let inner = self.inner.read().unwrap();
384 storage::store_at(&inner, tree, subtree, snapshot.tips())
385 }
386
387 async fn get_instance_metadata(&self) -> Result<Option<InstanceMetadata>> {
388 let inner = self.inner.read().unwrap();
389 Ok(inner.instance_metadata.clone())
390 }
391
392 async fn set_instance_metadata(&self, metadata: &InstanceMetadata) -> Result<()> {
393 let mut inner = self.inner.write().unwrap();
394 inner.instance_metadata = Some(metadata.clone());
395 Ok(())
396 }
397
398 async fn get_instance_secrets(&self) -> Result<Option<InstanceSecrets>> {
399 let inner = self.inner.read().unwrap();
400 Ok(inner.instance_secrets.clone())
401 }
402
403 async fn set_instance_secrets(&self, secrets: &InstanceSecrets) -> Result<()> {
404 let mut inner = self.inner.write().unwrap();
405 inner.instance_secrets = Some(secrets.clone());
406 Ok(())
407 }
408
409 async fn get_cached_crdt_state(
410 &self,
411 scope: &CacheScope,
412 entry_id: &ID,
413 subtree: &str,
414 ) -> Result<Option<Vec<u8>>> {
415 cache::get_cached_crdt_state(self, scope, entry_id, subtree)
416 }
417
418 async fn cache_crdt_state(
419 &self,
420 scope: CacheScope,
421 entry_id: &ID,
422 subtree: &str,
423 state: Vec<u8>,
424 ) -> Result<()> {
425 cache::cache_crdt_state(self, scope, entry_id, subtree, state)
426 }
427
428 async fn clear_crdt_cache(&self) -> Result<()> {
429 cache::clear_crdt_cache(self)
430 }
431
432 async fn get_sorted_store_parents(
433 &self,
434 tree_id: &ID,
435 entry_id: &ID,
436 subtree: &str,
437 ) -> Result<Vec<ID>> {
438 let inner = self.inner.read().unwrap();
439 traversal::get_sorted_store_parents(&inner, tree_id, entry_id, subtree)
440 }
441
442 async fn get_path_from_to(
443 &self,
444 tree_id: &ID,
445 subtree: &str,
446 from_id: &ID,
447 to_ids: &[ID],
448 ) -> Result<Vec<ID>> {
449 let inner = self.inner.read().unwrap();
450 traversal::get_path_from_to(&inner, tree_id, subtree, from_id, to_ids)
451 }
452}