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