Skip to main content

eryx_vfs/
storage.rs

1//! VFS storage trait and implementations.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5use std::time::SystemTime;
6
7use async_trait::async_trait;
8use tokio::sync::RwLock;
9
10use crate::error::{VfsError, VfsResult};
11
12/// Metadata for a file or directory.
13#[derive(Debug, Clone)]
14pub struct Metadata {
15    /// Whether this is a directory.
16    pub is_dir: bool,
17    /// Size in bytes (0 for directories).
18    pub size: u64,
19    /// Creation time.
20    pub created: SystemTime,
21    /// Last modification time.
22    pub modified: SystemTime,
23    /// Last access time.
24    pub accessed: SystemTime,
25}
26
27impl Default for Metadata {
28    fn default() -> Self {
29        let now = SystemTime::now();
30        Self {
31            is_dir: false,
32            size: 0,
33            created: now,
34            modified: now,
35            accessed: now,
36        }
37    }
38}
39
40/// A directory entry returned by listing.
41#[derive(Debug, Clone)]
42pub struct DirEntry {
43    /// Name of the entry (not full path).
44    pub name: String,
45    /// Metadata for the entry.
46    pub metadata: Metadata,
47}
48
49/// Trait for VFS storage backends.
50///
51/// Implementations must be thread-safe and support async operations.
52/// All paths are absolute paths starting with `/`.
53#[async_trait]
54pub trait VfsStorage: Send + Sync {
55    /// Downcasting hook for storage-specific operations (e.g. capturing an
56    /// in-memory snapshot).
57    ///
58    /// Concrete storages that support downcasting return `Some(self)`; the
59    /// default is `None`. [`InMemoryStorage`] returns `Some` so callers can
60    /// reach [`InMemoryStorage::snapshot`].
61    fn as_any(&self) -> Option<&dyn core::any::Any> {
62        None
63    }
64
65    /// Read file contents.
66    async fn read(&self, path: &str) -> VfsResult<Vec<u8>>;
67
68    /// Read a portion of file contents.
69    async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>>;
70
71    /// Write file contents (creates or overwrites).
72    async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()>;
73
74    /// Write at a specific offset, extending the file if necessary.
75    async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()>;
76
77    /// Truncate or extend file to the given size.
78    async fn set_size(&self, path: &str, size: u64) -> VfsResult<()>;
79
80    /// Delete a file.
81    async fn delete(&self, path: &str) -> VfsResult<()>;
82
83    /// Check if a path exists.
84    async fn exists(&self, path: &str) -> VfsResult<bool>;
85
86    /// List directory contents.
87    async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>>;
88
89    /// Get file/directory metadata.
90    async fn stat(&self, path: &str) -> VfsResult<Metadata>;
91
92    /// Create a directory.
93    async fn mkdir(&self, path: &str) -> VfsResult<()>;
94
95    /// Remove a directory (must be empty).
96    async fn rmdir(&self, path: &str) -> VfsResult<()>;
97
98    /// Rename/move a file or directory.
99    async fn rename(&self, from: &str, to: &str) -> VfsResult<()>;
100
101    /// Synchronously create a directory.
102    ///
103    /// This is useful for setup code that runs before the async runtime
104    /// is available. The default implementation returns an error - implementors
105    /// should override this if they support sync directory creation.
106    fn mkdir_sync(&self, _path: &str) -> VfsResult<()> {
107        Err(VfsError::Storage(
108            "mkdir_sync not implemented for this storage backend".to_string(),
109        ))
110    }
111}
112
113/// Type-erased VFS storage that wraps any [`VfsStorage`] implementation.
114///
115/// This avoids propagating generic type parameters through the entire
116/// executor stack while still allowing different storage backends
117/// (e.g., [`InMemoryStorage`], [`ScrubbingStorage`](crate::ScrubbingStorage))
118/// to be used interchangeably.
119///
120/// # Example
121///
122/// ```rust,ignore
123/// use eryx_vfs::{ArcStorage, InMemoryStorage};
124/// use std::sync::Arc;
125///
126/// // Wrap any VfsStorage in a ArcStorage
127/// let storage = ArcStorage::new(Arc::new(InMemoryStorage::new()));
128/// ```
129#[derive(Clone)]
130pub struct ArcStorage(Arc<dyn VfsStorage>);
131
132impl ArcStorage {
133    /// Wrap any `VfsStorage` implementation in a type-erased `ArcStorage`.
134    pub fn new(storage: Arc<dyn VfsStorage>) -> Self {
135        Self(storage)
136    }
137}
138
139impl std::fmt::Debug for ArcStorage {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_tuple("ArcStorage")
142            .field(&"<dyn VfsStorage>")
143            .finish()
144    }
145}
146
147#[async_trait]
148impl VfsStorage for ArcStorage {
149    async fn read(&self, path: &str) -> VfsResult<Vec<u8>> {
150        self.0.read(path).await
151    }
152
153    async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>> {
154        self.0.read_at(path, offset, len).await
155    }
156
157    async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()> {
158        self.0.write(path, data).await
159    }
160
161    async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()> {
162        self.0.write_at(path, offset, data).await
163    }
164
165    async fn set_size(&self, path: &str, size: u64) -> VfsResult<()> {
166        self.0.set_size(path, size).await
167    }
168
169    async fn delete(&self, path: &str) -> VfsResult<()> {
170        self.0.delete(path).await
171    }
172
173    async fn exists(&self, path: &str) -> VfsResult<bool> {
174        self.0.exists(path).await
175    }
176
177    async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>> {
178        self.0.list(path).await
179    }
180
181    async fn stat(&self, path: &str) -> VfsResult<Metadata> {
182        self.0.stat(path).await
183    }
184
185    async fn mkdir(&self, path: &str) -> VfsResult<()> {
186        self.0.mkdir(path).await
187    }
188
189    async fn rmdir(&self, path: &str) -> VfsResult<()> {
190        self.0.rmdir(path).await
191    }
192
193    async fn rename(&self, from: &str, to: &str) -> VfsResult<()> {
194        self.0.rename(from, to).await
195    }
196
197    fn mkdir_sync(&self, path: &str) -> VfsResult<()> {
198        self.0.mkdir_sync(path)
199    }
200}
201
202/// Internal state for in-memory storage.
203///
204/// Combining files and directories into a single struct allows us to use
205/// a single `RwLock`, avoiding potential deadlock issues from acquiring
206/// multiple locks.
207#[derive(Debug, Default, Clone)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209struct StorageState {
210    /// File contents: path -> data
211    files: HashMap<String, FileData>,
212    /// Directory markers: set of directory paths
213    directories: HashSet<String>,
214}
215
216/// Default limit on the total bytes of file content an [`InMemoryStorage`]
217/// will hold: 64 MiB.
218///
219/// File contents live in host memory, and a sandboxed guest chooses both the
220/// offsets it writes at and the sizes it truncates to. Without a limit, a
221/// one-byte write at a large offset turns into an arbitrarily large host
222/// allocation. Override with [`InMemoryStorage::with_max_bytes`].
223pub const DEFAULT_MAX_BYTES: u64 = 64 * 1024 * 1024;
224
225/// In-memory VFS storage implementation.
226///
227/// Stores files and directories in memory using `HashMap` and `HashSet`.
228/// Thread-safe via a single `RwLock` over the combined state.
229///
230/// Total file content is capped at [`DEFAULT_MAX_BYTES`] unless changed with
231/// [`InMemoryStorage::with_max_bytes`]; operations that would exceed the cap
232/// fail with [`VfsError::QuotaExceeded`], which guests see as `ENOSPC`.
233#[derive(Debug)]
234pub struct InMemoryStorage {
235    /// Combined state under a single lock to prevent deadlocks.
236    state: RwLock<StorageState>,
237    /// Maximum total bytes of file content, across all files.
238    max_bytes: u64,
239}
240
241#[derive(Debug, Clone)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
243struct FileData {
244    content: Vec<u8>,
245    created: SystemTime,
246    modified: SystemTime,
247    accessed: SystemTime,
248}
249
250/// A clonable, optionally-serializable snapshot of an [`InMemoryStorage`]'s
251/// complete contents (all files and directories).
252///
253/// Produced by [`InMemoryStorage::snapshot`]. Apply it to an existing storage
254/// with [`InMemoryStorage::restore`] (in place, preserving the storage's
255/// identity so existing `Arc` references stay valid), or create an independent
256/// copy with [`InMemoryStorage::from_snapshot`] (for forking a session).
257///
258/// With the `serde` feature, this derives `Serialize`/`Deserialize` so it can be
259/// persisted to bytes.
260#[derive(Debug, Clone)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
262pub struct InMemorySnapshot {
263    state: StorageState,
264}
265
266impl Default for InMemoryStorage {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl InMemoryStorage {
273    /// Create a new empty in-memory storage with root directory.
274    ///
275    /// Total file content is capped at [`DEFAULT_MAX_BYTES`].
276    #[must_use]
277    pub fn new() -> Self {
278        Self::with_max_bytes(DEFAULT_MAX_BYTES)
279    }
280
281    /// Create a new empty in-memory storage with an explicit size cap.
282    ///
283    /// `max_bytes` bounds the total content of all files combined. Because the
284    /// contents live in host memory and a sandboxed guest picks its own write
285    /// offsets, this is a limit on how much memory the guest can make the host
286    /// allocate - keep it well below what the host can afford to lose.
287    #[must_use]
288    pub fn with_max_bytes(max_bytes: u64) -> Self {
289        let mut directories = HashSet::new();
290        directories.insert("/".to_string());
291        Self {
292            state: RwLock::new(StorageState {
293                files: HashMap::new(),
294                directories,
295            }),
296            max_bytes,
297        }
298    }
299
300    /// The total-content cap, in bytes.
301    #[must_use]
302    pub fn max_bytes(&self) -> u64 {
303        self.max_bytes
304    }
305
306    /// Capture the entire contents (files and directories) as a snapshot.
307    ///
308    /// The snapshot is an independent copy; later mutations to this storage do
309    /// not affect it.
310    pub async fn snapshot(&self) -> InMemorySnapshot {
311        InMemorySnapshot {
312            state: self.state.read().await.clone(),
313        }
314    }
315
316    /// Replace this storage's entire contents with a snapshot, in place.
317    ///
318    /// The storage's identity is preserved, so any `Arc<dyn VfsStorage>` handles
319    /// (e.g. those wired into a running sandbox) continue to refer to it and see
320    /// the restored contents.
321    pub async fn restore(&self, snapshot: &InMemorySnapshot) {
322        *self.state.write().await = snapshot.state.clone();
323    }
324
325    /// Create a new, independent storage initialized from a snapshot.
326    ///
327    /// Useful for forking a session: the returned storage shares no state with
328    /// the original, so the two can diverge freely.
329    #[must_use]
330    pub fn from_snapshot(snapshot: &InMemorySnapshot) -> Self {
331        Self {
332            state: RwLock::new(snapshot.state.clone()),
333            max_bytes: DEFAULT_MAX_BYTES,
334        }
335    }
336
337    /// Normalize a path (remove trailing slashes, handle . and ..).
338    fn normalize_path(path: &str) -> VfsResult<String> {
339        if !path.starts_with('/') {
340            return Err(VfsError::InvalidPath(format!(
341                "path must be absolute: {path}"
342            )));
343        }
344
345        let mut components: Vec<&str> = Vec::new();
346        for component in path.split('/') {
347            match component {
348                "" | "." => continue,
349                ".." => {
350                    if components.is_empty() {
351                        return Err(VfsError::InvalidPath("path escapes root".to_string()));
352                    }
353                    components.pop();
354                }
355                c => components.push(c),
356            }
357        }
358
359        if components.is_empty() {
360            Ok("/".to_string())
361        } else {
362            Ok(format!("/{}", components.join("/")))
363        }
364    }
365
366    /// Get the parent directory of a path.
367    fn parent_path(path: &str) -> Option<String> {
368        if path == "/" {
369            return None;
370        }
371        let normalized = Self::normalize_path(path).ok()?;
372        if normalized == "/" {
373            return None;
374        }
375        match normalized.rfind('/') {
376            Some(0) => Some("/".to_string()),
377            Some(idx) => Some(normalized[..idx].to_string()),
378            None => None,
379        }
380    }
381
382    /// Check that resizing `path` to `new_len` keeps total content within
383    /// `max_bytes`, returning `new_len` as a `usize` ready to resize with.
384    ///
385    /// Called before every allocation so that a guest-chosen offset or length
386    /// can never turn into an unbounded host allocation. The current total is
387    /// summed on the spot rather than tracked incrementally: it is only needed
388    /// on the growth paths, and a running counter is one missed update away
389    /// from being wrong.
390    fn check_budget(&self, state: &StorageState, path: &str, new_len: u64) -> VfsResult<usize> {
391        let current_len = state
392            .files
393            .get(path)
394            .map_or(0, |f| f.content.len().try_into().unwrap_or(u64::MAX));
395
396        // Only growth needs checking; shrinking always fits.
397        if new_len > current_len {
398            let others: u64 = state
399                .files
400                .iter()
401                .filter(|(p, _)| p.as_str() != path)
402                .map(|(_, f)| f.content.len().try_into().unwrap_or(u64::MAX))
403                .fold(0u64, u64::saturating_add);
404
405            let total = others.saturating_add(new_len);
406            if total > self.max_bytes {
407                return Err(VfsError::QuotaExceeded(format!(
408                    "{path}: {new_len} bytes would put the filesystem at {total} bytes, over the {} byte limit",
409                    self.max_bytes
410                )));
411            }
412        }
413
414        // new_len <= max_bytes, which is a u64 the host was willing to allocate,
415        // so this only fails on a 32-bit host with an oversized cap.
416        new_len.try_into().map_err(|_| {
417            VfsError::QuotaExceeded(format!(
418                "{path}: {new_len} bytes exceeds host addressable size"
419            ))
420        })
421    }
422
423    /// Check if parent directory exists (requires state to already be borrowed).
424    fn check_parent_exists_with_state(state: &StorageState, path: &str) -> VfsResult<()> {
425        if let Some(parent) = Self::parent_path(path)
426            && !state.directories.contains(&parent)
427        {
428            return Err(VfsError::NotFound(format!("parent directory: {parent}")));
429        }
430        Ok(())
431    }
432}
433
434#[async_trait]
435impl VfsStorage for InMemoryStorage {
436    fn as_any(&self) -> Option<&dyn core::any::Any> {
437        Some(self)
438    }
439
440    async fn read(&self, path: &str) -> VfsResult<Vec<u8>> {
441        let path = Self::normalize_path(path)?;
442        let state = self.state.read().await;
443        match state.files.get(&path) {
444            Some(data) => Ok(data.content.clone()),
445            None => {
446                if state.directories.contains(&path) {
447                    Err(VfsError::NotFile(path))
448                } else {
449                    Err(VfsError::NotFound(path))
450                }
451            }
452        }
453    }
454
455    async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>> {
456        let path = Self::normalize_path(path)?;
457        let state = self.state.read().await;
458        match state.files.get(&path) {
459            Some(data) => {
460                // Both offset and len come from the guest, so stay in u64 and
461                // saturate: `offset + len` overflows for offsets near u64::MAX.
462                let content_len = data.content.len().try_into().unwrap_or(u64::MAX);
463                if offset >= content_len {
464                    Ok(Vec::new())
465                } else {
466                    // offset < content_len, and end is clamped to it, so both
467                    // fit in usize.
468                    let end = offset.saturating_add(len).min(content_len);
469                    Ok(data.content[offset as usize..end as usize].to_vec())
470                }
471            }
472            None => {
473                if state.directories.contains(&path) {
474                    Err(VfsError::NotFile(path))
475                } else {
476                    Err(VfsError::NotFound(path))
477                }
478            }
479        }
480    }
481
482    async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()> {
483        let path = Self::normalize_path(path)?;
484        let mut state = self.state.write().await;
485
486        Self::check_parent_exists_with_state(&state, &path)?;
487
488        // Check it's not a directory
489        if state.directories.contains(&path) {
490            return Err(VfsError::NotFile(path));
491        }
492
493        self.check_budget(&state, &path, data.len().try_into().unwrap_or(u64::MAX))?;
494
495        let now = SystemTime::now();
496        let file_data = state.files.entry(path).or_insert_with(|| FileData {
497            content: Vec::new(),
498            created: now,
499            modified: now,
500            accessed: now,
501        });
502        file_data.content = data.to_vec();
503        file_data.modified = now;
504        Ok(())
505    }
506
507    async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()> {
508        let path = Self::normalize_path(path)?;
509        let mut state = self.state.write().await;
510
511        Self::check_parent_exists_with_state(&state, &path)?;
512
513        // Check it's not a directory
514        if state.directories.contains(&path) {
515            return Err(VfsError::NotFile(path));
516        }
517
518        // The guest picks the offset, so a one-byte write can ask the host to
519        // materialize gigabytes of zeroes. Compute the end in u64 and check it
520        // against the budget before allocating anything; an offset that
521        // overflows u64 is over the limit by definition.
522        let data_len: u64 = data.len().try_into().unwrap_or(u64::MAX);
523        let end = offset.checked_add(data_len).ok_or_else(|| {
524            VfsError::QuotaExceeded(format!(
525                "{path}: write of {data_len} bytes at offset {offset} overflows the address space"
526            ))
527        })?;
528        let needed_len = self.check_budget(&state, &path, end)?;
529
530        let now = SystemTime::now();
531        let file_data = state.files.entry(path).or_insert_with(|| FileData {
532            content: Vec::new(),
533            created: now,
534            modified: now,
535            accessed: now,
536        });
537
538        // Extend file if necessary
539        if file_data.content.len() < needed_len {
540            file_data.content.resize(needed_len, 0);
541        }
542        let start = needed_len - data.len();
543        file_data.content[start..needed_len].copy_from_slice(data);
544        file_data.modified = now;
545        Ok(())
546    }
547
548    async fn set_size(&self, path: &str, size: u64) -> VfsResult<()> {
549        let path = Self::normalize_path(path)?;
550        let now = SystemTime::now();
551        let mut state = self.state.write().await;
552        if !state.files.contains_key(&path) {
553            return Err(VfsError::NotFound(path));
554        }
555
556        // `size` is guest-controlled: truncating up to u64::MAX would either
557        // abort the host on allocation failure or panic on capacity overflow.
558        let size = self.check_budget(&state, &path, size)?;
559
560        match state.files.get_mut(&path) {
561            Some(data) => {
562                data.content.resize(size, 0);
563                data.modified = now;
564                Ok(())
565            }
566            None => Err(VfsError::NotFound(path)),
567        }
568    }
569
570    async fn delete(&self, path: &str) -> VfsResult<()> {
571        let path = Self::normalize_path(path)?;
572        let mut state = self.state.write().await;
573        if state.files.remove(&path).is_some() {
574            Ok(())
575        } else if state.directories.contains(&path) {
576            Err(VfsError::NotFile(path))
577        } else {
578            Err(VfsError::NotFound(path))
579        }
580    }
581
582    async fn exists(&self, path: &str) -> VfsResult<bool> {
583        let path = Self::normalize_path(path)?;
584        let state = self.state.read().await;
585        Ok(state.files.contains_key(&path) || state.directories.contains(&path))
586    }
587
588    async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>> {
589        let path = Self::normalize_path(path)?;
590        let state = self.state.read().await;
591
592        // Check it's a directory
593        if !state.directories.contains(&path) {
594            if state.files.contains_key(&path) {
595                return Err(VfsError::NotDirectory(path));
596            } else {
597                return Err(VfsError::NotFound(path));
598            }
599        }
600
601        let prefix = if path == "/" {
602            "/".to_string()
603        } else {
604            format!("{path}/")
605        };
606
607        let mut entries = Vec::new();
608        let mut seen_names = HashSet::new();
609
610        // List files
611        for (file_path, data) in &state.files {
612            if let Some(rest) = file_path.strip_prefix(&prefix) {
613                // Only include direct children (no more slashes)
614                if !rest.contains('/') && !rest.is_empty() {
615                    seen_names.insert(rest.to_string());
616                    entries.push(DirEntry {
617                        name: rest.to_string(),
618                        metadata: Metadata {
619                            is_dir: false,
620                            size: data.content.len() as u64,
621                            created: data.created,
622                            modified: data.modified,
623                            accessed: data.accessed,
624                        },
625                    });
626                }
627            }
628        }
629
630        // List subdirectories
631        for dir_path in &state.directories {
632            if let Some(rest) = dir_path.strip_prefix(&prefix) {
633                // Only include direct children
634                if !rest.contains('/') && !rest.is_empty() && !seen_names.contains(rest) {
635                    let now = SystemTime::now();
636                    entries.push(DirEntry {
637                        name: rest.to_string(),
638                        metadata: Metadata {
639                            is_dir: true,
640                            size: 0,
641                            created: now,
642                            modified: now,
643                            accessed: now,
644                        },
645                    });
646                }
647            }
648        }
649
650        entries.sort_by(|a, b| a.name.cmp(&b.name));
651        Ok(entries)
652    }
653
654    async fn stat(&self, path: &str) -> VfsResult<Metadata> {
655        let path = Self::normalize_path(path)?;
656        let state = self.state.read().await;
657
658        // Check files first
659        if let Some(data) = state.files.get(&path) {
660            return Ok(Metadata {
661                is_dir: false,
662                size: data.content.len() as u64,
663                created: data.created,
664                modified: data.modified,
665                accessed: data.accessed,
666            });
667        }
668
669        // Check directories
670        if state.directories.contains(&path) {
671            let now = SystemTime::now();
672            return Ok(Metadata {
673                is_dir: true,
674                size: 0,
675                created: now,
676                modified: now,
677                accessed: now,
678            });
679        }
680
681        Err(VfsError::NotFound(path))
682    }
683
684    async fn mkdir(&self, path: &str) -> VfsResult<()> {
685        let path = Self::normalize_path(path)?;
686        let mut state = self.state.write().await;
687
688        Self::check_parent_exists_with_state(&state, &path)?;
689
690        // Check if already exists
691        if state.files.contains_key(&path) {
692            return Err(VfsError::AlreadyExists(path));
693        }
694        if state.directories.contains(&path) {
695            return Err(VfsError::AlreadyExists(path));
696        }
697
698        state.directories.insert(path);
699        Ok(())
700    }
701
702    async fn rmdir(&self, path: &str) -> VfsResult<()> {
703        let path = Self::normalize_path(path)?;
704
705        if path == "/" {
706            return Err(VfsError::PermissionDenied(
707                "cannot remove root directory".to_string(),
708            ));
709        }
710
711        let mut state = self.state.write().await;
712
713        // Check if it's a directory
714        if !state.directories.contains(&path) {
715            if state.files.contains_key(&path) {
716                return Err(VfsError::NotDirectory(path));
717            } else {
718                return Err(VfsError::NotFound(path));
719            }
720        }
721
722        // Check if empty
723        let prefix = format!("{path}/");
724        for file_path in state.files.keys() {
725            if file_path.starts_with(&prefix) {
726                return Err(VfsError::DirectoryNotEmpty(path));
727            }
728        }
729        for dir_path in &state.directories {
730            if dir_path.starts_with(&prefix) {
731                return Err(VfsError::DirectoryNotEmpty(path));
732            }
733        }
734
735        state.directories.remove(&path);
736        Ok(())
737    }
738
739    async fn rename(&self, from: &str, to: &str) -> VfsResult<()> {
740        let from = Self::normalize_path(from)?;
741        let to = Self::normalize_path(to)?;
742
743        if from == to {
744            return Ok(());
745        }
746
747        let mut state = self.state.write().await;
748
749        Self::check_parent_exists_with_state(&state, &to)?;
750
751        // Handle file rename
752        if state.files.contains_key(&from) {
753            // Check destination doesn't exist as directory
754            if state.directories.contains(&to) {
755                return Err(VfsError::AlreadyExists(to));
756            }
757
758            if let Some(data) = state.files.remove(&from) {
759                state.files.insert(to, data);
760                return Ok(());
761            }
762        }
763
764        // Handle directory rename
765        if state.directories.contains(&from) {
766            // Check destination doesn't exist as file
767            if state.files.contains_key(&to) {
768                return Err(VfsError::AlreadyExists(to));
769            }
770
771            // Rename directory and all contents
772            let from_prefix = format!("{from}/");
773            let to_prefix = format!("{to}/");
774
775            // Rename files under the directory
776            let files_to_rename: Vec<_> = state
777                .files
778                .keys()
779                .filter(|p| p.starts_with(&from_prefix))
780                .cloned()
781                .collect();
782            for old_path in files_to_rename {
783                if let Some(data) = state.files.remove(&old_path) {
784                    let new_path = old_path.replacen(&from_prefix, &to_prefix, 1);
785                    state.files.insert(new_path, data);
786                }
787            }
788
789            // Rename subdirectories
790            let dirs_to_rename: Vec<_> = state
791                .directories
792                .iter()
793                .filter(|p| *p == &from || p.starts_with(&from_prefix))
794                .cloned()
795                .collect();
796            for old_path in dirs_to_rename {
797                state.directories.remove(&old_path);
798                let new_path = if old_path == from {
799                    to.clone()
800                } else {
801                    old_path.replacen(&from_prefix, &to_prefix, 1)
802                };
803                state.directories.insert(new_path);
804            }
805
806            return Ok(());
807        }
808
809        Err(VfsError::NotFound(from))
810    }
811
812    fn mkdir_sync(&self, path: &str) -> VfsResult<()> {
813        let path = Self::normalize_path(path)?;
814
815        // Collect all directories to create first.
816        let mut dirs_to_create = Vec::new();
817        let mut current = String::new();
818        for component in path.split('/').filter(|s| !s.is_empty()) {
819            current = format!("{}/{}", current, component);
820            dirs_to_create.push(current.clone());
821        }
822
823        // Try to acquire the lock without blocking first.
824        // This handles the common case where no contention exists.
825        if let Ok(mut state) = self.state.try_write() {
826            for dir in dirs_to_create {
827                state.directories.insert(dir);
828            }
829            return Ok(());
830        }
831
832        // If try_write fails, we need to use a blocking approach.
833        // Check if we're in an async runtime context.
834        if let Ok(handle) = tokio::runtime::Handle::try_current() {
835            // We're in an async runtime - use block_on with async write.
836            // This is safe because mkdir_sync is only called during setup,
837            // before the store starts processing async WASM calls.
838            handle.block_on(async {
839                let mut state = self.state.write().await;
840                for dir in dirs_to_create {
841                    state.directories.insert(dir);
842                }
843            });
844        } else {
845            // Not in an async runtime - safe to use blocking_write.
846            let mut state = self.state.blocking_write();
847            for dir in dirs_to_create {
848                state.directories.insert(dir);
849            }
850        }
851
852        Ok(())
853    }
854}
855
856#[cfg(test)]
857#[allow(clippy::unwrap_used)]
858mod tests {
859    use super::*;
860
861    /// Every offset here used to turn a one-byte write into a multi-gigabyte
862    /// host allocation: 2^31 and 2^32 succeeded (2 GiB and 4 GiB of zeroes),
863    /// 2^40 aborted the process outright ("memory allocation of 1099511627777
864    /// bytes failed"), and u64::MAX panicked on `offset + len`. All of them are
865    /// reachable from sandboxed Python via `f.seek(off); f.write(b'X')`.
866    #[tokio::test]
867    async fn write_at_extreme_offset_is_refused_not_allocated() {
868        let storage = InMemoryStorage::new();
869        storage.write("/f", b"small content").await.unwrap();
870
871        for offset in [
872            1u64 << 31,
873            1 << 32,
874            1 << 40,
875            1 << 62,
876            u64::MAX - 1,
877            u64::MAX,
878        ] {
879            let err = storage.write_at("/f", offset, b"X").await.unwrap_err();
880            assert!(
881                matches!(err, VfsError::QuotaExceeded(_)),
882                "offset {offset} gave {err:?}"
883            );
884        }
885
886        // The file is untouched by the refused writes.
887        assert_eq!(storage.read("/f").await.unwrap(), b"small content");
888        assert_eq!(storage.stat("/f").await.unwrap().size, 13);
889    }
890
891    #[tokio::test]
892    async fn write_at_within_budget_still_extends_sparsely() {
893        let storage = InMemoryStorage::new();
894        storage.write("/f", b"abc").await.unwrap();
895
896        storage.write_at("/f", 1000, b"X").await.unwrap();
897
898        assert_eq!(storage.stat("/f").await.unwrap().size, 1001);
899        let content = storage.read("/f").await.unwrap();
900        assert_eq!(&content[..3], b"abc");
901        assert!(content[3..1000].iter().all(|b| *b == 0));
902        assert_eq!(content[1000], b'X');
903    }
904
905    #[tokio::test]
906    async fn write_at_offset_overwrites_in_place() {
907        let storage = InMemoryStorage::new();
908        storage.write("/f", b"aaaaa").await.unwrap();
909
910        storage.write_at("/f", 1, b"bb").await.unwrap();
911
912        assert_eq!(storage.read("/f").await.unwrap(), b"abbaa");
913        assert_eq!(storage.stat("/f").await.unwrap().size, 5);
914    }
915
916    #[tokio::test]
917    async fn set_size_beyond_budget_is_refused() {
918        let storage = InMemoryStorage::new();
919        storage.write("/f", b"x").await.unwrap();
920
921        // u64::MAX used to panic with "capacity overflow".
922        for size in [u64::MAX, 1 << 62, DEFAULT_MAX_BYTES + 1] {
923            let err = storage.set_size("/f", size).await.unwrap_err();
924            assert!(
925                matches!(err, VfsError::QuotaExceeded(_)),
926                "size {size} gave {err:?}"
927            );
928        }
929
930        assert_eq!(storage.stat("/f").await.unwrap().size, 1);
931        // Shrinking is always allowed.
932        storage.set_size("/f", 0).await.unwrap();
933        assert_eq!(storage.stat("/f").await.unwrap().size, 0);
934    }
935
936    #[tokio::test]
937    async fn read_at_huge_length_does_not_overflow() {
938        let storage = InMemoryStorage::new();
939        storage.write("/f", b"small content").await.unwrap();
940
941        // `offset + len` used to overflow and panic.
942        assert_eq!(
943            storage.read_at("/f", 5, u64::MAX).await.unwrap(),
944            b" content"
945        );
946        assert_eq!(
947            storage.read_at("/f", 0, u64::MAX).await.unwrap(),
948            b"small content"
949        );
950        assert!(
951            storage
952                .read_at("/f", u64::MAX, 10)
953                .await
954                .unwrap()
955                .is_empty()
956        );
957        assert!(
958            storage
959                .read_at("/f", 13, u64::MAX)
960                .await
961                .unwrap()
962                .is_empty()
963        );
964    }
965
966    #[tokio::test]
967    async fn budget_covers_all_files_together() {
968        let storage = InMemoryStorage::with_max_bytes(1024);
969
970        storage.write("/a", &vec![0u8; 600]).await.unwrap();
971        // 600 + 600 > 1024, so the second file does not fit.
972        let err = storage.write("/b", &vec![0u8; 600]).await.unwrap_err();
973        assert!(matches!(err, VfsError::QuotaExceeded(_)), "{err:?}");
974
975        // Freeing the first file makes room again.
976        storage.delete("/a").await.unwrap();
977        storage.write("/b", &vec![0u8; 600]).await.unwrap();
978
979        // Overwriting a file counts its new size, not the sum of both.
980        storage.write("/b", &vec![0u8; 1024]).await.unwrap();
981        assert_eq!(storage.stat("/b").await.unwrap().size, 1024);
982    }
983
984    #[tokio::test]
985    async fn max_bytes_is_reported() {
986        assert_eq!(InMemoryStorage::new().max_bytes(), DEFAULT_MAX_BYTES);
987        assert_eq!(InMemoryStorage::with_max_bytes(42).max_bytes(), 42);
988    }
989
990    #[tokio::test]
991    async fn test_snapshot_restore_and_fork() {
992        let storage = InMemoryStorage::new();
993        storage.mkdir("/data").await.unwrap();
994        storage.write("/data/a.txt", b"original").await.unwrap();
995
996        // Snapshot, then mutate.
997        let snap = storage.snapshot().await;
998        storage.write("/data/a.txt", b"changed").await.unwrap();
999        storage.write("/data/b.txt", b"new").await.unwrap();
1000
1001        // Restore in place: back to the snapshot, b.txt gone.
1002        storage.restore(&snap).await;
1003        assert_eq!(storage.read("/data/a.txt").await.unwrap(), b"original");
1004        assert!(storage.read("/data/b.txt").await.is_err());
1005
1006        // Fork: an independent copy that diverges without affecting the original.
1007        let forked = InMemoryStorage::from_snapshot(&snap);
1008        forked.write("/data/a.txt", b"forked").await.unwrap();
1009        assert_eq!(forked.read("/data/a.txt").await.unwrap(), b"forked");
1010        assert_eq!(storage.read("/data/a.txt").await.unwrap(), b"original");
1011    }
1012
1013    #[cfg(feature = "serde")]
1014    #[tokio::test]
1015    async fn test_snapshot_serde_round_trip() {
1016        let storage = InMemoryStorage::new();
1017        storage.mkdir("/d").await.unwrap();
1018        storage.write("/d/f.txt", b"payload").await.unwrap();
1019
1020        let snap = storage.snapshot().await;
1021        let bytes = serde_json::to_vec(&snap).unwrap();
1022        let restored: InMemorySnapshot = serde_json::from_slice(&bytes).unwrap();
1023
1024        let fresh = InMemoryStorage::from_snapshot(&restored);
1025        assert_eq!(fresh.read("/d/f.txt").await.unwrap(), b"payload");
1026    }
1027
1028    #[tokio::test]
1029    async fn test_file_operations() {
1030        let storage = InMemoryStorage::new();
1031
1032        // Write and read
1033        storage.write("/test.txt", b"hello").await.unwrap();
1034        let content = storage.read("/test.txt").await.unwrap();
1035        assert_eq!(content, b"hello");
1036
1037        // Read at offset
1038        let partial = storage.read_at("/test.txt", 2, 3).await.unwrap();
1039        assert_eq!(partial, b"llo");
1040
1041        // Overwrite
1042        storage.write("/test.txt", b"world").await.unwrap();
1043        let content = storage.read("/test.txt").await.unwrap();
1044        assert_eq!(content, b"world");
1045
1046        // Delete
1047        storage.delete("/test.txt").await.unwrap();
1048        assert!(storage.read("/test.txt").await.is_err());
1049    }
1050
1051    #[tokio::test]
1052    async fn test_directory_operations() {
1053        let storage = InMemoryStorage::new();
1054
1055        // Create directory
1056        storage.mkdir("/subdir").await.unwrap();
1057        assert!(storage.exists("/subdir").await.unwrap());
1058
1059        // Create file in directory
1060        storage.write("/subdir/file.txt", b"content").await.unwrap();
1061
1062        // List directory
1063        let entries = storage.list("/subdir").await.unwrap();
1064        assert_eq!(entries.len(), 1);
1065        assert_eq!(entries[0].name, "file.txt");
1066
1067        // Can't remove non-empty directory
1068        assert!(storage.rmdir("/subdir").await.is_err());
1069
1070        // Remove file, then directory
1071        storage.delete("/subdir/file.txt").await.unwrap();
1072        storage.rmdir("/subdir").await.unwrap();
1073        assert!(!storage.exists("/subdir").await.unwrap());
1074    }
1075
1076    #[tokio::test]
1077    async fn test_path_normalization() {
1078        let storage = InMemoryStorage::new();
1079
1080        storage.write("/test.txt", b"data").await.unwrap();
1081
1082        // Various path formats should work
1083        assert!(storage.exists("/test.txt").await.unwrap());
1084        assert!(storage.exists("/./test.txt").await.unwrap());
1085
1086        // Parent references
1087        storage.mkdir("/dir").await.unwrap();
1088        storage.write("/dir/file.txt", b"data").await.unwrap();
1089        let content = storage.read("/dir/../dir/file.txt").await.unwrap();
1090        assert_eq!(content, b"data");
1091    }
1092
1093    #[tokio::test]
1094    async fn test_rename() {
1095        let storage = InMemoryStorage::new();
1096
1097        // File rename
1098        storage.write("/old.txt", b"content").await.unwrap();
1099        storage.rename("/old.txt", "/new.txt").await.unwrap();
1100        assert!(!storage.exists("/old.txt").await.unwrap());
1101        assert!(storage.exists("/new.txt").await.unwrap());
1102
1103        // Directory rename
1104        storage.mkdir("/olddir").await.unwrap();
1105        storage.write("/olddir/file.txt", b"data").await.unwrap();
1106        storage.rename("/olddir", "/newdir").await.unwrap();
1107        assert!(!storage.exists("/olddir").await.unwrap());
1108        assert!(storage.exists("/newdir").await.unwrap());
1109        assert!(storage.exists("/newdir/file.txt").await.unwrap());
1110    }
1111
1112    #[tokio::test]
1113    async fn test_stat() {
1114        let storage = InMemoryStorage::new();
1115
1116        storage.write("/file.txt", b"hello").await.unwrap();
1117        let meta = storage.stat("/file.txt").await.unwrap();
1118        assert!(!meta.is_dir);
1119        assert_eq!(meta.size, 5);
1120
1121        storage.mkdir("/dir").await.unwrap();
1122        let meta = storage.stat("/dir").await.unwrap();
1123        assert!(meta.is_dir);
1124    }
1125
1126    #[tokio::test]
1127    async fn test_write_at() {
1128        let storage = InMemoryStorage::new();
1129
1130        // Write at offset in new file
1131        storage.write_at("/file.txt", 5, b"world").await.unwrap();
1132        let content = storage.read("/file.txt").await.unwrap();
1133        assert_eq!(content.len(), 10);
1134        assert_eq!(&content[5..], b"world");
1135        assert_eq!(&content[0..5], &[0, 0, 0, 0, 0]);
1136
1137        // Overwrite portion
1138        storage.write_at("/file.txt", 0, b"hello").await.unwrap();
1139        let content = storage.read("/file.txt").await.unwrap();
1140        assert_eq!(&content, b"helloworld");
1141    }
1142
1143    #[test]
1144    fn test_mkdir_sync() {
1145        let storage = InMemoryStorage::new();
1146
1147        // Create a directory synchronously
1148        storage.mkdir_sync("/data").unwrap();
1149
1150        // Verify using blocking read
1151        let state = storage.state.blocking_read();
1152        assert!(state.directories.contains("/data"));
1153    }
1154
1155    #[test]
1156    fn test_mkdir_sync_nested() {
1157        let storage = InMemoryStorage::new();
1158
1159        // Create nested directories synchronously
1160        storage.mkdir_sync("/data/subdir/nested").unwrap();
1161
1162        // Verify all intermediate directories were created
1163        let state = storage.state.blocking_read();
1164        assert!(state.directories.contains("/data"));
1165        assert!(state.directories.contains("/data/subdir"));
1166        assert!(state.directories.contains("/data/subdir/nested"));
1167    }
1168}