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, RwLock};
9use std::time::SystemTime;
10
11#[cfg(not(target_arch = "wasm32"))]
12mod native;
13#[cfg(not(target_arch = "wasm32"))]
14pub mod remote;
15#[cfg(not(target_arch = "wasm32"))]
16pub mod sandbox;
17#[cfg(target_arch = "wasm32")]
18mod wasm;
19
20#[cfg(not(target_arch = "wasm32"))]
21pub use native::NativeFsProvider;
22#[cfg(not(target_arch = "wasm32"))]
23pub use remote::{RemoteFsConfig, RemoteFsProvider};
24#[cfg(not(target_arch = "wasm32"))]
25pub use sandbox::SandboxFsProvider;
26#[cfg(target_arch = "wasm32")]
27pub use wasm::PlaceholderFsProvider;
28
29pub mod data_contract;
30
31use data_contract::{
32    DataChunkUploadRequest, DataChunkUploadTarget, DataManifestDescriptor, DataManifestRequest,
33};
34
35pub trait FileHandle: Read + Write + Seek + Send + Sync {}
36
37impl<T> FileHandle for T where T: Read + Write + Seek + Send + Sync + 'static {}
38
39#[derive(Clone, Debug, Default)]
40pub struct OpenFlags {
41    pub read: bool,
42    pub write: bool,
43    pub append: bool,
44    pub truncate: bool,
45    pub create: bool,
46    pub create_new: bool,
47}
48
49#[derive(Clone, Debug)]
50pub struct OpenOptions {
51    flags: OpenFlags,
52}
53
54impl OpenOptions {
55    pub fn new() -> Self {
56        Self {
57            flags: OpenFlags::default(),
58        }
59    }
60
61    pub fn read(&mut self, value: bool) -> &mut Self {
62        self.flags.read = value;
63        self
64    }
65
66    pub fn write(&mut self, value: bool) -> &mut Self {
67        self.flags.write = value;
68        self
69    }
70
71    pub fn append(&mut self, value: bool) -> &mut Self {
72        self.flags.append = value;
73        self
74    }
75
76    pub fn truncate(&mut self, value: bool) -> &mut Self {
77        self.flags.truncate = value;
78        self
79    }
80
81    pub fn create(&mut self, value: bool) -> &mut Self {
82        self.flags.create = value;
83        self
84    }
85
86    pub fn create_new(&mut self, value: bool) -> &mut Self {
87        self.flags.create_new = value;
88        self
89    }
90
91    pub fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
92        let resolved = resolve_path(path.as_ref());
93        with_provider(|provider| provider.open(&resolved, &self.flags)).map(File::from_handle)
94    }
95
96    pub fn flags(&self) -> &OpenFlags {
97        &self.flags
98    }
99}
100
101impl Default for OpenOptions {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub enum FsFileType {
109    Directory,
110    File,
111    Symlink,
112    Other,
113    Unknown,
114}
115
116#[derive(Clone, Debug)]
117pub struct FsMetadata {
118    file_type: FsFileType,
119    len: u64,
120    modified: Option<SystemTime>,
121    readonly: bool,
122    hash: Option<String>,
123}
124
125impl FsMetadata {
126    pub fn new(
127        file_type: FsFileType,
128        len: u64,
129        modified: Option<SystemTime>,
130        readonly: bool,
131    ) -> Self {
132        Self {
133            file_type,
134            len,
135            modified,
136            readonly,
137            hash: None,
138        }
139    }
140
141    pub fn new_with_hash(
142        file_type: FsFileType,
143        len: u64,
144        modified: Option<SystemTime>,
145        readonly: bool,
146        hash: Option<String>,
147    ) -> Self {
148        Self {
149            file_type,
150            len,
151            modified,
152            readonly,
153            hash,
154        }
155    }
156
157    pub fn file_type(&self) -> FsFileType {
158        self.file_type
159    }
160
161    pub fn is_dir(&self) -> bool {
162        matches!(self.file_type, FsFileType::Directory)
163    }
164
165    pub fn is_file(&self) -> bool {
166        matches!(self.file_type, FsFileType::File)
167    }
168
169    pub fn is_symlink(&self) -> bool {
170        matches!(self.file_type, FsFileType::Symlink)
171    }
172
173    pub fn len(&self) -> u64 {
174        self.len
175    }
176
177    pub fn hash(&self) -> Option<&str> {
178        self.hash.as_deref()
179    }
180
181    pub fn is_empty(&self) -> bool {
182        self.len == 0
183    }
184
185    pub fn modified(&self) -> Option<SystemTime> {
186        self.modified
187    }
188
189    pub fn is_readonly(&self) -> bool {
190        self.readonly
191    }
192}
193
194#[derive(Clone, Debug)]
195pub struct DirEntry {
196    path: PathBuf,
197    file_name: OsString,
198    file_type: FsFileType,
199}
200
201#[derive(Clone, Debug)]
202pub struct ReadManyEntry {
203    path: PathBuf,
204    bytes: Option<Vec<u8>>,
205    error: Option<String>,
206}
207
208impl ReadManyEntry {
209    pub fn new(path: PathBuf, bytes: Option<Vec<u8>>) -> Self {
210        Self {
211            path,
212            bytes,
213            error: None,
214        }
215    }
216
217    pub fn with_error(path: PathBuf, error: String) -> Self {
218        Self {
219            path,
220            bytes: None,
221            error: Some(error),
222        }
223    }
224
225    pub fn path(&self) -> &Path {
226        &self.path
227    }
228
229    pub fn bytes(&self) -> Option<&[u8]> {
230        self.bytes.as_deref()
231    }
232
233    pub fn into_bytes(self) -> Option<Vec<u8>> {
234        self.bytes
235    }
236
237    pub fn error(&self) -> Option<&str> {
238        self.error.as_deref()
239    }
240}
241
242impl DirEntry {
243    pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
244        Self {
245            path,
246            file_name,
247            file_type,
248        }
249    }
250
251    pub fn path(&self) -> &Path {
252        &self.path
253    }
254
255    pub fn file_name(&self) -> &OsString {
256        &self.file_name
257    }
258
259    pub fn file_type(&self) -> FsFileType {
260        self.file_type
261    }
262
263    pub fn is_dir(&self) -> bool {
264        matches!(self.file_type, FsFileType::Directory)
265    }
266}
267
268#[async_trait(?Send)]
269pub trait FsProvider: Send + Sync + 'static {
270    fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
271    async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
272    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
273    async fn remove_file(&self, path: &Path) -> io::Result<()>;
274    async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
275    async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
276    async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
277    async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
278    async fn create_dir(&self, path: &Path) -> io::Result<()>;
279    async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
280    async fn remove_dir(&self, path: &Path) -> io::Result<()>;
281    async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
282    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
283    async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
284
285    async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
286        let mut entries = Vec::with_capacity(paths.len());
287        for path in paths {
288            let entry = match self.read(path).await {
289                Ok(payload) => ReadManyEntry::new(path.clone(), Some(payload)),
290                Err(error) => {
291                    warn!(
292                        "fs.read_many.miss path={} kind={:?} error={}",
293                        path.to_string_lossy(),
294                        error.kind(),
295                        error
296                    );
297                    ReadManyEntry::with_error(
298                        path.clone(),
299                        format!("kind={:?}; error={}", error.kind(), error),
300                    )
301                }
302            };
303            entries.push(entry);
304        }
305        Ok(entries)
306    }
307
308    async fn data_manifest_descriptor(
309        &self,
310        _request: &DataManifestRequest,
311    ) -> io::Result<DataManifestDescriptor> {
312        Err(io::Error::new(
313            ErrorKind::Unsupported,
314            "data manifest descriptor is unsupported by this provider",
315        ))
316    }
317
318    async fn data_chunk_upload_targets(
319        &self,
320        _request: &DataChunkUploadRequest,
321    ) -> io::Result<Vec<DataChunkUploadTarget>> {
322        Err(io::Error::new(
323            ErrorKind::Unsupported,
324            "data chunk upload targets are unsupported by this provider",
325        ))
326    }
327
328    async fn data_upload_chunk(
329        &self,
330        _target: &DataChunkUploadTarget,
331        _data: &[u8],
332    ) -> io::Result<()> {
333        Err(io::Error::new(
334            ErrorKind::Unsupported,
335            "data chunk upload is unsupported by this provider",
336        ))
337    }
338}
339
340pub struct File {
341    inner: Box<dyn FileHandle>,
342}
343
344impl File {
345    fn from_handle(handle: Box<dyn FileHandle>) -> Self {
346        Self { inner: handle }
347    }
348
349    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
350        let mut opts = OpenOptions::new();
351        opts.read(true);
352        opts.open(path)
353    }
354
355    pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
356        let mut opts = OpenOptions::new();
357        opts.write(true).create(true).truncate(true);
358        opts.open(path)
359    }
360}
361
362impl fmt::Debug for File {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.debug_struct("File").finish_non_exhaustive()
365    }
366}
367
368impl Read for File {
369    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
370        self.inner.read(buf)
371    }
372}
373
374impl Write for File {
375    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
376        self.inner.write(buf)
377    }
378
379    fn flush(&mut self) -> io::Result<()> {
380        self.inner.flush()
381    }
382}
383
384impl Seek for File {
385    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
386        self.inner.seek(pos)
387    }
388}
389
390static PROVIDER: OnceCell<RwLock<Arc<dyn FsProvider>>> = OnceCell::new();
391#[cfg(target_arch = "wasm32")]
392static CURRENT_DIR: OnceCell<RwLock<PathBuf>> = OnceCell::new();
393
394fn provider_lock() -> &'static RwLock<Arc<dyn FsProvider>> {
395    PROVIDER.get_or_init(|| RwLock::new(default_provider()))
396}
397
398#[cfg(target_arch = "wasm32")]
399fn current_dir_lock() -> &'static RwLock<PathBuf> {
400    CURRENT_DIR.get_or_init(|| RwLock::new(PathBuf::from("/")))
401}
402
403fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
404    let guard = provider_lock()
405        .read()
406        .expect("filesystem provider lock poisoned");
407    f(&**guard)
408}
409
410fn resolve_path(path: &Path) -> PathBuf {
411    #[cfg(target_arch = "wasm32")]
412    {
413        if path.is_absolute() {
414            return path.to_path_buf();
415        }
416        if let Ok(base) = current_dir() {
417            return base.join(path);
418        }
419        return PathBuf::from("/").join(path);
420    }
421    #[cfg(not(target_arch = "wasm32"))]
422    {
423        path.to_path_buf()
424    }
425}
426
427pub fn set_provider(provider: Arc<dyn FsProvider>) {
428    let mut guard = provider_lock()
429        .write()
430        .expect("filesystem provider lock poisoned");
431    *guard = provider;
432}
433
434/// Temporarily replace the active provider and return a guard that restores the
435/// previous provider when dropped. Useful for tests that need to install a mock
436/// filesystem without permanently mutating global state.
437pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
438    let mut guard = provider_lock()
439        .write()
440        .expect("filesystem provider lock poisoned");
441    let previous = guard.clone();
442    *guard = provider;
443    ProviderGuard { previous }
444}
445
446/// Run a closure with the supplied provider installed, restoring the previous
447/// provider automatically afterwards.
448pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
449    let guard = replace_provider(provider);
450    let result = f();
451    drop(guard);
452    result
453}
454
455/// Returns the currently installed provider.
456pub fn current_provider() -> Arc<dyn FsProvider> {
457    provider_lock()
458        .read()
459        .expect("filesystem provider lock poisoned")
460        .clone()
461}
462
463pub fn current_dir() -> io::Result<PathBuf> {
464    #[cfg(target_arch = "wasm32")]
465    {
466        return Ok(current_dir_lock()
467            .read()
468            .expect("filesystem current dir lock poisoned")
469            .clone());
470    }
471    #[cfg(not(target_arch = "wasm32"))]
472    {
473        std::env::current_dir()
474    }
475}
476
477pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
478    #[cfg(target_arch = "wasm32")]
479    {
480        let mut target = PathBuf::from(path.as_ref());
481        if !target.is_absolute() {
482            let base = current_dir()?;
483            target = base.join(target);
484        }
485        let canonical =
486            futures::executor::block_on(canonicalize_async(&target)).unwrap_or(target.clone());
487        let metadata = futures::executor::block_on(metadata_async(&canonical))?;
488        if !metadata.is_dir() {
489            return Err(io::Error::new(
490                ErrorKind::NotFound,
491                format!("Not a directory: {}", canonical.display()),
492            ));
493        }
494        let mut guard = current_dir_lock()
495            .write()
496            .expect("filesystem current dir lock poisoned");
497        *guard = canonical;
498        return Ok(());
499    }
500    #[cfg(not(target_arch = "wasm32"))]
501    {
502        std::env::set_current_dir(path)
503    }
504}
505
506pub struct ProviderGuard {
507    previous: Arc<dyn FsProvider>,
508}
509
510impl Drop for ProviderGuard {
511    fn drop(&mut self) {
512        set_provider(self.previous.clone());
513    }
514}
515
516pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
517    let resolved = paths
518        .iter()
519        .map(|path| resolve_path(path.as_path()))
520        .collect::<Vec<_>>();
521    let provider = current_provider();
522    provider.read_many(&resolved).await
523}
524
525pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
526    let resolved = resolve_path(path.as_ref());
527    let provider = current_provider();
528    provider.read(&resolved).await
529}
530
531pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
532    let bytes = read_async(path).await?;
533    String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
534}
535
536pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
537    let resolved = resolve_path(path.as_ref());
538    let provider = current_provider();
539    provider.write(&resolved, data.as_ref()).await
540}
541
542pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
543    let resolved = resolve_path(path.as_ref());
544    let provider = current_provider();
545    provider.remove_file(&resolved).await
546}
547
548pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
549    let resolved = resolve_path(path.as_ref());
550    let provider = current_provider();
551    provider.metadata(&resolved).await
552}
553
554pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
555    let resolved = resolve_path(path.as_ref());
556    let provider = current_provider();
557    provider.symlink_metadata(&resolved).await
558}
559
560pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
561    let resolved = resolve_path(path.as_ref());
562    let provider = current_provider();
563    provider.read_dir(&resolved).await
564}
565
566pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
567    let resolved = resolve_path(path.as_ref());
568    let provider = current_provider();
569    provider.canonicalize(&resolved).await
570}
571
572pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
573    let resolved = resolve_path(path.as_ref());
574    let provider = current_provider();
575    provider.create_dir(&resolved).await
576}
577
578pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
579    let resolved = resolve_path(path.as_ref());
580    let provider = current_provider();
581    provider.create_dir_all(&resolved).await
582}
583
584pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
585    let resolved = resolve_path(path.as_ref());
586    let provider = current_provider();
587    provider.remove_dir(&resolved).await
588}
589
590pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
591    let resolved = resolve_path(path.as_ref());
592    let provider = current_provider();
593    provider.remove_dir_all(&resolved).await
594}
595
596pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
597    let resolved_from = resolve_path(from.as_ref());
598    let resolved_to = resolve_path(to.as_ref());
599    let provider = current_provider();
600    provider.rename(&resolved_from, &resolved_to).await
601}
602
603pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
604    let resolved = resolve_path(path.as_ref());
605    let provider = current_provider();
606    provider.set_readonly(&resolved, readonly).await
607}
608
609pub async fn data_manifest_descriptor_async(
610    request: &DataManifestRequest,
611) -> io::Result<DataManifestDescriptor> {
612    let provider = current_provider();
613    provider.data_manifest_descriptor(request).await
614}
615
616pub async fn data_chunk_upload_targets_async(
617    request: &DataChunkUploadRequest,
618) -> io::Result<Vec<DataChunkUploadTarget>> {
619    let provider = current_provider();
620    provider.data_chunk_upload_targets(request).await
621}
622
623pub async fn data_upload_chunk_async(
624    target: &DataChunkUploadTarget,
625    data: &[u8],
626) -> io::Result<()> {
627    let provider = current_provider();
628    provider.data_upload_chunk(target, data).await
629}
630
631/// Copy a file from `from` to `to`, truncating the destination when it exists.
632/// Returns the number of bytes written, matching `std::fs::copy`.
633pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
634    let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
635    let mut writer = OpenOptions::new()
636        .write(true)
637        .create(true)
638        .truncate(true)
639        .open(to.as_ref())?;
640    io::copy(&mut reader, &mut writer)
641}
642
643fn default_provider() -> Arc<dyn FsProvider> {
644    #[cfg(not(target_arch = "wasm32"))]
645    {
646        Arc::new(NativeFsProvider)
647    }
648    #[cfg(target_arch = "wasm32")]
649    {
650        Arc::new(PlaceholderFsProvider)
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use once_cell::sync::Lazy;
658    use std::io::{Read, Write};
659    use std::sync::Mutex;
660    use tempfile::tempdir;
661
662    static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
663
664    struct UnsupportedProvider;
665
666    #[async_trait(?Send)]
667    impl FsProvider for UnsupportedProvider {
668        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
669            Err(unsupported())
670        }
671
672        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
673            Err(unsupported())
674        }
675
676        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
677            Err(unsupported())
678        }
679
680        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
681            Err(unsupported())
682        }
683
684        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
685            Err(unsupported())
686        }
687
688        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
689            Err(unsupported())
690        }
691
692        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
693            Err(unsupported())
694        }
695
696        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
697            Err(unsupported())
698        }
699
700        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
701            Err(unsupported())
702        }
703
704        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
705            Err(unsupported())
706        }
707
708        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
709            Err(unsupported())
710        }
711
712        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
713            Err(unsupported())
714        }
715
716        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
717            Err(unsupported())
718        }
719
720        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
721            Err(unsupported())
722        }
723
724        async fn data_manifest_descriptor(
725            &self,
726            _request: &DataManifestRequest,
727        ) -> io::Result<DataManifestDescriptor> {
728            Err(unsupported())
729        }
730
731        async fn data_chunk_upload_targets(
732            &self,
733            _request: &DataChunkUploadRequest,
734        ) -> io::Result<Vec<DataChunkUploadTarget>> {
735            Err(unsupported())
736        }
737
738        async fn data_upload_chunk(
739            &self,
740            _target: &DataChunkUploadTarget,
741            _data: &[u8],
742        ) -> io::Result<()> {
743            Err(unsupported())
744        }
745    }
746
747    fn unsupported() -> io::Error {
748        io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
749    }
750
751    #[test]
752    fn copy_file_round_trip() {
753        let _guard = TEST_LOCK.lock().unwrap();
754        let dir = tempdir().expect("tempdir");
755        let src = dir.path().join("src.bin");
756        let dst = dir.path().join("dst.bin");
757        {
758            let mut file = std::fs::File::create(&src).expect("create src");
759            file.write_all(b"hello filesystem").expect("write src");
760        }
761
762        copy_file(&src, &dst).expect("copy");
763        let mut dst_file = File::open(&dst).expect("open dst");
764        let mut contents = Vec::new();
765        dst_file
766            .read_to_end(&mut contents)
767            .expect("read destination");
768        assert_eq!(contents, b"hello filesystem");
769    }
770
771    #[test]
772    fn set_readonly_flips_metadata_flag() {
773        let _guard = TEST_LOCK.lock().unwrap();
774        let dir = tempdir().expect("tempdir");
775        let path = dir.path().join("flag.txt");
776        futures::executor::block_on(write_async(&path, b"flag")).expect("write");
777
778        futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
779        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
780        assert!(meta.is_readonly());
781
782        futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
783        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
784        assert!(!meta.is_readonly());
785    }
786
787    #[test]
788    fn replace_provider_restores_previous() {
789        let _guard = TEST_LOCK.lock().unwrap();
790        let original = current_provider();
791        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
792        {
793            let _guard = replace_provider(custom.clone());
794            let active = current_provider();
795            assert!(Arc::ptr_eq(&active, &custom));
796        }
797        let final_provider = current_provider();
798        assert!(Arc::ptr_eq(&final_provider, &original));
799    }
800
801    #[test]
802    fn with_provider_restores_even_on_panic() {
803        let _guard = TEST_LOCK.lock().unwrap();
804        let original = current_provider();
805        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
806        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
807            with_provider_override(custom.clone(), || {
808                let active = current_provider();
809                assert!(Arc::ptr_eq(&active, &custom));
810                panic!("boom");
811            })
812        }));
813        assert!(result.is_err());
814        let final_provider = current_provider();
815        assert!(Arc::ptr_eq(&final_provider, &original));
816    }
817}