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