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 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
35#[async_trait(?Send)]
36pub trait FileHandle: Read + Write + Seek + Send + Sync {
37    async fn flush_async(&mut self) -> io::Result<()> {
38        self.flush()
39    }
40
41    async fn sync_all_async(&mut self) -> io::Result<()> {
42        self.flush_async().await
43    }
44}
45
46#[async_trait(?Send)]
47impl FileHandle for std::fs::File {
48    async fn sync_all_async(&mut self) -> io::Result<()> {
49        std::fs::File::sync_all(self)
50    }
51}
52
53#[derive(Clone, Debug, Default)]
54pub struct OpenFlags {
55    pub read: bool,
56    pub write: bool,
57    pub append: bool,
58    pub truncate: bool,
59    pub create: bool,
60    pub create_new: bool,
61}
62
63#[derive(Clone, Debug)]
64pub struct OpenOptions {
65    flags: OpenFlags,
66}
67
68impl OpenOptions {
69    pub fn new() -> Self {
70        Self {
71            flags: OpenFlags::default(),
72        }
73    }
74
75    pub fn read(&mut self, value: bool) -> &mut Self {
76        self.flags.read = value;
77        self
78    }
79
80    pub fn write(&mut self, value: bool) -> &mut Self {
81        self.flags.write = value;
82        self
83    }
84
85    pub fn append(&mut self, value: bool) -> &mut Self {
86        self.flags.append = value;
87        self
88    }
89
90    pub fn truncate(&mut self, value: bool) -> &mut Self {
91        self.flags.truncate = value;
92        self
93    }
94
95    pub fn create(&mut self, value: bool) -> &mut Self {
96        self.flags.create = value;
97        self
98    }
99
100    pub fn create_new(&mut self, value: bool) -> &mut Self {
101        self.flags.create_new = value;
102        self
103    }
104
105    pub fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
106        let resolved = resolve_path(path.as_ref());
107        with_provider(|provider| provider.open(&resolved, &self.flags)).map(File::from_handle)
108    }
109
110    pub async fn open_async(&self, path: impl AsRef<Path>) -> io::Result<File> {
111        let resolved = resolve_path(path.as_ref());
112        let provider = current_provider();
113        provider
114            .open_async(&resolved, &self.flags)
115            .await
116            .map(File::from_handle)
117    }
118
119    pub fn flags(&self) -> &OpenFlags {
120        &self.flags
121    }
122}
123
124impl Default for OpenOptions {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum FsFileType {
132    Directory,
133    File,
134    Symlink,
135    Other,
136    Unknown,
137}
138
139#[derive(Clone, Debug)]
140pub struct FsMetadata {
141    file_type: FsFileType,
142    len: u64,
143    modified: Option<SystemTime>,
144    readonly: bool,
145    hash: Option<String>,
146}
147
148impl FsMetadata {
149    pub fn new(
150        file_type: FsFileType,
151        len: u64,
152        modified: Option<SystemTime>,
153        readonly: bool,
154    ) -> Self {
155        Self {
156            file_type,
157            len,
158            modified,
159            readonly,
160            hash: None,
161        }
162    }
163
164    pub fn new_with_hash(
165        file_type: FsFileType,
166        len: u64,
167        modified: Option<SystemTime>,
168        readonly: bool,
169        hash: Option<String>,
170    ) -> Self {
171        Self {
172            file_type,
173            len,
174            modified,
175            readonly,
176            hash,
177        }
178    }
179
180    pub fn file_type(&self) -> FsFileType {
181        self.file_type
182    }
183
184    pub fn is_dir(&self) -> bool {
185        matches!(self.file_type, FsFileType::Directory)
186    }
187
188    pub fn is_file(&self) -> bool {
189        matches!(self.file_type, FsFileType::File)
190    }
191
192    pub fn is_symlink(&self) -> bool {
193        matches!(self.file_type, FsFileType::Symlink)
194    }
195
196    pub fn len(&self) -> u64 {
197        self.len
198    }
199
200    pub fn hash(&self) -> Option<&str> {
201        self.hash.as_deref()
202    }
203
204    pub fn is_empty(&self) -> bool {
205        self.len == 0
206    }
207
208    pub fn modified(&self) -> Option<SystemTime> {
209        self.modified
210    }
211
212    pub fn is_readonly(&self) -> bool {
213        self.readonly
214    }
215}
216
217#[derive(Clone, Debug)]
218pub struct DirEntry {
219    path: PathBuf,
220    file_name: OsString,
221    file_type: FsFileType,
222}
223
224#[derive(Clone, Debug)]
225pub struct ReadManyEntry {
226    path: PathBuf,
227    bytes: Option<Vec<u8>>,
228    error: Option<String>,
229}
230
231impl ReadManyEntry {
232    pub fn new(path: PathBuf, bytes: Option<Vec<u8>>) -> Self {
233        Self {
234            path,
235            bytes,
236            error: None,
237        }
238    }
239
240    pub fn with_error(path: PathBuf, error: String) -> Self {
241        Self {
242            path,
243            bytes: None,
244            error: Some(error),
245        }
246    }
247
248    pub fn path(&self) -> &Path {
249        &self.path
250    }
251
252    pub fn bytes(&self) -> Option<&[u8]> {
253        self.bytes.as_deref()
254    }
255
256    pub fn into_bytes(self) -> Option<Vec<u8>> {
257        self.bytes
258    }
259
260    pub fn error(&self) -> Option<&str> {
261        self.error.as_deref()
262    }
263}
264
265#[derive(Clone, Debug, PartialEq, Eq)]
266pub struct OpenFileDialogFilter {
267    pub patterns: Vec<String>,
268    pub description: Option<String>,
269}
270
271#[derive(Clone, Debug, Default, PartialEq, Eq)]
272pub struct OpenFileDialogRequest {
273    pub title: Option<String>,
274    pub default_path: Option<PathBuf>,
275    pub filters: Vec<OpenFileDialogFilter>,
276    pub multiselect: bool,
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
280pub struct OpenFileDialogSelection {
281    pub paths: Vec<PathBuf>,
282    pub filter_index: Option<usize>,
283}
284
285impl DirEntry {
286    pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
287        Self {
288            path,
289            file_name,
290            file_type,
291        }
292    }
293
294    pub fn path(&self) -> &Path {
295        &self.path
296    }
297
298    pub fn file_name(&self) -> &OsString {
299        &self.file_name
300    }
301
302    pub fn file_type(&self) -> FsFileType {
303        self.file_type
304    }
305
306    pub fn is_dir(&self) -> bool {
307        matches!(self.file_type, FsFileType::Directory)
308    }
309}
310
311#[async_trait(?Send)]
312pub trait FsProvider: Send + Sync + 'static {
313    fn current_dir_override(&self) -> Option<PathBuf> {
314        None
315    }
316
317    fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
318    async fn open_async(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
319        self.open(path, flags)
320    }
321    async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
322    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
323    async fn remove_file(&self, path: &Path) -> io::Result<()>;
324    async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
325    async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
326    async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
327    async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
328    async fn create_dir(&self, path: &Path) -> io::Result<()>;
329    async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
330    async fn remove_dir(&self, path: &Path) -> io::Result<()>;
331    async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
332    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
333    async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
334
335    async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
336        let mut entries = Vec::with_capacity(paths.len());
337        for path in paths {
338            let entry = match self.read(path).await {
339                Ok(payload) => ReadManyEntry::new(path.clone(), Some(payload)),
340                Err(error) => {
341                    warn!(
342                        "fs.read_many.miss path={} kind={:?} error={}",
343                        path.to_string_lossy(),
344                        error.kind(),
345                        error
346                    );
347                    ReadManyEntry::with_error(
348                        path.clone(),
349                        format!("kind={:?}; error={}", error.kind(), error),
350                    )
351                }
352            };
353            entries.push(entry);
354        }
355        Ok(entries)
356    }
357
358    async fn data_manifest_descriptor(
359        &self,
360        _request: &DataManifestRequest,
361    ) -> io::Result<DataManifestDescriptor> {
362        Err(io::Error::new(
363            ErrorKind::Unsupported,
364            "data manifest descriptor is unsupported by this provider",
365        ))
366    }
367
368    async fn data_chunk_upload_targets(
369        &self,
370        _request: &DataChunkUploadRequest,
371    ) -> io::Result<Vec<DataChunkUploadTarget>> {
372        Err(io::Error::new(
373            ErrorKind::Unsupported,
374            "data chunk upload targets are unsupported by this provider",
375        ))
376    }
377
378    async fn data_upload_chunk(
379        &self,
380        _target: &DataChunkUploadTarget,
381        _data: &[u8],
382    ) -> io::Result<()> {
383        Err(io::Error::new(
384            ErrorKind::Unsupported,
385            "data chunk upload is unsupported by this provider",
386        ))
387    }
388
389    async fn select_file_open(
390        &self,
391        _request: &OpenFileDialogRequest,
392    ) -> io::Result<Option<OpenFileDialogSelection>> {
393        Ok(None)
394    }
395}
396
397pub struct File {
398    inner: Box<dyn FileHandle>,
399}
400
401impl File {
402    fn from_handle(handle: Box<dyn FileHandle>) -> Self {
403        Self { inner: handle }
404    }
405
406    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
407        let mut opts = OpenOptions::new();
408        opts.read(true);
409        opts.open(path)
410    }
411
412    pub async fn open_async(path: impl AsRef<Path>) -> io::Result<Self> {
413        let mut opts = OpenOptions::new();
414        opts.read(true);
415        opts.open_async(path).await
416    }
417
418    pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
419        let mut opts = OpenOptions::new();
420        opts.write(true).create(true).truncate(true);
421        opts.open(path)
422    }
423
424    pub async fn create_async(path: impl AsRef<Path>) -> io::Result<Self> {
425        let mut opts = OpenOptions::new();
426        opts.write(true).create(true).truncate(true);
427        opts.open_async(path).await
428    }
429
430    pub async fn flush_async(&mut self) -> io::Result<()> {
431        self.inner.flush_async().await
432    }
433
434    pub async fn sync_all_async(&mut self) -> io::Result<()> {
435        self.inner.sync_all_async().await
436    }
437}
438
439impl fmt::Debug for File {
440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441        f.debug_struct("File").finish_non_exhaustive()
442    }
443}
444
445impl Read for File {
446    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
447        self.inner.read(buf)
448    }
449}
450
451impl Write for File {
452    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
453        self.inner.write(buf)
454    }
455
456    fn flush(&mut self) -> io::Result<()> {
457        self.inner.flush()
458    }
459}
460
461impl Seek for File {
462    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
463        self.inner.seek(pos)
464    }
465}
466
467struct ProviderState {
468    provider: Arc<dyn FsProvider>,
469    current_dir_override: Option<PathBuf>,
470}
471
472static PROVIDER_STATE: OnceCell<RwLock<ProviderState>> = OnceCell::new();
473static PROVIDER_OVERRIDE_LOCK: OnceCell<Mutex<()>> = OnceCell::new();
474
475fn provider_state_lock() -> &'static RwLock<ProviderState> {
476    PROVIDER_STATE.get_or_init(|| {
477        #[cfg(target_arch = "wasm32")]
478        let current_dir_override = Some(PathBuf::from("/"));
479        #[cfg(not(target_arch = "wasm32"))]
480        let current_dir_override = None;
481
482        RwLock::new(ProviderState {
483            provider: default_provider(),
484            current_dir_override,
485        })
486    })
487}
488
489/// Serializes tests and embedders that temporarily replace the process-wide
490/// filesystem provider.
491pub fn provider_override_lock() -> MutexGuard<'static, ()> {
492    PROVIDER_OVERRIDE_LOCK
493        .get_or_init(|| Mutex::new(()))
494        .lock()
495        .unwrap_or_else(|poisoned| poisoned.into_inner())
496}
497
498fn current_dir_override() -> Option<PathBuf> {
499    provider_state_lock()
500        .read()
501        .expect("filesystem provider lock poisoned")
502        .current_dir_override
503        .clone()
504}
505
506fn replace_current_dir_override(value: Option<PathBuf>) -> Option<PathBuf> {
507    let mut guard = provider_state_lock()
508        .write()
509        .expect("filesystem provider lock poisoned");
510    std::mem::replace(&mut guard.current_dir_override, value)
511}
512
513fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
514    let guard = provider_state_lock()
515        .read()
516        .expect("filesystem provider lock poisoned");
517    f(&*guard.provider)
518}
519
520fn resolve_path(path: &Path) -> PathBuf {
521    if path.is_absolute() {
522        return path.to_path_buf();
523    }
524    let state = provider_state_lock()
525        .read()
526        .expect("filesystem provider lock poisoned");
527    if let Some(base) = &state.current_dir_override {
528        if path.has_root() {
529            return path.to_path_buf();
530        }
531        return base.join(path);
532    }
533    path.to_path_buf()
534}
535
536fn next_current_dir_override(
537    current: Option<&PathBuf>,
538    provider_default: Option<PathBuf>,
539) -> Option<PathBuf> {
540    match provider_default {
541        Some(default) => current.cloned().or(Some(default)),
542        None => None,
543    }
544}
545
546pub fn set_provider(provider: Arc<dyn FsProvider>) {
547    let provider_default_current_dir = provider.current_dir_override();
548    let mut guard = provider_state_lock()
549        .write()
550        .expect("filesystem provider lock poisoned");
551    let current_dir_override = next_current_dir_override(
552        guard.current_dir_override.as_ref(),
553        provider_default_current_dir,
554    );
555    guard.provider = provider;
556    guard.current_dir_override = current_dir_override;
557}
558
559/// Temporarily replace the active provider and return a guard that restores the
560/// previous provider when dropped. Useful for tests that need to install a mock
561/// filesystem without permanently mutating global state.
562pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
563    let provider_default_current_dir = provider.current_dir_override();
564    let mut guard = provider_state_lock()
565        .write()
566        .expect("filesystem provider lock poisoned");
567    let previous = guard.provider.clone();
568    let previous_current_dir = guard.current_dir_override.clone();
569    let current_dir_override = next_current_dir_override(
570        guard.current_dir_override.as_ref(),
571        provider_default_current_dir,
572    );
573    guard.provider = provider;
574    guard.current_dir_override = current_dir_override;
575    ProviderGuard {
576        previous,
577        previous_current_dir,
578    }
579}
580
581/// Run a closure with the supplied provider installed, restoring the previous
582/// provider automatically afterwards.
583pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
584    let guard = replace_provider(provider);
585    let result = f();
586    drop(guard);
587    result
588}
589
590/// Returns the currently installed provider.
591pub fn current_provider() -> Arc<dyn FsProvider> {
592    provider_state_lock()
593        .read()
594        .expect("filesystem provider lock poisoned")
595        .provider
596        .clone()
597}
598
599pub fn current_dir() -> io::Result<PathBuf> {
600    if let Some(current) = current_dir_override() {
601        return Ok(current);
602    }
603    #[cfg(not(target_arch = "wasm32"))]
604    {
605        std::env::current_dir()
606    }
607    #[cfg(target_arch = "wasm32")]
608    {
609        Ok(PathBuf::from("/"))
610    }
611}
612
613pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
614    if current_dir_override().is_some() {
615        futures::executor::block_on(set_current_dir_async(path.as_ref().to_path_buf()))
616    } else {
617        #[cfg(not(target_arch = "wasm32"))]
618        {
619            std::env::set_current_dir(path)
620        }
621        #[cfg(target_arch = "wasm32")]
622        {
623            Ok(())
624        }
625    }
626}
627
628pub async fn set_current_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
629    if current_dir_override().is_some() {
630        let mut target = PathBuf::from(path.as_ref());
631        if !target.has_root() {
632            let base = current_dir()?;
633            target = base.join(target);
634        }
635        let canonical = canonicalize_async(&target).await.unwrap_or(target.clone());
636        let metadata = metadata_async(&canonical).await?;
637        if !metadata.is_dir() {
638            return Err(io::Error::new(
639                ErrorKind::NotFound,
640                format!("Not a directory: {}", canonical.display()),
641            ));
642        }
643        replace_current_dir_override(Some(canonical));
644        Ok(())
645    } else {
646        set_current_dir(path)
647    }
648}
649
650pub struct ProviderGuard {
651    previous: Arc<dyn FsProvider>,
652    previous_current_dir: Option<PathBuf>,
653}
654
655impl Drop for ProviderGuard {
656    fn drop(&mut self) {
657        let mut guard = provider_state_lock()
658            .write()
659            .expect("filesystem provider lock poisoned");
660        guard.provider = self.previous.clone();
661        guard.current_dir_override = self.previous_current_dir.clone();
662    }
663}
664
665pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
666    let resolved = paths
667        .iter()
668        .map(|path| resolve_path(path.as_path()))
669        .collect::<Vec<_>>();
670    let provider = current_provider();
671    provider.read_many(&resolved).await
672}
673
674pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
675    let resolved = resolve_path(path.as_ref());
676    let provider = current_provider();
677    provider.read(&resolved).await
678}
679
680pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
681    let bytes = read_async(path).await?;
682    String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
683}
684
685pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
686    let resolved = resolve_path(path.as_ref());
687    let provider = current_provider();
688    provider.write(&resolved, data.as_ref()).await
689}
690
691pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
692    let resolved = resolve_path(path.as_ref());
693    let provider = current_provider();
694    provider.remove_file(&resolved).await
695}
696
697pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
698    let resolved = resolve_path(path.as_ref());
699    let provider = current_provider();
700    provider.metadata(&resolved).await
701}
702
703pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
704    let resolved = resolve_path(path.as_ref());
705    let provider = current_provider();
706    provider.symlink_metadata(&resolved).await
707}
708
709pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
710    let resolved = resolve_path(path.as_ref());
711    let provider = current_provider();
712    provider.read_dir(&resolved).await
713}
714
715pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
716    let resolved = resolve_path(path.as_ref());
717    let provider = current_provider();
718    provider.canonicalize(&resolved).await
719}
720
721pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
722    let resolved = resolve_path(path.as_ref());
723    let provider = current_provider();
724    provider.create_dir(&resolved).await
725}
726
727pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
728    let resolved = resolve_path(path.as_ref());
729    let provider = current_provider();
730    provider.create_dir_all(&resolved).await
731}
732
733pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
734    let resolved = resolve_path(path.as_ref());
735    let provider = current_provider();
736    provider.remove_dir(&resolved).await
737}
738
739pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
740    let resolved = resolve_path(path.as_ref());
741    let provider = current_provider();
742    provider.remove_dir_all(&resolved).await
743}
744
745pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
746    let resolved_from = resolve_path(from.as_ref());
747    let resolved_to = resolve_path(to.as_ref());
748    let provider = current_provider();
749    provider.rename(&resolved_from, &resolved_to).await
750}
751
752pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
753    let resolved = resolve_path(path.as_ref());
754    let provider = current_provider();
755    provider.set_readonly(&resolved, readonly).await
756}
757
758pub async fn select_file_open_async(
759    request: &OpenFileDialogRequest,
760) -> io::Result<Option<OpenFileDialogSelection>> {
761    let mut resolved = request.clone();
762    if let Some(default_path) = resolved.default_path.as_mut() {
763        *default_path = resolve_path(default_path);
764    }
765    let provider = current_provider();
766    provider.select_file_open(&resolved).await
767}
768
769pub async fn data_manifest_descriptor_async(
770    request: &DataManifestRequest,
771) -> io::Result<DataManifestDescriptor> {
772    let provider = current_provider();
773    provider.data_manifest_descriptor(request).await
774}
775
776pub async fn data_chunk_upload_targets_async(
777    request: &DataChunkUploadRequest,
778) -> io::Result<Vec<DataChunkUploadTarget>> {
779    let provider = current_provider();
780    provider.data_chunk_upload_targets(request).await
781}
782
783pub async fn data_upload_chunk_async(
784    target: &DataChunkUploadTarget,
785    data: &[u8],
786) -> io::Result<()> {
787    let provider = current_provider();
788    provider.data_upload_chunk(target, data).await
789}
790
791/// Copy a file from `from` to `to`, truncating the destination when it exists.
792/// Returns the number of bytes written, matching `std::fs::copy`.
793pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
794    let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
795    let mut writer = OpenOptions::new()
796        .write(true)
797        .create(true)
798        .truncate(true)
799        .open(to.as_ref())?;
800    io::copy(&mut reader, &mut writer)
801}
802
803fn default_provider() -> Arc<dyn FsProvider> {
804    #[cfg(not(target_arch = "wasm32"))]
805    {
806        Arc::new(NativeFsProvider)
807    }
808    #[cfg(target_arch = "wasm32")]
809    {
810        Arc::new(PlaceholderFsProvider)
811    }
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817    use once_cell::sync::Lazy;
818    use std::collections::{HashMap, HashSet};
819    use std::io::{Read, Seek, SeekFrom, Write};
820    use std::sync::Mutex;
821    use tempfile::tempdir;
822
823    static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
824
825    fn test_lock() -> std::sync::MutexGuard<'static, ()> {
826        TEST_LOCK
827            .lock()
828            .unwrap_or_else(|poisoned| poisoned.into_inner())
829    }
830
831    fn comparable_path(path: impl AsRef<Path>) -> PathBuf {
832        #[cfg(windows)]
833        {
834            let text = path.as_ref().to_string_lossy();
835            if let Some(stripped) = text.strip_prefix(r"\\?\UNC\") {
836                return PathBuf::from(format!(r"\\{stripped}"));
837            }
838            if let Some(stripped) = text.strip_prefix(r"\\?\") {
839                return PathBuf::from(stripped);
840            }
841            PathBuf::from(text.as_ref())
842        }
843        #[cfg(not(windows))]
844        {
845            path.as_ref().to_path_buf()
846        }
847    }
848
849    fn assert_same_path(left: impl AsRef<Path>, right: impl AsRef<Path>) {
850        assert_eq!(comparable_path(left), comparable_path(right));
851    }
852
853    struct UnsupportedProvider;
854
855    struct AsyncOpenProvider {
856        opened_async: Arc<Mutex<bool>>,
857        flushed_async: Arc<Mutex<bool>>,
858    }
859
860    struct TestProviderStateGuard {
861        previous_provider: Arc<dyn FsProvider>,
862        previous_current_dir: Option<PathBuf>,
863    }
864
865    struct ProcessCwdGuard {
866        previous: PathBuf,
867    }
868
869    struct VirtualFsProvider {
870        default_current_dir: PathBuf,
871        dirs: Mutex<HashSet<PathBuf>>,
872        files: Mutex<HashMap<PathBuf, Vec<u8>>>,
873    }
874
875    impl Drop for ProcessCwdGuard {
876        fn drop(&mut self) {
877            let _ = std::env::set_current_dir(&self.previous);
878        }
879    }
880
881    impl TestProviderStateGuard {
882        fn capture() -> Self {
883            let guard = provider_state_lock()
884                .read()
885                .expect("filesystem provider lock poisoned");
886            Self {
887                previous_provider: guard.provider.clone(),
888                previous_current_dir: guard.current_dir_override.clone(),
889            }
890        }
891    }
892
893    impl Drop for TestProviderStateGuard {
894        fn drop(&mut self) {
895            let mut guard = provider_state_lock()
896                .write()
897                .expect("filesystem provider lock poisoned");
898            guard.provider = self.previous_provider.clone();
899            guard.current_dir_override = self.previous_current_dir.clone();
900        }
901    }
902
903    impl VirtualFsProvider {
904        fn new(default_current_dir: impl Into<PathBuf>, dirs: &[&str]) -> Self {
905            let default_current_dir = default_current_dir.into();
906            let mut all_dirs = HashSet::from([default_current_dir.clone()]);
907            for dir in dirs {
908                all_dirs.insert(PathBuf::from(dir));
909            }
910            Self {
911                default_current_dir,
912                dirs: Mutex::new(all_dirs),
913                files: Mutex::new(HashMap::new()),
914            }
915        }
916
917        fn file_bytes(&self, path: impl AsRef<Path>) -> Option<Vec<u8>> {
918            self.files.lock().unwrap().get(path.as_ref()).cloned()
919        }
920    }
921
922    struct AsyncTestHandle {
923        cursor: usize,
924        data: Vec<u8>,
925        flushed_async: Arc<Mutex<bool>>,
926    }
927
928    impl Read for AsyncTestHandle {
929        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
930            let remaining = self.data.len().saturating_sub(self.cursor);
931            let to_read = remaining.min(buf.len());
932            buf[..to_read].copy_from_slice(&self.data[self.cursor..self.cursor + to_read]);
933            self.cursor += to_read;
934            Ok(to_read)
935        }
936    }
937
938    impl Write for AsyncTestHandle {
939        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
940            let end = self.cursor + buf.len();
941            if end > self.data.len() {
942                self.data.resize(end, 0);
943            }
944            self.data[self.cursor..end].copy_from_slice(buf);
945            self.cursor = end;
946            Ok(buf.len())
947        }
948
949        fn flush(&mut self) -> io::Result<()> {
950            Ok(())
951        }
952    }
953
954    impl Seek for AsyncTestHandle {
955        fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
956            let next = match pos {
957                SeekFrom::Start(offset) => offset as i64,
958                SeekFrom::End(offset) => self.data.len() as i64 + offset,
959                SeekFrom::Current(offset) => self.cursor as i64 + offset,
960            };
961            if next < 0 {
962                return Err(io::Error::new(ErrorKind::InvalidInput, "seek before start"));
963            }
964            self.cursor = next as usize;
965            Ok(self.cursor as u64)
966        }
967    }
968
969    #[async_trait(?Send)]
970    impl FileHandle for AsyncTestHandle {
971        async fn flush_async(&mut self) -> io::Result<()> {
972            *self.flushed_async.lock().unwrap() = true;
973            Ok(())
974        }
975    }
976
977    #[async_trait(?Send)]
978    impl FsProvider for UnsupportedProvider {
979        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
980            Err(unsupported())
981        }
982
983        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
984            Err(unsupported())
985        }
986
987        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
988            Err(unsupported())
989        }
990
991        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
992            Err(unsupported())
993        }
994
995        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
996            Err(unsupported())
997        }
998
999        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1000            Err(unsupported())
1001        }
1002
1003        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1004            Err(unsupported())
1005        }
1006
1007        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1008            Err(unsupported())
1009        }
1010
1011        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
1012            Err(unsupported())
1013        }
1014
1015        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1016            Err(unsupported())
1017        }
1018
1019        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1020            Err(unsupported())
1021        }
1022
1023        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
1024            Err(unsupported())
1025        }
1026
1027        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1028            Err(unsupported())
1029        }
1030
1031        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1032            Err(unsupported())
1033        }
1034
1035        async fn data_manifest_descriptor(
1036            &self,
1037            _request: &DataManifestRequest,
1038        ) -> io::Result<DataManifestDescriptor> {
1039            Err(unsupported())
1040        }
1041
1042        async fn data_chunk_upload_targets(
1043            &self,
1044            _request: &DataChunkUploadRequest,
1045        ) -> io::Result<Vec<DataChunkUploadTarget>> {
1046            Err(unsupported())
1047        }
1048
1049        async fn data_upload_chunk(
1050            &self,
1051            _target: &DataChunkUploadTarget,
1052            _data: &[u8],
1053        ) -> io::Result<()> {
1054            Err(unsupported())
1055        }
1056    }
1057
1058    #[async_trait(?Send)]
1059    impl FsProvider for VirtualFsProvider {
1060        fn current_dir_override(&self) -> Option<PathBuf> {
1061            Some(self.default_current_dir.clone())
1062        }
1063
1064        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1065            Err(unsupported())
1066        }
1067
1068        async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
1069            self.files
1070                .lock()
1071                .unwrap()
1072                .get(path)
1073                .cloned()
1074                .ok_or_else(|| io::Error::new(ErrorKind::NotFound, path.display().to_string()))
1075        }
1076
1077        async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1078            self.files
1079                .lock()
1080                .unwrap()
1081                .insert(path.to_path_buf(), data.to_vec());
1082            Ok(())
1083        }
1084
1085        async fn remove_file(&self, path: &Path) -> io::Result<()> {
1086            self.files.lock().unwrap().remove(path);
1087            Ok(())
1088        }
1089
1090        async fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
1091            if self.dirs.lock().unwrap().contains(path) {
1092                return Ok(FsMetadata::new(FsFileType::Directory, 0, None, false));
1093            }
1094            if let Some(bytes) = self.files.lock().unwrap().get(path) {
1095                return Ok(FsMetadata::new(
1096                    FsFileType::File,
1097                    bytes.len() as u64,
1098                    None,
1099                    false,
1100                ));
1101            }
1102            Err(io::Error::new(
1103                ErrorKind::NotFound,
1104                path.display().to_string(),
1105            ))
1106        }
1107
1108        async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata> {
1109            self.metadata(path).await
1110        }
1111
1112        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1113            Ok(Vec::new())
1114        }
1115
1116        async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
1117            Ok(path.to_path_buf())
1118        }
1119
1120        async fn create_dir(&self, path: &Path) -> io::Result<()> {
1121            self.dirs.lock().unwrap().insert(path.to_path_buf());
1122            Ok(())
1123        }
1124
1125        async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
1126            let mut dirs = self.dirs.lock().unwrap();
1127            for ancestor in path.ancestors() {
1128                dirs.insert(ancestor.to_path_buf());
1129            }
1130            Ok(())
1131        }
1132
1133        async fn remove_dir(&self, path: &Path) -> io::Result<()> {
1134            self.dirs.lock().unwrap().remove(path);
1135            Ok(())
1136        }
1137
1138        async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
1139            self.dirs
1140                .lock()
1141                .unwrap()
1142                .retain(|dir| !dir.starts_with(path));
1143            Ok(())
1144        }
1145
1146        async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1147            let mut files = self.files.lock().unwrap();
1148            let data = files
1149                .remove(from)
1150                .ok_or_else(|| io::Error::new(ErrorKind::NotFound, from.display().to_string()))?;
1151            files.insert(to.to_path_buf(), data);
1152            Ok(())
1153        }
1154
1155        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1156            Ok(())
1157        }
1158    }
1159
1160    #[async_trait(?Send)]
1161    impl FsProvider for AsyncOpenProvider {
1162        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
1163            Err(unsupported())
1164        }
1165
1166        async fn open_async(
1167            &self,
1168            _path: &Path,
1169            _flags: &OpenFlags,
1170        ) -> io::Result<Box<dyn FileHandle>> {
1171            *self.opened_async.lock().unwrap() = true;
1172            Ok(Box::new(AsyncTestHandle {
1173                cursor: 0,
1174                data: b"async contents".to_vec(),
1175                flushed_async: self.flushed_async.clone(),
1176            }))
1177        }
1178
1179        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
1180            Err(unsupported())
1181        }
1182
1183        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
1184            Err(unsupported())
1185        }
1186
1187        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
1188            Err(unsupported())
1189        }
1190
1191        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1192            Err(unsupported())
1193        }
1194
1195        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
1196            Err(unsupported())
1197        }
1198
1199        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
1200            Err(unsupported())
1201        }
1202
1203        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1204            Err(unsupported())
1205        }
1206
1207        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
1208            Err(unsupported())
1209        }
1210
1211        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1212            Err(unsupported())
1213        }
1214
1215        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
1216            Err(unsupported())
1217        }
1218
1219        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
1220            Err(unsupported())
1221        }
1222
1223        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
1224            Err(unsupported())
1225        }
1226
1227        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
1228            Err(unsupported())
1229        }
1230    }
1231
1232    fn unsupported() -> io::Error {
1233        io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
1234    }
1235
1236    #[test]
1237    fn copy_file_round_trip() {
1238        let _guard = test_lock();
1239        let dir = tempdir().expect("tempdir");
1240        let src = dir.path().join("src.bin");
1241        let dst = dir.path().join("dst.bin");
1242        {
1243            let mut file = std::fs::File::create(&src).expect("create src");
1244            file.write_all(b"hello filesystem").expect("write src");
1245        }
1246
1247        copy_file(&src, &dst).expect("copy");
1248        let mut dst_file = File::open(&dst).expect("open dst");
1249        let mut contents = Vec::new();
1250        dst_file
1251            .read_to_end(&mut contents)
1252            .expect("read destination");
1253        assert_eq!(contents, b"hello filesystem");
1254    }
1255
1256    #[test]
1257    fn set_readonly_flips_metadata_flag() {
1258        let _guard = test_lock();
1259        let dir = tempdir().expect("tempdir");
1260        let path = dir.path().join("flag.txt");
1261        futures::executor::block_on(write_async(&path, b"flag")).expect("write");
1262
1263        futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
1264        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
1265        assert!(meta.is_readonly());
1266
1267        futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
1268        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
1269        assert!(!meta.is_readonly());
1270    }
1271
1272    #[test]
1273    fn replace_provider_restores_previous() {
1274        let _guard = test_lock();
1275        let original = current_provider();
1276        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1277        {
1278            let _guard = replace_provider(custom.clone());
1279            let active = current_provider();
1280            assert!(Arc::ptr_eq(&active, &custom));
1281        }
1282        let final_provider = current_provider();
1283        assert!(Arc::ptr_eq(&final_provider, &original));
1284    }
1285
1286    #[test]
1287    #[cfg(not(target_arch = "wasm32"))]
1288    fn native_provider_replacement_preserves_process_cwd_resolution() {
1289        let _guard = test_lock();
1290        let temp = tempdir().expect("tempdir");
1291        let previous = std::env::current_dir().expect("current dir");
1292        let _cwd_guard = ProcessCwdGuard { previous };
1293        std::env::set_current_dir(temp.path()).expect("set temp cwd");
1294
1295        let _provider_guard = replace_provider(Arc::new(NativeFsProvider));
1296        let current = current_dir().expect("vfs current dir");
1297        let expected = std::fs::canonicalize(temp.path()).expect("canonical temp");
1298        assert_same_path(&current, &expected);
1299
1300        futures::executor::block_on(write_async("native-relative.txt", b"native"))
1301            .expect("write relative path");
1302        assert_eq!(
1303            std::fs::read_to_string(temp.path().join("native-relative.txt")).expect("read file"),
1304            "native"
1305        );
1306
1307        std::fs::create_dir(temp.path().join("child")).expect("create child");
1308        set_current_dir("child").expect("set child cwd");
1309        assert_same_path(
1310            std::env::current_dir().expect("process cwd"),
1311            expected.join("child"),
1312        );
1313    }
1314
1315    #[test]
1316    fn set_provider_initializes_virtual_cwd_from_provider_default() {
1317        let _guard = test_lock();
1318        let _state_guard = TestProviderStateGuard::capture();
1319        replace_current_dir_override(None);
1320
1321        set_provider(Arc::new(VirtualFsProvider::new("/sandbox", &[])));
1322
1323        assert_eq!(
1324            current_dir().expect("virtual cwd"),
1325            PathBuf::from("/sandbox")
1326        );
1327    }
1328
1329    #[test]
1330    fn set_provider_preserves_existing_virtual_cwd() {
1331        let _guard = test_lock();
1332        let _state_guard = TestProviderStateGuard::capture();
1333        let initial = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1334        set_provider(initial);
1335        set_current_dir("/workspace").expect("set virtual cwd");
1336
1337        let replacement = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1338        set_provider(replacement.clone());
1339
1340        assert_eq!(
1341            current_dir().expect("virtual cwd"),
1342            PathBuf::from("/workspace")
1343        );
1344        futures::executor::block_on(write_async("data.txt", b"virtual")).expect("write relative");
1345        assert_eq!(
1346            replacement.file_bytes("/workspace/data.txt").as_deref(),
1347            Some(&b"virtual"[..])
1348        );
1349        assert_eq!(replacement.file_bytes("data.txt"), None);
1350    }
1351
1352    #[test]
1353    fn replace_provider_preserves_existing_virtual_cwd() {
1354        let _guard = test_lock();
1355        let initial = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1356        let _initial_guard = replace_provider(initial);
1357        set_current_dir("/workspace").expect("set virtual cwd");
1358
1359        let replacement = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1360        {
1361            let _replacement_guard = replace_provider(replacement.clone());
1362
1363            assert_eq!(
1364                current_dir().expect("virtual cwd"),
1365                PathBuf::from("/workspace")
1366            );
1367            futures::executor::block_on(write_async("nested.txt", b"replacement"))
1368                .expect("write relative");
1369        }
1370
1371        assert_eq!(
1372            replacement.file_bytes("/workspace/nested.txt").as_deref(),
1373            Some(&b"replacement"[..])
1374        );
1375        assert_eq!(replacement.file_bytes("nested.txt"), None);
1376    }
1377
1378    #[test]
1379    fn virtual_root_paths_do_not_resolve_relative_to_virtual_cwd() {
1380        let _guard = test_lock();
1381        let provider = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1382        let _provider_guard = replace_provider(provider.clone());
1383        set_current_dir("/workspace").expect("set virtual cwd");
1384
1385        futures::executor::block_on(write_async("/root.txt", b"root")).expect("write absolute");
1386
1387        assert_eq!(
1388            provider.file_bytes("/root.txt").as_deref(),
1389            Some(&b"root"[..])
1390        );
1391        assert_eq!(provider.file_bytes("/workspace/root.txt"), None);
1392    }
1393
1394    #[test]
1395    #[cfg(not(target_arch = "wasm32"))]
1396    fn native_provider_replacement_clears_virtual_cwd_override() {
1397        let _guard = test_lock();
1398        let temp = tempdir().expect("tempdir");
1399        let previous = std::env::current_dir().expect("current dir");
1400        let _cwd_guard = ProcessCwdGuard { previous };
1401        std::env::set_current_dir(temp.path()).expect("set temp cwd");
1402
1403        let virtual_provider = Arc::new(VirtualFsProvider::new("/", &["/workspace"]));
1404        let _virtual_guard = replace_provider(virtual_provider);
1405        set_current_dir("/workspace").expect("set virtual cwd");
1406
1407        {
1408            let _native_guard = replace_provider(Arc::new(NativeFsProvider));
1409            let expected = std::fs::canonicalize(temp.path()).expect("canonical temp");
1410            assert_same_path(current_dir().expect("native cwd"), &expected);
1411
1412            futures::executor::block_on(write_async("native.txt", b"native"))
1413                .expect("write native relative");
1414            assert_eq!(
1415                std::fs::read_to_string(temp.path().join("native.txt")).expect("read native file"),
1416                "native"
1417            );
1418        }
1419    }
1420
1421    #[test]
1422    fn open_async_and_flush_async_use_provider_async_paths() {
1423        let _guard = test_lock();
1424        let opened_async = Arc::new(Mutex::new(false));
1425        let flushed_async = Arc::new(Mutex::new(false));
1426        let provider = Arc::new(AsyncOpenProvider {
1427            opened_async: opened_async.clone(),
1428            flushed_async: flushed_async.clone(),
1429        });
1430        let _provider_guard = replace_provider(provider);
1431
1432        let mut file =
1433            futures::executor::block_on(OpenOptions::new().read(true).open_async("data.txt"))
1434                .expect("async open");
1435        let mut contents = String::new();
1436        file.read_to_string(&mut contents).expect("read contents");
1437        futures::executor::block_on(file.flush_async()).expect("async flush");
1438
1439        assert_eq!(contents, "async contents");
1440        assert!(*opened_async.lock().unwrap());
1441        assert!(*flushed_async.lock().unwrap());
1442    }
1443
1444    #[test]
1445    fn select_file_open_defaults_to_cancelled_selection() {
1446        let _guard = test_lock();
1447        let provider: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1448        let _provider_guard = replace_provider(provider);
1449        let request = OpenFileDialogRequest {
1450            title: Some("Open".to_string()),
1451            default_path: Some(PathBuf::from("data")),
1452            filters: vec![OpenFileDialogFilter {
1453                patterns: vec!["*.csv".to_string()],
1454                description: Some("CSV files".to_string()),
1455            }],
1456            multiselect: false,
1457        };
1458
1459        let selection =
1460            futures::executor::block_on(select_file_open_async(&request)).expect("select file");
1461
1462        assert_eq!(selection, None);
1463    }
1464
1465    #[test]
1466    fn with_provider_restores_even_on_panic() {
1467        let _guard = test_lock();
1468        let original = current_provider();
1469        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1470        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1471            with_provider_override(custom.clone(), || {
1472                let active = current_provider();
1473                assert!(Arc::ptr_eq(&active, &custom));
1474                panic!("boom");
1475            })
1476        }));
1477        assert!(result.is_err());
1478        let final_provider = current_provider();
1479        assert!(Arc::ptr_eq(&final_provider, &original));
1480    }
1481}