1#[cfg(all(target_os = "linux", feature = "io-uring"))]
36mod uring;
37#[cfg(all(target_os = "linux", feature = "io-uring"))]
38pub(crate) mod uring_worker;
39
40#[cfg(all(target_os = "linux", feature = "io-uring"))]
41pub use uring::UringVfs;
42
43#[cfg(target_arch = "wasm32")]
44mod opfs;
45
46#[cfg(target_arch = "wasm32")]
47mod worker_opfs;
48
49#[cfg(target_arch = "wasm32")]
50mod indexeddb_vfs;
51
52#[cfg(target_arch = "wasm32")]
53pub use opfs::OpfsVfs;
54
55#[cfg(target_arch = "wasm32")]
56pub use worker_opfs::WorkerOpfsVfs;
57
58#[cfg(target_arch = "wasm32")]
59pub use indexeddb_vfs::IndexedDbVfs;
60
61use async_trait::async_trait;
62use std::io;
63
64#[cfg(not(target_arch = "wasm32"))]
65use std::path::PathBuf;
66
67#[derive(Debug)]
69pub enum VfsError {
70 NotFound(String),
71 PermissionDenied(String),
72 AlreadyExists(String),
73 IoError(io::Error),
74 SerializationError(String),
75 JournalCorrupted(String),
76}
77
78impl std::fmt::Display for VfsError {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 VfsError::NotFound(s) => write!(f, "Not found: {}", s),
82 VfsError::PermissionDenied(s) => write!(f, "Permission denied: {}", s),
83 VfsError::AlreadyExists(s) => write!(f, "Already exists: {}", s),
84 VfsError::IoError(e) => write!(f, "IO error: {}", e),
85 VfsError::SerializationError(s) => write!(f, "Serialization error: {}", s),
86 VfsError::JournalCorrupted(s) => write!(f, "Journal corrupted: {}", s),
87 }
88 }
89}
90
91impl std::error::Error for VfsError {}
92
93impl From<io::Error> for VfsError {
94 fn from(e: io::Error) -> Self {
95 match e.kind() {
96 io::ErrorKind::NotFound => VfsError::NotFound(e.to_string()),
97 io::ErrorKind::PermissionDenied => VfsError::PermissionDenied(e.to_string()),
98 io::ErrorKind::AlreadyExists => VfsError::AlreadyExists(e.to_string()),
99 _ => VfsError::IoError(e),
100 }
101 }
102}
103
104pub type VfsResult<T> = Result<T, VfsError>;
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct DirEntry {
109 pub name: String,
111 pub is_directory: bool,
113}
114
115#[cfg(not(target_arch = "wasm32"))]
120#[async_trait]
121pub trait Vfs: Send + Sync {
122 async fn read(&self, path: &str) -> VfsResult<Vec<u8>>;
124
125 async fn read_to_string(&self, path: &str) -> VfsResult<String>;
127
128 async fn write(&self, path: &str, contents: &[u8]) -> VfsResult<()>;
130
131 async fn append(&self, path: &str, contents: &[u8]) -> VfsResult<()>;
133
134 async fn exists(&self, path: &str) -> VfsResult<bool>;
136
137 async fn remove(&self, path: &str) -> VfsResult<()>;
139
140 async fn create_dir_all(&self, path: &str) -> VfsResult<()>;
142
143 async fn rename(&self, from: &str, to: &str) -> VfsResult<()>;
145
146 async fn list_dir(&self, path: &str) -> VfsResult<Vec<DirEntry>>;
148}
149
150#[cfg(target_arch = "wasm32")]
152#[async_trait(?Send)]
153pub trait Vfs {
154 async fn read(&self, path: &str) -> VfsResult<Vec<u8>>;
156
157 async fn read_to_string(&self, path: &str) -> VfsResult<String>;
159
160 async fn write(&self, path: &str, contents: &[u8]) -> VfsResult<()>;
162
163 async fn append(&self, path: &str, contents: &[u8]) -> VfsResult<()>;
165
166 async fn exists(&self, path: &str) -> VfsResult<bool>;
168
169 async fn remove(&self, path: &str) -> VfsResult<()>;
171
172 async fn create_dir_all(&self, path: &str) -> VfsResult<()>;
174
175 async fn rename(&self, from: &str, to: &str) -> VfsResult<()>;
177
178 async fn list_dir(&self, path: &str) -> VfsResult<Vec<DirEntry>>;
180}
181
182#[cfg(not(target_arch = "wasm32"))]
184pub struct NativeVfs {
185 base_dir: PathBuf,
187}
188
189#[cfg(not(target_arch = "wasm32"))]
190impl NativeVfs {
191 pub fn new<P: Into<PathBuf>>(base_dir: P) -> Self {
193 Self {
194 base_dir: base_dir.into(),
195 }
196 }
197
198 fn resolve(&self, path: &str) -> PathBuf {
200 let clean = path.trim_start_matches('/').trim_start_matches("../");
202 self.base_dir.join(clean)
203 }
204}
205
206#[cfg(not(target_arch = "wasm32"))]
207#[async_trait]
208impl Vfs for NativeVfs {
209 async fn read(&self, path: &str) -> VfsResult<Vec<u8>> {
210 let full_path = self.resolve(path);
211 tokio::fs::read(&full_path).await.map_err(VfsError::from)
212 }
213
214 async fn read_to_string(&self, path: &str) -> VfsResult<String> {
215 let full_path = self.resolve(path);
216 tokio::fs::read_to_string(&full_path).await.map_err(VfsError::from)
217 }
218
219 async fn write(&self, path: &str, contents: &[u8]) -> VfsResult<()> {
220 let full_path = self.resolve(path);
221
222 if let Some(parent) = full_path.parent() {
224 tokio::fs::create_dir_all(parent).await?;
225 }
226
227 let temp_path = full_path.with_extension("tmp");
229 tokio::fs::write(&temp_path, contents).await?;
230 tokio::fs::rename(&temp_path, &full_path).await?;
231
232 Ok(())
233 }
234
235 async fn append(&self, path: &str, contents: &[u8]) -> VfsResult<()> {
236 use tokio::io::AsyncWriteExt;
237
238 let full_path = self.resolve(path);
239
240 if let Some(parent) = full_path.parent() {
242 tokio::fs::create_dir_all(parent).await?;
243 }
244
245 let mut file = tokio::fs::OpenOptions::new()
246 .create(true)
247 .append(true)
248 .open(&full_path)
249 .await?;
250
251 file.write_all(contents).await?;
252 file.sync_all().await?;
253
254 Ok(())
255 }
256
257 async fn exists(&self, path: &str) -> VfsResult<bool> {
258 let full_path = self.resolve(path);
259 Ok(full_path.exists())
260 }
261
262 async fn remove(&self, path: &str) -> VfsResult<()> {
263 let full_path = self.resolve(path);
264 tokio::fs::remove_file(&full_path).await.map_err(VfsError::from)
265 }
266
267 async fn create_dir_all(&self, path: &str) -> VfsResult<()> {
268 let full_path = self.resolve(path);
269 tokio::fs::create_dir_all(&full_path).await.map_err(VfsError::from)
270 }
271
272 async fn rename(&self, from: &str, to: &str) -> VfsResult<()> {
273 let from_path = self.resolve(from);
274 let to_path = self.resolve(to);
275 tokio::fs::rename(&from_path, &to_path).await.map_err(VfsError::from)
276 }
277
278 async fn list_dir(&self, path: &str) -> VfsResult<Vec<DirEntry>> {
279 let full_path = self.resolve(path);
280 let mut entries = Vec::new();
281 let mut read_dir = tokio::fs::read_dir(&full_path).await.map_err(VfsError::from)?;
282
283 while let Some(entry) = read_dir.next_entry().await.map_err(VfsError::from)? {
284 let metadata = entry.metadata().await.map_err(VfsError::from)?;
285 entries.push(DirEntry {
286 name: entry.file_name().to_string_lossy().into_owned(),
287 is_directory: metadata.is_dir(),
288 });
289 }
290
291 entries.sort_by(|a, b| {
293 match (a.is_directory, b.is_directory) {
294 (true, false) => std::cmp::Ordering::Less,
295 (false, true) => std::cmp::Ordering::Greater,
296 _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
297 }
298 });
299
300 Ok(entries)
301 }
302}
303
304#[cfg(not(target_arch = "wasm32"))]
306pub type PlatformVfs = NativeVfs;
307
308#[cfg(target_arch = "wasm32")]
309pub type PlatformVfs = WorkerOpfsVfs;
310
311#[cfg(not(target_arch = "wasm32"))]
318pub fn get_platform_vfs() -> Box<dyn Vfs + Send + Sync> {
319 #[cfg(all(target_os = "linux", feature = "io-uring"))]
320 {
321 match UringVfs::new(".") {
322 Ok(vfs) => return Box::new(vfs),
323 Err(_) => {} }
325 }
326 Box::new(NativeVfs::new("."))
327}
328
329#[cfg(target_arch = "wasm32")]
330pub fn get_platform_vfs() -> VfsResult<WorkerOpfsVfs> {
331 WorkerOpfsVfs::new()
332}
333
334#[cfg(target_arch = "wasm32")]
338#[derive(Clone)]
339pub enum WebVfs {
340 Opfs(WorkerOpfsVfs),
342 IndexedDb(IndexedDbVfs),
344}
345
346#[cfg(target_arch = "wasm32")]
347impl WebVfs {
348 pub fn is_fallback(&self) -> bool {
350 matches!(self, WebVfs::IndexedDb(_))
351 }
352
353 pub fn backend_name(&self) -> &'static str {
355 match self {
356 WebVfs::Opfs(_) => "OPFS",
357 WebVfs::IndexedDb(_) => "IndexedDB",
358 }
359 }
360}
361
362#[cfg(target_arch = "wasm32")]
363#[async_trait(?Send)]
364impl Vfs for WebVfs {
365 async fn read(&self, path: &str) -> VfsResult<Vec<u8>> {
366 match self {
367 WebVfs::Opfs(vfs) => vfs.read(path).await,
368 WebVfs::IndexedDb(vfs) => vfs.read(path).await,
369 }
370 }
371
372 async fn read_to_string(&self, path: &str) -> VfsResult<String> {
373 match self {
374 WebVfs::Opfs(vfs) => vfs.read_to_string(path).await,
375 WebVfs::IndexedDb(vfs) => vfs.read_to_string(path).await,
376 }
377 }
378
379 async fn write(&self, path: &str, contents: &[u8]) -> VfsResult<()> {
380 match self {
381 WebVfs::Opfs(vfs) => vfs.write(path, contents).await,
382 WebVfs::IndexedDb(vfs) => vfs.write(path, contents).await,
383 }
384 }
385
386 async fn append(&self, path: &str, contents: &[u8]) -> VfsResult<()> {
387 match self {
388 WebVfs::Opfs(vfs) => vfs.append(path, contents).await,
389 WebVfs::IndexedDb(vfs) => vfs.append(path, contents).await,
390 }
391 }
392
393 async fn exists(&self, path: &str) -> VfsResult<bool> {
394 match self {
395 WebVfs::Opfs(vfs) => vfs.exists(path).await,
396 WebVfs::IndexedDb(vfs) => vfs.exists(path).await,
397 }
398 }
399
400 async fn remove(&self, path: &str) -> VfsResult<()> {
401 match self {
402 WebVfs::Opfs(vfs) => vfs.remove(path).await,
403 WebVfs::IndexedDb(vfs) => vfs.remove(path).await,
404 }
405 }
406
407 async fn create_dir_all(&self, path: &str) -> VfsResult<()> {
408 match self {
409 WebVfs::Opfs(vfs) => vfs.create_dir_all(path).await,
410 WebVfs::IndexedDb(vfs) => vfs.create_dir_all(path).await,
411 }
412 }
413
414 async fn rename(&self, from: &str, to: &str) -> VfsResult<()> {
415 match self {
416 WebVfs::Opfs(vfs) => vfs.rename(from, to).await,
417 WebVfs::IndexedDb(vfs) => vfs.rename(from, to).await,
418 }
419 }
420
421 async fn list_dir(&self, path: &str) -> VfsResult<Vec<DirEntry>> {
422 match self {
423 WebVfs::Opfs(vfs) => vfs.list_dir(path).await,
424 WebVfs::IndexedDb(vfs) => vfs.list_dir(path).await,
425 }
426 }
427}
428
429#[cfg(target_arch = "wasm32")]
436pub async fn get_platform_vfs_with_fallback() -> VfsResult<WebVfs> {
437 use wasm_bindgen::prelude::*;
438
439 #[wasm_bindgen]
440 extern "C" {
441 #[wasm_bindgen(js_namespace = console)]
442 fn log(s: &str);
443 }
444
445 log("[VFS] Attempting OPFS initialization...");
446
447 match WorkerOpfsVfs::new() {
449 Ok(opfs) => {
450 match opfs.create_dir_all("/").await {
454 Ok(_) => {
455 log("[VFS] OPFS initialized successfully");
456 return Ok(WebVfs::Opfs(opfs));
457 }
458 Err(e) => {
459 log(&format!("[VFS] OPFS test failed: {:?}, trying IndexedDB...", e));
460 }
461 }
462 }
463 Err(e) => {
464 log(&format!("[VFS] OPFS creation failed: {:?}, trying IndexedDB...", e));
465 }
466 }
467
468 log("[VFS] Falling back to IndexedDB...");
470 match IndexedDbVfs::new().await {
471 Ok(idb) => {
472 log("[VFS] IndexedDB initialized successfully");
473 Ok(WebVfs::IndexedDb(idb))
474 }
475 Err(e) => {
476 log(&format!("[VFS] IndexedDB initialization failed: {:?}", e));
477 Err(e)
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use tempfile::TempDir;
486
487 #[tokio::test]
488 async fn test_native_vfs_read_write() {
489 let temp = TempDir::new().unwrap();
490 let vfs = NativeVfs::new(temp.path());
491
492 vfs.write("test.txt", b"hello world").await.unwrap();
493 let content = vfs.read_to_string("test.txt").await.unwrap();
494
495 assert_eq!(content, "hello world");
496 }
497
498 #[tokio::test]
499 async fn test_native_vfs_append() {
500 let temp = TempDir::new().unwrap();
501 let vfs = NativeVfs::new(temp.path());
502
503 vfs.append("log.txt", b"line1\n").await.unwrap();
504 vfs.append("log.txt", b"line2\n").await.unwrap();
505
506 let content = vfs.read_to_string("log.txt").await.unwrap();
507 assert_eq!(content, "line1\nline2\n");
508 }
509
510 #[tokio::test]
511 async fn test_native_vfs_nested_dirs() {
512 let temp = TempDir::new().unwrap();
513 let vfs = NativeVfs::new(temp.path());
514
515 vfs.write("a/b/c/file.txt", b"deep").await.unwrap();
516 let content = vfs.read_to_string("a/b/c/file.txt").await.unwrap();
517
518 assert_eq!(content, "deep");
519 }
520
521 #[tokio::test]
522 async fn test_native_vfs_list_dir() {
523 let temp = TempDir::new().unwrap();
524 let vfs = NativeVfs::new(temp.path());
525
526 vfs.write("file1.txt", b"content1").await.unwrap();
528 vfs.write("file2.txt", b"content2").await.unwrap();
529 vfs.write("subdir/nested.txt", b"nested").await.unwrap();
530
531 let entries = vfs.list_dir("").await.unwrap();
533
534 assert_eq!(entries.len(), 3);
536
537 assert_eq!(entries[0].name, "subdir");
539 assert!(entries[0].is_directory);
540
541 assert_eq!(entries[1].name, "file1.txt");
543 assert!(!entries[1].is_directory);
544 assert_eq!(entries[2].name, "file2.txt");
545 assert!(!entries[2].is_directory);
546 }
547
548 #[tokio::test]
551 async fn test_native_vfs_exists_returns_false_for_missing_file() {
552 let temp = TempDir::new().unwrap();
553 let vfs = NativeVfs::new(temp.path());
554
555 assert!(!vfs.exists("nope.txt").await.unwrap());
556 }
557
558 #[tokio::test]
559 async fn test_native_vfs_exists_returns_true_after_write() {
560 let temp = TempDir::new().unwrap();
561 let vfs = NativeVfs::new(temp.path());
562
563 vfs.write("yes.txt", b"data").await.unwrap();
564 assert!(vfs.exists("yes.txt").await.unwrap());
565 }
566
567 #[tokio::test]
568 async fn test_native_vfs_remove_deletes_file() {
569 let temp = TempDir::new().unwrap();
570 let vfs = NativeVfs::new(temp.path());
571
572 vfs.write("doomed.txt", b"bye").await.unwrap();
573 assert!(vfs.exists("doomed.txt").await.unwrap());
574
575 vfs.remove("doomed.txt").await.unwrap();
576 assert!(!vfs.exists("doomed.txt").await.unwrap());
577 }
578
579 #[tokio::test]
580 async fn test_native_vfs_remove_nonexistent_returns_error() {
581 let temp = TempDir::new().unwrap();
582 let vfs = NativeVfs::new(temp.path());
583
584 let result = vfs.remove("ghost.txt").await;
585 assert!(result.is_err());
586 }
587
588 #[tokio::test]
589 async fn test_native_vfs_rename_moves_file() {
590 let temp = TempDir::new().unwrap();
591 let vfs = NativeVfs::new(temp.path());
592
593 vfs.write("old.txt", b"content").await.unwrap();
594 vfs.rename("old.txt", "new.txt").await.unwrap();
595
596 assert!(!vfs.exists("old.txt").await.unwrap());
597 let content = vfs.read_to_string("new.txt").await.unwrap();
598 assert_eq!(content, "content");
599 }
600
601 #[tokio::test]
602 async fn test_native_vfs_create_dir_all() {
603 let temp = TempDir::new().unwrap();
604 let vfs = NativeVfs::new(temp.path());
605
606 vfs.create_dir_all("x/y/z").await.unwrap();
607 vfs.write("x/y/z/deep.txt", b"found").await.unwrap();
608 let content = vfs.read_to_string("x/y/z/deep.txt").await.unwrap();
609 assert_eq!(content, "found");
610 }
611
612 #[tokio::test]
615 async fn test_native_vfs_write_overwrites_existing() {
616 let temp = TempDir::new().unwrap();
617 let vfs = NativeVfs::new(temp.path());
618
619 vfs.write("over.txt", b"first").await.unwrap();
620 vfs.write("over.txt", b"second").await.unwrap();
621 let content = vfs.read_to_string("over.txt").await.unwrap();
622 assert_eq!(content, "second");
623 }
624
625 #[tokio::test]
626 async fn test_native_vfs_write_empty_file() {
627 let temp = TempDir::new().unwrap();
628 let vfs = NativeVfs::new(temp.path());
629
630 vfs.write("empty.txt", b"").await.unwrap();
631 let content = vfs.read("empty.txt").await.unwrap();
632 assert!(content.is_empty());
633 }
634
635 #[tokio::test]
636 async fn test_native_vfs_write_no_leftover_tmp() {
637 let temp = TempDir::new().unwrap();
638 let vfs = NativeVfs::new(temp.path());
639
640 vfs.write("clean.txt", b"data").await.unwrap();
641
642 assert!(!vfs.exists("clean.tmp").await.unwrap());
644 }
645
646 #[tokio::test]
649 async fn test_native_vfs_read_nonexistent_returns_not_found() {
650 let temp = TempDir::new().unwrap();
651 let vfs = NativeVfs::new(temp.path());
652
653 let result = vfs.read("missing.txt").await;
654 assert!(matches!(result, Err(VfsError::NotFound(_))));
655 }
656
657 #[tokio::test]
658 async fn test_native_vfs_read_to_string_nonexistent_returns_not_found() {
659 let temp = TempDir::new().unwrap();
660 let vfs = NativeVfs::new(temp.path());
661
662 let result = vfs.read_to_string("missing.txt").await;
663 assert!(matches!(result, Err(VfsError::NotFound(_))));
664 }
665
666 #[tokio::test]
669 async fn test_native_vfs_append_creates_file_if_missing() {
670 let temp = TempDir::new().unwrap();
671 let vfs = NativeVfs::new(temp.path());
672
673 vfs.append("new.txt", b"first").await.unwrap();
674 let content = vfs.read_to_string("new.txt").await.unwrap();
675 assert_eq!(content, "first");
676 }
677
678 #[tokio::test]
679 async fn test_native_vfs_append_multiple_sequential() {
680 let temp = TempDir::new().unwrap();
681 let vfs = NativeVfs::new(temp.path());
682
683 for i in 0..10 {
684 vfs.append("journal.log", format!("entry-{}\n", i).as_bytes())
685 .await
686 .unwrap();
687 }
688
689 let content = vfs.read_to_string("journal.log").await.unwrap();
690 let lines: Vec<&str> = content.lines().collect();
691 assert_eq!(lines.len(), 10);
692 assert_eq!(lines[0], "entry-0");
693 assert_eq!(lines[9], "entry-9");
694 }
695
696 #[tokio::test]
699 async fn test_native_vfs_read_write_binary() {
700 let temp = TempDir::new().unwrap();
701 let vfs = NativeVfs::new(temp.path());
702
703 let binary: Vec<u8> = (0u8..=255).collect();
704 vfs.write("binary.bin", &binary).await.unwrap();
705 let read_back = vfs.read("binary.bin").await.unwrap();
706 assert_eq!(read_back, binary);
707 }
708
709 #[tokio::test]
712 async fn test_native_vfs_resolve_strips_path_traversal() {
713 let temp = TempDir::new().unwrap();
714 let vfs = NativeVfs::new(temp.path());
715
716 vfs.write("../../../etc/passwd", b"nope").await.unwrap();
718 assert!(vfs.exists("etc/passwd").await.unwrap());
720 }
721
722 #[tokio::test]
723 async fn test_native_vfs_resolve_strips_leading_slash() {
724 let temp = TempDir::new().unwrap();
725 let vfs = NativeVfs::new(temp.path());
726
727 vfs.write("/absolute.txt", b"data").await.unwrap();
728 assert!(vfs.exists("absolute.txt").await.unwrap());
729 }
730
731 #[tokio::test]
734 async fn test_native_vfs_list_dir_empty() {
735 let temp = TempDir::new().unwrap();
736 let vfs = NativeVfs::new(temp.path());
737
738 let entries = vfs.list_dir("").await.unwrap();
739 assert!(entries.is_empty());
740 }
741
742 #[tokio::test]
743 async fn test_native_vfs_list_dir_nonexistent_returns_error() {
744 let temp = TempDir::new().unwrap();
745 let vfs = NativeVfs::new(temp.path());
746
747 let result = vfs.list_dir("nonexistent").await;
748 assert!(result.is_err());
749 }
750
751 #[tokio::test]
752 async fn test_native_vfs_list_dir_case_insensitive_sort() {
753 let temp = TempDir::new().unwrap();
754 let vfs = NativeVfs::new(temp.path());
755
756 vfs.write("Zebra.txt", b"z").await.unwrap();
757 vfs.write("apple.txt", b"a").await.unwrap();
758 vfs.write("Banana.txt", b"b").await.unwrap();
759
760 let entries = vfs.list_dir("").await.unwrap();
761 assert_eq!(entries.len(), 3);
762 assert_eq!(entries[0].name, "apple.txt");
763 assert_eq!(entries[1].name, "Banana.txt");
764 assert_eq!(entries[2].name, "Zebra.txt");
765 }
766
767 #[tokio::test]
770 async fn test_get_platform_vfs_returns_boxed_vfs() {
771 let vfs = get_platform_vfs();
774 let result = vfs.exists("__nonexistent_test_file__").await;
776 assert!(result.is_ok());
777 }
778
779 #[tokio::test]
780 async fn test_dyn_vfs_supports_all_operations() {
781 let temp = TempDir::new().unwrap();
782 let vfs: Box<dyn Vfs + Send + Sync> = Box::new(NativeVfs::new(temp.path()));
783
784 vfs.write("test.txt", b"hello").await.unwrap();
786 assert_eq!(vfs.read("test.txt").await.unwrap(), b"hello");
787
788 assert_eq!(vfs.read_to_string("test.txt").await.unwrap(), "hello");
790
791 assert!(vfs.exists("test.txt").await.unwrap());
793 assert!(!vfs.exists("nope.txt").await.unwrap());
794
795 vfs.append("test.txt", b" world").await.unwrap();
797 assert_eq!(vfs.read_to_string("test.txt").await.unwrap(), "hello world");
798
799 vfs.create_dir_all("a/b").await.unwrap();
801 vfs.write("a/b/nested.txt", b"deep").await.unwrap();
802 assert_eq!(vfs.read_to_string("a/b/nested.txt").await.unwrap(), "deep");
803
804 vfs.rename("test.txt", "moved.txt").await.unwrap();
806 assert!(!vfs.exists("test.txt").await.unwrap());
807 assert_eq!(
808 vfs.read_to_string("moved.txt").await.unwrap(),
809 "hello world"
810 );
811
812 let entries = vfs.list_dir("").await.unwrap();
814 assert!(entries.len() >= 2);
815
816 vfs.remove("moved.txt").await.unwrap();
818 assert!(!vfs.exists("moved.txt").await.unwrap());
819 }
820
821 #[tokio::test]
822 async fn test_dyn_vfs_can_be_wrapped_in_arc() {
823 let temp = TempDir::new().unwrap();
824 let vfs: std::sync::Arc<dyn Vfs + Send + Sync> =
825 std::sync::Arc::from(Box::new(NativeVfs::new(temp.path())) as Box<dyn Vfs + Send + Sync>);
826
827 let vfs_clone = vfs.clone();
829 vfs.write("arc_test.txt", b"shared").await.unwrap();
830 let content = vfs_clone.read_to_string("arc_test.txt").await.unwrap();
831 assert_eq!(content, "shared");
832 }
833
834 #[cfg(all(target_os = "linux", feature = "io-uring"))]
837 mod uring_tests {
838 use super::*;
839
840 #[tokio::test]
841 async fn test_uring_vfs_creation() {
842 let temp = TempDir::new().unwrap();
843 let result = super::super::UringVfs::new(temp.path());
844 assert!(result.is_ok(), "UringVfs::new should succeed on supported Linux kernels");
845 }
846
847 #[tokio::test]
848 async fn test_uring_vfs_read_write() {
849 let temp = TempDir::new().unwrap();
850 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
851
852 vfs.write("test.txt", b"hello world").await.unwrap();
853 let content = vfs.read_to_string("test.txt").await.unwrap();
854 assert_eq!(content, "hello world");
855 }
856
857 #[tokio::test]
858 async fn test_uring_vfs_read_bytes() {
859 let temp = TempDir::new().unwrap();
860 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
861
862 let data = b"binary data \x00\x01\x02\xff";
863 vfs.write("bin.dat", data).await.unwrap();
864 let read_back = vfs.read("bin.dat").await.unwrap();
865 assert_eq!(read_back, data);
866 }
867
868 #[tokio::test]
869 async fn test_uring_vfs_append() {
870 let temp = TempDir::new().unwrap();
871 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
872
873 vfs.append("log.txt", b"line1\n").await.unwrap();
874 vfs.append("log.txt", b"line2\n").await.unwrap();
875
876 let content = vfs.read_to_string("log.txt").await.unwrap();
877 assert_eq!(content, "line1\nline2\n");
878 }
879
880 #[tokio::test]
881 async fn test_uring_vfs_append_creates_file() {
882 let temp = TempDir::new().unwrap();
883 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
884
885 vfs.append("new.log", b"first entry\n").await.unwrap();
886 let content = vfs.read_to_string("new.log").await.unwrap();
887 assert_eq!(content, "first entry\n");
888 }
889
890 #[tokio::test]
891 async fn test_uring_vfs_append_many_sequential() {
892 let temp = TempDir::new().unwrap();
893 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
894
895 for i in 0..100 {
896 vfs.append("journal.log", format!("entry-{}\n", i).as_bytes())
897 .await
898 .unwrap();
899 }
900
901 let content = vfs.read_to_string("journal.log").await.unwrap();
902 let lines: Vec<&str> = content.lines().collect();
903 assert_eq!(lines.len(), 100);
904 assert_eq!(lines[0], "entry-0");
905 assert_eq!(lines[99], "entry-99");
906 }
907
908 #[tokio::test]
909 async fn test_uring_vfs_nested_dirs() {
910 let temp = TempDir::new().unwrap();
911 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
912
913 vfs.write("a/b/c/file.txt", b"deep").await.unwrap();
914 let content = vfs.read_to_string("a/b/c/file.txt").await.unwrap();
915 assert_eq!(content, "deep");
916 }
917
918 #[tokio::test]
919 async fn test_uring_vfs_exists() {
920 let temp = TempDir::new().unwrap();
921 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
922
923 assert!(!vfs.exists("missing.txt").await.unwrap());
924 vfs.write("present.txt", b"here").await.unwrap();
925 assert!(vfs.exists("present.txt").await.unwrap());
926 }
927
928 #[tokio::test]
929 async fn test_uring_vfs_remove() {
930 let temp = TempDir::new().unwrap();
931 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
932
933 vfs.write("doomed.txt", b"bye").await.unwrap();
934 assert!(vfs.exists("doomed.txt").await.unwrap());
935
936 vfs.remove("doomed.txt").await.unwrap();
937 assert!(!vfs.exists("doomed.txt").await.unwrap());
938 }
939
940 #[tokio::test]
941 async fn test_uring_vfs_remove_nonexistent_returns_error() {
942 let temp = TempDir::new().unwrap();
943 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
944
945 let result = vfs.remove("ghost.txt").await;
946 assert!(result.is_err());
947 }
948
949 #[tokio::test]
950 async fn test_uring_vfs_rename() {
951 let temp = TempDir::new().unwrap();
952 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
953
954 vfs.write("old.txt", b"content").await.unwrap();
955 vfs.rename("old.txt", "new.txt").await.unwrap();
956
957 assert!(!vfs.exists("old.txt").await.unwrap());
958 let content = vfs.read_to_string("new.txt").await.unwrap();
959 assert_eq!(content, "content");
960 }
961
962 #[tokio::test]
963 async fn test_uring_vfs_create_dir_all() {
964 let temp = TempDir::new().unwrap();
965 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
966
967 vfs.create_dir_all("x/y/z").await.unwrap();
968 vfs.write("x/y/z/deep.txt", b"found").await.unwrap();
969 let content = vfs.read_to_string("x/y/z/deep.txt").await.unwrap();
970 assert_eq!(content, "found");
971 }
972
973 #[tokio::test]
974 async fn test_uring_vfs_list_dir() {
975 let temp = TempDir::new().unwrap();
976 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
977
978 vfs.write("file1.txt", b"1").await.unwrap();
979 vfs.write("file2.txt", b"2").await.unwrap();
980 vfs.write("subdir/nested.txt", b"n").await.unwrap();
981
982 let entries = vfs.list_dir("").await.unwrap();
983 assert_eq!(entries.len(), 3);
984 assert_eq!(entries[0].name, "subdir");
985 assert!(entries[0].is_directory);
986 assert_eq!(entries[1].name, "file1.txt");
987 assert_eq!(entries[2].name, "file2.txt");
988 }
989
990 #[tokio::test]
991 async fn test_uring_vfs_list_dir_empty() {
992 let temp = TempDir::new().unwrap();
993 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
994
995 let entries = vfs.list_dir("").await.unwrap();
996 assert!(entries.is_empty());
997 }
998
999 #[tokio::test]
1000 async fn test_uring_vfs_atomic_write_overwrites() {
1001 let temp = TempDir::new().unwrap();
1002 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1003
1004 vfs.write("over.txt", b"first").await.unwrap();
1005 vfs.write("over.txt", b"second").await.unwrap();
1006 let content = vfs.read_to_string("over.txt").await.unwrap();
1007 assert_eq!(content, "second");
1008 }
1009
1010 #[tokio::test]
1011 async fn test_uring_vfs_atomic_write_no_leftover_tmp() {
1012 let temp = TempDir::new().unwrap();
1013 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1014
1015 vfs.write("clean.txt", b"data").await.unwrap();
1016 assert!(!vfs.exists("clean.tmp").await.unwrap());
1017 }
1018
1019 #[tokio::test]
1020 async fn test_uring_vfs_write_empty_file() {
1021 let temp = TempDir::new().unwrap();
1022 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1023
1024 vfs.write("empty.txt", b"").await.unwrap();
1025 let content = vfs.read("empty.txt").await.unwrap();
1026 assert!(content.is_empty());
1027 }
1028
1029 #[tokio::test]
1030 async fn test_uring_vfs_read_nonexistent_returns_error() {
1031 let temp = TempDir::new().unwrap();
1032 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1033
1034 let result = vfs.read("missing.txt").await;
1035 assert!(result.is_err());
1036 }
1037
1038 #[tokio::test]
1039 async fn test_uring_vfs_binary_roundtrip() {
1040 let temp = TempDir::new().unwrap();
1041 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1042
1043 let binary: Vec<u8> = (0u8..=255).collect();
1044 vfs.write("binary.bin", &binary).await.unwrap();
1045 let read_back = vfs.read("binary.bin").await.unwrap();
1046 assert_eq!(read_back, binary);
1047 }
1048
1049 #[tokio::test]
1050 async fn test_uring_vfs_large_file_roundtrip() {
1051 let temp = TempDir::new().unwrap();
1052 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1053
1054 let large: Vec<u8> = (0..1_048_576).map(|i| (i % 256) as u8).collect();
1056 vfs.write("large.bin", &large).await.unwrap();
1057 let read_back = vfs.read("large.bin").await.unwrap();
1058 assert_eq!(read_back.len(), large.len());
1059 assert_eq!(read_back, large);
1060 }
1061
1062 #[tokio::test]
1063 async fn test_uring_vfs_path_traversal_prevention() {
1064 let temp = TempDir::new().unwrap();
1065 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1066
1067 vfs.write("../../../etc/passwd", b"nope").await.unwrap();
1068 assert!(vfs.exists("etc/passwd").await.unwrap());
1069 }
1070
1071 #[tokio::test]
1072 async fn test_uring_vfs_concurrent_reads() {
1073 let temp = TempDir::new().unwrap();
1074 let vfs = std::sync::Arc::new(super::super::UringVfs::new(temp.path()).unwrap());
1075
1076 for i in 0..10 {
1078 vfs.write(&format!("file_{}.txt", i), format!("content_{}", i).as_bytes())
1079 .await
1080 .unwrap();
1081 }
1082
1083 let mut handles = Vec::new();
1085 for i in 0..10 {
1086 let vfs = vfs.clone();
1087 handles.push(tokio::spawn(async move {
1088 let content = vfs.read_to_string(&format!("file_{}.txt", i)).await.unwrap();
1089 assert_eq!(content, format!("content_{}", i));
1090 }));
1091 }
1092
1093 for handle in handles {
1094 handle.await.unwrap();
1095 }
1096 }
1097
1098 #[tokio::test]
1099 async fn test_uring_vfs_concurrent_appends() {
1100 let temp = TempDir::new().unwrap();
1101 let vfs = std::sync::Arc::new(super::super::UringVfs::new(temp.path()).unwrap());
1102
1103 let mut handles = Vec::new();
1105 for i in 0..50 {
1106 let vfs = vfs.clone();
1107 handles.push(tokio::spawn(async move {
1108 vfs.append("concurrent.log", format!("entry-{}\n", i).as_bytes())
1109 .await
1110 .unwrap();
1111 }));
1112 }
1113
1114 for handle in handles {
1115 handle.await.unwrap();
1116 }
1117
1118 let content = vfs.read_to_string("concurrent.log").await.unwrap();
1119 let lines: Vec<&str> = content.lines().collect();
1120 assert_eq!(lines.len(), 50);
1121 }
1122
1123 #[tokio::test]
1124 async fn test_uring_vfs_worker_shutdown_clean() {
1125 let temp = TempDir::new().unwrap();
1126
1127 {
1129 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1130 vfs.write("shutdown_test.txt", b"data").await.unwrap();
1131 }
1132 let vfs = NativeVfs::new(temp.path());
1136 let content = vfs.read_to_string("shutdown_test.txt").await.unwrap();
1137 assert_eq!(content, "data");
1138 }
1139
1140 #[tokio::test]
1141 async fn test_uring_vfs_operations_after_many_writes() {
1142 let temp = TempDir::new().unwrap();
1143 let vfs = super::super::UringVfs::new(temp.path()).unwrap();
1144
1145 for i in 0..200 {
1147 let name = format!("file_{}.txt", i);
1148 let content = format!("content_{}", i);
1149 vfs.write(&name, content.as_bytes()).await.unwrap();
1150 }
1151
1152 for i in 0..200 {
1153 let name = format!("file_{}.txt", i);
1154 let expected = format!("content_{}", i);
1155 let actual = vfs.read_to_string(&name).await.unwrap();
1156 assert_eq!(actual, expected, "file {} content mismatch", i);
1157 }
1158 }
1159
1160 #[tokio::test]
1161 async fn test_uring_vfs_matches_native_vfs_behavior() {
1162 let temp1 = TempDir::new().unwrap();
1163 let temp2 = TempDir::new().unwrap();
1164 let uring = super::super::UringVfs::new(temp1.path()).unwrap();
1165 let native = NativeVfs::new(temp2.path());
1166
1167 let ops = vec![
1169 ("write", "test.txt", b"hello" as &[u8]),
1170 ("write", "dir/nested.txt", b"nested"),
1171 ("append", "log.txt", b"line1\n"),
1172 ("append", "log.txt", b"line2\n"),
1173 ];
1174
1175 for (op, path, data) in &ops {
1176 match *op {
1177 "write" => {
1178 uring.write(path, data).await.unwrap();
1179 native.write(path, data).await.unwrap();
1180 }
1181 "append" => {
1182 uring.append(path, data).await.unwrap();
1183 native.append(path, data).await.unwrap();
1184 }
1185 _ => unreachable!(),
1186 }
1187 }
1188
1189 for path in &["test.txt", "dir/nested.txt", "log.txt"] {
1191 let uring_content = uring.read(path).await.unwrap();
1192 let native_content = native.read(path).await.unwrap();
1193 assert_eq!(uring_content, native_content, "mismatch for {}", path);
1194 }
1195
1196 let uring_entries = uring.list_dir("").await.unwrap();
1198 let native_entries = native.list_dir("").await.unwrap();
1199 assert_eq!(uring_entries.len(), native_entries.len());
1200 for (u, n) in uring_entries.iter().zip(native_entries.iter()) {
1201 assert_eq!(u.name, n.name);
1202 assert_eq!(u.is_directory, n.is_directory);
1203 }
1204 }
1205 }
1206
1207 #[cfg(all(target_os = "linux", feature = "io-uring"))]
1210 #[tokio::test]
1211 async fn test_uring_vfs_fallback_on_unsupported() {
1212 let temp = TempDir::new().unwrap();
1215 std::env::set_current_dir(temp.path()).unwrap();
1216
1217 let vfs = get_platform_vfs();
1218 vfs.write("fallback.txt", b"works").await.unwrap();
1219 let content = vfs.read_to_string("fallback.txt").await.unwrap();
1220 assert_eq!(content, "works");
1221 }
1222}