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
265impl DirEntry {
266    pub fn new(path: PathBuf, file_name: OsString, file_type: FsFileType) -> Self {
267        Self {
268            path,
269            file_name,
270            file_type,
271        }
272    }
273
274    pub fn path(&self) -> &Path {
275        &self.path
276    }
277
278    pub fn file_name(&self) -> &OsString {
279        &self.file_name
280    }
281
282    pub fn file_type(&self) -> FsFileType {
283        self.file_type
284    }
285
286    pub fn is_dir(&self) -> bool {
287        matches!(self.file_type, FsFileType::Directory)
288    }
289}
290
291#[async_trait(?Send)]
292pub trait FsProvider: Send + Sync + 'static {
293    fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>>;
294    async fn open_async(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
295        self.open(path, flags)
296    }
297    async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
298    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
299    async fn remove_file(&self, path: &Path) -> io::Result<()>;
300    async fn metadata(&self, path: &Path) -> io::Result<FsMetadata>;
301    async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata>;
302    async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
303    async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
304    async fn create_dir(&self, path: &Path) -> io::Result<()>;
305    async fn create_dir_all(&self, path: &Path) -> io::Result<()>;
306    async fn remove_dir(&self, path: &Path) -> io::Result<()>;
307    async fn remove_dir_all(&self, path: &Path) -> io::Result<()>;
308    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
309    async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()>;
310
311    async fn read_many(&self, paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
312        let mut entries = Vec::with_capacity(paths.len());
313        for path in paths {
314            let entry = match self.read(path).await {
315                Ok(payload) => ReadManyEntry::new(path.clone(), Some(payload)),
316                Err(error) => {
317                    warn!(
318                        "fs.read_many.miss path={} kind={:?} error={}",
319                        path.to_string_lossy(),
320                        error.kind(),
321                        error
322                    );
323                    ReadManyEntry::with_error(
324                        path.clone(),
325                        format!("kind={:?}; error={}", error.kind(), error),
326                    )
327                }
328            };
329            entries.push(entry);
330        }
331        Ok(entries)
332    }
333
334    async fn data_manifest_descriptor(
335        &self,
336        _request: &DataManifestRequest,
337    ) -> io::Result<DataManifestDescriptor> {
338        Err(io::Error::new(
339            ErrorKind::Unsupported,
340            "data manifest descriptor is unsupported by this provider",
341        ))
342    }
343
344    async fn data_chunk_upload_targets(
345        &self,
346        _request: &DataChunkUploadRequest,
347    ) -> io::Result<Vec<DataChunkUploadTarget>> {
348        Err(io::Error::new(
349            ErrorKind::Unsupported,
350            "data chunk upload targets are unsupported by this provider",
351        ))
352    }
353
354    async fn data_upload_chunk(
355        &self,
356        _target: &DataChunkUploadTarget,
357        _data: &[u8],
358    ) -> io::Result<()> {
359        Err(io::Error::new(
360            ErrorKind::Unsupported,
361            "data chunk upload is unsupported by this provider",
362        ))
363    }
364}
365
366pub struct File {
367    inner: Box<dyn FileHandle>,
368}
369
370impl File {
371    fn from_handle(handle: Box<dyn FileHandle>) -> Self {
372        Self { inner: handle }
373    }
374
375    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
376        let mut opts = OpenOptions::new();
377        opts.read(true);
378        opts.open(path)
379    }
380
381    pub async fn open_async(path: impl AsRef<Path>) -> io::Result<Self> {
382        let mut opts = OpenOptions::new();
383        opts.read(true);
384        opts.open_async(path).await
385    }
386
387    pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
388        let mut opts = OpenOptions::new();
389        opts.write(true).create(true).truncate(true);
390        opts.open(path)
391    }
392
393    pub async fn create_async(path: impl AsRef<Path>) -> io::Result<Self> {
394        let mut opts = OpenOptions::new();
395        opts.write(true).create(true).truncate(true);
396        opts.open_async(path).await
397    }
398
399    pub async fn flush_async(&mut self) -> io::Result<()> {
400        self.inner.flush_async().await
401    }
402
403    pub async fn sync_all_async(&mut self) -> io::Result<()> {
404        self.inner.sync_all_async().await
405    }
406}
407
408impl fmt::Debug for File {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        f.debug_struct("File").finish_non_exhaustive()
411    }
412}
413
414impl Read for File {
415    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
416        self.inner.read(buf)
417    }
418}
419
420impl Write for File {
421    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
422        self.inner.write(buf)
423    }
424
425    fn flush(&mut self) -> io::Result<()> {
426        self.inner.flush()
427    }
428}
429
430impl Seek for File {
431    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
432        self.inner.seek(pos)
433    }
434}
435
436static PROVIDER: OnceCell<RwLock<Arc<dyn FsProvider>>> = OnceCell::new();
437static PROVIDER_OVERRIDE_LOCK: OnceCell<Mutex<()>> = OnceCell::new();
438#[cfg(target_arch = "wasm32")]
439static CURRENT_DIR: OnceCell<RwLock<PathBuf>> = OnceCell::new();
440
441fn provider_lock() -> &'static RwLock<Arc<dyn FsProvider>> {
442    PROVIDER.get_or_init(|| RwLock::new(default_provider()))
443}
444
445/// Serializes tests and embedders that temporarily replace the process-wide
446/// filesystem provider.
447pub fn provider_override_lock() -> MutexGuard<'static, ()> {
448    PROVIDER_OVERRIDE_LOCK
449        .get_or_init(|| Mutex::new(()))
450        .lock()
451        .unwrap_or_else(|poisoned| poisoned.into_inner())
452}
453
454#[cfg(target_arch = "wasm32")]
455fn current_dir_lock() -> &'static RwLock<PathBuf> {
456    CURRENT_DIR.get_or_init(|| RwLock::new(PathBuf::from("/")))
457}
458
459fn with_provider<T>(f: impl FnOnce(&dyn FsProvider) -> T) -> T {
460    let guard = provider_lock()
461        .read()
462        .expect("filesystem provider lock poisoned");
463    f(&**guard)
464}
465
466fn resolve_path(path: &Path) -> PathBuf {
467    #[cfg(target_arch = "wasm32")]
468    {
469        if path.is_absolute() {
470            return path.to_path_buf();
471        }
472        if let Ok(base) = current_dir() {
473            return base.join(path);
474        }
475        PathBuf::from("/").join(path)
476    }
477    #[cfg(not(target_arch = "wasm32"))]
478    {
479        path.to_path_buf()
480    }
481}
482
483pub fn set_provider(provider: Arc<dyn FsProvider>) {
484    let mut guard = provider_lock()
485        .write()
486        .expect("filesystem provider lock poisoned");
487    *guard = provider;
488}
489
490/// Temporarily replace the active provider and return a guard that restores the
491/// previous provider when dropped. Useful for tests that need to install a mock
492/// filesystem without permanently mutating global state.
493pub fn replace_provider(provider: Arc<dyn FsProvider>) -> ProviderGuard {
494    let mut guard = provider_lock()
495        .write()
496        .expect("filesystem provider lock poisoned");
497    let previous = guard.clone();
498    *guard = provider;
499    ProviderGuard { previous }
500}
501
502/// Run a closure with the supplied provider installed, restoring the previous
503/// provider automatically afterwards.
504pub fn with_provider_override<R>(provider: Arc<dyn FsProvider>, f: impl FnOnce() -> R) -> R {
505    let guard = replace_provider(provider);
506    let result = f();
507    drop(guard);
508    result
509}
510
511/// Returns the currently installed provider.
512pub fn current_provider() -> Arc<dyn FsProvider> {
513    provider_lock()
514        .read()
515        .expect("filesystem provider lock poisoned")
516        .clone()
517}
518
519pub fn current_dir() -> io::Result<PathBuf> {
520    #[cfg(target_arch = "wasm32")]
521    {
522        return Ok(current_dir_lock()
523            .read()
524            .expect("filesystem current dir lock poisoned")
525            .clone());
526    }
527    #[cfg(not(target_arch = "wasm32"))]
528    {
529        std::env::current_dir()
530    }
531}
532
533pub fn set_current_dir(path: impl AsRef<Path>) -> io::Result<()> {
534    #[cfg(target_arch = "wasm32")]
535    {
536        let mut target = PathBuf::from(path.as_ref());
537        if !target.is_absolute() {
538            let base = current_dir()?;
539            target = base.join(target);
540        }
541        let canonical =
542            futures::executor::block_on(canonicalize_async(&target)).unwrap_or(target.clone());
543        let metadata = futures::executor::block_on(metadata_async(&canonical))?;
544        if !metadata.is_dir() {
545            return Err(io::Error::new(
546                ErrorKind::NotFound,
547                format!("Not a directory: {}", canonical.display()),
548            ));
549        }
550        let mut guard = current_dir_lock()
551            .write()
552            .expect("filesystem current dir lock poisoned");
553        *guard = canonical;
554        Ok(())
555    }
556    #[cfg(not(target_arch = "wasm32"))]
557    {
558        std::env::set_current_dir(path)
559    }
560}
561
562pub struct ProviderGuard {
563    previous: Arc<dyn FsProvider>,
564}
565
566impl Drop for ProviderGuard {
567    fn drop(&mut self) {
568        set_provider(self.previous.clone());
569    }
570}
571
572pub async fn read_many_async(paths: &[PathBuf]) -> io::Result<Vec<ReadManyEntry>> {
573    let resolved = paths
574        .iter()
575        .map(|path| resolve_path(path.as_path()))
576        .collect::<Vec<_>>();
577    let provider = current_provider();
578    provider.read_many(&resolved).await
579}
580
581pub async fn read_async(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
582    let resolved = resolve_path(path.as_ref());
583    let provider = current_provider();
584    provider.read(&resolved).await
585}
586
587pub async fn read_to_string_async(path: impl AsRef<Path>) -> io::Result<String> {
588    let bytes = read_async(path).await?;
589    String::from_utf8(bytes).map_err(|err| io::Error::new(ErrorKind::InvalidData, err.utf8_error()))
590}
591
592pub async fn write_async(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
593    let resolved = resolve_path(path.as_ref());
594    let provider = current_provider();
595    provider.write(&resolved, data.as_ref()).await
596}
597
598pub async fn remove_file_async(path: impl AsRef<Path>) -> io::Result<()> {
599    let resolved = resolve_path(path.as_ref());
600    let provider = current_provider();
601    provider.remove_file(&resolved).await
602}
603
604pub async fn metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
605    let resolved = resolve_path(path.as_ref());
606    let provider = current_provider();
607    provider.metadata(&resolved).await
608}
609
610pub async fn symlink_metadata_async(path: impl AsRef<Path>) -> io::Result<FsMetadata> {
611    let resolved = resolve_path(path.as_ref());
612    let provider = current_provider();
613    provider.symlink_metadata(&resolved).await
614}
615
616pub async fn read_dir_async(path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
617    let resolved = resolve_path(path.as_ref());
618    let provider = current_provider();
619    provider.read_dir(&resolved).await
620}
621
622pub async fn canonicalize_async(path: impl AsRef<Path>) -> io::Result<PathBuf> {
623    let resolved = resolve_path(path.as_ref());
624    let provider = current_provider();
625    provider.canonicalize(&resolved).await
626}
627
628pub async fn create_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
629    let resolved = resolve_path(path.as_ref());
630    let provider = current_provider();
631    provider.create_dir(&resolved).await
632}
633
634pub async fn create_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
635    let resolved = resolve_path(path.as_ref());
636    let provider = current_provider();
637    provider.create_dir_all(&resolved).await
638}
639
640pub async fn remove_dir_async(path: impl AsRef<Path>) -> io::Result<()> {
641    let resolved = resolve_path(path.as_ref());
642    let provider = current_provider();
643    provider.remove_dir(&resolved).await
644}
645
646pub async fn remove_dir_all_async(path: impl AsRef<Path>) -> io::Result<()> {
647    let resolved = resolve_path(path.as_ref());
648    let provider = current_provider();
649    provider.remove_dir_all(&resolved).await
650}
651
652pub async fn rename_async(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
653    let resolved_from = resolve_path(from.as_ref());
654    let resolved_to = resolve_path(to.as_ref());
655    let provider = current_provider();
656    provider.rename(&resolved_from, &resolved_to).await
657}
658
659pub async fn set_readonly_async(path: impl AsRef<Path>, readonly: bool) -> io::Result<()> {
660    let resolved = resolve_path(path.as_ref());
661    let provider = current_provider();
662    provider.set_readonly(&resolved, readonly).await
663}
664
665pub async fn data_manifest_descriptor_async(
666    request: &DataManifestRequest,
667) -> io::Result<DataManifestDescriptor> {
668    let provider = current_provider();
669    provider.data_manifest_descriptor(request).await
670}
671
672pub async fn data_chunk_upload_targets_async(
673    request: &DataChunkUploadRequest,
674) -> io::Result<Vec<DataChunkUploadTarget>> {
675    let provider = current_provider();
676    provider.data_chunk_upload_targets(request).await
677}
678
679pub async fn data_upload_chunk_async(
680    target: &DataChunkUploadTarget,
681    data: &[u8],
682) -> io::Result<()> {
683    let provider = current_provider();
684    provider.data_upload_chunk(target, data).await
685}
686
687/// Copy a file from `from` to `to`, truncating the destination when it exists.
688/// Returns the number of bytes written, matching `std::fs::copy`.
689pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
690    let mut reader = OpenOptions::new().read(true).open(from.as_ref())?;
691    let mut writer = OpenOptions::new()
692        .write(true)
693        .create(true)
694        .truncate(true)
695        .open(to.as_ref())?;
696    io::copy(&mut reader, &mut writer)
697}
698
699fn default_provider() -> Arc<dyn FsProvider> {
700    #[cfg(not(target_arch = "wasm32"))]
701    {
702        Arc::new(NativeFsProvider)
703    }
704    #[cfg(target_arch = "wasm32")]
705    {
706        Arc::new(PlaceholderFsProvider)
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use once_cell::sync::Lazy;
714    use std::io::{Read, Seek, SeekFrom, Write};
715    use std::sync::Mutex;
716    use tempfile::tempdir;
717
718    static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
719
720    struct UnsupportedProvider;
721
722    struct AsyncOpenProvider {
723        opened_async: Arc<Mutex<bool>>,
724        flushed_async: Arc<Mutex<bool>>,
725    }
726
727    struct AsyncTestHandle {
728        cursor: usize,
729        data: Vec<u8>,
730        flushed_async: Arc<Mutex<bool>>,
731    }
732
733    impl Read for AsyncTestHandle {
734        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
735            let remaining = self.data.len().saturating_sub(self.cursor);
736            let to_read = remaining.min(buf.len());
737            buf[..to_read].copy_from_slice(&self.data[self.cursor..self.cursor + to_read]);
738            self.cursor += to_read;
739            Ok(to_read)
740        }
741    }
742
743    impl Write for AsyncTestHandle {
744        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
745            let end = self.cursor + buf.len();
746            if end > self.data.len() {
747                self.data.resize(end, 0);
748            }
749            self.data[self.cursor..end].copy_from_slice(buf);
750            self.cursor = end;
751            Ok(buf.len())
752        }
753
754        fn flush(&mut self) -> io::Result<()> {
755            Ok(())
756        }
757    }
758
759    impl Seek for AsyncTestHandle {
760        fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
761            let next = match pos {
762                SeekFrom::Start(offset) => offset as i64,
763                SeekFrom::End(offset) => self.data.len() as i64 + offset,
764                SeekFrom::Current(offset) => self.cursor as i64 + offset,
765            };
766            if next < 0 {
767                return Err(io::Error::new(ErrorKind::InvalidInput, "seek before start"));
768            }
769            self.cursor = next as usize;
770            Ok(self.cursor as u64)
771        }
772    }
773
774    #[async_trait(?Send)]
775    impl FileHandle for AsyncTestHandle {
776        async fn flush_async(&mut self) -> io::Result<()> {
777            *self.flushed_async.lock().unwrap() = true;
778            Ok(())
779        }
780    }
781
782    #[async_trait(?Send)]
783    impl FsProvider for UnsupportedProvider {
784        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
785            Err(unsupported())
786        }
787
788        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
789            Err(unsupported())
790        }
791
792        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
793            Err(unsupported())
794        }
795
796        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
797            Err(unsupported())
798        }
799
800        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
801            Err(unsupported())
802        }
803
804        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
805            Err(unsupported())
806        }
807
808        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
809            Err(unsupported())
810        }
811
812        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
813            Err(unsupported())
814        }
815
816        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
817            Err(unsupported())
818        }
819
820        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
821            Err(unsupported())
822        }
823
824        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
825            Err(unsupported())
826        }
827
828        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
829            Err(unsupported())
830        }
831
832        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
833            Err(unsupported())
834        }
835
836        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
837            Err(unsupported())
838        }
839
840        async fn data_manifest_descriptor(
841            &self,
842            _request: &DataManifestRequest,
843        ) -> io::Result<DataManifestDescriptor> {
844            Err(unsupported())
845        }
846
847        async fn data_chunk_upload_targets(
848            &self,
849            _request: &DataChunkUploadRequest,
850        ) -> io::Result<Vec<DataChunkUploadTarget>> {
851            Err(unsupported())
852        }
853
854        async fn data_upload_chunk(
855            &self,
856            _target: &DataChunkUploadTarget,
857            _data: &[u8],
858        ) -> io::Result<()> {
859            Err(unsupported())
860        }
861    }
862
863    #[async_trait(?Send)]
864    impl FsProvider for AsyncOpenProvider {
865        fn open(&self, _path: &Path, _flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
866            Err(unsupported())
867        }
868
869        async fn open_async(
870            &self,
871            _path: &Path,
872            _flags: &OpenFlags,
873        ) -> io::Result<Box<dyn FileHandle>> {
874            *self.opened_async.lock().unwrap() = true;
875            Ok(Box::new(AsyncTestHandle {
876                cursor: 0,
877                data: b"async contents".to_vec(),
878                flushed_async: self.flushed_async.clone(),
879            }))
880        }
881
882        async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
883            Err(unsupported())
884        }
885
886        async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
887            Err(unsupported())
888        }
889
890        async fn remove_file(&self, _path: &Path) -> io::Result<()> {
891            Err(unsupported())
892        }
893
894        async fn metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
895            Err(unsupported())
896        }
897
898        async fn symlink_metadata(&self, _path: &Path) -> io::Result<FsMetadata> {
899            Err(unsupported())
900        }
901
902        async fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
903            Err(unsupported())
904        }
905
906        async fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
907            Err(unsupported())
908        }
909
910        async fn create_dir(&self, _path: &Path) -> io::Result<()> {
911            Err(unsupported())
912        }
913
914        async fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
915            Err(unsupported())
916        }
917
918        async fn remove_dir(&self, _path: &Path) -> io::Result<()> {
919            Err(unsupported())
920        }
921
922        async fn remove_dir_all(&self, _path: &Path) -> io::Result<()> {
923            Err(unsupported())
924        }
925
926        async fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
927            Err(unsupported())
928        }
929
930        async fn set_readonly(&self, _path: &Path, _readonly: bool) -> io::Result<()> {
931            Err(unsupported())
932        }
933    }
934
935    fn unsupported() -> io::Error {
936        io::Error::new(ErrorKind::Unsupported, "unsupported in test provider")
937    }
938
939    #[test]
940    fn copy_file_round_trip() {
941        let _guard = TEST_LOCK.lock().unwrap();
942        let dir = tempdir().expect("tempdir");
943        let src = dir.path().join("src.bin");
944        let dst = dir.path().join("dst.bin");
945        {
946            let mut file = std::fs::File::create(&src).expect("create src");
947            file.write_all(b"hello filesystem").expect("write src");
948        }
949
950        copy_file(&src, &dst).expect("copy");
951        let mut dst_file = File::open(&dst).expect("open dst");
952        let mut contents = Vec::new();
953        dst_file
954            .read_to_end(&mut contents)
955            .expect("read destination");
956        assert_eq!(contents, b"hello filesystem");
957    }
958
959    #[test]
960    fn set_readonly_flips_metadata_flag() {
961        let _guard = TEST_LOCK.lock().unwrap();
962        let dir = tempdir().expect("tempdir");
963        let path = dir.path().join("flag.txt");
964        futures::executor::block_on(write_async(&path, b"flag")).expect("write");
965
966        futures::executor::block_on(set_readonly_async(&path, true)).expect("set readonly");
967        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
968        assert!(meta.is_readonly());
969
970        futures::executor::block_on(set_readonly_async(&path, false)).expect("unset readonly");
971        let meta = futures::executor::block_on(metadata_async(&path)).expect("metadata");
972        assert!(!meta.is_readonly());
973    }
974
975    #[test]
976    fn replace_provider_restores_previous() {
977        let _guard = TEST_LOCK.lock().unwrap();
978        let original = current_provider();
979        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
980        {
981            let _guard = replace_provider(custom.clone());
982            let active = current_provider();
983            assert!(Arc::ptr_eq(&active, &custom));
984        }
985        let final_provider = current_provider();
986        assert!(Arc::ptr_eq(&final_provider, &original));
987    }
988
989    #[test]
990    fn open_async_and_flush_async_use_provider_async_paths() {
991        let _guard = TEST_LOCK.lock().unwrap();
992        let opened_async = Arc::new(Mutex::new(false));
993        let flushed_async = Arc::new(Mutex::new(false));
994        let provider = Arc::new(AsyncOpenProvider {
995            opened_async: opened_async.clone(),
996            flushed_async: flushed_async.clone(),
997        });
998        let _provider_guard = replace_provider(provider);
999
1000        let mut file =
1001            futures::executor::block_on(OpenOptions::new().read(true).open_async("data.txt"))
1002                .expect("async open");
1003        let mut contents = String::new();
1004        file.read_to_string(&mut contents).expect("read contents");
1005        futures::executor::block_on(file.flush_async()).expect("async flush");
1006
1007        assert_eq!(contents, "async contents");
1008        assert!(*opened_async.lock().unwrap());
1009        assert!(*flushed_async.lock().unwrap());
1010    }
1011
1012    #[test]
1013    fn with_provider_restores_even_on_panic() {
1014        let _guard = TEST_LOCK.lock().unwrap();
1015        let original = current_provider();
1016        let custom: Arc<dyn FsProvider> = Arc::new(UnsupportedProvider);
1017        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1018            with_provider_override(custom.clone(), || {
1019                let active = current_provider();
1020                assert!(Arc::ptr_eq(&active, &custom));
1021                panic!("boom");
1022            })
1023        }));
1024        assert!(result.is_err());
1025        let final_provider = current_provider();
1026        assert!(Arc::ptr_eq(&final_provider, &original));
1027    }
1028}