Skip to main content

runmat_filesystem/
lib.rs

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
329impl DirEntry {
330    pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
331        Self {
332            path,
333            file_name,
334            file_type,
335        }
336    }
337
338    pub fn path(&self) -> &Path {
339        &self.path
340    }
341
342    pub fn file_name(&self) -> &OsString {
343        &self.file_name
344    }
345
346    pub fn file_type(&self) -> FsFileType {
347        self.file_type
348    }
349
350    pub fn is_dir(&self) -> bool {
351        matches!(self.file_type, FsFileType::Directory)
352    }
353}
354
355#[async_trait(?Send)]
356pub trait FsProvider: Send + Sync + 'static {
357    fn current_dir_override(&self) -> Option<PathBuf> {
358        None
359    }
360
361    fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
362    async fn open_async(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
363        self.open(path, flags)
364    }
365    async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
366    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
367    async fn remove_file(&self, path: &Path) -> io::Result<()>;
368    async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
369    async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
370    async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
371    async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
372    async fn create_dir(&self, path: &Path) -> io::Result<()>;
373    async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
374    async fn remove_dir(&self, path: &Path) -> io::Result<()>;
375    async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
376    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
377    async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
378
379    async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
380        let mut entries = Vec::with_capacity(paths.len());
381        for path in paths {
382            let entry = match self.read(path).await {
383                Ok(payload) => ReadManyEntry::new(path.clone(), Some(payload)),
384                Err(error) => {
385                    warn!(
386                        "fs.read_many.miss path={} kind={:?} error={}",
387                        path.to_string_lossy(),
388                        error.kind(),
389                        error
390                    );
391                    ReadManyEntry::with_error(
392                        path.clone(),
393                        format!("kind={:?}; error={}", error.kind(), error),
394                    )
395                }
396            };
397            entries.push(entry);
398        }
399        Ok(entries)
400    }
401
402    async fn data_manifest_descriptor(
403        &self,
404        _request: &DataManifestRequest,
405    ) -> io::Result<DataManifestDescriptor> {
406        Err(io::Error::new(
407            ErrorKind::Unsupported,
408            "data manifest descriptor is unsupported by this provider",
409        ))
410    }
411
412    async fn data_chunk_upload_targets(
413        &self,
414        _request: &DataChunkUploadRequest,
415    ) -> io::Result<Vec<DataChunkUploadTarget>> {
416        Err(io::Error::new(
417            ErrorKind::Unsupported,
418            "data chunk upload targets are unsupported by this provider",
419        ))
420    }
421
422    async fn data_upload_chunk(
423        &self,
424        _target: &DataChunkUploadTarget,
425        _data: &[u8],
426    ) -> io::Result<()> {
427        Err(io::Error::new(
428            ErrorKind::Unsupported,
429            "data chunk upload is unsupported by this provider",
430        ))
431    }
432
433    async fn select_file_open(
434        &self,
435        _request: &OpenFileDialogRequest,
436    ) -> io::Result<Option<OpenFileDialogSelection>> {
437        Ok(None)
438    }
439
440    async fn select_file_save(
441        &self,
442        _request: &SaveFileDialogRequest,
443    ) -> io::Result<Option<SaveFileDialogSelection>> {
444        Ok(None)
445    }
446}
447
448pub struct File {
449    inner: Box<dyn FileHandle>,
450}
451
452impl File {
453    fn from_handle(handle: Box<dyn FileHandle>) -> Self {
454        Self { inner: handle }
455    }
456
457    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
458        let mut opts = OpenOptions::new();
459        opts.read(true);
460        opts.open(path)
461    }
462
463    pub async fn open_async(path: impl AsRef<Path>) -> io::Result<Self> {
464        let mut opts = OpenOptions::new();
465        opts.read(true);
466        opts.open_async(path).await
467    }
468
469    pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
470        let mut opts = OpenOptions::new();
471        opts.write(true).create(true).truncate(true);
472        opts.open(path)
473    }
474
475    pub async fn create_async(path: impl AsRef<Path>) -> io::Result<Self> {
476        let mut opts = OpenOptions::new();
477        opts.write(true).create(true).truncate(true);
478        opts.open_async(path).await
479    }
480
481    pub async fn flush_async(&mut self) -> io::Result<()> {
482        self.inner.flush_async().await
483    }
484
485    pub async fn metadata_async(&self) -> io::Result<FsMetadata> {
486        self.inner.metadata_async().await
487    }
488
489    pub async fn sync_all_async(&mut self) -> io::Result<()> {
490        self.inner.sync_all_async().await
491    }
492}
493
494impl fmt::Debug for File {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        f.debug_struct("File").finish_non_exhaustive()
497    }
498}
499
500impl Read for File {
501    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
502        self.inner.read(buf)
503    }
504}
505
506impl Write for File {
507    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
508        self.inner.write(buf)
509    }
510
511    fn flush(&mut self) -> io::Result<()> {
512        self.inner.flush()
513    }
514}
515
516impl Seek for File {
517    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
518        self.inner.seek(pos)
519    }
520}
521
522struct ProviderState {
523    provider: Arc<dyn FsProvider>,
524    current_dir_override: Option<PathBuf>,
525}
526
527static PROVIDER_STATE: OnceCell<RwLock<ProviderState>> = OnceCell::new();
528static PROVIDER_OVERRIDE_LOCK: OnceCell<Mutex<()>> = OnceCell::new();
529
530fn provider_state_lock() -> &'static RwLock<ProviderState> {
531    PROVIDER_STATE.get_or_init(|| {
532        #[cfg(target_arch = "wasm32")]
533        let current_dir_override = Some(PathBuf::from("/"));
534        #[cfg(not(target_arch = "wasm32"))]
535        let current_dir_override = None;
536
537        RwLock::new(ProviderState {
538            provider: default_provider(),
539            current_dir_override,
540        })
541    })
542}
543
544/// Serializes tests and embedders that temporarily replace the process-wide
545/// filesystem provider.
546pub fn provider_override_lock() -> MutexGuard<'static, ()> {
547    PROVIDER_OVERRIDE_LOCK
548        .get_or_init(|| Mutex::new(()))
549        .lock()
550        .unwrap_or_else(|poisoned| poisoned.into_inner())
551}
552
553fn current_dir_override() -> Option<PathBuf> {
554    provider_state_lock()
555        .read()
556        .expect("filesystem provider lock poisoned")
557        .current_dir_override
558        .clone()
559}
560
561fn replace_current_dir_override(value: Option<PathBuf>) -> Option<PathBuf> {
562    let mut guard = provider_state_lock()
563        .write()
564        .expect("filesystem provider lock poisoned");
565    std::mem::replace(&mut guard.current_dir_override, value)
566}
567
568fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
569    let guard = provider_state_lock()
570        .read()
571        .expect("filesystem provider lock poisoned");
572    f(&*guard.provider)
573}
574
575fn resolve_path(path: &Path) -> PathBuf {
576    if path.is_absolute() {
577        return path.to_path_buf();
578    }
579    let state = provider_state_lock()
580        .read()
581        .expect("filesystem provider lock poisoned");
582    if let Some(base) = &state.current_dir_override {
583        if path.has_root() {
584            return path.to_path_buf();
585        }
586        return base.join(path);
587    }
588    path.to_path_buf()
589}
590
591fn next_current_dir_override(
592    current: Option<&PathBuf>,
593    provider_default: Option<PathBuf>,
594) -> Option<PathBuf> {
595    match provider_default {
596        Some(default) => current.cloned().or(Some(default)),
597        None => None,
598    }
599}
600
601pub fn set_provider(provider: Arc<dyn FsProvider>) {
602    let provider_default_current_dir = provider.current_dir_override();
603    let mut guard = provider_state_lock()
604        .write()
605        .expect("filesystem provider lock poisoned");
606    let current_dir_override = next_current_dir_override(
607        guard.current_dir_override.as_ref(),
608        provider_default_current_dir,
609    );
610    guard.provider = provider;
611    guard.current_dir_override = current_dir_override;
612}
613
614/// Temporarily replace the active provider and return a guard that restores the
615/// previous provider when dropped. Useful for tests that need to install a mock
616/// filesystem without permanently mutating global state.
617pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
618    let provider_default_current_dir = provider.current_dir_override();
619    let mut guard = provider_state_lock()
620        .write()
621        .expect("filesystem provider lock poisoned");
622    let previous = guard.provider.clone();
623    let previous_current_dir = guard.current_dir_override.clone();
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    ProviderGuard {
631        previous,
632        previous_current_dir,
633    }
634}
635
636/// Run a closure with the supplied provider installed, restoring the previous
637/// provider automatically afterwards.
638pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
639    let guard = replace_provider(provider);
640    let result = f();
641    drop(guard);
642    result
643}
644
645/// Returns the currently installed provider.
646pub fn current_provider() -> Arc<dyn FsProvider> {
647    provider_state_lock()
648        .read()
649        .expect("filesystem provider lock poisoned")
650        .provider
651        .clone()
652}
653
654pub fn current_dir() -> io::Result<PathBuf> {
655    if let Some(current) = current_dir_override() {
656        return Ok(current);
657    }
658    #[cfg(not(target_arch = "wasm32"))]
659    {
660        std::env::current_dir()
661    }
662    #[cfg(target_arch = "wasm32")]
663    {
664        Ok(PathBuf::from("/"))
665    }
666}
667
668pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
669    if current_dir_override().is_some() {
670        futures::executor::block_on(set_current_dir_async(path.as_ref().to_path_buf()))
671    } else {
672        #[cfg(not(target_arch = "wasm32"))]
673        {
674            std::env::set_current_dir(path)
675        }
676        #[cfg(target_arch = "wasm32")]
677        {
678            Ok(())
679        }
680    }
681}
682
683pub async fn set_current_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
684    if current_dir_override().is_some() {
685        let mut target = PathBuf::from(path.as_ref());
686        if !target.has_root() {
687            let base = current_dir()?;
688            target = base.join(target);
689        }
690        let canonical = canonicalize_async(&target).await.unwrap_or(target.clone());
691        let metadata = metadata_async(&canonical).await?;
692        if !metadata.is_dir() {
693            return Err(io::Error::new(
694                ErrorKind::NotFound,
695                format!("Not a directory: {}", canonical.display()),
696            ));
697        }
698        replace_current_dir_override(Some(canonical));
699        Ok(())
700    } else {
701        set_current_dir(path)
702    }
703}
704
705pub struct ProviderGuard {
706    previous: Arc<dyn FsProvider>,
707    previous_current_dir: Option<PathBuf>,
708}
709
710impl Drop for ProviderGuard {
711    fn drop(&mut self) {
712        let mut guard = provider_state_lock()
713            .write()
714            .expect("filesystem provider lock poisoned");
715        guard.provider = self.previous.clone();
716        guard.current_dir_override = self.previous_current_dir.clone();
717    }
718}
719
720pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
721    let resolved = paths
722        .iter()
723        .map(|path| resolve_path(path.as_path()))
724        .collect::<Vec<_>>();
725    let provider = current_provider();
726    provider.read_many(&resolved).await
727}
728
729pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
730    let resolved = resolve_path(path.as_ref());
731    let provider = current_provider();
732    provider.read(&resolved).await
733}
734
735pub fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
736    let path = path.as_ref().to_path_buf();
737    wait_for_fs(move || async move { read_async(path).await })
738}
739
740pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
741    let bytes = read_async(path).await?;
742    String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
743}
744
745pub fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
746    let path = path.as_ref().to_path_buf();
747    wait_for_fs(move || async move { read_to_string_async(path).await })
748}
749
750pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
751    let resolved = resolve_path(path.as_ref());
752    let provider = current_provider();
753    provider.write(&resolved, data.as_ref()).await
754}
755
756pub fn write(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
757    let path = path.as_ref().to_path_buf();
758    let data = data.as_ref().to_vec();
759    wait_for_fs(move || async move { write_async(path, data).await })
760}
761
762pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
763    let resolved = resolve_path(path.as_ref());
764    let provider = current_provider();
765    provider.remove_file(&resolved).await
766}
767
768pub fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
769    let path = path.as_ref().to_path_buf();
770    wait_for_fs(move || async move { remove_file_async(path).await })
771}
772
773pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
774    let resolved = resolve_path(path.as_ref());
775    let provider = current_provider();
776    provider.metadata(&resolved).await
777}
778
779pub fn metadata(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
780    let path = path.as_ref().to_path_buf();
781    wait_for_fs(move || async move { metadata_async(path).await })
782}
783
784pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
785    let resolved = resolve_path(path.as_ref());
786    let provider = current_provider();
787    provider.symlink_metadata(&resolved).await
788}
789
790pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
791    let resolved = resolve_path(path.as_ref());
792    let provider = current_provider();
793    provider.read_dir(&resolved).await
794}
795
796pub fn read_dir(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
797    let path = path.as_ref().to_path_buf();
798    wait_for_fs(move || async move { read_dir_async(path).await })
799}
800
801pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
802    let resolved = resolve_path(path.as_ref());
803    let provider = current_provider();
804    provider.canonicalize(&resolved).await
805}
806
807pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
808    let resolved = resolve_path(path.as_ref());
809    let provider = current_provider();
810    provider.create_dir(&resolved).await
811}
812
813pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
814    let resolved = resolve_path(path.as_ref());
815    let provider = current_provider();
816    provider.create_dir_all(&resolved).await
817}
818
819pub fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
820    let path = path.as_ref().to_path_buf();
821    wait_for_fs(move || async move { create_dir_all_async(path).await })
822}
823
824pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
825    let resolved = resolve_path(path.as_ref());
826    let provider = current_provider();
827    provider.remove_dir(&resolved).await
828}
829
830pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
831    let resolved = resolve_path(path.as_ref());
832    let provider = current_provider();
833    provider.remove_dir_all(&resolved).await
834}
835
836pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
837    let resolved_from = resolve_path(from.as_ref());
838    let resolved_to = resolve_path(to.as_ref());
839    let provider = current_provider();
840    provider.rename(&resolved_from, &resolved_to).await
841}
842
843pub fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
844    let from = from.as_ref().to_path_buf();
845    let to = to.as_ref().to_path_buf();
846    wait_for_fs(move || async move { rename_async(from, to).await })
847}
848
849pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
850    let resolved = resolve_path(path.as_ref());
851    let provider = current_provider();
852    provider.set_readonly(&resolved, readonly).await
853}
854
855pub async fn select_file_open_async(
856    request: &OpenFileDialogRequest,
857) -> io::Result<Option<OpenFileDialogSelection>> {
858    let mut resolved = request.clone();
859    if let Some(default_path) = resolved.default_path.as_mut() {
860        *default_path = resolve_path(default_path);
861    }
862    let provider = current_provider();
863    provider.select_file_open(&resolved).await
864}
865
866pub async fn select_file_save_async(
867    request: &SaveFileDialogRequest,
868) -> io::Result<Option<SaveFileDialogSelection>> {
869    let mut resolved = request.clone();
870    if let Some(default_path) = resolved.default_path.as_mut() {
871        *default_path = resolve_path(default_path);
872    }
873    let provider = current_provider();
874    provider.select_file_save(&resolved).await
875}
876
877pub async fn data_manifest_descriptor_async(
878    request: &DataManifestRequest,
879) -> io::Result<DataManifestDescriptor> {
880    let provider = current_provider();
881    provider.data_manifest_descriptor(request).await
882}
883
884pub async fn data_chunk_upload_targets_async(
885    request: &DataChunkUploadRequest,
886) -> io::Result<Vec<DataChunkUploadTarget>> {
887    let provider = current_provider();
888    provider.data_chunk_upload_targets(request).await
889}
890
891pub async fn data_upload_chunk_async(
892    target: &DataChunkUploadTarget,
893    data: &[u8],
894) -> io::Result<()> {
895    let provider = current_provider();
896    provider.data_upload_chunk(target, data).await
897}
898
899/// Copy a file from `from` to `to`, truncating the destination when it exists.
900/// Returns the number of bytes written, matching `std::fs::copy`.
901pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
902    let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
903    let mut writer = OpenOptions::new()
904        .write(true)
905        .create(true)
906        .truncate(true)
907        .open(to.as_ref())?;
908    io::copy(&mut reader, &mut writer)
909}
910
911#[cfg(not(target_arch = "wasm32"))]
912fn wait_for_fs<T, F, Fut>(factory: F) -> io::Result<T>
913where
914    T: Send + 'static,
915    F: FnOnce() -> Fut + Send + 'static,
916    Fut: std::future::Future<Output = io::Result<T>> + 'static,
917{
918    std::thread::spawn(move || futures::executor::block_on(factory()))
919        .join()
920        .map_err(|_| io::Error::other("filesystem worker thread panicked"))?
921}
922
923#[cfg(target_arch = "wasm32")]
924fn wait_for_fs<T, F, Fut>(factory: F) -> io::Result<T>
925where
926    F: FnOnce() -> Fut,
927    Fut: std::future::Future<Output = io::Result<T>>,
928{
929    futures::executor::block_on(factory())
930}
931
932fn default_provider() -> Arc<dyn FsProvider> {
933    #[cfg(not(target_arch = "wasm32"))]
934    {
935        Arc::new(NativeFsProvider)
936    }
937    #[cfg(target_arch = "wasm32")]
938    {
939        Arc::new(PlaceholderFsProvider)
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946    use once_cell::sync::Lazy;
947    use std::collections::{HashMap, HashSet};
948    use std::io::{Read, Seek, SeekFrom, Write};
949    use std::sync::Mutex;
950    use tempfile::tempdir;
951
952    static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
953
954    fn test_lock() -> std::sync::MutexGuard<'static, ()> {
955        TEST_LOCK
956            .lock()
957            .unwrap_or_else(|poisoned| poisoned.into_inner())
958    }
959
960    fn comparable_path(path: impl AsRef<Path>) -> PathBuf {
961        #[cfg(windows)]
962        {
963            let text = path.as_ref().to_string_lossy();
964            if let Some(stripped) = text.strip_prefix(r"\\?\UNC\") {
965                return PathBuf::from(format!(r"\\{stripped}"));
966            }
967            if let Some(stripped) = text.strip_prefix(r"\\?\") {
968                return PathBuf::from(stripped);
969            }
970            PathBuf::from(text.as_ref())
971        }
972        #[cfg(not(windows))]
973        {
974            path.as_ref().to_path_buf()
975        }
976    }
977
978    fn assert_same_path(left: impl AsRef<Path>, right: impl AsRef<Path>) {
979        assert_eq!(comparable_path(left), comparable_path(right));
980    }
981
982    struct UnsupportedProvider;
983
984    struct AsyncOpenProvider {
985        opened_async: Arc<Mutex<bool>>,
986        flushed_async: Arc<Mutex<bool>>,
987    }
988
989    struct TestProviderStateGuard {
990        previous_provider: Arc<dyn FsProvider>,
991        previous_current_dir: Option<PathBuf>,
992    }
993
994    struct ProcessCwdGuard {
995        previous: PathBuf,
996    }
997
998    struct VirtualFsProvider {
999        default_current_dir: PathBuf,
1000        dirs: Mutex<HashSet<PathBuf>>,
1001        files: Mutex<HashMap<PathBuf, Vec<u8>>>,
1002    }
1003
1004    impl Drop for ProcessCwdGuard {
1005        fn drop(&mut self) {
1006            let _ = std::env::set_current_dir(&self.previous);
1007        }
1008    }
1009
1010    impl TestProviderStateGuard {
1011        fn capture() -> Self {
1012            let guard = provider_state_lock()
1013                .read()
1014                .expect("filesystem provider lock poisoned");
1015            Self {
1016                previous_provider: guard.provider.clone(),
1017                previous_current_dir: guard.current_dir_override.clone(),
1018            }
1019        }
1020    }
1021
1022    impl Drop for TestProviderStateGuard {
1023        fn drop(&mut self) {
1024            let mut guard = provider_state_lock()
1025                .write()
1026                .expect("filesystem provider lock poisoned");
1027            guard.provider = self.previous_provider.clone();
1028            guard.current_dir_override = self.previous_current_dir.clone();
1029        }
1030    }
1031
1032    impl VirtualFsProvider {
1033        fn new(default_current_dir: impl Into<PathBuf>, dirs: &[&str]) -> Self {
1034            let default_current_dir = default_current_dir.into();
1035            let mut all_dirs = HashSet::from([default_current_dir.clone()]);
1036            for dir in dirs {
1037                all_dirs.insert(PathBuf::from(dir));
1038            }
1039            Self {
1040                default_current_dir,
1041                dirs: Mutex::new(all_dirs),
1042                files: Mutex::new(HashMap::new()),
1043            }
1044        }
1045
1046        fn file_bytes(&self, path: impl AsRef<Path>) -> Option<Vec<u8>> {
1047            self.files.lock().unwrap().get(path.as_ref()).cloned()
1048        }
1049    }
1050
1051    struct AsyncTestHandle {
1052        cursor: usize,
1053        data: Vec<u8>,
1054        flushed_async: Arc<Mutex<bool>>,
1055    }
1056
1057    impl Read for AsyncTestHandle {
1058        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1059            let remaining = self.data.len().saturating_sub(self.cursor);
1060            let to_read = remaining.min(buf.len());
1061            buf[..to_read].copy_from_slice(&self.data[self.cursor..self.cursor + to_read]);
1062            self.cursor += to_read;
1063            Ok(to_read)
1064        }
1065    }
1066
1067    impl Write for AsyncTestHandle {
1068        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1069            let end = self.cursor + buf.len();
1070            if end > self.data.len() {
1071                self.data.resize(end, 0);
1072            }
1073            self.data[self.cursor..end].copy_from_slice(buf);
1074            self.cursor = end;
1075            Ok(buf.len())
1076        }
1077
1078        fn flush(&mut self) -> io::Result<()> {
1079            Ok(())
1080        }
1081    }
1082
1083    impl Seek for AsyncTestHandle {
1084        fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
1085            let next = match pos {
1086                SeekFrom::Start(offset) => offset as i64,
1087                SeekFrom::End(offset) => self.data.len() as i64 + offset,
1088                SeekFrom::Current(offset) => self.cursor as i64 + offset,
1089            };
1090            if next < 0 {
1091                return Err(io::Error::new(ErrorKind::InvalidInput, "seek before start"));
1092            }
1093            self.cursor = next as usize;
1094            Ok(self.cursor as u64)
1095        }
1096    }
1097
1098    #[async_trait(?Send)]
1099    impl FileHandle for AsyncTestHandle {
1100        async fn flush_async(&mut self) -> io::Result<()> {
1101            *self.flushed_async.lock().unwrap() = true;
1102            Ok(())
1103        }
1104    }
1105
1106    #[async_trait(?Send)]
1107    impl FsProvider for UnsupportedProvider {
1108        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1109            Err(unsupported())
1110        }
1111
1112        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
1113            Err(unsupported())
1114        }
1115
1116        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1117            Err(unsupported())
1118        }
1119
1120        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
1121            Err(unsupported())
1122        }
1123
1124        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1125            Err(unsupported())
1126        }
1127
1128        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1129            Err(unsupported())
1130        }
1131
1132        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1133            Err(unsupported())
1134        }
1135
1136        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1137            Err(unsupported())
1138        }
1139
1140        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
1141            Err(unsupported())
1142        }
1143
1144        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1145            Err(unsupported())
1146        }
1147
1148        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1149            Err(unsupported())
1150        }
1151
1152        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
1153            Err(unsupported())
1154        }
1155
1156        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1157            Err(unsupported())
1158        }
1159
1160        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1161            Err(unsupported())
1162        }
1163
1164        async fn data_manifest_descriptor(
1165            &self,
1166            _request: &DataManifestRequest,
1167        ) -> io::Result<DataManifestDescriptor> {
1168            Err(unsupported())
1169        }
1170
1171        async fn data_chunk_upload_targets(
1172            &self,
1173            _request: &DataChunkUploadRequest,
1174        ) -> io::Result<Vec<DataChunkUploadTarget>> {
1175            Err(unsupported())
1176        }
1177
1178        async fn data_upload_chunk(
1179            &self,
1180            _target: &DataChunkUploadTarget,
1181            _data: &[u8],
1182        ) -> io::Result<()> {
1183            Err(unsupported())
1184        }
1185    }
1186
1187    #[async_trait(?Send)]
1188    impl FsProvider for VirtualFsProvider {
1189        fn current_dir_override(&self) -> Option<PathBuf> {
1190            Some(self.default_current_dir.clone())
1191        }
1192
1193        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1194            Err(unsupported())
1195        }
1196
1197        async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
1198            self.files
1199                .lock()
1200                .unwrap()
1201                .get(path)
1202                .cloned()
1203                .ok_or_else(|| io::Error::new(ErrorKind::NotFound, path.display().to_string()))
1204        }
1205
1206        async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1207            self.files
1208                .lock()
1209                .unwrap()
1210                .insert(path.to_path_buf(), data.to_vec());
1211            Ok(())
1212        }
1213
1214        async fn remove_file(&self, path: &Path) -> io::Result<()> {
1215            self.files.lock().unwrap().remove(path);
1216            Ok(())
1217        }
1218
1219        async fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
1220            if self.dirs.lock().unwrap().contains(path) {
1221                return Ok(FsMetadata::new(FsFileType::Directory, 0, None, false));
1222            }
1223            if let Some(bytes) = self.files.lock().unwrap().get(path) {
1224                return Ok(FsMetadata::new(
1225                    FsFileType::File,
1226                    bytes.len() as u64,
1227                    None,
1228                    false,
1229                ));
1230            }
1231            Err(io::Error::new(
1232                ErrorKind::NotFound,
1233                path.display().to_string(),
1234            ))
1235        }
1236
1237        async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata> {
1238            self.metadata(path).await
1239        }
1240
1241        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1242            Ok(Vec::new())
1243        }
1244
1245        async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
1246            Ok(path.to_path_buf())
1247        }
1248
1249        async fn create_dir(&self, path: &Path) -> io::Result<()> {
1250            self.dirs.lock().unwrap().insert(path.to_path_buf());
1251            Ok(())
1252        }
1253
1254        async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
1255            let mut dirs = self.dirs.lock().unwrap();
1256            for ancestor in path.ancestors() {
1257                dirs.insert(ancestor.to_path_buf());
1258            }
1259            Ok(())
1260        }
1261
1262        async fn remove_dir(&self, path: &Path) -> io::Result<()> {
1263            self.dirs.lock().unwrap().remove(path);
1264            Ok(())
1265        }
1266
1267        async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
1268            self.dirs
1269                .lock()
1270                .unwrap()
1271                .retain(|dir| !dir.starts_with(path));
1272            Ok(())
1273        }
1274
1275        async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1276            let mut files = self.files.lock().unwrap();
1277            let data = files
1278                .remove(from)
1279                .ok_or_else(|| io::Error::new(ErrorKind::NotFound, from.display().to_string()))?;
1280            files.insert(to.to_path_buf(), data);
1281            Ok(())
1282        }
1283
1284        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1285            Ok(())
1286        }
1287    }
1288
1289    #[async_trait(?Send)]
1290    impl FsProvider for AsyncOpenProvider {
1291        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1292            Err(unsupported())
1293        }
1294
1295        async fn open_async(
1296            &self,
1297            _path: &Path,
1298            _flags: &OpenFlags,
1299        ) -> io::Result<Box<dyn FileHandle>> {
1300            *self.opened_async.lock().unwrap() = true;
1301            Ok(Box::new(AsyncTestHandle {
1302                cursor: 0,
1303                data: b"async contents".to_vec(),
1304                flushed_async: self.flushed_async.clone(),
1305            }))
1306        }
1307
1308        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
1309            Err(unsupported())
1310        }
1311
1312        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1313            Err(unsupported())
1314        }
1315
1316        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
1317            Err(unsupported())
1318        }
1319
1320        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1321            Err(unsupported())
1322        }
1323
1324        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1325            Err(unsupported())
1326        }
1327
1328        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1329            Err(unsupported())
1330        }
1331
1332        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1333            Err(unsupported())
1334        }
1335
1336        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
1337            Err(unsupported())
1338        }
1339
1340        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1341            Err(unsupported())
1342        }
1343
1344        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1345            Err(unsupported())
1346        }
1347
1348        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
1349            Err(unsupported())
1350        }
1351
1352        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1353            Err(unsupported())
1354        }
1355
1356        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1357            Err(unsupported())
1358        }
1359    }
1360
1361    fn unsupported() -> io::Error {
1362        io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
1363    }
1364
1365    #[test]
1366    fn copy_file_round_trip() {
1367        let _guard = test_lock();
1368        let dir = tempdir().expect("tempdir");
1369        let src = dir.path().join("src.bin");
1370        let dst = dir.path().join("dst.bin");
1371        {
1372            let mut file = std::fs::File::create(&src).expect("create src");
1373            file.write_all(b"hello filesystem").expect("write src");
1374        }
1375
1376        copy_file(&src, &dst).expect("copy");
1377        let mut dst_file = File::open(&dst).expect("open dst");
1378        let mut contents = Vec::new();
1379        dst_file
1380            .read_to_end(&mut contents)
1381            .expect("read destination");
1382        assert_eq!(contents, b"hello filesystem");
1383    }
1384
1385    #[test]
1386    fn set_readonly_flips_metadata_flag() {
1387        let _guard = test_lock();
1388        let dir = tempdir().expect("tempdir");
1389        let path = dir.path().join("flag.txt");
1390        futures::executor::block_on(write_async(&path, b"flag")).expect("write");
1391
1392        futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
1393        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
1394        assert!(meta.is_readonly());
1395
1396        futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
1397        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
1398        assert!(!meta.is_readonly());
1399    }
1400
1401    #[test]
1402    fn sync_helpers_work_inside_async_executor() {
1403        let _guard = TEST_LOCK.lock().unwrap();
1404        let dir = tempdir().expect("tempdir");
1405        let path = dir.path().join("nested").join("file.txt");
1406        let parent = path.parent().unwrap().to_path_buf();
1407
1408        futures::executor::block_on(async {
1409            create_dir_all(&parent).expect("create dir");
1410            write(&path, b"hello").expect("write");
1411            assert_eq!(read(&path).expect("read"), b"hello");
1412            assert_eq!(read_to_string(&path).expect("read string"), "hello");
1413            assert!(metadata(&path).expect("metadata").is_file());
1414            assert_eq!(read_dir(&parent).expect("read dir").len(), 1);
1415            remove_file(&path).expect("remove");
1416        });
1417    }
1418
1419    #[test]
1420    fn replace_provider_restores_previous() {
1421        let _guard = test_lock();
1422        let original = current_provider();
1423        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1424        {
1425            let _guard = replace_provider(custom.clone());
1426            let active = current_provider();
1427            assert!(Arc::ptr_eq(&active, &custom));
1428        }
1429        let final_provider = current_provider();
1430        assert!(Arc::ptr_eq(&final_provider, &original));
1431    }
1432
1433    #[test]
1434    #[cfg(not(target_arch = "wasm32"))]
1435    fn native_provider_replacement_preserves_process_cwd_resolution() {
1436        let _guard = test_lock();
1437        let temp = tempdir().expect("tempdir");
1438        let previous = std::env::current_dir().expect("current dir");
1439        let _cwd_guard = ProcessCwdGuard { previous };
1440        std::env::set_current_dir(temp.path()).expect("set temp cwd");
1441
1442        let _provider_guard = replace_provider(Arc::new(NativeFsProvider));
1443        let current = current_dir().expect("vfs current dir");
1444        let expected = std::fs::canonicalize(temp.path()).expect("canonical temp");
1445        assert_same_path(&current, &expected);
1446
1447        futures::executor::block_on(write_async("native-relative.txt", b"native"))
1448            .expect("write relative path");
1449        assert_eq!(
1450            std::fs::read_to_string(temp.path().join("native-relative.txt")).expect("read file"),
1451            "native"
1452        );
1453
1454        std::fs::create_dir(temp.path().join("child")).expect("create child");
1455        set_current_dir("child").expect("set child cwd");
1456        assert_same_path(
1457            std::env::current_dir().expect("process cwd"),
1458            expected.join("child"),
1459        );
1460    }
1461
1462    #[test]
1463    fn set_provider_initializes_virtual_cwd_from_provider_default() {
1464        let _guard = test_lock();
1465        let _state_guard = TestProviderStateGuard::capture();
1466        replace_current_dir_override(None);
1467
1468        set_provider(Arc::new(VirtualFsProvider::new("/sandbox", &[])));
1469
1470        assert_eq!(
1471            current_dir().expect("virtual cwd"),
1472            PathBuf::from("/sandbox")
1473        );
1474    }
1475
1476    #[test]
1477    fn set_provider_preserves_existing_virtual_cwd() {
1478        let _guard = test_lock();
1479        let _state_guard = TestProviderStateGuard::capture();
1480        let initial = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1481        set_provider(initial);
1482        set_current_dir("/workspace").expect("set virtual cwd");
1483
1484        let replacement = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1485        set_provider(replacement.clone());
1486
1487        assert_eq!(
1488            current_dir().expect("virtual cwd"),
1489            PathBuf::from("/workspace")
1490        );
1491        futures::executor::block_on(write_async("data.txt", b"virtual")).expect("write relative");
1492        assert_eq!(
1493            replacement.file_bytes("/workspace/data.txt").as_deref(),
1494            Some(&b"virtual"[..])
1495        );
1496        assert_eq!(replacement.file_bytes("data.txt"), None);
1497    }
1498
1499    #[test]
1500    fn replace_provider_preserves_existing_virtual_cwd() {
1501        let _guard = test_lock();
1502        let initial = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1503        let _initial_guard = replace_provider(initial);
1504        set_current_dir("/workspace").expect("set virtual cwd");
1505
1506        let replacement = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1507        {
1508            let _replacement_guard = replace_provider(replacement.clone());
1509
1510            assert_eq!(
1511                current_dir().expect("virtual cwd"),
1512                PathBuf::from("/workspace")
1513            );
1514            futures::executor::block_on(write_async("nested.txt", b"replacement"))
1515                .expect("write relative");
1516        }
1517
1518        assert_eq!(
1519            replacement.file_bytes("/workspace/nested.txt").as_deref(),
1520            Some(&b"replacement"[..])
1521        );
1522        assert_eq!(replacement.file_bytes("nested.txt"), None);
1523    }
1524
1525    #[test]
1526    fn virtual_root_paths_do_not_resolve_relative_to_virtual_cwd() {
1527        let _guard = test_lock();
1528        let provider = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1529        let _provider_guard = replace_provider(provider.clone());
1530        set_current_dir("/workspace").expect("set virtual cwd");
1531
1532        futures::executor::block_on(write_async("/root.txt", b"root")).expect("write absolute");
1533
1534        assert_eq!(
1535            provider.file_bytes("/root.txt").as_deref(),
1536            Some(&b"root"[..])
1537        );
1538        assert_eq!(provider.file_bytes("/workspace/root.txt"), None);
1539    }
1540
1541    #[test]
1542    #[cfg(not(target_arch = "wasm32"))]
1543    fn native_provider_replacement_clears_virtual_cwd_override() {
1544        let _guard = test_lock();
1545        let temp = tempdir().expect("tempdir");
1546        let previous = std::env::current_dir().expect("current dir");
1547        let _cwd_guard = ProcessCwdGuard { previous };
1548        std::env::set_current_dir(temp.path()).expect("set temp cwd");
1549
1550        let virtual_provider = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1551        let _virtual_guard = replace_provider(virtual_provider);
1552        set_current_dir("/workspace").expect("set virtual cwd");
1553
1554        {
1555            let _native_guard = replace_provider(Arc::new(NativeFsProvider));
1556            let expected = std::fs::canonicalize(temp.path()).expect("canonical temp");
1557            assert_same_path(current_dir().expect("native cwd"), &expected);
1558
1559            futures::executor::block_on(write_async("native.txt", b"native"))
1560                .expect("write native relative");
1561            assert_eq!(
1562                std::fs::read_to_string(temp.path().join("native.txt")).expect("read native file"),
1563                "native"
1564            );
1565        }
1566    }
1567
1568    #[test]
1569    fn open_async_and_flush_async_use_provider_async_paths() {
1570        let _guard = test_lock();
1571        let opened_async = Arc::new(Mutex::new(false));
1572        let flushed_async = Arc::new(Mutex::new(false));
1573        let provider = Arc::new(AsyncOpenProvider {
1574            opened_async: opened_async.clone(),
1575            flushed_async: flushed_async.clone(),
1576        });
1577        let _provider_guard = replace_provider(provider);
1578
1579        let mut file =
1580            futures::executor::block_on(OpenOptions::new().read(true).open_async("data.txt"))
1581                .expect("async open");
1582        let mut contents = String::new();
1583        file.read_to_string(&mut contents).expect("read contents");
1584        futures::executor::block_on(file.flush_async()).expect("async flush");
1585
1586        assert_eq!(contents, "async contents");
1587        assert!(*opened_async.lock().unwrap());
1588        assert!(*flushed_async.lock().unwrap());
1589    }
1590
1591    #[test]
1592    fn select_file_open_defaults_to_cancelled_selection() {
1593        let _guard = test_lock();
1594        let provider: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1595        let _provider_guard = replace_provider(provider);
1596        let request = OpenFileDialogRequest {
1597            title: Some("Open".to_string()),
1598            default_path: Some(PathBuf::from("data")),
1599            filters: vec![OpenFileDialogFilter {
1600                patterns: vec!["*.csv".to_string()],
1601                description: Some("CSV files".to_string()),
1602            }],
1603            multiselect: false,
1604        };
1605
1606        let selection =
1607            futures::executor::block_on(select_file_open_async(&request)).expect("select file");
1608
1609        assert_eq!(selection, None);
1610    }
1611
1612    #[test]
1613    fn select_file_save_defaults_to_cancelled_selection() {
1614        let _guard = test_lock();
1615        let provider: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1616        let _provider_guard = replace_provider(provider);
1617        let request = SaveFileDialogRequest {
1618            title: Some("Save".to_string()),
1619            default_path: Some(PathBuf::from("data.mat")),
1620            filters: vec![OpenFileDialogFilter {
1621                patterns: vec!["*.mat".to_string()],
1622                description: Some("MAT files".to_string()),
1623            }],
1624        };
1625
1626        let selection =
1627            futures::executor::block_on(select_file_save_async(&request)).expect("select file");
1628
1629        assert_eq!(selection, None);
1630    }
1631
1632    #[test]
1633    fn with_provider_restores_even_on_panic() {
1634        let _guard = test_lock();
1635        let original = current_provider();
1636        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1637        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1638            with_provider_override(custom.clone(), || {
1639                let active = current_provider();
1640                assert!(Arc::ptr_eq(&active, &custom));
1641                panic!("boom");
1642            })
1643        }));
1644        assert!(result.is_err());
1645        let final_provider = current_provider();
1646        assert!(Arc::ptr_eq(&final_provider, &original));
1647    }
1648}