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
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
562/// Serializes tests and embedders that temporarily replace the process-wide
563/// filesystem provider.
564pub 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
632/// Temporarily replace the active provider and return a guard that restores the
633/// previous provider when dropped. Useful for tests that need to install a mock
634/// filesystem without permanently mutating global state.
635pub 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
654/// Run a closure with the supplied provider installed, restoring the previous
655/// provider automatically afterwards.
656pub 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
663/// Returns the currently installed provider.
664pub 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
928/// Copy a file from `from` to `to`, truncating the destination when it exists.
929/// Returns the number of bytes written, matching `std::fs::copy`.
930pub 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(&current, &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}