Files
Graphite/document/container/src/backends/memory.rs
Dennis Kobert 6fe1af3afe Add document-container crate (#4191)
* Add document-container crate: container backends and archive codecs

* Address PR review: path/prefix split, safe size casts, OPFS stream aborts

* Address PR review round 2: mmap read check, UTF8 entry names, prefix normalization

* Make MmappedBytes::new fallible so mmap reads can't silently degrade

* Address PR review round 3: backend contract uniformity (symlinks, remove, list)

* Apply symlink-component check to FolderBackend listing paths

* Address PR review: idempotent OPFS delete logging, tar default-features, shared entry-size cap

* Fix validate_path doc: dotfiles pass, CurDir/ParentDir rejected

* Omit symlink entries from FolderBackend listings for consistency with resolve

* Preserve zip I/O errors and reject non-canonical paths in validate_path

* Extend archive apis to return the archive writer

* Rename document/document-container directory to document/container

* Add archive format sniffing and deserialize_auto

* Drop temporal hedge from checked_entry_size comment

* Tighten verbose doc comments in document-container

* Coalesce consecutive same-path OPFS appends to avoid O(n^2) file copies

* Review

* Update document-container for the deserialize/store_non_blocking renames

---------

Co-authored-by: Timon <me@timon.zip>
2026-06-05 13:28:02 -04:00

90 lines
2.5 KiB
Rust

//! 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<HashMap<String, Vec<u8>>>,
}
impl MemoryBackend {
pub fn new() -> Self {
Self::default()
}
}
impl Container for MemoryBackend {
fn read(&self, path: &str) -> Result<ByteHolder> {
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<Vec<String>> {
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<Vec<String>> {
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(())
}
}