1use async_trait::async_trait;
2use log::warn;
3use once_cell::sync::OnceCell;
4use std::ffi::OsString;
5use std::fmt;
6use std::io::{self, ErrorKind, Read, Seek, Write};
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex, MutexGuard, RwLock};
9use std::time::SystemTime;
10
11#[cfg(not(target_arch = "wasm32"))]
12mod memory;
13#[cfg(not(target_arch = "wasm32"))]
14mod native;
15#[cfg(not(target_arch = "wasm32"))]
16pub mod remote;
17#[cfg(not(target_arch = "wasm32"))]
18pub mod sandbox;
19#[cfg(target_arch = "wasm32")]
20mod wasm;
21
22#[cfg(not(target_arch = "wasm32"))]
23pub use memory::MemoryFsProvider;
24#[cfg(not(target_arch = "wasm32"))]
25pub use native::NativeFsProvider;
26#[cfg(not(target_arch = "wasm32"))]
27pub use remote::{RemoteFsConfig, RemoteFsProvider};
28#[cfg(not(target_arch = "wasm32"))]
29pub use sandbox::SandboxFsProvider;
30#[cfg(target_arch = "wasm32")]
31pub use wasm::PlaceholderFsProvider;
32
33pub mod data_contract;
34
35use data_contract::{
36 DataChunkUploadRequest, DataChunkUploadTarget, DataManifestDescriptor, DataManifestRequest,
37};
38
39#[async_trait(?Send)]
40pub trait FileHandle: Read + Write + Seek + Send + Sync {
41 async fn metadata_async(&self) -> io::Result<FsMetadata> {
42 Err(io::Error::new(
43 ErrorKind::Unsupported,
44 "file handle metadata is not supported by this provider",
45 ))
46 }
47
48 async fn flush_async(&mut self) -> io::Result<()> {
49 self.flush()
50 }
51
52 async fn sync_all_async(&mut self) -> io::Result<()> {
53 self.flush_async().await
54 }
55}
56
57#[async_trait(?Send)]
58impl FileHandle for std::fs::File {
59 async fn metadata_async(&self) -> io::Result<FsMetadata> {
60 let meta = std::fs::File::metadata(self)?;
61 let file_type = meta.file_type();
62 Ok(FsMetadata {
63 file_type: if file_type.is_dir() {
64 FsFileType::Directory
65 } else if file_type.is_file() {
66 FsFileType::File
67 } else if file_type.is_symlink() {
68 FsFileType::Symlink
69 } else {
70 FsFileType::Other
71 },
72 len: meta.len(),
73 modified: meta.modified().ok(),
74 readonly: meta.permissions().readonly(),
75 hash: None,
76 })
77 }
78
79 async fn sync_all_async(&mut self) -> io::Result<()> {
80 std::fs::File::sync_all(self)
81 }
82}
83
84#[derive(Clone, Debug, Default)]
85pub struct OpenFlags {
86 pub read: bool,
87 pub write: bool,
88 pub append: bool,
89 pub truncate: bool,
90 pub create: bool,
91 pub create_new: bool,
92}
93
94#[derive(Clone, Debug)]
95pub struct OpenOptions {
96 flags: OpenFlags,
97}
98
99impl OpenOptions {
100 pub fn new() -> Self {
101 Self {
102 flags: OpenFlags::default(),
103 }
104 }
105
106 pub fn read(&mut self, value: bool) -> &mut Self {
107 self.flags.read = value;
108 self
109 }
110
111 pub fn write(&mut self, value: bool) -> &mut Self {
112 self.flags.write = value;
113 self
114 }
115
116 pub fn append(&mut self, value: bool) -> &mut Self {
117 self.flags.append = value;
118 self
119 }
120
121 pub fn truncate(&mut self, value: bool) -> &mut Self {
122 self.flags.truncate = value;
123 self
124 }
125
126 pub fn create(&mut self, value: bool) -> &mut Self {
127 self.flags.create = value;
128 self
129 }
130
131 pub fn create_new(&mut self, value: bool) -> &mut Self {
132 self.flags.create_new = value;
133 self
134 }
135
136 pub fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
137 let resolved = resolve_path(path.as_ref());
138 with_provider(|provider| provider.open(&resolved, &self.flags)).map(File::from_handle)
139 }
140
141 pub async fn open_async(&self, path: impl AsRef<Path>) -> io::Result<File> {
142 let resolved = resolve_path(path.as_ref());
143 let provider = current_provider();
144 provider
145 .open_async(&resolved, &self.flags)
146 .await
147 .map(File::from_handle)
148 }
149
150 pub fn flags(&self) -> &OpenFlags {
151 &self.flags
152 }
153}
154
155impl Default for OpenOptions {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum FsFileType {
163 Directory,
164 File,
165 Symlink,
166 Other,
167 Unknown,
168}
169
170#[derive(Clone, Debug)]
171pub struct FsMetadata {
172 file_type: FsFileType,
173 len: u64,
174 modified: Option<SystemTime>,
175 readonly: bool,
176 hash: Option<String>,
177}
178
179impl FsMetadata {
180 pub fn new(
181 file_type: FsFileType,
182 len: u64,
183 modified: Option<SystemTime>,
184 readonly: bool,
185 ) -> Self {
186 Self {
187 file_type,
188 len,
189 modified,
190 readonly,
191 hash: None,
192 }
193 }
194
195 pub fn new_with_hash(
196 file_type: FsFileType,
197 len: u64,
198 modified: Option<SystemTime>,
199 readonly: bool,
200 hash: Option<String>,
201 ) -> Self {
202 Self {
203 file_type,
204 len,
205 modified,
206 readonly,
207 hash,
208 }
209 }
210
211 pub fn file_type(&self) -> FsFileType {
212 self.file_type
213 }
214
215 pub fn is_dir(&self) -> bool {
216 matches!(self.file_type, FsFileType::Directory)
217 }
218
219 pub fn is_file(&self) -> bool {
220 matches!(self.file_type, FsFileType::File)
221 }
222
223 pub fn is_symlink(&self) -> bool {
224 matches!(self.file_type, FsFileType::Symlink)
225 }
226
227 pub fn len(&self) -> u64 {
228 self.len
229 }
230
231 pub fn hash(&self) -> Option<&str> {
232 self.hash.as_deref()
233 }
234
235 pub fn is_empty(&self) -> bool {
236 self.len == 0
237 }
238
239 pub fn modified(&self) -> Option<SystemTime> {
240 self.modified
241 }
242
243 pub fn is_readonly(&self) -> bool {
244 self.readonly
245 }
246}
247
248#[derive(Clone, Debug)]
249pub struct DirEntry {
250 path: PathBuf,
251 file_name: OsString,
252 file_type: FsFileType,
253}
254
255#[derive(Clone, Debug)]
256pub struct ReadManyEntry {
257 path: PathBuf,
258 bytes: Option<Vec<u8>>,
259 error: Option<String>,
260}
261
262impl ReadManyEntry {
263 pub fn new(path: PathBuf, bytes: Option<Vec<u8>>) -> Self {
264 Self {
265 path,
266 bytes,
267 error: None,
268 }
269 }
270
271 pub fn with_error(path: PathBuf, error: String) -> Self {
272 Self {
273 path,
274 bytes: None,
275 error: Some(error),
276 }
277 }
278
279 pub fn path(&self) -> &Path {
280 &self.path
281 }
282
283 pub fn bytes(&self) -> Option<&[u8]> {
284 self.bytes.as_deref()
285 }
286
287 pub fn into_bytes(self) -> Option<Vec<u8>> {
288 self.bytes
289 }
290
291 pub fn error(&self) -> Option<&str> {
292 self.error.as_deref()
293 }
294}
295
296#[derive(Clone, Debug, PartialEq, Eq)]
297pub struct OpenFileDialogFilter {
298 pub patterns: Vec<String>,
299 pub description: Option<String>,
300}
301
302#[derive(Clone, Debug, Default, PartialEq, Eq)]
303pub struct OpenFileDialogRequest {
304 pub title: Option<String>,
305 pub default_path: Option<PathBuf>,
306 pub filters: Vec<OpenFileDialogFilter>,
307 pub multiselect: bool,
308}
309
310#[derive(Clone, Debug, PartialEq, Eq)]
311pub struct OpenFileDialogSelection {
312 pub paths: Vec<PathBuf>,
313 pub filter_index: Option<usize>,
314}
315
316#[derive(Clone, Debug, Default, PartialEq, Eq)]
317pub struct SaveFileDialogRequest {
318 pub title: Option<String>,
319 pub default_path: Option<PathBuf>,
320 pub filters: Vec<OpenFileDialogFilter>,
321}
322
323#[derive(Clone, Debug, PartialEq, Eq)]
324pub struct SaveFileDialogSelection {
325 pub path: PathBuf,
326 pub filter_index: Option<usize>,
327}
328
329#[derive(Clone, Debug, Default, PartialEq, Eq)]
330pub struct DirectoryDialogRequest {
331 pub title: Option<String>,
332 pub default_path: Option<PathBuf>,
333}
334
335#[derive(Clone, Debug, PartialEq, Eq)]
336pub struct DirectoryDialogSelection {
337 pub path: PathBuf,
338}
339
340impl DirEntry {
341 pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
342 Self {
343 path,
344 file_name,
345 file_type,
346 }
347 }
348
349 pub fn path(&self) -> &Path {
350 &self.path
351 }
352
353 pub fn file_name(&self) -> &OsString {
354 &self.file_name
355 }
356
357 pub fn file_type(&self) -> FsFileType {
358 self.file_type
359 }
360
361 pub fn is_dir(&self) -> bool {
362 matches!(self.file_type, FsFileType::Directory)
363 }
364}
365
366#[async_trait(?Send)]
367pub trait FsProvider: Send + Sync + 'static {
368 fn current_dir_override(&self) -> Option<PathBuf> {
369 None
370 }
371
372 fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
373 async fn open_async(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
374 self.open(path, flags)
375 }
376 async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
377 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
378 async fn remove_file(&self, path: &Path) -> io::Result<()>;
379 async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
380 async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
381 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
382 async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
383 async fn create_dir(&self, path: &Path) -> io::Result<()>;
384 async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
385 async fn remove_dir(&self, path: &Path) -> io::Result<()>;
386 async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
387 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
388 async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
389
390 async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
391 let mut entries = Vec::with_capacity(paths.len());
392 for path in paths {
393 let entry = match self.read(path).await {
394 Ok(payload) => ReadManyEntry::new(path.clone(), Some(payload)),
395 Err(error) => {
396 warn!(
397 "fs.read_many.miss path={} kind={:?} error={}",
398 path.to_string_lossy(),
399 error.kind(),
400 error
401 );
402 ReadManyEntry::with_error(
403 path.clone(),
404 format!("kind={:?}; error={}", error.kind(), error),
405 )
406 }
407 };
408 entries.push(entry);
409 }
410 Ok(entries)
411 }
412
413 async fn data_manifest_descriptor(
414 &self,
415 _request: &DataManifestRequest,
416 ) -> io::Result<DataManifestDescriptor> {
417 Err(io::Error::new(
418 ErrorKind::Unsupported,
419 "data manifest descriptor is unsupported by this provider",
420 ))
421 }
422
423 async fn data_chunk_upload_targets(
424 &self,
425 _request: &DataChunkUploadRequest,
426 ) -> io::Result<Vec<DataChunkUploadTarget>> {
427 Err(io::Error::new(
428 ErrorKind::Unsupported,
429 "data chunk upload targets are unsupported by this provider",
430 ))
431 }
432
433 async fn data_upload_chunk(
434 &self,
435 _target: &DataChunkUploadTarget,
436 _data: &[u8],
437 ) -> io::Result<()> {
438 Err(io::Error::new(
439 ErrorKind::Unsupported,
440 "data chunk upload is unsupported by this provider",
441 ))
442 }
443
444 async fn select_file_open(
445 &self,
446 _request: &OpenFileDialogRequest,
447 ) -> io::Result<Option<OpenFileDialogSelection>> {
448 Ok(None)
449 }
450
451 async fn select_file_save(
452 &self,
453 _request: &SaveFileDialogRequest,
454 ) -> io::Result<Option<SaveFileDialogSelection>> {
455 Ok(None)
456 }
457
458 async fn select_directory(
459 &self,
460 _request: &DirectoryDialogRequest,
461 ) -> io::Result<Option<DirectoryDialogSelection>> {
462 Ok(None)
463 }
464}
465
466pub struct File {
467 inner: Box<dyn FileHandle>,
468}
469
470impl File {
471 fn from_handle(handle: Box<dyn FileHandle>) -> Self {
472 Self { inner: handle }
473 }
474
475 pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
476 let mut opts = OpenOptions::new();
477 opts.read(true);
478 opts.open(path)
479 }
480
481 pub async fn open_async(path: impl AsRef<Path>) -> io::Result<Self> {
482 let mut opts = OpenOptions::new();
483 opts.read(true);
484 opts.open_async(path).await
485 }
486
487 pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
488 let mut opts = OpenOptions::new();
489 opts.write(true).create(true).truncate(true);
490 opts.open(path)
491 }
492
493 pub async fn create_async(path: impl AsRef<Path>) -> io::Result<Self> {
494 let mut opts = OpenOptions::new();
495 opts.write(true).create(true).truncate(true);
496 opts.open_async(path).await
497 }
498
499 pub async fn flush_async(&mut self) -> io::Result<()> {
500 self.inner.flush_async().await
501 }
502
503 pub async fn metadata_async(&self) -> io::Result<FsMetadata> {
504 self.inner.metadata_async().await
505 }
506
507 pub async fn sync_all_async(&mut self) -> io::Result<()> {
508 self.inner.sync_all_async().await
509 }
510}
511
512impl fmt::Debug for File {
513 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 f.debug_struct("File").finish_non_exhaustive()
515 }
516}
517
518impl Read for File {
519 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
520 self.inner.read(buf)
521 }
522}
523
524impl Write for File {
525 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
526 self.inner.write(buf)
527 }
528
529 fn flush(&mut self) -> io::Result<()> {
530 self.inner.flush()
531 }
532}
533
534impl Seek for File {
535 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
536 self.inner.seek(pos)
537 }
538}
539
540struct ProviderState {
541 provider: Arc<dyn FsProvider>,
542 current_dir_override: Option<PathBuf>,
543}
544
545static PROVIDER_STATE: OnceCell<RwLock<ProviderState>> = OnceCell::new();
546static PROVIDER_OVERRIDE_LOCK: OnceCell<Mutex<()>> = OnceCell::new();
547
548fn provider_state_lock() -> &'static RwLock<ProviderState> {
549 PROVIDER_STATE.get_or_init(|| {
550 #[cfg(target_arch = "wasm32")]
551 let current_dir_override = Some(PathBuf::from("/"));
552 #[cfg(not(target_arch = "wasm32"))]
553 let current_dir_override = None;
554
555 RwLock::new(ProviderState {
556 provider: default_provider(),
557 current_dir_override,
558 })
559 })
560}
561
562pub fn provider_override_lock() -> MutexGuard<'static, ()> {
565 PROVIDER_OVERRIDE_LOCK
566 .get_or_init(|| Mutex::new(()))
567 .lock()
568 .unwrap_or_else(|poisoned| poisoned.into_inner())
569}
570
571fn current_dir_override() -> Option<PathBuf> {
572 provider_state_lock()
573 .read()
574 .expect("filesystem provider lock poisoned")
575 .current_dir_override
576 .clone()
577}
578
579fn replace_current_dir_override(value: Option<PathBuf>) -> Option<PathBuf> {
580 let mut guard = provider_state_lock()
581 .write()
582 .expect("filesystem provider lock poisoned");
583 std::mem::replace(&mut guard.current_dir_override, value)
584}
585
586fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
587 let guard = provider_state_lock()
588 .read()
589 .expect("filesystem provider lock poisoned");
590 f(&*guard.provider)
591}
592
593fn resolve_path(path: &Path) -> PathBuf {
594 if path.is_absolute() {
595 return path.to_path_buf();
596 }
597 let state = provider_state_lock()
598 .read()
599 .expect("filesystem provider lock poisoned");
600 if let Some(base) = &state.current_dir_override {
601 if path.has_root() {
602 return path.to_path_buf();
603 }
604 return base.join(path);
605 }
606 path.to_path_buf()
607}
608
609fn next_current_dir_override(
610 current: Option<&PathBuf>,
611 provider_default: Option<PathBuf>,
612) -> Option<PathBuf> {
613 match provider_default {
614 Some(default) => current.cloned().or(Some(default)),
615 None => None,
616 }
617}
618
619pub fn set_provider(provider: Arc<dyn FsProvider>) {
620 let provider_default_current_dir = provider.current_dir_override();
621 let mut guard = provider_state_lock()
622 .write()
623 .expect("filesystem provider lock poisoned");
624 let current_dir_override = next_current_dir_override(
625 guard.current_dir_override.as_ref(),
626 provider_default_current_dir,
627 );
628 guard.provider = provider;
629 guard.current_dir_override = current_dir_override;
630}
631
632pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
636 let provider_default_current_dir = provider.current_dir_override();
637 let mut guard = provider_state_lock()
638 .write()
639 .expect("filesystem provider lock poisoned");
640 let previous = guard.provider.clone();
641 let previous_current_dir = guard.current_dir_override.clone();
642 let current_dir_override = next_current_dir_override(
643 guard.current_dir_override.as_ref(),
644 provider_default_current_dir,
645 );
646 guard.provider = provider;
647 guard.current_dir_override = current_dir_override;
648 ProviderGuard {
649 previous,
650 previous_current_dir,
651 }
652}
653
654pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
657 let guard = replace_provider(provider);
658 let result = f();
659 drop(guard);
660 result
661}
662
663pub fn current_provider() -> Arc<dyn FsProvider> {
665 provider_state_lock()
666 .read()
667 .expect("filesystem provider lock poisoned")
668 .provider
669 .clone()
670}
671
672pub fn current_dir() -> io::Result<PathBuf> {
673 if let Some(current) = current_dir_override() {
674 return Ok(current);
675 }
676 #[cfg(not(target_arch = "wasm32"))]
677 {
678 std::env::current_dir()
679 }
680 #[cfg(target_arch = "wasm32")]
681 {
682 Ok(PathBuf::from("/"))
683 }
684}
685
686pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
687 if current_dir_override().is_some() {
688 futures::executor::block_on(set_current_dir_async(path.as_ref().to_path_buf()))
689 } else {
690 #[cfg(not(target_arch = "wasm32"))]
691 {
692 std::env::set_current_dir(path)
693 }
694 #[cfg(target_arch = "wasm32")]
695 {
696 Ok(())
697 }
698 }
699}
700
701pub async fn set_current_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
702 if current_dir_override().is_some() {
703 let mut target = PathBuf::from(path.as_ref());
704 if !target.has_root() {
705 let base = current_dir()?;
706 target = base.join(target);
707 }
708 let canonical = canonicalize_async(&target).await.unwrap_or(target.clone());
709 let metadata = metadata_async(&canonical).await?;
710 if !metadata.is_dir() {
711 return Err(io::Error::new(
712 ErrorKind::NotFound,
713 format!("Not a directory: {}", canonical.display()),
714 ));
715 }
716 replace_current_dir_override(Some(canonical));
717 Ok(())
718 } else {
719 set_current_dir(path)
720 }
721}
722
723pub struct ProviderGuard {
724 previous: Arc<dyn FsProvider>,
725 previous_current_dir: Option<PathBuf>,
726}
727
728impl Drop for ProviderGuard {
729 fn drop(&mut self) {
730 let mut guard = provider_state_lock()
731 .write()
732 .expect("filesystem provider lock poisoned");
733 guard.provider = self.previous.clone();
734 guard.current_dir_override = self.previous_current_dir.clone();
735 }
736}
737
738pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
739 let resolved = paths
740 .iter()
741 .map(|path| resolve_path(path.as_path()))
742 .collect::<Vec<_>>();
743 let provider = current_provider();
744 provider.read_many(&resolved).await
745}
746
747pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
748 let resolved = resolve_path(path.as_ref());
749 let provider = current_provider();
750 provider.read(&resolved).await
751}
752
753pub fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
754 let path = path.as_ref().to_path_buf();
755 wait_for_fs(move || async move { read_async(path).await })
756}
757
758pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
759 let bytes = read_async(path).await?;
760 String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
761}
762
763pub fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
764 let path = path.as_ref().to_path_buf();
765 wait_for_fs(move || async move { read_to_string_async(path).await })
766}
767
768pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
769 let resolved = resolve_path(path.as_ref());
770 let provider = current_provider();
771 provider.write(&resolved, data.as_ref()).await
772}
773
774pub fn write(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
775 let path = path.as_ref().to_path_buf();
776 let data = data.as_ref().to_vec();
777 wait_for_fs(move || async move { write_async(path, data).await })
778}
779
780pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
781 let resolved = resolve_path(path.as_ref());
782 let provider = current_provider();
783 provider.remove_file(&resolved).await
784}
785
786pub fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
787 let path = path.as_ref().to_path_buf();
788 wait_for_fs(move || async move { remove_file_async(path).await })
789}
790
791pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
792 let resolved = resolve_path(path.as_ref());
793 let provider = current_provider();
794 provider.metadata(&resolved).await
795}
796
797pub fn metadata(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
798 let path = path.as_ref().to_path_buf();
799 wait_for_fs(move || async move { metadata_async(path).await })
800}
801
802pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
803 let resolved = resolve_path(path.as_ref());
804 let provider = current_provider();
805 provider.symlink_metadata(&resolved).await
806}
807
808pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
809 let resolved = resolve_path(path.as_ref());
810 let provider = current_provider();
811 provider.read_dir(&resolved).await
812}
813
814pub fn read_dir(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
815 let path = path.as_ref().to_path_buf();
816 wait_for_fs(move || async move { read_dir_async(path).await })
817}
818
819pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
820 let resolved = resolve_path(path.as_ref());
821 let provider = current_provider();
822 provider.canonicalize(&resolved).await
823}
824
825pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
826 let resolved = resolve_path(path.as_ref());
827 let provider = current_provider();
828 provider.create_dir(&resolved).await
829}
830
831pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
832 let resolved = resolve_path(path.as_ref());
833 let provider = current_provider();
834 provider.create_dir_all(&resolved).await
835}
836
837pub fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
838 let path = path.as_ref().to_path_buf();
839 wait_for_fs(move || async move { create_dir_all_async(path).await })
840}
841
842pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
843 let resolved = resolve_path(path.as_ref());
844 let provider = current_provider();
845 provider.remove_dir(&resolved).await
846}
847
848pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
849 let resolved = resolve_path(path.as_ref());
850 let provider = current_provider();
851 provider.remove_dir_all(&resolved).await
852}
853
854pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
855 let resolved_from = resolve_path(from.as_ref());
856 let resolved_to = resolve_path(to.as_ref());
857 let provider = current_provider();
858 provider.rename(&resolved_from, &resolved_to).await
859}
860
861pub fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
862 let from = from.as_ref().to_path_buf();
863 let to = to.as_ref().to_path_buf();
864 wait_for_fs(move || async move { rename_async(from, to).await })
865}
866
867pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
868 let resolved = resolve_path(path.as_ref());
869 let provider = current_provider();
870 provider.set_readonly(&resolved, readonly).await
871}
872
873pub async fn select_file_open_async(
874 request: &OpenFileDialogRequest,
875) -> io::Result<Option<OpenFileDialogSelection>> {
876 let mut resolved = request.clone();
877 if let Some(default_path) = resolved.default_path.as_mut() {
878 *default_path = resolve_path(default_path);
879 }
880 let provider = current_provider();
881 provider.select_file_open(&resolved).await
882}
883
884pub async fn select_file_save_async(
885 request: &SaveFileDialogRequest,
886) -> io::Result<Option<SaveFileDialogSelection>> {
887 let mut resolved = request.clone();
888 if let Some(default_path) = resolved.default_path.as_mut() {
889 *default_path = resolve_path(default_path);
890 }
891 let provider = current_provider();
892 provider.select_file_save(&resolved).await
893}
894
895pub async fn select_directory_async(
896 request: &DirectoryDialogRequest,
897) -> io::Result<Option<DirectoryDialogSelection>> {
898 let mut resolved = request.clone();
899 if let Some(default_path) = resolved.default_path.as_mut() {
900 *default_path = resolve_path(default_path);
901 }
902 let provider = current_provider();
903 provider.select_directory(&resolved).await
904}
905
906pub async fn data_manifest_descriptor_async(
907 request: &DataManifestRequest,
908) -> io::Result<DataManifestDescriptor> {
909 let provider = current_provider();
910 provider.data_manifest_descriptor(request).await
911}
912
913pub async fn data_chunk_upload_targets_async(
914 request: &DataChunkUploadRequest,
915) -> io::Result<Vec<DataChunkUploadTarget>> {
916 let provider = current_provider();
917 provider.data_chunk_upload_targets(request).await
918}
919
920pub async fn data_upload_chunk_async(
921 target: &DataChunkUploadTarget,
922 data: &[u8],
923) -> io::Result<()> {
924 let provider = current_provider();
925 provider.data_upload_chunk(target, data).await
926}
927
928pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
931 let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
932 let mut writer = OpenOptions::new()
933 .write(true)
934 .create(true)
935 .truncate(true)
936 .open(to.as_ref())?;
937 io::copy(&mut reader, &mut writer)
938}
939
940#[cfg(not(target_arch = "wasm32"))]
941fn wait_for_fs<T, F, Fut>(factory: F) -> io::Result<T>
942where
943 T: Send + 'static,
944 F: FnOnce() -> Fut + Send + 'static,
945 Fut: std::future::Future<Output = io::Result<T>> + 'static,
946{
947 std::thread::spawn(move || futures::executor::block_on(factory()))
948 .join()
949 .map_err(|_| io::Error::other("filesystem worker thread panicked"))?
950}
951
952#[cfg(target_arch = "wasm32")]
953fn wait_for_fs<T, F, Fut>(factory: F) -> io::Result<T>
954where
955 F: FnOnce() -> Fut,
956 Fut: std::future::Future<Output = io::Result<T>>,
957{
958 futures::executor::block_on(factory())
959}
960
961fn default_provider() -> Arc<dyn FsProvider> {
962 #[cfg(not(target_arch = "wasm32"))]
963 {
964 Arc::new(NativeFsProvider)
965 }
966 #[cfg(target_arch = "wasm32")]
967 {
968 Arc::new(PlaceholderFsProvider)
969 }
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975 use once_cell::sync::Lazy;
976 use std::collections::{HashMap, HashSet};
977 use std::io::{Read, Seek, SeekFrom, Write};
978 use std::sync::Mutex;
979 use tempfile::tempdir;
980
981 static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
982
983 fn test_lock() -> std::sync::MutexGuard<'static, ()> {
984 TEST_LOCK
985 .lock()
986 .unwrap_or_else(|poisoned| poisoned.into_inner())
987 }
988
989 fn comparable_path(path: impl AsRef<Path>) -> PathBuf {
990 #[cfg(windows)]
991 {
992 let text = path.as_ref().to_string_lossy();
993 if let Some(stripped) = text.strip_prefix(r"\\?\UNC\") {
994 return PathBuf::from(format!(r"\\{stripped}"));
995 }
996 if let Some(stripped) = text.strip_prefix(r"\\?\") {
997 return PathBuf::from(stripped);
998 }
999 PathBuf::from(text.as_ref())
1000 }
1001 #[cfg(not(windows))]
1002 {
1003 path.as_ref().to_path_buf()
1004 }
1005 }
1006
1007 fn assert_same_path(left: impl AsRef<Path>, right: impl AsRef<Path>) {
1008 assert_eq!(comparable_path(left), comparable_path(right));
1009 }
1010
1011 struct UnsupportedProvider;
1012
1013 struct AsyncOpenProvider {
1014 opened_async: Arc<Mutex<bool>>,
1015 flushed_async: Arc<Mutex<bool>>,
1016 }
1017
1018 struct TestProviderStateGuard {
1019 previous_provider: Arc<dyn FsProvider>,
1020 previous_current_dir: Option<PathBuf>,
1021 }
1022
1023 struct ProcessCwdGuard {
1024 previous: PathBuf,
1025 }
1026
1027 struct VirtualFsProvider {
1028 default_current_dir: PathBuf,
1029 dirs: Mutex<HashSet<PathBuf>>,
1030 files: Mutex<HashMap<PathBuf, Vec<u8>>>,
1031 }
1032
1033 impl Drop for ProcessCwdGuard {
1034 fn drop(&mut self) {
1035 let _ = std::env::set_current_dir(&self.previous);
1036 }
1037 }
1038
1039 impl TestProviderStateGuard {
1040 fn capture() -> Self {
1041 let guard = provider_state_lock()
1042 .read()
1043 .expect("filesystem provider lock poisoned");
1044 Self {
1045 previous_provider: guard.provider.clone(),
1046 previous_current_dir: guard.current_dir_override.clone(),
1047 }
1048 }
1049 }
1050
1051 impl Drop for TestProviderStateGuard {
1052 fn drop(&mut self) {
1053 let mut guard = provider_state_lock()
1054 .write()
1055 .expect("filesystem provider lock poisoned");
1056 guard.provider = self.previous_provider.clone();
1057 guard.current_dir_override = self.previous_current_dir.clone();
1058 }
1059 }
1060
1061 impl VirtualFsProvider {
1062 fn new(default_current_dir: impl Into<PathBuf>, dirs: &[&str]) -> Self {
1063 let default_current_dir = default_current_dir.into();
1064 let mut all_dirs = HashSet::from([default_current_dir.clone()]);
1065 for dir in dirs {
1066 all_dirs.insert(PathBuf::from(dir));
1067 }
1068 Self {
1069 default_current_dir,
1070 dirs: Mutex::new(all_dirs),
1071 files: Mutex::new(HashMap::new()),
1072 }
1073 }
1074
1075 fn file_bytes(&self, path: impl AsRef<Path>) -> Option<Vec<u8>> {
1076 self.files.lock().unwrap().get(path.as_ref()).cloned()
1077 }
1078 }
1079
1080 struct AsyncTestHandle {
1081 cursor: usize,
1082 data: Vec<u8>,
1083 flushed_async: Arc<Mutex<bool>>,
1084 }
1085
1086 impl Read for AsyncTestHandle {
1087 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1088 let remaining = self.data.len().saturating_sub(self.cursor);
1089 let to_read = remaining.min(buf.len());
1090 buf[..to_read].copy_from_slice(&self.data[self.cursor..self.cursor + to_read]);
1091 self.cursor += to_read;
1092 Ok(to_read)
1093 }
1094 }
1095
1096 impl Write for AsyncTestHandle {
1097 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1098 let end = self.cursor + buf.len();
1099 if end > self.data.len() {
1100 self.data.resize(end, 0);
1101 }
1102 self.data[self.cursor..end].copy_from_slice(buf);
1103 self.cursor = end;
1104 Ok(buf.len())
1105 }
1106
1107 fn flush(&mut self) -> io::Result<()> {
1108 Ok(())
1109 }
1110 }
1111
1112 impl Seek for AsyncTestHandle {
1113 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
1114 let next = match pos {
1115 SeekFrom::Start(offset) => offset as i64,
1116 SeekFrom::End(offset) => self.data.len() as i64 + offset,
1117 SeekFrom::Current(offset) => self.cursor as i64 + offset,
1118 };
1119 if next < 0 {
1120 return Err(io::Error::new(ErrorKind::InvalidInput, "seek before start"));
1121 }
1122 self.cursor = next as usize;
1123 Ok(self.cursor as u64)
1124 }
1125 }
1126
1127 #[async_trait(?Send)]
1128 impl FileHandle for AsyncTestHandle {
1129 async fn flush_async(&mut self) -> io::Result<()> {
1130 *self.flushed_async.lock().unwrap() = true;
1131 Ok(())
1132 }
1133 }
1134
1135 #[async_trait(?Send)]
1136 impl FsProvider for UnsupportedProvider {
1137 fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1138 Err(unsupported())
1139 }
1140
1141 async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
1142 Err(unsupported())
1143 }
1144
1145 async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1146 Err(unsupported())
1147 }
1148
1149 async fn remove_file(&self, _path: &Path) -> io::Result<()> {
1150 Err(unsupported())
1151 }
1152
1153 async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1154 Err(unsupported())
1155 }
1156
1157 async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1158 Err(unsupported())
1159 }
1160
1161 async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1162 Err(unsupported())
1163 }
1164
1165 async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1166 Err(unsupported())
1167 }
1168
1169 async fn create_dir(&self, _path: &Path) -> io::Result<()> {
1170 Err(unsupported())
1171 }
1172
1173 async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1174 Err(unsupported())
1175 }
1176
1177 async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1178 Err(unsupported())
1179 }
1180
1181 async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
1182 Err(unsupported())
1183 }
1184
1185 async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1186 Err(unsupported())
1187 }
1188
1189 async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1190 Err(unsupported())
1191 }
1192
1193 async fn data_manifest_descriptor(
1194 &self,
1195 _request: &DataManifestRequest,
1196 ) -> io::Result<DataManifestDescriptor> {
1197 Err(unsupported())
1198 }
1199
1200 async fn data_chunk_upload_targets(
1201 &self,
1202 _request: &DataChunkUploadRequest,
1203 ) -> io::Result<Vec<DataChunkUploadTarget>> {
1204 Err(unsupported())
1205 }
1206
1207 async fn data_upload_chunk(
1208 &self,
1209 _target: &DataChunkUploadTarget,
1210 _data: &[u8],
1211 ) -> io::Result<()> {
1212 Err(unsupported())
1213 }
1214 }
1215
1216 #[async_trait(?Send)]
1217 impl FsProvider for VirtualFsProvider {
1218 fn current_dir_override(&self) -> Option<PathBuf> {
1219 Some(self.default_current_dir.clone())
1220 }
1221
1222 fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1223 Err(unsupported())
1224 }
1225
1226 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
1227 self.files
1228 .lock()
1229 .unwrap()
1230 .get(path)
1231 .cloned()
1232 .ok_or_else(|| io::Error::new(ErrorKind::NotFound, path.display().to_string()))
1233 }
1234
1235 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1236 self.files
1237 .lock()
1238 .unwrap()
1239 .insert(path.to_path_buf(), data.to_vec());
1240 Ok(())
1241 }
1242
1243 async fn remove_file(&self, path: &Path) -> io::Result<()> {
1244 self.files.lock().unwrap().remove(path);
1245 Ok(())
1246 }
1247
1248 async fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
1249 if self.dirs.lock().unwrap().contains(path) {
1250 return Ok(FsMetadata::new(FsFileType::Directory, 0, None, false));
1251 }
1252 if let Some(bytes) = self.files.lock().unwrap().get(path) {
1253 return Ok(FsMetadata::new(
1254 FsFileType::File,
1255 bytes.len() as u64,
1256 None,
1257 false,
1258 ));
1259 }
1260 Err(io::Error::new(
1261 ErrorKind::NotFound,
1262 path.display().to_string(),
1263 ))
1264 }
1265
1266 async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata> {
1267 self.metadata(path).await
1268 }
1269
1270 async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1271 Ok(Vec::new())
1272 }
1273
1274 async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
1275 Ok(path.to_path_buf())
1276 }
1277
1278 async fn create_dir(&self, path: &Path) -> io::Result<()> {
1279 self.dirs.lock().unwrap().insert(path.to_path_buf());
1280 Ok(())
1281 }
1282
1283 async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
1284 let mut dirs = self.dirs.lock().unwrap();
1285 for ancestor in path.ancestors() {
1286 dirs.insert(ancestor.to_path_buf());
1287 }
1288 Ok(())
1289 }
1290
1291 async fn remove_dir(&self, path: &Path) -> io::Result<()> {
1292 self.dirs.lock().unwrap().remove(path);
1293 Ok(())
1294 }
1295
1296 async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
1297 self.dirs
1298 .lock()
1299 .unwrap()
1300 .retain(|dir| !dir.starts_with(path));
1301 Ok(())
1302 }
1303
1304 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1305 let mut files = self.files.lock().unwrap();
1306 let data = files
1307 .remove(from)
1308 .ok_or_else(|| io::Error::new(ErrorKind::NotFound, from.display().to_string()))?;
1309 files.insert(to.to_path_buf(), data);
1310 Ok(())
1311 }
1312
1313 async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1314 Ok(())
1315 }
1316 }
1317
1318 #[async_trait(?Send)]
1319 impl FsProvider for AsyncOpenProvider {
1320 fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1321 Err(unsupported())
1322 }
1323
1324 async fn open_async(
1325 &self,
1326 _path: &Path,
1327 _flags: &OpenFlags,
1328 ) -> io::Result<Box<dyn FileHandle>> {
1329 *self.opened_async.lock().unwrap() = true;
1330 Ok(Box::new(AsyncTestHandle {
1331 cursor: 0,
1332 data: b"async contents".to_vec(),
1333 flushed_async: self.flushed_async.clone(),
1334 }))
1335 }
1336
1337 async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
1338 Err(unsupported())
1339 }
1340
1341 async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1342 Err(unsupported())
1343 }
1344
1345 async fn remove_file(&self, _path: &Path) -> io::Result<()> {
1346 Err(unsupported())
1347 }
1348
1349 async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1350 Err(unsupported())
1351 }
1352
1353 async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1354 Err(unsupported())
1355 }
1356
1357 async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1358 Err(unsupported())
1359 }
1360
1361 async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1362 Err(unsupported())
1363 }
1364
1365 async fn create_dir(&self, _path: &Path) -> io::Result<()> {
1366 Err(unsupported())
1367 }
1368
1369 async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1370 Err(unsupported())
1371 }
1372
1373 async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1374 Err(unsupported())
1375 }
1376
1377 async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
1378 Err(unsupported())
1379 }
1380
1381 async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1382 Err(unsupported())
1383 }
1384
1385 async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1386 Err(unsupported())
1387 }
1388 }
1389
1390 fn unsupported() -> io::Error {
1391 io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
1392 }
1393
1394 #[test]
1395 fn copy_file_round_trip() {
1396 let _guard = test_lock();
1397 let dir = tempdir().expect("tempdir");
1398 let src = dir.path().join("src.bin");
1399 let dst = dir.path().join("dst.bin");
1400 {
1401 let mut file = std::fs::File::create(&src).expect("create src");
1402 file.write_all(b"hello filesystem").expect("write src");
1403 }
1404
1405 copy_file(&src, &dst).expect("copy");
1406 let mut dst_file = File::open(&dst).expect("open dst");
1407 let mut contents = Vec::new();
1408 dst_file
1409 .read_to_end(&mut contents)
1410 .expect("read destination");
1411 assert_eq!(contents, b"hello filesystem");
1412 }
1413
1414 #[test]
1415 fn set_readonly_flips_metadata_flag() {
1416 let _guard = test_lock();
1417 let dir = tempdir().expect("tempdir");
1418 let path = dir.path().join("flag.txt");
1419 futures::executor::block_on(write_async(&path, b"flag")).expect("write");
1420
1421 futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
1422 let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
1423 assert!(meta.is_readonly());
1424
1425 futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
1426 let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
1427 assert!(!meta.is_readonly());
1428 }
1429
1430 #[test]
1431 fn sync_helpers_work_inside_async_executor() {
1432 let _guard = TEST_LOCK.lock().unwrap();
1433 let dir = tempdir().expect("tempdir");
1434 let path = dir.path().join("nested").join("file.txt");
1435 let parent = path.parent().unwrap().to_path_buf();
1436
1437 futures::executor::block_on(async {
1438 create_dir_all(&parent).expect("create dir");
1439 write(&path, b"hello").expect("write");
1440 assert_eq!(read(&path).expect("read"), b"hello");
1441 assert_eq!(read_to_string(&path).expect("read string"), "hello");
1442 assert!(metadata(&path).expect("metadata").is_file());
1443 assert_eq!(read_dir(&parent).expect("read dir").len(), 1);
1444 remove_file(&path).expect("remove");
1445 });
1446 }
1447
1448 #[test]
1449 fn replace_provider_restores_previous() {
1450 let _guard = test_lock();
1451 let original = current_provider();
1452 let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1453 {
1454 let _guard = replace_provider(custom.clone());
1455 let active = current_provider();
1456 assert!(Arc::ptr_eq(&active, &custom));
1457 }
1458 let final_provider = current_provider();
1459 assert!(Arc::ptr_eq(&final_provider, &original));
1460 }
1461
1462 #[test]
1463 #[cfg(not(target_arch = "wasm32"))]
1464 fn native_provider_replacement_preserves_process_cwd_resolution() {
1465 let _guard = test_lock();
1466 let temp = tempdir().expect("tempdir");
1467 let previous = std::env::current_dir().expect("current dir");
1468 let _cwd_guard = ProcessCwdGuard { previous };
1469 std::env::set_current_dir(temp.path()).expect("set temp cwd");
1470
1471 let _provider_guard = replace_provider(Arc::new(NativeFsProvider));
1472 let current = current_dir().expect("vfs current dir");
1473 let expected = std::fs::canonicalize(temp.path()).expect("canonical temp");
1474 assert_same_path(¤t, &expected);
1475
1476 futures::executor::block_on(write_async("native-relative.txt", b"native"))
1477 .expect("write relative path");
1478 assert_eq!(
1479 std::fs::read_to_string(temp.path().join("native-relative.txt")).expect("read file"),
1480 "native"
1481 );
1482
1483 std::fs::create_dir(temp.path().join("child")).expect("create child");
1484 set_current_dir("child").expect("set child cwd");
1485 assert_same_path(
1486 std::env::current_dir().expect("process cwd"),
1487 expected.join("child"),
1488 );
1489 }
1490
1491 #[test]
1492 fn set_provider_initializes_virtual_cwd_from_provider_default() {
1493 let _guard = test_lock();
1494 let _state_guard = TestProviderStateGuard::capture();
1495 replace_current_dir_override(None);
1496
1497 set_provider(Arc::new(VirtualFsProvider::new("/sandbox", &[])));
1498
1499 assert_eq!(
1500 current_dir().expect("virtual cwd"),
1501 PathBuf::from("/sandbox")
1502 );
1503 }
1504
1505 #[test]
1506 fn set_provider_preserves_existing_virtual_cwd() {
1507 let _guard = test_lock();
1508 let _state_guard = TestProviderStateGuard::capture();
1509 let initial = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1510 set_provider(initial);
1511 set_current_dir("/workspace").expect("set virtual cwd");
1512
1513 let replacement = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1514 set_provider(replacement.clone());
1515
1516 assert_eq!(
1517 current_dir().expect("virtual cwd"),
1518 PathBuf::from("/workspace")
1519 );
1520 futures::executor::block_on(write_async("data.txt", b"virtual")).expect("write relative");
1521 assert_eq!(
1522 replacement.file_bytes("/workspace/data.txt").as_deref(),
1523 Some(&b"virtual"[..])
1524 );
1525 assert_eq!(replacement.file_bytes("data.txt"), None);
1526 }
1527
1528 #[test]
1529 fn replace_provider_preserves_existing_virtual_cwd() {
1530 let _guard = test_lock();
1531 let initial = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1532 let _initial_guard = replace_provider(initial);
1533 set_current_dir("/workspace").expect("set virtual cwd");
1534
1535 let replacement = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1536 {
1537 let _replacement_guard = replace_provider(replacement.clone());
1538
1539 assert_eq!(
1540 current_dir().expect("virtual cwd"),
1541 PathBuf::from("/workspace")
1542 );
1543 futures::executor::block_on(write_async("nested.txt", b"replacement"))
1544 .expect("write relative");
1545 }
1546
1547 assert_eq!(
1548 replacement.file_bytes("/workspace/nested.txt").as_deref(),
1549 Some(&b"replacement"[..])
1550 );
1551 assert_eq!(replacement.file_bytes("nested.txt"), None);
1552 }
1553
1554 #[test]
1555 fn virtual_root_paths_do_not_resolve_relative_to_virtual_cwd() {
1556 let _guard = test_lock();
1557 let provider = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1558 let _provider_guard = replace_provider(provider.clone());
1559 set_current_dir("/workspace").expect("set virtual cwd");
1560
1561 futures::executor::block_on(write_async("/root.txt", b"root")).expect("write absolute");
1562
1563 assert_eq!(
1564 provider.file_bytes("/root.txt").as_deref(),
1565 Some(&b"root"[..])
1566 );
1567 assert_eq!(provider.file_bytes("/workspace/root.txt"), None);
1568 }
1569
1570 #[test]
1571 #[cfg(not(target_arch = "wasm32"))]
1572 fn native_provider_replacement_clears_virtual_cwd_override() {
1573 let _guard = test_lock();
1574 let temp = tempdir().expect("tempdir");
1575 let previous = std::env::current_dir().expect("current dir");
1576 let _cwd_guard = ProcessCwdGuard { previous };
1577 std::env::set_current_dir(temp.path()).expect("set temp cwd");
1578
1579 let virtual_provider = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1580 let _virtual_guard = replace_provider(virtual_provider);
1581 set_current_dir("/workspace").expect("set virtual cwd");
1582
1583 {
1584 let _native_guard = replace_provider(Arc::new(NativeFsProvider));
1585 let expected = std::fs::canonicalize(temp.path()).expect("canonical temp");
1586 assert_same_path(current_dir().expect("native cwd"), &expected);
1587
1588 futures::executor::block_on(write_async("native.txt", b"native"))
1589 .expect("write native relative");
1590 assert_eq!(
1591 std::fs::read_to_string(temp.path().join("native.txt")).expect("read native file"),
1592 "native"
1593 );
1594 }
1595 }
1596
1597 #[test]
1598 fn open_async_and_flush_async_use_provider_async_paths() {
1599 let _guard = test_lock();
1600 let opened_async = Arc::new(Mutex::new(false));
1601 let flushed_async = Arc::new(Mutex::new(false));
1602 let provider = Arc::new(AsyncOpenProvider {
1603 opened_async: opened_async.clone(),
1604 flushed_async: flushed_async.clone(),
1605 });
1606 let _provider_guard = replace_provider(provider);
1607
1608 let mut file =
1609 futures::executor::block_on(OpenOptions::new().read(true).open_async("data.txt"))
1610 .expect("async open");
1611 let mut contents = String::new();
1612 file.read_to_string(&mut contents).expect("read contents");
1613 futures::executor::block_on(file.flush_async()).expect("async flush");
1614
1615 assert_eq!(contents, "async contents");
1616 assert!(*opened_async.lock().unwrap());
1617 assert!(*flushed_async.lock().unwrap());
1618 }
1619
1620 #[test]
1621 fn select_file_open_defaults_to_cancelled_selection() {
1622 let _guard = test_lock();
1623 let provider: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1624 let _provider_guard = replace_provider(provider);
1625 let request = OpenFileDialogRequest {
1626 title: Some("Open".to_string()),
1627 default_path: Some(PathBuf::from("data")),
1628 filters: vec![OpenFileDialogFilter {
1629 patterns: vec!["*.csv".to_string()],
1630 description: Some("CSV files".to_string()),
1631 }],
1632 multiselect: false,
1633 };
1634
1635 let selection =
1636 futures::executor::block_on(select_file_open_async(&request)).expect("select file");
1637
1638 assert_eq!(selection, None);
1639 }
1640
1641 #[test]
1642 fn select_file_save_defaults_to_cancelled_selection() {
1643 let _guard = test_lock();
1644 let provider: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1645 let _provider_guard = replace_provider(provider);
1646 let request = SaveFileDialogRequest {
1647 title: Some("Save".to_string()),
1648 default_path: Some(PathBuf::from("data.mat")),
1649 filters: vec![OpenFileDialogFilter {
1650 patterns: vec!["*.mat".to_string()],
1651 description: Some("MAT files".to_string()),
1652 }],
1653 };
1654
1655 let selection =
1656 futures::executor::block_on(select_file_save_async(&request)).expect("select file");
1657
1658 assert_eq!(selection, None);
1659 }
1660
1661 #[test]
1662 fn select_directory_defaults_to_cancelled_selection() {
1663 let _guard = test_lock();
1664 let provider: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1665 let _provider_guard = replace_provider(provider);
1666 let request = DirectoryDialogRequest {
1667 title: Some("Select folder".to_string()),
1668 default_path: Some(PathBuf::from("data")),
1669 };
1670
1671 let selection =
1672 futures::executor::block_on(select_directory_async(&request)).expect("select folder");
1673
1674 assert_eq!(selection, None);
1675 }
1676
1677 #[test]
1678 fn with_provider_restores_even_on_panic() {
1679 let _guard = test_lock();
1680 let original = current_provider();
1681 let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1682 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1683 with_provider_override(custom.clone(), || {
1684 let active = current_provider();
1685 assert!(Arc::ptr_eq(&active, &custom));
1686 panic!("boom");
1687 })
1688 }));
1689 assert!(result.is_err());
1690 let final_provider = current_provider();
1691 assert!(Arc::ptr_eq(&final_provider, &original));
1692 }
1693}