Skip to main content

runmat_filesystem/
lib.rs

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