Skip to main content

wasmtime_wasi/
filesystem.rs

1use crate::clocks::Datetime;
2use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking};
3use cap_fs_ext::{FileTypeExt as _, MetadataExt as _, SystemTimeSpec};
4use io_lifetimes::AsFilelike;
5use std::collections::hash_map;
6use std::sync::Arc;
7use std::time::SystemTime;
8use tracing::debug;
9use wasmtime::component::{HasData, Resource, ResourceTable};
10use wasmtime::error::Context as _;
11
12#[cfg(unix)]
13pub(crate) mod unix;
14#[cfg(unix)]
15pub(crate) use unix as sys;
16#[cfg(windows)]
17pub(crate) mod windows;
18#[cfg(windows)]
19pub(crate) use windows as sys;
20
21/// A helper struct which implements [`HasData`] for the `wasi:filesystem` APIs.
22///
23/// This can be useful when directly calling `add_to_linker` functions directly,
24/// such as [`wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker`] as
25/// the `D` type parameter. See [`HasData`] for more information about the type
26/// parameter's purpose.
27///
28/// When using this type you can skip the [`WasiFilesystemView`] trait, for
29/// example.
30///
31/// [`wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker`]: crate::p2::bindings::filesystem::types::add_to_linker
32///
33/// # Examples
34///
35/// ```
36/// use wasmtime::component::{Linker, ResourceTable};
37/// use wasmtime::{Engine, Result};
38/// use wasmtime_wasi::filesystem::*;
39///
40/// struct MyStoreState {
41///     table: ResourceTable,
42///     filesystem: WasiFilesystemCtx,
43/// }
44///
45/// fn main() -> Result<()> {
46///     let engine = Engine::default();
47///     let mut linker = Linker::new(&engine);
48///
49///     wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker::<MyStoreState, WasiFilesystem>(
50///         &mut linker,
51///         |state| WasiFilesystemCtxView {
52///             table: &mut state.table,
53///             ctx: &mut state.filesystem,
54///         },
55///     )?;
56///     Ok(())
57/// }
58/// ```
59pub struct WasiFilesystem;
60
61impl HasData for WasiFilesystem {
62    type Data<'a> = WasiFilesystemCtxView<'a>;
63}
64
65#[derive(Clone, Default)]
66pub struct WasiFilesystemCtx {
67    pub(crate) allow_blocking_current_thread: bool,
68    pub(crate) preopens: Vec<(Dir, String)>,
69}
70
71pub struct WasiFilesystemCtxView<'a> {
72    pub ctx: &'a mut WasiFilesystemCtx,
73    pub table: &'a mut ResourceTable,
74}
75
76pub trait WasiFilesystemView: Send {
77    fn filesystem(&mut self) -> WasiFilesystemCtxView<'_>;
78}
79
80bitflags::bitflags! {
81    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
82    pub struct FilePerms: usize {
83        const READ = 0b1;
84        const WRITE = 0b10;
85    }
86}
87
88bitflags::bitflags! {
89    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
90    pub struct OpenMode: usize {
91        const READ = 0b1;
92        const WRITE = 0b10;
93    }
94}
95
96bitflags::bitflags! {
97    /// Permission bits for operating on a directory.
98    ///
99    /// Directories can be limited to being readonly. This will restrict what
100    /// can be done with them, for example preventing creation of new files.
101    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
102    pub struct DirPerms: usize {
103        /// This directory can be read, for example its entries can be iterated
104        /// over and files can be opened.
105        const READ = 0b1;
106
107        /// This directory can be mutated, for example by creating new files
108        /// within it.
109        const MUTATE = 0b10;
110    }
111}
112
113bitflags::bitflags! {
114    /// Flags determining the method of how paths are resolved.
115    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
116    pub(crate) struct PathFlags: usize {
117        /// This directory can be read, for example its entries can be iterated
118        /// over and files can be opened.
119        const SYMLINK_FOLLOW = 0b1;
120    }
121}
122
123bitflags::bitflags! {
124    /// Open flags used by `open-at`.
125    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
126    pub(crate) struct OpenFlags: usize {
127        /// Create file if it does not exist, similar to `O_CREAT` in POSIX.
128        const CREATE = 0b1;
129        /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX.
130        const DIRECTORY = 0b10;
131        /// Fail if file already exists, similar to `O_EXCL` in POSIX.
132        const EXCLUSIVE = 0b100;
133        /// Truncate file to size 0, similar to `O_TRUNC` in POSIX.
134        const TRUNCATE = 0b1000;
135    }
136}
137
138bitflags::bitflags! {
139    /// Descriptor flags.
140    ///
141    /// Note: This was called `fdflags` in earlier versions of WASI.
142    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
143    pub(crate) struct DescriptorFlags: usize {
144        /// Read mode: Data can be read.
145        const READ = 0b1;
146        /// Write mode: Data can be written to.
147        const WRITE = 0b10;
148        /// Request that writes be performed according to synchronized I/O file
149        /// integrity completion. The data stored in the file and the file's
150        /// metadata are synchronized. This is similar to `O_SYNC` in POSIX.
151        ///
152        /// The precise semantics of this operation have not yet been defined for
153        /// WASI. At this time, it should be interpreted as a request, and not a
154        /// requirement.
155        const FILE_INTEGRITY_SYNC = 0b100;
156        /// Request that writes be performed according to synchronized I/O data
157        /// integrity completion. Only the data stored in the file is
158        /// synchronized. This is similar to `O_DSYNC` in POSIX.
159        ///
160        /// The precise semantics of this operation have not yet been defined for
161        /// WASI. At this time, it should be interpreted as a request, and not a
162        /// requirement.
163        const DATA_INTEGRITY_SYNC = 0b1000;
164        /// Requests that reads be performed at the same level of integrity
165        /// requested for writes. This is similar to `O_RSYNC` in POSIX.
166        ///
167        /// The precise semantics of this operation have not yet been defined for
168        /// WASI. At this time, it should be interpreted as a request, and not a
169        /// requirement.
170        const REQUESTED_WRITE_SYNC = 0b10000;
171        /// Mutating directories mode: Directory contents may be mutated.
172        ///
173        /// When this flag is unset on a descriptor, operations using the
174        /// descriptor which would create, rename, delete, modify the data or
175        /// metadata of filesystem objects, or obtain another handle which
176        /// would permit any of those, shall fail with `error-code::read-only` if
177        /// they would otherwise succeed.
178        ///
179        /// This may only be set on directories.
180        const MUTATE_DIRECTORY = 0b100000;
181    }
182}
183
184/// Error codes returned by functions, similar to `errno` in POSIX.
185/// Not all of these error codes are returned by the functions provided by this
186/// API; some are used in higher-level library layers, and others are provided
187/// merely for alignment with POSIX.
188#[cfg_attr(
189    windows,
190    expect(dead_code, reason = "on Windows, some of these are not used")
191)]
192pub(crate) enum ErrorCode {
193    /// Permission denied, similar to `EACCES` in POSIX.
194    Access,
195    /// Connection already in progress, similar to `EALREADY` in POSIX.
196    Already,
197    /// Bad descriptor, similar to `EBADF` in POSIX.
198    BadDescriptor,
199    /// Device or resource busy, similar to `EBUSY` in POSIX.
200    Busy,
201    /// File exists, similar to `EEXIST` in POSIX.
202    Exist,
203    /// File too large, similar to `EFBIG` in POSIX.
204    FileTooLarge,
205    /// Illegal byte sequence, similar to `EILSEQ` in POSIX.
206    IllegalByteSequence,
207    /// Operation in progress, similar to `EINPROGRESS` in POSIX.
208    InProgress,
209    /// Interrupted function, similar to `EINTR` in POSIX.
210    Interrupted,
211    /// Invalid argument, similar to `EINVAL` in POSIX.
212    Invalid,
213    /// I/O error, similar to `EIO` in POSIX.
214    Io,
215    /// Is a directory, similar to `EISDIR` in POSIX.
216    IsDirectory,
217    /// Too many levels of symbolic links, similar to `ELOOP` in POSIX.
218    Loop,
219    /// Too many links, similar to `EMLINK` in POSIX.
220    TooManyLinks,
221    /// Filename too long, similar to `ENAMETOOLONG` in POSIX.
222    NameTooLong,
223    /// No such file or directory, similar to `ENOENT` in POSIX.
224    NoEntry,
225    /// Not enough space, similar to `ENOMEM` in POSIX.
226    InsufficientMemory,
227    /// No space left on device, similar to `ENOSPC` in POSIX.
228    InsufficientSpace,
229    /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX.
230    NotDirectory,
231    /// Directory not empty, similar to `ENOTEMPTY` in POSIX.
232    NotEmpty,
233    /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX.
234    Unsupported,
235    /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX.
236    Overflow,
237    /// Operation not permitted, similar to `EPERM` in POSIX.
238    NotPermitted,
239    /// Broken pipe, similar to `EPIPE` in POSIX.
240    Pipe,
241    /// Invalid seek, similar to `ESPIPE` in POSIX.
242    InvalidSeek,
243}
244
245fn datetime_from(t: std::time::SystemTime) -> Datetime {
246    // FIXME make this infallible or handle errors properly
247    Datetime::try_from(cap_std::time::SystemTime::from_std(t)).unwrap()
248}
249
250/// The type of a filesystem object referenced by a descriptor.
251///
252/// Note: This was called `filetype` in earlier versions of WASI.
253pub(crate) enum DescriptorType {
254    /// The type of the descriptor or file is unknown or is different from
255    /// any of the other types specified.
256    Unknown,
257    /// The descriptor refers to a block device inode.
258    BlockDevice,
259    /// The descriptor refers to a character device inode.
260    CharacterDevice,
261    /// The descriptor refers to a directory inode.
262    Directory,
263    /// The file refers to a symbolic link inode.
264    SymbolicLink,
265    /// The descriptor refers to a regular file inode.
266    RegularFile,
267}
268
269impl From<cap_std::fs::FileType> for DescriptorType {
270    fn from(ft: cap_std::fs::FileType) -> Self {
271        if ft.is_dir() {
272            DescriptorType::Directory
273        } else if ft.is_symlink() {
274            DescriptorType::SymbolicLink
275        } else if ft.is_block_device() {
276            DescriptorType::BlockDevice
277        } else if ft.is_char_device() {
278            DescriptorType::CharacterDevice
279        } else if ft.is_file() {
280            DescriptorType::RegularFile
281        } else {
282            DescriptorType::Unknown
283        }
284    }
285}
286
287/// File attributes.
288///
289/// Note: This was called `filestat` in earlier versions of WASI.
290pub(crate) struct DescriptorStat {
291    /// File type.
292    pub type_: DescriptorType,
293    /// Number of hard links to the file.
294    pub link_count: u64,
295    /// For regular files, the file size in bytes. For symbolic links, the
296    /// length in bytes of the pathname contained in the symbolic link.
297    pub size: u64,
298    /// Last data access timestamp.
299    ///
300    /// If the `option` is none, the platform doesn't maintain an access
301    /// timestamp for this file.
302    pub data_access_timestamp: Option<Datetime>,
303    /// Last data modification timestamp.
304    ///
305    /// If the `option` is none, the platform doesn't maintain a
306    /// modification timestamp for this file.
307    pub data_modification_timestamp: Option<Datetime>,
308    /// Last file status-change timestamp.
309    ///
310    /// If the `option` is none, the platform doesn't maintain a
311    /// status-change timestamp for this file.
312    pub status_change_timestamp: Option<Datetime>,
313}
314
315impl From<cap_std::fs::Metadata> for DescriptorStat {
316    fn from(meta: cap_std::fs::Metadata) -> Self {
317        Self {
318            type_: meta.file_type().into(),
319            link_count: meta.nlink(),
320            size: meta.len(),
321            data_access_timestamp: meta.accessed().map(|t| datetime_from(t.into_std())).ok(),
322            data_modification_timestamp: meta.modified().map(|t| datetime_from(t.into_std())).ok(),
323            status_change_timestamp: meta.created().map(|t| datetime_from(t.into_std())).ok(),
324        }
325    }
326}
327
328/// A 128-bit hash value, split into parts because wasm doesn't have a
329/// 128-bit integer type.
330pub(crate) struct MetadataHashValue {
331    /// 64 bits of a 128-bit hash value.
332    pub lower: u64,
333    /// Another 64 bits of a 128-bit hash value.
334    pub upper: u64,
335}
336
337impl From<&cap_std::fs::Metadata> for MetadataHashValue {
338    fn from(meta: &cap_std::fs::Metadata) -> Self {
339        use cap_fs_ext::MetadataExt;
340        // Without incurring any deps, std provides us with a 64 bit hash
341        // function:
342        use std::hash::Hasher;
343        // Note that this means that the metadata hash (which becomes a preview1 ino) may
344        // change when a different rustc release is used to build this host implementation:
345        let mut hasher = hash_map::DefaultHasher::new();
346        hasher.write_u64(meta.dev());
347        hasher.write_u64(meta.ino());
348        let lower = hasher.finish();
349        // MetadataHashValue has a pair of 64-bit members for representing a
350        // single 128-bit number. However, we only have 64 bits of entropy. To
351        // synthesize the upper 64 bits, lets xor the lower half with an arbitrary
352        // constant, in this case the 64 bit integer corresponding to the IEEE
353        // double representation of (a number as close as possible to) pi.
354        // This seems better than just repeating the same bits in the upper and
355        // lower parts outright, which could make folks wonder if the struct was
356        // mangled in the ABI, or worse yet, lead to consumers of this interface
357        // expecting them to be equal.
358        let upper = lower ^ 4614256656552045848u64;
359        Self { lower, upper }
360    }
361}
362
363#[derive(Copy, Clone, Debug)]
364pub(crate) enum Advice {
365    Normal,
366    Sequential,
367    Random,
368    WillNeed,
369    DontNeed,
370    NoReuse,
371}
372
373#[cfg(unix)]
374fn from_raw_os_error(err: Option<i32>) -> Option<ErrorCode> {
375    use rustix::io::Errno as RustixErrno;
376    if err.is_none() {
377        return None;
378    }
379    Some(match RustixErrno::from_raw_os_error(err.unwrap()) {
380        RustixErrno::PIPE => ErrorCode::Pipe,
381        RustixErrno::PERM => ErrorCode::NotPermitted,
382        RustixErrno::NOENT => ErrorCode::NoEntry,
383        RustixErrno::NOMEM => ErrorCode::InsufficientMemory,
384        RustixErrno::IO => ErrorCode::Io,
385        RustixErrno::BADF => ErrorCode::BadDescriptor,
386        RustixErrno::BUSY => ErrorCode::Busy,
387        RustixErrno::ACCESS => ErrorCode::Access,
388        RustixErrno::NOTDIR => ErrorCode::NotDirectory,
389        RustixErrno::ISDIR => ErrorCode::IsDirectory,
390        RustixErrno::INVAL => ErrorCode::Invalid,
391        RustixErrno::EXIST => ErrorCode::Exist,
392        RustixErrno::FBIG => ErrorCode::FileTooLarge,
393        RustixErrno::NOSPC => ErrorCode::InsufficientSpace,
394        RustixErrno::SPIPE => ErrorCode::InvalidSeek,
395        RustixErrno::MLINK => ErrorCode::TooManyLinks,
396        RustixErrno::NAMETOOLONG => ErrorCode::NameTooLong,
397        RustixErrno::NOTEMPTY => ErrorCode::NotEmpty,
398        RustixErrno::LOOP => ErrorCode::Loop,
399        RustixErrno::OVERFLOW => ErrorCode::Overflow,
400        RustixErrno::ILSEQ => ErrorCode::IllegalByteSequence,
401        RustixErrno::NOTSUP => ErrorCode::Unsupported,
402        RustixErrno::ALREADY => ErrorCode::Already,
403        RustixErrno::INPROGRESS => ErrorCode::InProgress,
404        RustixErrno::INTR => ErrorCode::Interrupted,
405
406        // On some platforms, these have the same value as other errno values.
407        #[allow(unreachable_patterns, reason = "see comment")]
408        RustixErrno::OPNOTSUPP => ErrorCode::Unsupported,
409
410        _ => return None,
411    })
412}
413
414#[cfg(windows)]
415fn from_raw_os_error(raw_os_error: Option<i32>) -> Option<ErrorCode> {
416    use windows_sys::Win32::Foundation;
417    Some(match raw_os_error.map(|code| code as u32) {
418        Some(Foundation::ERROR_FILE_NOT_FOUND) => ErrorCode::NoEntry,
419        Some(Foundation::ERROR_PATH_NOT_FOUND) => ErrorCode::NoEntry,
420        Some(Foundation::ERROR_ACCESS_DENIED) => ErrorCode::Access,
421        Some(Foundation::ERROR_SHARING_VIOLATION) => ErrorCode::Access,
422        Some(Foundation::ERROR_PRIVILEGE_NOT_HELD) => ErrorCode::NotPermitted,
423        Some(Foundation::ERROR_INVALID_HANDLE) => ErrorCode::BadDescriptor,
424        Some(Foundation::ERROR_INVALID_NAME) => ErrorCode::NoEntry,
425        Some(Foundation::ERROR_NOT_ENOUGH_MEMORY) => ErrorCode::InsufficientMemory,
426        Some(Foundation::ERROR_OUTOFMEMORY) => ErrorCode::InsufficientMemory,
427        Some(Foundation::ERROR_DIR_NOT_EMPTY) => ErrorCode::NotEmpty,
428        Some(Foundation::ERROR_NOT_READY) => ErrorCode::Busy,
429        Some(Foundation::ERROR_BUSY) => ErrorCode::Busy,
430        Some(Foundation::ERROR_NOT_SUPPORTED) => ErrorCode::Unsupported,
431        Some(Foundation::ERROR_FILE_EXISTS) => ErrorCode::Exist,
432        Some(Foundation::ERROR_BROKEN_PIPE) => ErrorCode::Pipe,
433        Some(Foundation::ERROR_BUFFER_OVERFLOW) => ErrorCode::NameTooLong,
434        Some(Foundation::ERROR_NOT_A_REPARSE_POINT) => ErrorCode::Invalid,
435        Some(Foundation::ERROR_NEGATIVE_SEEK) => ErrorCode::Invalid,
436        Some(Foundation::ERROR_DIRECTORY) => ErrorCode::NotDirectory,
437        Some(Foundation::ERROR_ALREADY_EXISTS) => ErrorCode::Exist,
438        Some(Foundation::ERROR_STOPPED_ON_SYMLINK) => ErrorCode::Loop,
439        Some(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED) => ErrorCode::IsDirectory,
440        _ => return None,
441    })
442}
443
444impl<'a> From<&'a std::io::Error> for ErrorCode {
445    fn from(err: &'a std::io::Error) -> ErrorCode {
446        match from_raw_os_error(err.raw_os_error()) {
447            Some(errno) => errno,
448            None => {
449                debug!("unknown raw os error: {err}");
450                match err.kind() {
451                    std::io::ErrorKind::NotFound => ErrorCode::NoEntry,
452                    std::io::ErrorKind::PermissionDenied => ErrorCode::NotPermitted,
453                    std::io::ErrorKind::AlreadyExists => ErrorCode::Exist,
454                    std::io::ErrorKind::InvalidInput => ErrorCode::Invalid,
455                    _ => ErrorCode::Io,
456                }
457            }
458        }
459    }
460}
461
462impl From<std::io::Error> for ErrorCode {
463    fn from(err: std::io::Error) -> ErrorCode {
464        ErrorCode::from(&err)
465    }
466}
467
468#[derive(Clone)]
469pub enum Descriptor {
470    File(File),
471    Dir(Dir),
472}
473
474impl Descriptor {
475    pub(crate) fn file(&self) -> Result<&File, ErrorCode> {
476        match self {
477            Descriptor::File(f) => Ok(f),
478            Descriptor::Dir(_) => Err(ErrorCode::BadDescriptor),
479        }
480    }
481
482    pub(crate) fn dir(&self) -> Result<&Dir, ErrorCode> {
483        match self {
484            Descriptor::Dir(d) => Ok(d),
485            Descriptor::File(_) => Err(ErrorCode::NotDirectory),
486        }
487    }
488
489    async fn get_metadata(&self) -> std::io::Result<cap_std::fs::Metadata> {
490        match self {
491            Self::File(f) => {
492                // No permissions check on metadata: if opened, allowed to stat it
493                f.run_blocking(|f| f.metadata()).await
494            }
495            Self::Dir(d) => {
496                // No permissions check on metadata: if opened, allowed to stat it
497                d.run_blocking(|d| d.dir_metadata()).await
498            }
499        }
500    }
501
502    pub(crate) async fn sync_data(&self) -> Result<(), ErrorCode> {
503        match self {
504            Self::File(f) => {
505                match f.run_blocking(|f| f.sync_data()).await {
506                    Ok(()) => Ok(()),
507                    // On windows, `sync_data` uses `FileFlushBuffers` which fails with
508                    // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore
509                    // this error, for POSIX compatibility.
510                    #[cfg(windows)]
511                    Err(err)
512                        if err.raw_os_error()
513                            == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>
514                    {
515                        Ok(())
516                    }
517                    Err(err) => Err(err.into()),
518                }
519            }
520            Self::Dir(d) => {
521                d.run_blocking(|d| {
522                    let d = d.open(std::path::Component::CurDir)?;
523                    d.sync_data()?;
524                    Ok(())
525                })
526                .await
527            }
528        }
529    }
530
531    pub(crate) async fn get_flags(&self) -> Result<DescriptorFlags, ErrorCode> {
532        match self {
533            Self::File(f) => {
534                let mut flags = f.run_blocking(|f| sys::get_flags(f)).await?;
535                if f.open_mode.contains(OpenMode::READ) {
536                    flags |= DescriptorFlags::READ;
537                }
538                if f.open_mode.contains(OpenMode::WRITE) {
539                    flags |= DescriptorFlags::WRITE;
540                }
541                Ok(flags)
542            }
543            Self::Dir(d) => {
544                let mut flags = d.run_blocking(|d| sys::get_flags(d)).await?;
545                if d.open_mode.contains(OpenMode::READ) {
546                    flags |= DescriptorFlags::READ;
547                }
548                if d.open_mode.contains(OpenMode::WRITE) {
549                    flags |= DescriptorFlags::MUTATE_DIRECTORY;
550                }
551                Ok(flags)
552            }
553        }
554    }
555
556    pub(crate) async fn get_type(&self) -> Result<DescriptorType, ErrorCode> {
557        match self {
558            Self::File(f) => {
559                let meta = f.run_blocking(|f| f.metadata()).await?;
560                Ok(meta.file_type().into())
561            }
562            Self::Dir(_) => Ok(DescriptorType::Directory),
563        }
564    }
565
566    pub(crate) async fn set_times(
567        &self,
568        atim: Option<SystemTime>,
569        mtim: Option<SystemTime>,
570    ) -> Result<(), ErrorCode> {
571        let mut times = std::fs::FileTimes::new();
572        if let Some(atim) = atim {
573            times = times.set_accessed(atim);
574        }
575        if let Some(mtim) = mtim {
576            times = times.set_modified(mtim);
577        }
578        match self {
579            Self::File(f) => {
580                if !f.perms.contains(FilePerms::WRITE) {
581                    return Err(ErrorCode::NotPermitted);
582                }
583                f.run_blocking(move |f| f.as_filelike_view::<std::fs::File>().set_times(times))
584                    .await?;
585                Ok(())
586            }
587            Self::Dir(d) => {
588                if !d.perms.contains(DirPerms::MUTATE) {
589                    return Err(ErrorCode::NotPermitted);
590                }
591                d.run_blocking(move |d| d.as_filelike_view::<std::fs::File>().set_times(times))
592                    .await?;
593                Ok(())
594            }
595        }
596    }
597
598    pub(crate) async fn sync(&self) -> Result<(), ErrorCode> {
599        match self {
600            Self::File(f) => {
601                match f.run_blocking(|f| f.sync_all()).await {
602                    Ok(()) => Ok(()),
603                    // On windows, `sync_data` uses `FileFlushBuffers` which fails with
604                    // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore
605                    // this error, for POSIX compatibility.
606                    #[cfg(windows)]
607                    Err(err)
608                        if err.raw_os_error()
609                            == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) =>
610                    {
611                        Ok(())
612                    }
613                    Err(err) => Err(err.into()),
614                }
615            }
616            Self::Dir(d) => {
617                d.run_blocking(|d| {
618                    let d = d.open(std::path::Component::CurDir)?;
619                    d.sync_all()?;
620                    Ok(())
621                })
622                .await
623            }
624        }
625    }
626
627    pub(crate) async fn stat(&self) -> Result<DescriptorStat, ErrorCode> {
628        match self {
629            Self::File(f) => {
630                // No permissions check on stat: if opened, allowed to stat it
631                let meta = f.run_blocking(|f| f.metadata()).await?;
632                Ok(meta.into())
633            }
634            Self::Dir(d) => {
635                // No permissions check on stat: if opened, allowed to stat it
636                let meta = d.run_blocking(|d| d.dir_metadata()).await?;
637                Ok(meta.into())
638            }
639        }
640    }
641
642    pub(crate) async fn is_same_object(&self, other: &Self) -> wasmtime::Result<bool> {
643        use cap_fs_ext::MetadataExt;
644        let meta_a = self.get_metadata().await?;
645        let meta_b = other.get_metadata().await?;
646        if meta_a.dev() == meta_b.dev() && meta_a.ino() == meta_b.ino() {
647            // MetadataHashValue does not derive eq, so use a pair of
648            // comparisons to check equality:
649            debug_assert_eq!(
650                MetadataHashValue::from(&meta_a).upper,
651                MetadataHashValue::from(&meta_b).upper,
652            );
653            debug_assert_eq!(
654                MetadataHashValue::from(&meta_a).lower,
655                MetadataHashValue::from(&meta_b).lower,
656            );
657            Ok(true)
658        } else {
659            // Hash collisions are possible, so don't assert the negative here
660            Ok(false)
661        }
662    }
663
664    pub(crate) async fn metadata_hash(&self) -> Result<MetadataHashValue, ErrorCode> {
665        let meta = self.get_metadata().await?;
666        Ok(MetadataHashValue::from(&meta))
667    }
668}
669
670#[derive(Clone)]
671pub struct File {
672    /// The operating system File this struct is mediating access to.
673    ///
674    /// Wrapped in an Arc because the same underlying file is used for
675    /// implementing the stream types. A copy is also needed for
676    /// `spawn_blocking`.
677    pub file: Arc<cap_std::fs::File>,
678    /// Permissions to enforce on access to the file. These permissions are
679    /// specified by a user of the `crate::WasiCtxBuilder`, and are
680    /// enforced prior to any enforced by the underlying operating system.
681    pub perms: FilePerms,
682    /// The mode the file was opened under: bits for reading, and writing.
683    /// Required to correctly report the DescriptorFlags, because cap-std
684    /// doesn't presently provide a cross-platform equivalent of reading the
685    /// oflags back out using fcntl.
686    pub open_mode: OpenMode,
687
688    allow_blocking_current_thread: bool,
689}
690
691impl File {
692    pub fn new(
693        file: cap_std::fs::File,
694        perms: FilePerms,
695        open_mode: OpenMode,
696        allow_blocking_current_thread: bool,
697    ) -> Self {
698        Self {
699            file: Arc::new(file),
700            perms,
701            open_mode,
702            allow_blocking_current_thread,
703        }
704    }
705
706    /// Execute the blocking `body` function.
707    ///
708    /// Depending on how the WasiCtx was configured, the body may either be:
709    /// - Executed directly on the current thread. In this case the `async`
710    ///   signature of this method is effectively a lie and the returned
711    ///   Future will always be immediately Ready. Or:
712    /// - Spawned on a background thread using [`tokio::task::spawn_blocking`]
713    ///   and immediately awaited.
714    ///
715    /// Intentionally blocking the executor thread might seem unorthodox, but is
716    /// not actually a problem for specific workloads. See:
717    /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`]
718    /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973)
719    /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190)
720    pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R
721    where
722        F: FnOnce(&cap_std::fs::File) -> R + Send + 'static,
723        R: Send + 'static,
724    {
725        match self.as_blocking_file() {
726            Some(file) => body(file),
727            None => self.spawn_blocking(body).await,
728        }
729    }
730
731    pub(crate) fn spawn_blocking<F, R>(&self, body: F) -> AbortOnDropJoinHandle<R>
732    where
733        F: FnOnce(&cap_std::fs::File) -> R + Send + 'static,
734        R: Send + 'static,
735    {
736        let f = self.file.clone();
737        spawn_blocking(move || body(&f))
738    }
739
740    /// Returns `Some` when the current thread is allowed to block in filesystem
741    /// operations, and otherwise returns `None` to indicate that
742    /// `spawn_blocking` must be used.
743    pub(crate) fn as_blocking_file(&self) -> Option<&cap_std::fs::File> {
744        if self.allow_blocking_current_thread {
745            Some(&self.file)
746        } else {
747            None
748        }
749    }
750
751    /// Returns reference to the underlying [`cap_std::fs::File`]
752    #[cfg(feature = "p3")]
753    pub(crate) fn as_file(&self) -> &Arc<cap_std::fs::File> {
754        &self.file
755    }
756
757    pub(crate) async fn advise(
758        &self,
759        offset: u64,
760        len: u64,
761        advice: Advice,
762    ) -> Result<(), ErrorCode> {
763        self.run_blocking(move |f| sys::advise(f, offset, len, advice))
764            .await?;
765        Ok(())
766    }
767
768    pub(crate) async fn set_size(&self, size: u64) -> Result<(), ErrorCode> {
769        if !self.perms.contains(FilePerms::WRITE) {
770            return Err(ErrorCode::NotPermitted);
771        }
772        self.run_blocking(move |f| f.set_len(size)).await?;
773        Ok(())
774    }
775}
776
777#[derive(Clone)]
778pub struct Dir {
779    /// The operating system file descriptor this struct is mediating access
780    /// to.
781    ///
782    /// Wrapped in an Arc because a copy is needed for `run_blocking`.
783    pub dir: Arc<cap_std::fs::Dir>,
784    /// Permissions to enforce on access to this directory. These permissions
785    /// are specified by a user of the `crate::WasiCtxBuilder`, and
786    /// are enforced prior to any enforced by the underlying operating system.
787    ///
788    /// These permissions are also enforced on any directories opened under
789    /// this directory.
790    pub perms: DirPerms,
791    /// Permissions to enforce on any files opened under this directory.
792    pub file_perms: FilePerms,
793    /// The mode the directory was opened under: bits for reading, and writing.
794    /// Required to correctly report the DescriptorFlags, because cap-std
795    /// doesn't presently provide a cross-platform equivalent of reading the
796    /// oflags back out using fcntl.
797    pub open_mode: OpenMode,
798
799    pub(crate) allow_blocking_current_thread: bool,
800}
801
802impl Dir {
803    pub fn new(
804        dir: cap_std::fs::Dir,
805        perms: DirPerms,
806        file_perms: FilePerms,
807        open_mode: OpenMode,
808        allow_blocking_current_thread: bool,
809    ) -> Self {
810        Dir {
811            dir: Arc::new(dir),
812            perms,
813            file_perms,
814            open_mode,
815            allow_blocking_current_thread,
816        }
817    }
818
819    /// Execute the blocking `body` function.
820    ///
821    /// Depending on how the WasiCtx was configured, the body may either be:
822    /// - Executed directly on the current thread. In this case the `async`
823    ///   signature of this method is effectively a lie and the returned
824    ///   Future will always be immediately Ready. Or:
825    /// - Spawned on a background thread using [`tokio::task::spawn_blocking`]
826    ///   and immediately awaited.
827    ///
828    /// Intentionally blocking the executor thread might seem unorthodox, but is
829    /// not actually a problem for specific workloads. See:
830    /// - [`crate::WasiCtxBuilder::allow_blocking_current_thread`]
831    /// - [Poor performance of wasmtime file I/O maybe because tokio](https://github.com/bytecodealliance/wasmtime/issues/7973)
832    /// - [Implement opt-in for enabling WASI to block the current thread](https://github.com/bytecodealliance/wasmtime/pull/8190)
833    pub(crate) async fn run_blocking<F, R>(&self, body: F) -> R
834    where
835        F: FnOnce(&cap_std::fs::Dir) -> R + Send + 'static,
836        R: Send + 'static,
837    {
838        if self.allow_blocking_current_thread {
839            body(&self.dir)
840        } else {
841            let d = self.dir.clone();
842            spawn_blocking(move || body(&d)).await
843        }
844    }
845
846    /// Returns reference to the underlying [`cap_std::fs::Dir`]
847    #[cfg(feature = "p3")]
848    pub(crate) fn as_dir(&self) -> &Arc<cap_std::fs::Dir> {
849        &self.dir
850    }
851
852    pub(crate) async fn create_directory_at(&self, path: String) -> Result<(), ErrorCode> {
853        if !self.perms.contains(DirPerms::MUTATE) {
854            return Err(ErrorCode::NotPermitted);
855        }
856        self.run_blocking(move |d| d.create_dir(&path)).await?;
857        Ok(())
858    }
859
860    pub(crate) async fn stat_at(
861        &self,
862        path_flags: PathFlags,
863        path: String,
864    ) -> Result<DescriptorStat, ErrorCode> {
865        if !self.perms.contains(DirPerms::READ) {
866            return Err(ErrorCode::NotPermitted);
867        }
868
869        let meta = if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
870            self.run_blocking(move |d| d.metadata(&path)).await?
871        } else {
872            self.run_blocking(move |d| d.symlink_metadata(&path))
873                .await?
874        };
875        Ok(meta.into())
876    }
877
878    pub(crate) async fn set_times_at(
879        &self,
880        path_flags: PathFlags,
881        path: String,
882        atim: Option<SystemTime>,
883        mtim: Option<SystemTime>,
884    ) -> Result<(), ErrorCode> {
885        use cap_fs_ext::DirExt as _;
886
887        if !self.perms.contains(DirPerms::MUTATE) {
888            return Err(ErrorCode::NotPermitted);
889        }
890        let atim = atim.map(|t| SystemTimeSpec::Absolute(cap_std::time::SystemTime::from_std(t)));
891        let mtim = mtim.map(|t| SystemTimeSpec::Absolute(cap_std::time::SystemTime::from_std(t)));
892        if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
893            self.run_blocking(move |d| d.set_times(&path, atim, mtim))
894                .await?;
895        } else {
896            self.run_blocking(move |d| d.set_symlink_times(&path, atim, mtim))
897                .await?;
898        }
899        Ok(())
900    }
901
902    pub(crate) async fn link_at(
903        &self,
904        old_path_flags: PathFlags,
905        old_path: String,
906        new_dir: &Self,
907        new_path: String,
908    ) -> Result<(), ErrorCode> {
909        if !self.perms.contains(DirPerms::MUTATE) {
910            return Err(ErrorCode::NotPermitted);
911        }
912        if !new_dir.perms.contains(DirPerms::MUTATE) {
913            return Err(ErrorCode::NotPermitted);
914        }
915        if old_path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
916            return Err(ErrorCode::Invalid);
917        }
918        let new_dir_handle = Arc::clone(&new_dir.dir);
919        self.run_blocking(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path))
920            .await?;
921        Ok(())
922    }
923
924    pub(crate) async fn open_at(
925        &self,
926        path_flags: PathFlags,
927        path: String,
928        oflags: OpenFlags,
929        flags: DescriptorFlags,
930        allow_blocking_current_thread: bool,
931    ) -> Result<Descriptor, ErrorCode> {
932        use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt};
933
934        if !self.perms.contains(DirPerms::READ) {
935            return Err(ErrorCode::NotPermitted);
936        }
937
938        if !self.perms.contains(DirPerms::MUTATE) {
939            if oflags.contains(OpenFlags::CREATE) || oflags.contains(OpenFlags::TRUNCATE) {
940                return Err(ErrorCode::NotPermitted);
941            }
942            if flags.contains(DescriptorFlags::WRITE) {
943                return Err(ErrorCode::NotPermitted);
944            }
945        }
946
947        // Track whether we are creating file, for permission check:
948        let mut create = false;
949        // Track open mode, for permission check and recording in created descriptor:
950        let mut open_mode = OpenMode::empty();
951        // Construct the OpenOptions to give the OS:
952        let mut opts = cap_std::fs::OpenOptions::new();
953        opts.maybe_dir(true);
954
955        if oflags.contains(OpenFlags::CREATE) {
956            if oflags.contains(OpenFlags::EXCLUSIVE) {
957                opts.create_new(true);
958            } else {
959                opts.create(true);
960            }
961            create = true;
962            opts.write(true);
963            open_mode |= OpenMode::WRITE;
964        }
965
966        if oflags.contains(OpenFlags::TRUNCATE) {
967            opts.truncate(true).write(true);
968            open_mode |= OpenMode::WRITE;
969        }
970        if flags.contains(DescriptorFlags::READ) {
971            opts.read(true);
972            open_mode |= OpenMode::READ;
973        }
974        if flags.contains(DescriptorFlags::WRITE) {
975            opts.write(true);
976            open_mode |= OpenMode::WRITE;
977        } else {
978            // If not opened write, open read. This way the OS lets us open
979            // the file, but we can use perms to reject use of the file later.
980            opts.read(true);
981            open_mode |= OpenMode::READ;
982        }
983        if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
984            opts.follow(FollowSymlinks::Yes);
985        } else {
986            opts.follow(FollowSymlinks::No);
987        }
988
989        // These flags are not yet supported in cap-std:
990        if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC)
991            || flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC)
992            || flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC)
993        {
994            return Err(ErrorCode::Unsupported);
995        }
996
997        if oflags.contains(OpenFlags::DIRECTORY) {
998            if oflags.contains(OpenFlags::CREATE)
999                || oflags.contains(OpenFlags::EXCLUSIVE)
1000                || oflags.contains(OpenFlags::TRUNCATE)
1001            {
1002                return Err(ErrorCode::Invalid);
1003            }
1004        }
1005
1006        // Now enforce this WasiCtx's permissions before letting the OS have
1007        // its shot:
1008        if !self.perms.contains(DirPerms::MUTATE) && create {
1009            return Err(ErrorCode::NotPermitted);
1010        }
1011        if !self.file_perms.contains(FilePerms::WRITE) && open_mode.contains(OpenMode::WRITE) {
1012            return Err(ErrorCode::NotPermitted);
1013        }
1014
1015        // Represents each possible outcome from the spawn_blocking operation.
1016        // This makes sure we don't have to give spawn_blocking any way to
1017        // manipulate the table.
1018        enum OpenResult {
1019            Dir(cap_std::fs::Dir),
1020            File(cap_std::fs::File),
1021            NotDir,
1022        }
1023
1024        let opened = self
1025            .run_blocking::<_, std::io::Result<OpenResult>>(move |d| {
1026                let opened = d.open_with(&path, &opts)?;
1027                if opened.metadata()?.is_dir() {
1028                    Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file(
1029                        opened.into_std(),
1030                    )))
1031                } else if oflags.contains(OpenFlags::DIRECTORY) {
1032                    Ok(OpenResult::NotDir)
1033                } else {
1034                    Ok(OpenResult::File(opened))
1035                }
1036            })
1037            .await?;
1038
1039        match opened {
1040            // Paper over a divergence between Windows and POSIX, where
1041            // POSIX returns EISDIR if you open a directory with the
1042            // WRITE flag: https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html#:~:text=EISDIR
1043            #[cfg(windows)]
1044            OpenResult::Dir(_) if flags.contains(DescriptorFlags::WRITE) => {
1045                Err(ErrorCode::IsDirectory)
1046            }
1047
1048            OpenResult::Dir(dir) => Ok(Descriptor::Dir(Dir::new(
1049                dir,
1050                self.perms,
1051                self.file_perms,
1052                open_mode,
1053                allow_blocking_current_thread,
1054            ))),
1055
1056            OpenResult::File(file) => Ok(Descriptor::File(File::new(
1057                file,
1058                self.file_perms,
1059                open_mode,
1060                allow_blocking_current_thread,
1061            ))),
1062
1063            OpenResult::NotDir => Err(ErrorCode::NotDirectory),
1064        }
1065    }
1066
1067    pub(crate) async fn readlink_at(&self, path: String) -> Result<String, ErrorCode> {
1068        if !self.perms.contains(DirPerms::READ) {
1069            return Err(ErrorCode::NotPermitted);
1070        }
1071        let link = self.run_blocking(move |d| d.read_link(&path)).await?;
1072        link.into_os_string()
1073            .into_string()
1074            .or(Err(ErrorCode::IllegalByteSequence))
1075    }
1076
1077    pub(crate) async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> {
1078        if !self.perms.contains(DirPerms::MUTATE) {
1079            return Err(ErrorCode::NotPermitted);
1080        }
1081        self.run_blocking(move |d| d.remove_dir(&path)).await?;
1082        Ok(())
1083    }
1084
1085    pub(crate) async fn rename_at(
1086        &self,
1087        old_path: String,
1088        new_dir: &Self,
1089        new_path: String,
1090    ) -> Result<(), ErrorCode> {
1091        if !self.perms.contains(DirPerms::MUTATE) {
1092            return Err(ErrorCode::NotPermitted);
1093        }
1094        if !new_dir.perms.contains(DirPerms::MUTATE) {
1095            return Err(ErrorCode::NotPermitted);
1096        }
1097        let new_dir_handle = Arc::clone(&new_dir.dir);
1098        self.run_blocking(move |d| d.rename(&old_path, &new_dir_handle, &new_path))
1099            .await?;
1100        Ok(())
1101    }
1102
1103    pub(crate) async fn symlink_at(
1104        &self,
1105        src_path: String,
1106        dest_path: String,
1107    ) -> Result<(), ErrorCode> {
1108        // On windows, Dir.symlink is provided by DirExt
1109        #[cfg(windows)]
1110        use cap_fs_ext::DirExt;
1111
1112        if !self.perms.contains(DirPerms::MUTATE) {
1113            return Err(ErrorCode::NotPermitted);
1114        }
1115        self.run_blocking(move |d| d.symlink(&src_path, &dest_path))
1116            .await?;
1117        Ok(())
1118    }
1119
1120    pub(crate) async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> {
1121        use cap_fs_ext::DirExt;
1122
1123        if !self.perms.contains(DirPerms::MUTATE) {
1124            return Err(ErrorCode::NotPermitted);
1125        }
1126        self.run_blocking(move |d| d.remove_file_or_symlink(&path))
1127            .await?;
1128        Ok(())
1129    }
1130
1131    pub(crate) async fn metadata_hash_at(
1132        &self,
1133        path_flags: PathFlags,
1134        path: String,
1135    ) -> Result<MetadataHashValue, ErrorCode> {
1136        // No permissions check on metadata: if dir opened, allowed to stat it
1137        let meta = self
1138            .run_blocking(move |d| {
1139                if path_flags.contains(PathFlags::SYMLINK_FOLLOW) {
1140                    d.metadata(path)
1141                } else {
1142                    d.symlink_metadata(path)
1143                }
1144            })
1145            .await?;
1146        Ok(MetadataHashValue::from(&meta))
1147    }
1148}
1149
1150impl WasiFilesystemCtxView<'_> {
1151    pub(crate) fn get_directories(
1152        &mut self,
1153    ) -> wasmtime::Result<Vec<(Resource<Descriptor>, String)>> {
1154        let preopens = self.ctx.preopens.clone();
1155        let mut results = Vec::with_capacity(preopens.len());
1156        for (dir, name) in preopens {
1157            let fd = self
1158                .table
1159                .push(Descriptor::Dir(dir))
1160                .with_context(|| format!("failed to push preopen {name}"))?;
1161            results.push((fd, name));
1162        }
1163        Ok(results)
1164    }
1165}