Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

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}