//! In-memory backend. Useful for tests and as the deserialize target for archive codecs. use crate::{ByteHolder, Container, ContainerError, Result, validate_path, validate_prefix, with_trailing_slash}; use std::collections::{HashMap, HashSet}; use std::sync::Mutex; #[derive(Default)] pub struct MemoryBackend { files: Mutex>>, } impl MemoryBackend { pub fn new() -> Self { Self::default() } } impl Container for MemoryBackend { fn read(&self, path: &str) -> Result { validate_path(path)?; self.files .lock() .unwrap() .get(path) .map(|bytes| ByteHolder::Owned(bytes.clone())) .ok_or_else(|| ContainerError::NotFound(path.to_string())) } fn write(&self, path: &str, bytes: &[u8]) -> Result<()> { validate_path(path)?; self.files.lock().unwrap().insert(path.to_string(), bytes.to_vec()); Ok(()) } fn append(&self, path: &str, bytes: &[u8]) -> Result<()> { validate_path(path)?; self.files.lock().unwrap().entry(path.to_string()).or_default().extend_from_slice(bytes); Ok(()) } fn list(&self, prefix: &str) -> Result> { validate_prefix(prefix)?; let files = self.files.lock().unwrap(); // A prefix that is itself a stored key names a file, not a directory. if files.contains_key(prefix) { return Err(ContainerError::NotADirectory(prefix.to_string())); } let normalized = with_trailing_slash(prefix); let results = files.keys().filter(|path| path.starts_with(&normalized) && !path[normalized.len()..].contains('/')).cloned().collect(); Ok(results) } fn list_dirs(&self, prefix: &str) -> Result> { validate_prefix(prefix)?; let files = self.files.lock().unwrap(); if files.contains_key(prefix) { return Err(ContainerError::NotADirectory(prefix.to_string())); } let normalized = with_trailing_slash(prefix); let mut seen = HashSet::new(); let mut results = Vec::new(); for path in files.keys() { if !path.starts_with(&normalized) { continue; } let remainder = &path[normalized.len()..]; if let Some((segment, _)) = remainder.split_once('/') { let dir = format!("{normalized}{segment}"); if seen.insert(dir.clone()) { results.push(dir); } } } Ok(results) } fn exists(&self, path: &str) -> bool { validate_path(path).is_ok() && self.files.lock().unwrap().contains_key(path) } fn remove(&self, path: &str) -> Result<()> { validate_path(path)?; // Idempotent: removing a missing path is not an error. self.files.lock().unwrap().remove(path); Ok(()) } }