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
489pub 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
559pub 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
581pub 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
590pub 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
791pub 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(¤t, &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}