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