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