Skip to main content

vfs_host/
filesystem_types.rs

1// WASI Filesystem Types Host Implementation
2//
3// Implements wasi:filesystem/types interface using fs-core directly
4
5use super::{FsDescriptorWrapper, FsDirectoryEntryStreamWrapper, VfsHostState};
6use bytes::Bytes;
7use fs_core::Fs;
8use std::sync::Arc;
9use wasmtime::component::Resource;
10use wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode;
11use wasmtime_wasi::{
12    HostInputStream, HostOutputStream, StreamError, StreamResult, Subscribe, TrappableError,
13};
14
15#[cfg(feature = "s3-sync")]
16use super::SyncHooks;
17
18// fs-core open flags
19const O_RDONLY: u32 = 0;
20const O_WRONLY: u32 = 1;
21const O_RDWR: u32 = 2;
22const O_CREAT: u32 = 0o100;
23const O_TRUNC: u32 = 0o1000;
24
25/// Wrapper for fs-core InputStream that implements HostInputStream
26pub struct FsInputStreamWrapper {
27    /// Reference to shared VFS (no external lock needed)
28    shared_vfs: Arc<Fs>,
29    /// The fs-core file descriptor
30    fd: u32,
31    /// Current offset position
32    offset: u64,
33    /// File path for sync hooks
34    path: Option<String>,
35    /// Optional sync hooks for S3 synchronization
36    #[cfg(feature = "s3-sync")]
37    sync_hooks: Option<Arc<dyn SyncHooks>>,
38    /// Whether S3 refresh has been performed (to avoid repeated refreshes)
39    #[cfg(feature = "s3-sync")]
40    s3_refreshed: bool,
41}
42
43impl FsInputStreamWrapper {
44    pub fn new(shared_vfs: Arc<Fs>, fd: u32, offset: u64) -> Self {
45        Self {
46            shared_vfs,
47            fd,
48            offset,
49            path: None,
50            #[cfg(feature = "s3-sync")]
51            sync_hooks: None,
52            #[cfg(feature = "s3-sync")]
53            s3_refreshed: false,
54        }
55    }
56
57    /// Create with path and sync hooks for S3 synchronization
58    #[cfg(feature = "s3-sync")]
59    pub fn new_with_sync(
60        shared_vfs: Arc<Fs>,
61        fd: u32,
62        offset: u64,
63        path: Option<String>,
64        sync_hooks: Option<Arc<dyn SyncHooks>>,
65    ) -> Self {
66        Self {
67            shared_vfs,
68            fd,
69            offset,
70            path,
71            sync_hooks,
72            s3_refreshed: false,
73        }
74    }
75}
76
77/// Wrapper for fs-core OutputStream that implements HostOutputStream
78pub struct FsOutputStreamWrapper {
79    /// Reference to shared VFS (no external lock needed)
80    shared_vfs: Arc<Fs>,
81    /// The fs-core file descriptor
82    fd: u32,
83    /// Current offset position (None means append mode)
84    offset: Option<u64>,
85    /// File path for sync hooks
86    path: Option<String>,
87    /// Optional sync hooks for S3 synchronization
88    #[cfg(feature = "s3-sync")]
89    sync_hooks: Option<Arc<dyn SyncHooks>>,
90}
91
92impl FsOutputStreamWrapper {
93    pub fn new(shared_vfs: Arc<Fs>, fd: u32, offset: Option<u64>) -> Self {
94        Self {
95            shared_vfs,
96            fd,
97            offset,
98            path: None,
99            #[cfg(feature = "s3-sync")]
100            sync_hooks: None,
101        }
102    }
103
104    /// Create with path and sync hooks for S3 synchronization
105    #[cfg(feature = "s3-sync")]
106    pub fn new_with_sync(
107        shared_vfs: Arc<Fs>,
108        fd: u32,
109        offset: Option<u64>,
110        path: Option<String>,
111        sync_hooks: Option<Arc<dyn SyncHooks>>,
112    ) -> Self {
113        Self {
114            shared_vfs,
115            fd,
116            offset,
117            path,
118            sync_hooks,
119        }
120    }
121
122    /// Create with path (without sync hooks)
123    pub fn new_with_path(
124        shared_vfs: Arc<Fs>,
125        fd: u32,
126        offset: Option<u64>,
127        path: Option<String>,
128    ) -> Self {
129        Self {
130            shared_vfs,
131            fd,
132            offset,
133            path,
134            #[cfg(feature = "s3-sync")]
135            sync_hooks: None,
136        }
137    }
138}
139
140impl wasmtime_wasi::bindings::sync::filesystem::types::Host for VfsHostState {
141    fn filesystem_error_code(
142        &mut self,
143        err: Resource<anyhow::Error>,
144    ) -> Result<Option<ErrorCode>, anyhow::Error> {
145        let _error = self.table.get(&err)?;
146        Ok(None)
147    }
148
149    fn convert_error_code(
150        &mut self,
151        err: TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
152    ) -> Result<wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode, anyhow::Error> {
153        let nonsync_error = err.downcast()?;
154
155        use wasmtime_wasi::bindings::filesystem::types::ErrorCode as NonSync;
156        use wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode as Sync;
157
158        let sync_error = match nonsync_error {
159            NonSync::Access => Sync::Access,
160            NonSync::WouldBlock => Sync::WouldBlock,
161            NonSync::Already => Sync::Already,
162            NonSync::BadDescriptor => Sync::BadDescriptor,
163            NonSync::Busy => Sync::Busy,
164            NonSync::Deadlock => Sync::Deadlock,
165            NonSync::Quota => Sync::Quota,
166            NonSync::Exist => Sync::Exist,
167            NonSync::FileTooLarge => Sync::FileTooLarge,
168            NonSync::IllegalByteSequence => Sync::IllegalByteSequence,
169            NonSync::InProgress => Sync::InProgress,
170            NonSync::Interrupted => Sync::Interrupted,
171            NonSync::Invalid => Sync::Invalid,
172            NonSync::Io => Sync::Io,
173            NonSync::IsDirectory => Sync::IsDirectory,
174            NonSync::Loop => Sync::Loop,
175            NonSync::TooManyLinks => Sync::TooManyLinks,
176            NonSync::MessageSize => Sync::MessageSize,
177            NonSync::NameTooLong => Sync::NameTooLong,
178            NonSync::NoDevice => Sync::NoDevice,
179            NonSync::NoEntry => Sync::NoEntry,
180            NonSync::NoLock => Sync::NoLock,
181            NonSync::InsufficientMemory => Sync::InsufficientMemory,
182            NonSync::InsufficientSpace => Sync::InsufficientSpace,
183            NonSync::NotDirectory => Sync::NotDirectory,
184            NonSync::NotEmpty => Sync::NotEmpty,
185            NonSync::NotRecoverable => Sync::NotRecoverable,
186            NonSync::Unsupported => Sync::Unsupported,
187            NonSync::NoTty => Sync::NoTty,
188            NonSync::NoSuchDevice => Sync::NoSuchDevice,
189            NonSync::Overflow => Sync::Overflow,
190            NonSync::NotPermitted => Sync::NotPermitted,
191            NonSync::Pipe => Sync::Pipe,
192            NonSync::ReadOnly => Sync::ReadOnly,
193            NonSync::InvalidSeek => Sync::InvalidSeek,
194            NonSync::TextFileBusy => Sync::TextFileBusy,
195            NonSync::CrossDevice => Sync::CrossDevice,
196        };
197
198        Ok(sync_error)
199    }
200}
201
202impl VfsHostState {
203    /// Helper: Get fs-core fd and path from host descriptor resource
204    fn get_fs_descriptor(
205        &self,
206        host_desc: &Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
207    ) -> Result<(u32, Option<String>), anyhow::Error> {
208        let rep = host_desc.rep();
209        let wrapper_resource: Resource<FsDescriptorWrapper> = Resource::new_borrow(rep);
210        let wrapper = self
211            .table
212            .get(&wrapper_resource)
213            .map_err(|e| anyhow::anyhow!("Failed to get descriptor from table: {}", e))?;
214        Ok((wrapper.fd, wrapper.path.clone()))
215    }
216
217    /// Helper: Resolve relative path from directory descriptor
218    fn resolve_path(&self, dir_path: &Option<String>, relative_path: &str) -> String {
219        match dir_path {
220            Some(dir) if dir == "/" => format!("/{}", relative_path.trim_start_matches('/')),
221            Some(dir) => format!(
222                "{}/{}",
223                dir.trim_end_matches('/'),
224                relative_path.trim_start_matches('/')
225            ),
226            None => format!("/{}", relative_path.trim_start_matches('/')),
227        }
228    }
229}
230
231#[async_trait::async_trait]
232impl Subscribe for FsInputStreamWrapper {
233    async fn ready(&mut self) {
234        // For in-memory VFS, streams are always ready
235    }
236}
237
238impl HostInputStream for FsInputStreamWrapper {
239    fn read(&mut self, size: usize) -> StreamResult<Bytes> {
240        // S3 refresh (once per stream, on first read)
241        #[cfg(feature = "s3-sync")]
242        if !self.s3_refreshed {
243            if let (Some(ref hooks), Some(ref path)) = (&self.sync_hooks, &self.path) {
244                hooks.on_read(path);
245            }
246            self.s3_refreshed = true;
247        }
248
249        let offset = self.offset;
250
251        // Read data at offset atomically
252        let mut buf = vec![0u8; size];
253        let n = self
254            .shared_vfs
255            .read_at(self.fd, offset, &mut buf)
256            .map_err(|e| {
257                StreamError::LastOperationFailed(anyhow::anyhow!("read_at failed: {:?}", e))
258            })?;
259
260        buf.truncate(n);
261        self.offset += n as u64;
262        Ok(Bytes::from(buf))
263    }
264
265    fn skip(&mut self, nelem: usize) -> StreamResult<usize> {
266        // Simply advance the offset
267        self.offset += nelem as u64;
268        Ok(nelem)
269    }
270}
271
272#[async_trait::async_trait]
273impl Subscribe for FsOutputStreamWrapper {
274    async fn ready(&mut self) {
275        // For in-memory VFS, streams are always ready
276    }
277}
278
279impl HostOutputStream for FsOutputStreamWrapper {
280    fn write(&mut self, bytes: Bytes) -> StreamResult<()> {
281        match self.offset {
282            Some(offset) => {
283                // Positioned write at offset atomically
284                let n = self
285                    .shared_vfs
286                    .write_at(self.fd, offset, &bytes)
287                    .map_err(|e| {
288                        StreamError::LastOperationFailed(anyhow::anyhow!(
289                            "write_at failed: {:?}",
290                            e
291                        ))
292                    })?;
293
294                self.offset = Some(offset + n as u64);
295            }
296            None => {
297                // Append mode: use append_write
298                self.shared_vfs.append_write(self.fd, &bytes).map_err(|e| {
299                    StreamError::LastOperationFailed(anyhow::anyhow!(
300                        "append_write failed: {:?}",
301                        e
302                    ))
303                })?;
304            }
305        }
306
307        // Note: Sync hook is NOT called here - it's called in Drop to avoid
308        // triggering multiple uploads when writing large files in chunks
309
310        Ok(())
311    }
312
313    fn flush(&mut self) -> StreamResult<()> {
314        // In-memory FS: no-op
315        Ok(())
316    }
317
318    fn check_write(&mut self) -> StreamResult<usize> {
319        // In-memory FS: can always write
320        Ok(1024 * 1024) // 1MB buffer
321    }
322}
323
324/// Trigger sync hook when the output stream is dropped (file closed)
325#[cfg(feature = "s3-sync")]
326impl Drop for FsOutputStreamWrapper {
327    fn drop(&mut self) {
328        // Trigger sync hook when stream is closed
329        if let (Some(ref hooks), Some(ref path)) = (&self.sync_hooks, &self.path) {
330            hooks.on_write(path);
331        }
332    }
333}
334
335impl wasmtime_wasi::bindings::sync::filesystem::types::HostDescriptor for VfsHostState {
336    fn read(
337        &mut self,
338        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
339        len: u64,
340        offset: u64,
341    ) -> Result<
342        (Vec<u8>, bool),
343        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
344    > {
345        let (fd, _) = self
346            .get_fs_descriptor(&self_)
347            .map_err(TrappableError::trap)?;
348
349        // Read data at offset atomically
350        let mut buf = vec![0u8; len as usize];
351        let n = self
352            .shared_vfs
353            .read_at(fd, offset, &mut buf)
354            .map_err(convert_fs_error_to_trappable)?;
355
356        buf.truncate(n);
357        let eof = n < len as usize;
358        Ok((buf, eof))
359    }
360
361    fn write(
362        &mut self,
363        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
364        buffer: Vec<u8>,
365        offset: u64,
366    ) -> Result<u64, TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
367        let (fd, _) = self
368            .get_fs_descriptor(&self_)
369            .map_err(TrappableError::trap)?;
370
371        // Write data at offset atomically
372        let n = self
373            .shared_vfs
374            .write_at(fd, offset, &buffer)
375            .map_err(convert_fs_error_to_trappable)?;
376
377        Ok(n as u64)
378    }
379
380    fn drop(
381        &mut self,
382        rep: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
383    ) -> Result<(), anyhow::Error> {
384        let wrapper_resource: Resource<FsDescriptorWrapper> = Resource::new_own(rep.rep());
385        let wrapper = self.table.delete(wrapper_resource)?;
386
387        // Close the fd in fs-core
388        let _ = self.shared_vfs.close(wrapper.fd); // Ignore close errors
389        Ok(())
390    }
391
392    fn read_via_stream(
393        &mut self,
394        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
395        offset: u64,
396    ) -> Result<
397        Resource<Box<dyn wasmtime_wasi::HostInputStream>>,
398        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
399    > {
400        let (fd, path) = self
401            .get_fs_descriptor(&self_)
402            .map_err(TrappableError::trap)?;
403
404        #[cfg(feature = "s3-sync")]
405        let wrapper = FsInputStreamWrapper::new_with_sync(
406            Arc::clone(&self.shared_vfs),
407            fd,
408            offset,
409            path,
410            self.sync_hooks.clone(),
411        );
412
413        #[cfg(not(feature = "s3-sync"))]
414        let wrapper = FsInputStreamWrapper::new(Arc::clone(&self.shared_vfs), fd, offset);
415
416        let resource = self
417            .table
418            .push(Box::new(wrapper) as Box<dyn HostInputStream>)
419            .map_err(TrappableError::trap)?;
420
421        Ok(resource)
422    }
423
424    fn write_via_stream(
425        &mut self,
426        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
427        offset: u64,
428    ) -> Result<
429        Resource<Box<dyn wasmtime_wasi::HostOutputStream>>,
430        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
431    > {
432        let (fd, path) = self
433            .get_fs_descriptor(&self_)
434            .map_err(TrappableError::trap)?;
435
436        #[cfg(feature = "s3-sync")]
437        let wrapper = FsOutputStreamWrapper::new_with_sync(
438            Arc::clone(&self.shared_vfs),
439            fd,
440            Some(offset),
441            path,
442            self.sync_hooks.clone(),
443        );
444        #[cfg(not(feature = "s3-sync"))]
445        let wrapper = FsOutputStreamWrapper::new_with_path(
446            Arc::clone(&self.shared_vfs),
447            fd,
448            Some(offset),
449            path,
450        );
451
452        let resource = self
453            .table
454            .push(Box::new(wrapper) as Box<dyn HostOutputStream>)
455            .map_err(TrappableError::trap)?;
456
457        Ok(resource)
458    }
459
460    fn append_via_stream(
461        &mut self,
462        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
463    ) -> Result<
464        Resource<Box<dyn wasmtime_wasi::HostOutputStream>>,
465        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
466    > {
467        let (fd, path) = self
468            .get_fs_descriptor(&self_)
469            .map_err(TrappableError::trap)?;
470
471        // None offset means append mode
472        #[cfg(feature = "s3-sync")]
473        let wrapper = FsOutputStreamWrapper::new_with_sync(
474            Arc::clone(&self.shared_vfs),
475            fd,
476            None,
477            path,
478            self.sync_hooks.clone(),
479        );
480        #[cfg(not(feature = "s3-sync"))]
481        let wrapper =
482            FsOutputStreamWrapper::new_with_path(Arc::clone(&self.shared_vfs), fd, None, path);
483
484        let resource = self
485            .table
486            .push(Box::new(wrapper) as Box<dyn HostOutputStream>)
487            .map_err(TrappableError::trap)?;
488
489        Ok(resource)
490    }
491
492    fn advise(
493        &mut self,
494        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
495        _offset: u64,
496        _len: u64,
497        _advice: wasmtime_wasi::bindings::sync::filesystem::types::Advice,
498    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
499        // Advisory hints: can safely ignore
500        Ok(())
501    }
502
503    fn sync_data(
504        &mut self,
505        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
506    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
507        // In-memory FS: sync is no-op
508        Ok(())
509    }
510
511    fn get_flags(
512        &mut self,
513        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
514    ) -> Result<
515        wasmtime_wasi::bindings::sync::filesystem::types::DescriptorFlags,
516        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
517    > {
518        // Return read+write flags as default
519        use wasmtime_wasi::bindings::sync::filesystem::types::DescriptorFlags;
520        Ok(DescriptorFlags::READ | DescriptorFlags::WRITE)
521    }
522
523    fn get_type(
524        &mut self,
525        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
526    ) -> Result<
527        wasmtime_wasi::bindings::sync::filesystem::types::DescriptorType,
528        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
529    > {
530        let (fd, _) = self
531            .get_fs_descriptor(&self_)
532            .map_err(TrappableError::trap)?;
533
534        let meta = self
535            .shared_vfs
536            .fstat(fd)
537            .map_err(convert_fs_error_to_trappable)?;
538
539        use wasmtime_wasi::bindings::sync::filesystem::types::DescriptorType;
540        if meta.is_dir {
541            Ok(DescriptorType::Directory)
542        } else {
543            Ok(DescriptorType::RegularFile)
544        }
545    }
546
547    fn set_size(
548        &mut self,
549        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
550        size: u64,
551    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
552        let (fd, _) = self
553            .get_fs_descriptor(&self_)
554            .map_err(TrappableError::trap)?;
555
556        self.shared_vfs
557            .ftruncate(fd, size)
558            .map_err(convert_fs_error_to_trappable)?;
559
560        Ok(())
561    }
562
563    fn set_times(
564        &mut self,
565        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
566        _data_access_timestamp: wasmtime_wasi::bindings::sync::filesystem::types::NewTimestamp,
567        _data_modification_timestamp: wasmtime_wasi::bindings::sync::filesystem::types::NewTimestamp,
568    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
569        // fs-core doesn't support setting timestamps
570        Err(convert_sync_to_nonsync_error(ErrorCode::Unsupported))
571    }
572
573    fn set_times_at(
574        &mut self,
575        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
576        _path_flags: wasmtime_wasi::bindings::sync::filesystem::types::PathFlags,
577        _path: String,
578        _data_access_timestamp: wasmtime_wasi::bindings::sync::filesystem::types::NewTimestamp,
579        _data_modification_timestamp: wasmtime_wasi::bindings::sync::filesystem::types::NewTimestamp,
580    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
581        // fs-core doesn't support setting timestamps
582        Err(convert_sync_to_nonsync_error(ErrorCode::Unsupported))
583    }
584
585    fn read_directory(
586        &mut self,
587        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
588    ) -> Result<
589        Resource<wasmtime_wasi::bindings::filesystem::types::DirectoryEntryStream>,
590        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
591    > {
592        let (fd, _) = self
593            .get_fs_descriptor(&self_)
594            .map_err(TrappableError::trap)?;
595
596        // Get directory entries from fs-core
597        let entries = self
598            .shared_vfs
599            .readdir_fd(fd)
600            .map_err(convert_fs_error_to_trappable)?;
601
602        // Create stream wrapper
603        let wrapper = FsDirectoryEntryStreamWrapper {
604            entries,
605            position: 0,
606        };
607        let wrapper_resource: Resource<FsDirectoryEntryStreamWrapper> = self.table.push(wrapper)?;
608
609        // Transmute to expected return type
610        const _: () = {
611            use std::mem::{align_of, size_of};
612            assert!(
613                size_of::<Resource<FsDirectoryEntryStreamWrapper>>()
614                    == size_of::<
615                        Resource<wasmtime_wasi::bindings::filesystem::types::DirectoryEntryStream>,
616                    >()
617            );
618            assert!(
619                align_of::<Resource<FsDirectoryEntryStreamWrapper>>()
620                    == align_of::<
621                        Resource<wasmtime_wasi::bindings::filesystem::types::DirectoryEntryStream>,
622                    >()
623            );
624        };
625        let host_stream = unsafe {
626            std::mem::transmute::<
627                Resource<FsDirectoryEntryStreamWrapper>,
628                Resource<wasmtime_wasi::bindings::filesystem::types::DirectoryEntryStream>,
629            >(wrapper_resource)
630        };
631        Ok(host_stream)
632    }
633
634    fn sync(
635        &mut self,
636        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
637    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
638        Ok(())
639    }
640
641    fn create_directory_at(
642        &mut self,
643        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
644        path: String,
645    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
646        let (_, dir_path) = self
647            .get_fs_descriptor(&self_)
648            .map_err(TrappableError::trap)?;
649        let full_path = self.resolve_path(&dir_path, &path);
650
651        self.shared_vfs
652            .mkdir(&full_path)
653            .map_err(convert_fs_error_to_trappable)?;
654
655        Ok(())
656    }
657
658    fn stat(
659        &mut self,
660        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
661    ) -> Result<
662        wasmtime_wasi::bindings::sync::filesystem::types::DescriptorStat,
663        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
664    > {
665        let (fd, _) = self
666            .get_fs_descriptor(&self_)
667            .map_err(TrappableError::trap)?;
668
669        let meta = self
670            .shared_vfs
671            .fstat(fd)
672            .map_err(convert_fs_error_to_trappable)?;
673
674        Ok(convert_metadata_to_stat(&meta))
675    }
676
677    fn stat_at(
678        &mut self,
679        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
680        _path_flags: wasmtime_wasi::bindings::sync::filesystem::types::PathFlags,
681        path: String,
682    ) -> Result<
683        wasmtime_wasi::bindings::sync::filesystem::types::DescriptorStat,
684        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
685    > {
686        let (_, dir_path) = self
687            .get_fs_descriptor(&self_)
688            .map_err(TrappableError::trap)?;
689        let full_path = self.resolve_path(&dir_path, &path);
690
691        let meta = self
692            .shared_vfs
693            .stat(&full_path)
694            .map_err(convert_fs_error_to_trappable)?;
695
696        Ok(convert_metadata_to_stat(&meta))
697    }
698
699    fn link_at(
700        &mut self,
701        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
702        _old_path_flags: wasmtime_wasi::bindings::sync::filesystem::types::PathFlags,
703        _old_path: String,
704        _new_descriptor: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
705        _new_path: String,
706    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
707        // fs-core doesn't support hard links
708        Err(convert_sync_to_nonsync_error(ErrorCode::Unsupported))
709    }
710
711    fn open_at(
712        &mut self,
713        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
714        _path_flags: wasmtime_wasi::bindings::sync::filesystem::types::PathFlags,
715        path: String,
716        open_flags: wasmtime_wasi::bindings::sync::filesystem::types::OpenFlags,
717        flags: wasmtime_wasi::bindings::sync::filesystem::types::DescriptorFlags,
718    ) -> Result<
719        Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
720        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
721    > {
722        let (_, dir_path) = self
723            .get_fs_descriptor(&self_)
724            .map_err(TrappableError::trap)?;
725        let full_path = self.resolve_path(&dir_path, &path);
726
727        // Trigger on_open hook for metadata sync (like s3fs HEAD request)
728        // This checks S3 for updates before opening the file
729        #[cfg(feature = "s3-sync")]
730        if let Some(ref hooks) = self.sync_hooks {
731            hooks.on_open(&full_path);
732        }
733
734        // Convert flags to fs-core flags
735        let mut fs_flags = 0u32;
736
737        // Read/write mode
738        use wasmtime_wasi::bindings::sync::filesystem::types::DescriptorFlags;
739        if flags.contains(DescriptorFlags::READ) && flags.contains(DescriptorFlags::WRITE) {
740            fs_flags |= O_RDWR;
741        } else if flags.contains(DescriptorFlags::WRITE) {
742            fs_flags |= O_WRONLY;
743        } else {
744            fs_flags |= O_RDONLY;
745        }
746
747        // Open flags
748        use wasmtime_wasi::bindings::sync::filesystem::types::OpenFlags;
749        if open_flags.contains(OpenFlags::CREATE) {
750            fs_flags |= O_CREAT;
751        }
752        if open_flags.contains(OpenFlags::TRUNCATE) {
753            fs_flags |= O_TRUNC;
754        }
755
756        let fd = self
757            .shared_vfs
758            .open_path_with_flags(&full_path, fs_flags)
759            .map_err(convert_fs_error_to_trappable)?;
760
761        // Create wrapper - store path for both files and directories
762        // (needed for S3 sync hooks and directory operations)
763        let wrapper = FsDescriptorWrapper {
764            fd,
765            path: Some(full_path),
766        };
767        let wrapper_resource: Resource<FsDescriptorWrapper> = self.table.push(wrapper)?;
768
769        // Transmute to expected return type
770        const _: () = {
771            use std::mem::{align_of, size_of};
772            assert!(
773                size_of::<Resource<FsDescriptorWrapper>>()
774                    == size_of::<Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>>(
775                    )
776            );
777            assert!(
778                align_of::<Resource<FsDescriptorWrapper>>()
779                    == align_of::<Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>>(
780                    )
781            );
782        };
783        let host_descriptor = unsafe {
784            std::mem::transmute::<
785                Resource<FsDescriptorWrapper>,
786                Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
787            >(wrapper_resource)
788        };
789        Ok(host_descriptor)
790    }
791
792    fn readlink_at(
793        &mut self,
794        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
795        _path: String,
796    ) -> Result<String, TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
797        // fs-core doesn't support symlinks
798        Err(convert_sync_to_nonsync_error(ErrorCode::Unsupported))
799    }
800
801    fn remove_directory_at(
802        &mut self,
803        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
804        path: String,
805    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
806        let (_, dir_path) = self
807            .get_fs_descriptor(&self_)
808            .map_err(TrappableError::trap)?;
809        let full_path = self.resolve_path(&dir_path, &path);
810
811        self.shared_vfs
812            .rmdir(&full_path)
813            .map_err(convert_fs_error_to_trappable)?;
814
815        Ok(())
816    }
817
818    fn rename_at(
819        &mut self,
820        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
821        old_path: String,
822        _new_descriptor: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
823        new_path: String,
824    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
825        let (_, dir_path) = self
826            .get_fs_descriptor(&self_)
827            .map_err(TrappableError::trap)?;
828        let old_full = self.resolve_path(&dir_path, &old_path);
829        let new_full = self.resolve_path(&dir_path, &new_path);
830
831        self.shared_vfs
832            .rename(&old_full, &new_full)
833            .map_err(convert_fs_error_to_trappable)?;
834
835        Ok(())
836    }
837
838    fn symlink_at(
839        &mut self,
840        _self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
841        _old_path: String,
842        _new_path: String,
843    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
844        // fs-core doesn't support symlinks
845        Err(convert_sync_to_nonsync_error(ErrorCode::Unsupported))
846    }
847
848    fn unlink_file_at(
849        &mut self,
850        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
851        path: String,
852    ) -> Result<(), TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>> {
853        let (_, dir_path) = self
854            .get_fs_descriptor(&self_)
855            .map_err(TrappableError::trap)?;
856        let full_path = self.resolve_path(&dir_path, &path);
857
858        self.shared_vfs
859            .unlink(&full_path)
860            .map_err(convert_fs_error_to_trappable)?;
861
862        // Trigger sync hook after successful delete
863        #[cfg(feature = "s3-sync")]
864        if let Some(ref hooks) = self.sync_hooks {
865            hooks.on_delete(&full_path);
866        }
867
868        Ok(())
869    }
870
871    fn is_same_object(
872        &mut self,
873        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
874        other: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
875    ) -> Result<bool, anyhow::Error> {
876        let (fd1, _) = self.get_fs_descriptor(&self_)?;
877        let (fd2, _) = self.get_fs_descriptor(&other)?;
878        Ok(fd1 == fd2)
879    }
880
881    fn metadata_hash(
882        &mut self,
883        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
884    ) -> Result<
885        wasmtime_wasi::bindings::sync::filesystem::types::MetadataHashValue,
886        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
887    > {
888        let (fd, _) = self
889            .get_fs_descriptor(&self_)
890            .map_err(TrappableError::trap)?;
891
892        let meta = self
893            .shared_vfs
894            .fstat(fd)
895            .map_err(convert_fs_error_to_trappable)?;
896
897        // Simple hash based on size and timestamps
898        let lower = meta.size;
899        let upper = meta.modified;
900
901        Ok(wasmtime_wasi::bindings::sync::filesystem::types::MetadataHashValue { lower, upper })
902    }
903
904    fn metadata_hash_at(
905        &mut self,
906        self_: Resource<wasmtime_wasi::bindings::filesystem::types::Descriptor>,
907        _path_flags: wasmtime_wasi::bindings::sync::filesystem::types::PathFlags,
908        path: String,
909    ) -> Result<
910        wasmtime_wasi::bindings::sync::filesystem::types::MetadataHashValue,
911        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
912    > {
913        let (_, dir_path) = self
914            .get_fs_descriptor(&self_)
915            .map_err(TrappableError::trap)?;
916        let full_path = self.resolve_path(&dir_path, &path);
917
918        let meta = self
919            .shared_vfs
920            .stat(&full_path)
921            .map_err(convert_fs_error_to_trappable)?;
922
923        let lower = meta.size;
924        let upper = meta.modified;
925
926        Ok(wasmtime_wasi::bindings::sync::filesystem::types::MetadataHashValue { lower, upper })
927    }
928}
929
930impl wasmtime_wasi::bindings::sync::filesystem::types::HostDirectoryEntryStream for VfsHostState {
931    fn read_directory_entry(
932        &mut self,
933        self_: Resource<wasmtime_wasi::bindings::filesystem::types::DirectoryEntryStream>,
934    ) -> Result<
935        Option<wasmtime_wasi::bindings::sync::filesystem::types::DirectoryEntry>,
936        TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode>,
937    > {
938        let rep = self_.rep();
939        let wrapper_resource: Resource<FsDirectoryEntryStreamWrapper> = Resource::new_borrow(rep);
940
941        // Get mutable access to the stream wrapper
942        let wrapper = self.table.get_mut(&wrapper_resource).map_err(|e| {
943            TrappableError::trap(anyhow::anyhow!(
944                "Failed to get directory stream from table: {}",
945                e
946            ))
947        })?;
948
949        if wrapper.position >= wrapper.entries.len() {
950            return Ok(None);
951        }
952
953        let (name, is_dir) = wrapper.entries[wrapper.position].clone();
954        wrapper.position += 1;
955
956        use wasmtime_wasi::bindings::sync::filesystem::types::DescriptorType;
957        let type_ = if is_dir {
958            DescriptorType::Directory
959        } else {
960            DescriptorType::RegularFile
961        };
962
963        Ok(Some(
964            wasmtime_wasi::bindings::sync::filesystem::types::DirectoryEntry { type_, name },
965        ))
966    }
967
968    fn drop(
969        &mut self,
970        rep: Resource<wasmtime_wasi::bindings::filesystem::types::DirectoryEntryStream>,
971    ) -> Result<(), anyhow::Error> {
972        let wrapper_resource: Resource<FsDirectoryEntryStreamWrapper> =
973            Resource::new_own(rep.rep());
974        self.table.delete(wrapper_resource)?;
975        Ok(())
976    }
977}
978
979/// Helper to convert sync ErrorCode to non-sync ErrorCode for TrappableError
980fn convert_sync_to_nonsync_error(
981    sync_error: wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode,
982) -> TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode> {
983    use wasmtime_wasi::bindings::filesystem::types::ErrorCode as NonSync;
984    use wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode as Sync;
985
986    let nonsync_error = match sync_error {
987        Sync::Access => NonSync::Access,
988        Sync::WouldBlock => NonSync::WouldBlock,
989        Sync::Already => NonSync::Already,
990        Sync::BadDescriptor => NonSync::BadDescriptor,
991        Sync::Busy => NonSync::Busy,
992        Sync::Deadlock => NonSync::Deadlock,
993        Sync::Quota => NonSync::Quota,
994        Sync::Exist => NonSync::Exist,
995        Sync::FileTooLarge => NonSync::FileTooLarge,
996        Sync::IllegalByteSequence => NonSync::IllegalByteSequence,
997        Sync::InProgress => NonSync::InProgress,
998        Sync::Interrupted => NonSync::Interrupted,
999        Sync::Invalid => NonSync::Invalid,
1000        Sync::Io => NonSync::Io,
1001        Sync::IsDirectory => NonSync::IsDirectory,
1002        Sync::Loop => NonSync::Loop,
1003        Sync::TooManyLinks => NonSync::TooManyLinks,
1004        Sync::MessageSize => NonSync::MessageSize,
1005        Sync::NameTooLong => NonSync::NameTooLong,
1006        Sync::NoDevice => NonSync::NoDevice,
1007        Sync::NoEntry => NonSync::NoEntry,
1008        Sync::NoLock => NonSync::NoLock,
1009        Sync::InsufficientMemory => NonSync::InsufficientMemory,
1010        Sync::InsufficientSpace => NonSync::InsufficientSpace,
1011        Sync::NotDirectory => NonSync::NotDirectory,
1012        Sync::NotEmpty => NonSync::NotEmpty,
1013        Sync::NotRecoverable => NonSync::NotRecoverable,
1014        Sync::Unsupported => NonSync::Unsupported,
1015        Sync::NoTty => NonSync::NoTty,
1016        Sync::NoSuchDevice => NonSync::NoSuchDevice,
1017        Sync::Overflow => NonSync::Overflow,
1018        Sync::NotPermitted => NonSync::NotPermitted,
1019        Sync::Pipe => NonSync::Pipe,
1020        Sync::ReadOnly => NonSync::ReadOnly,
1021        Sync::InvalidSeek => NonSync::InvalidSeek,
1022        Sync::TextFileBusy => NonSync::TextFileBusy,
1023        Sync::CrossDevice => NonSync::CrossDevice,
1024    };
1025
1026    TrappableError::from(nonsync_error)
1027}
1028
1029/// Helper to convert fs-core error to TrappableError
1030fn convert_fs_error_to_trappable(
1031    error: fs_core::FsError,
1032) -> TrappableError<wasmtime_wasi::bindings::filesystem::types::ErrorCode> {
1033    let sync_error = super::convert_fs_error(error);
1034    convert_sync_to_nonsync_error(sync_error)
1035}
1036
1037/// Helper to convert fs-core Metadata to WASI DescriptorStat
1038fn convert_metadata_to_stat(
1039    meta: &fs_core::Metadata,
1040) -> wasmtime_wasi::bindings::sync::filesystem::types::DescriptorStat {
1041    use wasmtime_wasi::bindings::sync::filesystem::types::{
1042        Datetime, DescriptorStat, DescriptorType,
1043    };
1044
1045    DescriptorStat {
1046        type_: if meta.is_dir {
1047            DescriptorType::Directory
1048        } else {
1049            DescriptorType::RegularFile
1050        },
1051        link_count: 1,
1052        size: meta.size,
1053        data_access_timestamp: Some(Datetime {
1054            seconds: meta.modified,
1055            nanoseconds: 0,
1056        }),
1057        data_modification_timestamp: Some(Datetime {
1058            seconds: meta.modified,
1059            nanoseconds: 0,
1060        }),
1061        status_change_timestamp: Some(Datetime {
1062            seconds: meta.created,
1063            nanoseconds: 0,
1064        }),
1065    }
1066}