wasmer_wasix/fs/
mod.rs

1// TODO: currently, hard links are broken in the presence or renames.
2// It is impossible to fix them with the current setup, since a hard
3// link must point to the actual file rather than its path, but the
4// only way we can get to a file on a FileSystem instance is by going
5// through its repective FileOpener and giving it a path as input.
6// TODO: refactor away the InodeVal type
7
8mod fd;
9mod fd_list;
10mod inode_guard;
11mod notification;
12
13use std::{
14    borrow::{Borrow, Cow},
15    collections::{HashMap, HashSet},
16    ops::{Deref, DerefMut},
17    path::{Component, Path, PathBuf},
18    pin::Pin,
19    sync::{
20        Arc, Mutex, RwLock, Weak,
21        atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering},
22    },
23    task::{Context, Poll},
24};
25
26use self::fd_list::FdList;
27use crate::{
28    net::socket::InodeSocketKind,
29    state::{Stderr, Stdin, Stdout},
30};
31use futures::{Future, future::BoxFuture};
32#[cfg(feature = "enable-serde")]
33use serde_derive::{Deserialize, Serialize};
34use tokio::io::AsyncWriteExt;
35use tracing::{debug, trace};
36use virtual_fs::{
37    ArcFileSystem, FileSystem, FsError, MountFileSystem, OpenOptions, OverlayFileSystem,
38    TmpFileSystem, VirtualFile, limiter::DynFsMemoryLimiter,
39};
40use wasmer_config::package::PackageId;
41use wasmer_wasix_types::{
42    types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
43    wasi::{
44        Errno, Fd as WasiFd, Fdflags, Fdflagsext, Fdstat, Filesize, Filestat, Filetype,
45        Preopentype, Prestat, PrestatEnum, Rights, Socktype,
46    },
47};
48
49pub use self::fd::{Fd, FdInner, InodeVal, Kind};
50pub(crate) use self::inode_guard::{
51    InodeValFilePollGuard, InodeValFilePollGuardJoin, InodeValFilePollGuardMode,
52    InodeValFileReadGuard, InodeValFileWriteGuard, WasiStateFileGuard,
53};
54pub use self::notification::NotificationInner;
55use crate::syscalls::map_io_err;
56use crate::{ALL_RIGHTS, bin_factory::BinaryPackage, state::PreopenedDir};
57
58/// the fd value of the virtual root
59///
60/// Used for interacting with the file system when it has no
61/// pre-opened file descriptors at the root level. Normally
62/// a WASM process will do this in the libc initialization stage
63/// however that does not happen when the WASM process has never
64/// been run. Further that logic could change at any time in libc
65/// which would then break functionality. Instead we use this fixed
66/// file descriptor
67///
68/// This is especially important for fuse mounting journals which
69/// use the same syscalls as a normal WASI application but do not
70/// run the libc initialization logic
71pub const VIRTUAL_ROOT_FD: WasiFd = 3;
72
73/// The root inode and stdio inodes are the first inodes in the
74/// file system tree
75pub const FS_STDIN_INO: Inode = Inode(10);
76pub const FS_STDOUT_INO: Inode = Inode(11);
77pub const FS_STDERR_INO: Inode = Inode(12);
78pub const FS_ROOT_INO: Inode = Inode(13);
79
80const STDIN_DEFAULT_RIGHTS: Rights = {
81    // This might seem a bit overenineered, but it's the only way I
82    // discovered for getting the values in a const environment
83    Rights::from_bits_truncate(
84        Rights::FD_DATASYNC.bits()
85            | Rights::FD_READ.bits()
86            | Rights::FD_SYNC.bits()
87            | Rights::FD_ADVISE.bits()
88            | Rights::FD_FILESTAT_GET.bits()
89            | Rights::FD_FDSTAT_SET_FLAGS.bits()
90            | Rights::POLL_FD_READWRITE.bits(),
91    )
92};
93const STDOUT_DEFAULT_RIGHTS: Rights = {
94    // This might seem a bit overenineered, but it's the only way I
95    // discovered for getting the values in a const environment
96    Rights::from_bits_truncate(
97        Rights::FD_DATASYNC.bits()
98            | Rights::FD_SYNC.bits()
99            | Rights::FD_WRITE.bits()
100            | Rights::FD_ADVISE.bits()
101            | Rights::FD_FILESTAT_GET.bits()
102            | Rights::FD_FDSTAT_SET_FLAGS.bits()
103            | Rights::POLL_FD_READWRITE.bits(),
104    )
105};
106const STDERR_DEFAULT_RIGHTS: Rights = STDOUT_DEFAULT_RIGHTS;
107
108/// A completely aribtrary "big enough" number used as the upper limit for
109/// the number of symlinks that can be traversed when resolving a path
110pub const MAX_SYMLINKS: u32 = 128;
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
113#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
114pub struct Inode(u64);
115
116impl Inode {
117    pub fn as_u64(&self) -> u64 {
118        self.0
119    }
120
121    pub fn from_path(str: &str) -> Self {
122        Inode(xxhash_rust::xxh64::xxh64(str.as_bytes(), 0))
123    }
124}
125
126#[derive(Debug, Clone)]
127pub struct InodeGuard {
128    ino: Inode,
129    inner: Arc<InodeVal>,
130
131    // This exists because self.inner doesn't really represent the
132    // number of FDs referencing this InodeGuard. We need that number
133    // so we can know when to drop the file handle, which should result
134    // in the backing file (which may be a host file) getting closed.
135    open_handles: Arc<AtomicI32>,
136}
137impl InodeGuard {
138    pub fn ino(&self) -> Inode {
139        self.ino
140    }
141
142    pub fn downgrade(&self) -> InodeWeakGuard {
143        InodeWeakGuard {
144            ino: self.ino,
145            open_handles: self.open_handles.clone(),
146            inner: Arc::downgrade(&self.inner),
147        }
148    }
149
150    pub fn ref_cnt(&self) -> usize {
151        Arc::strong_count(&self.inner)
152    }
153
154    pub fn handle_count(&self) -> u32 {
155        self.open_handles.load(Ordering::SeqCst) as u32
156    }
157
158    pub fn acquire_handle(&self) {
159        let prev_handles = self.open_handles.fetch_add(1, Ordering::SeqCst);
160        trace!(ino = %self.ino.0, new_count = %(prev_handles + 1), "acquiring handle for InodeGuard");
161    }
162
163    pub fn drop_one_handle(&self) {
164        let prev_handles = self.open_handles.fetch_sub(1, Ordering::SeqCst);
165
166        trace!(ino = %self.ino.0, %prev_handles, "dropping handle for InodeGuard");
167
168        // If this wasn't the last handle, nothing else to do...
169        if prev_handles > 1 {
170            return;
171        }
172
173        // ... otherwise, drop the VirtualFile reference
174        let mut guard = self.inner.write();
175
176        // Must have at least one open handle before we can drop.
177        // This check happens after `inner` is locked so we can
178        // poison the lock and keep people from using this (possibly
179        // corrupt) InodeGuard.
180        if prev_handles != 1 {
181            panic!("InodeGuard handle dropped too many times");
182        }
183
184        // Re-check the open handles to account for race conditions
185        if self.open_handles.load(Ordering::SeqCst) != 0 {
186            return;
187        }
188
189        let ino = self.ino.0;
190        trace!(%ino, "InodeGuard has no more open handles");
191
192        match guard.deref_mut() {
193            Kind::File { handle, .. } if handle.is_some() => {
194                let file_ref_count = Arc::strong_count(handle.as_ref().unwrap());
195                trace!(%file_ref_count, %ino, "dropping file handle");
196                drop(handle.take().unwrap());
197            }
198            Kind::PipeRx { rx } => {
199                trace!(%ino, "closing pipe rx");
200                rx.close();
201            }
202            Kind::PipeTx { tx } => {
203                trace!(%ino, "closing pipe tx");
204                tx.close();
205            }
206            _ => (),
207        }
208    }
209}
210impl std::ops::Deref for InodeGuard {
211    type Target = InodeVal;
212    fn deref(&self) -> &Self::Target {
213        self.inner.deref()
214    }
215}
216
217#[derive(Debug, Clone)]
218pub struct InodeWeakGuard {
219    ino: Inode,
220    // Needed for when we want to upgrade back. We don't exactly
221    // care too much when the AtomicI32 is dropped, so this is
222    // a strong reference to keep things simple.
223    open_handles: Arc<AtomicI32>,
224    inner: Weak<InodeVal>,
225}
226impl InodeWeakGuard {
227    pub fn ino(&self) -> Inode {
228        self.ino
229    }
230    pub fn upgrade(&self) -> Option<InodeGuard> {
231        Weak::upgrade(&self.inner).map(|inner| InodeGuard {
232            ino: self.ino,
233            open_handles: self.open_handles.clone(),
234            inner,
235        })
236    }
237}
238
239#[derive(Debug)]
240#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
241struct EphemeralSymlinkEntry {
242    base_po_dir: WasiFd,
243    path_to_symlink: PathBuf,
244    relative_path: PathBuf,
245}
246
247#[derive(Debug)]
248#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
249struct WasiInodesProtected {
250    lookup: HashMap<Inode, Weak<InodeVal>>,
251}
252
253#[derive(Clone, Debug)]
254#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
255pub struct WasiInodes {
256    protected: Arc<RwLock<WasiInodesProtected>>,
257}
258
259fn normalize_virtual_symlink_key(path: &Path) -> PathBuf {
260    let mut normalized = PathBuf::new();
261    for component in path.components() {
262        match component {
263            Component::RootDir => normalized.push("/"),
264            Component::CurDir => {}
265            Component::ParentDir => {
266                if !normalized.as_os_str().is_empty() && normalized.as_os_str() != "/" {
267                    normalized.pop();
268                }
269            }
270            Component::Normal(seg) => normalized.push(seg),
271            Component::Prefix(_) => {}
272        }
273    }
274    if normalized.as_os_str().is_empty() {
275        PathBuf::from("/")
276    } else {
277        normalized
278    }
279}
280
281impl WasiInodes {
282    pub fn new() -> Self {
283        Self {
284            protected: Arc::new(RwLock::new(WasiInodesProtected {
285                lookup: Default::default(),
286            })),
287        }
288    }
289
290    /// adds another value to the inodes
291    pub fn add_inode_val(&self, val: InodeVal) -> InodeGuard {
292        let val = Arc::new(val);
293        let st_ino = {
294            let guard = val.stat.read().unwrap();
295            guard.st_ino
296        };
297
298        let mut guard = self.protected.write().unwrap();
299        let ino = Inode(st_ino);
300        guard.lookup.insert(ino, Arc::downgrade(&val));
301
302        // every 100 calls we clear out dead weaks
303        if guard.lookup.len() % 100 == 1 {
304            guard.lookup.retain(|_, v| Weak::strong_count(v) > 0);
305        }
306
307        let open_handles = Arc::new(AtomicI32::new(0));
308
309        InodeGuard {
310            ino,
311            open_handles,
312            inner: val,
313        }
314    }
315
316    /// Get the `VirtualFile` object at stdout
317    pub(crate) fn stdout(fd_map: &RwLock<FdList>) -> Result<InodeValFileReadGuard, FsError> {
318        Self::std_dev_get(fd_map, __WASI_STDOUT_FILENO)
319    }
320    /// Get the `VirtualFile` object at stdout mutably
321    pub(crate) fn stdout_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
322        Self::std_dev_get_mut(fd_map, __WASI_STDOUT_FILENO)
323    }
324
325    /// Get the `VirtualFile` object at stderr
326    pub(crate) fn stderr(fd_map: &RwLock<FdList>) -> Result<InodeValFileReadGuard, FsError> {
327        Self::std_dev_get(fd_map, __WASI_STDERR_FILENO)
328    }
329    /// Get the `VirtualFile` object at stderr mutably
330    pub(crate) fn stderr_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
331        Self::std_dev_get_mut(fd_map, __WASI_STDERR_FILENO)
332    }
333
334    /// Get the `VirtualFile` object at stdin
335    /// TODO: Review why this is dead
336    #[allow(dead_code)]
337    pub(crate) fn stdin(fd_map: &RwLock<FdList>) -> Result<InodeValFileReadGuard, FsError> {
338        Self::std_dev_get(fd_map, __WASI_STDIN_FILENO)
339    }
340    /// Get the `VirtualFile` object at stdin mutably
341    pub(crate) fn stdin_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
342        Self::std_dev_get_mut(fd_map, __WASI_STDIN_FILENO)
343    }
344
345    /// Internal helper function to get a standard device handle.
346    /// Expects one of `__WASI_STDIN_FILENO`, `__WASI_STDOUT_FILENO`, `__WASI_STDERR_FILENO`.
347    fn std_dev_get(fd_map: &RwLock<FdList>, fd: WasiFd) -> Result<InodeValFileReadGuard, FsError> {
348        if let Some(fd) = fd_map.read().unwrap().get(fd) {
349            let guard = fd.inode.read();
350            if let Kind::File {
351                handle: Some(handle),
352                ..
353            } = guard.deref()
354            {
355                Ok(InodeValFileReadGuard::new(handle))
356            } else {
357                // Our public API should ensure that this is not possible
358                Err(FsError::NotAFile)
359            }
360        } else {
361            // this should only trigger if we made a mistake in this crate
362            Err(FsError::NoDevice)
363        }
364    }
365    /// Internal helper function to mutably get a standard device handle.
366    /// Expects one of `__WASI_STDIN_FILENO`, `__WASI_STDOUT_FILENO`, `__WASI_STDERR_FILENO`.
367    fn std_dev_get_mut(
368        fd_map: &RwLock<FdList>,
369        fd: WasiFd,
370    ) -> Result<InodeValFileWriteGuard, FsError> {
371        if let Some(fd) = fd_map.read().unwrap().get(fd) {
372            let guard = fd.inode.read();
373            if let Kind::File {
374                handle: Some(handle),
375                ..
376            } = guard.deref()
377            {
378                Ok(InodeValFileWriteGuard::new(handle))
379            } else {
380                // Our public API should ensure that this is not possible
381                Err(FsError::NotAFile)
382            }
383        } else {
384            // this should only trigger if we made a mistake in this crate
385            Err(FsError::NoDevice)
386        }
387    }
388}
389
390impl Default for WasiInodes {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396#[derive(Debug, Clone)]
397pub struct WasiFsRoot {
398    root: Arc<MountFileSystem>,
399    memory_limiter: Option<DynFsMemoryLimiter>,
400}
401
402impl WasiFsRoot {
403    pub fn from_mount_fs(root: MountFileSystem) -> Self {
404        Self {
405            root: Arc::new(root),
406            memory_limiter: None,
407        }
408    }
409
410    pub fn from_filesystem(fs: Arc<dyn FileSystem + Send + Sync>) -> Self {
411        let root = MountFileSystem::new();
412        root.mount(Path::new("/"), fs)
413            .expect("mounting the root fs on an empty mount fs should succeed");
414
415        Self {
416            root: Arc::new(root),
417            memory_limiter: None,
418        }
419    }
420
421    pub fn with_memory_limiter_opt(mut self, limiter: Option<DynFsMemoryLimiter>) -> Self {
422        self.memory_limiter = limiter;
423        self
424    }
425
426    pub(crate) fn memory_limiter(&self) -> Option<&DynFsMemoryLimiter> {
427        self.memory_limiter.as_ref()
428    }
429
430    pub(crate) fn root(&self) -> &Arc<MountFileSystem> {
431        &self.root
432    }
433
434    pub(crate) fn writable_root(&self) -> Option<TmpFileSystem> {
435        let root = self.root.filesystem_at(Path::new("/"))?;
436        find_writable_root(root.as_ref())
437    }
438
439    pub(crate) fn stack_root_filesystem(
440        &self,
441        lower: Arc<dyn FileSystem + Send + Sync>,
442    ) -> Result<(), FsError> {
443        let current = self
444            .root
445            .filesystem_at(Path::new("/"))
446            .ok_or(FsError::EntryNotFound)?;
447        let overlay =
448            OverlayFileSystem::new(ArcFileSystem::new(current), [ArcFileSystem::new(lower)]);
449        self.root.set_mount(Path::new("/"), Arc::new(overlay))
450    }
451}
452
453fn find_writable_root(fs: &(dyn FileSystem + Send + Sync)) -> Option<TmpFileSystem> {
454    if let Some(tmp) = fs.upcast_any_ref().downcast_ref::<TmpFileSystem>() {
455        return Some(tmp.clone());
456    }
457
458    if let Some(arc_fs) = fs.upcast_any_ref().downcast_ref::<ArcFileSystem>() {
459        return find_writable_root(arc_fs.inner().as_ref());
460    }
461
462    if let Some(overlay) = fs
463        .upcast_any_ref()
464        .downcast_ref::<OverlayFileSystem<ArcFileSystem, Vec<Arc<dyn FileSystem + Send + Sync>>>>()
465    {
466        return find_writable_root(overlay.primary());
467    }
468
469    if let Some(overlay) = fs
470        .upcast_any_ref()
471        .downcast_ref::<OverlayFileSystem<ArcFileSystem, [ArcFileSystem; 1]>>()
472    {
473        return find_writable_root(overlay.primary());
474    }
475
476    None
477}
478
479impl FileSystem for WasiFsRoot {
480    fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
481        self.root.readlink(path)
482    }
483
484    fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
485        self.root.read_dir(path)
486    }
487
488    fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
489        self.root.create_dir(path)
490    }
491
492    fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
493        self.root.create_symlink(source, target)
494    }
495
496    fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
497        self.root.remove_dir(path)
498    }
499
500    fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
501        let from = from.to_owned();
502        let to = to.to_owned();
503        let this = self.clone();
504        Box::pin(async move { this.root.rename(&from, &to).await })
505    }
506
507    fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
508        self.root.metadata(path)
509    }
510
511    fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
512        self.root.symlink_metadata(path)
513    }
514
515    fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
516        self.root.remove_file(path)
517    }
518
519    fn new_open_options(&self) -> OpenOptions<'_> {
520        self.root.new_open_options()
521    }
522}
523
524/// Warning, modifying these fields directly may cause invariants to break and
525/// should be considered unsafe.  These fields may be made private in a future release
526#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
527pub struct WasiFs {
528    //pub repo: Repo,
529    pub preopen_fds: RwLock<Vec<u32>>,
530    pub fd_map: RwLock<FdList>,
531    pub current_dir: Mutex<String>,
532    #[cfg_attr(feature = "enable-serde", serde(skip, default))]
533    pub root_fs: WasiFsRoot,
534    pub root_inode: InodeGuard,
535    pub has_unioned: Mutex<HashSet<PackageId>>,
536    ephemeral_symlinks: Arc<RwLock<HashMap<PathBuf, EphemeralSymlinkEntry>>>,
537
538    // TODO: remove
539    // using an atomic is a hack to enable customization after construction,
540    // but it shouldn't be necessary
541    // It should not be necessary at all.
542    is_wasix: AtomicBool,
543
544    // The preopens when this was initialized
545    pub(crate) init_preopens: Vec<PreopenedDir>,
546    // The virtual file system preopens when this was initialized
547    pub(crate) init_vfs_preopens: Vec<String>,
548}
549
550impl WasiFs {
551    fn writable_package_mount(
552        fs: Arc<dyn FileSystem + Send + Sync>,
553        limiter: Option<&DynFsMemoryLimiter>,
554    ) -> Arc<dyn FileSystem + Send + Sync> {
555        let upper = TmpFileSystem::new();
556        if let Some(limiter) = limiter {
557            upper.set_memory_limiter(limiter.clone());
558        }
559
560        Arc::new(OverlayFileSystem::new(upper, [ArcFileSystem::new(fs)]))
561    }
562
563    pub fn is_wasix(&self) -> bool {
564        // NOTE: this will only be set once very early in the instance lifetime,
565        // so Relaxed should be okay.
566        self.is_wasix.load(Ordering::Relaxed)
567    }
568
569    pub fn set_is_wasix(&self, is_wasix: bool) {
570        self.is_wasix.store(is_wasix, Ordering::SeqCst);
571    }
572
573    pub(crate) fn register_ephemeral_symlink(
574        &self,
575        full_path: PathBuf,
576        base_po_dir: WasiFd,
577        path_to_symlink: PathBuf,
578        relative_path: PathBuf,
579    ) {
580        let mut guard = self.ephemeral_symlinks.write().unwrap();
581        guard.insert(
582            normalize_virtual_symlink_key(&full_path),
583            EphemeralSymlinkEntry {
584                base_po_dir,
585                path_to_symlink: normalize_virtual_symlink_key(&path_to_symlink),
586                relative_path,
587            },
588        );
589    }
590
591    pub(crate) fn ephemeral_symlink_at(
592        &self,
593        full_path: &Path,
594    ) -> Option<(WasiFd, PathBuf, PathBuf)> {
595        let guard = self.ephemeral_symlinks.read().unwrap();
596        let entry = guard.get(&normalize_virtual_symlink_key(full_path))?;
597        Some((
598            entry.base_po_dir,
599            entry.path_to_symlink.clone(),
600            entry.relative_path.clone(),
601        ))
602    }
603
604    pub(crate) fn unregister_ephemeral_symlink(&self, full_path: &Path) {
605        let mut guard = self.ephemeral_symlinks.write().unwrap();
606        guard.remove(&normalize_virtual_symlink_key(full_path));
607    }
608
609    pub(crate) fn move_ephemeral_symlink(
610        &self,
611        old_full_path: &Path,
612        new_full_path: &Path,
613        base_po_dir: WasiFd,
614        path_to_symlink: PathBuf,
615        relative_path: PathBuf,
616    ) {
617        let old_key = normalize_virtual_symlink_key(old_full_path);
618        let new_key = normalize_virtual_symlink_key(new_full_path);
619
620        let mut guard = self.ephemeral_symlinks.write().unwrap();
621        guard.remove(&old_key);
622        guard.insert(
623            new_key,
624            EphemeralSymlinkEntry {
625                base_po_dir,
626                path_to_symlink: normalize_virtual_symlink_key(&path_to_symlink),
627                relative_path,
628            },
629        );
630    }
631
632    /// Forking the WasiState is used when either fork or vfork is called
633    pub fn fork(&self) -> Self {
634        Self {
635            preopen_fds: RwLock::new(self.preopen_fds.read().unwrap().clone()),
636            fd_map: RwLock::new(self.fd_map.read().unwrap().clone()),
637            current_dir: Mutex::new(self.current_dir.lock().unwrap().clone()),
638            is_wasix: AtomicBool::new(self.is_wasix.load(Ordering::Acquire)),
639            root_fs: self.root_fs.clone(),
640            root_inode: self.root_inode.clone(),
641            has_unioned: Mutex::new(self.has_unioned.lock().unwrap().clone()),
642            ephemeral_symlinks: self.ephemeral_symlinks.clone(),
643            init_preopens: self.init_preopens.clone(),
644            init_vfs_preopens: self.init_vfs_preopens.clone(),
645        }
646    }
647
648    /// Closes all the file handles.
649    pub async fn close_cloexec_fds(&self) {
650        let to_close = {
651            if let Ok(map) = self.fd_map.read() {
652                map.iter()
653                    .filter_map(|(k, v)| {
654                        if v.inner.fd_flags.contains(Fdflagsext::CLOEXEC)
655                            && !v.is_stdio
656                            && !v.inode.is_preopened
657                        {
658                            tracing::trace!(fd = %k, "Closing FD due to CLOEXEC flag");
659                            Some(k)
660                        } else {
661                            None
662                        }
663                    })
664                    .collect::<HashSet<_>>()
665            } else {
666                HashSet::new()
667            }
668        };
669
670        let _ = tokio::join!(async {
671            for fd in &to_close {
672                self.flush(*fd).await.ok();
673                self.close_fd(*fd).ok();
674            }
675        });
676
677        if let Ok(mut map) = self.fd_map.write() {
678            for fd in &to_close {
679                map.remove(*fd);
680            }
681        }
682    }
683
684    /// Closes all the file handles.
685    pub async fn close_all(&self) {
686        let mut to_close = {
687            if let Ok(map) = self.fd_map.read() {
688                map.keys().collect::<HashSet<_>>()
689            } else {
690                HashSet::new()
691            }
692        };
693        to_close.insert(__WASI_STDOUT_FILENO);
694        to_close.insert(__WASI_STDERR_FILENO);
695
696        let _ = tokio::join!(async {
697            for fd in to_close {
698                self.flush(fd).await.ok();
699                self.close_fd(fd).ok();
700            }
701        });
702
703        if let Ok(mut map) = self.fd_map.write() {
704            map.clear();
705        }
706    }
707
708    /// Will conditionally union the binary file system with this one
709    /// if it has not already been unioned
710    pub async fn conditional_union(
711        &self,
712        binary: &BinaryPackage,
713    ) -> Result<(), virtual_fs::FsError> {
714        let Some(package_mounts) = &binary.package_mounts else {
715            return Ok(());
716        };
717
718        let needs_to_be_unioned = self.has_unioned.lock().unwrap().insert(binary.id.clone());
719        if !needs_to_be_unioned {
720            return Ok(());
721        }
722
723        if let Some(root_layer) = &package_mounts.root_layer {
724            self.root_fs
725                .stack_root_filesystem(Self::writable_package_mount(
726                    root_layer.clone(),
727                    self.root_fs.memory_limiter(),
728                ))?;
729        }
730
731        for mount in &package_mounts.mounts {
732            self.root_fs.root().mount_with_source(
733                &mount.guest_path,
734                &mount.source_path,
735                Self::writable_package_mount(mount.fs.clone(), self.root_fs.memory_limiter()),
736            )?;
737        }
738
739        Ok(())
740    }
741
742    /// Created for the builder API. like `new` but with more information
743    pub(crate) fn new_with_preopen(
744        inodes: &WasiInodes,
745        preopens: &[PreopenedDir],
746        vfs_preopens: &[String],
747        fs_backing: WasiFsRoot,
748    ) -> Result<Self, String> {
749        let mut wasi_fs = Self::new_init(fs_backing, inodes, FS_ROOT_INO)?;
750        wasi_fs.init_preopens = preopens.to_vec();
751        wasi_fs.init_vfs_preopens = vfs_preopens.to_vec();
752        wasi_fs.create_preopens(inodes, false)?;
753        Ok(wasi_fs)
754    }
755
756    /// Converts a relative path into an absolute path
757    pub(crate) fn relative_path_to_absolute(&self, path: String) -> String {
758        if path.starts_with('/') {
759            return path;
760        }
761
762        let current_dir = self.current_dir.lock().unwrap();
763        format!("{}/{}", current_dir.trim_end_matches('/'), path)
764    }
765
766    /// Private helper function to init the filesystem, called in `new` and
767    /// `new_with_preopen`
768    fn new_init(
769        fs_backing: WasiFsRoot,
770        inodes: &WasiInodes,
771        st_ino: Inode,
772    ) -> Result<Self, String> {
773        debug!("Initializing WASI filesystem");
774
775        let stat = Filestat {
776            st_filetype: Filetype::Directory,
777            st_ino: st_ino.as_u64(),
778            ..Filestat::default()
779        };
780        let root_kind = Kind::Root {
781            entries: HashMap::new(),
782        };
783        let root_inode = inodes.add_inode_val(InodeVal {
784            stat: RwLock::new(stat),
785            is_preopened: true,
786            name: RwLock::new("/".into()),
787            kind: RwLock::new(root_kind),
788        });
789
790        let wasi_fs = Self {
791            preopen_fds: RwLock::new(vec![]),
792            fd_map: RwLock::new(FdList::new()),
793            current_dir: Mutex::new("/".to_string()),
794            is_wasix: AtomicBool::new(false),
795            root_fs: fs_backing,
796            root_inode,
797            has_unioned: Mutex::new(HashSet::new()),
798            ephemeral_symlinks: Arc::new(RwLock::new(HashMap::new())),
799            init_preopens: Default::default(),
800            init_vfs_preopens: Default::default(),
801        };
802        wasi_fs.create_stdin(inodes);
803        wasi_fs.create_stdout(inodes);
804        wasi_fs.create_stderr(inodes);
805        wasi_fs.create_rootfd()?;
806
807        Ok(wasi_fs)
808    }
809
810    /// This function is like create dir all, but it also opens it.
811    /// Function is unsafe because it may break invariants and hasn't been tested.
812    /// This is an experimental function and may be removed
813    ///
814    /// # Safety
815    /// - Virtual directories created with this function must not conflict with
816    ///   the standard operation of the WASI filesystem.  This is vague and
817    ///   unlikely in pratice.  [Join the discussion](https://github.com/wasmerio/wasmer/issues/1219)
818    ///   for what the newer, safer WASI FS APIs should look like.
819    #[allow(dead_code)]
820    #[allow(clippy::too_many_arguments)]
821    pub unsafe fn open_dir_all(
822        &mut self,
823        inodes: &WasiInodes,
824        base: WasiFd,
825        name: String,
826        rights: Rights,
827        rights_inheriting: Rights,
828        flags: Fdflags,
829        fd_flags: Fdflagsext,
830    ) -> Result<WasiFd, FsError> {
831        // TODO: check permissions here? probably not, but this should be
832        // an explicit choice, so justify it in a comment when we remove this one
833        let mut cur_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
834
835        let path: &Path = Path::new(&name);
836        //let n_components = path.components().count();
837        for c in path.components() {
838            let segment_name = c.as_os_str().to_string_lossy().to_string();
839            let guard = cur_inode.read();
840            match guard.deref() {
841                Kind::Dir { entries, .. } | Kind::Root { entries } => {
842                    if let Some(_entry) = entries.get(&segment_name) {
843                        // TODO: this should be fixed
844                        return Err(FsError::AlreadyExists);
845                    }
846
847                    let kind = Kind::Dir {
848                        parent: cur_inode.downgrade(),
849                        path: PathBuf::from(""),
850                        entries: HashMap::new(),
851                    };
852
853                    drop(guard);
854                    let inode = self.create_inode_with_default_stat(
855                        inodes,
856                        kind,
857                        false,
858                        segment_name.clone().into(),
859                    );
860
861                    // reborrow to insert
862                    {
863                        let mut guard = cur_inode.write();
864                        match guard.deref_mut() {
865                            Kind::Dir { entries, .. } | Kind::Root { entries } => {
866                                entries.insert(segment_name, inode.clone());
867                            }
868                            _ => unreachable!("Dir or Root became not Dir or Root"),
869                        }
870                    }
871                    cur_inode = inode;
872                }
873                _ => return Err(FsError::BaseNotDirectory),
874            }
875        }
876
877        // TODO: review open flags (read, write); they were added without consideration
878        self.create_fd(
879            rights,
880            rights_inheriting,
881            flags,
882            fd_flags,
883            Fd::READ | Fd::WRITE,
884            cur_inode,
885        )
886        .map_err(fs_error_from_wasi_err)
887    }
888
889    /// Opens a user-supplied file in the directory specified with the
890    /// name and flags given
891    // dead code because this is an API for external use
892    // TODO: is this used anywhere? Is it even sound?
893    #[allow(dead_code, clippy::too_many_arguments)]
894    pub fn open_file_at(
895        &mut self,
896        inodes: &WasiInodes,
897        base: WasiFd,
898        file: Box<dyn VirtualFile + Send + Sync + 'static>,
899        open_flags: u16,
900        name: String,
901        rights: Rights,
902        rights_inheriting: Rights,
903        flags: Fdflags,
904        fd_flags: Fdflagsext,
905    ) -> Result<WasiFd, FsError> {
906        // TODO: check permissions here? probably not, but this should be
907        // an explicit choice, so justify it in a comment when we remove this one
908        let base_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
909
910        let guard = base_inode.read();
911        match guard.deref() {
912            Kind::Dir { entries, .. } | Kind::Root { entries } => {
913                if let Some(_entry) = entries.get(&name) {
914                    // TODO: eventually change the logic here to allow overwrites
915                    return Err(FsError::AlreadyExists);
916                }
917
918                let kind = Kind::File {
919                    handle: Some(Arc::new(RwLock::new(file))),
920                    path: PathBuf::from(""),
921                    fd: None,
922                };
923
924                drop(guard);
925                let inode = self
926                    .create_inode(inodes, kind, false, name.clone())
927                    .map_err(|_| FsError::IOError)?;
928
929                {
930                    let mut guard = base_inode.write();
931                    match guard.deref_mut() {
932                        Kind::Dir { entries, .. } | Kind::Root { entries } => {
933                            entries.insert(name, inode.clone());
934                        }
935                        _ => unreachable!("Dir or Root became not Dir or Root"),
936                    }
937                }
938
939                // Here, we clone the inode so we can use it to overwrite the fd field below.
940                let real_fd = self
941                    .create_fd(
942                        rights,
943                        rights_inheriting,
944                        flags,
945                        fd_flags,
946                        open_flags,
947                        inode.clone(),
948                    )
949                    .map_err(fs_error_from_wasi_err)?;
950
951                {
952                    let mut guard = inode.kind.write().unwrap();
953                    match guard.deref_mut() {
954                        Kind::File { fd, .. } => {
955                            *fd = Some(real_fd);
956                        }
957                        _ => unreachable!("We just created a Kind::File"),
958                    }
959                }
960
961                Ok(real_fd)
962            }
963            _ => Err(FsError::BaseNotDirectory),
964        }
965    }
966
967    /// Change the backing of a given file descriptor
968    /// Returns the old backing
969    /// TODO: add examples
970    #[allow(dead_code)]
971    pub fn swap_file(
972        &self,
973        fd: WasiFd,
974        mut file: Box<dyn VirtualFile + Send + Sync + 'static>,
975    ) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
976        match fd {
977            __WASI_STDIN_FILENO => {
978                let mut target = WasiInodes::stdin_mut(&self.fd_map)?;
979                Ok(Some(target.swap(file)))
980            }
981            __WASI_STDOUT_FILENO => {
982                let mut target = WasiInodes::stdout_mut(&self.fd_map)?;
983                Ok(Some(target.swap(file)))
984            }
985            __WASI_STDERR_FILENO => {
986                let mut target = WasiInodes::stderr_mut(&self.fd_map)?;
987                Ok(Some(target.swap(file)))
988            }
989            _ => {
990                let base_inode = self.get_fd_inode(fd).map_err(fs_error_from_wasi_err)?;
991                {
992                    // happy path
993                    let guard = base_inode.read();
994                    match guard.deref() {
995                        Kind::File { handle, .. } => {
996                            if let Some(handle) = handle {
997                                let mut handle = handle.write().unwrap();
998                                std::mem::swap(handle.deref_mut(), &mut file);
999                                return Ok(Some(file));
1000                            }
1001                        }
1002                        _ => return Err(FsError::NotAFile),
1003                    }
1004                }
1005                // slow path
1006                let mut guard = base_inode.write();
1007                match guard.deref_mut() {
1008                    Kind::File { handle, .. } => {
1009                        if let Some(handle) = handle {
1010                            let mut handle = handle.write().unwrap();
1011                            std::mem::swap(handle.deref_mut(), &mut file);
1012                            Ok(Some(file))
1013                        } else {
1014                            handle.replace(Arc::new(RwLock::new(file)));
1015                            Ok(None)
1016                        }
1017                    }
1018                    _ => Err(FsError::NotAFile),
1019                }
1020            }
1021        }
1022    }
1023
1024    /// refresh size from filesystem
1025    pub fn filestat_resync_size(&self, fd: WasiFd) -> Result<Filesize, Errno> {
1026        let inode = self.get_fd_inode(fd)?;
1027        let mut guard = inode.write();
1028        match guard.deref_mut() {
1029            Kind::File { handle, .. } => {
1030                if let Some(h) = handle {
1031                    let h = h.read().unwrap();
1032                    let new_size = h.size();
1033                    drop(h);
1034                    drop(guard);
1035
1036                    inode.stat.write().unwrap().st_size = new_size;
1037                    Ok(new_size as Filesize)
1038                } else {
1039                    Err(Errno::Badf)
1040                }
1041            }
1042            Kind::Dir { .. } | Kind::Root { .. } => Err(Errno::Isdir),
1043            _ => Err(Errno::Inval),
1044        }
1045    }
1046
1047    /// Changes the current directory
1048    pub fn set_current_dir(&self, path: &str) {
1049        let mut guard = self.current_dir.lock().unwrap();
1050        *guard = path.to_string();
1051    }
1052
1053    /// Gets the current directory
1054    pub fn get_current_dir(
1055        &self,
1056        inodes: &WasiInodes,
1057        base: WasiFd,
1058    ) -> Result<(InodeGuard, String), Errno> {
1059        self.get_current_dir_inner(inodes, base, 0)
1060    }
1061
1062    pub(crate) fn get_current_dir_inner(
1063        &self,
1064        inodes: &WasiInodes,
1065        base: WasiFd,
1066        symlink_count: u32,
1067    ) -> Result<(InodeGuard, String), Errno> {
1068        let current_dir = {
1069            let guard = self.current_dir.lock().unwrap();
1070            guard.clone()
1071        };
1072        let cur_inode = self.get_fd_inode(base)?;
1073        let inode = self.get_inode_at_path_inner(
1074            inodes,
1075            cur_inode,
1076            current_dir.as_str(),
1077            symlink_count,
1078            true,
1079        )?;
1080        Ok((inode, current_dir))
1081    }
1082
1083    /// Internal part of the core path resolution function which implements path
1084    /// traversal logic such as resolving relative path segments (such as
1085    /// `.` and `..`) and resolving symlinks (while preventing infinite
1086    /// loops/stack overflows).
1087    ///
1088    /// TODO: expand upon exactly what the state of the returned value is,
1089    /// explaining lazy-loading from the real file system and synchronizing
1090    /// between them.
1091    ///
1092    /// This is where a lot of the magic happens, be very careful when editing
1093    /// this code.
1094    ///
1095    /// TODO: write more tests for this code
1096    fn get_inode_at_path_inner(
1097        &self,
1098        inodes: &WasiInodes,
1099        mut cur_inode: InodeGuard,
1100        path_str: &str,
1101        mut symlink_count: u32,
1102        follow_symlinks: bool,
1103    ) -> Result<InodeGuard, Errno> {
1104        if symlink_count > MAX_SYMLINKS {
1105            return Err(Errno::Mlink);
1106        }
1107
1108        // Absolute root paths should resolve to the mounted "/" inode when present.
1109        // This keeps "/" behavior aligned with historical path traversal semantics.
1110        if path_str == "/" {
1111            let guard = cur_inode.read();
1112            if let Kind::Root { entries } = guard.deref()
1113                && let Some(root_entry) = entries.get("/")
1114            {
1115                return Ok(root_entry.clone());
1116            }
1117        }
1118
1119        let path: &Path = Path::new(path_str);
1120        let n_components = path.components().count();
1121
1122        // TODO: rights checks
1123        'path_iter: for (i, component) in path.components().enumerate() {
1124            // Since we're resolving the path against the given inode, we want to
1125            // assume '/a/b' to be the same as `a/b` relative to the inode, so
1126            // we skip over the RootDir component.
1127            if matches!(component, Component::RootDir) {
1128                continue;
1129            }
1130
1131            // used to terminate symlink resolution properly
1132            let last_component = i + 1 == n_components;
1133            // for each component traverse file structure
1134            // loading inodes as necessary
1135            'symlink_resolution: while symlink_count < MAX_SYMLINKS {
1136                let processing_cur_inode = cur_inode.clone();
1137                let mut guard = processing_cur_inode.write();
1138                match guard.deref_mut() {
1139                    Kind::Buffer { .. } => unimplemented!("state::get_inode_at_path for buffers"),
1140                    Kind::Dir {
1141                        entries,
1142                        path,
1143                        parent,
1144                        ..
1145                    } => {
1146                        match component.as_os_str().to_string_lossy().borrow() {
1147                            ".." => {
1148                                if let Some(p) = parent.upgrade() {
1149                                    cur_inode = p;
1150                                    continue 'path_iter;
1151                                } else {
1152                                    return Err(Errno::Access);
1153                                }
1154                            }
1155                            "." => continue 'path_iter,
1156                            _ => (),
1157                        }
1158                        // used for full resolution of symlinks
1159                        let mut loop_for_symlink = false;
1160                        if let Some(entry) =
1161                            entries.get(component.as_os_str().to_string_lossy().as_ref())
1162                        {
1163                            cur_inode = entry.clone();
1164                        } else {
1165                            let file = {
1166                                let mut cd = path.clone();
1167                                cd.push(component);
1168                                cd
1169                            };
1170                            // we want to insert newly opened dirs and files, but not transient symlinks
1171                            // TODO: explain why (think about this deeply when well rested)
1172                            let should_insert;
1173
1174                            let kind = if let Some((base_po_dir, path_to_symlink, relative_path)) =
1175                                self.ephemeral_symlink_at(&file)
1176                            {
1177                                // Ephemeral symlinks are transient records;
1178                                // we resolve them but don't cache them as dir entries.
1179                                should_insert = false;
1180                                loop_for_symlink = true;
1181                                symlink_count += 1;
1182                                Kind::Symlink {
1183                                    base_po_dir,
1184                                    path_to_symlink,
1185                                    relative_path,
1186                                }
1187                            } else {
1188                                let metadata = self
1189                                    .root_fs
1190                                    .symlink_metadata(&file)
1191                                    .ok()
1192                                    .ok_or(Errno::Noent)?;
1193                                let file_type = metadata.file_type();
1194                                if file_type.is_dir() {
1195                                    should_insert = true;
1196                                    // load DIR
1197                                    Kind::Dir {
1198                                        parent: cur_inode.downgrade(),
1199                                        path: file.clone(),
1200                                        entries: Default::default(),
1201                                    }
1202                                } else if file_type.is_file() {
1203                                    should_insert = true;
1204                                    // load file
1205                                    Kind::File {
1206                                        handle: None,
1207                                        path: file.clone(),
1208                                        fd: None,
1209                                    }
1210                                } else if file_type.is_symlink() {
1211                                    should_insert = false;
1212                                    let link_value =
1213                                        self.root_fs.readlink(&file).ok().ok_or(Errno::Noent)?;
1214                                    debug!("attempting to decompose path {:?}", link_value);
1215                                    let (pre_open_dir_fd, path_to_symlink) =
1216                                        self.path_into_pre_open_and_relative_path(&file)?;
1217                                    loop_for_symlink = true;
1218                                    symlink_count += 1;
1219                                    Kind::Symlink {
1220                                        base_po_dir: pre_open_dir_fd,
1221                                        path_to_symlink: path_to_symlink.to_owned(),
1222                                        relative_path: link_value,
1223                                    }
1224                                } else {
1225                                    #[cfg(unix)]
1226                                    {
1227                                        //use std::os::unix::fs::FileTypeExt;
1228                                        let file_type: Filetype = if file_type.is_char_device() {
1229                                            Filetype::CharacterDevice
1230                                        } else if file_type.is_block_device() {
1231                                            Filetype::BlockDevice
1232                                        } else if file_type.is_fifo() {
1233                                            // FIFO doesn't seem to fit any other type, so unknown
1234                                            Filetype::Unknown
1235                                        } else if file_type.is_socket() {
1236                                            // TODO: how do we know if it's a `SocketStream` or
1237                                            // a `SocketDgram`?
1238                                            Filetype::SocketStream
1239                                        } else {
1240                                            unimplemented!(
1241                                                "state::get_inode_at_path unknown file type: not file, directory, symlink, char device, block device, fifo, or socket"
1242                                            );
1243                                        };
1244
1245                                        let kind = Kind::File {
1246                                            handle: None,
1247                                            path: file.clone(),
1248                                            fd: None,
1249                                        };
1250                                        drop(guard);
1251                                        let new_inode = self.create_inode_with_stat(
1252                                            inodes,
1253                                            kind,
1254                                            false,
1255                                            file.to_string_lossy().to_string().into(),
1256                                            Filestat {
1257                                                st_filetype: file_type,
1258                                                st_ino: Inode::from_path(path_str).as_u64(),
1259                                                st_size: metadata.len(),
1260                                                st_ctim: metadata.created(),
1261                                                st_mtim: metadata.modified(),
1262                                                st_atim: metadata.accessed(),
1263                                                ..Filestat::default()
1264                                            },
1265                                        );
1266
1267                                        let mut guard = cur_inode.write();
1268                                        if let Kind::Dir { entries, .. } = guard.deref_mut() {
1269                                            entries.insert(
1270                                                component.as_os_str().to_string_lossy().to_string(),
1271                                                new_inode.clone(),
1272                                            );
1273                                        } else {
1274                                            unreachable!(
1275                                                "Attempted to insert special device into non-directory"
1276                                            );
1277                                        }
1278                                        // perhaps just continue with symlink resolution and return at the end
1279                                        return Ok(new_inode);
1280                                    }
1281                                    #[cfg(not(unix))]
1282                                    unimplemented!(
1283                                        "state::get_inode_at_path unknown file type: not file, directory, or symlink"
1284                                    );
1285                                }
1286                            };
1287                            drop(guard);
1288
1289                            let new_inode = self.create_inode(
1290                                inodes,
1291                                kind,
1292                                false,
1293                                file.to_string_lossy().to_string(),
1294                            )?;
1295                            if should_insert {
1296                                let mut guard = processing_cur_inode.write();
1297                                if let Kind::Dir { entries, .. } = guard.deref_mut() {
1298                                    entries.insert(
1299                                        component.as_os_str().to_string_lossy().to_string(),
1300                                        new_inode.clone(),
1301                                    );
1302                                }
1303                            }
1304                            cur_inode = new_inode;
1305
1306                            if loop_for_symlink && follow_symlinks {
1307                                debug!("Following symlink to {:?}", cur_inode);
1308                                continue 'symlink_resolution;
1309                            }
1310                        }
1311                    }
1312                    Kind::Root { entries } => {
1313                        match component {
1314                            // the root's parent is the root
1315                            Component::ParentDir => continue 'path_iter,
1316                            // the root's current directory is the root
1317                            Component::CurDir => continue 'path_iter,
1318                            _ => {}
1319                        }
1320
1321                        let component = component.as_os_str().to_string_lossy();
1322
1323                        if let Some(entry) = entries.get(component.as_ref()) {
1324                            cur_inode = entry.clone();
1325                        } else if let Some(root) = entries.get(&"/".to_string()) {
1326                            cur_inode = root.clone();
1327                            continue 'symlink_resolution;
1328                        } else {
1329                            // Root is not capable of having something other then preopenned folders
1330                            return Err(Errno::Notcapable);
1331                        }
1332                    }
1333                    Kind::File { .. }
1334                    | Kind::Socket { .. }
1335                    | Kind::PipeRx { .. }
1336                    | Kind::PipeTx { .. }
1337                    | Kind::DuplexPipe { .. }
1338                    | Kind::EventNotifications { .. }
1339                    | Kind::Epoll { .. } => {
1340                        return Err(Errno::Notdir);
1341                    }
1342                    Kind::Symlink {
1343                        base_po_dir,
1344                        path_to_symlink,
1345                        relative_path,
1346                    } => {
1347                        let (new_base_inode, new_path) = if relative_path.is_absolute() {
1348                            // Absolute symlink targets must resolve from the virtual root
1349                            // rather than from the directory containing the symlink.
1350                            (
1351                                self.get_fd_inode(VIRTUAL_ROOT_FD)?,
1352                                relative_path.to_string_lossy().to_string(),
1353                            )
1354                        } else {
1355                            let new_base_dir = *base_po_dir;
1356                            let new_base_inode = self.get_fd_inode(new_base_dir)?;
1357                            // allocate to reborrow mutabily to recur
1358                            let new_path = {
1359                                /*if let Kind::Root { .. } = self.inodes[base_po_dir].kind {
1360                                    assert!(false, "symlinks should never be relative to the root");
1361                                }*/
1362                                let mut base = path_to_symlink.clone();
1363                                // remove the symlink file itself from the path, leaving just the path from the base
1364                                // to the dir containing the symlink
1365                                base.pop();
1366                                base.push(relative_path);
1367                                base.to_string_lossy().to_string()
1368                            };
1369                            (new_base_inode, new_path)
1370                        };
1371                        debug!("Following symlink recursively");
1372                        drop(guard);
1373                        let symlink_inode = self.get_inode_at_path_inner(
1374                            inodes,
1375                            new_base_inode,
1376                            &new_path,
1377                            symlink_count + 1,
1378                            follow_symlinks,
1379                        )?;
1380                        cur_inode = symlink_inode;
1381                        // if we're at the very end and we found a file, then we're done
1382                        // TODO: figure out if this should also happen for directories?
1383                        let guard = cur_inode.read();
1384                        if let Kind::File { .. } = guard.deref() {
1385                            // check if on last step
1386                            if last_component {
1387                                break 'symlink_resolution;
1388                            }
1389                        }
1390                        continue 'symlink_resolution;
1391                    }
1392                }
1393                break 'symlink_resolution;
1394            }
1395        }
1396
1397        Ok(cur_inode)
1398    }
1399
1400    /// Finds the preopened directory that is the "best match" for the given path and
1401    /// returns a path relative to this preopened directory.
1402    ///
1403    /// The "best match" is the preopened directory that has the longest prefix of the
1404    /// given path. For example, given preopened directories [`a`, `a/b`, `a/c`] and
1405    /// the path `a/b/c/file`, we will return the fd corresponding to the preopened
1406    /// directory, `a/b` and the relative path `c/file`.
1407    ///
1408    /// In the case of a tie, the later preopened fd is preferred.
1409    fn path_into_pre_open_and_relative_path<'path>(
1410        &self,
1411        path: &'path Path,
1412    ) -> Result<(WasiFd, &'path Path), Errno> {
1413        enum BaseFdAndRelPath<'a> {
1414            None,
1415            BestMatch {
1416                fd: WasiFd,
1417                rel_path: &'a Path,
1418                max_seen: usize,
1419            },
1420        }
1421
1422        impl BaseFdAndRelPath<'_> {
1423            const fn max_seen(&self) -> usize {
1424                match self {
1425                    Self::None => 0,
1426                    Self::BestMatch { max_seen, .. } => *max_seen,
1427                }
1428            }
1429        }
1430        let mut res = BaseFdAndRelPath::None;
1431        // for each preopened directory
1432        let preopen_fds = self.preopen_fds.read().unwrap();
1433        for po_fd in preopen_fds.deref() {
1434            let po_inode = self
1435                .fd_map
1436                .read()
1437                .unwrap()
1438                .get(*po_fd)
1439                .unwrap()
1440                .inode
1441                .clone();
1442            let guard = po_inode.read();
1443            let po_path = match guard.deref() {
1444                Kind::Dir { path, .. } => &**path,
1445                Kind::Root { .. } => Path::new("/"),
1446                _ => unreachable!("Preopened FD that's not a directory or the root"),
1447            };
1448            // stem path based on it
1449            if let Ok(stripped_path) = path.strip_prefix(po_path) {
1450                // find the max
1451                let new_prefix_len = po_path.as_os_str().len();
1452                // we use >= to favor later preopens because we iterate in order
1453                // whereas WASI libc iterates in reverse to get this behavior.
1454                if new_prefix_len >= res.max_seen() {
1455                    res = BaseFdAndRelPath::BestMatch {
1456                        fd: *po_fd,
1457                        rel_path: stripped_path,
1458                        max_seen: new_prefix_len,
1459                    };
1460                }
1461            }
1462        }
1463        match res {
1464            // this error may not make sense depending on where it's called
1465            BaseFdAndRelPath::None => Err(Errno::Inval),
1466            BaseFdAndRelPath::BestMatch { fd, rel_path, .. } => Ok((fd, rel_path)),
1467        }
1468    }
1469
1470    pub(crate) fn path_into_pre_open_and_relative_path_owned(
1471        &self,
1472        path: &Path,
1473    ) -> Result<(WasiFd, PathBuf), Errno> {
1474        let (fd, rel_path) = self.path_into_pre_open_and_relative_path(path)?;
1475        Ok((fd, rel_path.to_owned()))
1476    }
1477
1478    /// gets a host file from a base directory and a path
1479    /// this function ensures the fs remains sandboxed
1480    // NOTE: follow symlinks is super weird right now
1481    // even if it's false, it still follows symlinks, just not the last
1482    // symlink so
1483    // This will be resolved when we have tests asserting the correct behavior
1484    pub(crate) fn get_inode_at_path(
1485        &self,
1486        inodes: &WasiInodes,
1487        base: WasiFd,
1488        path: &str,
1489        follow_symlinks: bool,
1490    ) -> Result<InodeGuard, Errno> {
1491        let base_inode = self.get_fd_inode(base)?;
1492        self.get_inode_at_path_inner(inodes, base_inode, path, 0, follow_symlinks)
1493    }
1494
1495    /// Returns the parent Dir or Root that the file at a given path is in and the file name
1496    /// stripped off
1497    pub(crate) fn get_parent_inode_at_path(
1498        &self,
1499        inodes: &WasiInodes,
1500        base: WasiFd,
1501        path: &Path,
1502        follow_symlinks: bool,
1503    ) -> Result<(InodeGuard, String), Errno> {
1504        let mut parent_dir = std::path::PathBuf::new();
1505        let mut components = path.components().rev();
1506        let new_entity_name = components
1507            .next()
1508            .ok_or(Errno::Inval)?
1509            .as_os_str()
1510            .to_string_lossy()
1511            .to_string();
1512        for comp in components.rev() {
1513            parent_dir.push(comp);
1514        }
1515        self.get_inode_at_path(inodes, base, &parent_dir.to_string_lossy(), follow_symlinks)
1516            .map(|v| (v, new_entity_name))
1517    }
1518
1519    pub fn get_fd(&self, fd: WasiFd) -> Result<Fd, Errno> {
1520        let ret = self
1521            .fd_map
1522            .read()
1523            .unwrap()
1524            .get(fd)
1525            .ok_or(Errno::Badf)
1526            .cloned();
1527
1528        if ret.is_err() && fd == VIRTUAL_ROOT_FD {
1529            Ok(Fd {
1530                inner: FdInner {
1531                    rights: ALL_RIGHTS,
1532                    rights_inheriting: ALL_RIGHTS,
1533                    flags: Fdflags::empty(),
1534                    offset: Arc::new(AtomicU64::new(0)),
1535                    fd_flags: Fdflagsext::empty(),
1536                },
1537                open_flags: 0,
1538                inode: self.root_inode.clone(),
1539                is_stdio: false,
1540            })
1541        } else {
1542            ret
1543        }
1544    }
1545
1546    pub fn get_fd_inode(&self, fd: WasiFd) -> Result<InodeGuard, Errno> {
1547        // see `VIRTUAL_ROOT_FD` for details as to why this exists
1548        if fd == VIRTUAL_ROOT_FD {
1549            return Ok(self.root_inode.clone());
1550        }
1551        self.fd_map
1552            .read()
1553            .unwrap()
1554            .get(fd)
1555            .ok_or(Errno::Badf)
1556            .map(|a| a.inode.clone())
1557    }
1558
1559    pub fn filestat_fd(&self, fd: WasiFd) -> Result<Filestat, Errno> {
1560        let inode = self.get_fd_inode(fd)?;
1561        let guard = inode.stat.read().unwrap();
1562        Ok(*guard.deref())
1563    }
1564
1565    pub fn fdstat(&self, fd: WasiFd) -> Result<Fdstat, Errno> {
1566        match fd {
1567            __WASI_STDIN_FILENO => {
1568                return Ok(Fdstat {
1569                    fs_filetype: Filetype::CharacterDevice,
1570                    fs_flags: Fdflags::empty(),
1571                    fs_rights_base: STDIN_DEFAULT_RIGHTS,
1572                    fs_rights_inheriting: Rights::empty(),
1573                });
1574            }
1575            __WASI_STDOUT_FILENO => {
1576                return Ok(Fdstat {
1577                    fs_filetype: Filetype::CharacterDevice,
1578                    fs_flags: Fdflags::APPEND,
1579                    fs_rights_base: STDOUT_DEFAULT_RIGHTS,
1580                    fs_rights_inheriting: Rights::empty(),
1581                });
1582            }
1583            __WASI_STDERR_FILENO => {
1584                return Ok(Fdstat {
1585                    fs_filetype: Filetype::CharacterDevice,
1586                    fs_flags: Fdflags::APPEND,
1587                    fs_rights_base: STDERR_DEFAULT_RIGHTS,
1588                    fs_rights_inheriting: Rights::empty(),
1589                });
1590            }
1591            VIRTUAL_ROOT_FD => {
1592                return Ok(Fdstat {
1593                    fs_filetype: Filetype::Directory,
1594                    fs_flags: Fdflags::empty(),
1595                    // TODO: fix this
1596                    fs_rights_base: ALL_RIGHTS,
1597                    fs_rights_inheriting: ALL_RIGHTS,
1598                });
1599            }
1600            _ => (),
1601        }
1602        let fd = self.get_fd(fd)?;
1603
1604        let guard = fd.inode.read();
1605        let deref = guard.deref();
1606        Ok(Fdstat {
1607            fs_filetype: match deref {
1608                Kind::File { .. } => Filetype::RegularFile,
1609                Kind::Dir { .. } => Filetype::Directory,
1610                Kind::Symlink { .. } => Filetype::SymbolicLink,
1611                Kind::Socket { socket } => match &socket.inner.protected.read().unwrap().kind {
1612                    InodeSocketKind::TcpStream { .. } => Filetype::SocketStream,
1613                    InodeSocketKind::Raw { .. } => Filetype::SocketRaw,
1614                    InodeSocketKind::PreSocket { props, .. } => match props.ty {
1615                        Socktype::Stream => Filetype::SocketStream,
1616                        Socktype::Dgram => Filetype::SocketDgram,
1617                        Socktype::Raw => Filetype::SocketRaw,
1618                        Socktype::Seqpacket => Filetype::SocketSeqpacket,
1619                        _ => Filetype::Unknown,
1620                    },
1621                    _ => Filetype::Unknown,
1622                },
1623                _ => Filetype::Unknown,
1624            },
1625            fs_flags: fd.inner.flags,
1626            fs_rights_base: fd.inner.rights,
1627            fs_rights_inheriting: fd.inner.rights_inheriting, // TODO(lachlan): Is this right?
1628        })
1629    }
1630
1631    pub fn prestat_fd(&self, fd: WasiFd) -> Result<Prestat, Errno> {
1632        let inode = self.get_fd_inode(fd)?;
1633        //trace!("in prestat_fd {:?}", self.get_fd(fd)?);
1634
1635        if inode.is_preopened {
1636            Ok(self.prestat_fd_inner(inode.deref()))
1637        } else {
1638            Err(Errno::Badf)
1639        }
1640    }
1641
1642    pub(crate) fn prestat_fd_inner(&self, inode_val: &InodeVal) -> Prestat {
1643        Prestat {
1644            pr_type: Preopentype::Dir,
1645            u: PrestatEnum::Dir {
1646                // WASI spec: pr_name_len is the length of the path string, NOT including null terminator
1647                pr_name_len: inode_val.name.read().unwrap().len() as u32,
1648            }
1649            .untagged(),
1650        }
1651    }
1652
1653    #[allow(clippy::await_holding_lock)]
1654    pub async fn flush(&self, fd: WasiFd) -> Result<(), Errno> {
1655        match fd {
1656            __WASI_STDIN_FILENO => (),
1657            __WASI_STDOUT_FILENO => {
1658                let mut file =
1659                    WasiInodes::stdout_mut(&self.fd_map).map_err(fs_error_into_wasi_err)?;
1660                file.flush().await.map_err(map_io_err)?
1661            }
1662            __WASI_STDERR_FILENO => {
1663                let mut file =
1664                    WasiInodes::stderr_mut(&self.fd_map).map_err(fs_error_into_wasi_err)?;
1665                file.flush().await.map_err(map_io_err)?
1666            }
1667            _ => {
1668                let fd = self.get_fd(fd)?;
1669                if !fd.inner.rights.contains(Rights::FD_DATASYNC) {
1670                    return Err(Errno::Access);
1671                }
1672
1673                let file = {
1674                    let guard = fd.inode.read();
1675                    match guard.deref() {
1676                        Kind::File {
1677                            handle: Some(file), ..
1678                        } => file.clone(),
1679                        // TODO: verify this behavior
1680                        Kind::Dir { .. } => return Err(Errno::Isdir),
1681                        Kind::Buffer { .. } => return Ok(()),
1682                        _ => return Err(Errno::Io),
1683                    }
1684                };
1685                drop(fd);
1686
1687                struct FlushPoller {
1688                    file: Arc<RwLock<Box<dyn VirtualFile + Send + Sync>>>,
1689                }
1690                impl Future for FlushPoller {
1691                    type Output = Result<(), Errno>;
1692                    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1693                        let mut file = self.file.write().unwrap();
1694                        Pin::new(file.as_mut())
1695                            .poll_flush(cx)
1696                            .map_err(|_| Errno::Io)
1697                    }
1698                }
1699                FlushPoller { file }.await?;
1700            }
1701        }
1702        Ok(())
1703    }
1704
1705    /// Creates an inode and inserts it given a Kind and some extra data
1706    pub(crate) fn create_inode(
1707        &self,
1708        inodes: &WasiInodes,
1709        kind: Kind,
1710        is_preopened: bool,
1711        name: String,
1712    ) -> Result<InodeGuard, Errno> {
1713        let stat = self.get_stat_for_kind(&kind)?;
1714        Ok(self.create_inode_with_stat(inodes, kind, is_preopened, name.into(), stat))
1715    }
1716
1717    /// Creates an inode and inserts it given a Kind, does not assume the file exists.
1718    pub(crate) fn create_inode_with_default_stat(
1719        &self,
1720        inodes: &WasiInodes,
1721        kind: Kind,
1722        is_preopened: bool,
1723        name: Cow<'static, str>,
1724    ) -> InodeGuard {
1725        let stat = Filestat::default();
1726        self.create_inode_with_stat(inodes, kind, is_preopened, name, stat)
1727    }
1728
1729    /// Creates an inode with the given filestat and inserts it.
1730    pub(crate) fn create_inode_with_stat(
1731        &self,
1732        inodes: &WasiInodes,
1733        kind: Kind,
1734        is_preopened: bool,
1735        name: Cow<'static, str>,
1736        mut stat: Filestat,
1737    ) -> InodeGuard {
1738        match &kind {
1739            Kind::File {
1740                handle: Some(handle),
1741                ..
1742            } => {
1743                let guard = handle.read().unwrap();
1744                stat.st_size = guard.size();
1745            }
1746            Kind::Buffer { buffer } => {
1747                stat.st_size = buffer.len() as u64;
1748            }
1749            _ => {}
1750        }
1751
1752        let inode_key: Cow<'_, str> = match &kind {
1753            Kind::File { path, .. } | Kind::Dir { path, .. } => {
1754                let path_str = path.to_string_lossy();
1755                if path_str.is_empty() {
1756                    Cow::Borrowed(name.as_ref())
1757                } else {
1758                    path_str
1759                }
1760            }
1761            Kind::Symlink {
1762                base_po_dir,
1763                path_to_symlink,
1764                ..
1765            } => {
1766                let path_str = path_to_symlink.to_string_lossy();
1767                if path_str.is_empty() {
1768                    Cow::Owned(format!("{base_po_dir}:{}", name.as_ref()))
1769                } else {
1770                    Cow::Owned(format!("{base_po_dir}:{path_str}"))
1771                }
1772            }
1773            _ => Cow::Borrowed(name.as_ref()),
1774        };
1775
1776        let st_ino = Inode::from_path(&inode_key);
1777        stat.st_ino = st_ino.as_u64();
1778
1779        inodes.add_inode_val(InodeVal {
1780            stat: RwLock::new(stat),
1781            is_preopened,
1782            name: RwLock::new(name),
1783            kind: RwLock::new(kind),
1784        })
1785    }
1786
1787    pub fn create_fd(
1788        &self,
1789        rights: Rights,
1790        rights_inheriting: Rights,
1791        fs_flags: Fdflags,
1792        fd_flags: Fdflagsext,
1793        open_flags: u16,
1794        inode: InodeGuard,
1795    ) -> Result<WasiFd, Errno> {
1796        self.create_fd_ext(
1797            rights,
1798            rights_inheriting,
1799            fs_flags,
1800            fd_flags,
1801            open_flags,
1802            inode,
1803            None,
1804            false,
1805        )
1806    }
1807
1808    #[allow(clippy::too_many_arguments)]
1809    pub fn with_fd(
1810        &self,
1811        rights: Rights,
1812        rights_inheriting: Rights,
1813        fs_flags: Fdflags,
1814        fd_flags: Fdflagsext,
1815        open_flags: u16,
1816        inode: InodeGuard,
1817        idx: WasiFd,
1818    ) -> Result<(), Errno> {
1819        self.create_fd_ext(
1820            rights,
1821            rights_inheriting,
1822            fs_flags,
1823            fd_flags,
1824            open_flags,
1825            inode,
1826            Some(idx),
1827            true,
1828        )?;
1829        Ok(())
1830    }
1831
1832    #[allow(clippy::too_many_arguments)]
1833    pub fn create_fd_ext(
1834        &self,
1835        rights: Rights,
1836        rights_inheriting: Rights,
1837        fs_flags: Fdflags,
1838        fd_flags: Fdflagsext,
1839        open_flags: u16,
1840        inode: InodeGuard,
1841        idx: Option<WasiFd>,
1842        exclusive: bool,
1843    ) -> Result<WasiFd, Errno> {
1844        let is_stdio = matches!(
1845            idx,
1846            Some(__WASI_STDIN_FILENO) | Some(__WASI_STDOUT_FILENO) | Some(__WASI_STDERR_FILENO)
1847        );
1848        let fd = Fd {
1849            inner: FdInner {
1850                rights,
1851                rights_inheriting,
1852                flags: fs_flags,
1853                offset: Arc::new(AtomicU64::new(0)),
1854                fd_flags,
1855            },
1856            open_flags,
1857            inode,
1858            is_stdio,
1859        };
1860
1861        let mut guard = self.fd_map.write().unwrap();
1862
1863        match idx {
1864            Some(idx) => {
1865                if guard.insert(exclusive, idx, fd) {
1866                    Ok(idx)
1867                } else {
1868                    Err(Errno::Exist)
1869                }
1870            }
1871            None => Ok(guard.insert_first_free(fd)),
1872        }
1873    }
1874
1875    pub fn clone_fd(&self, fd: WasiFd) -> Result<WasiFd, Errno> {
1876        self.clone_fd_ext(fd, 0, None)
1877    }
1878
1879    pub fn clone_fd_ext(
1880        &self,
1881        fd: WasiFd,
1882        min_result_fd: WasiFd,
1883        cloexec: Option<bool>,
1884    ) -> Result<WasiFd, Errno> {
1885        let fd = self.get_fd(fd)?;
1886        Ok(self.fd_map.write().unwrap().insert_first_free_after(
1887            Fd {
1888                inner: FdInner {
1889                    rights: fd.inner.rights,
1890                    rights_inheriting: fd.inner.rights_inheriting,
1891                    flags: fd.inner.flags,
1892                    offset: fd.inner.offset.clone(),
1893                    fd_flags: match cloexec {
1894                        None => fd.inner.fd_flags,
1895                        Some(cloexec) => {
1896                            let mut f = fd.inner.fd_flags;
1897                            f.set(Fdflagsext::CLOEXEC, cloexec);
1898                            f
1899                        }
1900                    },
1901                },
1902                open_flags: fd.open_flags,
1903                inode: fd.inode,
1904                is_stdio: fd.is_stdio,
1905            },
1906            min_result_fd,
1907        ))
1908    }
1909
1910    /// Low level function to remove an inode, that is it deletes the WASI FS's
1911    /// knowledge of a file.
1912    ///
1913    /// This function returns the inode if it existed and was removed.
1914    ///
1915    /// # Safety
1916    /// - The caller must ensure that all references to the specified inode have
1917    ///   been removed from the filesystem.
1918    pub unsafe fn remove_inode(&self, inodes: &WasiInodes, ino: Inode) -> Option<Arc<InodeVal>> {
1919        let mut guard = inodes.protected.write().unwrap();
1920        guard.lookup.remove(&ino).and_then(|a| Weak::upgrade(&a))
1921    }
1922
1923    pub(crate) fn create_stdout(&self, inodes: &WasiInodes) {
1924        self.create_std_dev_inner(
1925            inodes,
1926            Box::<Stdout>::default(),
1927            "stdout",
1928            __WASI_STDOUT_FILENO,
1929            STDOUT_DEFAULT_RIGHTS,
1930            Fdflags::APPEND,
1931            FS_STDOUT_INO,
1932        );
1933    }
1934
1935    pub(crate) fn create_stdin(&self, inodes: &WasiInodes) {
1936        self.create_std_dev_inner(
1937            inodes,
1938            Box::<Stdin>::default(),
1939            "stdin",
1940            __WASI_STDIN_FILENO,
1941            STDIN_DEFAULT_RIGHTS,
1942            Fdflags::empty(),
1943            FS_STDIN_INO,
1944        );
1945    }
1946
1947    pub(crate) fn create_stderr(&self, inodes: &WasiInodes) {
1948        self.create_std_dev_inner(
1949            inodes,
1950            Box::<Stderr>::default(),
1951            "stderr",
1952            __WASI_STDERR_FILENO,
1953            STDERR_DEFAULT_RIGHTS,
1954            Fdflags::APPEND,
1955            FS_STDERR_INO,
1956        );
1957    }
1958
1959    pub(crate) fn create_rootfd(&self) -> Result<(), String> {
1960        // create virtual root
1961        let all_rights = ALL_RIGHTS;
1962        // TODO: make this a list of positive rigths instead of negative ones
1963        // root gets all right for now
1964        let root_rights = all_rights
1965            /*
1966            & (!Rights::FD_WRITE)
1967            & (!Rights::FD_ALLOCATE)
1968            & (!Rights::PATH_CREATE_DIRECTORY)
1969            & (!Rights::PATH_CREATE_FILE)
1970            & (!Rights::PATH_LINK_SOURCE)
1971            & (!Rights::PATH_RENAME_SOURCE)
1972            & (!Rights::PATH_RENAME_TARGET)
1973            & (!Rights::PATH_FILESTAT_SET_SIZE)
1974            & (!Rights::PATH_FILESTAT_SET_TIMES)
1975            & (!Rights::FD_FILESTAT_SET_SIZE)
1976            & (!Rights::FD_FILESTAT_SET_TIMES)
1977            & (!Rights::PATH_SYMLINK)
1978            & (!Rights::PATH_UNLINK_FILE)
1979            & (!Rights::PATH_REMOVE_DIRECTORY)
1980            */;
1981        let fd = self
1982            .create_fd(
1983                root_rights,
1984                root_rights,
1985                Fdflags::empty(),
1986                Fdflagsext::empty(),
1987                Fd::READ,
1988                self.root_inode.clone(),
1989            )
1990            .map_err(|e| format!("Could not create root fd: {e}"))?;
1991        self.preopen_fds.write().unwrap().push(fd);
1992        Ok(())
1993    }
1994
1995    pub(crate) fn create_preopens(
1996        &self,
1997        inodes: &WasiInodes,
1998        ignore_duplicates: bool,
1999    ) -> Result<(), String> {
2000        for preopen_name in self.init_vfs_preopens.iter() {
2001            let kind = Kind::Dir {
2002                parent: self.root_inode.downgrade(),
2003                path: PathBuf::from(preopen_name),
2004                entries: Default::default(),
2005            };
2006            let rights = Rights::FD_ADVISE
2007                | Rights::FD_TELL
2008                | Rights::FD_SEEK
2009                | Rights::FD_READ
2010                | Rights::PATH_OPEN
2011                | Rights::FD_READDIR
2012                | Rights::PATH_READLINK
2013                | Rights::PATH_FILESTAT_GET
2014                | Rights::FD_FILESTAT_GET
2015                | Rights::PATH_LINK_SOURCE
2016                | Rights::PATH_RENAME_SOURCE
2017                | Rights::POLL_FD_READWRITE
2018                | Rights::SOCK_SHUTDOWN;
2019            let inode = self
2020                .create_inode(inodes, kind, true, preopen_name.clone())
2021                .map_err(|e| {
2022                    format!(
2023                        "Failed to create inode for preopened dir (name `{preopen_name}`): WASI error code: {e}",
2024                    )
2025                })?;
2026            let fd_flags = Fd::READ;
2027            let fd = self
2028                .create_fd(
2029                    rights,
2030                    rights,
2031                    Fdflags::empty(),
2032                    Fdflagsext::empty(),
2033                    fd_flags,
2034                    inode.clone(),
2035                )
2036                .map_err(|e| format!("Could not open fd for file {preopen_name:?}: {e}"))?;
2037            {
2038                let mut guard = self.root_inode.write();
2039                if let Kind::Root { entries } = guard.deref_mut() {
2040                    let existing_entry = entries.insert(preopen_name.clone(), inode);
2041                    if existing_entry.is_some() && !ignore_duplicates {
2042                        return Err(format!("Found duplicate entry for alias `{preopen_name}`"));
2043                    }
2044                }
2045            }
2046            self.preopen_fds.write().unwrap().push(fd);
2047        }
2048
2049        for PreopenedDir {
2050            path,
2051            alias,
2052            read,
2053            write,
2054            create,
2055        } in self.init_preopens.iter()
2056        {
2057            debug!(
2058                "Attempting to preopen {} with alias {:?}",
2059                &path.to_string_lossy(),
2060                &alias
2061            );
2062            let cur_dir_metadata = self
2063                .root_fs
2064                .metadata(path)
2065                .map_err(|e| format!("Could not get metadata for file {path:?}: {e}"))?;
2066
2067            let kind = if cur_dir_metadata.is_dir() {
2068                Kind::Dir {
2069                    parent: self.root_inode.downgrade(),
2070                    path: path.clone(),
2071                    entries: Default::default(),
2072                }
2073            } else {
2074                return Err(format!(
2075                    "WASI only supports pre-opened directories right now; found \"{}\"",
2076                    &path.to_string_lossy()
2077                ));
2078            };
2079
2080            let rights = {
2081                // TODO: review tell' and fd_readwrite
2082                let mut rights = Rights::FD_ADVISE | Rights::FD_TELL | Rights::FD_SEEK;
2083                if *read {
2084                    rights |= Rights::FD_READ
2085                        | Rights::PATH_OPEN
2086                        | Rights::FD_READDIR
2087                        | Rights::PATH_READLINK
2088                        | Rights::PATH_FILESTAT_GET
2089                        | Rights::FD_FILESTAT_GET
2090                        | Rights::PATH_LINK_SOURCE
2091                        | Rights::PATH_RENAME_SOURCE
2092                        | Rights::POLL_FD_READWRITE
2093                        | Rights::SOCK_SHUTDOWN;
2094                }
2095                if *write {
2096                    rights |= Rights::FD_DATASYNC
2097                        | Rights::FD_FDSTAT_SET_FLAGS
2098                        | Rights::FD_WRITE
2099                        | Rights::FD_SYNC
2100                        | Rights::FD_ALLOCATE
2101                        | Rights::PATH_OPEN
2102                        | Rights::PATH_RENAME_TARGET
2103                        | Rights::PATH_FILESTAT_SET_SIZE
2104                        | Rights::PATH_FILESTAT_SET_TIMES
2105                        | Rights::FD_FILESTAT_SET_SIZE
2106                        | Rights::FD_FILESTAT_SET_TIMES
2107                        | Rights::PATH_REMOVE_DIRECTORY
2108                        | Rights::PATH_UNLINK_FILE
2109                        | Rights::POLL_FD_READWRITE
2110                        | Rights::SOCK_SHUTDOWN;
2111                }
2112                if *create {
2113                    rights |= Rights::PATH_CREATE_DIRECTORY
2114                        | Rights::PATH_CREATE_FILE
2115                        | Rights::PATH_LINK_TARGET
2116                        | Rights::PATH_OPEN
2117                        | Rights::PATH_RENAME_TARGET
2118                        | Rights::PATH_SYMLINK;
2119                }
2120
2121                rights
2122            };
2123            let inode = if let Some(alias) = &alias {
2124                self.create_inode(inodes, kind, true, alias.clone())
2125            } else {
2126                self.create_inode(inodes, kind, true, path.to_string_lossy().into_owned())
2127            }
2128            .map_err(|e| {
2129                format!("Failed to create inode for preopened dir: WASI error code: {e}")
2130            })?;
2131            let fd_flags = {
2132                let mut fd_flags = 0;
2133                if *read {
2134                    fd_flags |= Fd::READ;
2135                }
2136                if *write {
2137                    // TODO: introduce API for finer grained control
2138                    fd_flags |= Fd::WRITE | Fd::APPEND | Fd::TRUNCATE;
2139                }
2140                if *create {
2141                    fd_flags |= Fd::CREATE;
2142                }
2143                fd_flags
2144            };
2145            let fd = self
2146                .create_fd(
2147                    rights,
2148                    rights,
2149                    Fdflags::empty(),
2150                    Fdflagsext::empty(),
2151                    fd_flags,
2152                    inode.clone(),
2153                )
2154                .map_err(|e| format!("Could not open fd for file {path:?}: {e}"))?;
2155            {
2156                let mut guard = self.root_inode.write();
2157                if let Kind::Root { entries } = guard.deref_mut() {
2158                    let key = if let Some(alias) = &alias {
2159                        alias.clone()
2160                    } else {
2161                        path.to_string_lossy().into_owned()
2162                    };
2163                    let existing_entry = entries.insert(key.clone(), inode);
2164                    if existing_entry.is_some() && !ignore_duplicates {
2165                        return Err(format!("Found duplicate entry for alias `{key}`"));
2166                    }
2167                }
2168            }
2169            self.preopen_fds.write().unwrap().push(fd);
2170        }
2171
2172        Ok(())
2173    }
2174
2175    #[allow(clippy::too_many_arguments)]
2176    pub(crate) fn create_std_dev_inner(
2177        &self,
2178        inodes: &WasiInodes,
2179        handle: Box<dyn VirtualFile + Send + Sync + 'static>,
2180        name: &'static str,
2181        raw_fd: WasiFd,
2182        rights: Rights,
2183        fd_flags: Fdflags,
2184        st_ino: Inode,
2185    ) {
2186        let inode = {
2187            let stat = Filestat {
2188                st_filetype: Filetype::CharacterDevice,
2189                st_ino: st_ino.as_u64(),
2190                ..Filestat::default()
2191            };
2192            let kind = Kind::File {
2193                fd: Some(raw_fd),
2194                handle: Some(Arc::new(RwLock::new(handle))),
2195                path: "".into(),
2196            };
2197            inodes.add_inode_val(InodeVal {
2198                stat: RwLock::new(stat),
2199                is_preopened: true,
2200                name: RwLock::new(name.to_string().into()),
2201                kind: RwLock::new(kind),
2202            })
2203        };
2204        self.fd_map.write().unwrap().insert(
2205            false,
2206            raw_fd,
2207            Fd {
2208                inner: FdInner {
2209                    rights,
2210                    rights_inheriting: Rights::empty(),
2211                    flags: fd_flags,
2212                    offset: Arc::new(AtomicU64::new(0)),
2213                    fd_flags: Fdflagsext::empty(),
2214                },
2215                // since we're not calling open on this, we don't need open flags
2216                open_flags: 0,
2217                inode,
2218                is_stdio: true,
2219            },
2220        );
2221    }
2222
2223    pub fn get_stat_for_kind(&self, kind: &Kind) -> Result<Filestat, Errno> {
2224        let md = match kind {
2225            Kind::File { handle, path, .. } => match handle {
2226                Some(wf) => {
2227                    let wf = wf.read().unwrap();
2228                    return Ok(Filestat {
2229                        st_filetype: Filetype::RegularFile,
2230                        st_ino: Inode::from_path(path.to_string_lossy().as_ref()).as_u64(),
2231                        st_size: wf.size(),
2232                        st_atim: wf.last_accessed(),
2233                        st_mtim: wf.last_modified(),
2234                        st_ctim: wf.created_time(),
2235
2236                        ..Filestat::default()
2237                    });
2238                }
2239                None => self
2240                    .root_fs
2241                    .metadata(path)
2242                    .map_err(fs_error_into_wasi_err)?,
2243            },
2244            Kind::Dir { path, .. } => self
2245                .root_fs
2246                .metadata(path)
2247                .map_err(fs_error_into_wasi_err)?,
2248            Kind::Symlink {
2249                base_po_dir,
2250                path_to_symlink,
2251                ..
2252            } => {
2253                let guard = self.fd_map.read().unwrap();
2254                let base_po_inode = &guard.get(*base_po_dir).unwrap().inode;
2255                let guard = base_po_inode.read();
2256                match guard.deref() {
2257                    Kind::Root { .. } => self
2258                        .root_fs
2259                        .symlink_metadata(path_to_symlink)
2260                        .map_err(fs_error_into_wasi_err)?,
2261                    Kind::Dir { path, .. } => {
2262                        let mut real_path = path.clone();
2263                        // PHASE 1: ignore all possible symlinks in `relative_path`
2264                        // TODO: walk the segments of `relative_path` via the entries of the Dir
2265                        //       use helper function to avoid duplicating this logic (walking this will require
2266                        //       &self to be &mut sel
2267                        // TODO: adjust size of symlink, too
2268                        //      for all paths adjusted think about this
2269                        real_path.push(path_to_symlink);
2270                        self.root_fs
2271                            .symlink_metadata(&real_path)
2272                            .map_err(fs_error_into_wasi_err)?
2273                    }
2274                    // if this triggers, there's a bug in the symlink code
2275                    _ => unreachable!(
2276                        "Symlink pointing to something that's not a directory as its base preopened directory"
2277                    ),
2278                }
2279            }
2280            _ => return Err(Errno::Io),
2281        };
2282        Ok(Filestat {
2283            st_filetype: virtual_file_type_to_wasi_file_type(md.file_type()),
2284            st_size: md.len(),
2285            st_atim: md.accessed(),
2286            st_mtim: md.modified(),
2287            st_ctim: md.created(),
2288            ..Filestat::default()
2289        })
2290    }
2291
2292    /// Closes an open FD, handling all details such as FD being preopen
2293    pub(crate) fn close_fd(&self, fd: WasiFd) -> Result<(), Errno> {
2294        let mut fd_map = self.fd_map.write().unwrap();
2295
2296        let pfd = fd_map.remove(fd).ok_or(Errno::Badf);
2297        match pfd {
2298            Ok(fd_ref) => {
2299                let inode = fd_ref.inode.ino().as_u64();
2300                let ref_cnt = fd_ref.inode.ref_cnt();
2301                if ref_cnt == 1 {
2302                    trace!(%fd, %inode, %ref_cnt, "closing file descriptor");
2303                } else {
2304                    trace!(%fd, %inode, %ref_cnt, "weakening file descriptor");
2305                }
2306            }
2307            Err(err) => {
2308                trace!(%fd, "closing file descriptor failed - {}", err);
2309            }
2310        }
2311        Ok(())
2312    }
2313}
2314
2315impl std::fmt::Debug for WasiFs {
2316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2317        if let Ok(guard) = self.current_dir.try_lock() {
2318            write!(f, "current_dir={} ", guard.as_str())?;
2319        } else {
2320            write!(f, "current_dir=(locked) ")?;
2321        }
2322        if let Ok(guard) = self.fd_map.read() {
2323            write!(
2324                f,
2325                "next_fd={} max_fd={:?} ",
2326                guard.next_free_fd(),
2327                guard.last_fd()
2328            )?;
2329        } else {
2330            write!(f, "next_fd=(locked) max_fd=(locked) ")?;
2331        }
2332        write!(f, "{:?}", self.root_fs)
2333    }
2334}
2335
2336/// Returns the default filesystem backing
2337pub fn default_fs_backing() -> Arc<dyn virtual_fs::FileSystem + Send + Sync> {
2338    cfg_if::cfg_if! {
2339        if #[cfg(feature = "host-fs")] {
2340            Arc::new(virtual_fs::host_fs::FileSystem::new(tokio::runtime::Handle::current(), "/").unwrap())
2341        } else if #[cfg(not(feature = "host-fs"))] {
2342            Arc::<virtual_fs::mem_fs::FileSystem>::default()
2343        } else {
2344            Arc::<FallbackFileSystem>::default()
2345        }
2346    }
2347}
2348
2349#[derive(Debug, Default)]
2350pub struct FallbackFileSystem;
2351
2352impl FallbackFileSystem {
2353    fn fail() -> ! {
2354        panic!(
2355            "No filesystem set for wasmer-wasi, please enable either the `host-fs` or `mem-fs` feature or set your custom filesystem with `WasiEnvBuilder::set_fs`"
2356        );
2357    }
2358}
2359
2360impl FileSystem for FallbackFileSystem {
2361    fn readlink(&self, _path: &Path) -> virtual_fs::Result<PathBuf> {
2362        Self::fail()
2363    }
2364    fn read_dir(&self, _path: &Path) -> Result<virtual_fs::ReadDir, FsError> {
2365        Self::fail();
2366    }
2367    fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
2368        Self::fail();
2369    }
2370    fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
2371        Self::fail();
2372    }
2373    fn rename<'a>(&'a self, _from: &Path, _to: &Path) -> BoxFuture<'a, Result<(), FsError>> {
2374        Self::fail();
2375    }
2376    fn metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2377        Self::fail();
2378    }
2379    fn symlink_metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2380        Self::fail();
2381    }
2382    fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
2383        Self::fail();
2384    }
2385    fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
2386        Self::fail();
2387    }
2388}
2389
2390pub fn virtual_file_type_to_wasi_file_type(file_type: virtual_fs::FileType) -> Filetype {
2391    // TODO: handle other file types
2392    if file_type.is_dir() {
2393        Filetype::Directory
2394    } else if file_type.is_file() {
2395        Filetype::RegularFile
2396    } else if file_type.is_symlink() {
2397        Filetype::SymbolicLink
2398    } else {
2399        Filetype::Unknown
2400    }
2401}
2402
2403pub fn fs_error_from_wasi_err(err: Errno) -> FsError {
2404    match err {
2405        Errno::Badf => FsError::InvalidFd,
2406        Errno::Exist => FsError::AlreadyExists,
2407        Errno::Io => FsError::IOError,
2408        Errno::Addrinuse => FsError::AddressInUse,
2409        Errno::Addrnotavail => FsError::AddressNotAvailable,
2410        Errno::Pipe => FsError::BrokenPipe,
2411        Errno::Connaborted => FsError::ConnectionAborted,
2412        Errno::Connrefused => FsError::ConnectionRefused,
2413        Errno::Connreset => FsError::ConnectionReset,
2414        Errno::Intr => FsError::Interrupted,
2415        Errno::Inval => FsError::InvalidInput,
2416        Errno::Notconn => FsError::NotConnected,
2417        Errno::Nodev => FsError::NoDevice,
2418        Errno::Noent => FsError::EntryNotFound,
2419        Errno::Perm => FsError::PermissionDenied,
2420        Errno::Timedout => FsError::TimedOut,
2421        Errno::Proto => FsError::UnexpectedEof,
2422        Errno::Again => FsError::WouldBlock,
2423        Errno::Nospc => FsError::WriteZero,
2424        Errno::Notempty => FsError::DirectoryNotEmpty,
2425        _ => FsError::UnknownError,
2426    }
2427}
2428
2429pub fn fs_error_into_wasi_err(fs_error: FsError) -> Errno {
2430    match fs_error {
2431        FsError::AlreadyExists => Errno::Exist,
2432        FsError::AddressInUse => Errno::Addrinuse,
2433        FsError::AddressNotAvailable => Errno::Addrnotavail,
2434        FsError::BaseNotDirectory => Errno::Notdir,
2435        FsError::BrokenPipe => Errno::Pipe,
2436        FsError::ConnectionAborted => Errno::Connaborted,
2437        FsError::ConnectionRefused => Errno::Connrefused,
2438        FsError::ConnectionReset => Errno::Connreset,
2439        FsError::Interrupted => Errno::Intr,
2440        FsError::InvalidData => Errno::Io,
2441        FsError::InvalidFd => Errno::Badf,
2442        FsError::InvalidInput => Errno::Inval,
2443        FsError::IOError => Errno::Io,
2444        FsError::NoDevice => Errno::Nodev,
2445        FsError::NotAFile => Errno::Inval,
2446        FsError::NotConnected => Errno::Notconn,
2447        FsError::EntryNotFound => Errno::Noent,
2448        FsError::PermissionDenied => Errno::Perm,
2449        FsError::TimedOut => Errno::Timedout,
2450        FsError::UnexpectedEof => Errno::Proto,
2451        FsError::WouldBlock => Errno::Again,
2452        FsError::WriteZero => Errno::Nospc,
2453        FsError::DirectoryNotEmpty => Errno::Notempty,
2454        FsError::StorageFull => Errno::Overflow,
2455        FsError::Lock | FsError::UnknownError => Errno::Io,
2456        FsError::Unsupported => Errno::Notsup,
2457    }
2458}
2459
2460#[cfg(test)]
2461mod tests {
2462    use super::*;
2463    use once_cell::sync::OnceCell;
2464    use tempfile::tempdir;
2465    use virtual_fs::{RootFileSystemBuilder, TmpFileSystem};
2466    use wasmer::Engine;
2467    use wasmer_config::package::PackageId;
2468
2469    use crate::WasiEnvBuilder;
2470    use crate::bin_factory::{BinaryPackage, BinaryPackageMount, BinaryPackageMounts};
2471
2472    #[tokio::test]
2473    async fn test_relative_path_to_absolute() {
2474        let inodes = WasiInodes::new();
2475        let fs_backing =
2476            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
2477        let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
2478
2479        // Test absolute path (returned as-is, no normalization)
2480        assert_eq!(
2481            wasi_fs.relative_path_to_absolute("/foo/bar".to_string()),
2482            "/foo/bar"
2483        );
2484        assert_eq!(wasi_fs.relative_path_to_absolute("/".to_string()), "/");
2485
2486        // Absolute paths with special components are not normalized
2487        assert_eq!(
2488            wasi_fs.relative_path_to_absolute("//foo//bar//".to_string()),
2489            "//foo//bar//"
2490        );
2491        assert_eq!(
2492            wasi_fs.relative_path_to_absolute("/a/b/./c".to_string()),
2493            "/a/b/./c"
2494        );
2495        assert_eq!(
2496            wasi_fs.relative_path_to_absolute("/a/b/../c".to_string()),
2497            "/a/b/../c"
2498        );
2499
2500        // Test relative path with root as current dir
2501        assert_eq!(
2502            wasi_fs.relative_path_to_absolute("foo/bar".to_string()),
2503            "/foo/bar"
2504        );
2505        assert_eq!(wasi_fs.relative_path_to_absolute("foo".to_string()), "/foo");
2506
2507        // Test with different current directory
2508        wasi_fs.set_current_dir("/home/user");
2509        assert_eq!(
2510            wasi_fs.relative_path_to_absolute("file.txt".to_string()),
2511            "/home/user/file.txt"
2512        );
2513        assert_eq!(
2514            wasi_fs.relative_path_to_absolute("dir/file.txt".to_string()),
2515            "/home/user/dir/file.txt"
2516        );
2517
2518        // Test relative paths with . and .. components
2519        wasi_fs.set_current_dir("/a/b/c");
2520        assert_eq!(
2521            wasi_fs.relative_path_to_absolute("./file.txt".to_string()),
2522            "/a/b/c/./file.txt"
2523        );
2524        assert_eq!(
2525            wasi_fs.relative_path_to_absolute("../file.txt".to_string()),
2526            "/a/b/c/../file.txt"
2527        );
2528        assert_eq!(
2529            wasi_fs.relative_path_to_absolute("../../file.txt".to_string()),
2530            "/a/b/c/../../file.txt"
2531        );
2532
2533        // Test edge cases
2534        assert_eq!(
2535            wasi_fs.relative_path_to_absolute(".".to_string()),
2536            "/a/b/c/."
2537        );
2538        assert_eq!(
2539            wasi_fs.relative_path_to_absolute("..".to_string()),
2540            "/a/b/c/.."
2541        );
2542        assert_eq!(wasi_fs.relative_path_to_absolute("".to_string()), "/a/b/c/");
2543
2544        // Test current directory with trailing slash
2545        wasi_fs.set_current_dir("/home/user/");
2546        assert_eq!(
2547            wasi_fs.relative_path_to_absolute("file.txt".to_string()),
2548            "/home/user/file.txt"
2549        );
2550
2551        // Test current directory without trailing slash
2552        wasi_fs.set_current_dir("/home/user");
2553        assert_eq!(
2554            wasi_fs.relative_path_to_absolute("file.txt".to_string()),
2555            "/home/user/file.txt"
2556        );
2557    }
2558
2559    #[cfg(feature = "host-fs")]
2560    #[tokio::test]
2561    async fn mapped_preopen_inode_paths_should_stay_in_guest_space() {
2562        let root_dir = tempdir().unwrap();
2563        let hamlet_dir = root_dir.path().join("hamlet");
2564        std::fs::create_dir_all(&hamlet_dir).unwrap();
2565
2566        let host_fs = virtual_fs::host_fs::FileSystem::new(
2567            tokio::runtime::Handle::current(),
2568            root_dir.path(),
2569        )
2570        .unwrap();
2571
2572        let init = WasiEnvBuilder::new("test_prog")
2573            .engine(Engine::default())
2574            .fs(Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>)
2575            .map_dir("hamlet", "/hamlet")
2576            .unwrap()
2577            .build_init()
2578            .unwrap();
2579
2580        let preopen_inode = {
2581            let guard = init.state.fs.root_inode.read();
2582            let Kind::Root { entries } = guard.deref() else {
2583                panic!("expected root inode");
2584            };
2585            entries.get("hamlet").unwrap().clone()
2586        };
2587        let guard = preopen_inode.read();
2588
2589        let Kind::Dir { path, .. } = guard.deref() else {
2590            panic!("expected preopen inode to be a directory");
2591        };
2592
2593        assert_eq!(path, std::path::Path::new("/hamlet"));
2594    }
2595
2596    #[tokio::test]
2597    async fn dot_mapped_preopen_uses_guest_current_dir() {
2598        let init = WasiEnvBuilder::new("test_prog")
2599            .engine(Engine::default())
2600            .current_dir("/work")
2601            .map_dir(".", "/work")
2602            .unwrap()
2603            .build_init()
2604            .unwrap();
2605
2606        let preopen_inode = {
2607            let guard = init.state.fs.root_inode.read();
2608            let Kind::Root { entries } = guard.deref() else {
2609                panic!("expected root inode");
2610            };
2611            entries.get(".").unwrap().clone()
2612        };
2613        let guard = preopen_inode.read();
2614
2615        let Kind::Dir { path, .. } = guard.deref() else {
2616            panic!("expected preopen inode to be a directory");
2617        };
2618
2619        assert_eq!(path, std::path::Path::new("/work"));
2620    }
2621
2622    #[tokio::test]
2623    async fn writable_root_is_preserved_through_root_overlays() {
2624        let base_root = Arc::new(RootFileSystemBuilder::default().build_tmp());
2625        let root = WasiFsRoot::from_filesystem(base_root);
2626        assert!(root.writable_root().is_some());
2627
2628        let lower = Arc::new(TmpFileSystem::new()) as Arc<dyn FileSystem + Send + Sync>;
2629        root.stack_root_filesystem(lower).unwrap();
2630
2631        assert!(root.writable_root().is_some());
2632    }
2633
2634    #[tokio::test]
2635    async fn conditional_union_merges_root_and_non_root_package_mounts_once() {
2636        let inodes = WasiInodes::new();
2637        let fs_backing =
2638            WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
2639        let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
2640
2641        let root_layer = TmpFileSystem::new();
2642        root_layer
2643            .new_open_options()
2644            .create(true)
2645            .write(true)
2646            .open(Path::new("/root.txt"))
2647            .unwrap();
2648
2649        let public_mount = TmpFileSystem::new();
2650        public_mount
2651            .new_open_options()
2652            .create(true)
2653            .write(true)
2654            .open(Path::new("/index.html"))
2655            .unwrap();
2656
2657        let pkg = BinaryPackage {
2658            id: PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap()),
2659            package_ids: vec![],
2660            when_cached: None,
2661            entrypoint_cmd: None,
2662            hash: OnceCell::new(),
2663            package_mounts: Some(Arc::new(BinaryPackageMounts {
2664                root_layer: Some(Arc::new(root_layer)),
2665                mounts: vec![BinaryPackageMount {
2666                    guest_path: PathBuf::from("/public"),
2667                    fs: Arc::new(public_mount),
2668                    source_path: PathBuf::from("/"),
2669                }],
2670            })),
2671            commands: vec![],
2672            uses: vec![],
2673            file_system_memory_footprint: 0,
2674            additional_host_mapped_directories: vec![],
2675        };
2676
2677        wasi_fs.conditional_union(&pkg).await.unwrap();
2678        assert!(
2679            wasi_fs
2680                .root_fs
2681                .metadata(Path::new("/root.txt"))
2682                .unwrap()
2683                .is_file()
2684        );
2685        assert!(
2686            wasi_fs
2687                .root_fs
2688                .metadata(Path::new("/public/index.html"))
2689                .unwrap()
2690                .is_file()
2691        );
2692
2693        wasi_fs.conditional_union(&pkg).await.unwrap();
2694        assert!(
2695            wasi_fs
2696                .root_fs
2697                .metadata(Path::new("/root.txt"))
2698                .unwrap()
2699                .is_file()
2700        );
2701        assert!(
2702            wasi_fs
2703                .root_fs
2704                .metadata(Path::new("/public/index.html"))
2705                .unwrap()
2706                .is_file()
2707        );
2708    }
2709}