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
434pub 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
446pub 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
455pub 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
631pub 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}