Skip to main content

wasmtime_wasi/
filesystem.rs

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