1use 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#[derive(Debug, Clone)]
14pub struct Metadata {
15 pub is_dir: bool,
17 pub size: u64,
19 pub created: SystemTime,
21 pub modified: SystemTime,
23 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#[derive(Debug, Clone)]
42pub struct DirEntry {
43 pub name: String,
45 pub metadata: Metadata,
47}
48
49#[async_trait]
54pub trait VfsStorage: Send + Sync {
55 fn as_any(&self) -> Option<&dyn core::any::Any> {
62 None
63 }
64
65 async fn read(&self, path: &str) -> VfsResult<Vec<u8>>;
67
68 async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>>;
70
71 async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()>;
73
74 async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()>;
76
77 async fn set_size(&self, path: &str, size: u64) -> VfsResult<()>;
79
80 async fn delete(&self, path: &str) -> VfsResult<()>;
82
83 async fn exists(&self, path: &str) -> VfsResult<bool>;
85
86 async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>>;
88
89 async fn stat(&self, path: &str) -> VfsResult<Metadata>;
91
92 async fn mkdir(&self, path: &str) -> VfsResult<()>;
94
95 async fn rmdir(&self, path: &str) -> VfsResult<()>;
97
98 async fn rename(&self, from: &str, to: &str) -> VfsResult<()>;
100
101 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#[derive(Clone)]
130pub struct ArcStorage(Arc<dyn VfsStorage>);
131
132impl ArcStorage {
133 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#[derive(Debug, Default, Clone)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209struct StorageState {
210 files: HashMap<String, FileData>,
212 directories: HashSet<String>,
214}
215
216pub const DEFAULT_MAX_BYTES: u64 = 64 * 1024 * 1024;
224
225#[derive(Debug)]
234pub struct InMemoryStorage {
235 state: RwLock<StorageState>,
237 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#[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 #[must_use]
277 pub fn new() -> Self {
278 Self::with_max_bytes(DEFAULT_MAX_BYTES)
279 }
280
281 #[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 #[must_use]
302 pub fn max_bytes(&self) -> u64 {
303 self.max_bytes
304 }
305
306 pub async fn snapshot(&self) -> InMemorySnapshot {
311 InMemorySnapshot {
312 state: self.state.read().await.clone(),
313 }
314 }
315
316 pub async fn restore(&self, snapshot: &InMemorySnapshot) {
322 *self.state.write().await = snapshot.state.clone();
323 }
324
325 #[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 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 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 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 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.try_into().map_err(|_| {
417 VfsError::QuotaExceeded(format!(
418 "{path}: {new_len} bytes exceeds host addressable size"
419 ))
420 })
421 }
422
423 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 let content_len = data.content.len().try_into().unwrap_or(u64::MAX);
463 if offset >= content_len {
464 Ok(Vec::new())
465 } else {
466 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 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 if state.directories.contains(&path) {
515 return Err(VfsError::NotFile(path));
516 }
517
518 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 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 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 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 for (file_path, data) in &state.files {
612 if let Some(rest) = file_path.strip_prefix(&prefix) {
613 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 for dir_path in &state.directories {
632 if let Some(rest) = dir_path.strip_prefix(&prefix) {
633 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 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 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 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 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 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 if state.files.contains_key(&from) {
753 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 if state.directories.contains(&from) {
766 if state.files.contains_key(&to) {
768 return Err(VfsError::AlreadyExists(to));
769 }
770
771 let from_prefix = format!("{from}/");
773 let to_prefix = format!("{to}/");
774
775 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 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 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 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 let Ok(handle) = tokio::runtime::Handle::try_current() {
835 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 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 #[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 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 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 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 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 let err = storage.write("/b", &vec![0u8; 600]).await.unwrap_err();
973 assert!(matches!(err, VfsError::QuotaExceeded(_)), "{err:?}");
974
975 storage.delete("/a").await.unwrap();
977 storage.write("/b", &vec![0u8; 600]).await.unwrap();
978
979 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 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 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 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 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 let partial = storage.read_at("/test.txt", 2, 3).await.unwrap();
1039 assert_eq!(partial, b"llo");
1040
1041 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 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 storage.mkdir("/subdir").await.unwrap();
1057 assert!(storage.exists("/subdir").await.unwrap());
1058
1059 storage.write("/subdir/file.txt", b"content").await.unwrap();
1061
1062 let entries = storage.list("/subdir").await.unwrap();
1064 assert_eq!(entries.len(), 1);
1065 assert_eq!(entries[0].name, "file.txt");
1066
1067 assert!(storage.rmdir("/subdir").await.is_err());
1069
1070 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 assert!(storage.exists("/test.txt").await.unwrap());
1084 assert!(storage.exists("/./test.txt").await.unwrap());
1085
1086 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 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 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 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 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 storage.mkdir_sync("/data").unwrap();
1149
1150 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 storage.mkdir_sync("/data/subdir/nested").unwrap();
1161
1162 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}