diff --git a/Cargo.lock b/Cargo.lock index 2202f6137d35..182b5286a4b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,18 +346,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cap-fs-ext" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ff379b70af8e08307a8f65e7040c7301cb4a572538ade16b4984f0da77847f" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes 3.0.1", - "windows-sys 0.61.2", -] - [[package]] name = "cap-primitives" version = "4.0.3" @@ -376,18 +364,6 @@ dependencies = [ "winx", ] -[[package]] -name = "cap-std" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes 3.0.1", - "rustix 1.1.4", -] - [[package]] name = "capstone" version = "0.14.0" @@ -5165,12 +5141,12 @@ dependencies = [ "async-trait", "bitflags 2.11.1", "bytes", - "cap-fs-ext", "cap-primitives", "env_logger 0.11.5", "futures", "rand 0.10.1", "rustix 1.1.4", + "rustix-linux-procfs", "tempfile", "test-log", "test-programs-artifacts", @@ -5184,6 +5160,7 @@ dependencies = [ "wasmtime-wasi-io", "wiggle", "windows-sys 0.61.2", + "winx", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8af2632f615b..3c6425c6a2a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -364,12 +364,7 @@ regalloc2 = "0.15.2" wasip1 = { version = "1.0.0", default-features = false } # cap-std family: -# -# Note that `cap-fs-ext` should be avoided where possible to use -# `cap-primitives` instead. target-lexicon = "0.13.5" -cap-primitives = "4.0.3" -cap-fs-ext-avoid-using-this = { version = "4.0.3", package = 'cap-fs-ext' } rustix = "1.1.4" # wit-bindgen: wit-bindgen = { version = "0.61.1", default-features = false } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 48775b62c56b..fcf250429fd5 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -26,8 +26,7 @@ tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "r bytes = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["std", "attributes"] } -cap-primitives = { workspace = true } -cap-fs-ext-avoid-using-this = { workspace = true } +public-cap-primitives = { version = "4.0.3", package = 'cap-primitives' } bitflags = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } @@ -46,9 +45,11 @@ env_logger = { workspace = true } [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["event", "fs", "net"] } +rustix-linux-procfs = "0.1.1" [target.'cfg(windows)'.dependencies] rustix = { workspace = true, features = ["event", "net"] } +winx = "0.36.0" [target.'cfg(windows)'.dependencies.windows-sys] workspace = true @@ -57,6 +58,7 @@ features = [ "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_IO", + "Win32_System_Ioctl", "Win32_System_Performance", ] diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs index 99a8c71c3a9c..a54ed070af52 100644 --- a/crates/wasi/src/ctx.rs +++ b/crates/wasi/src/ctx.rs @@ -4,7 +4,6 @@ use crate::filesystem::{Dir, WasiFilesystemCtx}; use crate::random::WasiRandomCtx; use crate::sockets::{SocketAddrCheck, SocketAddrUse, WasiSocketsCtx}; use crate::{FsPerms, OpenMode}; -use cap_primitives::ambient_authority; use rand::Rng; use std::future::Future; use std::mem; @@ -300,7 +299,7 @@ impl WasiCtxBuilder { guest_path: impl AsRef, perms: FsPerms, ) -> Result<&mut Self> { - let dir = cap_primitives::fs::open_ambient_dir(host_path.as_ref(), ambient_authority())?; + let dir = crate::filesystem::primitives::open_ambient_dir(host_path.as_ref())?; let open_mode = match perms { FsPerms::ReadOnly => OpenMode::READ, FsPerms::ReadWrite => OpenMode::READ | OpenMode::WRITE, diff --git a/crates/wasi/src/filesystem.rs b/crates/wasi/src/filesystem.rs index e7b9190bb68c..f74621ad0302 100644 --- a/crates/wasi/src/filesystem.rs +++ b/crates/wasi/src/filesystem.rs @@ -1,6 +1,6 @@ use crate::clocks::Datetime; +use crate::filesystem::primitives::{DirOptions, FollowSymlinks, Metadata, OpenOptions}; use crate::runtime::{AbortOnDropJoinHandle, spawn_blocking}; -use cap_primitives::fs::{DirOptions, FollowSymlinks, Metadata, OpenOptions, SystemTimeSpec}; use std::collections::hash_map; use std::sync::Arc; use std::time::SystemTime; @@ -17,6 +17,8 @@ pub(crate) mod windows; #[cfg(windows)] pub(crate) use windows as sys; +pub(crate) mod primitives; + /// A helper struct which implements [`HasData`] for the `wasi:filesystem` APIs. /// /// This can be useful when directly calling `add_to_linker` functions directly, @@ -274,8 +276,8 @@ pub(crate) enum DescriptorType { RegularFile, } -impl From for DescriptorType { - fn from(ft: cap_primitives::fs::FileType) -> Self { +impl From for DescriptorType { + fn from(ft: crate::filesystem::primitives::FileType) -> Self { if ft.is_dir() { DescriptorType::Directory } else if ft.is_symlink() { @@ -327,15 +329,12 @@ impl DescriptorStat { data_access_timestamp: meta .accessed() .ok() - .and_then(|t| Datetime::try_from(t.into_std()).ok()), + .and_then(|t| Datetime::try_from(t).ok()), data_modification_timestamp: meta .modified() .ok() - .and_then(|t| Datetime::try_from(t.into_std()).ok()), - status_change_timestamp: meta - .created() - .ok() - .and_then(|t| Datetime::try_from(t.into_std()).ok()), + .and_then(|t| Datetime::try_from(t).ok()), + status_change_timestamp: meta.created().ok().and_then(|t| Datetime::try_from(t).ok()), } } } @@ -524,7 +523,7 @@ impl Descriptor { } Self::Dir(d) => { d.run_blocking(|d| { - let d = cap_primitives::fs::open( + let d = crate::filesystem::primitives::open( d, std::path::Component::CurDir.as_ref(), OpenOptions::new().read(true), @@ -622,7 +621,7 @@ impl Descriptor { } Self::Dir(d) => { d.run_blocking(|d| { - let d = cap_primitives::fs::open( + let d = crate::filesystem::primitives::open( d, std::path::Component::CurDir.as_ref(), OpenOptions::new().read(true), @@ -855,7 +854,7 @@ impl Dir { return Err(ErrorCode::NotPermitted); } self.run_blocking(move |d| { - cap_primitives::fs::create_dir(d, path.as_ref(), &DirOptions::new()) + crate::filesystem::primitives::create_dir(d, path.as_ref(), &DirOptions::new()) }) .await?; Ok(()) @@ -887,16 +886,14 @@ impl Dir { if self.perms.write_not_permitted() { return Err(ErrorCode::NotPermitted); } - let atim = - atim.map(|t| SystemTimeSpec::Absolute(cap_primitives::time::SystemTime::from_std(t))); - let mtim = - mtim.map(|t| SystemTimeSpec::Absolute(cap_primitives::time::SystemTime::from_std(t))); if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { - self.run_blocking(move |d| cap_primitives::fs::set_times(d, path.as_ref(), atim, mtim)) - .await?; + self.run_blocking(move |d| { + crate::filesystem::primitives::set_times(d, path.as_ref(), atim, mtim) + }) + .await?; } else { self.run_blocking(move |d| { - cap_primitives::fs::set_times_nofollow(d, path.as_ref(), atim, mtim) + crate::filesystem::primitives::set_times_nofollow(d, path.as_ref(), atim, mtim) }) .await?; } @@ -924,7 +921,12 @@ impl Dir { } let new_dir_handle = Arc::clone(&new_dir.dir); self.run_blocking(move |d| { - cap_primitives::fs::hard_link(d, old_path.as_ref(), &new_dir_handle, new_path.as_ref()) + crate::filesystem::primitives::hard_link( + d, + old_path.as_ref(), + &new_dir_handle, + new_path.as_ref(), + ) }) .await?; Ok(()) @@ -975,17 +977,10 @@ impl Dir { open_mode |= OpenMode::READ; } - // Note that this is intentionally scoped to a separate block to - // minimize the surface area that is depended on by cap-fs-ext. Ideally - // the underlying functionality in `cap-primitives` would get exposed, - // but that'll require an upstream PR. - { - use cap_fs_ext_avoid_using_this::OpenOptionsFollowExt; - if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { - opts.follow(FollowSymlinks::Yes); - } else { - opts.follow(FollowSymlinks::No); - } + if path_flags.contains(PathFlags::SYMLINK_FOLLOW) { + opts.follow(FollowSymlinks::Yes); + } else { + opts.follow(FollowSymlinks::No); } // These flags are not yet supported in cap-primitives: @@ -1024,7 +1019,7 @@ impl Dir { let opened = self .run_blocking::<_, std::io::Result>(move |d| { - let opened = cap_primitives::fs::open(d, path.as_ref(), &opts)?; + let opened = crate::filesystem::primitives::open(d, path.as_ref(), &opts)?; if Metadata::from_file(&opened)?.is_dir() { Ok(OpenResult::Dir(opened)) } else if oflags.contains(OpenFlags::DIRECTORY) { @@ -1064,7 +1059,7 @@ impl Dir { pub(crate) async fn readlink_at(&self, path: String) -> Result { let link = self - .run_blocking(move |d| cap_primitives::fs::read_link(d, path.as_ref())) + .run_blocking(move |d| crate::filesystem::primitives::read_link(d, path.as_ref())) .await?; link.into_os_string() .into_string() @@ -1075,7 +1070,7 @@ impl Dir { if self.perms.write_not_permitted() { return Err(ErrorCode::NotPermitted); } - self.run_blocking(move |d| cap_primitives::fs::remove_dir(d, path.as_ref())) + self.run_blocking(move |d| crate::filesystem::primitives::remove_dir(d, path.as_ref())) .await?; Ok(()) } @@ -1097,7 +1092,12 @@ impl Dir { } let new_dir_handle = Arc::clone(&new_dir.dir); self.run_blocking(move |d| { - cap_primitives::fs::rename(d, old_path.as_ref(), &new_dir_handle, new_path.as_ref()) + crate::filesystem::primitives::rename( + d, + old_path.as_ref(), + &new_dir_handle, + new_path.as_ref(), + ) }) .await?; Ok(()) diff --git a/crates/wasi/src/filesystem/primitives/create_dir.rs b/crates/wasi/src/filesystem/primitives/create_dir.rs new file mode 100644 index 000000000000..0d33961817fb --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/create_dir.rs @@ -0,0 +1,14 @@ +//! This defines `create_dir`, the primary entrypoint to sandboxed directory +//! creation. + +use crate::filesystem::primitives::{DirOptions, create_dir_impl}; +use std::path::Path; +use std::{fs, io}; + +/// Perform a `mkdirat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. +#[inline] +pub fn create_dir(start: &fs::File, path: &Path, options: &DirOptions) -> io::Result<()> { + // Call the underlying implementation. + create_dir_impl(start, path, options) +} diff --git a/crates/wasi/src/filesystem/primitives/dir_entry.rs b/crates/wasi/src/filesystem/primitives/dir_entry.rs new file mode 100644 index 000000000000..013880d48926 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/dir_entry.rs @@ -0,0 +1,67 @@ +use crate::filesystem::primitives::{DirEntryInner, Metadata}; +#[cfg(not(windows))] +use rustix::fs::DirEntryExt; +use std::ffi::OsString; +use std::{fmt, io}; + +/// Entries returned by the `ReadDir` iterator. +/// +/// This corresponds to [`std::fs::DirEntry`]. +/// +/// Unlike `std::fs::DirEntry`, this API has no `DirEntry::path`, because +/// absolute paths don't interoperate well with the capability model. +/// +/// There is a `file_name` function, however there are also `open`, +/// `open_with`, `open_dir`, `remove_file`, and `remove_dir` functions for +/// opening or removing the entry directly, which can be more efficient and +/// convenient. +/// +/// There is no `from_std` method, as `std::fs::DirEntry` doesn't provide a way +/// to construct a `DirEntry` without opening directories by ambient paths. +pub struct DirEntry { + pub(crate) inner: DirEntryInner, +} + +impl DirEntry { + /// Returns the metadata for the file that this entry points at. + /// + /// This corresponds to [`std::fs::DirEntry::metadata`]. + /// + /// # Platform-specific behavior + /// + /// On Windows, this produces a `Metadata` object which does not contain + /// the optional values returned by [`MetadataExt`]. Use + /// [`cap_fs_ext::DirEntryExt::full_metadata`] to obtain a `Metadata` with + /// the values filled in. + /// + /// [`MetadataExt`]: https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html + /// [`cap_fs_ext::DirEntryExt::full_metadata`]: https://docs.rs/cap-fs-ext/latest/cap_fs_ext/trait.DirEntryExt.html#tymethod.full_metadata + #[inline] + pub fn metadata(&self) -> io::Result { + self.inner.metadata() + } + + /// Returns the bare file name of this directory entry without any other + /// leading path component. + /// + /// This corresponds to [`std::fs::DirEntry::file_name`]. + #[inline] + pub fn file_name(&self) -> OsString { + self.inner.file_name() + } +} + +#[cfg(not(windows))] +impl DirEntryExt for DirEntry { + #[inline] + fn ino(&self) -> u64 { + self.inner.ino() + } +} + +impl fmt::Debug for DirEntry { + // Like libstd's version, but doesn't print the path. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} diff --git a/crates/wasi/src/filesystem/primitives/dir_options.rs b/crates/wasi/src/filesystem/primitives/dir_options.rs new file mode 100644 index 000000000000..096bfa446988 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/dir_options.rs @@ -0,0 +1,34 @@ +#[cfg(not(target_os = "wasi"))] +use crate::filesystem::primitives::DirOptionsExt; + +/// Options and flags which can be used to configure how a directory is +/// created. +/// +/// This is to `create_dir` what to `OpenOptions` is to `open`. +#[derive(Debug, Clone)] +pub struct DirOptions { + #[cfg(not(target_os = "wasi"))] + #[allow(dead_code)] + pub(crate) ext: DirOptionsExt, +} + +impl DirOptions { + /// Creates a blank new set of options ready for configuration. + #[allow(clippy::new_without_default)] + #[inline] + pub const fn new() -> Self { + Self { + #[cfg(not(target_os = "wasi"))] + ext: DirOptionsExt::new(), + } + } +} + +#[cfg(target_os = "vxworks")] +impl crate::fs::DirBuilderExt for DirOptions { + #[inline] + fn mode(&mut self, mode: u32) -> &mut Self { + self.ext.mode(mode); + self + } +} diff --git a/crates/wasi/src/filesystem/primitives/errors.rs b/crates/wasi/src/filesystem/primitives/errors.rs new file mode 100644 index 000000000000..d0dcc949111a --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/errors.rs @@ -0,0 +1,14 @@ +use std::io; + +#[cfg(not(windows))] +pub(crate) use crate::filesystem::primitives::rustix::fs::errors::*; +#[cfg(windows)] +pub(crate) use crate::filesystem::primitives::windows::fs::errors::*; + +#[cold] +pub(crate) fn escape_attempt() -> io::Error { + io::Error::new( + io::ErrorKind::PermissionDenied, + "a path led outside of the filesystem", + ) +} diff --git a/crates/wasi/src/filesystem/primitives/file_type.rs b/crates/wasi/src/filesystem/primitives/file_type.rs new file mode 100644 index 000000000000..20ebf31f2b14 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/file_type.rs @@ -0,0 +1,123 @@ +//! The `FileType` struct. + +use crate::filesystem::primitives::ImplFileTypeExt; + +/// `FileType`'s inner state. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Inner { + /// A directory. + Dir, + + /// A file. + File, + + /// An unknown entity. + Unknown, + + /// A `FileTypeExt` type. + Ext(ImplFileTypeExt), +} + +/// A structure representing a type of file with accessors for each file type. +/// +/// This corresponds to [`std::fs::FileType`]. +/// +///
+/// We need to define our own version because the libstd `FileType` doesn't +/// have a public constructor that we can use. +///
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[repr(transparent)] +pub struct FileType(Inner); + +impl FileType { + /// Creates a `FileType` for which `is_dir()` returns `true`. + #[inline] + pub const fn dir() -> Self { + Self(Inner::Dir) + } + + /// Creates a `FileType` for which `is_file()` returns `true`. + #[inline] + pub const fn file() -> Self { + Self(Inner::File) + } + + /// Creates a `FileType` for which `is_unknown()` returns `true`. + #[inline] + pub const fn unknown() -> Self { + Self(Inner::Unknown) + } + + /// Creates a `FileType` from extension type. + #[inline] + pub(crate) const fn ext(ext: ImplFileTypeExt) -> Self { + Self(Inner::Ext(ext)) + } + + /// Tests whether this file type represents a directory. + /// + /// This corresponds to [`std::fs::FileType::is_dir`]. + #[inline] + pub fn is_dir(&self) -> bool { + self.0 == Inner::Dir + } + + /// Tests whether this file type represents a regular file. + /// + /// This corresponds to [`std::fs::FileType::is_file`]. + #[inline] + pub fn is_file(&self) -> bool { + self.0 == Inner::File + } + + /// Tests whether this file type represents a symbolic link. + /// + /// This corresponds to [`std::fs::FileType::is_symlink`]. + #[inline] + pub fn is_symlink(&self) -> bool { + if let Inner::Ext(ext) = self.0 { + ext.is_symlink() + } else { + false + } + } +} + +/// Unix-specific extensions for [`FileType`]. +/// +/// This corresponds to [`std::os::unix::fs::FileTypeExt`]. +#[cfg(any(unix, target_os = "vxworks"))] +pub trait FileTypeExt { + /// Returns `true` if this file type is a block device. + fn is_block_device(&self) -> bool; + /// Returns `true` if this file type is a character device. + fn is_char_device(&self) -> bool; +} + +#[cfg(any(unix, target_os = "vxworks"))] +impl FileTypeExt for FileType { + #[inline] + fn is_block_device(&self) -> bool { + self.0 == Inner::Ext(ImplFileTypeExt::block_device()) + } + + #[inline] + fn is_char_device(&self) -> bool { + self.0 == Inner::Ext(ImplFileTypeExt::char_device()) + } +} + +/// Extension trait to allow `is_block_device` etc. to be exposed by +/// the `cap-fs-ext` crate. +/// +/// This is hidden from the main API since this functionality isn't present in +/// `std`. Use `cap_fs_ext::FileTypeExt` instead of calling this directly. +#[cfg(windows)] +#[doc(hidden)] +pub trait _WindowsFileTypeExt { + fn is_block_device(&self) -> bool; + fn is_char_device(&self) -> bool; + fn is_fifo(&self) -> bool; + fn is_socket(&self) -> bool; +} diff --git a/crates/wasi/src/filesystem/primitives/follow_symlinks.rs b/crates/wasi/src/filesystem/primitives/follow_symlinks.rs new file mode 100644 index 000000000000..55774d4ae747 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/follow_symlinks.rs @@ -0,0 +1,16 @@ +/// Should symlinks be followed in the last component of a path? +/// +/// This doesn't affect path components other than the last. So for example in +/// "foo/bar/baz", if "foo" or "bar" are symlinks, they will always be +/// followed. This enum value only determines whether "baz" is followed. +/// +/// Instead of passing bare `bool`s as parameters, pass a distinct enum so that +/// the intent is clear. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum FollowSymlinks { + /// Yes, do follow symlinks in the last component of a path. + Yes, + + /// No, do not follow symlinks in the last component of a path. + No, +} diff --git a/crates/wasi/src/filesystem/primitives/hard_link.rs b/crates/wasi/src/filesystem/primitives/hard_link.rs new file mode 100644 index 000000000000..e46bd3c7b134 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/hard_link.rs @@ -0,0 +1,19 @@ +//! This defines `hard_link`, the primary entrypoint to sandboxed hard-link +//! creation. + +use crate::filesystem::primitives::hard_link_impl; +use std::path::Path; +use std::{fs, io}; + +/// Perform a `linkat`-like operation, ensuring that the resolution of the path +/// never escapes the directory tree rooted at `start`. +#[inline] +pub fn hard_link( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + // Call the underlying implementation. + hard_link_impl(old_start, old_path, new_start, new_path) +} diff --git a/crates/wasi/src/filesystem/primitives/manually/canonical_path.rs b/crates/wasi/src/filesystem/primitives/manually/canonical_path.rs new file mode 100644 index 000000000000..b843cb703b20 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/manually/canonical_path.rs @@ -0,0 +1,55 @@ +use std::ffi::OsStr; +use std::path::{Component, PathBuf}; + +/// Utility for collecting the canonical path components. +pub(super) struct CanonicalPath<'path_buf> { + /// If the user requested a canonical path, a reference to the `PathBuf` to + /// write it to. + path: Option<&'path_buf mut PathBuf>, +} + +impl<'path_buf> CanonicalPath<'path_buf> { + pub(super) fn new(path: Option<&'path_buf mut PathBuf>) -> Self { + Self { path } + } + + pub(super) fn push(&mut self, one: &OsStr) { + if let Some(path) = &mut self.path { + path.push(one) + } + } + + pub(super) fn pop(&mut self) -> bool { + if let Some(path) = &mut self.path { + path.pop() + } else { + true + } + } + + /// The complete canonical path has been scanned. Set `path` to `None` + /// so that it isn't cleared when `self` is dropped. + pub(super) fn complete(&mut self) { + // Replace "" with ".", since "" as a relative path is interpreted as + // an error. + if let Some(path) = &mut self.path { + if path.as_os_str().is_empty() { + path.push(Component::CurDir); + } + self.path = None; + } + } +} + +impl<'path_buf> Drop for CanonicalPath<'path_buf> { + fn drop(&mut self) { + // If `self.path` is still `Some` here, it means that we haven't called + // `complete()` yet, meaning the `CanonicalPath` is being dropped + // before the complete path has been processed. In that case, clear + // `path` to indicate that we weren't able to obtain a complete path. + if let Some(path) = &mut self.path { + path.clear(); + self.path = None; + } + } +} diff --git a/crates/wasi/src/filesystem/primitives/manually/cow_component.rs b/crates/wasi/src/filesystem/primitives/manually/cow_component.rs new file mode 100644 index 000000000000..0cf454ffd222 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/manually/cow_component.rs @@ -0,0 +1,44 @@ +use std::borrow::Cow; +use std::ffi::OsStr; +use std::path::Component; + +/// Like `std::path::Component` except we combine `Prefix` and `RootDir` since +/// we don't support absolute paths, and `Normal` has a `Cow` instead of a +/// plain `OsStr` reference, so it can optionally own its own string. +pub(super) enum CowComponent<'borrow> { + PrefixOrRootDir, + CurDir, + ParentDir, + Normal(Cow<'borrow, OsStr>), +} + +impl<'borrow> CowComponent<'borrow> { + /// Convert a `Component` into a `CowComponent` which borrows strings. + pub(super) fn borrowed(component: Component<'borrow>) -> Self { + match component { + Component::Prefix(_) | Component::RootDir => Self::PrefixOrRootDir, + Component::CurDir => Self::CurDir, + Component::ParentDir => Self::ParentDir, + Component::Normal(os_str) => Self::Normal(os_str.into()), + } + } + + /// Convert a `Component` into a `CowComponent` which owns strings. + pub(super) fn owned(component: Component) -> Self { + match component { + Component::Prefix(_) | Component::RootDir => Self::PrefixOrRootDir, + Component::CurDir => Self::CurDir, + Component::ParentDir => Self::ParentDir, + Component::Normal(os_str) => Self::Normal(os_str.to_os_string().into()), + } + } + + /// Test whether `self` is `Component::Normal`. + #[cfg(windows)] + pub(super) fn is_normal(&self) -> bool { + match self { + CowComponent::Normal(_) => true, + _ => false, + } + } +} diff --git a/crates/wasi/src/filesystem/primitives/manually/mod.rs b/crates/wasi/src/filesystem/primitives/manually/mod.rs new file mode 100644 index 000000000000..3712e9abaf65 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/manually/mod.rs @@ -0,0 +1,13 @@ +//! Functions that perform path lookup manually, one component +//! at a time, with manual symlink resolution. + +mod canonical_path; +mod cow_component; +mod open; +mod read_link_one; + +use canonical_path::CanonicalPath; +use cow_component::CowComponent; +use read_link_one::read_link_one; + +pub(crate) use open::{open, stat}; diff --git a/crates/wasi/src/filesystem/primitives/manually/open.rs b/crates/wasi/src/filesystem/primitives/manually/open.rs new file mode 100644 index 000000000000..06bef1aba961 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/manually/open.rs @@ -0,0 +1,510 @@ +//! Manual path resolution, one component at a time, with manual symlink +//! resolution, in order to enforce sandboxing. + +use super::{CanonicalPath, CowComponent, read_link_one}; +use crate::filesystem::primitives::{ + FollowSymlinks, MaybeOwnedFile, Metadata, OpenOptions, OpenUncheckedError, dir_options, errors, + open_unchecked, path_has_trailing_dot, path_has_trailing_slash, stat_unchecked, +}; +#[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))] +use rustix::fs::OFlags; +use std::ffi::OsStr; +use std::path::{Component, Path, PathBuf}; +use std::{fs, io, mem}; +#[cfg(windows)] +use { + crate::filesystem::primitives::{ + SymlinkKind, open_dir_unchecked, path_really_has_trailing_dot, + }, + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY, +}; + +/// Implement `open` by breaking up the path into components, resolving each +/// component individually, and resolving symbolic links manually. +pub(crate) fn open(start: &fs::File, path: &Path, options: &OpenOptions) -> io::Result { + let mut symlink_count = 0; + let start = MaybeOwnedFile::borrowed(start); + let maybe_owned = internal_open(start, path, options, &mut symlink_count, None)?; + maybe_owned.into_file(options) +} + +/// Context for performing manual component-at-a-time path resolution. +struct Context<'start> { + /// The current base directory handle for path lookups. + base: MaybeOwnedFile<'start>, + + /// The stack of directory handles below the base. + dirs: Vec>, + + /// The current worklist stack of path components to process. + components: Vec>, + + /// If requested, the canonical path is constructed here. + canonical_path: CanonicalPath<'start>, + + /// Does the path end in `/` or similar, so it requires a directory? + dir_required: bool, + + /// Are we requesting write permissions, so we can't open a directory? + dir_precluded: bool, + + /// Where there a trailing slash on the path? + trailing_slash: bool, + + /// If a path ends in `.`, `..`, or `/`, including after expanding + /// symlinks, we need to follow path resolution by opening `.` so that we + /// obtain a full `dir_options` file descriptor and confirm that we have + /// search rights in the last component. + follow_with_dot: bool, + + /// A `PathBuf` that we reuse for calling `read_link_one` to minimize + /// allocations. + reuse: PathBuf, +} + +impl<'start> Context<'start> { + /// Construct a new instance of `Self`. + fn new( + start: MaybeOwnedFile<'start>, + path: &'start Path, + _options: &OpenOptions, + canonical_path: Option<&'start mut PathBuf>, + ) -> Self { + let trailing_slash = path_has_trailing_slash(path); + let trailing_dot = path_has_trailing_dot(path); + let trailing_dotdot = path.ends_with(Component::ParentDir); + + let mut components: Vec = Vec::new(); + + #[cfg(windows)] + { + // Windows resolves `..` before doing filesystem lookups. + for component in path.components().map(CowComponent::borrowed) { + match component { + CowComponent::ParentDir + if !components.is_empty() && components.last().unwrap().is_normal() => + { + let _ = components.pop(); + } + _ => components.push(component), + } + } + components.reverse(); + } + + #[cfg(not(windows))] + { + // Add the path components to the worklist. Rust's `Path` + // normalizes away `.` components, however a trailing `.` affects + // path lookup, so special-case it here. + if trailing_dot { + components.push(CowComponent::CurDir); + } + components.extend(path.components().rev().map(CowComponent::borrowed)); + } + + Self { + base: start, + dirs: Vec::with_capacity(components.len()), + components, + canonical_path: CanonicalPath::new(canonical_path), + dir_required: trailing_slash, + + #[cfg(not(windows))] + dir_precluded: _options.write || _options.append, + + #[cfg(windows)] + dir_precluded: false, + + trailing_slash, + + follow_with_dot: trailing_dot | trailing_dotdot, + + reuse: PathBuf::new(), + } + } + + fn check_dot_access(&self) -> io::Result<()> { + // Manually check that we have permissions to search `self.base` to + // search for `.` in it, since we otherwise resolve `.` and `..` + // ourselves by just manipulating the `dirs` stack. + #[cfg(not(windows))] + { + // Use `faccess` with `AT_EACCESS`. `AT_EACCESS` is not often the + // right tool for the job; in POSIX, it's better to ask for errno + // than to ask for permission. But we use `check_dot_access` to + // check access for opening `.` and `..` in situations where we + // already have open handles to them, and now we're accessing them + // through different paths, and we need to check whether these + // paths allow us access. + // + // Android and Emscripten lack `AT_EACCESS`. + // + #[cfg(any(target_os = "emscripten", target_os = "android"))] + let at_flags = rustix::fs::AtFlags::empty(); + #[cfg(not(any(target_os = "emscripten", target_os = "android")))] + let at_flags = rustix::fs::AtFlags::EACCESS; + + // Always use `CurDir`, even though this code is used to check + // permissions for both `.` and `..`, because in both cases we + // already know we can access the referenced directory, and we + // just need to check for the ability to search for `.` or `..` + // within `self.base`, which should always be the same. And + // using `.` means we avoid asking the OS to access a `..` path + // for us. + Ok(rustix::fs::accessat( + &*self.base, + Component::CurDir.as_os_str(), + rustix::fs::Access::EXEC_OK, + at_flags, + )?) + } + #[cfg(windows)] + open_dir_unchecked(&self.base, Component::CurDir.as_ref()).map(|_| ()) + } + + /// Handle a "." path component. + fn cur_dir(&mut self) -> io::Result<()> { + // This is a no-op. If this occurs at the end of the path, it does + // imply that we need search access to the directory, and it requires + // we open a directory, however we'll handle that in the + // `follow_with_dot` check. + Ok(()) + } + + /// Handle a ".." path component. + fn parent_dir(&mut self) -> io::Result<()> { + // We hold onto all the parent directory descriptors so that we + // don't have to re-open anything when we encounter a `..`. This + // way, even if the directory is concurrently moved, we don't have + // to worry about `..` leaving the sandbox. + match self.dirs.pop() { + Some(dir) => { + // Check that we have permission to look up `..`. + self.check_dot_access()?; + + // Looks good. + self.base = dir; + } + None => return Err(errors::escape_attempt()), + } + assert!(self.canonical_path.pop()); + + Ok(()) + } + + /// Handle a "normal" path component. + fn normal( + &mut self, + one: &OsStr, + options: &OpenOptions, + symlink_count: &mut u8, + ) -> io::Result<()> { + // If there are more named components left, this will be a base + // directory from which to open subsequent components, so use "path" + // options (`O_PATH` on Linux). + let use_options = if self.components.is_empty() { + options.clone() + } else { + dir_options() + }; + + let dir_required = self.dir_required || use_options.dir_required; + + #[allow(clippy::redundant_clone)] + match open_unchecked( + &self.base, + one.as_ref(), + use_options + .clone() + .follow(FollowSymlinks::No) + .dir_required(dir_required), + ) { + Ok(file) => { + // Emulate `O_PATH` + `FollowSymlinks::Yes` on Linux. If `file` + // is a symlink, follow it. + #[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))] + if should_emulate_o_path(&use_options) { + match read_link_one( + &file, + Default::default(), + symlink_count, + mem::take(&mut self.reuse), + ) { + Ok(destination) => { + return self.push_symlink_destination(destination); + } + // If it isn't a symlink, handle it as normal. + // `readlinkat` returns `ENOENT` if the file isn't a + // symlink in this situation. + Err(err) if err.kind() == io::ErrorKind::NotFound => (), + // If `readlinkat` fails any other way, pass it on. + Err(err) => return Err(err), + } + } + + // Normal case + let prev_base = self.base.descend_to(MaybeOwnedFile::owned(file)); + self.dirs.push(prev_base); + self.canonical_path.push(one); + + Ok(()) + } + #[cfg(not(windows))] + Err(OpenUncheckedError::Symlink(err, ())) => { + self.maybe_last_component_symlink(one, symlink_count, options.follow, err) + } + #[cfg(windows)] + Err(OpenUncheckedError::Symlink(err, SymlinkKind::Dir)) => { + // If this is a Windows directory symlink, require a directory. + self.dir_required |= self.components.is_empty(); + self.maybe_last_component_symlink(one, symlink_count, options.follow, err) + } + #[cfg(windows)] + Err(OpenUncheckedError::Symlink(err, SymlinkKind::File)) => { + // If this is a Windows file symlink, preclude a directory. + self.dir_precluded = true; + self.maybe_last_component_symlink(one, symlink_count, options.follow, err) + } + Err(OpenUncheckedError::NotFound(err)) => Err(err), + Err(OpenUncheckedError::Other(err)) => { + // An error occurred. If this was the last component, and the + // error wasn't due to invalid inputs (eg. the path has an + // embedded NUL), record it as the last component of the + // canonical path, even if we couldn't open it. + if self.components.is_empty() && err.kind() != io::ErrorKind::InvalidInput { + self.canonical_path.push(one); + self.canonical_path.complete(); + } + Err(err) + } + } + } + + /// Dereference one symlink level. + fn symlink(&mut self, one: &OsStr, symlink_count: &mut u8) -> io::Result<()> { + let destination = + read_link_one(&self.base, one, symlink_count, mem::take(&mut self.reuse))?; + self.push_symlink_destination(destination) + } + + /// Push the components of `destination` onto the worklist stack. + fn push_symlink_destination(&mut self, destination: PathBuf) -> io::Result<()> { + let at_end = self.components.is_empty(); + let trailing_slash = path_has_trailing_slash(&destination); + let trailing_dot = path_has_trailing_dot(&destination); + let trailing_dotdot = destination.ends_with(Component::ParentDir); + + #[cfg(windows)] + { + // `path_has_trailing_dot` returns false so that we don't open `.` + // at the end of path resolution. But for determining the Windows + // symlink restrictions, we need to know whether the path really + // ends in a `.`. + let trailing_dot_really = path_really_has_trailing_dot(&destination); + + // Windows appears to disallow symlinks to paths with trailing + // slashes, slashdots, or slashdotdots. + if trailing_slash + || (trailing_dot_really && destination.as_os_str() != Component::CurDir.as_os_str()) + || (trailing_dotdot && destination.as_os_str() != Component::ParentDir.as_os_str()) + { + return Err(io::Error::from_raw_os_error(123)); + } + + // Windows resolves `..` before doing filesystem lookups. + let mut components: Vec = Vec::new(); + for component in destination.components().map(CowComponent::owned) { + match component { + CowComponent::ParentDir + if !components.is_empty() && components.last().unwrap().is_normal() => + { + let _ = components.pop(); + } + _ => components.push(component), + } + } + self.components.extend(components.into_iter().rev()); + } + + #[cfg(not(windows))] + { + // Rust's `Path` hides a trailing dot, so handle it manually. + if trailing_dot { + self.components.push(CowComponent::CurDir); + } + self.components + .extend(destination.components().rev().map(CowComponent::owned)); + } + + // Record whether the new components ended with a path that implies + // an open of `.` at the end of path resolution. + if at_end { + self.follow_with_dot |= trailing_dot | trailing_dotdot; + self.trailing_slash |= trailing_slash; + self.dir_required |= trailing_slash; + } + + // As an optimization, hold onto the `PathBuf` buffer for later reuse. + self.reuse = destination; + + Ok(()) + } + + /// Check whether this is the last component and we don't need + /// to dereference; otherwise call `Self::symlink`. + fn maybe_last_component_symlink( + &mut self, + one: &OsStr, + symlink_count: &mut u8, + follow: FollowSymlinks, + err: io::Error, + ) -> io::Result<()> { + if follow == FollowSymlinks::No && !self.trailing_slash && self.components.is_empty() { + self.canonical_path.push(one); + self.canonical_path.complete(); + return Err(err); + } + + self.symlink(one, symlink_count) + } +} + +/// Internal implementation of manual `open`, exposing some additional +/// parameters. +/// +/// Callers can request the canonical path by passing `Some` to +/// `canonical_path`. If the complete canonical path is processed, it will be +/// stored in the provided `&mut PathBuf`, even if the actual open fails. If +/// a failure occurs before the complete canonical path is processed, the +/// provided `&mut PathBuf` is cleared to empty. +/// +/// A note on lifetimes: `path` and `canonical_path` here don't strictly +/// need `'start`, but using them makes it easier to store them in the +/// `Context` struct. +pub(super) fn internal_open<'start>( + start: MaybeOwnedFile<'start>, + path: &'start Path, + options: &OpenOptions, + symlink_count: &mut u8, + canonical_path: Option<&'start mut PathBuf>, +) -> io::Result> { + // POSIX returns `ENOENT` on an empty path. TODO: On Windows, we should + // be compatible with what Windows does instead. + if path.as_os_str().is_empty() { + return Err(errors::no_such_file_or_directory()); + } + + let mut ctx = Context::new(start, path, options, canonical_path); + + while let Some(c) = ctx.components.pop() { + match c { + CowComponent::PrefixOrRootDir => return Err(errors::escape_attempt()), + CowComponent::CurDir => ctx.cur_dir()?, + CowComponent::ParentDir => ctx.parent_dir()?, + CowComponent::Normal(one) => ctx.normal(&one, options, symlink_count)?, + } + } + + // We've now finished all the path components other than any trailing `.`s, + // so we have the complete canonical path. + ctx.canonical_path.complete(); + + // If the path ended in `.` (explicit or implied) or `..`, we may have + // opened the directory with eg. `O_PATH` on Linux, or we may have skipped + // checking for search access to `.`, so re-open it. + if ctx.follow_with_dot { + if ctx.dir_precluded { + return Err(errors::is_directory()); + } + ctx.base = MaybeOwnedFile::owned(open_unchecked( + &ctx.base, + Component::CurDir.as_ref(), + options, + )?); + } + + Ok(ctx.base) +} + +/// Implement manual `stat` in a similar manner as manual `open`. +pub(crate) fn stat(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io::Result { + // POSIX returns `ENOENT` on an empty path. TODO: On Windows, we should + // be compatible with what Windows does instead. + if path.as_os_str().is_empty() { + return Err(errors::no_such_file_or_directory()); + } + + let mut options = OpenOptions::new(); + options.follow(follow); + let mut symlink_count = 0; + let mut ctx = Context::new(MaybeOwnedFile::borrowed(start), path, &options, None); + assert!(!ctx.dir_precluded); + + while let Some(c) = ctx.components.pop() { + match c { + CowComponent::PrefixOrRootDir => return Err(errors::escape_attempt()), + CowComponent::CurDir => ctx.cur_dir()?, + CowComponent::ParentDir => ctx.parent_dir()?, + CowComponent::Normal(one) => { + if ctx.components.is_empty() { + // If this is the last component, do a non-following + // `stat_unchecked` on it. + let stat = stat_unchecked(&ctx.base, one.as_ref(), FollowSymlinks::No)?; + + // If we weren't asked to follow symlinks, or it wasn't a + // symlink, we're done. + if options.follow == FollowSymlinks::No || !stat.file_type().is_symlink() { + if stat.is_dir() { + if ctx.dir_precluded { + return Err(errors::is_directory()); + } + } else if ctx.dir_required { + return Err(errors::is_not_directory()); + } + return Ok(stat); + } + + // On Windows, symlinks know whether they are a file or + // directory. + #[cfg(windows)] + if stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + ctx.dir_required = true; + } else { + ctx.dir_precluded = true; + } + + // If it was a symlink and we're asked to follow symlinks, + // dereference it. + ctx.symlink(&one, &mut symlink_count)? + } else { + // Otherwise open the path component normally. + ctx.normal(&one, &options, &mut symlink_count)? + } + } + } + } + + // If the path ended in `.` (explicit or implied) or `..`, we may have + // opened the directory with eg. `O_PATH` on Linux, or we may have skipped + // checking for search access to `.`, so re-check it. + if ctx.follow_with_dot { + if ctx.dir_precluded { + return Err(errors::is_directory()); + } + + ctx.check_dot_access()?; + } + + // If the path ended in `.` or `..`, we already have it open, so just do + // `.metadata()` on it. + Metadata::from_file(&ctx.base) +} + +/// Test whether the given options imply that we should treat an open file as +/// potentially being a symlink we need to follow, due to use of `O_PATH`. +#[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))] +fn should_emulate_o_path(use_options: &OpenOptions) -> bool { + (use_options.ext.custom_flags & (OFlags::PATH.bits() as i32)) == (OFlags::PATH.bits() as i32) + && use_options.follow == FollowSymlinks::Yes +} diff --git a/crates/wasi/src/filesystem/primitives/manually/read_link_one.rs b/crates/wasi/src/filesystem/primitives/manually/read_link_one.rs new file mode 100644 index 000000000000..9f54f58bdc60 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/manually/read_link_one.rs @@ -0,0 +1,36 @@ +use crate::filesystem::primitives::{MAX_SYMLINK_EXPANSIONS, errors, read_link_unchecked}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// This is a wrapper around `read_link_unchecked` which performs a single +/// symlink expansion on a single path component, and which enforces the +/// recursion limit. +pub(super) fn read_link_one( + base: &fs::File, + name: &OsStr, + symlink_count: &mut u8, + reuse: PathBuf, +) -> io::Result { + let name: &Path = name.as_ref(); + assert!( + name.as_os_str().is_empty() || name.file_name().is_some(), + "read_link_one expects a single normal path component, got '{}'", + name.display() + ); + assert!( + name.as_os_str().is_empty() || name.parent().unwrap().as_os_str().is_empty(), + "read_link_one expects a single normal path component, got '{}'", + name.display() + ); + + if *symlink_count == MAX_SYMLINK_EXPANSIONS { + return Err(errors::too_many_symlinks()); + } + + let destination = read_link_unchecked(base, name, reuse)?; + + *symlink_count += 1; + + Ok(destination) +} diff --git a/crates/wasi/src/filesystem/primitives/maybe_owned_file.rs b/crates/wasi/src/filesystem/primitives/maybe_owned_file.rs new file mode 100644 index 000000000000..aba86dd0c9d4 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/maybe_owned_file.rs @@ -0,0 +1,75 @@ +use crate::filesystem::primitives::{OpenOptions, open_unchecked}; +use std::ops::Deref; +use std::path::Component; +use std::{fmt, fs, io, mem}; + +/// Several places in the code need to be able to handle either owned or +/// borrowed [`std::fs::File]`s. Cloning a `File` to let them always have an +/// owned `File` is expensive and fallible, so use this `struct` to hold either +/// one, and implement [`Deref`] to allow them to be handled in a uniform way. +/// +/// This is similar to [`Cow`], except without the copy-on-write part ;-). +/// `Cow` requires a `Clone` implementation, which `File` doesn't have, and +/// most users of this type don't need copy-on-write behavior. +/// +/// And, this type has the special `descend_to`, which just does an assignment, +/// but also some useful assertion checks. +/// +/// [`Deref`]: std::ops::Deref +/// [`Cow`]: std::borrow::Cow +pub(super) enum MaybeOwnedFile<'borrow> { + Borrowed(&'borrow fs::File), + Owned(fs::File), +} + +impl<'borrow> MaybeOwnedFile<'borrow> { + /// Constructs a new `MaybeOwnedFile` which is not owned. + pub(super) fn borrowed(file: &'borrow fs::File) -> Self { + Self::Borrowed(file) + } + + /// Constructs a new `MaybeOwnedFile` which is owned. + pub(super) fn owned(file: fs::File) -> Self { + Self::Owned(file) + } + + /// Set this `MaybeOwnedFile` to a new owned file which is from a subtree + /// of the current file. Return a `MaybeOwnedFile` representing the + /// previous state. + pub(super) fn descend_to(&mut self, to: MaybeOwnedFile<'borrow>) -> Self { + mem::replace(self, to) + } + + /// Produce an owned `File`. This uses `open` on "." if needed to convert a + /// borrowed `File` to an owned one. + #[cfg_attr(windows, allow(dead_code))] + pub(super) fn into_file(self, options: &OpenOptions) -> io::Result { + match self { + Self::Owned(file) => Ok(file), + Self::Borrowed(file) => { + // The only situation in which we'd be asked to produce an owned + // `File` is when there's a need to open "." within a directory + // to obtain a new handle. + open_unchecked(file, Component::CurDir.as_ref(), options).map_err(Into::into) + } + } + } +} + +impl<'borrow> Deref for MaybeOwnedFile<'borrow> { + type Target = fs::File; + + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Borrowed(file) => file, + Self::Owned(file) => file, + } + } +} + +impl<'borrow> fmt::Debug for MaybeOwnedFile<'borrow> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.deref().fmt(f) + } +} diff --git a/crates/wasi/src/filesystem/primitives/metadata.rs b/crates/wasi/src/filesystem/primitives/metadata.rs new file mode 100644 index 000000000000..ee2d9d770c4a --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/metadata.rs @@ -0,0 +1,241 @@ +use crate::filesystem::primitives::{FileType, ImplFileTypeExt, ImplMetadataExt}; +use std::time::SystemTime; +use std::{fs, io}; + +/// Metadata information about a file. +/// +/// This corresponds to [`std::fs::Metadata`]. +/// +///
+/// We need to define our own version because the libstd `Metadata` doesn't +/// have a public constructor that we can use. +///
+#[derive(Debug, Clone)] +pub struct Metadata { + pub(crate) file_type: FileType, + pub(crate) len: u64, + pub(crate) modified: Option, + pub(crate) accessed: Option, + pub(crate) created: Option, + pub(crate) ext: ImplMetadataExt, +} + +#[allow(clippy::len_without_is_empty)] +impl Metadata { + /// Constructs a new instance of `Self` from the given [`std::fs::File`]. + #[inline] + pub fn from_file(file: &fs::File) -> io::Result { + let std = file.metadata()?; + let ext = ImplMetadataExt::from(file, &std)?; + let file_type = ImplFileTypeExt::from(file, &std)?; + Ok(Self::from_parts(std, ext, file_type)) + } + + /// Constructs a new instance of `Self` from the given + /// [`std::fs::Metadata`]. + /// + /// As with the comments in [`std::fs::Metadata::volume_serial_number`] and + /// nearby functions, some fields of the resulting metadata will be `None`. + /// + /// [`std::fs::Metadata::volume_serial_number`]: https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html#tymethod.volume_serial_number + #[cfg(windows)] + #[inline] + pub fn from_just_metadata(std: fs::Metadata) -> Self { + let ext = ImplMetadataExt::from_just_metadata(&std); + let file_type = ImplFileTypeExt::from_just_metadata(&std); + Self::from_parts(std, ext, file_type) + } + + #[inline] + fn from_parts(std: fs::Metadata, ext: ImplMetadataExt, file_type: FileType) -> Self { + Self { + file_type, + len: std.len(), + modified: std.modified().ok(), + accessed: std.accessed().ok(), + created: std.created().ok(), + ext, + } + } + + /// Returns the file type for this metadata. + /// + /// This corresponds to [`std::fs::Metadata::file_type`]. + #[inline] + pub const fn file_type(&self) -> FileType { + self.file_type + } + + /// Returns `true` if this metadata is for a directory. + /// + /// This corresponds to [`std::fs::Metadata::is_dir`]. + #[inline] + pub fn is_dir(&self) -> bool { + self.file_type.is_dir() + } + + /// Returns the size of the file, in bytes, this metadata is for. + /// + /// This corresponds to [`std::fs::Metadata::len`]. + #[inline] + pub const fn len(&self) -> u64 { + self.len + } + + /// Returns the last modification time listed in this metadata. + /// + /// This corresponds to [`std::fs::Metadata::modified`]. + #[inline] + pub fn modified(&self) -> io::Result { + self.modified.ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "modified time metadata not available on this platform", + ) + }) + } + + /// Returns the last access time of this metadata. + /// + /// This corresponds to [`std::fs::Metadata::accessed`]. + #[inline] + pub fn accessed(&self) -> io::Result { + self.accessed.ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "accessed time metadata not available on this platform", + ) + }) + } + + /// Returns the creation time listed in this metadata. + /// + /// This corresponds to [`std::fs::Metadata::created`]. + #[inline] + pub fn created(&self) -> io::Result { + self.created.ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "created time metadata not available on this platform", + ) + }) + } + + /// `MetadataExt` requires nightly to be implemented, but we sometimes + /// just need the file attributes. + #[cfg(windows)] + #[inline] + pub(crate) fn file_attributes(&self) -> u32 { + self.ext.file_attributes() + } +} + +/// Unix-specific extensions for [`MetadataExt`]. +/// +/// This corresponds to [`std::os::unix::fs::MetadataExt`]. +#[cfg(any(unix, target_os = "vxworks"))] +pub trait MetadataExt { + /// Returns the ID of the device containing the file. + fn dev(&self) -> u64; + /// Returns the inode number. + fn ino(&self) -> u64; + /// Returns the number of hard links pointing to this file. + fn nlink(&self) -> u64; + #[cfg(target_os = "vxworks")] + fn attrib(&self) -> u8; +} + +/// WASI-specific extensions for [`MetadataExt`]. +/// +/// This corresponds to [`std::os::wasi::fs::MetadataExt`]. +#[cfg(target_os = "wasi")] +pub trait MetadataExt { + /// Returns the ID of the device containing the file. + fn dev(&self) -> u64; + /// Returns the inode number. + fn ino(&self) -> u64; + /// Returns the number of hard links pointing to this file. + fn nlink(&self) -> u64; +} + +/// Windows-specific extensions to [`Metadata`]. +/// +/// This corresponds to [`std::os::windows::fs::MetadataExt`]. +#[cfg(windows)] +pub trait MetadataExt { + /// Returns the value of the `dwFileAttributes` field of this metadata. + fn file_attributes(&self) -> u32; +} + +#[cfg(unix)] +impl MetadataExt for Metadata { + #[inline] + fn dev(&self) -> u64 { + crate::filesystem::primitives::MetadataExt::dev(&self.ext) + } + + #[inline] + fn ino(&self) -> u64 { + crate::filesystem::primitives::MetadataExt::ino(&self.ext) + } + + #[inline] + fn nlink(&self) -> u64 { + crate::filesystem::primitives::MetadataExt::nlink(&self.ext) + } +} + +#[cfg(target_os = "wasi")] +impl MetadataExt for Metadata { + #[inline] + fn dev(&self) -> u64 { + crate::filesystem::primitives::MetadataExt::dev(&self.ext) + } + + #[inline] + fn ino(&self) -> u64 { + crate::filesystem::primitives::MetadataExt::ino(&self.ext) + } + + #[inline] + fn nlink(&self) -> u64 { + crate::filesystem::primitives::MetadataExt::nlink(&self.ext) + } +} + +#[cfg(target_os = "vxworks")] +impl MetadataExt for Metadata { + #[inline] + fn dev(&self) -> u64 { + self.ext.dev() + } + + #[inline] + fn ino(&self) -> u64 { + self.ext.ino() + } + + #[inline] + fn nlink(&self) -> u64 { + self.ext.nlink() + } +} + +#[cfg(windows)] +impl MetadataExt for Metadata { + #[inline] + fn file_attributes(&self) -> u32 { + self.ext.file_attributes() + } +} + +/// Extension trait to allow `volume_serial_number` etc. to be exposed by +/// the `cap-fs-ext` crate. +/// +/// This is hidden from the main API since this functionality isn't present in +/// `std`. Use `cap_fs_ext::MetadataExt` instead of calling this directly. +#[cfg(windows)] +#[doc(hidden)] +pub trait _WindowsByHandle { + fn number_of_links(&self) -> Option; +} diff --git a/crates/wasi/src/filesystem/primitives/mod.rs b/crates/wasi/src/filesystem/primitives/mod.rs new file mode 100644 index 000000000000..6ce84fb66ef9 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/mod.rs @@ -0,0 +1,86 @@ +//! Filesystem utilities. + +#![allow( + trivial_numeric_casts, + reason = "preexisting from when cap-primitives was imported" +)] +#![allow( + unsafe_op_in_unsafe_fn, + reason = "preexisting from when cap-primitives was imported" +)] +#![allow( + clippy::unnecessary_fallible_conversions, + reason = "platform-agnostic code can't always take advantage of this" +)] +#![allow( + clippy::allow_attributes_without_reason, + reason = "preexisting from when cap-primitives was imported" +)] + +mod create_dir; +mod dir_entry; +mod dir_options; +mod file_type; +mod follow_symlinks; +mod hard_link; +mod maybe_owned_file; +mod metadata; +mod open; +mod open_dir; +mod open_options; +mod open_unchecked_error; +mod read_dir; +mod read_link; +mod remove_dir; +mod remove_file; +mod rename; +mod set_times; +mod stat; +mod symlink; + +pub(crate) mod errors; +pub(crate) mod manually; +pub(crate) mod via_parent; + +use maybe_owned_file::MaybeOwnedFile; + +pub(crate) use open_unchecked_error::*; + +#[cfg(not(windows))] +mod rustix; +#[cfg(not(windows))] +pub(crate) use self::rustix::fs::*; +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub(crate) use self::windows::fs::*; + +pub use create_dir::create_dir; +pub use dir_entry::DirEntry; +pub use dir_options::DirOptions; +#[cfg(windows)] +pub use file_type::_WindowsFileTypeExt; +pub use file_type::FileType; +#[cfg(any(unix, target_os = "vxworks"))] +pub use file_type::FileTypeExt; +pub use follow_symlinks::FollowSymlinks; +pub use hard_link::hard_link; +#[cfg(windows)] +pub use metadata::_WindowsByHandle; +pub use metadata::{Metadata, MetadataExt}; +pub use open::open; +pub use open_dir::*; +pub use open_options::*; +pub use read_dir::read_base_dir; +pub use read_link::read_link; +pub use remove_dir::remove_dir; +pub use remove_file::remove_file; +pub use rename::rename; +pub use set_times::{set_times, set_times_nofollow}; +pub use stat::stat; +#[cfg(not(windows))] +pub use symlink::symlink; +#[cfg(windows)] +pub use symlink::{symlink_dir, symlink_file}; +#[cfg(test)] +mod tests; diff --git a/crates/wasi/src/filesystem/primitives/open.rs b/crates/wasi/src/filesystem/primitives/open.rs new file mode 100644 index 000000000000..5fc94be9eb4c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/open.rs @@ -0,0 +1,14 @@ +//! This defines `open`, the primary entrypoint to sandboxed file and directory +//! opening. + +use crate::filesystem::primitives::{OpenOptions, open_impl}; +use std::path::Path; +use std::{fs, io}; + +/// Perform an `openat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. +#[inline] +pub fn open(start: &fs::File, path: &Path, options: &OpenOptions) -> io::Result { + // Call the underlying implementation. + open_impl(start, path, options) +} diff --git a/crates/wasi/src/filesystem/primitives/open_dir.rs b/crates/wasi/src/filesystem/primitives/open_dir.rs new file mode 100644 index 000000000000..351b57af1a52 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/open_dir.rs @@ -0,0 +1,49 @@ +//! This defines `open_dir`, a wrapper around `open` which can be used to open +//! path as a directory. + +#[allow(unused_imports)] +use crate::filesystem::primitives::open_unchecked; +use crate::filesystem::primitives::{ + FollowSymlinks, dir_options, open, open_ambient_dir_impl, readdir_options, +}; +use std::path::Path; +use std::{fs, io}; + +/// Open a directory by performing an `openat`-like operation, +/// ensuring that the resolution of the path never escapes +/// the directory tree rooted at `start`. +#[inline] +pub fn open_dir(start: &fs::File, path: &Path) -> io::Result { + open(start, path, &dir_options()) +} + +/// Open a directory by performing an unsandboxed `openat`-like operation. +#[inline] +#[allow(dead_code)] +pub(crate) fn open_dir_unchecked(start: &fs::File, path: &Path) -> io::Result { + open_unchecked(start, path, &dir_options()).map_err(Into::into) +} + +/// Like `open_dir_unchecked`, but additionally request the ability to read the +/// directory entries. +#[inline] +#[allow(dead_code)] +pub(crate) fn open_dir_for_reading_unchecked( + start: &fs::File, + path: &Path, + follow: FollowSymlinks, +) -> io::Result { + open_unchecked(start, path, readdir_options().follow(follow)).map_err(Into::into) +} + +/// Open a directory named by a bare path, using the host process' ambient +/// authority. +/// +/// # Ambient Authority +/// +/// This function is not sandboxed and may trivially access any path that the +/// host process has access to. +#[inline] +pub fn open_ambient_dir(path: &Path) -> io::Result { + open_ambient_dir_impl(path) +} diff --git a/crates/wasi/src/filesystem/primitives/open_options.rs b/crates/wasi/src/filesystem/primitives/open_options.rs new file mode 100644 index 000000000000..73b43e2ef287 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/open_options.rs @@ -0,0 +1,327 @@ +use crate::filesystem::primitives::{FollowSymlinks, ImplOpenOptionsExt}; + +/// Options and flags which can be used to configure how a file is opened. +/// +/// This corresponds to [`std::fs::OpenOptions`]. +/// +/// This `OpenOptions` has no `open` method. To open a file with an +/// `OptionOptions`, first obtain a [`Dir`] containing the path, and then call +/// [`Dir::open_with`]. +/// +/// [`Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html +/// [`Dir::open_with`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html#method.open_with +/// +///
+/// We need to define our own version because the libstd `OpenOptions` doesn't +/// have public accessors that we can use. +///
+#[derive(Debug, Clone)] +pub struct OpenOptions { + pub(crate) read: bool, + pub(crate) write: bool, + pub(crate) append: bool, + pub(crate) truncate: bool, + pub(crate) create: bool, + pub(crate) create_new: bool, + pub(crate) dir_required: bool, + #[cfg(windows)] + pub(crate) maybe_dir: bool, + pub(crate) sync: bool, + pub(crate) dsync: bool, + #[cfg(not(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + target_os = "freebsd", + target_os = "fuchsia", + windows, + )))] + pub(crate) rsync: bool, + #[cfg(not(windows))] + pub(crate) nonblock: bool, + pub(crate) readdir_required: bool, + pub(crate) follow: FollowSymlinks, + + #[cfg(any(unix, windows, target_os = "vxworks"))] + pub(crate) ext: ImplOpenOptionsExt, +} + +impl OpenOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// This corresponds to [`std::fs::OpenOptions::new`]. + #[allow(clippy::new_without_default)] + #[inline] + pub const fn new() -> Self { + Self { + read: false, + write: false, + append: false, + truncate: false, + create: false, + create_new: false, + dir_required: false, + #[cfg(windows)] + maybe_dir: false, + sync: false, + dsync: false, + #[cfg(not(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + target_os = "freebsd", + target_os = "fuchsia", + windows, + )))] + rsync: false, + #[cfg(not(windows))] + nonblock: false, + readdir_required: false, + follow: FollowSymlinks::Yes, + + #[cfg(any(unix, windows, target_os = "vxworks"))] + ext: ImplOpenOptionsExt::new(), + } + } + + /// Sets the option for read access. + /// + /// This corresponds to [`std::fs::OpenOptions::read`]. + #[inline] + pub fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + + /// Sets the option for write access. + /// + /// This corresponds to [`std::fs::OpenOptions::write`]. + #[inline] + pub fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } + + /// Sets the option for truncating a previous file. + /// + /// This corresponds to [`std::fs::OpenOptions::truncate`]. + #[inline] + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + /// Sets the option to create a new file. + /// + /// This corresponds to [`std::fs::OpenOptions::create`]. + #[inline] + pub fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + + /// Sets the option to always create a new file. + /// + /// This corresponds to [`std::fs::OpenOptions::create_new`]. + #[inline] + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } + + /// Sets the option to enable or suppress following of symlinks. + #[inline] + pub(crate) fn follow(&mut self, follow: FollowSymlinks) -> &mut Self { + self.follow = follow; + self + } + + /// Sets the option to enable an error if the opened object is not a + /// directory. + #[inline] + pub(crate) fn dir_required(&mut self, dir_required: bool) -> &mut Self { + self.dir_required = dir_required; + self + } + + /// Sets the option to request the ability to read directory entries. + #[inline] + pub(crate) fn readdir_required(&mut self, readdir_required: bool) -> &mut Self { + self.readdir_required = readdir_required; + self + } + + /// Wrapper to allow `follow` to be exposed by the `cap-fs-ext` crate. + /// + /// This is hidden from the main API since this functionality isn't present + /// in `std`. Use `cap_fs_ext::OpenOptionsFollowExt` instead of calling + /// this directly. + #[doc(hidden)] + #[inline] + pub fn _cap_fs_ext_follow(&mut self, follow: FollowSymlinks) -> &mut Self { + self.follow(follow) + } +} + +/// Unix-specific extensions to [`fs::OpenOptions`]. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub trait OpenOptionsExt { + /// Pass custom flags to the `flags` argument of `open`. + fn custom_flags(&mut self, flags: i32) -> &mut Self; +} + +/// WASI-specific extensions to [`fs::OpenOptions`]. +#[cfg(target_os = "wasi")] +pub trait OpenOptionsExt { + /// Pass custom `dirflags` argument to `path_open`. + fn lookup_flags(&mut self, flags: u32) -> &mut Self; + + /// Indicates whether `OpenOptions` must open a directory or not. + fn directory(&mut self, dir: bool) -> &mut Self; + + /// Indicates whether `__WASI_FDFLAG_DSYNC` is passed in the `fs_flags` + /// field of `path_open`. + fn dsync(&mut self, dsync: bool) -> &mut Self; + + /// Indicates whether `__WASI_FDFLAG_NONBLOCK` is passed in the `fs_flags` + /// field of `path_open`. + fn nonblock(&mut self, nonblock: bool) -> &mut Self; + + /// Indicates whether `__WASI_FDFLAG_RSYNC` is passed in the `fs_flags` + /// field of `path_open`. + fn rsync(&mut self, rsync: bool) -> &mut Self; + + /// Indicates whether `__WASI_FDFLAG_SYNC` is passed in the `fs_flags` + /// field of `path_open`. + fn sync(&mut self, sync: bool) -> &mut Self; + + /// Indicates the value that should be passed in for the `fs_rights_base` + /// parameter of `path_open`. + fn fs_rights_base(&mut self, rights: u64) -> &mut Self; + + /// Indicates the value that should be passed in for the + /// `fs_rights_inheriting` parameter of `path_open`. + fn fs_rights_inheriting(&mut self, rights: u64) -> &mut Self; + + /// Open a file or directory. + fn open_at>( + &self, + file: &std::fs::File, + path: P, + ) -> std::io::Result; +} + +/// Windows-specific extensions to [`fs::OpenOptions`]. +#[cfg(windows)] +pub trait OpenOptionsExt { + /// Overrides the `dwDesiredAccess` argument to the call to [`CreateFile`] + /// with the specified value. + fn access_mode(&mut self, access: u32) -> &mut Self; + + /// Overrides the `dwShareMode` argument to the call to [`CreateFile`] with + /// the specified value. + fn share_mode(&mut self, val: u32) -> &mut Self; + + /// Sets extra flags for the `dwFileFlags` argument to the call to + /// [`CreateFile2`] to the specified value (or combines it with + /// `attributes` and `security_qos_flags` to set the `dwFlagsAndAttributes` + /// for [`CreateFile`]). + /// + /// [`CreateFile`]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea + /// [`CreateFile2`]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfile2 + fn custom_flags(&mut self, flags: u32) -> &mut Self; +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +impl OpenOptionsExt for OpenOptions { + #[inline] + fn custom_flags(&mut self, flags: i32) -> &mut Self { + self.ext.custom_flags(flags); + self + } +} + +#[cfg(target_os = "wasi")] +impl OpenOptionsExt for OpenOptions { + fn lookup_flags(&mut self, _: u32) -> &mut Self { + todo!() + } + + fn directory(&mut self, dir_required: bool) -> &mut Self { + self.dir_required = dir_required; + self + } + + fn dsync(&mut self, _: bool) -> &mut Self { + todo!() + } + + fn nonblock(&mut self, _: bool) -> &mut Self { + todo!() + } + + fn rsync(&mut self, _: bool) -> &mut Self { + todo!() + } + + fn sync(&mut self, _: bool) -> &mut Self { + todo!() + } + + fn fs_rights_base(&mut self, _: u64) -> &mut Self { + todo!() + } + + fn fs_rights_inheriting(&mut self, _: u64) -> &mut Self { + todo!() + } + + fn open_at

(&self, dirfd: &std::fs::File, path: P) -> Result + where + P: AsRef, + { + crate::fs::open(dirfd, path.as_ref(), self) + } +} + +#[cfg(target_os = "vxworks")] +impl OpenOptionsExt for OpenOptions { + #[inline] + fn mode(&mut self, mode: u32) -> &mut Self { + self.ext.mode(mode); + self + } + + #[inline] + fn custom_flags(&mut self, flags: i32) -> &mut Self { + self.ext.custom_flags(flags); + self + } +} + +#[cfg(windows)] +impl OpenOptionsExt for OpenOptions { + #[inline] + fn access_mode(&mut self, access: u32) -> &mut Self { + self.ext.access_mode(access); + self + } + + /// To prevent race conditions on Windows, handles for directories must be + /// opened without `FILE_SHARE_DELETE`. + #[inline] + fn share_mode(&mut self, val: u32) -> &mut Self { + self.ext.share_mode(val); + self + } + + #[inline] + fn custom_flags(&mut self, flags: u32) -> &mut Self { + self.ext.custom_flags(flags); + self + } +} diff --git a/crates/wasi/src/filesystem/primitives/open_unchecked_error.rs b/crates/wasi/src/filesystem/primitives/open_unchecked_error.rs new file mode 100644 index 000000000000..09ad9a6fca6b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/open_unchecked_error.rs @@ -0,0 +1,37 @@ +use std::io; + +#[derive(Debug)] +pub(crate) enum OpenUncheckedError { + Other(io::Error), + Symlink(io::Error, SymlinkKind), + NotFound(io::Error), +} + +#[cfg(not(windows))] +pub(crate) type SymlinkKind = (); + +#[cfg(windows)] +#[derive(Debug)] +pub(crate) enum SymlinkKind { + File, + Dir, +} + +impl OpenUncheckedError { + #[allow(dead_code)] + pub(crate) fn kind(&self) -> io::ErrorKind { + match self { + Self::Other(err) | Self::Symlink(err, _) | Self::NotFound(err) => err.kind(), + } + } +} + +impl From for io::Error { + fn from(error: OpenUncheckedError) -> Self { + match error { + OpenUncheckedError::Other(err) + | OpenUncheckedError::Symlink(err, _) + | OpenUncheckedError::NotFound(err) => err, + } + } +} diff --git a/crates/wasi/src/filesystem/primitives/read_dir.rs b/crates/wasi/src/filesystem/primitives/read_dir.rs new file mode 100644 index 000000000000..dd6fd0c07f8d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/read_dir.rs @@ -0,0 +1,38 @@ +use crate::filesystem::primitives::{DirEntry, ReadDirInner}; +use std::{fmt, fs, io}; + +/// Like `read_dir` but operates on the base directory itself, rather than +/// on a path based on it. +#[inline] +pub fn read_base_dir(start: &fs::File) -> io::Result { + Ok(ReadDir { + inner: ReadDirInner::read_base_dir(start)?, + }) +} + +/// Iterator over the entries in a directory. +/// +/// This corresponds to [`std::fs::ReadDir`]. +/// +/// There is no `from_std` method, as `std::fs::ReadDir` doesn't provide a way +/// to construct a `ReadDir` without opening directories by ambient paths. +pub struct ReadDir { + pub(crate) inner: ReadDirInner, +} + +impl Iterator for ReadDir { + type Item = io::Result; + + #[inline] + fn next(&mut self) -> Option { + self.inner + .next() + .map(|inner| inner.map(|inner| DirEntry { inner })) + } +} + +impl fmt::Debug for ReadDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} diff --git a/crates/wasi/src/filesystem/primitives/read_link.rs b/crates/wasi/src/filesystem/primitives/read_link.rs new file mode 100644 index 000000000000..9678ea08e1be --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/read_link.rs @@ -0,0 +1,36 @@ +//! This defines `read_link`, the primary entrypoint to sandboxed symbolic link +//! dereferencing. + +use crate::filesystem::primitives::{errors, read_link_impl}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// Perform a `readlinkat`-like operation, ensuring that the resolution of the +/// link path never escapes the directory tree rooted at `start`. +#[inline] +pub fn read_link_contents(start: &fs::File, path: &Path) -> io::Result { + // Call the underlying implementation. + read_link_impl(start, path) +} + +/// Perform a `readlinkat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`, and also verifies +/// that the link target is not absolute. +#[inline] +pub fn read_link(start: &fs::File, path: &Path) -> io::Result { + // Call the underlying implementation. + let result = read_link_contents(start, path); + + // Don't allow reading symlinks to absolute paths. This isn't strictly + // necessary to preserve the sandbox, since `open` will refuse to follow + // absolute paths in any case. However, it is useful to enforce this + // restriction to avoid leaking information about the host filesystem + // outside the sandbox. + if let Ok(path) = &result { + if path.has_root() { + return Err(errors::escape_attempt()); + } + } + + result +} diff --git a/crates/wasi/src/filesystem/primitives/remove_dir.rs b/crates/wasi/src/filesystem/primitives/remove_dir.rs new file mode 100644 index 000000000000..adb98caea2e4 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/remove_dir.rs @@ -0,0 +1,14 @@ +//! This defines `remove_dir`, the primary entrypoint to sandboxed file +//! removal. + +use crate::filesystem::primitives::remove_dir_impl; +use std::path::Path; +use std::{fs, io}; + +/// Perform a `rmdirat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. +#[inline] +pub fn remove_dir(start: &fs::File, path: &Path) -> io::Result<()> { + // Call the underlying implementation. + remove_dir_impl(start, path) +} diff --git a/crates/wasi/src/filesystem/primitives/remove_file.rs b/crates/wasi/src/filesystem/primitives/remove_file.rs new file mode 100644 index 000000000000..a678ea039bd8 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/remove_file.rs @@ -0,0 +1,14 @@ +//! This defines `remove_file`, the primary entrypoint to sandboxed file +//! removal. + +use crate::filesystem::primitives::remove_file_impl; +use std::path::Path; +use std::{fs, io}; + +/// Perform a `remove_fileat`-like operation, ensuring that the resolution of +/// the path never escapes the directory tree rooted at `start`. +#[inline] +pub fn remove_file(start: &fs::File, path: &Path) -> io::Result<()> { + // Call the underlying implementation. + remove_file_impl(start, path) +} diff --git a/crates/wasi/src/filesystem/primitives/rename.rs b/crates/wasi/src/filesystem/primitives/rename.rs new file mode 100644 index 000000000000..18ce6930c5cf --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rename.rs @@ -0,0 +1,19 @@ +//! This defines `rename`, the primary entrypoint to sandboxed renaming. + +use crate::filesystem::primitives::rename_impl; +use std::path::Path; +use std::{fs, io}; + +/// Perform a `renameat`-like operation, ensuring that the resolution of both +/// the old and new paths never escape the directory tree rooted at their +/// respective starts. +#[inline] +pub fn rename( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + // Call the underlying implementation. + rename_impl(old_start, old_path, new_start, new_path) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/check.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/check.rs new file mode 100644 index 000000000000..6e7c70605175 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/check.rs @@ -0,0 +1,40 @@ +use rustix::cstr; +use rustix::fs::{AtFlags, CWD, Mode, OFlags, openat, statat}; +use rustix::io::Errno; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + +static WORKING: AtomicBool = AtomicBool::new(false); +static CHECKED: AtomicBool = AtomicBool::new(false); + +#[inline] +pub(crate) fn beneath_supported() -> bool { + if WORKING.load(Relaxed) { + return true; + } + if CHECKED.load(Relaxed) { + return false; + } + check_beneath_supported() +} + +#[cold] +fn check_beneath_supported() -> bool { + // `RESOLVE_BENEATH` was introduced in FreeBSD 13, but opening `..` within + // the root directory re-opened the root directory. In FreeBSD 14, it fails + // as cap-std expects. + if let Ok(root) = openat( + CWD, + cstr!("/"), + OFlags::RDONLY | OFlags::CLOEXEC, + Mode::empty(), + ) { + // Unknown O_ flags get ignored but AT_ flags have strict checks, so we use that. + if let Err(Errno::NOTCAPABLE) = statat(root, cstr!(".."), AtFlags::RESOLVE_BENEATH) { + WORKING.store(true, Relaxed); + return true; + } + } + + CHECKED.store(true, Relaxed); + false +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/mod.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/mod.rs new file mode 100644 index 000000000000..dd8fcfc5472b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/mod.rs @@ -0,0 +1,13 @@ +mod check; +mod open_impl; +mod remove_dir_impl; +mod remove_file_impl; +mod set_times_impl; +mod stat_impl; + +pub(crate) use check::beneath_supported; +pub(crate) use open_impl::open_impl; +pub(crate) use remove_dir_impl::remove_dir_impl; +pub(crate) use remove_file_impl::remove_file_impl; +pub(crate) use set_times_impl::{set_times_impl, set_times_nofollow_impl}; +pub(crate) use stat_impl::stat_impl; diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/open_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/open_impl.rs new file mode 100644 index 000000000000..d29ce5b9f198 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/open_impl.rs @@ -0,0 +1,29 @@ +use super::super::super::fs::compute_oflags; +use crate::filesystem::primitives::{OpenOptions, errors, manually}; +use rustix::fs::{Mode, OFlags, RawMode, openat}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn open_impl( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> io::Result { + if !super::beneath_supported() { + return manually::open(start, path, options); + } + + let oflags = compute_oflags(options)? | OFlags::RESOLVE_BENEATH; + + let mode = if oflags.contains(OFlags::CREATE) { + Mode::from_bits((options.ext.mode & 0o7777) as RawMode).unwrap() + } else { + Mode::empty() + }; + + match openat(start, path, oflags, mode) { + Ok(file) => Ok(file.into()), + Err(rustix::io::Errno::NOTCAPABLE) => Err(errors::escape_attempt()), + Err(err) => Err(err.into()), + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/remove_dir_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/remove_dir_impl.rs new file mode 100644 index 000000000000..e4a440dbfbee --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/remove_dir_impl.rs @@ -0,0 +1,16 @@ +use crate::filesystem::primitives::via_parent; +use rustix::fs::{AtFlags, unlinkat}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn remove_dir_impl(start: &fs::File, path: &Path) -> io::Result<()> { + if !super::beneath_supported() { + return via_parent::remove_dir(start, path); + } + + Ok(unlinkat( + start, + path, + AtFlags::RESOLVE_BENEATH | AtFlags::REMOVEDIR, + )?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/remove_file_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/remove_file_impl.rs new file mode 100644 index 000000000000..5c50e640976e --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/remove_file_impl.rs @@ -0,0 +1,12 @@ +use crate::filesystem::primitives::via_parent; +use rustix::fs::{AtFlags, unlinkat}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn remove_file_impl(start: &fs::File, path: &Path) -> io::Result<()> { + if !super::beneath_supported() { + return via_parent::remove_file(start, path); + } + + Ok(unlinkat(start, path, AtFlags::RESOLVE_BENEATH)?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/set_times_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/set_times_impl.rs new file mode 100644 index 000000000000..d11303eb1169 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/set_times_impl.rs @@ -0,0 +1,46 @@ +use crate::filesystem::primitives::{to_timespec, via_parent}; +use rustix::fs::{AtFlags, Timestamps, utimensat}; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +pub(crate) fn set_times_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + if !super::beneath_supported() { + return super::super::super::fs::set_times_manually(start, path, atime, mtime); + } + + let times = Timestamps { + last_access: to_timespec(atime)?, + last_modification: to_timespec(mtime)?, + }; + + Ok(utimensat(start, path, ×, AtFlags::RESOLVE_BENEATH)?) +} + +pub(crate) fn set_times_nofollow_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + if !super::beneath_supported() { + return via_parent::set_times_nofollow(start, path, atime, mtime); + } + + let times = Timestamps { + last_access: to_timespec(atime)?, + last_modification: to_timespec(mtime)?, + }; + + Ok(utimensat( + start, + path, + ×, + AtFlags::RESOLVE_BENEATH | AtFlags::SYMLINK_NOFOLLOW, + )?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/stat_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/stat_impl.rs new file mode 100644 index 000000000000..12993601186d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/fs/stat_impl.rs @@ -0,0 +1,22 @@ +use crate::filesystem::primitives::{FollowSymlinks, ImplMetadataExt, Metadata, manually}; +use rustix::fs::{AtFlags, statat}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn stat_impl( + start: &fs::File, + path: &Path, + follow: FollowSymlinks, +) -> io::Result { + if !super::beneath_supported() { + return manually::stat(start, path, follow); + } + + let flags = AtFlags::RESOLVE_BENEATH + | if follow == FollowSymlinks::Yes { + AtFlags::empty() + } else { + AtFlags::SYMLINK_NOFOLLOW + }; + Ok(ImplMetadataExt::from_rustix(statat(start, path, flags)?)) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/freebsd/mod.rs b/crates/wasi/src/filesystem/primitives/rustix/freebsd/mod.rs new file mode 100644 index 000000000000..aabc2afc5ad4 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/freebsd/mod.rs @@ -0,0 +1 @@ +pub(crate) mod fs; diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/create_dir_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/create_dir_unchecked.rs new file mode 100644 index 000000000000..246bed20da64 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/create_dir_unchecked.rs @@ -0,0 +1,19 @@ +use crate::filesystem::primitives::DirOptions; +use rustix::fs::{Mode, RawMode, mkdirat}; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `create_dir`, but which does not perform +/// sandboxing. +pub(crate) fn create_dir_unchecked( + start: &fs::File, + path: &Path, + options: &DirOptions, +) -> io::Result<()> { + #[cfg(not(target_os = "wasi"))] + let raw_mode = options.ext.mode as RawMode; + #[cfg(target_os = "wasi")] + let raw_mode = 0; + + Ok(mkdirat(start, path, Mode::from_bits(raw_mode).unwrap())?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/cvt.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/cvt.rs new file mode 100644 index 000000000000..a6aa15b35fd4 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/cvt.rs @@ -0,0 +1,19 @@ +use std::io; + +#[allow(dead_code)] +pub(crate) fn cvt_i32(t: i32) -> io::Result { + if t == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(t) + } +} + +#[allow(dead_code)] +pub(crate) fn cvt_i64(t: i64) -> io::Result { + if t == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(t) + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/dir_entry_inner.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/dir_entry_inner.rs new file mode 100644 index 000000000000..450f6df688bd --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/dir_entry_inner.rs @@ -0,0 +1,41 @@ +use crate::filesystem::primitives::{Metadata, ReadDirInner}; +use rustix::fs::DirEntry; +use std::ffi::{OsStr, OsString}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::OsStrExt; +use std::{fmt, io}; + +pub(crate) struct DirEntryInner { + pub(super) rustix: DirEntry, + pub(super) read_dir: ReadDirInner, +} + +impl DirEntryInner { + #[inline] + pub(crate) fn metadata(&self) -> io::Result { + self.read_dir.metadata(self.file_name_bytes()) + } + + #[inline] + pub(crate) fn file_name(&self) -> OsString { + self.file_name_bytes().to_os_string() + } + + #[inline] + pub(crate) fn ino(&self) -> u64 { + self.rustix.ino() + } + + fn file_name_bytes(&self) -> &OsStr { + OsStr::from_bytes(self.rustix.file_name().to_bytes()) + } +} + +impl fmt::Debug for DirEntryInner { + // Like libstd's version, but doesn't print the path. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("DirEntry").field(&self.file_name()).finish() + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/dir_options_ext.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/dir_options_ext.rs new file mode 100644 index 000000000000..474d6aea40ed --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/dir_options_ext.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone)] +pub(crate) struct DirOptionsExt { + pub(super) mode: u32, +} + +impl DirOptionsExt { + pub(crate) const fn new() -> Self { + Self { + // The default value; see + // + mode: 0o777, + } + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/dir_utils.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/dir_utils.rs new file mode 100644 index 000000000000..078b7042f24d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/dir_utils.rs @@ -0,0 +1,212 @@ +use crate::filesystem::primitives::OpenOptions; +use rustix::fs::OFlags; +use std::ffi::{OsStr, OsString}; +use std::ops::Deref; +#[cfg(unix)] +use std::os::unix::{ + ffi::{OsStrExt, OsStringExt}, + fs::OpenOptionsExt, +}; +#[cfg(target_os = "wasi")] +use std::os::wasi::{ + ffi::{OsStrExt, OsStringExt}, + fs::OpenOptionsExt, +}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// Rust's `Path` implicitly strips redundant slashes, however they aren't +/// redundant in one case: at the end of a path they indicate that a path is +/// expected to name a directory. +pub(crate) fn path_requires_dir(path: &Path) -> bool { + let bytes = path.as_os_str().as_bytes(); + + // If a path ends with '/' or '.', it's a directory. These aren't the only + // cases, but they are the only cases that Rust's `Path` implicitly + // normalizes away. + bytes.ends_with(b"/") || bytes.ends_with(b"/.") +} + +/// Rust's `Path` implicitly strips trailing `.` components, however they +/// aren't redundant in one case: at the end of a path they are the final path +/// component, which has different path lookup behavior. +pub(crate) fn path_has_trailing_dot(path: &Path) -> bool { + let mut bytes = path.as_os_str().as_bytes(); + + // If a path ends with '.' followed by any number of '/'s, it's a trailing dot. + while let Some((last, rest)) = bytes.split_last() { + if *last == b'/' { + bytes = rest; + } else { + break; + } + } + + bytes.ends_with(b"/.") || bytes == b"." +} + +/// Rust's `Path` implicitly strips trailing `/`s, however they aren't +/// redundant in one case: at the end of a path they are the final path +/// component, which has different path lookup behavior. +pub(crate) fn path_has_trailing_slash(path: &Path) -> bool { + let bytes = path.as_os_str().as_bytes(); + + bytes.ends_with(b"/") +} + +/// Append a trailing `/`. This can be used to require that the given `path` +/// names a directory. +pub(crate) fn append_dir_suffix(path: PathBuf) -> PathBuf { + let mut bytes = path.into_os_string().into_vec(); + bytes.push(b'/'); + OsString::from_vec(bytes).into() +} + +/// Strip trailing `/`s, unless this reduces `path` to `/` itself. This is +/// used by `create_dir` and others to prevent paths like `foo/` from +/// canonicalizing to `foo/.` since these syscalls treat these differently. +#[allow(clippy::indexing_slicing)] +pub(crate) fn strip_dir_suffix(path: &Path) -> impl Deref + '_ { + let mut bytes = path.as_os_str().as_bytes(); + while bytes.len() > 1 && *bytes.last().unwrap() == b'/' { + bytes = &bytes[..bytes.len() - 1]; + } + OsStr::from_bytes(bytes).as_ref() +} + +/// Return an `OpenOptions` for opening directories. +pub(crate) fn dir_options() -> OpenOptions { + OpenOptions::new().read(true).dir_required(true).clone() +} + +/// Like `dir_options`, but additionally request the ability to read the +/// directory entries. +pub(crate) fn readdir_options() -> OpenOptions { + OpenOptions::new() + .read(true) + .dir_required(true) + .readdir_required(true) + .clone() +} + +/// Open a directory named by a bare path, using the host process' ambient +/// authority. +/// +/// # Ambient Authority +/// +/// This function is not sandboxed and may trivially access any path that the +/// host process has access to. +pub(crate) fn open_ambient_dir_impl(path: &Path) -> io::Result { + let mut options = fs::OpenOptions::new(); + options.read(true); + + // This is for `std::fs`, so we don't have `dir_required`, so set + // `O_DIRECTORY` manually. + options.custom_flags((OFlags::DIRECTORY | target_o_path()).bits() as i32); + + options.open(path) +} + +/// Use `O_PATH` on platforms which have it, or none otherwise. +#[inline] +pub(crate) const fn target_o_path() -> OFlags { + #[cfg(any( + target_os = "android", + target_os = "emscripten", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux", + target_os = "redox", + ))] + { + OFlags::PATH + } + + #[cfg(any( + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "wasi", + target_os = "illumos", + target_os = "solaris", + ))] + { + OFlags::empty() + } +} + +#[test] +fn strip_dir_suffix_tests() { + assert_eq!(&*strip_dir_suffix(Path::new("/foo//")), Path::new("/foo")); + assert_eq!(&*strip_dir_suffix(Path::new("/foo/")), Path::new("/foo")); + assert_eq!(&*strip_dir_suffix(Path::new("foo/")), Path::new("foo")); + assert_eq!(&*strip_dir_suffix(Path::new("foo")), Path::new("foo")); + assert_eq!(&*strip_dir_suffix(Path::new("/")), Path::new("/")); + assert_eq!(&*strip_dir_suffix(Path::new("//")), Path::new("/")); + assert_eq!(&*strip_dir_suffix(Path::new("/.")), Path::new("/.")); + assert_eq!(&*strip_dir_suffix(Path::new("//.")), Path::new("/.")); + assert_eq!(&*strip_dir_suffix(Path::new(".")), Path::new(".")); + assert_eq!(&*strip_dir_suffix(Path::new("foo/.")), Path::new("foo/.")); +} + +#[test] +fn test_path_requires_dir() { + assert!(!path_requires_dir(Path::new("."))); + assert!(path_requires_dir(Path::new("/"))); + assert!(path_requires_dir(Path::new("//"))); + assert!(path_requires_dir(Path::new("/./."))); + assert!(path_requires_dir(Path::new("foo/"))); + assert!(path_requires_dir(Path::new("foo//"))); + assert!(path_requires_dir(Path::new("foo//."))); + assert!(path_requires_dir(Path::new("foo/./."))); + assert!(path_requires_dir(Path::new("foo/./"))); + assert!(path_requires_dir(Path::new("foo/.//"))); +} + +#[test] +fn test_path_has_trailing_dot() { + assert!(!path_has_trailing_dot(Path::new("foo"))); + assert!(!path_has_trailing_dot(Path::new("foo."))); + + assert!(!path_has_trailing_dot(Path::new("/./foo"))); + assert!(!path_has_trailing_dot(Path::new(".."))); + assert!(!path_has_trailing_dot(Path::new("/.."))); + + assert!(!path_has_trailing_dot(Path::new("/"))); + assert!(!path_has_trailing_dot(Path::new("//"))); + assert!(!path_has_trailing_dot(Path::new("foo//"))); + assert!(!path_has_trailing_dot(Path::new("foo/"))); + + assert!(path_has_trailing_dot(Path::new("."))); + + assert!(path_has_trailing_dot(Path::new("/./."))); + assert!(path_has_trailing_dot(Path::new("foo//."))); + assert!(path_has_trailing_dot(Path::new("foo/./."))); + assert!(path_has_trailing_dot(Path::new("foo/./"))); + assert!(path_has_trailing_dot(Path::new("foo/.//"))); +} + +#[test] +fn test_path_has_trailing_slash() { + assert!(path_has_trailing_slash(Path::new("/"))); + assert!(path_has_trailing_slash(Path::new("//"))); + assert!(path_has_trailing_slash(Path::new("foo//"))); + assert!(path_has_trailing_slash(Path::new("foo/"))); + assert!(path_has_trailing_slash(Path::new("foo/./"))); + assert!(path_has_trailing_slash(Path::new("foo/.//"))); + + assert!(!path_has_trailing_slash(Path::new("foo"))); + assert!(!path_has_trailing_slash(Path::new("foo."))); + assert!(!path_has_trailing_slash(Path::new("/./foo"))); + assert!(!path_has_trailing_slash(Path::new(".."))); + assert!(!path_has_trailing_slash(Path::new("/.."))); + assert!(!path_has_trailing_slash(Path::new("."))); + assert!(!path_has_trailing_slash(Path::new("/./."))); + assert!(!path_has_trailing_slash(Path::new("foo//."))); + assert!(!path_has_trailing_slash(Path::new("foo/./."))); +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/errors.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/errors.rs new file mode 100644 index 000000000000..203fa114966b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/errors.rs @@ -0,0 +1,21 @@ +use std::io; + +#[cold] +pub(crate) fn no_such_file_or_directory() -> io::Error { + rustix::io::Errno::NOENT.into() +} + +#[cold] +pub(crate) fn is_directory() -> io::Error { + rustix::io::Errno::ISDIR.into() +} + +#[cold] +pub(crate) fn is_not_directory() -> io::Error { + rustix::io::Errno::NOTDIR.into() +} + +#[cold] +pub(crate) fn too_many_symlinks() -> io::Error { + rustix::io::Errno::LOOP.into() +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/file_type_ext.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/file_type_ext.rs new file mode 100644 index 000000000000..72504cc1b404 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/file_type_ext.rs @@ -0,0 +1,114 @@ +use crate::filesystem::primitives::FileType; +use rustix::fs::RawMode; +use std::{fs, io}; + +/// A type that implements `FileTypeExt` for this platform. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub(crate) enum ImplFileTypeExt { + Symlink, + BlockDevice, + CharDevice, + Fifo, + Socket, +} + +impl ImplFileTypeExt { + /// Constructs a new instance of `FileType` from the given + /// [`std::fs::File`] and [`std::fs::FileType`]. + #[inline] + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn from(_file: &fs::File, metadata: &fs::Metadata) -> io::Result { + // On `rustix`-style platforms, the `Metadata` has everything we need. + Ok(Self::from_just_metadata(metadata)) + } + + /// Constructs a new instance of `FileType` from the given + /// [`std::fs::Metadata`]. + #[inline] + pub(crate) fn from_just_metadata(metadata: &fs::Metadata) -> FileType { + let std = metadata.file_type(); + Self::from_std(std) + } + + /// Constructs a new instance of `Self` from the given + /// [`std::fs::FileType`]. + #[inline] + pub(crate) fn from_std(std: fs::FileType) -> FileType { + use rustix::fs::FileTypeExt; + if std.is_file() { + FileType::file() + } else if std.is_dir() { + FileType::dir() + } else if std.is_symlink() { + FileType::ext(Self::Symlink) + } else if std.is_block_device() { + FileType::ext(Self::BlockDevice) + } else if std.is_char_device() { + FileType::ext(Self::CharDevice) + } else { + #[cfg(not(target_os = "wasi"))] + if std.is_fifo() { + return FileType::ext(Self::Fifo); + } + #[cfg(not(target_os = "wasi"))] + if std.is_socket() { + return FileType::ext(Self::Socket); + } + FileType::unknown() + } + } + + /// Constructs a new instance of `FileType` from the given + /// [`RawMode`]. + #[inline] + pub(crate) const fn from_raw_mode(mode: RawMode) -> FileType { + match rustix::fs::FileType::from_raw_mode(mode) { + rustix::fs::FileType::RegularFile => FileType::file(), + rustix::fs::FileType::Directory => FileType::dir(), + rustix::fs::FileType::Symlink => FileType::ext(Self::symlink()), + #[cfg(not(target_os = "wasi"))] + rustix::fs::FileType::Fifo => FileType::ext(Self::fifo()), + rustix::fs::FileType::CharacterDevice => FileType::ext(Self::char_device()), + rustix::fs::FileType::BlockDevice => FileType::ext(Self::block_device()), + #[cfg(not(target_os = "wasi"))] + rustix::fs::FileType::Socket => FileType::ext(Self::socket()), + _ => FileType::unknown(), + } + } + + /// Creates a `FileType` for which `is_symlink()` returns `true`. + #[inline] + pub(crate) const fn symlink() -> Self { + Self::Symlink + } + + /// Creates a `FileType` for which `is_block_device()` returns `true`. + #[inline] + pub(crate) const fn block_device() -> Self { + Self::BlockDevice + } + + /// Creates a `FileType` for which `is_char_device()` returns `true`. + #[inline] + pub(crate) const fn char_device() -> Self { + Self::CharDevice + } + + /// Creates a `FileType` for which `is_fifo()` returns `true`. + #[inline] + pub(crate) const fn fifo() -> Self { + Self::Fifo + } + + /// Creates a `FileType` for which `is_socket()` returns `true`. + #[inline] + pub(crate) const fn socket() -> Self { + Self::Socket + } + + /// Tests whether this file type represents a symbolic link. + #[inline] + pub(crate) fn is_symlink(&self) -> bool { + *self == Self::Symlink + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/hard_link_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/hard_link_unchecked.rs new file mode 100644 index 000000000000..7b280797b987 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/hard_link_unchecked.rs @@ -0,0 +1,24 @@ +use rustix::fs::{AtFlags, linkat}; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `hard_link`, but which does not perform +/// sandboxing. +/// +/// Even though POSIX `linkat` has the ability to follow symlinks in +/// `old_path`, using `AT_SYMLINK_FOLLOW`, Rust's `hard_link` doesn't need +/// that, so we don't expose it here. +pub(crate) fn hard_link_unchecked( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + Ok(linkat( + old_start, + old_path, + new_start, + new_path, + AtFlags::empty(), + )?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/is_same_file.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/is_same_file.rs new file mode 100644 index 000000000000..dc74f1d99c90 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/is_same_file.rs @@ -0,0 +1,33 @@ +use crate::filesystem::primitives::{Metadata, MetadataExt}; +use std::{fs, io}; + +/// Determine if `a` and `b` refer to the same inode on the same device. +pub(crate) fn is_same_file(a: &fs::File, b: &fs::File) -> io::Result { + let a_metadata = Metadata::from_file(a)?; + let b_metadata = Metadata::from_file(b)?; + is_same_file_metadata(&a_metadata, &b_metadata) +} + +/// Determine if `a` and `b` are metadata for the same inode on the same +/// device. +pub(crate) fn is_same_file_metadata(a: &Metadata, b: &Metadata) -> io::Result { + Ok(a.dev() == b.dev() && a.ino() == b.ino()) +} + +/// Determine if `a` and `b` definitely refer to different inodes. +/// +/// This is similar to `is_same_file`, but is conservative, and doesn't depend +/// on nightly-only features. +#[allow(dead_code)] +pub(crate) fn is_different_file(a: &fs::File, b: &fs::File) -> io::Result { + is_same_file(a, b).map(|same| !same) +} + +/// Determine if `a` and `b` are metadata for definitely different inodes. +/// +/// This is similar to `is_same_file_metadata`, but is conservative, and +/// doesn't depend on nightly-only features. +#[allow(dead_code)] +pub(crate) fn is_different_file_metadata(a: &Metadata, b: &Metadata) -> io::Result { + is_same_file_metadata(a, b).map(|same| !same) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/metadata_ext.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/metadata_ext.rs new file mode 100644 index 000000000000..8e4400ebf9eb --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/metadata_ext.rs @@ -0,0 +1,181 @@ +#![allow(clippy::useless_conversion)] + +use crate::filesystem::primitives::{ImplFileTypeExt, Metadata}; +use rustix::fs::{RawMode, Stat}; +#[cfg(target_os = "linux")] +use rustix::fs::{Statx, StatxFlags, makedev}; +use std::time::{Duration, SystemTime}; +use std::{fs, io}; + +#[derive(Debug, Clone)] +pub(crate) struct ImplMetadataExt { + dev: u64, + ino: u64, + nlink: u64, +} + +impl ImplMetadataExt { + /// Constructs a new instance of `Self` from the given [`std::fs::File`] + /// and [`std::fs::Metadata`]. + #[inline] + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn from(_file: &fs::File, std: &fs::Metadata) -> io::Result { + // On `rustix`-style platforms, the `Metadata` has everything we need. + Ok(Self::from_just_metadata(std)) + } + + /// Constructs a new instance of `Self` from the given + /// [`std::fs::Metadata`]. + #[inline] + pub(crate) fn from_just_metadata(std: &fs::Metadata) -> Self { + use rustix::fs::MetadataExt; + Self { + dev: std.dev(), + ino: std.ino(), + nlink: std.nlink(), + } + } + + /// Constructs a new instance of `Metadata` from the given `Stat`. + #[inline] + #[allow(unused_comparisons)] // NB: rust-lang/rust#115823 requires this here instead of on `st_dev` processing below + pub(crate) fn from_rustix(stat: Stat) -> Metadata { + Metadata { + file_type: ImplFileTypeExt::from_raw_mode(stat.st_mode as RawMode), + len: u64::try_from(stat.st_size).unwrap(), + + #[cfg(not(target_os = "wasi"))] + modified: system_time_from_rustix( + stat.st_mtime.try_into().unwrap(), + stat.st_mtime_nsec as _, + ), + #[cfg(not(target_os = "wasi"))] + accessed: system_time_from_rustix( + stat.st_atime.try_into().unwrap(), + stat.st_atime_nsec as _, + ), + + #[cfg(target_os = "wasi")] + modified: system_time_from_rustix(stat.st_mtim.tv_sec, stat.st_mtim.tv_nsec as _), + #[cfg(target_os = "wasi")] + accessed: system_time_from_rustix(stat.st_atim.tv_sec, stat.st_atim.tv_nsec as _), + + #[cfg(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + target_os = "ios", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + ))] + created: system_time_from_rustix( + stat.st_birthtime.try_into().unwrap(), + stat.st_birthtime_nsec as _, + ), + + // `stat.st_ctime` is the latest status change; we want the creation. + #[cfg(not(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "macos", + target_os = "ios", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + target_os = "netbsd" + )))] + created: None, + + ext: Self { + // The type of `st_dev` is `dev_t` which is signed on some + // platforms and unsigned on other platforms. A `u64` is enough + // to work for all unsigned platforms, and for signed platforms + // perform a sign extension to `i64` and then view that as an + // unsigned 64-bit number instead. + // + // Note that the `unused_comparisons` is ignored here for + // platforms where it's unsigned since the first branch here + // will never be taken. + dev: if stat.st_dev < 0 { + i64::try_from(stat.st_dev).unwrap() as u64 + } else { + u64::try_from(stat.st_dev).unwrap() + }, + ino: stat.st_ino.into(), + nlink: u64::from(stat.st_nlink), + }, + } + } + + /// Constructs a new instance of `Metadata` from the given `Statx`. + #[cfg(target_os = "linux")] + #[inline] + pub(crate) fn from_rustix_statx(statx: Statx) -> Metadata { + Metadata { + file_type: ImplFileTypeExt::from_raw_mode(RawMode::from(statx.stx_mode)), + len: u64::try_from(statx.stx_size).unwrap(), + modified: if statx.stx_mask & StatxFlags::MTIME.bits() != 0 { + system_time_from_rustix(statx.stx_mtime.tv_sec, statx.stx_mtime.tv_nsec as _) + } else { + None + }, + accessed: if statx.stx_mask & StatxFlags::ATIME.bits() != 0 { + system_time_from_rustix(statx.stx_atime.tv_sec, statx.stx_atime.tv_nsec as _) + } else { + None + }, + created: if statx.stx_mask & StatxFlags::BTIME.bits() != 0 { + system_time_from_rustix(statx.stx_btime.tv_sec, statx.stx_btime.tv_nsec as _) + } else { + None + }, + + ext: Self { + dev: makedev(statx.stx_dev_major, statx.stx_dev_minor), + ino: statx.stx_ino.into(), + nlink: u64::from(statx.stx_nlink), + }, + } + } +} + +#[allow(clippy::similar_names)] +fn system_time_from_rustix(sec: i64, nsec: u64) -> Option { + if sec >= 0 { + SystemTime::UNIX_EPOCH.checked_add(Duration::new(u64::try_from(sec).unwrap(), nsec as _)) + } else { + SystemTime::UNIX_EPOCH + .checked_sub(Duration::new(sec.unsigned_abs(), 0)) + .map(|t| t.checked_add(Duration::new(0, nsec as u32))) + .flatten() + } +} + +impl crate::filesystem::primitives::MetadataExt for ImplMetadataExt { + #[inline] + fn dev(&self) -> u64 { + self.dev + } + + #[inline] + fn ino(&self) -> u64 { + self.ino + } + + #[inline] + fn nlink(&self) -> u64 { + self.nlink + } +} + +/// It should be possible to represent times before the Epoch. +/// https://github.com/bytecodealliance/cap-std/issues/328 +#[test] +fn negative_time() { + let system_time = system_time_from_rustix(-1, 1).unwrap(); + let d = SystemTime::UNIX_EPOCH.duration_since(system_time).unwrap(); + assert_eq!(d.as_secs(), 0); + assert_eq!(d.subsec_nanos(), 999999999); +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/mod.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/mod.rs new file mode 100644 index 000000000000..df34362c9417 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/mod.rs @@ -0,0 +1,87 @@ +mod create_dir_unchecked; +mod dir_entry_inner; +#[cfg(not(target_os = "wasi"))] +mod dir_options_ext; +mod dir_utils; +mod file_type_ext; +mod hard_link_unchecked; +mod is_same_file; +mod metadata_ext; +mod oflags; +mod open_options_ext; +mod open_unchecked; +mod read_dir_inner; +mod read_link_unchecked; +mod remove_dir_unchecked; +mod remove_file_unchecked; +mod rename_unchecked; +#[cfg(not(any(target_os = "android", target_os = "linux")))] +mod set_times_impl; +mod stat_unchecked; +mod symlink_unchecked; +mod times; + +pub(crate) mod errors; + +// On Linux, use optimized implementations based on +// `openat2` and `O_PATH` when available. +// +// On FreeBSD, use optimized implementations based on +// `O_RESOLVE_BENEATH`/`AT_RESOLVE_BENEATH` and `O_PATH` when available. +#[cfg(target_os = "freebsd")] +pub(crate) use crate::filesystem::primitives::rustix::freebsd::fs::*; +#[cfg(any(target_os = "android", target_os = "linux"))] +pub(crate) use crate::filesystem::primitives::rustix::linux::fs::*; +#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "freebsd")))] +#[rustfmt::skip] +pub(crate) use crate::filesystem::primitives::{ + manually::open as open_impl, + manually::stat as stat_impl, + via_parent::set_times_nofollow as set_times_nofollow_impl, +}; +#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "freebsd")))] +pub(crate) use set_times_impl::set_times_impl; +#[cfg(target_os = "freebsd")] +pub(crate) use set_times_impl::set_times_impl as set_times_manually; +#[rustfmt::skip] +pub(crate) use crate::filesystem::primitives::{ + via_parent::hard_link as hard_link_impl, + via_parent::create_dir as create_dir_impl, + via_parent::read_link as read_link_impl, + via_parent::rename as rename_impl, + via_parent::symlink as symlink_impl, +}; +#[cfg(not(target_os = "freebsd"))] +#[rustfmt::skip] +pub(crate) use crate::filesystem::primitives::{ + via_parent::remove_dir as remove_dir_impl, + via_parent::remove_file as remove_file_impl, +}; + +pub(crate) use create_dir_unchecked::create_dir_unchecked; +pub(crate) use dir_entry_inner::DirEntryInner; +#[cfg(not(target_os = "wasi"))] +pub(crate) use dir_options_ext::DirOptionsExt; +pub(crate) use dir_utils::*; +pub(crate) use file_type_ext::ImplFileTypeExt; +pub(crate) use hard_link_unchecked::hard_link_unchecked; +#[allow(unused_imports)] +pub(crate) use is_same_file::{is_different_file, is_different_file_metadata, is_same_file}; +pub(crate) use metadata_ext::ImplMetadataExt; +pub(crate) use open_options_ext::ImplOpenOptionsExt; +pub(crate) use open_unchecked::open_unchecked; +pub(crate) use read_dir_inner::ReadDirInner; +pub(crate) use read_link_unchecked::read_link_unchecked; +pub(crate) use remove_dir_unchecked::remove_dir_unchecked; +pub(crate) use remove_file_unchecked::remove_file_unchecked; +pub(crate) use rename_unchecked::rename_unchecked; +pub(crate) use stat_unchecked::stat_unchecked; +pub(crate) use symlink_unchecked::symlink_unchecked; +#[allow(unused_imports)] +pub(crate) use times::{set_times_follow_unchecked, set_times_nofollow_unchecked, to_timespec}; + +// On Linux, there is a limit of 40 symlink expansions. +// Source: +pub(crate) const MAX_SYMLINK_EXPANSIONS: u8 = 40; + +pub(super) use oflags::*; diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/oflags.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/oflags.rs new file mode 100644 index 000000000000..9953d015b99b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/oflags.rs @@ -0,0 +1,101 @@ +use crate::filesystem::primitives::{FollowSymlinks, OpenOptions, target_o_path}; +use rustix::fs::OFlags; +use std::io; + +pub(in super::super) fn compute_oflags(options: &OpenOptions) -> io::Result { + let mut oflags = OFlags::CLOEXEC; + oflags |= get_access_mode(options)?; + oflags |= get_creation_mode(options)?; + if options.follow == FollowSymlinks::No { + oflags |= OFlags::NOFOLLOW; + } + if options.sync { + oflags |= OFlags::SYNC; + } + if options.dsync { + #[cfg(not(target_os = "freebsd"))] + { + oflags |= OFlags::DSYNC; + } + + // Where needed, approximate `DSYNC` with `SYNC`. + #[cfg(target_os = "freebsd")] + { + oflags |= OFlags::SYNC; + } + } + #[cfg(not(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + target_os = "freebsd", + target_os = "fuchsia" + )))] + if options.rsync { + oflags |= OFlags::RSYNC; + } + if options.nonblock { + oflags |= OFlags::NONBLOCK; + } + if options.dir_required { + oflags |= OFlags::DIRECTORY; + + // If the target has `O_PATH`, we don't need to read the directory + // entries, and we're not requesting write access (which need to + // fail on a directory), use it. + if !options.readdir_required && !options.write && !options.append { + oflags |= target_o_path(); + } + } + // Use `RWMODE` here instead of `ACCMODE` so that we preserve the `O_PATH` + // flag. + #[cfg(not(target_os = "wasi"))] + { + oflags |= OFlags::from_bits(options.ext.custom_flags as _).expect("unrecognized OFlags") + & !OFlags::RWMODE; + } + Ok(oflags) +} + +// `OpenOptions` translation code derived from Rust's +// library/std/src/sys/unix/fs.rs at revision +// 108e90ca78f052c0c1c49c42a22c85620be19712. + +pub(crate) fn get_access_mode(options: &OpenOptions) -> io::Result { + match (options.read, options.write, options.append) { + (true, false, false) => Ok(OFlags::RDONLY), + (false, true, false) => Ok(OFlags::WRONLY), + (true, true, false) => Ok(OFlags::RDWR), + (false, _, true) => Ok(OFlags::WRONLY | OFlags::APPEND), + (true, _, true) => Ok(OFlags::RDWR | OFlags::APPEND), + (false, false, false) => Err(rustix::io::Errno::INVAL.into()), + } +} + +pub(crate) fn get_creation_mode(options: &OpenOptions) -> io::Result { + match (options.write, options.append) { + (true, false) => {} + (false, false) => { + if options.truncate || options.create || options.create_new { + return Err(rustix::io::Errno::INVAL.into()); + } + } + (_, true) => { + if options.truncate && !options.create_new { + return Err(rustix::io::Errno::INVAL.into()); + } + } + } + + Ok( + match (options.create, options.truncate, options.create_new) { + (false, false, false) => OFlags::empty(), + (true, false, false) => OFlags::CREATE, + (false, true, false) => OFlags::TRUNC, + (true, true, false) => OFlags::CREATE | OFlags::TRUNC, + (_, _, true) => OFlags::CREATE | OFlags::EXCL, + }, + ) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/open_options_ext.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/open_options_ext.rs new file mode 100644 index 000000000000..dc457f11e44c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/open_options_ext.rs @@ -0,0 +1,20 @@ +#[derive(Debug, Clone)] +pub(crate) struct ImplOpenOptionsExt { + pub(crate) mode: u32, + pub(crate) custom_flags: i32, +} + +impl ImplOpenOptionsExt { + pub(crate) const fn new() -> Self { + Self { + mode: 0o666, + custom_flags: 0, + } + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) fn custom_flags(&mut self, flags: i32) -> &mut Self { + self.custom_flags = flags; + self + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/open_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/open_unchecked.rs new file mode 100644 index 000000000000..5e429f8c3d38 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/open_unchecked.rs @@ -0,0 +1,57 @@ +use super::compute_oflags; +use crate::filesystem::primitives::{OpenOptions, OpenUncheckedError, stat_unchecked}; +use rustix::fs::{Mode, openat}; +use rustix::io; +use std::fs; +use std::path::Path; + +/// *Unsandboxed* function similar to `open`, but which does not perform +/// sandboxing. +pub(crate) fn open_unchecked( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> Result { + let oflags = compute_oflags(options).map_err(OpenUncheckedError::Other)?; + + #[allow(clippy::useless_conversion)] + #[cfg(not(target_os = "wasi"))] + let mode = Mode::from_bits_truncate(options.ext.mode as _); + #[cfg(target_os = "wasi")] + let mode = Mode::empty(); + + let err = match openat(start, path, oflags, mode) { + Ok(file) => { + return Ok(fs::File::from(file)); + } + Err(err) => err, + }; + match err { + // `ELOOP` is the POSIX standard and most widely used error code to + // indicate that a symlink was found when `O_NOFOLLOW` was set. + #[cfg(not(any(target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd")))] + io::Errno::LOOP => Err(OpenUncheckedError::Symlink(err.into(), ())), + + // FreeBSD and similar (but not Darwin) use `EMLINK`. + #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] + io::Errno::MLINK => Err(OpenUncheckedError::Symlink(err.into(), ())), + + // NetBSD uses `EFTYPE`. + #[cfg(target_os = "netbsd")] + io::Errno::FTYPE => Err(OpenUncheckedError::Symlink(err.into(), ())), + + io::Errno::NOENT => Err(OpenUncheckedError::NotFound(err.into())), + io::Errno::NOTDIR => { + if options.dir_required + && stat_unchecked(start, path, options.follow) + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false) + { + Err(OpenUncheckedError::Symlink(err.into(), ())) + } else { + Err(OpenUncheckedError::NotFound(err.into())) + } + } + _ => Err(OpenUncheckedError::Other(err.into())), + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/read_dir_inner.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/read_dir_inner.rs new file mode 100644 index 000000000000..558189b2c990 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/read_dir_inner.rs @@ -0,0 +1,87 @@ +use crate::filesystem::primitives::{ + DirEntryInner, FollowSymlinks, Metadata, open_dir_for_reading_unchecked, stat_unchecked, +}; +use rustix::fd::{AsFd, OwnedFd}; +use rustix::fs::Dir; +use std::ffi::OsStr; +use std::mem::ManuallyDrop; +use std::os::fd::{AsRawFd, FromRawFd, RawFd}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::OsStrExt; +use std::path::Component; +use std::sync::{Arc, Mutex}; +use std::{fmt, fs, io}; + +pub(crate) struct ReadDirInner { + raw_fd: RawFd, + + // `Dir` doesn't implement `AsFd`, because libc `fdopendir` has UB if the + // file descriptor is used in almost any way, so we hold a separate + // `OwnedFd` that we can do `as_fd()` on. + rustix: Arc>, +} + +impl ReadDirInner { + pub(crate) fn read_base_dir(start: &fs::File) -> io::Result { + // Open ".", to obtain a new independent file descriptor. Don't use + // `dup` since in that case the resulting file descriptor would share + // a current position with the original, and `read_dir` calls after + // the first `read_dir` call wouldn't start from the beginning. + let fd = + open_dir_for_reading_unchecked(start, Component::CurDir.as_ref(), FollowSymlinks::No)?; + let dir = Dir::read_from(fd.as_fd())?; + Ok(Self { + raw_fd: fd.as_fd().as_raw_fd(), + rustix: Arc::new(Mutex::new((dir, fd.into()))), + }) + } + + pub(super) fn metadata(&self, file_name: &OsStr) -> io::Result { + stat_unchecked(&self.as_file_view(), file_name.as_ref(), FollowSymlinks::No) + } + + #[allow(unsafe_code)] + fn as_file_view(&self) -> ManuallyDrop { + // Safety: `self.rustix` owns the file descriptor. We just hold a + // copy outside so that we can read it without taking a lock. + ManuallyDrop::new(unsafe { fs::File::from_raw_fd(self.raw_fd) }) + } +} + +impl Iterator for ReadDirInner { + type Item = io::Result; + + fn next(&mut self) -> Option { + loop { + let entry = self.rustix.lock().unwrap().0.read()?; + let entry = match entry { + Ok(entry) => entry, + Err(e) => return Some(Err(e.into())), + }; + let file_name = entry.file_name().to_bytes(); + if file_name != Component::CurDir.as_os_str().as_bytes() + && file_name != Component::ParentDir.as_os_str().as_bytes() + { + let clone = Arc::clone(&self.rustix); + return Some(Ok(DirEntryInner { + rustix: entry, + read_dir: Self { + raw_fd: self.raw_fd, + rustix: clone, + }, + })); + } + } + } +} + +impl fmt::Debug for ReadDirInner { + // Like libstd's version, but doesn't print the path. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut b = f.debug_struct("ReadDir"); + b.field("raw_fd", &self.raw_fd); + b.finish() + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/read_link_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/read_link_unchecked.rs new file mode 100644 index 000000000000..08ef42dac1e2 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/read_link_unchecked.rs @@ -0,0 +1,19 @@ +use rustix::fs::readlinkat; +use std::ffi::OsString; +#[cfg(unix)] +use std::os::unix::ffi::OsStringExt; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::OsStringExt; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `read_link`, but which does not perform +/// sandboxing. +pub(crate) fn read_link_unchecked( + start: &fs::File, + path: &Path, + reuse: PathBuf, +) -> io::Result { + Ok(readlinkat(start, path, reuse.into_os_string().into_vec()) + .map(|path| OsString::from_vec(path.into_bytes()).into())?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/remove_dir_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/remove_dir_unchecked.rs new file mode 100644 index 000000000000..6106566847d5 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/remove_dir_unchecked.rs @@ -0,0 +1,9 @@ +use rustix::fs::{AtFlags, unlinkat}; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `remove_dir`, but which does not perform +/// sandboxing. +pub(crate) fn remove_dir_unchecked(start: &fs::File, path: &Path) -> io::Result<()> { + Ok(unlinkat(start, path, AtFlags::REMOVEDIR)?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/remove_file_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/remove_file_unchecked.rs new file mode 100644 index 000000000000..3b1adaa1a044 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/remove_file_unchecked.rs @@ -0,0 +1,9 @@ +use rustix::fs::{AtFlags, unlinkat}; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `remove_file`, but which does not perform +/// sandboxing. +pub(crate) fn remove_file_unchecked(start: &fs::File, path: &Path) -> io::Result<()> { + Ok(unlinkat(start, path, AtFlags::empty())?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/rename_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/rename_unchecked.rs new file mode 100644 index 000000000000..a929ebb1ad5d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/rename_unchecked.rs @@ -0,0 +1,14 @@ +use rustix::fs::renameat; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `rename`, but which does not perform +/// sandboxing. +pub(crate) fn rename_unchecked( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + Ok(renameat(old_start, old_path, new_start, new_path)?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/set_times_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/set_times_impl.rs new file mode 100644 index 000000000000..4e7363c50d9c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/set_times_impl.rs @@ -0,0 +1,52 @@ +//! This module consists of helper types and functions for dealing +//! with setting the file times. + +use crate::filesystem::primitives::{OpenOptions, open}; +use rustix::io::Errno; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +pub(crate) fn set_times_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + let mut times = fs::FileTimes::new(); + if let Some(atime) = atime { + times = times.set_accessed(atime); + } + if let Some(mtime) = mtime { + times = times.set_modified(mtime); + } + // Try `futimens` with a normal handle. Normal handles need some kind of + // access, so first try write. + match open(start, path, OpenOptions::new().write(true)) { + Ok(file) => return fs::File::from(file).set_times(times), + Err(err) => match Errno::from_io_error(&err) { + Some(Errno::ACCESS) | Some(Errno::ISDIR) => (), + _ => return Err(err), + }, + } + + // Next try read. + match open(start, path, OpenOptions::new().read(true)) { + Ok(file) => return fs::File::from(file).set_times(times), + Err(err) => match Errno::from_io_error(&err) { + Some(Errno::ACCESS) => (), + _ => return Err(err), + }, + } + + // It's not possible to do anything else with generic POSIX. Plain + // `utimensat` has two options: + // - Follow symlinks, which would open up a race in which a concurrent + // modification of the symlink could point outside the sandbox and we + // wouldn't be able to detect it, or + // - Don't follow symlinks, which would modify the timestamp of the symlink + // instead of the file we're trying to get to. + // + // So neither does what we need. + Err(Errno::NOTSUP.into()) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/stat_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/stat_unchecked.rs new file mode 100644 index 000000000000..25bf59a62036 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/stat_unchecked.rs @@ -0,0 +1,79 @@ +use crate::filesystem::primitives::{FollowSymlinks, ImplMetadataExt, Metadata}; +use rustix::fs::{AtFlags, statat}; +use std::path::Path; +use std::{fs, io}; + +#[cfg(target_os = "linux")] +use rustix::fs::{StatxFlags, statx}; +#[cfg(target_os = "linux")] +use std::sync::atomic::{AtomicU8, Ordering}; + +/// *Unsandboxed* function similar to `stat`, but which does not perform +/// sandboxing. +pub(crate) fn stat_unchecked( + start: &fs::File, + path: &Path, + follow: FollowSymlinks, +) -> io::Result { + let atflags = match follow { + FollowSymlinks::Yes => AtFlags::empty(), + FollowSymlinks::No => AtFlags::SYMLINK_NOFOLLOW, + }; + + // `statx` is preferred on regular Linux because it can return creation + // times. Linux kernels prior to 4.11 don't have `statx` and return + // `ENOSYS`. Older versions of Docker/seccomp would return `EPERM` for + // `statx`; see . We store + // the availability in a global to avoid unnecessary syscalls. + // + // On Android, the [seccomp policy] prevents us from even + // detecting whether `statx` is supported, so don't even try. + // + // [seccomp policy]: https://android-developers.googleblog.com/2017/07/seccomp-filter-in-android-o.html + #[cfg(target_os = "linux")] + { + // 0: Unknown + // 1: Not available + // 2: Available + static STATX_STATE: AtomicU8 = AtomicU8::new(0); + let state = STATX_STATE.load(Ordering::Relaxed); + + if state != 1 { + let statx_result = statx( + start, + path, + atflags, + StatxFlags::BASIC_STATS | StatxFlags::BTIME, + ); + match statx_result { + Ok(statx) => { + if state == 0 { + STATX_STATE.store(2, Ordering::Relaxed); + } + return Ok(ImplMetadataExt::from_rustix_statx(statx)); + } + Err(rustix::io::Errno::NOSYS) => STATX_STATE.store(1, Ordering::Relaxed), + Err(rustix::io::Errno::PERM) if state == 0 => { + // This is an unlikely case, as `statx` doesn't normally + // return `PERM` errors. One way this can happen is when + // running on old versions of seccomp/Docker. If `statx` on + // the current working directory returns a similar error, + // then stop using `statx`. + if let Err(rustix::io::Errno::PERM) = statx( + rustix::fs::CWD, + "", + AtFlags::EMPTY_PATH, + StatxFlags::empty(), + ) { + STATX_STATE.store(1, Ordering::Relaxed); + } else { + return Err(rustix::io::Errno::PERM.into()); + } + } + Err(e) => return Err(e.into()), + } + } + } + + Ok(statat(start, path, atflags).map(ImplMetadataExt::from_rustix)?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/symlink_unchecked.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/symlink_unchecked.rs new file mode 100644 index 000000000000..bdd9141289a9 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/symlink_unchecked.rs @@ -0,0 +1,13 @@ +use rustix::fs::symlinkat; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `symlink`, but which does not perform +/// sandboxing. +pub(crate) fn symlink_unchecked( + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + Ok(symlinkat(old_path, new_start, new_path)?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/fs/times.rs b/crates/wasi/src/filesystem/primitives/rustix/fs/times.rs new file mode 100644 index 000000000000..b3253e94c420 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/fs/times.rs @@ -0,0 +1,57 @@ +use rustix::fs::{AtFlags, Timestamps, UTIME_NOW, UTIME_OMIT, utimensat}; +use rustix::time::Timespec; +use std::os::fd::BorrowedFd; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +#[allow(clippy::useless_conversion)] +pub(crate) fn to_timespec(ft: Option) -> io::Result { + Ok(match ft { + None => Timespec { + tv_sec: 0, + tv_nsec: UTIME_OMIT.into(), + }, + Some(ft) => { + let duration = ft.duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let nanoseconds = duration.subsec_nanos(); + assert_ne!(i64::from(nanoseconds), i64::from(UTIME_OMIT)); + assert_ne!(i64::from(nanoseconds), i64::from(UTIME_NOW)); + Timespec { + tv_sec: duration + .as_secs() + .try_into() + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?, + tv_nsec: nanoseconds.try_into().unwrap(), + } + } + }) +} + +#[allow(dead_code)] +pub(crate) fn set_times_nofollow_unchecked( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + let times = Timestamps { + last_access: to_timespec(atime)?, + last_modification: to_timespec(mtime)?, + }; + Ok(utimensat(start, path, ×, AtFlags::SYMLINK_NOFOLLOW)?) +} + +#[allow(dead_code)] +pub(crate) fn set_times_follow_unchecked( + start: BorrowedFd<'_>, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + let times = Timestamps { + last_access: to_timespec(atime)?, + last_modification: to_timespec(mtime)?, + }; + Ok(utimensat(start, path, ×, AtFlags::empty())?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/fs/file_metadata.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/file_metadata.rs new file mode 100644 index 000000000000..b02b95c3b765 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/file_metadata.rs @@ -0,0 +1,27 @@ +use crate::filesystem::primitives::{ImplMetadataExt, Metadata}; +use rustix::fs::{AtFlags, statat}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; +use std::{fs, io}; + +/// Like `file.metadata()`, but works with `O_PATH` descriptors on old (pre +/// 3.6) versions of Linux too. +pub(super) fn file_metadata(file: &fs::File) -> io::Result { + // Record whether we've seen an `EBADF` from an `fstat` on an `O_PATH` + // file descriptor, meaning we're on a Linux that doesn't support it. + static FSTAT_PATH_BADF: AtomicBool = AtomicBool::new(false); + + if !FSTAT_PATH_BADF.load(Relaxed) { + match Metadata::from_file(file) { + Ok(metadata) => return Ok(metadata), + Err(err) => match rustix::io::Errno::from_io_error(&err) { + // Before Linux 3.6, `fstat` with `O_PATH` returned `EBADF`. + Some(rustix::io::Errno::BADF) => FSTAT_PATH_BADF.store(true, Relaxed), + _ => return Err(err), + }, + } + } + + // If `fstat` with `O_PATH` isn't supported, use `statat` with `AT_EMPTY_PATH`. + Ok(statat(file, "", AtFlags::EMPTY_PATH).map(ImplMetadataExt::from_rustix)?) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/fs/mod.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/mod.rs new file mode 100644 index 000000000000..300e5a771472 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/mod.rs @@ -0,0 +1,21 @@ +#[cfg(target_os = "linux")] +mod file_metadata; +mod open_impl; +mod procfs; +mod set_times_impl; +#[cfg(target_os = "linux")] +mod stat_impl; + +#[cfg(target_os = "android")] +pub(crate) use crate::filesystem::primitives::manually::stat as stat_impl; +pub(crate) use crate::filesystem::primitives::via_parent::set_times_nofollow as set_times_nofollow_impl; +#[cfg(target_os = "linux")] +pub(crate) use open_impl::open_beneath; +pub(crate) use open_impl::open_impl; +pub(crate) use set_times_impl::set_times_impl; +#[cfg(target_os = "linux")] +pub(crate) use stat_impl::stat_impl; + +// In theory we could optimize `link` using `openat2` with `O_PATH` and +// `linkat` with `AT_EMPTY_PATH`, however that requires `CAP_DAC_READ_SEARCH`, +// so it isn't very widely applicable. diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/fs/open_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/open_impl.rs new file mode 100644 index 000000000000..0cd20cdde9e8 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/open_impl.rs @@ -0,0 +1,126 @@ +//! Linux 5.6 and later have a syscall `openat2`, with flags that allow it to +//! enforce the sandboxing property we want. See the [LWN article] for an +//! overview and the [`openat2` documentation] for details. +//! +//! [LWN article]: https://lwn.net/Articles/796868/ +//! [`openat2` documentation]: https://man7.org/linux/man-pages/man2/openat2.2.html +//! +//! On older Linux, fall back to `manually::open`. + +use crate::filesystem::primitives::{OpenOptions, manually}; +use std::path::Path; +use std::{fs, io}; +#[cfg(target_os = "linux")] +use { + super::super::super::fs::compute_oflags, + crate::filesystem::primitives::errors, + rustix::fs::{Mode, OFlags, RawMode, ResolveFlags, openat2}, + rustix::path::Arg, + std::sync::atomic::AtomicBool, + std::sync::atomic::Ordering::Relaxed, +}; + +/// Call the `openat2` system call, or use a fallback if that's unavailable. +pub(crate) fn open_impl( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> io::Result { + // On regular Linux, attempt to use `openat2` to accelerate sandboxed + // lookups. On Android, the [seccomp policy] prevents us from even + // detecting whether `openat2` is supported, so don't even try. + // + // [seccomp policy]: https://android-developers.googleblog.com/2017/07/seccomp-filter-in-android-o.html + #[cfg(target_os = "linux")] + { + let result = open_beneath(start, path, options); + + // If we got anything other than a `ENOSYS` error, that's our result. + match result { + Err(err) if err.raw_os_error() == Some(rustix::io::Errno::NOSYS.raw_os_error()) => {} + Err(err) => return Err(err), + Ok(fd) => return Ok(fd), + } + } + + manually::open(start, path, options) +} + +/// Call the `openat2` system call with `RESOLVE_BENEATH`. If the syscall is +/// unavailable, mark it so for future calls. If `openat2` is unavailable +/// either permanently or temporarily, return `ENOSYS`. +#[cfg(target_os = "linux")] +pub(crate) fn open_beneath( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> io::Result { + static INVALID: AtomicBool = AtomicBool::new(false); + if INVALID.load(Relaxed) { + // `openat2` is permanently unavailable. + return Err(rustix::io::Errno::NOSYS.into()); + } + + let oflags = compute_oflags(options)?; + + // Do two `contains` checks because `TMPFILE` may be represented with + // multiple flags and we need to ensure they're all set. + let mode = if oflags.contains(OFlags::CREATE) || oflags.contains(OFlags::TMPFILE) { + Mode::from_bits((options.ext.mode & 0o7777) as RawMode).unwrap() + } else { + Mode::empty() + }; + + // We know `openat2` needs a `&CStr` internally; to avoid allocating on + // each iteration of the loop below, allocate the `CString` now. + path.into_with_c_str(|path_c_str| { + // `openat2` fails with `EAGAIN` if a rename happens anywhere on the host + // while it's running, so use a loop to retry it a few times. But not too many + // times, because there's no limit on how often this can happen. The actual + // number here is currently an arbitrarily chosen guess. + for _ in 0..4 { + match openat2( + start, + path_c_str, + oflags, + mode, + ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS, + ) { + Ok(file) => { + let file = fs::File::from(file); + + return Ok(file); + } + Err(err) => match err { + // A rename or similar happened. Try again. + rustix::io::Errno::AGAIN => continue, + + // `EPERM` is used by some `seccomp` sandboxes to indicate + // that `openat2` is unimplemented: + // + // + // However, `EPERM` may also indicate a failed `O_NOATIME` + // or a file seal prevented the operation, and it's complex + // to detect those cases, so exit the loop and use the + // fallback. + rustix::io::Errno::PERM => break, + + // `ENOSYS` means `openat2` is permanently unavailable; + // mark it so and exit the loop. + rustix::io::Errno::NOSYS => { + INVALID.store(true, Relaxed); + break; + } + + _ => return Err(err), + }, + } + } + + Err(rustix::io::Errno::NOSYS) + }) + .map_err(|err| match err { + rustix::io::Errno::XDEV => errors::escape_attempt(), + err => err.into(), + }) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/fs/procfs.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/procfs.rs new file mode 100644 index 000000000000..3fba32fadf3e --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/procfs.rs @@ -0,0 +1,43 @@ +//! Utilities for working with `/proc`, where Linux's `procfs` is typically +//! mounted. `/proc` serves as an adjunct to Linux's main syscall surface area, +//! providing additional features with an awkward interface. +//! +//! This module does a considerable amount of work to determine whether `/proc` +//! is mounted, with actual `procfs`, and without any additional mount points +//! on top of the paths we open. + +use crate::filesystem::primitives::OpenOptionsExt; +use crate::filesystem::primitives::{OpenOptions, open, set_times_follow_unchecked}; +use rustix::fd::AsFd; +use rustix::fs::OFlags; +use rustix::path::DecInt; +use rustix_linux_procfs::proc_self_fd; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +pub(crate) fn set_times_through_proc_self_fd( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + let opath = open( + start, + path, + OpenOptions::new() + .read(true) + .custom_flags(OFlags::PATH.bits() as i32), + )?; + + // Don't pass `AT_SYMLINK_NOFOLLOW`, because we do actually want to follow + // the first symlink. We don't want to follow any subsequent symlinks, but + // omitting `O_NOFOLLOW` above ensures that the destination of the link + // isn't a symlink. + set_times_follow_unchecked( + proc_self_fd()?.as_fd(), + DecInt::from_fd(&opath).as_ref(), + atime, + mtime, + ) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/fs/set_times_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/set_times_impl.rs new file mode 100644 index 000000000000..9372ce23536a --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/set_times_impl.rs @@ -0,0 +1,44 @@ +//! This module consists of helper types and functions for dealing +//! with setting the file times specific to Linux. + +use super::procfs::set_times_through_proc_self_fd; +use crate::filesystem::primitives::{OpenOptions, open}; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +pub(crate) fn set_times_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + let mut times = fs::FileTimes::new(); + if let Some(atime) = atime { + times = times.set_accessed(atime); + } + if let Some(mtime) = mtime { + times = times.set_modified(mtime); + } + // Try `futimens` with a normal handle. Normal handles need some kind of + // access, so first try write. + match open(start, path, OpenOptions::new().write(true)) { + Ok(file) => return file.set_times(times), + Err(err) => match rustix::io::Errno::from_io_error(&err) { + Some(rustix::io::Errno::ACCESS) | Some(rustix::io::Errno::ISDIR) => (), + _ => return Err(err), + }, + } + + // Next try read. + match open(start, path, OpenOptions::new().read(true)) { + Ok(file) => return file.set_times(times), + Err(err) => match rustix::io::Errno::from_io_error(&err) { + Some(rustix::io::Errno::ACCESS) => (), + _ => return Err(err), + }, + } + + // If neither of those worked, turn to `/proc`. + set_times_through_proc_self_fd(start, path, atime, mtime) +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/fs/stat_impl.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/stat_impl.rs new file mode 100644 index 000000000000..01a6e09a5fce --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/fs/stat_impl.rs @@ -0,0 +1,58 @@ +//! Linux has an `O_PATH` flag which allows opening a file without necessary +//! having read or write access to it; we can use that with `openat2` and +//! `fstat` to perform a fast sandboxed `stat`. + +use super::file_metadata::file_metadata; +use crate::filesystem::primitives::{ + FollowSymlinks, Metadata, OpenOptions, manually, open_beneath, +}; +use rustix::fs::OFlags; +use std::path::Path; +use std::{fs, io}; + +/// Use `openat2` with `O_PATH` and `fstat`. If that's not available, fallback +/// to `manually::stat`. +pub(crate) fn stat_impl( + start: &fs::File, + path: &Path, + follow: FollowSymlinks, +) -> io::Result { + use crate::filesystem::primitives::{OpenOptionsExt, stat_unchecked}; + use std::path::Component; + + // Optimization: if path has exactly one component and it's not ".." or + // anything non-normal and we're not following symlinks we can go straight + // to `stat_unchecked`, which is faster than doing an open with a separate + // `fstat`. + if follow == FollowSymlinks::No { + let mut components = path.components(); + if let Some(Component::Normal(component)) = components.next() { + if components.next().is_none() { + return stat_unchecked(start, component.as_ref(), FollowSymlinks::No); + } + } + } + + // Open the path with `O_PATH`. Use `read(true)` even though we don't need + // `read` permissions, because Rust's libstd requires an access mode, and + // Linux ignores `O_RDONLY` with `O_PATH`. + let result = open_beneath( + start, + path, + OpenOptions::new() + .read(true) + .follow(follow) + .custom_flags(OFlags::PATH.bits() as i32), + ); + + // If that worked, call `fstat`. + match result { + Ok(file) => file_metadata(&file), + Err(err) => match rustix::io::Errno::from_io_error(&err) { + // `ENOSYS` from `open_beneath` means `openat2` is unavailable + // and we should use a fallback. + Some(rustix::io::Errno::NOSYS) => manually::stat(start, path, follow), + _ => Err(err), + }, + } +} diff --git a/crates/wasi/src/filesystem/primitives/rustix/linux/mod.rs b/crates/wasi/src/filesystem/primitives/rustix/linux/mod.rs new file mode 100644 index 000000000000..3ddb2c3266bb --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/linux/mod.rs @@ -0,0 +1,6 @@ +//! Following [`std`], we don't carry workarounds for Linux versions +//! older than 2.6.32. +//! +//! [`std`]: https://github.com/rust-lang/rust/pull/74163 + +pub(crate) mod fs; diff --git a/crates/wasi/src/filesystem/primitives/rustix/mod.rs b/crates/wasi/src/filesystem/primitives/rustix/mod.rs new file mode 100644 index 000000000000..94e6b2f297db --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/rustix/mod.rs @@ -0,0 +1,9 @@ +//! The `rustix` module contains code specific to the Posix-ish platforms +//! supported by the `rustix` crate. + +pub(crate) mod fs; + +#[cfg(target_os = "freebsd")] +mod freebsd; +#[cfg(any(target_os = "android", target_os = "linux"))] +mod linux; diff --git a/crates/wasi/src/filesystem/primitives/set_times.rs b/crates/wasi/src/filesystem/primitives/set_times.rs new file mode 100644 index 000000000000..f1aff11552d8 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/set_times.rs @@ -0,0 +1,33 @@ +//! This defines `set_times`, the primary entrypoint to sandboxed +//! filesystem times modification. +//! +//! TODO: `check_set_times` etc. + +use crate::filesystem::primitives::{set_times_impl, set_times_nofollow_impl}; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +/// Perform a `utimensat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. This function +/// follows symlinks. +#[inline] +pub fn set_times( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + set_times_impl(start, path, atime, mtime) +} + +/// Like `set_times`, but never follows symlinks. +#[inline] +pub fn set_times_nofollow( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + set_times_nofollow_impl(start, path, atime, mtime) +} diff --git a/crates/wasi/src/filesystem/primitives/stat.rs b/crates/wasi/src/filesystem/primitives/stat.rs new file mode 100644 index 000000000000..f5ef8037953c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/stat.rs @@ -0,0 +1,13 @@ +//! This defines `stat`, the primary entrypoint to sandboxed metadata querying. + +use crate::filesystem::primitives::{FollowSymlinks, Metadata, stat_impl}; +use std::path::Path; +use std::{fs, io}; + +/// Perform an `fstatat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. +#[inline] +pub fn stat(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io::Result { + // Call the underlying implementation. + stat_impl(start, path, follow) +} diff --git a/crates/wasi/src/filesystem/primitives/symlink.rs b/crates/wasi/src/filesystem/primitives/symlink.rs new file mode 100644 index 000000000000..e297dcb399f1 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/symlink.rs @@ -0,0 +1,64 @@ +//! This defines `symlink`, the primary entrypoint to sandboxed symlink +//! creation. + +use crate::filesystem::primitives::errors; +use std::path::Path; +use std::{fs, io}; + +/// Perform a `symlinkat`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. An error is +/// returned if the target path is absolute. +#[cfg(not(windows))] +#[inline] +pub fn symlink(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> { + // Don't allow creating symlinks to absolute paths. This isn't strictly + // necessary to preserve the sandbox, since `open` will refuse to follow + // absolute symlinks in any case. However, it is useful to enforce this + // restriction so that a WASI program can't trick some other non-WASI + // program into following an absolute path. + if old_path.has_root() { + return Err(errors::escape_attempt()); + } + + write_symlink_impl(old_path, new_start, new_path) +} + +#[cfg(not(windows))] +fn write_symlink_impl(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> { + use crate::filesystem::primitives::symlink_impl; + + // Call the underlying implementation. + symlink_impl(old_path, new_start, new_path) +} + +/// Perform a `symlink_file`-like operation, ensuring that the resolution of +/// the path never escapes the directory tree rooted at `start`. +#[cfg(windows)] +#[inline] +pub fn symlink_file(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> { + use crate::filesystem::primitives::symlink_file_impl; + + // As above, don't allow creating symlinks to absolute paths. + if old_path.has_root() { + return Err(errors::escape_attempt()); + } + + // Call the underlying implementation. + symlink_file_impl(old_path, new_start, new_path) +} + +/// Perform a `symlink_dir`-like operation, ensuring that the resolution of the +/// path never escapes the directory tree rooted at `start`. +#[cfg(windows)] +#[inline] +pub fn symlink_dir(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> { + use crate::filesystem::primitives::symlink_dir_impl; + + // As above, don't allow creating symlinks to absolute paths. + if old_path.has_root() { + return Err(errors::escape_attempt()); + } + + // Call the underlying implementation. + symlink_dir_impl(old_path, new_start, new_path) +} diff --git a/crates/wasi/src/filesystem/primitives/tests/cap_basics.rs b/crates/wasi/src/filesystem/primitives/tests/cap_basics.rs new file mode 100644 index 000000000000..3d9388f7f1ec --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/cap_basics.rs @@ -0,0 +1,244 @@ +use super::helpers as h; +use super::sys_common::io::tmpdir; +#[allow(unused_imports)] +use super::sys_common::symlink_supported; +use crate::filesystem::primitives as p; + +use std::path::Path; + +#[test] +fn cap_smoke_test() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + check!(h::write(&dir, "red.txt", b"hello world\n")); + check!(h::write(&dir, "dir/green.txt", b"goodmight moon\n")); + check!(h::write(&dir, "dir/inner/blue.txt", b"hey mars\n")); + + let inner = check!(p::open_dir(&dir, Path::new("dir/inner"))); + + check!(h::open(&dir, "red.txt")); + + #[cfg(not(windows))] + error!(h::open(&dir, "blue.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "blue.txt"), 2); + + #[cfg(not(windows))] + error!(h::open(&dir, "green.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "green.txt"), 2); + + check!(h::open(&dir, "./red.txt")); + + #[cfg(not(windows))] + error!(h::open(&dir, "./blue.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "./blue.txt"), 2); + + #[cfg(not(windows))] + error!(h::open(&dir, "./green.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "./green.txt"), 2); + + #[cfg(not(windows))] + error!(h::open(&dir, "dir/red.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "dir/red.txt"), 2); + + check!(h::open(&dir, "dir/green.txt")); + + #[cfg(not(windows))] + error!(h::open(&dir, "dir/blue.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "dir/blue.txt"), 2); + + #[cfg(not(windows))] + error!(h::open(&dir, "dir/inner/red.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "dir/inner/red.txt"), 2); + + #[cfg(not(windows))] + error!(h::open(&dir, "dir/inner/green.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&dir, "dir/inner/green.txt"), 2); + + check!(h::open(&dir, "dir/inner/blue.txt")); + + check!(h::open(&dir, "dir/../red.txt")); + check!(h::open(&dir, "dir/inner/../../red.txt")); + check!(h::open(&dir, "dir/inner/../inner/../../red.txt")); + + #[cfg(not(windows))] + error!(h::open(&inner, "red.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&inner, "red.txt"), 2); + + #[cfg(not(windows))] + error!(h::open(&inner, "green.txt"), "No such file"); + #[cfg(windows)] + error!(h::open(&inner, "green.txt"), 2); + + error_contains!( + h::open(&inner, "../inner/blue.txt"), + "a path led outside of the filesystem" + ); + error_contains!( + h::open(&inner, "../inner/red.txt"), + "a path led outside of the filesystem" + ); + + #[cfg(not(windows))] + error!(p::open_dir(&inner, Path::new("")), "No such file"); + #[cfg(windows)] + error!(p::open_dir(&inner, Path::new("")), 2); + + error_contains!( + p::open_dir(&inner, Path::new("/")), + "a path led outside of the filesystem" + ); + error_contains!( + p::open_dir(&inner, Path::new("/etc/services")), + "a path led outside of the filesystem" + ); + check!(p::open_dir(&inner, Path::new("."))); + check!(p::open_dir(&inner, Path::new("./"))); + check!(p::open_dir(&inner, Path::new("./."))); + error_contains!( + p::open_dir(&inner, Path::new("..")), + "a path led outside of the filesystem" + ); + error_contains!( + p::open_dir(&inner, Path::new("../")), + "a path led outside of the filesystem" + ); + error_contains!( + p::open_dir(&inner, Path::new("../.")), + "a path led outside of the filesystem" + ); + error_contains!( + p::open_dir(&inner, Path::new("./..")), + "a path led outside of the filesystem" + ); +} + +#[test] +fn symlinks() { + #[cfg(windows)] + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + check!(h::write(&dir, "red.txt", b"hello world\n")); + check!(h::write(&dir, "dir/green.txt", b"goodmight moon\n")); + check!(h::write(&dir, "dir/inner/blue.txt", b"hey mars\n")); + + let inner = check!(p::open_dir(&dir, Path::new("dir/inner"))); + + check!(h::symlink(&dir, "dir", "link")); + #[cfg(not(windows))] + check!(h::symlink(&dir, "does_not_exist", "badlink")); + + check!(h::open(&dir, "link/../red.txt")); + check!(h::open(&dir, "link/green.txt")); + check!(h::open(&dir, "link/inner/blue.txt")); + #[cfg(not(windows))] + { + error_contains!(h::open(&dir, "link/red.txt"), "No such file"); + error_contains!(h::open(&dir, "link/../green.txt"), "No such file"); + } + #[cfg(windows)] + { + error_contains!( + h::open(&dir, "link/red.txt"), + "The system cannot find the file specified." + ); + error_contains!( + h::open(&dir, "link/../green.txt"), + "The system cannot find the file specified." + ); + } + + check!(h::open(&dir, "./dir/.././/link/..///./red.txt")); + check!(h::open(&dir, "link/inner/../inner/../../red.txt")); + error_contains!( + h::open(&inner, "../inner/../inner/../../link/other.txt"), + "a path led outside of the filesystem" + ); + #[cfg(not(windows))] + { + error_contains!( + h::open(&dir, "./dir/.././/link/..///./not.txt"), + "No such file" + ); + error_contains!(h::open(&dir, "link/other.txt"), "No such file"); + error_contains!(h::open(&dir, "badlink/../red.txt"), "No such file"); + } + #[cfg(windows)] + { + error_contains!( + h::open(&dir, "./dir/.././/link/..///./not.txt"), + "The system cannot find the file specified." + ); + error_contains!( + h::open(&dir, "link/other.txt"), + "The system cannot find the file specified." + ); + } +} + +#[test] +#[cfg(not(windows))] +fn symlink_loop() { + let tmpdir = tmpdir(); + + let dir = h::dir_of(&tmpdir); + check!(h::symlink(&dir, "link", "link")); + // TODO: Check the error message + error_contains!(h::open(&dir, "link"), ""); +} + +#[test] +fn symlink_loop_from_rename() { + #[cfg(windows)] + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let dir = h::dir_of(&tmpdir); + check!(h::create(&dir, "file")); + check!(h::symlink(&dir, "file", "link")); + check!(h::open(&dir, "link")); + check!(p::rename( + &dir, + Path::new("file"), + &dir, + Path::new("renamed") + )); + error_contains!(h::open(&dir, "link"), ""); + check!(p::rename(&dir, Path::new("link"), &dir, Path::new("file"))); + error_contains!(h::open(&dir, "file"), ""); + check!(p::rename(&dir, Path::new("file"), &dir, Path::new("link"))); + error_contains!(h::open(&dir, "link"), ""); + check!(p::rename( + &dir, + Path::new("renamed"), + &dir, + Path::new("file") + )); + check!(h::open(&dir, "link")); +} + +#[cfg(target_os = "linux")] +#[test] +fn proc_self_fd() { + let dir = check!(std::fs::File::open("/proc/self/fd")); + // This should fail with "too many levels of symbolic links". + h::open(&dir, "0").unwrap_err(); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/file_type_ext.rs b/crates/wasi/src/filesystem/primitives/tests/file_type_ext.rs new file mode 100644 index 000000000000..d61114e75a6d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/file_type_ext.rs @@ -0,0 +1,39 @@ +// This file contains tests for `FileTypeExt`. + +use super::sys_common::io::tmpdir; +#[cfg(unix)] +use crate::filesystem::primitives::FileTypeExt; +use crate::filesystem::primitives::{Metadata, OpenOptions, open, open_ambient_dir}; +use std::path::Path; + +#[test] +fn test_file_type_ext() { + let tmpdir = tmpdir(); + let dir = check!(open_ambient_dir(tmpdir.path(),)); + let a = check!(open( + &dir, + Path::new("a"), + OpenOptions::new().write(true).create(true).truncate(true) + )); + + let tmpdir_metadata = check!(Metadata::from_file(&dir)); + let a_metadata = check!(Metadata::from_file(&a)); + + #[cfg(unix)] + { + assert!(!tmpdir_metadata.file_type().is_char_device()); + assert!(!a_metadata.file_type().is_char_device()); + + assert!(!tmpdir_metadata.file_type().is_block_device()); + assert!(!a_metadata.file_type().is_block_device()); + } + + assert!(tmpdir_metadata.file_type().is_dir()); + assert!(!a_metadata.file_type().is_dir()); + + assert!(!tmpdir_metadata.file_type().is_file()); + assert!(a_metadata.file_type().is_file()); + + assert!(!tmpdir_metadata.file_type().is_symlink()); + assert!(!a_metadata.file_type().is_symlink()); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/fs.rs b/crates/wasi/src/filesystem/primitives/tests/fs.rs new file mode 100644 index 000000000000..10f16ed9e9e6 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/fs.rs @@ -0,0 +1,1239 @@ +// This file is derived from Rust's library/std/src/fs/tests.rs at revision +// e4b1d5841494d6eb7f4944c91a057e16b0f0a9ea. + +use super::helpers as h; +use super::sys_common::io::tmpdir; +use super::sys_common::symlink_junction; +use crate::filesystem::primitives as p; +use rand::Rng; +use std::fs::File; +use std::io::prelude::*; +use std::io::{ErrorKind, SeekFrom}; +#[cfg(unix)] +use std::os::unix::fs::FileExt; +use std::path::{Path, PathBuf}; +use std::str; +use std::thread; + +// Several test fail on windows if the user does not have permission to +// create symlinks (the `SeCreateSymbolicLinkPrivilege`). Instead of +// disabling these test on Windows, use this function to test whether we +// have permission, and return otherwise. This way, we still don't run these +// tests most of the time, but at least we do if the user has the right +// permissions. +pub fn got_symlink_permission(tmpdir: &File) -> bool { + if cfg!(unix) { + return true; + } + let link = "some_hopefully_unique_link_name"; + + match h::symlink_file(tmpdir, r"nonexisting_target", link) { + // ERROR_PRIVILEGE_NOT_HELD = 1314 + Err(ref err) if err.raw_os_error() == Some(1314) => false, + Ok(_) | Err(_) => true, + } +} + +fn able_to_not_follow_symlinks_while_hard_linking() -> bool { + return true; +} + +#[test] +fn open_directory_with_truncate_is_error() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let mut options = p::OpenOptions::new(); + // The `maybe_dir` part of this test is gone along with the option itself. + options.truncate(true).read(true).write(true); + p::create_dir(&start, Path::new("test"), &p::DirOptions::new()).unwrap(); + assert!(p::open(&start, Path::new("test"), &options).is_err()); +} + +#[test] +fn dir_entry_methods() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + h::create_dir_all(&start, "a").unwrap(); + h::create(&start, "b").unwrap(); + + // `DirEntry::file_type` is gone; the metadata checks still cover this. + for file in h::read_dir(&start, ".").unwrap().map(|f| f.unwrap()) { + let fname = file.file_name(); + match fname.to_str() { + Some("a") => { + assert!(file.metadata().unwrap().is_dir()); + } + Some("b") => { + assert!(file.metadata().unwrap().file_type().is_file()); + } + f => panic!("unknown file name: {f:?}"), + } + } +} + +#[test] +fn open_flavors() { + use crate::filesystem::primitives::OpenOptions as OO; + fn c(t: &T) -> T { + t.clone() + } + + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let mut r = OO::new(); + r.read(true); + let mut w = OO::new(); + w.write(true); + let mut rw = OO::new(); + rw.read(true).write(true); + + #[cfg(windows)] + let invalid_options = 87; // ERROR_INVALID_PARAMETER + #[cfg(any(all(unix, not(target_os = "vxworks")), target_os = "wasi"))] + let invalid_options = "Invalid argument"; + #[cfg(target_os = "vxworks")] + let invalid_options = "invalid argument"; + + // Test various combinations of creation modes and access modes. + // + // Allowed: + // creation mode | read | write | read-write | + // | :-----------------------|:-----:|:-----:|:----------:| + // not set (open existing) | X | X | X | + // create | | X | X | + // truncate | | X | X | + // create and truncate | | X | X | + // create_new | | X | X | + // + // tested in reverse order, so 'create_new' creates the file, and 'open + // existing' opens it. + // + // The append and read-append rows are not covered: `OpenOptions::append` + // was dropped in this vendoring. + + // write-only + check!(p::open(&start, Path::new("a"), c(&w).create_new(true))); + check!(p::open( + &start, + Path::new("a"), + c(&w).create(true).truncate(true) + )); + check!(p::open(&start, Path::new("a"), c(&w).truncate(true))); + check!(p::open(&start, Path::new("a"), c(&w).create(true))); + check!(p::open(&start, Path::new("a"), &c(&w))); + + // read-only + error!( + p::open(&start, Path::new("b"), c(&r).create_new(true)), + invalid_options + ); + error!( + p::open(&start, Path::new("b"), c(&r).create(true).truncate(true)), + invalid_options + ); + error!( + p::open(&start, Path::new("b"), c(&r).truncate(true)), + invalid_options + ); + error!( + p::open(&start, Path::new("b"), c(&r).create(true)), + invalid_options + ); + check!(p::open(&start, Path::new("a"), &c(&r))); // try opening the file created with write_only + + // read-write + check!(p::open(&start, Path::new("c"), c(&rw).create_new(true))); + check!(p::open( + &start, + Path::new("c"), + c(&rw).create(true).truncate(true) + )); + check!(p::open(&start, Path::new("c"), c(&rw).truncate(true))); + check!(p::open(&start, Path::new("c"), c(&rw).create(true))); + check!(p::open(&start, Path::new("c"), &c(&rw))); + + // Test opening a file without setting an access mode + let mut blank = OO::new(); + error!( + p::open(&start, Path::new("f"), blank.create(true)), + invalid_options + ); + + // Test write works + check!(check!(h::create(&start, "h")).write("foobar".as_bytes())); + + // Test write fails for read-only + check!(p::open(&start, Path::new("h"), &r)); + { + let mut f = check!(p::open(&start, Path::new("h"), &r)); + assert!(f.write("wut".as_bytes()).is_err()); + } + + // Test write overwrites + { + let mut f = check!(p::open(&start, Path::new("h"), &c(&w))); + check!(f.write("baz".as_bytes())); + } + { + let mut f = check!(p::open(&start, Path::new("h"), &c(&r))); + let mut b = vec![0; 6]; + check!(f.read(&mut b)); + assert_eq!(b, "bazbar".as_bytes()); + } + + // Test truncate works + { + let mut f = check!(p::open(&start, Path::new("h"), c(&w).truncate(true))); + check!(f.write("foo".as_bytes())); + } + assert_eq!(check!(h::metadata(&start, "h")).len(), 3); +} + +#[test] +fn file_test_io_smoke_test() { + let message = "it's alright. have a good time"; + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test.txt"; + { + let mut write_stream = check!(h::create(&start, filename)); + check!(write_stream.write(message.as_bytes())); + } + { + let mut read_stream = check!(h::open(&start, filename)); + let mut read_buf = [0; 1028]; + let read_str = match check!(read_stream.read(&mut read_buf)) { + 0 => panic!("shouldn't happen"), + n => str::from_utf8(&read_buf[..n]).unwrap().to_string(), + }; + assert_eq!(read_str, message); + } + check!(p::remove_file(&start, Path::new(filename))); +} + +#[test] +fn invalid_path_raises() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_that_does_not_exist.txt"; + let result = h::open(&start, filename); + + #[cfg(any(all(unix, not(target_os = "vxworks")), target_os = "wasi"))] + error!(result, "No such file or directory"); + #[cfg(target_os = "vxworks")] + error!(result, "no such file or directory"); + #[cfg(windows)] + error!(result, 2); // ERROR_FILE_NOT_FOUND +} + +#[test] +fn file_test_iounlinking_invalid_path_should_raise_condition() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_another_file_that_does_not_exist.txt"; + + let result = p::remove_file(&start, Path::new(filename)); + + #[cfg(any(all(unix, not(target_os = "vxworks")), target_os = "wasi"))] + error!(result, "No such file or directory"); + #[cfg(target_os = "vxworks")] + error!(result, "no such file or directory"); + #[cfg(windows)] + error!(result, 2); // ERROR_FILE_NOT_FOUND +} + +#[test] +fn file_test_io_non_positional_read() { + let message: &str = "ten-four"; + let mut read_mem = [0; 8]; + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_positional.txt"; + { + let mut rw_stream = check!(h::create(&start, filename)); + check!(rw_stream.write(message.as_bytes())); + } + { + let mut read_stream = check!(h::open(&start, filename)); + { + let read_buf = &mut read_mem[0..4]; + check!(read_stream.read(read_buf)); + } + { + let read_buf = &mut read_mem[4..8]; + check!(read_stream.read(read_buf)); + } + } + check!(p::remove_file(&start, Path::new(filename))); + let read_str = str::from_utf8(&read_mem).unwrap(); + assert_eq!(read_str, message); +} + +#[test] +fn file_test_io_seek_and_tell_smoke_test() { + let message = "ten-four"; + let mut read_mem = [0; 4]; + let set_cursor = 4_u64; + let tell_pos_pre_read; + let tell_pos_post_read; + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_seeking.txt"; + { + let mut rw_stream = check!(h::create(&start, filename)); + check!(rw_stream.write(message.as_bytes())); + } + { + let mut read_stream = check!(h::open(&start, filename)); + check!(read_stream.seek(SeekFrom::Start(set_cursor))); + tell_pos_pre_read = check!(read_stream.seek(SeekFrom::Current(0))); + check!(read_stream.read(&mut read_mem)); + tell_pos_post_read = check!(read_stream.seek(SeekFrom::Current(0))); + } + check!(p::remove_file(&start, Path::new(filename))); + let read_str = str::from_utf8(&read_mem).unwrap(); + assert_eq!(read_str, &message[4..8]); + assert_eq!(tell_pos_pre_read, set_cursor); + assert_eq!(tell_pos_post_read, message.len() as u64); +} + +#[test] +fn file_test_io_seek_and_write() { + let initial_msg = "food-is-yummy"; + let overwrite_msg = "-the-bar!!"; + let final_msg = "foo-the-bar!!"; + let seek_idx = 3; + let mut read_mem = [0; 13]; + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_seek_and_write.txt"; + { + let mut rw_stream = check!(h::create(&start, filename)); + check!(rw_stream.write(initial_msg.as_bytes())); + check!(rw_stream.seek(SeekFrom::Start(seek_idx))); + check!(rw_stream.write(overwrite_msg.as_bytes())); + } + { + let mut read_stream = check!(h::open(&start, filename)); + check!(read_stream.read(&mut read_mem)); + } + check!(p::remove_file(&start, Path::new(filename))); + let read_str = str::from_utf8(&read_mem).unwrap(); + assert!(read_str == final_msg); +} + +#[test] +fn file_test_io_seek_shakedown() { + // 01234567890123 + let initial_msg = "qwer-asdf-zxcv"; + let chunk_one: &str = "qwer"; + let chunk_two: &str = "asdf"; + let chunk_three: &str = "zxcv"; + let mut read_mem = [0; 4]; + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_seek_shakedown.txt"; + { + let mut rw_stream = check!(h::create(&start, filename)); + check!(rw_stream.write(initial_msg.as_bytes())); + } + { + let mut read_stream = check!(h::open(&start, filename)); + + check!(read_stream.seek(SeekFrom::End(-4))); + check!(read_stream.read(&mut read_mem)); + assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_three); + + check!(read_stream.seek(SeekFrom::Current(-9))); + check!(read_stream.read(&mut read_mem)); + assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_two); + + check!(read_stream.seek(SeekFrom::Start(0))); + check!(read_stream.read(&mut read_mem)); + assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_one); + } + check!(p::remove_file(&start, Path::new(filename))); +} + +#[test] +fn file_test_io_eof() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_eof.txt"; + let mut buf = [0; 256]; + { + let oo = p::OpenOptions::new() + .create_new(true) + .write(true) + .read(true) + .clone(); + let mut rw = check!(p::open(&start, Path::new(filename), &oo)); + assert_eq!(check!(rw.read(&mut buf)), 0); + assert_eq!(check!(rw.read(&mut buf)), 0); + } + check!(p::remove_file(&start, Path::new(filename))); +} + +#[test] +#[cfg(unix)] +fn file_test_io_read_write_at() { + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_read_write_at.txt"; + let mut buf = [0; 256]; + let write1 = "asdf"; + let write2 = "qwer-"; + let write3 = "-zxcv"; + let content = "qwer-asdf-zxcv"; + { + let oo = p::OpenOptions::new() + .create_new(true) + .write(true) + .read(true) + .clone(); + let mut rw = check!(p::open(&start, Path::new(filename), &oo)); + assert_eq!(check!(rw.write_at(write1.as_bytes(), 5)), write1.len()); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 0); + assert_eq!(check!(rw.read_at(&mut buf, 5)), write1.len()); + assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 0); + assert_eq!( + check!(rw.read_at(&mut buf[..write2.len()], 0)), + write2.len() + ); + assert_eq!(str::from_utf8(&buf[..write2.len()]), Ok("\0\0\0\0\0")); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 0); + assert_eq!(check!(rw.write(write2.as_bytes())), write2.len()); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 5); + assert_eq!(check!(rw.read(&mut buf)), write1.len()); + assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 9); + assert_eq!( + check!(rw.read_at(&mut buf[..write2.len()], 0)), + write2.len() + ); + assert_eq!(str::from_utf8(&buf[..write2.len()]), Ok(write2)); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 9); + assert_eq!(check!(rw.write_at(write3.as_bytes(), 9)), write3.len()); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 9); + } + { + let mut read = check!(h::open(&start, filename)); + assert_eq!(check!(read.read_at(&mut buf, 0)), content.len()); + assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 0); + assert_eq!(check!(read.seek(SeekFrom::End(-5))), 9); + assert_eq!(check!(read.read_at(&mut buf, 0)), content.len()); + assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 9); + assert_eq!(check!(read.read(&mut buf)), write3.len()); + assert_eq!(str::from_utf8(&buf[..write3.len()]), Ok(write3)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + assert_eq!(check!(read.read_at(&mut buf, 0)), content.len()); + assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + assert_eq!(check!(read.read_at(&mut buf, 14)), 0); + assert_eq!(check!(read.read_at(&mut buf, 15)), 0); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + } + check!(p::remove_file(&start, Path::new(filename))); +} + +// Darwin doesn't have a way to change the permissions on a file, relative +// to a directory handle, with no read or write access, without blindly +// following symlinks. +#[test] +#[cfg(unix)] +#[cfg_attr( + any( + target_os = "macos", + target_os = "ios", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + ), + ignore +)] +#[test] +#[cfg(windows)] +fn file_test_io_seek_read_write() { + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + let filename = "file_rt_io_file_test_seek_read_write.txt"; + let mut buf = [0; 256]; + let write1 = "asdf"; + let write2 = "qwer-"; + let write3 = "-zxcv"; + let content = "qwer-asdf-zxcv"; + { + let oo = p::OpenOptions::new() + .create_new(true) + .write(true) + .read(true) + .clone(); + let mut rw = check!(p::open(&start, Path::new(filename), &oo)); + assert_eq!(check!(rw.seek_write(write1.as_bytes(), 5)), write1.len()); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 9); + assert_eq!(check!(rw.seek_read(&mut buf, 5)), write1.len()); + assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 9); + assert_eq!(check!(rw.seek(SeekFrom::Start(0))), 0); + assert_eq!(check!(rw.write(write2.as_bytes())), write2.len()); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 5); + assert_eq!(check!(rw.read(&mut buf)), write1.len()); + assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 9); + assert_eq!( + check!(rw.seek_read(&mut buf[..write2.len()], 0)), + write2.len() + ); + assert_eq!(str::from_utf8(&buf[..write2.len()]), Ok(write2)); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 5); + assert_eq!(check!(rw.seek_write(write3.as_bytes(), 9)), write3.len()); + assert_eq!(check!(rw.seek(SeekFrom::Current(0))), 14); + } + { + let mut read = check!(h::open(&start, filename)); + assert_eq!(check!(read.seek_read(&mut buf, 0)), content.len()); + assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + assert_eq!(check!(read.seek(SeekFrom::End(-5))), 9); + assert_eq!(check!(read.seek_read(&mut buf, 0)), content.len()); + assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + assert_eq!(check!(read.seek(SeekFrom::End(-5))), 9); + assert_eq!(check!(read.read(&mut buf)), write3.len()); + assert_eq!(str::from_utf8(&buf[..write3.len()]), Ok(write3)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + assert_eq!(check!(read.seek_read(&mut buf, 0)), content.len()); + assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); + assert_eq!(check!(read.seek(SeekFrom::Current(0))), 14); + assert_eq!(check!(read.seek_read(&mut buf, 14)), 0); + assert_eq!(check!(read.seek_read(&mut buf, 15)), 0); + } + check!(p::remove_file(&start, Path::new(filename))); +} + +#[test] +fn file_test_stat_is_correct_on_is_file() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_stat_correct_on_is_file.txt"; + { + let mut opts = p::OpenOptions::new(); + let mut fs = check!(p::open( + &start, + Path::new(filename), + opts.read(true).write(true).create(true) + )); + let msg = "hw"; + fs.write(msg.as_bytes()).unwrap(); + + let fstat_res = check!(fs.metadata()); + assert!(fstat_res.is_file()); + } + let stat_res_fn = check!(h::metadata(&start, filename)); + assert!(stat_res_fn.file_type().is_file()); + check!(p::remove_file(&start, Path::new(filename))); +} + +#[test] +fn file_test_stat_is_correct_on_is_dir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let filename = "file_stat_correct_on_is_dir"; + check!(h::create_dir(&start, filename)); + let stat_res_fn = check!(h::metadata(&start, filename)); + assert!(stat_res_fn.is_dir()); + check!(p::remove_dir(&start, Path::new(filename))); +} + +#[test] +fn file_test_fileinfo_false_when_checking_is_file_on_a_directory() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "fileinfo_false_on_dir"; + check!(h::create_dir(&start, dir)); + assert!(!h::is_file(&start, dir)); + check!(p::remove_dir(&start, Path::new(dir))); +} + +#[test] +fn file_test_fileinfo_check_exists_before_and_after_file_creation() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let file = "fileinfo_check_exists_b_and_a.txt"; + check!(check!(h::create(&start, file)).write(b"foo")); + assert!(h::exists(&start, file)); + check!(p::remove_file(&start, Path::new(file))); + assert!(!h::exists(&start, file)); +} + +#[test] +fn file_test_directoryinfo_check_exists_before_and_after_mkdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "before_and_after_dir"; + assert!(!h::exists(&start, dir)); + check!(h::create_dir(&start, dir)); + assert!(h::exists(&start, dir)); + assert!(h::is_dir(&start, dir)); + check!(p::remove_dir(&start, Path::new(dir))); + assert!(!h::exists(&start, dir)); +} + +#[test] +fn file_test_directoryinfo_readdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "di_readdir"; + check!(h::create_dir(&start, dir)); + let prefix = "foo"; + for n in 0..3 { + let f = format!("{n}.txt"); + let mut w = check!(h::create(&start, &f)); + let msg_str = format!("{}{}", prefix, n.to_string()); + let msg = msg_str.as_bytes(); + check!(w.write(msg)); + } + let files = check!(h::read_dir(&start, dir)); + let mut mem = [0; 4]; + for f in files { + let f = f.unwrap().file_name(); + { + check!(check!(h::open(&start, &f)).read(&mut mem)); + let read_str = str::from_utf8(&mem).unwrap(); + let expected = format!("{}{}", prefix, f.to_str().unwrap()); + assert_eq!(expected, read_str); + } + check!(p::remove_file(&start, Path::new(&f))); + } + check!(p::remove_dir(&start, Path::new(dir))); +} + +#[test] +fn file_create_new_already_exists_error() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let file = "file_create_new_error_exists"; + check!(h::create(&start, file)); + let e = p::open( + &start, + Path::new(file), + p::OpenOptions::new().write(true).create_new(true), + ) + .unwrap_err(); + assert_eq!(e.kind(), ErrorKind::AlreadyExists); +} + +#[test] +fn mkdir_path_already_exists_error() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "mkdir_error_twice"; + check!(h::create_dir(&start, dir)); + let e = h::create_dir(&start, dir).unwrap_err(); + assert_eq!(e.kind(), ErrorKind::AlreadyExists); +} + +#[test] +fn recursive_mkdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "d1/d2"; + check!(h::create_dir_all(&start, dir)); + assert!(h::is_dir(&start, dir)); +} + +#[test] +fn concurrent_recursive_mkdir() { + for _ in 0..100 { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let mut name = PathBuf::from("a"); + for _ in 0..40 { + name = name.join("a"); + } + let mut join = vec![]; + for _ in 0..8 { + let dir = check!(start.try_clone()); + let name = name.clone(); + join.push(thread::spawn(move || { + check!(h::create_dir_all(&dir, &name)); + })) + } + + // No `Display` on result of `join()` + join.drain(..).map(|join| join.join().unwrap()).count(); + } +} + +#[test] +fn recursive_mkdir_slash() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + error_contains!( + h::create_dir_all(&start, Path::new("/")), + "a path led outside of the filesystem" + ); +} + +#[test] +fn recursive_mkdir_dot() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir_all(&start, Path::new("."))); +} + +#[test] +fn recursive_mkdir_empty() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir_all(&start, Path::new(""))); +} + +#[test] +fn unicode_path_is_dir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + assert!(h::is_dir(&start, Path::new("."))); + assert!(!h::is_dir(&start, Path::new("test/stdtest/fs.rs"))); + + let mut dirpath = PathBuf::new(); + dirpath.push("test-가一ー你好"); + check!(h::create_dir(&start, &dirpath)); + assert!(h::is_dir(&start, &dirpath)); + + let mut filepath = dirpath; + filepath.push("unicode-file-\u{ac00}\u{4e00}\u{30fc}\u{4f60}\u{597d}.rs"); + check!(h::create(&start, &filepath)); // ignore return; touch only + assert!(!h::is_dir(&start, &filepath)); + assert!(h::exists(&start, filepath)); +} + +#[test] +fn unicode_path_exists() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + assert!(h::exists(&start, Path::new("."))); + assert!(!h::exists(&start, Path::new("test/nonexistent-bogus-path"))); + + let unicode = PathBuf::new(); + let unicode = unicode.join("test-각丁ー再见"); + check!(h::create_dir(&start, &unicode)); + assert!(h::exists(&start, unicode)); + assert!(!h::exists( + &start, + Path::new("test/unicode-bogus-path-각丁ー再见") + )); +} + +#[test] +fn symlinks_work() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + if !got_symlink_permission(&start) { + return; + }; + + let input = "in.txt"; + let out = "out.txt"; + + check!(check!(h::create(&start, &input)).write("foobar".as_bytes())); + check!(h::symlink_file(&start, &input, &out)); + assert!( + check!(h::symlink_metadata(&start, out)) + .file_type() + .is_symlink() + ); + assert_eq!( + check!(h::metadata(&start, &out)).len(), + check!(h::metadata(&start, &input)).len() + ); + let mut v = Vec::new(); + check!(check!(h::open(&start, &out)).read_to_end(&mut v)); + assert_eq!(v, b"foobar".to_vec()); +} + +#[test] +fn symlink_noexist() { + // Symlinks can point to things that don't exist + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + if !got_symlink_permission(&start) { + return; + }; + + // Use a relative path for testing. Symlinks get normalized by Windows, + // so we might not get the same path back for absolute paths + check!(h::symlink_file(&start, &"foo", "bar")); + assert_eq!( + check!(p::read_link(&start, Path::new("bar"))) + .to_str() + .unwrap(), + "foo" + ); +} + +#[test] +fn read_link() { + if cfg!(windows) { + // directory symlink + let root = h::open_ambient_dir(r"C:\").unwrap(); + error_contains!( + p::read_link(&root, Path::new(r"Users\All Users")), + "a path led outside of the filesystem" + ); + // junction + error_contains!( + p::read_link(&root, Path::new(r"Users\Default User")), + "a path led outside of the filesystem" + ); + // junction with special permissions + error_contains!( + p::read_link(&root, Path::new(r"Documents and Settings\")), + "a path led outside of the filesystem" + ); + } + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let link = "link"; + if !got_symlink_permission(&start) { + return; + }; + check!(h::symlink_file(&start, &"foo", &link)); + assert_eq!( + check!(p::read_link(&start, Path::new(&link))) + .to_str() + .unwrap(), + "foo" + ); +} + +#[test] +fn readlink_not_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + match p::read_link(&start, Path::new(".")) { + Ok(..) => panic!("wanted a failure"), + Err(..) => {} + } +} + +#[cfg(not(windows))] +#[test] +fn read_link_contents() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let link = "link"; + if !got_symlink_permission(&start) { + return; + }; + check!(h::symlink_file(&start, &"foo", &link)); + assert_eq!( + check!(super::super::read_link::read_link_contents( + &start, + Path::new(link) + )) + .to_str() + .unwrap(), + "foo" + ); +} + +#[cfg(not(windows))] +#[test] +fn read_link_contents_absolute() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let link = "link"; + if !got_symlink_permission(&start) { + return; + }; + check!(std::os::unix::fs::symlink("/foo", tmpdir.path().join(link))); + assert_eq!( + check!(super::super::read_link::read_link_contents( + &start, + Path::new(link) + )) + .to_str() + .unwrap(), + "/foo" + ); +} + +#[test] +fn links_work() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let input = "in.txt"; + let out = "out.txt"; + + check!(check!(h::create(&start, &input)).write("foobar".as_bytes())); + check!(p::hard_link( + &start, + Path::new(&input), + &start, + Path::new(&out) + )); + assert_eq!( + check!(h::metadata(&start, &out)).len(), + check!(h::metadata(&start, &input)).len() + ); + assert_eq!( + check!(h::metadata(&start, &out)).len(), + check!(h::metadata(&start, input)).len() + ); + let mut v = Vec::new(); + check!(check!(h::open(&start, &out)).read_to_end(&mut v)); + assert_eq!(v, b"foobar".to_vec()); + + // can't link to yourself + match p::hard_link(&start, Path::new(&input), &start, Path::new(&input)) { + Ok(..) => panic!("wanted a failure"), + Err(..) => {} + } + // can't link to something that doesn't exist + match p::hard_link(&start, Path::new("foo"), &start, Path::new("bar")) { + Ok(..) => panic!("wanted a failure"), + Err(..) => {} + } +} + +#[test] +fn sync_doesnt_kill_anything() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let path = "in.txt"; + + let mut file = check!(h::create(&start, &path)); + check!(file.sync_all()); + check!(file.sync_data()); + check!(file.write(b"foo")); + check!(file.sync_all()); + check!(file.sync_data()); +} + +#[test] +fn truncate_works() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let path = "in.txt"; + + let mut file = check!(h::create(&start, &path)); + check!(file.write(b"foo")); + check!(file.sync_all()); + + // Do some simple things with truncation + assert_eq!(check!(file.metadata()).len(), 3); + check!(file.set_len(10)); + assert_eq!(check!(file.metadata()).len(), 10); + check!(file.write(b"bar")); + check!(file.sync_all()); + assert_eq!(check!(file.metadata()).len(), 10); + + let mut v = Vec::new(); + check!(check!(h::open(&start, &path)).read_to_end(&mut v)); + assert_eq!(v, b"foobar\0\0\0\0".to_vec()); + + // Truncate to a smaller length, don't seek, and then write something. + // Ensure that the intermediate zeroes are all filled in (we have `seek`ed + // past the end of the file). + check!(file.set_len(2)); + assert_eq!(check!(file.metadata()).len(), 2); + check!(file.write(b"wut")); + check!(file.sync_all()); + assert_eq!(check!(file.metadata()).len(), 9); + let mut v = Vec::new(); + check!(check!(h::open(&start, &path)).read_to_end(&mut v)); + assert_eq!(v, b"fo\0\0\0\0wut".to_vec()); +} + +#[test] +fn _assert_send_sync() { + fn _assert_send_sync() {} + _assert_send_sync::(); +} + +#[test] +fn binary_file() { + let mut bytes = [0; 1024]; + rand::rng().fill_bytes(&mut bytes); + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + + check!(check!(h::create(&start, "test")).write(&bytes)); + let mut v = Vec::new(); + check!(check!(h::open(&start, "test")).read_to_end(&mut v)); + assert!(v == &bytes[..]); +} + +#[test] +fn write_then_read() { + let mut bytes = [0; 1024]; + rand::rng().fill_bytes(&mut bytes); + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + + check!(h::write(&start, "test", &bytes[..])); + let v = check!(h::read(&start, "test")); + assert!(v == &bytes[..]); + + check!(h::write(&start, "not-utf8", &[0xFF])); + error_contains!( + h::read_to_string(&start, "not-utf8"), + "stream did not contain valid UTF-8" + ); + + let s = "𐁁𐀓𐀠𐀴𐀍"; + check!(h::write(&start, "utf8", s.as_bytes())); + let string = check!(h::read_to_string(&start, "utf8")); + assert_eq!(string, s); +} + +#[test] +fn file_try_clone() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let mut f1 = check!(p::open( + &start, + Path::new("test"), + p::OpenOptions::new().read(true).write(true).create(true) + )); + let mut f2 = check!(f1.try_clone()); + + check!(f1.write_all(b"hello world")); + check!(f1.seek(SeekFrom::Start(2))); + + let mut buf = vec![]; + check!(f2.read_to_end(&mut buf)); + assert_eq!(buf, b"llo world"); + drop(f2); + + check!(f1.write_all(b"!")); +} + +#[test] +fn mkdir_trailing_slash() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let path = PathBuf::from("file"); + check!(h::create_dir_all(&start, &path.join("a/"))); +} + +#[test] +fn dir_entry_debug() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + h::create(&start, "b").unwrap(); + let mut read_dir = h::read_dir(&start, ".").unwrap(); + let dir_entry = read_dir.next().unwrap().unwrap(); + let actual = format!("{dir_entry:?}"); + let expected = format!("DirEntry({:?})", dir_entry.file_name()); + assert_eq!(actual, expected); +} + +#[test] +fn read_dir_not_found() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let res = h::read_dir(&start, "path/that/does/not/exist"); + assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); +} + +// On Windows, `symlink_junction` somehow creates a symlink where `read_link` +// returns a relative path prefixed with "\\\\?\\", which `std::path::Path` +// parses as a `Prefix`, making cap-std think it's an absolute path and +// therefore a sandbox escape attempt. This only seems to happen with +// `symlink_junction`, and not symlinks created with standard library APIs. I +// don't know what the right thing to do here is. For now, disable these tests. +#[cfg_attr(windows, ignore)] +#[test] +fn create_dir_all_with_junctions() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let target = "target"; + + let junction = PathBuf::from("junction"); + let b = junction.join("a/b"); + + let link = PathBuf::from("link"); + let d = link.join("c/d"); + + h::create_dir(&start, &target).unwrap(); + + check!(symlink_junction(&target, &start, &junction)); + check!(h::create_dir_all(&start, &b)); + // the junction itself is not a directory, but `is_dir()` on a Path + // follows links + assert!(h::is_dir(&start, junction)); + assert!(h::exists(&start, b)); + + if !got_symlink_permission(&start) { + return; + }; + check!(h::symlink_dir(&start, &target, &link)); + check!(h::create_dir_all(&start, &d)); + assert!(h::is_dir(&start, link)); + assert!(h::exists(&start, d)); +} + +#[test] +fn metadata_access_times() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let b = "b"; + h::create(&start, &b).unwrap(); + + let a = check!(h::metadata(&start, ".")); + let b = check!(h::metadata(&start, &b)); + + assert_eq!(check!(a.accessed()), check!(a.accessed())); + assert_eq!(check!(a.modified()), check!(a.modified())); + // This assert from std's testsuite is racy. + //assert_eq!(check!(b.accessed()), check!(b.modified())); + + if cfg!(target_os = "macos") || cfg!(target_os = "windows") { + check!(a.created()); + check!(b.created()); + } + + if cfg!(any(target_os = "android", target_os = "linux")) { + // Not always available + match (a.created(), b.created()) { + (Ok(t1), Ok(t2)) => assert!(t1 <= t2), + (Err(e1), Err(e2)) + if e1.kind() == ErrorKind::Other && e2.kind() == ErrorKind::Other => {} + (a, b) => { + panic!("creation time must be always supported or not supported: {a:?} {b:?}",) + } + } + } +} + +/// Test creating hard links to symlinks. +#[test] +fn symlink_hard_link() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + if !got_symlink_permission(&start) { + return; + } + if !able_to_not_follow_symlinks_while_hard_linking() { + return; + } + + // Create "file", a file. + check!(h::create(&start, "file")); + + // Create "symlink", a symlink to "file". + check!(h::symlink_file(&start, "file", "symlink")); + + // Create "hard_link", a hard link to "symlink". + check!(p::hard_link( + &start, + Path::new("symlink"), + &start, + Path::new("hard_link") + )); + + // "hard_link" should appear as a symlink. + assert!( + check!(h::symlink_metadata(&start, "hard_link")) + .file_type() + .is_symlink() + ); + + // We sould be able to open "file" via any of the above names. + let _ = check!(h::open(&start, "file")); + assert!(h::open(&start, "file.renamed").is_err()); + let _ = check!(h::open(&start, "symlink")); + let _ = check!(h::open(&start, "hard_link")); + + // Rename "file" to "file.renamed". + check!(p::rename( + &start, + Path::new("file"), + &start, + Path::new("file.renamed") + )); + + // Now, the symlink and the hard link should be dangling. + assert!(h::open(&start, "file").is_err()); + let _ = check!(h::open(&start, "file.renamed")); + assert!(h::open(&start, "symlink").is_err()); + assert!(h::open(&start, "hard_link").is_err()); + + // The symlink and the hard link should both still point to "file". + assert!(p::read_link(&start, Path::new("file")).is_err()); + assert!(p::read_link(&start, Path::new("file.renamed")).is_err()); + assert_eq!( + check!(p::read_link(&start, Path::new("symlink"))), + Path::new("file") + ); + assert_eq!( + check!(p::read_link(&start, Path::new("hard_link"))), + Path::new("file") + ); + + // Remove "file.renamed". + check!(p::remove_file(&start, Path::new("file.renamed"))); + + // Now, we can't open the file by any name. + assert!(h::open(&start, "file").is_err()); + assert!(h::open(&start, "file.renamed").is_err()); + assert!(h::open(&start, "symlink").is_err()); + assert!(h::open(&start, "hard_link").is_err()); + + // "hard_link" should still appear as a symlink. + assert!( + check!(h::symlink_metadata(&start, "hard_link")) + .file_type() + .is_symlink() + ); +} + +/// Ensure `fs::create_dir` works on Windows with longer paths. +#[test] +#[cfg(windows)] +fn create_dir_long_paths() { + use std::ffi::OsStr; + use std::iter; + use std::os::windows::ffi::OsStrExt; + const PATH_LEN: usize = 247; + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + let mut path = PathBuf::new(); + path.push("a"); + let mut path = path.into_os_string(); + + let utf16_len = path.encode_wide().count(); + if utf16_len >= PATH_LEN { + // Skip the test in the unlikely event the local user has a long temp directory + // path. This should not affect CI. + return; + } + // Increase the length of the path. + path.extend(iter::repeat(OsStr::new("a")).take(PATH_LEN - utf16_len)); + + // This should succeed. + h::create_dir(&start, &path).unwrap(); + + // This will fail if the path isn't converted to verbatim. + path.push("a"); + h::create_dir(&start, &path).unwrap(); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs b/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs new file mode 100644 index 000000000000..24de32f33873 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs @@ -0,0 +1,1359 @@ +// This file contains additional fs tests that didn't make it into `fs.rs`. +// The reason for additional module to contain those is so that `fs.rs` mirrors +// Rust's libstd tests. +use super::helpers as h; +use super::sys_common::io::tmpdir; +use super::sys_common::symlink_supported; +use crate::filesystem::primitives as p; +use std::io::{Read, Write}; +use std::path::Path; +use std::str; + +#[test] +fn dir_writable() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + #[cfg(not(windows))] + error_contains!(h::create(&start, "dir"), "Is a directory"); + #[cfg(windows)] + error!(h::create(&start, "dir"), 5); + error_contains!( + p::open(&start, Path::new("dir"), p::OpenOptions::new().write(true)), + "Is a directory" + ); + + error_contains!(h::create(&start, "dir/."), "Is a directory"); + error_contains!( + p::open( + &start, + Path::new("dir/."), + p::OpenOptions::new().write(true) + ), + "Is a directory" + ); + + error_contains!(h::create(&start, "dir/.."), "Is a directory"); + error_contains!( + p::open( + &start, + Path::new("dir/.."), + p::OpenOptions::new().write(true) + ), + "Is a directory" + ); +} + +#[test] +fn readdir_write() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + assert!(p::open(&start, Path::new("dir"), p::OpenOptions::new().write(true)).is_err()); + assert!(p::open(&start, Path::new("dir/"), p::OpenOptions::new().write(true)).is_err()); + + #[cfg(any(target_os = "android", target_os = "linux"))] + { + use crate::filesystem::primitives::OpenOptionsExt; + assert!( + p::open( + &start, + Path::new("dir"), + p::OpenOptions::new() + .write(true) + .custom_flags(rustix::fs::OFlags::DIRECTORY.bits() as i32) + ) + .is_err() + ); + } +} + +#[test] +fn maybe_dir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + + // Opening directories works on non-Windows platforms. + #[cfg(not(windows))] + check!(h::open(&start, "dir")); + + // Opening directories fails on Windows. + #[cfg(windows)] + assert!(h::open(&start, "dir").is_err()); +} + +#[test] +fn optionally_recursive_mkdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "d1/d2"; + check!(h::create_dir_all(&start, dir)); + assert!(h::is_dir(&start, dir)); +} + +#[test] +fn optionally_nonrecursive_mkdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "d1/d2"; + #[cfg(not(windows))] + error!( + p::create_dir(&start, Path::new(dir), &p::DirOptions::new()), + "No such file" + ); + #[cfg(windows)] + error!( + p::create_dir(&start, Path::new(dir), &p::DirOptions::new()), + 2 + ); + + assert!(!h::exists(&start, dir)); +} + +#[test] +fn dotdot_at_end_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir(&start, "b")); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "..", "up")); + + // Do some things with `path` that might break with an `O_PATH` fd. + // The `permissions` part of this test is gone with `set_permissions`, but + // the `read_dir` part is the part that exercises the `O_PATH` fd. + let path = "b/up"; + + check!(h::metadata(&start, path)); + + let contents = check!(h::read_dir(&start, path)); + for entry in contents { + let _entry = check!(entry); + } +} + +#[test] +fn dotdot_at_end_of_symlink_all_inside_dir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::create_dir(&start, "dir")); + check!(h::write(&start, "dir/target", foo)); + check!(h::create_dir(&start, "dir/b")); + let b = check!(p::open_dir(&start, Path::new("dir/b"))); + check!(h::symlink_dir(&b, "..", "up")); + + // Do some things with `path` that might break with an `O_PATH` fd. + // The `permissions` part of this test is gone with `set_permissions`, but + // the `read_dir` part is the part that exercises the `O_PATH` fd. + let path = "dir/b/up"; + + check!(h::metadata(&start, path)); + + let contents = check!(h::read_dir(&start, path)); + for entry in contents { + let _entry = check!(entry); + } +} + +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_at_end_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir(&start, "b")); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "../.", "up")); + + // Do some things with `path` that might break with an `O_PATH` fd. + // The `permissions` part of this test is gone with `set_permissions`, but + // the `read_dir` part is the part that exercises the `O_PATH` fd. + let path = "b/up"; + + check!(h::metadata(&start, path)); + + let contents = check!(h::read_dir(&start, path)); + for entry in contents { + let _entry = check!(entry); + } +} + +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_at_end_of_symlink_all_inside_dir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::create_dir(&start, "dir")); + check!(h::write(&start, "dir/target", foo)); + check!(h::create_dir(&start, "dir/b")); + let b = check!(p::open_dir(&start, Path::new("dir/b"))); + check!(h::symlink_dir(&b, "../.", "up")); + + // Do some things with `path` that might break with an `O_PATH` fd. + // The `permissions` part of this test is gone with `set_permissions`, but + // the `read_dir` part is the part that exercises the `O_PATH` fd. + let path = "dir/b/up"; + + check!(h::metadata(&start, path)); + + let contents = check!(h::read_dir(&start, path)); + for entry in contents { + let _entry = check!(entry); + } +} + +#[test] +fn recursive_mkdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "d1/d2"; + check!(h::create_dir_all(&start, dir)); + assert!(h::is_dir(&start, "d1")); + let dir = check!(p::open_dir(&start, Path::new("d1"))); + assert!(h::is_dir(&dir, "d2")); + assert!(h::is_dir(&start, "d1/d2")); +} + +#[test] +#[cfg_attr(windows, ignore)] // TODO investigate why this one is failing +fn open_various() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + #[cfg(not(windows))] + error!(h::create(&start, ""), "No such file"); + #[cfg(windows)] + error!(h::create(&start, ""), 2); + + #[cfg(not(windows))] + error!(h::create(&start, "."), "Is a directory"); + #[cfg(windows)] + error!(h::create(&start, "."), 2); +} + +#[test] +fn trailing_slash() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create(&start, "file")); + + #[cfg(not(windows))] + { + error!(h::open(&start, "file/../file"), "Not a directory"); + error!(h::open(&start, "file/.."), "Not a directory"); + error!(h::open(&start, "file/."), "Not a directory"); + error!(h::open(&start, "file/../file/"), "Not a directory"); + error!(h::open(&start, "file/"), "Not a directory"); + error!( + p::open_dir(&start, Path::new("file/../file/")), + "Not a directory" + ); + error!( + p::open_dir(&start, Path::new("file/../file")), + "Not a directory" + ); + error!(p::open_dir(&start, Path::new("file/..")), "Not a directory"); + error!(p::open_dir(&start, Path::new("file/.")), "Not a directory"); + error!(p::open_dir(&start, Path::new("file/")), "Not a directory"); + } + + #[cfg(windows)] + { + assert!(check!(check!(h::open(&start, "file/../file")).metadata()).is_file()); + assert!( + check!(p::Metadata::from_file(&check!(p::open_dir( + &start, + Path::new("file/..") + )))) + .is_dir() + ); + assert!(check!(check!(h::open(&start, "file/.")).metadata()).is_file()); + assert!(p::open_dir(&start, Path::new("file/../file/")).is_err()); + assert!(p::open_dir(&start, Path::new("file/./")).is_err()); + assert!(p::open_dir(&start, Path::new("file//")).is_err()); + assert!(p::open_dir(&start, Path::new("file/../file")).is_err()); + assert!(p::open_dir(&start, Path::new("file/.")).is_err()); + assert!(p::open_dir(&start, Path::new("file/")).is_err()); + } +} + +#[test] +fn trailing_slash_in_dir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + check!(h::create(&start, "dir/file")); + + #[cfg(not(windows))] + { + error!(h::open(&start, "dir/file/../file"), "Not a directory"); + error!(h::open(&start, "dir/file/.."), "Not a directory"); + error!(h::open(&start, "dir/file/."), "Not a directory"); + error!(h::open(&start, "dir/file/../file/"), "Not a directory"); + error!(h::open(&start, "dir/file/"), "Not a directory"); + error!( + p::open_dir(&start, Path::new("dir/file/../file/")), + "Not a directory" + ); + error!( + p::open_dir(&start, Path::new("dir/file/../file")), + "Not a directory" + ); + error!( + p::open_dir(&start, Path::new("dir/file/..")), + "Not a directory" + ); + error!( + p::open_dir(&start, Path::new("dir/file/.")), + "Not a directory" + ); + error!( + p::open_dir(&start, Path::new("dir/file/")), + "Not a directory" + ); + } + + #[cfg(windows)] + { + assert!(check!(check!(h::open(&start, "dir/file/../file")).metadata()).is_file()); + assert!( + check!(p::Metadata::from_file(&check!(p::open_dir( + &start, + Path::new("dir/file/..") + )))) + .is_dir() + ); + assert!(check!(check!(h::open(&start, "dir/file/.")).metadata()).is_file()); + assert!(h::open(&start, "dir/file/../file/").is_err()); + let _ = check!(h::open(&start, "dir/file/../file/.")); + assert!(h::open(&start, "dir/file/../file/./").is_err()); + assert!(h::open(&start, "dir/file/").is_err()); + let _ = check!(h::open(&start, "dir/file/.")); + let _ = check!(h::open(&start, "dir/file/../file/.")); + assert!(h::open(&start, "dir/file/../file/./").is_err()); + assert!(h::open(&start, "dir/file/").is_err()); + let _ = check!(h::open(&start, "dir/file/.")); + assert!(h::open(&start, "dir/file/./").is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/../file/")).is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/../file/.")).is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/../file/./")).is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/../file")).is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/.")).is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/./")).is_err()); + assert!(p::open_dir(&start, Path::new("dir/file/")).is_err()); + } +} + +#[test] +#[cfg_attr(windows, ignore)] // TODO investigate why this one is failing +fn rename_slashdots() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + check!(p::rename( + &start, + Path::new("dir"), + &start, + Path::new("dir") + )); + check!(p::rename( + &start, + Path::new("dir"), + &start, + Path::new("dir/") + )); + check!(p::rename( + &start, + Path::new("dir/"), + &start, + Path::new("dir") + )); + check!(p::rename( + &start, + Path::new("dir/"), + &start, + Path::new("dir/") + )); + + // TODO: Platform-specific error code. + error_contains!( + p::rename(&start, Path::new("dir"), &start, Path::new("dir/.")), + "" + ); + error_contains!( + p::rename(&start, Path::new("dir/."), &start, Path::new("dir")), + "" + ); +} + +#[test] +#[cfg_attr(windows, ignore)] // TODO investigate why this one is failing +fn rename_slashdots_ambient() { + let dir = tempfile::tempdir().unwrap(); + + check!(std::fs::create_dir_all(dir.path().join("dir"))); + check!(std::fs::rename( + dir.path().join("dir"), + dir.path().join("dir") + )); + check!(std::fs::rename( + dir.path().join("dir"), + dir.path().join("dir/") + )); + check!(std::fs::rename( + dir.path().join("dir/"), + dir.path().join("dir") + )); + check!(std::fs::rename( + dir.path().join("dir/"), + dir.path().join("dir/") + )); + + // TODO: Platform-specific error code. + error_contains!( + std::fs::rename(dir.path().join("dir"), dir.path().join("dir/.")), + "" + ); + error_contains!( + std::fs::rename(dir.path().join("dir/."), dir.path().join("dir")), + "" + ); +} + +#[test] +fn try_exists() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + assert_eq!(h::exists(&start, "somefile"), false); + let dir = Path::new("d1/d2"); + let parent = dir.parent().unwrap(); + assert_eq!(h::exists(&start, parent), false); + assert_eq!(h::exists(&start, dir), false); + check!(h::create_dir(&start, parent)); + assert_eq!(h::exists(&start, parent), true); + assert_eq!(h::exists(&start, dir), false); + check!(h::create_dir(&start, dir)); + assert_eq!(h::exists(&start, dir), true); +} + +#[test] +fn file_test_directoryinfo_readdir() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + let dir = "di_readdir"; + check!(h::create_dir(&start, dir)); + let prefix = "foo"; + for n in 0..3 { + let f = format!("{n}.txt"); + let mut w = check!(h::create(&start, &f)); + let msg_str = format!("{}{}", prefix, n.to_string()); + let msg = msg_str.as_bytes(); + check!(w.write(msg)); + } + let sub = check!(p::open_dir(&start, Path::new(dir))); + let files = check!(p::read_base_dir(&sub)); + let mut mem = [0; 4]; + for f in files { + let f = f.unwrap(); + { + check!(check!(h::open(&sub, f.file_name())).read(&mut mem)); + let read_str = str::from_utf8(&mem).unwrap(); + let expected = format!("{}{}", prefix, f.file_name().to_str().unwrap()); + assert_eq!(expected, read_str); + } + check!(p::remove_file(&sub, Path::new(&f.file_name()))); + } + drop(sub); + check!(p::remove_dir(&start, Path::new(dir))); +} + +#[test] +fn follow_dotdot_symlink() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + check!(h::create_dir_all(&start, "a/b")); + check!(h::symlink_dir(&start, "..", "a/b/c")); + check!(h::symlink_dir(&start, "../..", "a/b/d")); + check!(h::symlink_dir(&start, "../../..", "a/b/e")); + check!(h::symlink_dir(&start, "../../../..", "a/b/f")); + + check!(p::open_dir(&start, Path::new("a/b/c"))); + assert!(check!(h::metadata(&start, "a/b/c")).is_dir()); + + #[cfg(windows)] + { + error!(p::open_dir(&start, Path::new("a/b/d")), 123); + error!(h::metadata(&start, "a/b/d"), 123); + + error!(p::open_dir(&start, Path::new("a/b/e")), 123); + error!(h::metadata(&start, "a/b/e"), 123); + + error!(p::open_dir(&start, Path::new("a/b/f")), 123); + error!(h::metadata(&start, "a/b/f"), 123); + } + + #[cfg(not(windows))] + { + check!(p::open_dir(&start, Path::new("a/b/d"))); + assert!(check!(h::metadata(&start, "a/b/d")).is_dir()); + + assert!(p::open_dir(&start, Path::new("a/b/e")).is_err()); + assert!(h::metadata(&start, "a/b/e").is_err()); + + assert!(p::open_dir(&start, Path::new("a/b/f")).is_err()); + assert!(h::metadata(&start, "a/b/f").is_err()); + } +} + +#[test] +fn follow_dotdot_symlink_ambient() { + #[cfg(unix)] + use std::os::unix::fs::symlink as symlink_dir; + #[cfg(windows)] + use std::os::windows::fs::symlink_dir; + + if !symlink_supported() { + return; + } + + let dir = tempfile::tempdir().unwrap(); + check!(std::fs::create_dir_all(dir.path().join("a/b"))); + check!(symlink_dir("..", dir.path().join("a/b/c"))); + check!(symlink_dir("../..", dir.path().join("a/b/d"))); + check!(symlink_dir("../../..", dir.path().join("a/b/e"))); + check!(symlink_dir("../../../..", dir.path().join("a/b/f"))); + + check!(h::open_ambient_dir(dir.path().join("a/b/c"))); + assert!(check!(std::fs::metadata(dir.path().join("a/b/c"))).is_dir()); + + #[cfg(windows)] + { + error!(h::open_ambient_dir(dir.path().join("a/b/d")), 123); + error!(std::fs::metadata(dir.path().join("a/b/d")), 123); + + error!(h::open_ambient_dir(dir.path().join("a/b/e")), 123); + error!(std::fs::metadata(dir.path().join("a/b/e")), 123); + + error!(h::open_ambient_dir(dir.path().join("a/b/f")), 123); + error!(std::fs::metadata(dir.path().join("a/b/f")), 123); + } + + #[cfg(not(windows))] + { + check!(h::open_ambient_dir(dir.path().join("a/b/d"))); + assert!(check!(std::fs::metadata(dir.path().join("a/b/d"))).is_dir()); + + check!(h::open_ambient_dir(dir.path().join("a/b/e"))); + assert!(check!(std::fs::metadata(dir.path().join("a/b/e"))).is_dir()); + + check!(h::open_ambient_dir(dir.path().join("a/b/f"))); + assert!(check!(std::fs::metadata(dir.path().join("a/b/f"))).is_dir()); + } +} + +#[test] +fn follow_file_symlink() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + + check!(h::create(&start, "file")); + + check!(h::symlink_file(&start, "file", "link")); + check!(h::symlink_dir(&start, "file/", "link_slash")); + check!(h::symlink_file(&start, "file/.", "link_slashdot")); + check!(h::symlink_dir(&start, "file/..", "link_slashdotdot")); + + check!(h::open(&start, "link")); + assert!(h::open(&start, "link_slash").is_err()); + + #[cfg(windows)] + { + error!(h::open(&start, "link_slashdot"), 123); + error!(p::open_dir(&start, Path::new("link_slashdotdot")), 123); + } + #[cfg(not(windows))] + { + assert!(h::open(&start, "link_slash").is_err()); + assert!(h::open(&start, "link_slashdot").is_err()); + assert!(p::open_dir(&start, Path::new("link_slashdotdot")).is_err()); + } +} + +/// This test is the same as `check_dot_access` but uses `std::fs`' +/// ambient API instead of `cap_std`. The purpose of this test is to +/// confirm fundamentally OS-specific differences. +#[cfg(unix)] +#[test] +fn check_dot_access_ambient() { + use std::fs; + use std::os::unix::fs::DirBuilderExt; + + let dir = tempfile::tempdir().unwrap(); + + let mut options = std::fs::DirBuilder::new(); + options.mode(0o477); + check!(options.create(dir.path().join("dir"))); + + check!(fs::metadata(dir.path().join("."))); + check!(fs::metadata(dir.path().join("dir"))); + check!(fs::metadata(dir.path().join("dir/"))); + check!(fs::metadata(dir.path().join("dir//"))); + + if !cfg!(target_os = "freebsd") { + assert!(fs::metadata(dir.path().join("dir/.")).is_err()); + assert!(fs::metadata(dir.path().join("dir/./")).is_err()); + assert!(fs::metadata(dir.path().join("dir/.//")).is_err()); + assert!(fs::metadata(dir.path().join("dir/./.")).is_err()); + assert!(fs::metadata(dir.path().join("dir/.//.")).is_err()); + assert!(fs::metadata(dir.path().join("dir/..")).is_err()); + assert!(fs::metadata(dir.path().join("dir/../")).is_err()); + assert!(fs::metadata(dir.path().join("dir/..//")).is_err()); + assert!(fs::metadata(dir.path().join("dir/../.")).is_err()); + assert!(fs::metadata(dir.path().join("dir/..//.")).is_err()); + } +} + +// Windows allows one to open "file/." and "file/.." and similar, however it +// doesn't allow "file/" or similar. +#[cfg(windows)] +#[test] +fn file_with_trailing_slashdot() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create(&start, "file")); + check!(h::open(&start, "file")); + check!(h::open(&start, "file\\.")); + check!(h::open(&start, "file/.")); + check!(h::open(&start, "file\\.\\.")); + check!(h::open(&start, "file/./.")); + assert!(h::open(&start, "file\\").is_err()); + assert!(h::open(&start, "file/").is_err()); + assert!(h::open(&start, "file\\.\\").is_err()); + assert!(h::open(&start, "file/./").is_err()); + check!(p::open_dir(&start, Path::new("file\\.."))); + check!(p::open_dir(&start, Path::new("file/.."))); + check!(p::open_dir(&start, Path::new("file\\.\\.."))); + check!(p::open_dir(&start, Path::new("file/./.."))); + check!(p::open_dir(&start, Path::new("file\\..\\."))); + check!(p::open_dir(&start, Path::new("file/../."))); + check!(p::open_dir(&start, Path::new("file\\..\\"))); + check!(p::open_dir(&start, Path::new("file/../"))); + assert!(p::open_dir(&start, Path::new("file\\...")).is_err()); + assert!(p::open_dir(&start, Path::new("file/...")).is_err()); +} + +/// This is just to confirm that Windows really does allow one to open "file/." +/// and "file/..", and similar, however it doesn't allow "file/" or similar. +#[cfg(windows)] +#[test] +fn file_with_trailing_slashdot_ambient() { + let dir = tempfile::tempdir().unwrap(); + check!(std::fs::File::create(dir.path().join("file"))); + check!(std::fs::File::open(dir.path().join("file"))); + check!(std::fs::File::open(dir.path().join("file\\."))); + check!(std::fs::File::open(dir.path().join("file/."))); + check!(std::fs::File::open(dir.path().join("file\\.\\."))); + check!(std::fs::File::open(dir.path().join("file/./."))); + assert!(std::fs::File::open(dir.path().join("file\\")).is_err()); + assert!(std::fs::File::open(dir.path().join("file/")).is_err()); + assert!(std::fs::File::open(dir.path().join("file\\.\\")).is_err()); + assert!(std::fs::File::open(dir.path().join("file/./")).is_err()); + check!(h::open_ambient_dir(dir.path().join("file/.."))); + check!(h::open_ambient_dir(dir.path().join("file\\.\\.."))); + check!(h::open_ambient_dir(dir.path().join("file/./.."))); + check!(h::open_ambient_dir(dir.path().join("file\\..\\."))); + check!(h::open_ambient_dir(dir.path().join("file/../."))); + check!(h::open_ambient_dir(dir.path().join("file\\..\\"))); + check!(h::open_ambient_dir(dir.path().join("file/../"))); + assert!(h::open_ambient_dir(dir.path().join("file\\...")).is_err()); + assert!(h::open_ambient_dir(dir.path().join("file/...")).is_err()); +} + +#[cfg(all( + unix, + not(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + )) +))] + +/// This test is the same as `dir_searchable_unreadable` but uses `std::fs`' +/// ambient API instead of `cap_std`. The purpose of this test is to +/// confirm fundamentally OS-specific differences. +#[cfg(all( + unix, + not(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + )) +))] +#[test] +fn dir_searchable_unreadable_ambient() { + use std::fs; + use std::os::unix::fs::DirBuilderExt; + + let dir = tempfile::tempdir().unwrap(); + + let mut options = std::fs::DirBuilder::new(); + options.mode(0o333); + check!(options.create(dir.path().join("dir"))); + check!(options.create(dir.path().join("dir/writeable_subdir"))); + options.mode(0o111); + check!(options.create(dir.path().join("dir/subdir"))); + + assert!(check!(fs::metadata(dir.path().join("dir/."))).is_dir()); + assert!(check!(fs::metadata(dir.path().join("dir/subdir"))).is_dir()); + assert!(check!(fs::metadata(dir.path().join("dir/subdir/."))).is_dir()); +} + +/// On Darwin, we don't have a race-free way to create a subdirectory within +/// a directory that we don't have read access to. +#[cfg(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", +))] + +/// Like `dir_unsearchable_unreadable`, but uses ambient-authority APIs +/// to test underlying host functionality. +#[cfg(unix)] +#[test] +fn dir_unsearchable_unreadable_ambient() { + use std::fs::DirBuilder; + use std::os::unix::fs::DirBuilderExt; + + let dir = tempfile::tempdir().unwrap(); + + let mut options = DirBuilder::new(); + options.mode(0o000); + check!(options.create(dir.path().join("dir"))); + + if cfg!(any( + target_os = "android", + target_os = "linux", + target_os = "redox", + )) { + assert!(std::fs::File::open(dir.path().join("dir")).is_err()); + assert!(std::fs::read_dir(dir.path().join("dir")).is_err()); + assert!(std::fs::File::open(dir.path().join("dir/.")).is_err()); + } +} + +/// This test is the same as `symlink_hard_link` but uses `std::fs`' +/// ambient API instead of `cap_std`. The purpose of this test is to +/// confirm fundamentally OS-specific behaviors. +#[test] +fn symlink_hard_link_ambient() { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(windows)] + use std::os::windows::fs::symlink_file; + + if !symlink_supported() { + return; + } + + let dir = tempfile::tempdir().unwrap(); + + check!(std::fs::File::create(dir.path().join("file"))); + #[cfg(not(windows))] + check!(symlink("file", dir.path().join("symlink"))); + #[cfg(windows)] + check!(symlink_file("file", dir.path().join("symlink"))); + check!(std::fs::hard_link( + dir.path().join("symlink"), + dir.path().join("hard_link") + )); + assert!( + check!(std::fs::symlink_metadata(dir.path().join("hard_link"))) + .file_type() + .is_symlink() + ); + let _ = check!(std::fs::File::open(dir.path().join("file"))); + assert!(std::fs::File::open(dir.path().join("file.renamed")).is_err()); + let _ = check!(std::fs::File::open(dir.path().join("symlink"))); + let _ = check!(std::fs::File::open(dir.path().join("hard_link"))); + check!(std::fs::rename( + dir.path().join("file"), + dir.path().join("file.renamed") + )); + assert!(std::fs::File::open(dir.path().join("file")).is_err()); + let _ = check!(std::fs::File::open(dir.path().join("file.renamed"))); + assert!(std::fs::File::open(dir.path().join("symlink")).is_err()); + assert!(std::fs::File::open(dir.path().join("hard_link")).is_err()); + assert!(std::fs::read_link(dir.path().join("file")).is_err()); + assert!(std::fs::read_link(dir.path().join("file.renamed")).is_err()); + assert_eq!( + check!(std::fs::read_link(dir.path().join("symlink"))), + Path::new("file") + ); + assert_eq!( + check!(std::fs::read_link(dir.path().join("hard_link"))), + Path::new("file") + ); + check!(std::fs::remove_file(dir.path().join("file.renamed"))); + assert!(std::fs::File::open(dir.path().join("file")).is_err()); + assert!(std::fs::File::open(dir.path().join("file.renamed")).is_err()); + assert!(std::fs::File::open(dir.path().join("symlink")).is_err()); + assert!(std::fs::File::open(dir.path().join("hard_link")).is_err()); + assert!( + check!(std::fs::symlink_metadata(dir.path().join("hard_link"))) + .file_type() + .is_symlink() + ); +} + +/// POSIX says that whether or not `link` follows symlinks in the `old` +/// path is implementation-defined. We want `hard_link` to not follow +/// symbolic links. +#[test] +fn symlink_hard_link() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + + check!(h::create(&start, "file")); + check!(h::symlink_file(&start, "file", "symlink")); + check!(p::hard_link( + &start, + Path::new("symlink"), + &start, + Path::new("hard_link") + )); + assert!( + check!(h::symlink_metadata(&start, "hard_link")) + .file_type() + .is_symlink() + ); + let _ = check!(h::open(&start, "file")); + assert!(h::open(&start, "file.renamed").is_err()); + let _ = check!(h::open(&start, "symlink")); + let _ = check!(h::open(&start, "hard_link")); + check!(p::rename( + &start, + Path::new("file"), + &start, + Path::new("file.renamed") + )); + assert!(h::open(&start, "file").is_err()); + let _ = check!(h::open(&start, "file.renamed")); + assert!(h::open(&start, "symlink").is_err()); + assert!(h::open(&start, "hard_link").is_err()); + assert!(p::read_link(&start, Path::new("file")).is_err()); + assert!(p::read_link(&start, Path::new("file.renamed")).is_err()); + assert_eq!( + check!(p::read_link(&start, Path::new("symlink"))), + Path::new("file") + ); + assert_eq!( + check!(p::read_link(&start, Path::new("hard_link"))), + Path::new("file") + ); + check!(p::remove_file(&start, Path::new("file.renamed"))); + assert!(h::open(&start, "file").is_err()); + assert!(h::open(&start, "file.renamed").is_err()); + assert!(h::open(&start, "symlink").is_err()); + assert!(h::open(&start, "hard_link").is_err()); + assert!( + check!(h::symlink_metadata(&start, "hard_link")) + .file_type() + .is_symlink() + ); +} + +#[test] +fn readdir_with_trailing_slashdot() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + check!(h::create(&start, "dir/red")); + check!(h::create(&start, "dir/green")); + check!(h::create(&start, "dir/blue")); + + assert_eq!(check!(h::read_dir(&start, "dir")).count(), 3); + assert_eq!(check!(h::read_dir(&start, "dir/")).count(), 3); + assert_eq!(check!(h::read_dir(&start, "dir/.")).count(), 3); +} + +#[test] +fn metadata_vs_std_fs() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "dir")); + let dir = check!(p::open_dir(&start, Path::new("dir"))); + let file = check!(h::create(&dir, "file")); + + let cap_std_dir = check!(p::Metadata::from_file(&dir)); + let cap_std_file = check!(p::Metadata::from_file(&file)); + let cap_std_dir_entry = { + let mut entries = check!(p::read_base_dir(&dir)); + let entry = check!(entries.next().unwrap()); + assert_eq!(entry.file_name(), "file"); + assert!(entries.next().is_none(), "unexpected dir entry"); + check!(entry.metadata()) + }; + + let std_dir = check!(dir.metadata()); + let std_file = check!(file.metadata()); + + match std_dir.created() { + Ok(_) => println!("std::fs supports file created times"), + Err(e) => println!("std::fs doesn't support file created times: {e}"), + } + + check_metadata(&std_dir, &cap_std_dir); + check_metadata(&std_file, &cap_std_file); + check_metadata(&std_file, &cap_std_dir_entry); +} + +fn check_metadata(std: &std::fs::Metadata, cap: &p::Metadata) { + assert_eq!(std.is_dir(), cap.is_dir()); + assert_eq!(std.is_file(), cap.file_type().is_file()); + assert_eq!(std.is_symlink(), cap.file_type().is_symlink()); + assert_eq!(std.file_type().is_dir(), cap.file_type().is_dir()); + assert_eq!(std.file_type().is_file(), cap.file_type().is_file()); + assert_eq!(std.file_type().is_symlink(), cap.file_type().is_symlink()); + #[cfg(unix)] + { + assert_eq!( + std::os::unix::fs::FileTypeExt::is_block_device(&std.file_type()), + p::FileTypeExt::is_block_device(&cap.file_type()) + ); + assert_eq!( + std::os::unix::fs::FileTypeExt::is_char_device(&std.file_type()), + p::FileTypeExt::is_char_device(&cap.file_type()) + ); + } + + assert_eq!(std.len(), cap.len()); + + // If the standard library supports file modified/accessed/created times, + // then the primitives should too. + match std.modified() { + Ok(expected) => assert_eq!(expected, check!(cap.modified())), + Err(e) => assert!( + cap.modified().is_err(), + "modified time should be error ({}), got {:#?}", + e, + cap.modified() + ), + } + // The access times might be a little different due to either our own + // or concurrent accesses. + const ACCESS_TOLERANCE_SEC: u32 = 60; + match std.accessed() { + Ok(expected) => { + let access_tolerance = std::time::Duration::from_secs(ACCESS_TOLERANCE_SEC.into()); + assert!( + ((expected - access_tolerance)..(expected + access_tolerance)) + .contains(&check!(cap.accessed())), + "std accessed {:#?}, cap accessed {:#?}", + expected, + cap.accessed() + ); + } + Err(e) => assert!( + cap.accessed().is_err(), + "accessed time should be error ({}), got {:#?}", + e, + cap.accessed() + ), + } + match std.created() { + Ok(expected) => assert_eq!(expected, check!(cap.created())), + Err(e) => { + // An earlier bug returned the Unix epoch instead of `None` when + // created times were unavailable. This tries to catch such errors, + // while also allowing some targets to return valid created times + // even when std doesn't. + if let Ok(actual) = cap.created() { + println!("std returned error for created time ({e}) but got {actual:#?}"); + assert_ne!(actual, std::time::SystemTime::UNIX_EPOCH); + } + } + } + + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + assert_eq!(std.dev(), p::MetadataExt::dev(cap)); + assert_eq!(std.ino(), p::MetadataExt::ino(cap)); + assert_eq!(std.nlink(), p::MetadataExt::nlink(cap)); + } +} + +/// Test that a symlink in the middle of a path containing ".." doesn't cause +/// the path to be treated as if it ends with "..". +#[test] +fn dotdot_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir(&start, "b")); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "..", "up")); + + let path = "b/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Like `dotdot_in_middle_of_symlink` but with a `/.` at the end. +/// +/// Windows doesn't appear to like symlinks that end with `/.`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir(&start, "b")); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "../.", "up")); + + let path = "b/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Same as `dotdot_in_middle_of_symlink`, but use two levels of `..`. +/// +/// Windows doesn't appear to like symlinks that end with `/..`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_more_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir_all(&start, "b/c")); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "c/../..", "up")); + + let path = "b/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Like `dotdot_more_in_middle_of_symlink`, but with a `/.` at the end. +/// +/// Windows doesn't appear to like symlinks that end with `/.`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_more_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir_all(&start, "b/c")); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "c/../../.", "up")); + + let path = "b/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Same as `dotdot_more_in_middle_of_symlink`, but the symlink doesn't +/// include `c`. +/// +/// Windows doesn't appear to like symlinks that end with `/..`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_other_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir_all(&start, "b/c")); + let c = check!(p::open_dir(&start, Path::new("b/c"))); + check!(h::symlink_dir(&c, "../..", "up")); + + let path = "b/c/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Like `dotdot_other_in_middle_of_symlink`, but with `/.` at the end. +/// +/// Windows doesn't appear to like symlinks that end with `/.`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_other_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::write(&start, "target", foo)); + check!(h::create_dir_all(&start, "b/c")); + let c = check!(p::open_dir(&start, Path::new("b/c"))); + check!(h::symlink_dir(&c, "../../.", "up")); + + let path = "b/c/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Same as `dotdot_more_in_middle_of_symlink`, but use a symlink that +/// doesn't end with `..`. +#[test] +fn dotdot_even_more_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::create_dir_all(&start, "b/c")); + check!(h::write(&start, "b/target", foo)); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "c/../../b", "up")); + + let path = "b/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Like `dotdot_even_more_in_middle_of_symlink`, but with a `/.` at the end. +/// +/// Windows doesn't appear to like symlinks that end with `/.`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_even_more_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::create_dir_all(&start, "b/c")); + check!(h::write(&start, "b/target", foo)); + let b = check!(p::open_dir(&start, Path::new("b"))); + check!(h::symlink_dir(&b, "c/../../b/.", "up")); + + let path = "b/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Same as `dotdot_even_more_in_middle_of_symlink`, but the symlink doesn't +/// include `c`. +#[test] +fn dotdot_even_other_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::create_dir_all(&start, "b/c")); + check!(h::write(&start, "b/target", foo)); + let c = check!(p::open_dir(&start, Path::new("b/c"))); + check!(h::symlink_dir(&c, "../../b", "up")); + + let path = "b/c/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Like `dotdot_even_other_in_middle_of_symlink`, but with a `/.` at the end. +/// +/// Windows doesn't appear to like symlinks that end with `/.`. +#[test] +#[cfg_attr(windows, ignore)] +fn dotdot_slashdot_even_other_in_middle_of_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + let foo = b"foo"; + check!(h::create_dir_all(&start, "b/c")); + check!(h::write(&start, "b/target", foo)); + let c = check!(p::open_dir(&start, Path::new("b/c"))); + check!(h::symlink_dir(&c, "../../b/.", "up")); + + let path = "b/c/up/target"; + let mut file = check!(h::open(&start, path)); + let mut data = Vec::new(); + check!(file.read_to_end(&mut data)); + assert_eq!(data, foo); +} + +/// Ensure that a path of "/" is rejected. +#[test] +fn statat_slash() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + // FreeBSD 14+ uses `O_RESOLVE_BENEATH` which issues different errors. + #[cfg(target_os = "freebsd")] + { + error_contains!(h::metadata(&start, "/"), "Capabilities insufficient"); + error_contains!(h::metadata(&start, "/foo"), "Capabilities insufficient"); + error_contains!( + h::symlink_metadata(&start, "/"), + "Capabilities insufficient" + ); + error_contains!( + h::symlink_metadata(&start, "/foo"), + "Capabilities insufficient" + ); + } + + #[cfg(not(target_os = "freebsd"))] + { + error_contains!( + h::metadata(&start, "/"), + "a path led outside of the filesystem" + ); + error_contains!( + h::metadata(&start, "/foo"), + "a path led outside of the filesystem" + ); + error_contains!( + h::symlink_metadata(&start, "/"), + "a path led outside of the filesyste" + ); + error_contains!( + h::symlink_metadata(&start, "/foo"), + "a path led outside of the filesyste" + ); + } +} + +/// Test interactions between symlinks and trailing slashes. +#[test] +fn trailing_slash_symlink() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + check!(h::create_dir(&start, "sandbox")); + check!(h::symlink_dir(&start, "../outside", "sandbox/hidden")); + check!(h::symlink_dir(&start, "hidden/", "sandbox/indirect")); + + let sandbox = check!(p::open_dir(&start, Path::new("sandbox"))); + + for path in ["hidden", "hidden/", "indirect", "indirect/"] { + error_contains!( + p::open_dir(&sandbox, Path::new(path)), + "a path led outside of the filesystem" + ); + error_contains!( + h::read_dir(&sandbox, path), + "a path led outside of the filesystem" + ); + } +} + +/// Similar to `trailing_slash_symlink`, but populates the test directory +/// outside the sandbox, so it can cover more cases. +#[test] +fn trailing_slash_symlink_more() { + let tmpdir = tempfile::tempdir().unwrap(); + + check!(std::fs::create_dir(tmpdir.path().join("sandbox"))); + #[cfg(unix)] + { + check!(std::os::unix::fs::symlink( + "../outside", + tmpdir.path().join("sandbox/hidden") + )); + check!(std::os::unix::fs::symlink( + "hidden/", + tmpdir.path().join("sandbox/indirect") + )); + check!(std::os::unix::fs::symlink( + "/.", + tmpdir.path().join("sandbox/root_link") + )); + } + #[cfg(windows)] + { + check!(std::os::windows::fs::symlink_dir( + "../outside", + tmpdir.path().join("sandbox/hidden") + )); + check!(std::os::windows::fs::symlink_dir( + "hidden/", + tmpdir.path().join("sandbox/indirect") + )); + check!(std::os::windows::fs::symlink_dir( + "/.", + tmpdir.path().join("sandbox/root_link") + )); + } + #[cfg(not(any(unix, windows)))] + { + compile_error!("not implemented yet"); + } + + let start = check!(h::open_ambient_dir(tmpdir.path())); + + let sandbox = check!(p::open_dir(&start, Path::new("sandbox"))); + + for path in [ + "hidden", + "hidden/", + "indirect", + "indirect/", + "root_link", + "root_link/", + ] { + error_contains!( + p::open_dir(&sandbox, Path::new(path)), + "a path led outside of the filesystem" + ); + error_contains!( + h::read_dir(&sandbox, path), + "a path led outside of the filesystem" + ); + } +} diff --git a/crates/wasi/src/filesystem/primitives/tests/helpers/mod.rs b/crates/wasi/src/filesystem/primitives/tests/helpers/mod.rs new file mode 100644 index 000000000000..22a36c6a0060 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/helpers/mod.rs @@ -0,0 +1,152 @@ +//! Shared helpers for the tests imported from `cap-std`. +//! +//! These tests were written against `cap_std::fs::Dir`, whose methods bundle up +//! several `crate::filesystem::primitives` calls. Anything that is a bare +//! forward to a primitive is called as `p::foo(..)` directly at the call site; +//! only the operations that add options, flags, or logic live here. + +use crate::filesystem::primitives as p; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use tempfile::TempDir; + +/// Open a temporary directory as a start-directory handle. +pub fn dir_of(t: &TempDir) -> File { + p::open_ambient_dir(t.path()).unwrap() +} + +/// `open_ambient_dir` with the ambient authority supplied. +pub fn open_ambient_dir(path: impl AsRef) -> io::Result { + p::open_ambient_dir(path.as_ref()) +} + +/// `Dir::create`: open for writing, creating and truncating. +pub fn create(d: &File, path: impl AsRef) -> io::Result { + p::open( + d, + path.as_ref(), + p::OpenOptions::new() + .write(true) + .create(true) + .truncate(true), + ) +} + +/// `Dir::open`: open for reading. +pub fn open(d: &File, path: impl AsRef) -> io::Result { + p::open(d, path.as_ref(), p::OpenOptions::new().read(true)) +} + +/// `Dir::create_dir`, with the default `DirOptions`. +pub fn create_dir(d: &File, path: impl AsRef) -> io::Result<()> { + p::create_dir(d, path.as_ref(), &p::DirOptions::new()) +} + +/// `Dir::create_dir_all`, in terms of the single-level `create_dir`. +pub fn create_dir_all(d: &File, path: impl AsRef) -> io::Result<()> { + let mut acc = PathBuf::new(); + for component in path.as_ref().components() { + acc.push(component); + match p::create_dir(d, &acc, &p::DirOptions::new()) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} + Err(e) => return Err(e), + } + } + Ok(()) +} + +/// `Dir::metadata`: stat, following symlinks. +pub fn metadata(d: &File, path: impl AsRef) -> io::Result { + p::stat(d, path.as_ref(), p::FollowSymlinks::Yes) +} + +/// `Dir::symlink_metadata`: stat, without following symlinks. +pub fn symlink_metadata(d: &File, path: impl AsRef) -> io::Result { + p::stat(d, path.as_ref(), p::FollowSymlinks::No) +} + +/// `DirExt::open_dir_nofollow`. +pub fn open_dir_nofollow(d: &File, path: impl AsRef) -> io::Result { + p::open( + d, + path.as_ref(), + p::dir_options().follow(p::FollowSymlinks::No), + ) +} + +/// `Dir::read_dir`: open the subdirectory, then read its entries. +pub fn read_dir(d: &File, path: impl AsRef) -> io::Result { + p::read_base_dir(&p::open_dir(d, path.as_ref())?) +} + +pub fn exists(d: &File, path: impl AsRef) -> bool { + metadata(d, path).is_ok() +} + +pub fn is_dir(d: &File, path: impl AsRef) -> bool { + metadata(d, path).map(|m| m.is_dir()).unwrap_or(false) +} + +pub fn is_file(d: &File, path: impl AsRef) -> bool { + metadata(d, path) + .map(|m| m.file_type().is_file()) + .unwrap_or(false) +} + +pub fn write(d: &File, path: impl AsRef, contents: impl AsRef<[u8]>) -> io::Result<()> { + use std::io::Write; + let mut f = create(d, path)?; + f.write_all(contents.as_ref()) +} + +pub fn read(d: &File, path: impl AsRef) -> io::Result> { + use std::io::Read; + let mut f = open(d, path)?; + let mut v = Vec::new(); + f.read_to_end(&mut v)?; + Ok(v) +} + +pub fn read_to_string(d: &File, path: impl AsRef) -> io::Result { + use std::io::Read; + let mut f = open(d, path)?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + Ok(s) +} + +#[cfg(not(windows))] +pub fn symlink(d: &File, src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + p::symlink(src.as_ref(), d, dst.as_ref()) +} + +#[cfg(windows)] +pub fn symlink(d: &File, src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + if is_dir(d, src.as_ref()) { + p::symlink_dir(src.as_ref(), d, dst.as_ref()) + } else { + p::symlink_file(src.as_ref(), d, dst.as_ref()) + } +} + +#[cfg(not(windows))] +pub fn symlink_file(d: &File, src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + p::symlink(src.as_ref(), d, dst.as_ref()) +} + +#[cfg(windows)] +pub fn symlink_file(d: &File, src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + p::symlink_file(src.as_ref(), d, dst.as_ref()) +} + +#[cfg(not(windows))] +pub fn symlink_dir(d: &File, src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + p::symlink(src.as_ref(), d, dst.as_ref()) +} + +#[cfg(windows)] +pub fn symlink_dir(d: &File, src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + p::symlink_dir(src.as_ref(), d, dst.as_ref()) +} diff --git a/crates/wasi/src/filesystem/primitives/tests/metadata_ext.rs b/crates/wasi/src/filesystem/primitives/tests/metadata_ext.rs new file mode 100644 index 000000000000..6d4fae861f78 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/metadata_ext.rs @@ -0,0 +1,90 @@ +// This file contains tests for `MetadataExt`. +// +// `dev`/`ino`/`nlink` are only on the Unix `MetadataExt`. +#![cfg(unix)] + +use super::helpers as h; +use super::sys_common::io::tmpdir; +use super::sys_common::symlink_supported; +use crate::filesystem::primitives::{ + FollowSymlinks, Metadata, MetadataExt, hard_link, open_ambient_dir, stat, +}; +use std::path::Path; + +#[test] +fn test_metadata_ext() { + let tmpdir = tmpdir(); + let dir = check!(open_ambient_dir(tmpdir.path(),)); + let a = check!(h::create(&dir, "a")); + let b = check!(h::create(&dir, "b")); + let tmpdir_metadata = check!(Metadata::from_file(&dir)); + let a_metadata = check!(Metadata::from_file(&a)); + let b_metadata = check!(Metadata::from_file(&b)); + let a_dir_metadata = check!(stat(&dir, Path::new("a"), FollowSymlinks::Yes)); + let b_dir_metadata = check!(stat(&dir, Path::new("b"), FollowSymlinks::Yes)); + let a_symlink_metadata = check!(stat(&dir, Path::new("a"), FollowSymlinks::No)); + let b_symlink_metadata = check!(stat(&dir, Path::new("b"), FollowSymlinks::No)); + + // The directory and files inside it should be on the same device. + assert_eq!(tmpdir_metadata.dev(), a_metadata.dev()); + assert_eq!(a_metadata.dev(), b_metadata.dev()); + + // They should all have distinct inodes. + assert_ne!(tmpdir_metadata.ino(), a_metadata.ino()); + assert_ne!(tmpdir_metadata.ino(), b_metadata.ino()); + assert_ne!(a_metadata.ino(), b_metadata.ino()); + + // The files should start with just one link. + assert_eq!(a_metadata.nlink(), 1); + assert_eq!(b_metadata.nlink(), 1); + + // Add another link and check for it. + check!(hard_link(&dir, Path::new("b"), &dir, Path::new("c"))); + let b_metadata = check!(Metadata::from_file(&b)); + assert_eq!(b_metadata.nlink(), 2); + + // Check that the metadata has dev/nlink/ino. + tmpdir_metadata.dev(); + tmpdir_metadata.nlink(); + tmpdir_metadata.ino(); + a_metadata.dev(); + a_metadata.nlink(); + a_metadata.ino(); + b_metadata.dev(); + b_metadata.nlink(); + b_metadata.ino(); + a_dir_metadata.dev(); + a_dir_metadata.nlink(); + a_dir_metadata.ino(); + b_dir_metadata.dev(); + b_dir_metadata.nlink(); + b_dir_metadata.ino(); + a_symlink_metadata.dev(); + a_symlink_metadata.nlink(); + a_symlink_metadata.ino(); + b_symlink_metadata.dev(); + b_symlink_metadata.nlink(); + b_symlink_metadata.ino(); + + if symlink_supported() { + check!(h::symlink_file(&dir, "b", "d")); + let d_metadata = check!(stat(&dir, Path::new("d"), FollowSymlinks::Yes)); + let d_symlink_metadata = check!(stat(&dir, Path::new("d"), FollowSymlinks::No)); + + d_metadata.dev(); + d_metadata.nlink(); + d_metadata.ino(); + d_symlink_metadata.dev(); + d_symlink_metadata.nlink(); + d_symlink_metadata.ino(); + + assert_ne!( + (d_symlink_metadata.ino(), d_symlink_metadata.dev()), + (b_metadata.ino(), b_metadata.dev()) + ); + assert_eq!( + (d_metadata.ino(), d_metadata.dev()), + (b_metadata.ino(), b_metadata.dev()) + ); + } +} diff --git a/crates/wasi/src/filesystem/primitives/tests/mod.rs b/crates/wasi/src/filesystem/primitives/tests/mod.rs new file mode 100644 index 000000000000..85545e17dfd2 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/mod.rs @@ -0,0 +1,19 @@ +#[macro_use] +mod sys_common; + +mod helpers; + +mod cap_basics; +mod file_type_ext; +mod fs; +mod fs_additional; +mod metadata_ext; +mod paths_containing_nul; +mod readdir; +mod rename; +mod rename_directory; +mod reopendir; +mod set_times; +mod symlinks; +mod windows_open; +mod windows_symlinks; diff --git a/crates/wasi/src/filesystem/primitives/tests/paths_containing_nul.rs b/crates/wasi/src/filesystem/primitives/tests/paths_containing_nul.rs new file mode 100644 index 000000000000..4c9247ab19f5 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/paths_containing_nul.rs @@ -0,0 +1,77 @@ +// This test module derived from Rust's src/test/ui/paths-containing-nul.rs +// at revision 108e90ca78f052c0c1c49c42a22c85620be19712. + +// run-pass + +#![allow(deprecated)] +// ignore-cloudabi no files or I/O +// ignore-wasm32-bare no files or I/O +// ignore-emscripten no files +// ignore-sgx no files + +use super::sys_common::io::tmpdir; +use crate::filesystem::primitives::{ + DirOptions, FollowSymlinks, OpenOptions, create_dir, hard_link, open, open_ambient_dir, + read_link, remove_dir, remove_file, rename, stat, +}; +use std::io; +use std::path::Path; + +fn assert_invalid_input(on: &str, result: io::Result) { + fn inner(on: &str, result: io::Result<()>) { + match result { + Ok(()) => panic!("{on} didn't return an error on a path with NUL"), + Err(_e) => { + // TODO: Re-enable this assertion once the `io_error_more` + // feature is available. + /* + assert_eq!( + e.kind(), + io::ErrorKind::InvalidInput || io::ErrorKind::InvalidFilename, + "{} returned a strange {:?} on a path with NUL", + on, + e + ); + */ + } + } + } + inner(on, result.map(drop)) +} + +#[test] +fn paths_containing_nul() { + let tmpdir = tmpdir(); + let dir = open_ambient_dir(tmpdir.path()).unwrap(); + let nul = Path::new("\0"); + + assert_invalid_input("open", open(&dir, nul, OpenOptions::new().read(true))); + assert_invalid_input( + "create", + open( + &dir, + nul, + OpenOptions::new().write(true).create(true).truncate(true), + ), + ); + assert_invalid_input("remove_file", remove_file(&dir, nul)); + assert_invalid_input("metadata", stat(&dir, nul, FollowSymlinks::Yes)); + assert_invalid_input("symlink_metadata", stat(&dir, nul, FollowSymlinks::No)); + + // Create a file inside the sandbox. + let dummy_file = Path::new("dummy_file"); + open( + &dir, + dummy_file, + OpenOptions::new().write(true).create(true).truncate(true), + ) + .expect("creating dummy_file"); + + assert_invalid_input("rename1", rename(&dir, nul, &dir, Path::new("a"))); + assert_invalid_input("rename2", rename(&dir, dummy_file, &dir, nul)); + assert_invalid_input("hard_link1", hard_link(&dir, nul, &dir, Path::new("a"))); + assert_invalid_input("hard_link2", hard_link(&dir, dummy_file, &dir, nul)); + assert_invalid_input("read_link", read_link(&dir, nul)); + assert_invalid_input("create_dir", create_dir(&dir, nul, &DirOptions::new())); + assert_invalid_input("remove_dir", remove_dir(&dir, nul)); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/readdir.rs b/crates/wasi/src/filesystem/primitives/tests/readdir.rs new file mode 100644 index 000000000000..8ceea29fae5c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/readdir.rs @@ -0,0 +1,79 @@ +use crate::filesystem::primitives::{DirEntry, open_ambient_dir, read_base_dir}; +use std::collections::HashMap; +use std::fs::File; +use std::path::Path; + +#[test] +fn test_dir_entries() { + let tmpdir = tempfile::tempdir().expect("construct tempdir"); + + let entries = dir_entries(&tmpdir.path()); + assert_eq!(entries.len(), 0, "empty dir"); + + let _f1 = std::fs::File::create(tmpdir.path().join("file1")).expect("create file1"); + + let entries = dir_entries(&tmpdir.path()); + assert!( + entries.get("file1").is_some(), + "directory contains `file1`: {entries:?}" + ); + assert_eq!(entries.len(), 1); + + let _f2 = std::fs::File::create(tmpdir.path().join("file2")).expect("create file1"); + let entries = dir_entries(&tmpdir.path()); + assert!( + entries.get("file1").is_some(), + "directory contains `file1`: {entries:?}" + ); + assert!( + entries.get("file2").is_some(), + "directory contains `file2`: {entries:?}" + ); + assert_eq!(entries.len(), 2); +} + +#[test] +fn test_reread_entries() { + let tmpdir = tempfile::tempdir().expect("construct tempdir"); + let dir = open_ambient_dir(tmpdir.path()).unwrap(); + + let entries = read_entries(&dir); + assert_eq!(entries.len(), 0, "empty dir"); + + let _f1 = std::fs::File::create(tmpdir.path().join("file1")).expect("create file1"); + + let entries = read_entries(&dir); + assert!( + entries.get("file1").is_some(), + "directory contains `file1`: {entries:?}" + ); + assert_eq!(entries.len(), 1); + + let _f2 = std::fs::File::create(tmpdir.path().join("file2")).expect("create file1"); + let entries = read_entries(&dir); + assert!( + entries.get("file1").is_some(), + "directory contains `file1`: {entries:?}" + ); + assert!( + entries.get("file2").is_some(), + "directory contains `file2`: {entries:?}" + ); + assert_eq!(entries.len(), 2); +} + +fn dir_entries(path: &Path) -> HashMap { + let dir = open_ambient_dir(path).unwrap(); + read_entries(&dir) +} + +fn read_entries(dir: &File) -> HashMap { + let mut out = HashMap::new(); + for e in read_base_dir(dir).unwrap() { + let e = e.expect("non-error entry"); + let name = e.file_name().to_str().expect("utf8 filename").to_owned(); + assert!(out.get(&name).is_none(), "name already read: {name}"); + out.insert(name, e); + } + out +} diff --git a/crates/wasi/src/filesystem/primitives/tests/rename.rs b/crates/wasi/src/filesystem/primitives/tests/rename.rs new file mode 100644 index 000000000000..ca371eb6f7b3 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/rename.rs @@ -0,0 +1,238 @@ +use super::helpers as h; +use super::sys_common::io::tmpdir; +use crate::filesystem::primitives as p; + +use std::path::Path; + +/* +#[cfg(not(windows))] +fn rename_path_in_use() -> String { + rustix::io::Errno::BUSY.into().to_string() +} +#[cfg(windows)] +fn rename_path_in_use() -> String { + todo!("work out error for rename_path_in_use condition") +} +*/ + +#[cfg(not(windows))] +fn no_such_file_or_directory() -> String { + rustix::io::Errno::NOENT.to_string() +} +#[cfg(windows)] +fn no_such_file_or_directory() -> String { + std::io::Error::from_raw_os_error(windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND as i32) + .to_string() +} + +/* // TODO: Platform-specific error code. +cfg_if::cfg_if! { + if #[cfg(any( + target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "tvos", + target_os = "watchos", + target_os = "visionos", + target_os = "dragonfly" + ))] { + fn rename_file_over_dir() -> String { + rustix::io::Errno::ISDIR.into().to_string() + } + + fn rename_file_over_dot() -> String { + rename_file_over_dir() + } + + fn rename_dot_over_file() -> String { + rustix::io::Errno::INVAL.into().to_string() + } + } else { + fn rename_file_over_dir() -> String { + rustix::io::Errno::NOTEMPTY.into().to_string() + } + + fn rename_file_over_dot() -> String { + rename_path_in_use() + } + + fn rename_dot_over_file() -> String { + rename_path_in_use() + } + } +} +*/ + +#[test] +#[cfg_attr(windows, ignore)] // TODO: Blocked on error message discrepancies +fn rename_basics() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + + check!(h::create_dir_all(&dir, "foo/bar")); + check!(h::create(&dir, "foo/bar/file.txt")); + + check!(p::rename( + &dir, + Path::new("foo/bar/file.txt"), + &dir, + Path::new("foo/bar/renamed.txt") + )); + assert!(!h::exists(&dir, "foo/bar/file.txt")); + assert!(h::exists(&dir, "foo/bar/renamed.txt")); + + check!(p::rename( + &dir, + Path::new("foo/bar/renamed.txt"), + &dir, + Path::new("foo/bar/renamed.txt") + )); + error_contains!( + p::rename( + &dir, + Path::new("foo/bar/renamed.txt"), + &dir, + Path::new("..") + ), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename( + &dir, + Path::new("foo/bar/renamed.txt"), + &dir, + Path::new("foo/../..") + ), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename( + &dir, + Path::new("foo/bar/renamed.txt"), + &dir, + Path::new("/tmp") + ), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename( + &dir, + Path::new("foo/bar/renamed.txt"), + &dir, + Path::new("foo/bar/baz/..") + ), + &no_such_file_or_directory() + ); + /* // TODO: Platform-specific error code. + error!( + p::rename(&dir, Path::new("foo/bar/renamed.txt"), &dir, Path::new("foo/bar")), + &rename_file_over_dir() + ); + */ + check!(p::rename( + &dir, + Path::new("foo/bar"), + &dir, + Path::new("foo/bar") + )); + check!(p::rename( + &dir, + Path::new("foo/bar/renamed.txt"), + &dir, + Path::new("file.txt") + )); + assert!(!h::exists(&dir, "foo/bar/renamed.txt")); + assert!(h::exists(&dir, "file.txt")); + + /* // TODO: Platform-specific error code. + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("foo/..")), + &rename_path_in_use() + ); + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("foo/.")), + &rename_path_in_use() + ); + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("foo/bar/../..")), + &rename_path_in_use() + ); + */ + error_contains!( + p::rename( + &dir, + Path::new("file.txt"), + &dir, + Path::new("foo/bar/../../..") + ), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename( + &dir, + Path::new("file.txt"), + &dir, + Path::new("foo/bar/../../../something") + ), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("")), + "No such file" + ); + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("/")), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("/.")), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("/..")), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename(&dir, Path::new("/"), &dir, Path::new("nope.txt")), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename(&dir, Path::new("/.."), &dir, Path::new("nope.txt")), + "a path led outside of the filesystem" + ); + error_contains!( + p::rename(&dir, Path::new("file.txt/"), &dir, Path::new("nope.txt")), + "Not a directory" + ); + + /* // TODO: Platform-specific error code. + error!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new(".")), + &rename_file_over_dot() + ); + error!( + p::rename(&dir, Path::new("file.txt"), &dir, Path::new("..")), + &rename_path_in_use() + ); + error!( + p::rename(&dir, Path::new(".."), &dir, Path::new("nope.txt")), + &rename_path_in_use() + ); + error!( + p::rename(&dir, Path::new("."), &dir, Path::new("nope.txt")), + &rename_dot_over_file() + ); + */ + + check!(h::create(&dir, "existing.txt")); + check!(p::rename( + &dir, + Path::new("file.txt"), + &dir, + Path::new("existing.txt") + )); + assert!(!h::exists(&dir, "file.txt")); + assert!(h::exists(&dir, "existing.txt")); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/rename_directory.rs b/crates/wasi/src/filesystem/primitives/tests/rename_directory.rs new file mode 100644 index 000000000000..a9e208994a1a --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/rename_directory.rs @@ -0,0 +1,33 @@ +// This test module derived from Rust's +// src/test/ui-fulldeps/rename-directory.rs at revision +// 108e90ca78f052c0c1c49c42a22c85620be19712. + +// run-pass + +#![allow(unused_must_use)] +// This test can't be a unit test in std, +// because it needs `TempDir`, which is in extra + +// ignore-cross-compile + +use super::helpers as h; +use super::sys_common::io::tmpdir; +use crate::filesystem::primitives::rename; +use std::path::Path; + +#[test] +fn rename_directory() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + let old_path = Path::new("foo/bar/baz"); + h::create_dir_all(&dir, &old_path).unwrap(); + let test_file = &old_path.join("temp.txt"); + + h::create(&dir, test_file).unwrap(); + + let new_path = Path::new("quux/blat"); + h::create_dir_all(&dir, &new_path).unwrap(); + rename(&dir, &old_path, &dir, &new_path.join("newdir")); + assert!(h::is_dir(&dir, &new_path.join("newdir"))); + assert!(h::exists(&dir, &new_path.join("newdir/temp.txt"))); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/reopendir.rs b/crates/wasi/src/filesystem/primitives/tests/reopendir.rs new file mode 100644 index 000000000000..3ce310a03fc5 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/reopendir.rs @@ -0,0 +1,72 @@ +//! Tests for various forms of reopening a directory handle. + +use super::helpers as h; +use super::sys_common::io::tmpdir; +use crate::filesystem::primitives::open_dir; +use std::path::Path; + +#[test] +fn reopendir_a() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + + let inner = check!(open_dir(&dir, Path::new("dir/inner"))); + + check!(open_dir(&inner, Path::new("."))); +} + +#[test] +fn reopendir_b() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + + let inner = check!(open_dir(&dir, Path::new("dir/inner"))); + + check!(open_dir(&inner, Path::new("./"))); +} + +#[test] +fn reopendir_c() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + + let inner = check!(open_dir(&dir, Path::new("dir/inner"))); + + check!(open_dir(&inner, Path::new("./."))); +} + +#[test] +fn reopendir_d() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + + let _inner = check!(open_dir(&dir, Path::new("dir/inner"))); + + check!(open_dir(&dir, Path::new("dir/inner"))); +} + +#[test] +fn reopendir_e() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + + let _inner = check!(open_dir(&dir, Path::new("dir/inner"))); + + check!(open_dir(&dir, Path::new("dir/inner/."))); +} + +#[test] +fn reopendir_f() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create_dir_all(&dir, "dir/inner")); + + let _inner = check!(open_dir(&dir, Path::new("dir/inner"))); + + check!(open_dir(&dir, Path::new("dir/inner/"))); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/set_times.rs b/crates/wasi/src/filesystem/primitives/tests/set_times.rs new file mode 100644 index 000000000000..836b8d95fa41 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/set_times.rs @@ -0,0 +1,156 @@ +use super::helpers as h; +use super::sys_common::io::tmpdir; +use super::sys_common::symlink_supported; +use crate::filesystem::primitives::{Metadata, set_times, set_times_nofollow}; +use std::path::Path; +use std::time::SystemTime; + +fn modified_time(meta: Metadata) -> SystemTime { + meta.modified().unwrap() +} + +#[test] +fn basic_times() { + let test_symlinks = symlink_supported(); + + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create(&dir, "file")); + check!(h::create_dir(&dir, "dir")); + if test_symlinks { + check!(h::symlink_file(&dir, "file", "file_symlink_file")); + check!(h::symlink_dir(&dir, "dir", "dir_symlink_dir")); + } + + let file_time = SystemTime::UNIX_EPOCH; + check!(set_times(&dir, Path::new("file"), None, Some(file_time))); + assert_eq!(modified_time(check!(h::metadata(&dir, "file"))), file_time); + if test_symlinks { + assert_eq!( + modified_time(check!(h::metadata(&dir, "file_symlink_file"))), + file_time + ); + } + + let dir_time = SystemTime::UNIX_EPOCH; + check!(set_times(&dir, Path::new("dir"), None, Some(dir_time))); + assert_eq!(modified_time(check!(h::metadata(&dir, "dir"))), dir_time); + if test_symlinks { + assert_eq!( + modified_time(check!(h::metadata(&dir, "dir_symlink_dir"))), + dir_time + ); + } +} + +#[test] +fn symlink_times() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create(&dir, "file")); + check!(h::create_dir(&dir, "dir")); + check!(h::symlink_file(&dir, "file", "file_symlink_file")); + check!(h::symlink_dir(&dir, "dir", "dir_symlink_dir")); + + let file_time = SystemTime::UNIX_EPOCH; + check!(set_times( + &dir, + Path::new("file_symlink_file"), + None, + Some(file_time) + )); + assert_eq!(modified_time(check!(h::metadata(&dir, "file"))), file_time); + assert_eq!( + modified_time(check!(h::metadata(&dir, "file_symlink_file"))), + file_time + ); + assert_eq!( + modified_time(check!(h::symlink_metadata(&dir, "file"))), + file_time + ); + assert_ne!( + modified_time(check!(h::symlink_metadata(&dir, "file_symlink_file"))), + file_time + ); + + let dir_time = SystemTime::UNIX_EPOCH; + check!(set_times( + &dir, + Path::new("dir_symlink_dir"), + None, + Some(file_time) + )); + assert_eq!(modified_time(check!(h::metadata(&dir, "dir"))), dir_time); + assert_eq!( + modified_time(check!(h::metadata(&dir, "dir_symlink_dir"))), + dir_time + ); + assert_eq!( + modified_time(check!(h::symlink_metadata(&dir, "dir"))), + dir_time + ); + assert_ne!( + modified_time(check!(h::symlink_metadata(&dir, "dir_symlink_dir"))), + dir_time + ); +} + +#[test] +fn symlink_itself_times() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + check!(h::create(&dir, "file")); + check!(h::create_dir(&dir, "dir")); + check!(h::symlink_file(&dir, "file", "file_symlink_file")); + check!(h::symlink_dir(&dir, "dir", "dir_symlink_dir")); + + let file_time = SystemTime::UNIX_EPOCH; + check!(set_times_nofollow( + &dir, + Path::new("file_symlink_file"), + None, + Some(file_time) + )); + assert_ne!(modified_time(check!(h::metadata(&dir, "file"))), file_time); + assert_ne!( + modified_time(check!(h::metadata(&dir, "file_symlink_file"))), + file_time + ); + assert_ne!( + modified_time(check!(h::symlink_metadata(&dir, "file"))), + file_time + ); + assert_eq!( + modified_time(check!(h::symlink_metadata(&dir, "file_symlink_file"))), + file_time + ); + + let dir_time = SystemTime::UNIX_EPOCH; + check!(set_times_nofollow( + &dir, + Path::new("dir_symlink_dir"), + None, + Some(file_time) + )); + assert_ne!(modified_time(check!(h::metadata(&dir, "dir"))), dir_time); + assert_ne!( + modified_time(check!(h::metadata(&dir, "dir_symlink_dir"))), + dir_time + ); + assert_ne!( + modified_time(check!(h::symlink_metadata(&dir, "dir"))), + dir_time + ); + assert_eq!( + modified_time(check!(h::symlink_metadata(&dir, "dir_symlink_dir"))), + dir_time + ); +} diff --git a/crates/wasi/src/filesystem/primitives/tests/symlinks.rs b/crates/wasi/src/filesystem/primitives/tests/symlinks.rs new file mode 100644 index 000000000000..3c2e7d9ec243 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/symlinks.rs @@ -0,0 +1,568 @@ +use super::helpers as h; +use super::sys_common::io::tmpdir; +use super::sys_common::symlink_supported; +use crate::filesystem::primitives as p; + +use std::path::Path; + +#[test] +fn basic_symlinks() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let dir = h::dir_of(&tmpdir); + + check!(h::create(&dir, "file")); + check!(h::create_dir(&dir, "dir")); + assert!(check!(h::metadata(&dir, "file")).file_type().is_file()); + assert!(!check!(h::metadata(&dir, "file")).is_dir()); + assert!(check!(h::metadata(&dir, "dir")).is_dir()); + assert!(!check!(h::metadata(&dir, "dir")).file_type().is_file()); + assert!(!check!(h::metadata(&dir, "file")).file_type().is_symlink()); + assert!(!check!(h::metadata(&dir, "dir")).file_type().is_symlink()); + + check!(h::symlink_file(&dir, "file", "file_symlink_file")); + check!(h::symlink_dir(&dir, "dir", "dir_symlink_dir")); + check!(h::symlink(&dir, "file", "file_symlink")); + check!(h::symlink(&dir, "dir", "dir_symlink")); + + assert!( + check!(h::metadata(&dir, "file_symlink_file")) + .file_type() + .is_file() + ); + assert!(check!(h::metadata(&dir, "dir_symlink_dir")).is_dir()); + assert!( + check!(h::metadata(&dir, "file_symlink")) + .file_type() + .is_file() + ); + assert!(check!(h::metadata(&dir, "dir_symlink")).is_dir()); + + assert!( + !check!(h::metadata(&dir, "file_symlink_file")) + .file_type() + .is_symlink() + ); + assert!( + !check!(h::metadata(&dir, "dir_symlink_dir")) + .file_type() + .is_symlink() + ); + assert!( + !check!(h::metadata(&dir, "file_symlink")) + .file_type() + .is_symlink() + ); + assert!( + !check!(h::metadata(&dir, "dir_symlink")) + .file_type() + .is_symlink() + ); + + assert!( + check!(h::symlink_metadata(&dir, "file_symlink_file")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&dir, "dir_symlink_dir")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&dir, "file_symlink")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&dir, "dir_symlink")) + .file_type() + .is_symlink() + ); + + assert!( + !check!(h::metadata(&dir, "file_symlink_file")) + .file_type() + .is_symlink() + ); + assert!( + !check!(h::metadata(&dir, "dir_symlink_dir")) + .file_type() + .is_symlink() + ); + assert!( + !check!(h::metadata(&dir, "file_symlink")) + .file_type() + .is_symlink() + ); + assert!( + !check!(h::metadata(&dir, "dir_symlink")) + .file_type() + .is_symlink() + ); + + assert!( + check!(h::symlink_metadata(&dir, "file_symlink_file")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&dir, "dir_symlink_dir")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&dir, "file_symlink")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&dir, "dir_symlink")) + .file_type() + .is_symlink() + ); +} + +#[test] +fn symlink_absolute() { + let tmpdir = tmpdir(); + let dir = h::dir_of(&tmpdir); + + error_contains!( + h::symlink(&dir, "/thing", "thing_symlink_file"), + "a path led outside of the filesystem" + ); + error_contains!( + h::symlink_file(&dir, "/file", "file_symlink_file"), + "a path led outside of the filesystem" + ); + error_contains!( + h::symlink_dir(&dir, "/dir", "dir_symlink_dir"), + "a path led outside of the filesystem" + ); +} + +#[test] +fn readlink_absolute() { + if !symlink_supported() { + return; + } + + let dir = tempfile::tempdir().unwrap(); + + #[cfg(not(windows))] + check!(std::os::unix::fs::symlink( + "/thing", + dir.path().join("thing_symlink") + )); + #[cfg(windows)] + check!(std::os::windows::fs::symlink_file( + "/file", + dir.path().join("file_symlink_file") + )); + #[cfg(windows)] + check!(std::os::windows::fs::symlink_dir( + "/dir", + dir.path().join("dir_symlink_dir") + )); + + let dir = check!(h::open_ambient_dir(dir.path())); + + #[cfg(not(windows))] + error_contains!( + p::read_link(&dir, Path::new("thing_symlink")), + "a path led outside of the filesystem" + ); + #[cfg(windows)] + error_contains!( + p::read_link(&dir, Path::new("file_symlink_file")), + "a path led outside of the filesystem" + ); + #[cfg(windows)] + error_contains!( + p::read_link(&dir, Path::new("dir_symlink_dir")), + "a path led outside of the filesystem" + ); +} + +/// Opening directories without following symlinks. +#[test] +fn open_dir_nofollow() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let dir = h::dir_of(&tmpdir); + + check!(h::create(&dir, "file")); + check!(h::create_dir(&dir, "dir")); + check!(h::symlink_file(&dir, "file", "symlink_file")); + check!(h::symlink_dir(&dir, "dir", "symlink_dir")); + check!(h::symlink_dir(&dir, "dir/", "symlink_dir_slash")); + check!(h::symlink_dir(&dir, "dir/.", "symlink_dir_slashdot")); + check!(h::symlink_dir(&dir, "dir/..", "symlink_dir_slashdotdot")); + check!(h::symlink_dir( + &dir, + "dir/../", + "symlink_dir_slashdotdotslash" + )); + check!(h::symlink_dir(&dir, ".", "symlink_dot")); + check!(h::symlink_dir(&dir, "./", "symlink_dotslash")); + + // First try without `nofollow`. The "symlink_dir" case should succeed. + assert!(p::open_dir(&dir, Path::new("file")).is_err()); + assert!(p::open_dir(&dir, Path::new("symlink_file")).is_err()); + check!(p::open_dir(&dir, Path::new("symlink_dir"))); + #[cfg(windows)] + check!(p::open_dir(&dir, Path::new("symlink_dir\\"))); + check!(p::open_dir(&dir, Path::new("symlink_dir/"))); + #[cfg(windows)] + { + error!(p::open_dir(&dir, Path::new("symlink_dir_slash")), 123); + error!(p::open_dir(&dir, Path::new("symlink_dir_slashdotdot")), 123); + error!( + p::open_dir(&dir, Path::new("symlink_dir_slashdotdotslash")), + 123 + ); + error!(p::open_dir(&dir, Path::new("symlink_dotslash")), 123); + error!(p::open_dir(&dir, Path::new("symlink_dir_slashdot")), 123); + } + #[cfg(not(windows))] + { + check!(p::open_dir(&dir, Path::new("symlink_dir_slash"))); + check!(p::open_dir(&dir, Path::new("symlink_dir_slashdotdot"))); + check!(p::open_dir(&dir, Path::new("symlink_dir_slashdotdotslash"))); + check!(p::open_dir(&dir, Path::new("symlink_dotslash"))); + check!(p::open_dir(&dir, Path::new("symlink_dir_slashdot"))); + } + check!(p::open_dir(&dir, Path::new("symlink_dot"))); + check!(p::open_dir(&dir, Path::new("dir"))); + + // Next try with `nofollow`. The "symlink_dir" case should fail. + assert!(h::open_dir_nofollow(&dir, "file").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_file").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dir").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dir_slash").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dir_slashdot").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dir_slashdotdot").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dir_slashdotdotslash").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dot").is_err()); + assert!(h::open_dir_nofollow(&dir, "symlink_dotslash").is_err()); + check!(h::open_dir_nofollow(&dir, "dir")); + + // Check various ways of spelling `dir/../symlink_dir`. + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../symlink_dir"); + check!(p::open_dir(&dir, Path::new(&name))); + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + + // Check various paths which end with a symlink (even though the symlink + // expansion may end with `/` or a non-symlink). + for suffix in &[""] { + for symlink_dir in &["symlink_dot"] { + let name = format!("{symlink_dir}{suffix}"); + check!(p::open_dir(&dir, Path::new(&name))); + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + check!(p::open_dir(&dir, Path::new(&name))); + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + } + } + + // Check more paths which end with a symlink. On Windows, these fail due to + // the symlink-to-path-ending-in-trailing-slash error. + for suffix in &[""] { + for symlink_dir in &[ + "symlink_dir_slashdotdot", + "symlink_dir_slashdot", + "symlink_dir_slash", + "symlink_dir_slashdotdotslash", + "symlink_dotslash", + ] { + let name = format!("{symlink_dir}{suffix}"); + #[cfg(windows)] + { + error!(p::open_dir(&dir, Path::new(&name)), 123); + } + #[cfg(not(windows))] + { + check!(p::open_dir(&dir, Path::new(&name))); + } + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + #[cfg(windows)] + { + error!(p::open_dir(&dir, Path::new(&name)), 123); + } + #[cfg(not(windows))] + { + check!(p::open_dir(&dir, Path::new(&name))); + } + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + } + } + + // Check those same paths, but with various suffixes appended, so that + // `open_dir_nofollow` can open them. + for suffix in &["/", "/.", "/./"] { + for symlink_dir in &["symlink_dir", "symlink_dot"] { + let name = format!("{symlink_dir}{suffix}"); + check!(p::open_dir(&dir, Path::new(&name))); + // On Windows, a trailing dot is stripped early. + if cfg!(not(windows)) || suffix != &"/." { + check!(h::open_dir_nofollow(&dir, &name)); + } else { + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + check!(p::open_dir(&dir, Path::new(&name))); + // On Windows, a trailing dot is stripped early. + if cfg!(not(windows)) || suffix != &"/." { + check!(h::open_dir_nofollow(&dir, &name)); + } else { + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + } + } + } + + // Check those same paths, but with various suffixes appended. On + // Windows, these fail due to the symlink-to-path-ending-in-trailing-slash + // error. + for suffix in &["/", "/.", "/./"] { + for symlink_dir in &[ + "symlink_dir_slash", + "symlink_dir_slashdot", + "symlink_dir_slashdotdot", + "symlink_dir_slashdotdotslash", + "symlink_dotslash", + ] { + let name = format!("{symlink_dir}{suffix}"); + #[cfg(windows)] + { + error!(p::open_dir(&dir, Path::new(&name)), 123); + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + #[cfg(not(windows))] + { + check!(p::open_dir(&dir, Path::new(&name))); + check!(h::open_dir_nofollow(&dir, &name)); + } + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + #[cfg(windows)] + { + error!(p::open_dir(&dir, Path::new(&name)), 123); + assert!(h::open_dir_nofollow(&dir, &name).is_err()); + } + #[cfg(not(windows))] + { + check!(p::open_dir(&dir, Path::new(&name))); + check!(h::open_dir_nofollow(&dir, &name)); + } + } + } + } + + // Check various ways of spelling `.`. + for cur_dir in &["dir/..", "dir/../", ".", "./"] { + check!(p::open_dir(&dir, Path::new(cur_dir))); + check!(h::open_dir_nofollow(&dir, cur_dir)); + } +} + +/// This test is the same as `open_dir_nofollow` but uses ambient APIs instead +/// of `cap_std`. The purpose of this test is to confirm fundamentally +/// OS-specific behaviors. +#[test] +fn open_dir_nofollow_ambient() { + #[cfg(unix)] + use std::os::unix::fs::{symlink as symlink_file, symlink as symlink_dir}; + #[cfg(windows)] + use std::os::windows::fs::{symlink_dir, symlink_file}; + + if !symlink_supported() { + return; + } + + let dir = tempfile::tempdir().unwrap(); + + check!(std::fs::File::create(dir.path().join("file"))); + check!(std::fs::create_dir(dir.path().join("dir"))); + check!(symlink_file("file", dir.path().join("symlink_file"))); + check!(symlink_dir("dir", dir.path().join("symlink_dir"))); + check!(symlink_dir("dir/", dir.path().join("symlink_dir_slash"))); + check!(symlink_dir( + "dir/.", + dir.path().join("symlink_dir_slashdot") + )); + check!(symlink_dir( + "dir/..", + dir.path().join("symlink_dir_slashdotdot") + )); + check!(symlink_dir( + "dir/../", + dir.path().join("symlink_dir_slashdotdotslash") + )); + check!(symlink_dir("./", dir.path().join("symlink_dotslash"))); + check!(symlink_dir(".", dir.path().join("symlink_dot"))); + + assert!(h::open_ambient_dir(dir.path().join("file")).is_err()); + assert!(h::open_ambient_dir(dir.path().join("symlink_file")).is_err()); + check!(h::open_ambient_dir(dir.path().join("symlink_dir"))); + #[cfg(windows)] + check!(h::open_ambient_dir(dir.path().join("symlink_dir\\"))); + check!(h::open_ambient_dir(dir.path().join("symlink_dir/"))); + #[cfg(windows)] + { + error!( + h::open_ambient_dir(dir.path().join("symlink_dir_slash")), + 123 + ); + error!( + h::open_ambient_dir(dir.path().join("symlink_dir_slashdotdot")), + 123 + ); + error!( + h::open_ambient_dir(dir.path().join("symlink_dir_slashdotdotslash")), + 123 + ); + error!( + h::open_ambient_dir(dir.path().join("symlink_dotslash")), + 123 + ); + error!( + h::open_ambient_dir(dir.path().join("symlink_dir_slashdot")), + 123 + ); + } + #[cfg(not(windows))] + { + check!(h::open_ambient_dir(dir.path().join("symlink_dir_slash"))); + check!(h::open_ambient_dir( + dir.path().join("symlink_dir_slashdotdot") + )); + check!(h::open_ambient_dir( + dir.path().join("symlink_dir_slashdotdotslash") + )); + check!(h::open_ambient_dir(dir.path().join("symlink_dotslash"))); + check!(h::open_ambient_dir(dir.path().join("symlink_dir_slashdot"))); + } + check!(h::open_ambient_dir(dir.path().join("symlink_dot"))); + check!(h::open_ambient_dir(dir.path().join("dir"))); + + // Check various ways of spelling `dir/../symlink_dir`. + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../symlink_dir"); + check!(h::open_ambient_dir(dir.path().join(&name))); + } + + // Check various paths which end with a symlink (even though the symlink + // expansion may end with `/` or a non-symlink). + for suffix in &[""] { + for symlink_dir in &["symlink_dot"] { + let name = format!("{symlink_dir}{suffix}"); + check!(h::open_ambient_dir(dir.path().join(&name))); + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + check!(h::open_ambient_dir(dir.path().join(&name))); + } + } + } + + // Check more paths which end with a symlink. On Windows, these fail due to + // the symlink-to-path-ending-in-trailing-slash error. + for suffix in &[""] { + for symlink_dir in &[ + "symlink_dir_slashdotdot", + "symlink_dir_slashdot", + "symlink_dir_slash", + "symlink_dir_slashdotdotslash", + "symlink_dotslash", + ] { + let name = format!("{symlink_dir}{suffix}"); + #[cfg(windows)] + { + error!(h::open_ambient_dir(dir.path().join(&name)), 123); + } + #[cfg(not(windows))] + { + check!(h::open_ambient_dir(dir.path().join(&name))); + } + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + #[cfg(windows)] + { + error!(h::open_ambient_dir(dir.path().join(&name)), 123); + } + #[cfg(not(windows))] + { + check!(h::open_ambient_dir(dir.path().join(&name))); + } + } + } + } + + // Check those same paths, but with various suffixes appended. + for suffix in &["/", "/.", "/./"] { + for symlink_dir in &["symlink_dir", "symlink_dot"] { + let name = format!("{symlink_dir}{suffix}"); + check!(h::open_ambient_dir(dir.path().join(&name))); + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + check!(h::open_ambient_dir(dir.path().join(&name))); + } + } + } + + // Check those same paths, but with various suffixes appended. On + // Windows, these fail due to the + // symlink-to-path-ending-in-trailing-slash error. + for suffix in &["/", "/.", "/./"] { + for symlink_dir in &[ + "symlink_dir_slash", + "symlink_dir_slashdot", + "symlink_dir_slashdotdot", + "symlink_dir_slashdotdotslash", + "symlink_dotslash", + ] { + let name = format!("{symlink_dir}{suffix}"); + #[cfg(windows)] + { + error!(h::open_ambient_dir(dir.path().join(&name)), 123); + } + #[cfg(not(windows))] + { + check!(h::open_ambient_dir(dir.path().join(&name))); + } + for dir_name in &["dir", "symlink_dir"] { + let name = format!("{dir_name}/../{name}"); + #[cfg(windows)] + { + error!(h::open_ambient_dir(dir.path().join(&name)), 123); + } + #[cfg(not(windows))] + { + check!(h::open_ambient_dir(dir.path().join(&name))); + } + } + } + } + + // Check various ways of spelling `.`. + for cur_dir in &["dir/..", "dir/../", ".", "./"] { + check!(h::open_ambient_dir(dir.path().join(cur_dir))); + } +} diff --git a/crates/wasi/src/filesystem/primitives/tests/sys_common/io.rs b/crates/wasi/src/filesystem/primitives/tests/sys_common/io.rs new file mode 100644 index 000000000000..e0dc74003acf --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/sys_common/io.rs @@ -0,0 +1,6 @@ +pub use tempfile::TempDir; + +#[allow(unused)] +pub fn tmpdir() -> TempDir { + tempfile::tempdir().expect("expected to be able to create a temporary directory") +} diff --git a/crates/wasi/src/filesystem/primitives/tests/sys_common/mod.rs b/crates/wasi/src/filesystem/primitives/tests/sys_common/mod.rs new file mode 100644 index 000000000000..d13e417d64e4 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/sys_common/mod.rs @@ -0,0 +1,96 @@ +#![allow(unused_imports)] + +mod symlink_junction; + +pub mod io; + +pub use symlink_junction::*; + +#[allow(unused)] +macro_rules! check { + ($e:expr) => { + match $e { + Ok(t) => t, + Err(e) => panic!("{} failed with: {}", stringify!($e), e), + } + }; +} + +#[cfg(windows)] +#[allow(unused)] +macro_rules! error { + ($e:expr, $s:expr) => { + match $e { + Ok(_) => panic!("Unexpected success. Should've been: {:?}", $s), + Err(ref err) => { + assert!( + err.raw_os_error() == Some($s), + "`{}` did not have a code of `{}`", + err, + $s + ) + } + } + }; +} + +#[cfg(any(unix, target_os = "wasi"))] +#[allow(unused)] +macro_rules! error { + ($e:expr, $s:expr) => { + error_contains!($e, $s) + }; +} + +#[allow(unused)] +macro_rules! error_contains { + ($e:expr, $s:expr) => { + match $e { + Ok(_) => panic!("Unexpected success. Should've been: {:?}", $s), + Err(ref err) => { + assert!( + err.to_string().contains($s), + "`{}` did not contain `{}`", + err, + $s + ) + } + } + }; +} + +// The following is derived from Rust's +// src/tools/cargo/crates/cargo-test-support/src/lib.rs at revision +// a78a62fc996ba16f7a111c99520b23f77029f4eb. + +#[cfg(windows)] +#[allow(dead_code)] +pub fn symlink_supported() -> bool { + let dir = tempfile::tempdir().unwrap(); + + let src = dir.path().join("symlink_src"); + std::fs::write(&src, "").unwrap(); + let dst = dir.path().join("symlink_dst"); + let result = match std::os::windows::fs::symlink_file(&src, &dst) { + Ok(_) => { + std::fs::remove_file(&dst).unwrap(); + true + } + Err(e) => { + eprintln!( + "symlinks not supported: {:?}\n\ + Windows 10 users should enable developer mode.", + e + ); + false + } + }; + std::fs::remove_file(&src).unwrap(); + return result; +} + +#[cfg(not(windows))] +#[allow(dead_code)] +pub fn symlink_supported() -> bool { + true +} diff --git a/crates/wasi/src/filesystem/primitives/tests/sys_common/symlink_junction.rs b/crates/wasi/src/filesystem/primitives/tests/sys_common/symlink_junction.rs new file mode 100644 index 000000000000..840dd8fd147c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/sys_common/symlink_junction.rs @@ -0,0 +1,144 @@ +// Implementation derived from `symlink_junction` and related code in Rust's +// library/std/src/sys/windows/fs.rs at revision +// 3ffb27ff89db780e88abe829783565a7122be1c5. + +use std::path::Path; +use std::{fs, io}; + +#[cfg(not(windows))] +#[allow(dead_code)] +pub fn symlink_junction, Q: AsRef>( + src: P, + dst_dir: &fs::File, + dst: Q, +) -> io::Result<()> { + crate::filesystem::primitives::symlink(src.as_ref(), dst_dir, dst.as_ref()) +} + +#[cfg(windows)] +#[allow(dead_code)] +pub fn symlink_junction, Q: AsRef>( + src: P, + dst_dir: &fs::File, + dst: Q, +) -> io::Result<()> { + symlink_junction_inner(src.as_ref(), dst_dir, dst.as_ref()) +} + +/// Align the inner value to 8 bytes. +/// +/// This is enough for almost all of the buffers we're likely to work with in +/// the Windows APIs we use. +#[cfg(windows)] +#[repr(C, align(8))] +#[derive(Copy, Clone)] +struct Align8(pub T); + +#[cfg(windows)] +#[allow(dead_code)] +#[allow(non_snake_case)] +#[repr(C)] +pub struct REPARSE_MOUNTPOINT_DATA_BUFFER { + pub ReparseTag: u32, + pub ReparseDataLength: u32, + pub Reserved: u16, + pub ReparseTargetLength: u16, + pub ReparseTargetMaximumLength: u16, + pub Reserved1: u16, + pub ReparseTarget: u16, +} + +#[cfg(windows)] +#[allow(dead_code)] +pub fn cvt(i: windows_sys::core::BOOL) -> io::Result { + if i == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(i) + } +} + +// Creating a directory junction on windows involves dealing with reparse +// points and the `DeviceIoControl` function, and this code is a skeleton of +// what can be found here: +// +// http://www.flexhex.com/docs/articles/hard-links.phtml +#[cfg(windows)] +#[allow(dead_code)] +fn symlink_junction_inner(original: &Path, dir: &fs::File, junction: &Path) -> io::Result<()> { + use crate::filesystem::primitives::{ + DirOptions, OpenOptions, OpenOptionsExt, create_dir, open, + }; + use std::mem::MaybeUninit; + use std::os::windows::ffi::OsStrExt; + use std::os::windows::io::AsRawHandle; + use std::{mem, ptr}; + use windows_sys::Win32::Storage::FileSystem::MAXIMUM_REPARSE_DATA_BUFFER_SIZE; + + create_dir(dir, junction, &DirOptions::new())?; + + let mut opts = OpenOptions::new(); + opts.write(true); + opts.custom_flags( + windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT + | windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS, + ); + let f = open(dir, junction, &opts)?; + let h = f.as_raw_handle(); + unsafe { + let mut data = + Align8([MaybeUninit::::uninit(); MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize]); + let data_ptr = data.0.as_mut_ptr(); + let data_end = data_ptr.add(MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize); + let db = data_ptr.cast::(); + // Zero the header to ensure it's fully initialized, including reserved parameters. + *db = mem::zeroed(); + let reparse_target_slice = { + let buf_start = ptr::addr_of_mut!((*db).ReparseTarget).cast::(); + // Compute offset in bytes and then divide so that we round down + // rather than hit any UB (admittedly this arithmetic should work + // out so that this isn't necessary) + // TODO: use `byte_offfset_from` when pointer_byte_offsets is stable + // let buf_len_bytes = usize::try_from(data_end.byte_offset_from(buf_start)).unwrap(); + let buf_len_bytes = + usize::try_from(data_end.cast::().offset_from(buf_start.cast::())).unwrap(); + let buf_len_wchars = buf_len_bytes / core::mem::size_of::(); + core::slice::from_raw_parts_mut(buf_start, buf_len_wchars) + }; + // FIXME: this conversion is very hacky + let iter = br"\??\" + .iter() + .map(|x| *x as u16) + .chain(original.as_os_str().encode_wide()) + .chain(core::iter::once(0)); + let mut i = 0; + for c in iter { + if i >= reparse_target_slice.len() { + return Err(io::Error::new( + // TODO: use io::ErrorKind::InvalidFilename when io_error_more is stabilized + io::ErrorKind::Other, + "Input filename is too long", + )); + } + reparse_target_slice[i] = c; + i += 1; + } + (*db).ReparseTag = windows_sys::Win32::System::SystemServices::IO_REPARSE_TAG_MOUNT_POINT; + (*db).ReparseTargetMaximumLength = (i * 2) as u16; + (*db).ReparseTargetLength = ((i - 1) * 2) as u16; + (*db).ReparseDataLength = (*db).ReparseTargetLength as u32 + 12; + + let mut ret = 0; + cvt(windows_sys::Win32::System::IO::DeviceIoControl( + h as _, + windows_sys::Win32::System::Ioctl::FSCTL_SET_REPARSE_POINT, + data_ptr.cast(), + (*db).ReparseDataLength + 8, + ptr::null_mut(), + 0, + &mut ret, + ptr::null_mut(), + )) + .map(drop) + } +} diff --git a/crates/wasi/src/filesystem/primitives/tests/windows_open.rs b/crates/wasi/src/filesystem/primitives/tests/windows_open.rs new file mode 100644 index 000000000000..d449e4a83891 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/windows_open.rs @@ -0,0 +1,208 @@ +//! On Windows, cap-std uses the technique of looking up absolute paths for +//! directory handles. This would be racy, except that cap-std uses Windows' +//! sharing modes to prevent open directories from being removed or renamed. +//! Test that this works. + +#![cfg(windows)] + +use super::helpers as h; +use super::sys_common::io::tmpdir; +use crate::filesystem::primitives as p; +use std::path::Path; + +#[test] +fn windows_open_one() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "aaa")); + + let dir = check!(p::open_dir(&start, Path::new("aaa"))); + + // Attempts to remove or rename the open directory should fail. + p::remove_dir(&start, Path::new("aaa")).unwrap_err(); + p::rename(&start, Path::new("aaa"), &start, Path::new("zzz")).unwrap_err(); + + drop(dir); + + // Now that we've dropped the handle, the same operations should succeed. + check!(p::rename( + &start, + Path::new("aaa"), + &start, + Path::new("xxx") + )); + check!(p::remove_dir(&start, Path::new("xxx"))); +} + +#[test] +fn windows_open_multiple() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir_all(&start, "aaa/bbb")); + + let dir = check!(p::open_dir(&start, Path::new("aaa/bbb"))); + + // Attempts to remove or rename any component of the open directory should + // fail. + p::remove_dir(&start, Path::new("aaa/bbb")).unwrap_err(); + p::remove_dir(&start, Path::new("aaa")).unwrap_err(); + p::rename(&start, Path::new("aaa/bbb"), &start, Path::new("aaa/yyy")).unwrap_err(); + p::rename(&start, Path::new("aaa"), &start, Path::new("zzz")).unwrap_err(); + + drop(dir); + + // Now that we've dropped the handle, the same operations should succeed. + check!(p::rename( + &start, + Path::new("aaa/bbb"), + &start, + Path::new("aaa/www") + )); + check!(p::rename( + &start, + Path::new("aaa"), + &start, + Path::new("xxx") + )); + check!(p::remove_dir(&start, Path::new("xxx/www"))); + check!(p::remove_dir(&start, Path::new("xxx"))); +} + +/// Like `windows_open_multiple`, but does so within a directory that we +/// can close and then independently mutate. +#[test] +fn windows_open_tricky() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + check!(h::create_dir(&start, "qqq")); + + let qqq = check!(p::open_dir(&start, Path::new("qqq"))); + check!(h::create_dir_all(&qqq, "aaa/bbb")); + + let dir = check!(p::open_dir(&qqq, Path::new("aaa/bbb"))); + + // Now drop `qqq`. + drop(qqq); + + // Attempts to remove or rename any component of the open directory should + // fail. + p::remove_dir(&dir, Path::new("aaa/bbb")).unwrap_err(); + p::remove_dir(&dir, Path::new("aaa")).unwrap_err(); + p::rename(&dir, Path::new("aaa/bbb"), &dir, Path::new("aaa/yyy")).unwrap_err(); + p::rename(&dir, Path::new("aaa"), &dir, Path::new("zzz")).unwrap_err(); + p::remove_dir(&start, Path::new("qqq/aaa/bbb")).unwrap_err(); + p::remove_dir(&start, Path::new("qqq/aaa")).unwrap_err(); + p::remove_dir(&start, Path::new("qqq")).unwrap_err(); + p::rename( + &dir, + Path::new("qqq/aaa/bbb"), + &dir, + Path::new("qqq/aaa/yyy"), + ) + .unwrap_err(); + p::rename(&start, Path::new("qqq/aaa"), &start, Path::new("qqq/zzz")).unwrap_err(); + p::rename(&start, Path::new("qqq"), &start, Path::new("vvv")).unwrap_err(); + + drop(dir); + + // Now that we've dropped the handle, the same operations should succeed. + check!(p::rename( + &start, + Path::new("qqq/aaa/bbb"), + &start, + Path::new("qqq/aaa/www") + )); + check!(p::rename( + &start, + Path::new("qqq/aaa"), + &start, + Path::new("qqq/xxx") + )); + check!(p::rename( + &start, + Path::new("qqq"), + &start, + Path::new("uuu") + )); + check!(p::remove_dir(&start, Path::new("uuu/xxx/www"))); + check!(p::remove_dir(&start, Path::new("uuu/xxx"))); + check!(p::remove_dir(&start, Path::new("uuu"))); +} + +/// Like `windows_open_one` but uses `open_ambient_dir` instead of `open_dir`. +#[test] +fn windows_open_ambient() { + let ambient_dir = tempfile::tempdir().unwrap(); + + let start = check!(h::open_ambient_dir(ambient_dir.path())); + check!(h::create_dir(&start, "aaa")); + + let dir = check!(h::open_ambient_dir(ambient_dir.path().join("aaa"))); + + // Attempts to remove or rename the open directory should fail. + p::remove_dir(&start, Path::new("aaa")).unwrap_err(); + p::rename(&start, Path::new("aaa"), &start, Path::new("zzz")).unwrap_err(); + + drop(dir); + + // Now that we've dropped the handle, the same operations should succeed. + check!(p::rename( + &start, + Path::new("aaa"), + &start, + Path::new("xxx") + )); + check!(p::remove_dir(&start, Path::new("xxx"))); +} + +#[test] +fn windows_open_special() { + let tmpdir = tmpdir(); + let start = h::dir_of(&tmpdir); + + // Opening any of these should fail. + for device in &[ + "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", + "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³", + ] { + for suffix in &[ + "", + " ", + ".", + ". ", + ".ext", + ".ext.", + ".ext. ", + ".ext ", + ".ext.more", + ".ext.more.", + ".ext.more ", + ".ext.more. ", + ".ext.more .", + ] { + let name = format!("{}{}", device, suffix); + eprintln!("testing '{}'", name); + + match h::open(&start, &name).unwrap_err().kind() { + std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => {} + kind => panic!("unexpected error: {:?}", kind), + } + + let mut options = p::OpenOptions::new(); + options.write(true); + match p::open(&start, Path::new(&name), &options) + .unwrap_err() + .kind() + { + std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => {} + kind => panic!("unexpected error: {:?}", kind), + } + + match h::create(&start, &name).unwrap_err().kind() { + std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => {} + kind => panic!("unexpected error: {:?}", kind), + } + } + } +} diff --git a/crates/wasi/src/filesystem/primitives/tests/windows_symlinks.rs b/crates/wasi/src/filesystem/primitives/tests/windows_symlinks.rs new file mode 100644 index 000000000000..3a8c88b7835f --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/tests/windows_symlinks.rs @@ -0,0 +1,84 @@ +#![cfg(windows)] + +use super::helpers as h; +use super::sys_common::io::tmpdir; +use super::sys_common::symlink_supported; +use crate::filesystem::primitives as p; + +use std::path::Path; + +#[test] +fn windows_symlinks() { + if !symlink_supported() { + return; + } + + let tmpdir = tmpdir(); + + let start = h::dir_of(&tmpdir); + + check!(h::create(&start, "file")); + check!(h::create_dir(&start, "dir")); + + // Windows lets these succeed. + check!(h::symlink_dir(&start, "file", "file_symlink_dir")); + check!(h::symlink_file(&start, "dir", "dir_symlink_file")); + + // But accessing them fails. + assert!(h::open(&start, "dir_symlink_file").is_err()); + assert!(h::open(&start, "file_symlink_dir").is_err()); + assert!(p::open_dir(&start, Path::new("dir_symlink_file")).is_err()); + assert!(p::open_dir(&start, Path::new("file_symlink_dir")).is_err()); + assert!(h::metadata(&start, "dir_symlink_file").is_err()); + assert!(h::metadata(&start, "file_symlink_dir").is_err()); + + assert!( + check!(h::symlink_metadata(&start, "file_symlink_dir")) + .file_type() + .is_symlink() + ); + assert!( + check!(h::symlink_metadata(&start, "dir_symlink_file")) + .file_type() + .is_symlink() + ); +} + +#[test] +fn windows_symlinks_ambient() { + use std::fs; + use std::os::windows::fs::{symlink_dir, symlink_file}; + + if !symlink_supported() { + return; + } + + let dir = tempfile::tempdir().unwrap(); + + check!(fs::File::create(dir.path().join("file"))); + check!(fs::create_dir(dir.path().join("dir"))); + + // Windows lets these succeed. + check!(symlink_dir("file", dir.path().join("file_symlink_dir"))); + check!(symlink_file("dir", dir.path().join("dir_symlink_file"))); + + // But accessing them fails. + assert!(fs::File::open(dir.path().join("dir_symlink_file")).is_err()); + assert!(fs::File::open(dir.path().join("file_symlink_dir")).is_err()); + + assert!(h::open_ambient_dir(dir.path().join("dir_symlink_file")).is_err()); + assert!(h::open_ambient_dir(dir.path().join("file_symlink_dir")).is_err()); + assert!(fs::metadata(dir.path().join("dir_symlink_file")).is_err()); + assert!(fs::metadata(dir.path().join("file_symlink_dir")).is_err()); + + assert!( + check!(fs::symlink_metadata(dir.path().join("file_symlink_dir"))) + .file_type() + .is_symlink() + ); + assert!( + check!(fs::symlink_metadata(dir.path().join("dir_symlink_file"))) + .file_type() + .is_symlink() + ); +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/create_dir.rs b/crates/wasi/src/filesystem/primitives/via_parent/create_dir.rs new file mode 100644 index 000000000000..900420de1e02 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/create_dir.rs @@ -0,0 +1,21 @@ +use super::open_parent; +use crate::filesystem::primitives::{ + DirOptions, MaybeOwnedFile, create_dir_unchecked, strip_dir_suffix, +}; +use std::path::Path; +use std::{fs, io}; + +/// Implement `create_dir` by `open`ing up the parent component of the path and +/// then calling `create_dir_unchecked` on the last component. +pub(crate) fn create_dir(start: &fs::File, path: &Path, options: &DirOptions) -> io::Result<()> { + let start = MaybeOwnedFile::borrowed(start); + + // As a special case, `create_dir` ignores a trailing slash rather than + // treating it as equivalent to a trailing slash-dot, so strip any trailing + // slashes. + let path = strip_dir_suffix(path); + + let (dir, basename) = open_parent(start, &path)?; + + create_dir_unchecked(&dir, basename.as_ref(), options) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/hard_link.rs b/crates/wasi/src/filesystem/primitives/via_parent/hard_link.rs new file mode 100644 index 000000000000..b725ca7f6f1f --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/hard_link.rs @@ -0,0 +1,26 @@ +use super::open_parent; +use crate::filesystem::primitives::{MaybeOwnedFile, hard_link_unchecked}; +use std::path::Path; +use std::{fs, io}; + +/// Implement `hard_link` by `open`ing up the parent component of the path and +/// then calling `hard_link_unchecked` on the last component. +pub(crate) fn hard_link( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + let old_start = MaybeOwnedFile::borrowed(old_start); + let new_start = MaybeOwnedFile::borrowed(new_start); + + let (old_dir, old_basename) = open_parent(old_start, old_path)?; + let (new_dir, new_basename) = open_parent(new_start, new_path)?; + + hard_link_unchecked( + &old_dir, + old_basename.as_ref(), + &new_dir, + new_basename.as_ref(), + ) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/mod.rs b/crates/wasi/src/filesystem/primitives/via_parent/mod.rs new file mode 100644 index 000000000000..5c6241e7f2c3 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/mod.rs @@ -0,0 +1,31 @@ +//! In many operations, the last component of a path is special. For example, +//! in `create_dir`, the last component names the path to be created, while the +//! rest of the components just name the place to create it in. + +mod create_dir; +mod hard_link; +mod open_parent; +#[cfg(not(windows))] // doesn't work on windows; use a windows-specific impl +mod read_link; +mod remove_dir; +mod remove_file; +mod rename; +#[cfg(not(windows))] +mod set_times_nofollow; +mod symlink; + +use open_parent::open_parent; + +pub(crate) use create_dir::create_dir; +pub(crate) use hard_link::hard_link; +#[cfg(not(windows))] // doesn't work on windows; use a windows-specific impl +pub(crate) use read_link::read_link; +pub(crate) use remove_dir::remove_dir; +pub(crate) use remove_file::remove_file; +pub(crate) use rename::rename; +#[cfg(not(windows))] +pub(crate) use set_times_nofollow::set_times_nofollow; +#[cfg(not(windows))] +pub(crate) use symlink::symlink; +#[cfg(windows)] +pub(crate) use symlink::{symlink_dir, symlink_file}; diff --git a/crates/wasi/src/filesystem/primitives/via_parent/open_parent.rs b/crates/wasi/src/filesystem/primitives/via_parent/open_parent.rs new file mode 100644 index 000000000000..974e4ebfd565 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/open_parent.rs @@ -0,0 +1,126 @@ +/// `open_parent` is the key building block in all `via_parent` functions. +/// It opens the parent directory of the given path, and returns the basename, +/// so that all the `via_parent` functions need to do is make sure they +/// don't follow symlinks in the basename. +use crate::filesystem::primitives::{MaybeOwnedFile, errors, open_dir, path_requires_dir}; +use std::ffi::OsStr; +use std::io; +use std::path::{Component, Path}; + +/// Open the "parent" of `path`, relative to `start`. The return value on +/// success is a tuple of the newly opened directory and an `OsStr` referencing +/// the single remaining path component. This last component will not be `..`, +/// though it may be `.` or a symbolic link to anywhere (possibly +/// including `..` or an absolute path). +pub(super) fn open_parent<'path, 'borrow>( + start: MaybeOwnedFile<'borrow>, + path: &'path Path, +) -> io::Result<(MaybeOwnedFile<'borrow>, &'path OsStr)> { + let (dirname, basename) = split_parent(path).ok_or_else(errors::no_such_file_or_directory)?; + + let dir = if dirname.as_os_str().is_empty() { + start + } else { + MaybeOwnedFile::owned(open_dir(&start, dirname)?) + }; + + Ok((dir, basename.as_os_str())) +} + +/// Split `path` into parent and basename parts. Return `None` if `path` +/// is empty. +/// +/// This differs from `path.parent()` and `path.file_name()` in several +/// respects: +/// - Treat paths ending in `/` or `/.` as implying a directory. +/// - Treat the path `.` as a normal component rather than a parent. +/// - Append a `.` to a path with a trailing `..` to avoid requiring our +/// callers to special-case `..`. +/// - Bare absolute paths are ok. +fn split_parent(path: &Path) -> Option<(&Path, Component<'_>)> { + if path.as_os_str().is_empty() { + return None; + } + + if !path_requires_dir(path) { + let mut comps = path.components(); + if let Some(p) = comps.next_back() { + match p { + Component::Normal(_) | Component::CurDir => return Some((comps.as_path(), p)), + _ => (), + } + } + } + + Some((path, Component::CurDir)) +} + +#[test] +fn split_parent_basics() { + assert_eq!( + split_parent(Path::new("foo/bar/qux")).unwrap(), + ( + Path::new("foo/bar"), + Component::Normal(Path::new("qux").as_ref()) + ) + ); + assert_eq!( + split_parent(Path::new("foo/bar")).unwrap(), + ( + Path::new("foo"), + Component::Normal(Path::new("bar").as_ref()) + ) + ); + assert_eq!( + split_parent(Path::new("foo")).unwrap(), + (Path::new(""), Component::Normal(Path::new("foo").as_ref())) + ); +} + +#[test] +fn split_parent_special_cases() { + assert!(split_parent(Path::new("")).is_none()); + assert_eq!( + split_parent(Path::new("foo/")).unwrap(), + (Path::new("foo"), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new("foo/.")).unwrap(), + (Path::new("foo"), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new(".")).unwrap(), + (Path::new(""), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new("..")).unwrap(), + (Path::new(".."), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new("../..")).unwrap(), + (Path::new("../.."), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new("../foo")).unwrap(), + ( + Path::new(".."), + Component::Normal(Path::new("foo").as_ref()) + ) + ); + assert_eq!( + split_parent(Path::new("foo/..")).unwrap(), + (Path::new("foo/.."), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new("/foo")).unwrap(), + (Path::new("/"), Component::Normal(Path::new("foo").as_ref())) + ); + assert_eq!( + split_parent(Path::new("/foo/")).unwrap(), + (Path::new("/foo"), Component::CurDir) + ); + assert_eq!( + split_parent(Path::new("/")).unwrap(), + (Path::new("/"), Component::CurDir) + ); +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/read_link.rs b/crates/wasi/src/filesystem/primitives/via_parent/read_link.rs new file mode 100644 index 000000000000..edd03a4bafd0 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/read_link.rs @@ -0,0 +1,21 @@ +use super::open_parent; +use crate::filesystem::primitives::{MaybeOwnedFile, read_link_unchecked}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// Implement `read_link` by `open`ing up the parent component of the path and +/// then calling `read_link_unchecked` on the last component. +/// +/// This technique doesn't work in all cases on Windows. In particular, a +/// directory symlink such as `C:\Documents and Settings` may not grant any +/// access other than what is needed to resolve the symlink, so `open_parent`'s +/// technique of returning a relative path of `.` from that point doesn't work, +/// because opening `.` within such a directory is denied. Consequently, we use +/// a different implementation on Windows. +pub(crate) fn read_link(start: &fs::File, path: &Path) -> io::Result { + let start = MaybeOwnedFile::borrowed(start); + + let (dir, basename) = open_parent(start, path)?; + + read_link_unchecked(&dir, basename.as_ref(), PathBuf::new()) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/remove_dir.rs b/crates/wasi/src/filesystem/primitives/via_parent/remove_dir.rs new file mode 100644 index 000000000000..9a46149df73a --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/remove_dir.rs @@ -0,0 +1,14 @@ +use super::open_parent; +use crate::filesystem::primitives::{MaybeOwnedFile, remove_dir_unchecked}; +use std::path::Path; +use std::{fs, io}; + +/// Implement `remove_dir` by `open`ing up the parent component of the path and +/// then calling `remove_dir_unchecked` on the last component. +pub(crate) fn remove_dir(start: &fs::File, path: &Path) -> io::Result<()> { + let start = MaybeOwnedFile::borrowed(start); + + let (dir, basename) = open_parent(start, path)?; + + remove_dir_unchecked(&dir, basename.as_ref()) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/remove_file.rs b/crates/wasi/src/filesystem/primitives/via_parent/remove_file.rs new file mode 100644 index 000000000000..7b06f727e4d7 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/remove_file.rs @@ -0,0 +1,14 @@ +use super::open_parent; +use crate::filesystem::primitives::{MaybeOwnedFile, remove_file_unchecked}; +use std::path::Path; +use std::{fs, io}; + +/// Implement `remove_file` by `open`ing up the parent component of the path +/// and then calling `remove_file_unchecked` on the last component. +pub(crate) fn remove_file(start: &fs::File, path: &Path) -> io::Result<()> { + let start = MaybeOwnedFile::borrowed(start); + + let (dir, basename) = open_parent(start, path)?; + + remove_file_unchecked(&dir, basename.as_ref()) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/rename.rs b/crates/wasi/src/filesystem/primitives/via_parent/rename.rs new file mode 100644 index 000000000000..0e62eb36258b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/rename.rs @@ -0,0 +1,50 @@ +use super::open_parent; +use crate::filesystem::primitives::{MaybeOwnedFile, rename_unchecked, strip_dir_suffix}; +#[cfg(unix)] +use crate::filesystem::primitives::{append_dir_suffix, path_has_trailing_slash}; +use std::path::Path; +use std::{fs, io}; + +/// Implement `rename` by `open`ing up the parent component of the path and +/// then calling `rename_unchecked` on the last component. +pub(crate) fn rename( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + let old_start = MaybeOwnedFile::borrowed(old_start); + let new_start = MaybeOwnedFile::borrowed(new_start); + + // As a special case, `rename` ignores a trailing slash rather than treating + // it as equivalent to a trailing slash-dot, so strip any trailing slashes + // for the purposes of `open_parent`. + // + // And on Unix, remember whether the source started with a slash so that we + // can still fail if it is and the source is a regular file. + #[cfg(unix)] + let old_starts_with_slash = path_has_trailing_slash(old_path); + let old_path = strip_dir_suffix(old_path); + let new_path = strip_dir_suffix(new_path); + + let (old_dir, old_basename) = open_parent(old_start, &old_path)?; + let (new_dir, new_basename) = open_parent(new_start, &new_path)?; + + // On Unix, re-append a slash if needed. + #[cfg(unix)] + let concat; + #[cfg(unix)] + let old_basename = if old_starts_with_slash { + concat = append_dir_suffix(old_basename.to_owned().into()); + concat.as_os_str() + } else { + old_basename + }; + + rename_unchecked( + &old_dir, + old_basename.as_ref(), + &new_dir, + new_basename.as_ref(), + ) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/set_times_nofollow.rs b/crates/wasi/src/filesystem/primitives/via_parent/set_times_nofollow.rs new file mode 100644 index 000000000000..8a4146e0afd8 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/set_times_nofollow.rs @@ -0,0 +1,19 @@ +use super::open_parent; +use crate::filesystem::primitives::{MaybeOwnedFile, set_times_nofollow_unchecked}; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; + +#[inline] +pub(crate) fn set_times_nofollow( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + let start = MaybeOwnedFile::borrowed(start); + + let (dir, basename) = open_parent(start, path)?; + + set_times_nofollow_unchecked(&dir, basename.as_ref(), atime, mtime) +} diff --git a/crates/wasi/src/filesystem/primitives/via_parent/symlink.rs b/crates/wasi/src/filesystem/primitives/via_parent/symlink.rs new file mode 100644 index 000000000000..6ea53afc8f43 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/via_parent/symlink.rs @@ -0,0 +1,48 @@ +use super::open_parent; +use crate::filesystem::primitives::MaybeOwnedFile; +use std::path::Path; +use std::{fs, io}; + +/// Implement `symlink` by `open`ing up the parent component of the path and +/// then calling `symlink_unchecked` on the last component. +#[cfg(not(windows))] +pub(crate) fn symlink(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> { + use crate::filesystem::primitives::symlink_unchecked; + let new_start = MaybeOwnedFile::borrowed(new_start); + + let (new_dir, new_basename) = open_parent(new_start, new_path)?; + + symlink_unchecked(old_path, &new_dir, new_basename.as_ref()) +} + +/// Implement `symlink_file` by `open`ing up the parent component of the path +/// and then calling `symlink_file` on the last component. +#[cfg(windows)] +pub(crate) fn symlink_file( + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + use crate::filesystem::primitives::symlink_file_unchecked; + let new_start = MaybeOwnedFile::borrowed(new_start); + + let (new_dir, new_basename) = open_parent(new_start, new_path)?; + + symlink_file_unchecked(old_path, &new_dir, new_basename.as_ref()) +} + +/// Implement `symlink_dir` by `open`ing up the parent component of the path +/// and then calling `symlink_dir` on the last component. +#[cfg(windows)] +pub(crate) fn symlink_dir( + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + use crate::filesystem::primitives::symlink_dir_unchecked; + let new_start = MaybeOwnedFile::borrowed(new_start); + + let (new_dir, new_basename) = open_parent(new_start, new_path)?; + + symlink_dir_unchecked(old_path, &new_dir, new_basename.as_ref()) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/create_dir_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/create_dir_unchecked.rs new file mode 100644 index 000000000000..442b38916999 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/create_dir_unchecked.rs @@ -0,0 +1,18 @@ +use super::get_path::concatenate; +use crate::filesystem::primitives::DirOptions; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `create_dir`, but which does not perform +/// sandboxing. +/// +/// Windows doesn't have any extra flags in `DirOptions`, so the `options` +/// parameter is ignored. +pub(crate) fn create_dir_unchecked( + start: &fs::File, + path: &Path, + _options: &DirOptions, +) -> io::Result<()> { + let out_path = concatenate(start, path)?; + fs::create_dir(out_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/create_file_at_w.rs b/crates/wasi/src/filesystem/primitives/windows/fs/create_file_at_w.rs new file mode 100644 index 000000000000..be0b7c6649ca --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/create_file_at_w.rs @@ -0,0 +1,280 @@ +#![allow(unsafe_code)] + +use std::mem; +use std::os::windows::io::HandleOrInvalid; +use std::ptr::null_mut; +use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; +use windows_sys::Wdk::Storage::FileSystem::{ + FILE_CREATE, FILE_DELETE_ON_CLOSE, FILE_NO_INTERMEDIATE_BUFFERING, FILE_NON_DIRECTORY_FILE, + FILE_OPEN, FILE_OPEN_FOR_BACKUP_INTENT, FILE_OPEN_IF, FILE_OPEN_REPARSE_POINT, FILE_OVERWRITE, + FILE_OVERWRITE_IF, FILE_RANDOM_ACCESS, FILE_SEQUENTIAL_ONLY, FILE_SYNCHRONOUS_IO_NONALERT, + FILE_WRITE_THROUGH, NtCreateFile, +}; +use windows_sys::Win32::Foundation::{ + ERROR_ALREADY_EXISTS, ERROR_FILE_EXISTS, ERROR_INVALID_NAME, ERROR_INVALID_PARAMETER, + ERROR_NOT_SUPPORTED, GENERIC_ALL, GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE, + RtlNtStatusToDosError, STATUS_OBJECT_NAME_COLLISION, STATUS_PENDING, STATUS_SUCCESS, SUCCESS, + SetLastError, UNICODE_STRING, +}; +use windows_sys::Win32::Foundation::{OBJ_CASE_INSENSITIVE, OBJ_INHERIT}; +use windows_sys::Win32::Security::{ + SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, SECURITY_DYNAMIC_TRACKING, + SECURITY_QUALITY_OF_SERVICE, SECURITY_STATIC_TRACKING, +}; +use windows_sys::Win32::Storage::FileSystem::{ + CREATE_ALWAYS, CREATE_NEW, DELETE, FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_COMPRESSED, + FILE_ATTRIBUTE_DEVICE, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_EA, FILE_ATTRIBUTE_ENCRYPTED, + FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_INTEGRITY_STREAM, FILE_ATTRIBUTE_NO_SCRUB_DATA, + FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, FILE_ATTRIBUTE_OFFLINE, + FILE_ATTRIBUTE_PINNED, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS, + FILE_ATTRIBUTE_RECALL_ON_OPEN, FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_SPARSE_FILE, + FILE_ATTRIBUTE_SYSTEM, FILE_ATTRIBUTE_TEMPORARY, FILE_ATTRIBUTE_UNPINNED, + FILE_ATTRIBUTE_VIRTUAL, FILE_CREATION_DISPOSITION, FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_NO_BUFFERING, FILE_FLAG_OPEN_NO_RECALL, + FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_OVERLAPPED, FILE_FLAG_POSIX_SEMANTICS, + FILE_FLAG_RANDOM_ACCESS, FILE_FLAG_SEQUENTIAL_SCAN, FILE_FLAG_SESSION_AWARE, + FILE_FLAG_WRITE_THROUGH, FILE_FLAGS_AND_ATTRIBUTES, FILE_READ_ATTRIBUTES, FILE_SHARE_MODE, + OPEN_ALWAYS, OPEN_EXISTING, SECURITY_CONTEXT_TRACKING, SECURITY_EFFECTIVE_ONLY, + SECURITY_SQOS_PRESENT, SYNCHRONIZE, TRUNCATE_EXISTING, +}; +use windows_sys::Win32::System::IO::IO_STATUS_BLOCK; +use windows_sys::Win32::System::WindowsProgramming::{ + FILE_OPEN_NO_RECALL, FILE_OPEN_REMOTE_INSTANCE, FILE_OPENED, FILE_OVERWRITTEN, +}; + +// All currently known `FILE_ATTRIBUTE_*` constants, according to +// windows-sys' documentation. +const FILE_ATTRIBUTE_VALID_FLAGS: FILE_FLAGS_AND_ATTRIBUTES = FILE_ATTRIBUTE_EA + | FILE_ATTRIBUTE_DEVICE + | FILE_ATTRIBUTE_HIDDEN + | FILE_ATTRIBUTE_NORMAL + | FILE_ATTRIBUTE_PINNED + | FILE_ATTRIBUTE_SYSTEM + | FILE_ATTRIBUTE_ARCHIVE + | FILE_ATTRIBUTE_OFFLINE + | FILE_ATTRIBUTE_VIRTUAL + | FILE_ATTRIBUTE_READONLY + | FILE_ATTRIBUTE_UNPINNED + | FILE_ATTRIBUTE_DIRECTORY + | FILE_ATTRIBUTE_ENCRYPTED + | FILE_ATTRIBUTE_TEMPORARY + | FILE_ATTRIBUTE_COMPRESSED + | FILE_ATTRIBUTE_SPARSE_FILE + | FILE_ATTRIBUTE_NO_SCRUB_DATA + | FILE_ATTRIBUTE_REPARSE_POINT + | FILE_ATTRIBUTE_RECALL_ON_OPEN + | FILE_ATTRIBUTE_INTEGRITY_STREAM + | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED + | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS; + +/// Like Windows' `CreateFileW`, but takes a `dir` argument to use as the +/// root directory. +/// +/// Also, the `lpfilename` is a Rust slice instead of a C-style NUL-terminated +/// array, because that's what our callers have and it's closer to what +/// `NtCreatePath` takes. +#[allow(non_snake_case)] +pub unsafe fn CreateFileAtW( + dir: HANDLE, + lpfilename: &[u16], + dwdesiredaccess: u32, + dwsharemode: FILE_SHARE_MODE, + lpsecurityattributes: *const SECURITY_ATTRIBUTES, + dwcreationdisposition: FILE_CREATION_DISPOSITION, + dwflagsandattributes: FILE_FLAGS_AND_ATTRIBUTES, + htemplatefile: HANDLE, +) -> HandleOrInvalid { + // Absolute paths are not yet implemented here. + // + // It seems like `NtCreatePath` needs the apparently NT-internal `\??\` + // prefix prepended to absolute paths. It's possible it needs other + // path transforms as well. `RtlDosPathNameToNtPathName_U` may be a + // function that does these things, though it's not available in + // windows-sys and not documented, though one can find + // [unofficial blog posts], though even they say things like "I`m + // sorry that I cannot give more details on these functions". + // + // [unofficial blog posts]: https://mecanik.dev/en/posts/convert-dos-and-nt-paths-using-rtl-functions/ + assert!(dir != 0 as HANDLE); + + // Extended attributes are not implemented yet. + if htemplatefile != 0 as HANDLE { + SetLastError(ERROR_NOT_SUPPORTED); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + } + + // Convert `dwcreationdisposition` to the `createdisposition` argument + // to `NtCreateFile`. Do this before converting `lpfilename` so that + // we can return early on failure. + let createdisposition = match dwcreationdisposition { + CREATE_NEW => FILE_CREATE, + CREATE_ALWAYS => FILE_OVERWRITE_IF, + OPEN_EXISTING => FILE_OPEN, + OPEN_ALWAYS => FILE_OPEN_IF, + TRUNCATE_EXISTING => FILE_OVERWRITE, + _ => { + SetLastError(ERROR_INVALID_PARAMETER); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + } + }; + + // Convert `lpfilename` to a `UNICODE_STRING`. + let byte_length = lpfilename.len() * mem::size_of::(); + let length: u16 = match byte_length.try_into() { + Ok(length) => length, + Err(_) => { + SetLastError(ERROR_INVALID_NAME); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + } + }; + let mut unicode_string = UNICODE_STRING { + Buffer: lpfilename.as_ptr() as *mut u16, + Length: length, + MaximumLength: length, + }; + + let mut handle = INVALID_HANDLE_VALUE; + + // Convert `dwdesiredaccess` and `dwflagsandattributes` to the + // `desiredaccess` argument to `NtCreateFile`. + let mut desiredaccess = dwdesiredaccess | SYNCHRONIZE | FILE_READ_ATTRIBUTES; + if dwflagsandattributes & FILE_FLAG_DELETE_ON_CLOSE != 0 { + desiredaccess |= DELETE; + } + + // Compute `objectattributes`' `Attributes` field. Case-insensitive is + // the expected behavior on Windows. + let mut attributes = 0; + if dwflagsandattributes & FILE_FLAG_POSIX_SEMANTICS != 0 { + attributes |= OBJ_CASE_INSENSITIVE as u32; + }; + if !lpsecurityattributes.is_null() && (*lpsecurityattributes).bInheritHandle != 0 { + attributes |= OBJ_INHERIT as u32; + } + + // Compute the `objectattributes` argument to `NtCreateFile`. + let mut objectattributes = mem::zeroed::(); + objectattributes.Length = mem::size_of::() as _; + objectattributes.RootDirectory = dir; + objectattributes.ObjectName = &mut unicode_string; + objectattributes.Attributes = attributes; + if !lpsecurityattributes.is_null() { + objectattributes.SecurityDescriptor = (*lpsecurityattributes) + .lpSecurityDescriptor + .cast::(); + } + + // If needed, set `objectattributes`' `SecurityQualityOfService` field. + let mut qos; + if dwflagsandattributes & SECURITY_SQOS_PRESENT != 0 { + qos = mem::zeroed::(); + qos.Length = mem::size_of::() as _; + qos.ImpersonationLevel = ((dwflagsandattributes >> 16) & 0x3) as _; + qos.ContextTrackingMode = if dwflagsandattributes & SECURITY_CONTEXT_TRACKING != 0 { + SECURITY_DYNAMIC_TRACKING + } else { + SECURITY_STATIC_TRACKING + } as u8; + qos.EffectiveOnly = ((dwflagsandattributes & SECURITY_EFFECTIVE_ONLY) != 0) as _; + + objectattributes.SecurityQualityOfService = + (&mut qos as *mut SECURITY_QUALITY_OF_SERVICE).cast(); + } + + let mut iostatusblock = mem::zeroed::(); + iostatusblock.Anonymous.Status = STATUS_PENDING; + + // Compute the `fileattributes` argument to `NtCreateFile`. Mask off + // unrecognized flags. + let mut fileattributes = dwflagsandattributes & FILE_ATTRIBUTE_VALID_FLAGS; + if fileattributes == 0 { + fileattributes = FILE_ATTRIBUTE_NORMAL; + } + + // Compute the `createoptions` argument to `NtCreateFile`. + let mut createoptions = 0; + if dwflagsandattributes & FILE_FLAG_BACKUP_SEMANTICS == 0 { + createoptions |= FILE_NON_DIRECTORY_FILE; + } else { + if dwdesiredaccess & GENERIC_ALL != 0 { + createoptions |= FILE_OPEN_FOR_BACKUP_INTENT | FILE_OPEN_REMOTE_INSTANCE; + } else { + if dwdesiredaccess & GENERIC_READ != 0 { + createoptions |= FILE_OPEN_FOR_BACKUP_INTENT; + } + if dwdesiredaccess & GENERIC_WRITE != 0 { + createoptions |= FILE_OPEN_REMOTE_INSTANCE; + } + } + } + if dwflagsandattributes & FILE_FLAG_DELETE_ON_CLOSE != 0 { + createoptions |= FILE_DELETE_ON_CLOSE; + } + if dwflagsandattributes & FILE_FLAG_NO_BUFFERING != 0 { + createoptions |= FILE_NO_INTERMEDIATE_BUFFERING; + } + if dwflagsandattributes & FILE_FLAG_OPEN_NO_RECALL != 0 { + createoptions |= FILE_OPEN_NO_RECALL; + } + if dwflagsandattributes & FILE_FLAG_OPEN_REPARSE_POINT != 0 { + createoptions |= FILE_OPEN_REPARSE_POINT; + } + if dwflagsandattributes & FILE_FLAG_OVERLAPPED == 0 { + createoptions |= FILE_SYNCHRONOUS_IO_NONALERT; + } + // FILE_FLAG_POSIX_SEMANTICS is handled above. + if dwflagsandattributes & FILE_FLAG_RANDOM_ACCESS != 0 { + createoptions |= FILE_RANDOM_ACCESS; + } + if dwflagsandattributes & FILE_FLAG_SESSION_AWARE != 0 { + // TODO: How should we handle FILE_FLAG_SESSION_AWARE? + SetLastError(ERROR_NOT_SUPPORTED); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + } + if dwflagsandattributes & FILE_FLAG_SEQUENTIAL_SCAN != 0 { + createoptions |= FILE_SEQUENTIAL_ONLY; + } + if dwflagsandattributes & FILE_FLAG_WRITE_THROUGH != 0 { + createoptions |= FILE_WRITE_THROUGH; + } + + // Ok, we have what we need to call `NtCreateFile` now! + let status = NtCreateFile( + &mut handle, + desiredaccess, + &mut objectattributes, + &mut iostatusblock, + null_mut(), + fileattributes, + dwsharemode, + createdisposition, + createoptions, + null_mut(), + 0, + ); + + // Check for errors. + if status != STATUS_SUCCESS { + handle = INVALID_HANDLE_VALUE; + if status == STATUS_OBJECT_NAME_COLLISION { + SetLastError(ERROR_FILE_EXISTS); + } else { + SetLastError(RtlNtStatusToDosError(status)); + } + } else if (dwcreationdisposition == CREATE_ALWAYS + && iostatusblock.Information == FILE_OVERWRITTEN as usize) + || (dwcreationdisposition == OPEN_ALWAYS + && iostatusblock.Information == FILE_OPENED as usize) + { + // Set `ERROR_ALREADY_EXISTS` according to the table for + // `dwCreationDisposition` in the [`CreateFileW` docs]. + // + // [`CreateFileW` docs]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + SetLastError(ERROR_ALREADY_EXISTS); + } else { + // Otherwise indicate that we succeeded. + SetLastError(SUCCESS); + } + + HandleOrInvalid::from_raw_handle(handle as _) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/dir_entry_inner.rs b/crates/wasi/src/filesystem/primitives/windows/fs/dir_entry_inner.rs new file mode 100644 index 000000000000..c70104683ffe --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/dir_entry_inner.rs @@ -0,0 +1,31 @@ +use crate::filesystem::primitives::Metadata; +use std::ffi::OsString; +use std::{fmt, fs, io}; + +pub(crate) struct DirEntryInner { + std: fs::DirEntry, +} + +impl DirEntryInner { + #[inline] + pub(crate) fn metadata(&self) -> io::Result { + self.std.metadata().map(Metadata::from_just_metadata) + } + + #[inline] + pub(crate) fn file_name(&self) -> OsString { + self.std.file_name() + } + + #[inline] + pub(super) fn from_std(std: fs::DirEntry) -> Self { + Self { std } + } +} + +impl fmt::Debug for DirEntryInner { + // Like libstd's version, but doesn't print the path. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("DirEntry").field(&self.file_name()).finish() + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/dir_options_ext.rs b/crates/wasi/src/filesystem/primitives/windows/fs/dir_options_ext.rs new file mode 100644 index 000000000000..728f59629b66 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/dir_options_ext.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone)] +pub(crate) struct DirOptionsExt {} + +impl DirOptionsExt { + pub(crate) const fn new() -> Self { + Self {} + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/dir_utils.rs b/crates/wasi/src/filesystem/primitives/windows/fs/dir_utils.rs new file mode 100644 index 000000000000..e4b8c28a449d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/dir_utils.rs @@ -0,0 +1,190 @@ +use crate::filesystem::primitives::OpenOptionsExt; +use crate::filesystem::primitives::{OpenOptions, errors}; +use std::ffi::OsString; +use std::ops::Deref; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; +use windows_sys::Win32::Storage::FileSystem::{ + FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_READ, FILE_SHARE_WRITE, +}; + +/// Rust's `Path` implicitly strips redundant slashes, however they aren't +/// redundant in one case: at the end of a path they indicate that a path is +/// expected to name a directory. +pub(crate) fn path_requires_dir(path: &Path) -> bool { + let wide: Vec = path.as_os_str().encode_wide().collect(); + wide.ends_with(&['/' as u16]) + || wide.ends_with(&['/' as u16, '.' as _]) + || wide.ends_with(&['\\' as u16]) + || wide.ends_with(&['\\' as u16, '.' as _]) +} + +/// Windows treats `foo/.` as equivalent to `foo` even if `foo` does not +/// exist or is not a directory. So we don't do the special trailing-dot +/// handling that we do on Posix-ish platforms. +pub(crate) fn path_has_trailing_dot(_path: &Path) -> bool { + false +} + +/// For the purposes of emulating Windows symlink resolution, we sometimes +/// need to know whether a path really does end in a trailing dot though. +pub(crate) fn path_really_has_trailing_dot(path: &Path) -> bool { + let wide: Vec = path.as_os_str().encode_wide().collect(); + + wide.ends_with(&['/' as u16, '.' as u16]) || wide.ends_with(&['\\' as u16, '.' as u16]) +} + +/// Rust's `Path` implicitly strips trailing `/`s, however they aren't +/// redundant in one case: at the end of a path they are the final path +/// component, which has different path lookup behavior. +pub(crate) fn path_has_trailing_slash(path: &Path) -> bool { + let wide: Vec = path.as_os_str().encode_wide().collect(); + + wide.ends_with(&['/' as u16]) || wide.ends_with(&['\\' as u16]) +} + +/// Strip trailing `/`s, unless this reduces `path` to `/` itself. This is +/// used by `create_dir` and others to prevent paths like `foo/` from +/// canonicalizing to `foo/.` since these syscalls treat these differently. +pub(crate) fn strip_dir_suffix(path: &Path) -> impl Deref + '_ { + let mut wide: Vec = path.as_os_str().encode_wide().collect(); + while wide.len() > 1 + && (*wide.last().unwrap() == '/' as u16 || *wide.last().unwrap() == '\\' as u16) + { + wide.pop(); + } + PathBuf::from(OsString::from_wide(&wide)) +} + +/// Return an `OpenOptions` for opening directories. +pub(crate) fn dir_options() -> OpenOptions { + // Set `FILE_FLAG_BACKUP_SEMANTICS` so that we can open directories. Unset + // `FILE_SHARE_DELETE` so that directories can't be renamed or deleted + // underneath us, since we use paths to implement many directory operations. + OpenOptions::new() + .read(true) + .dir_required(true) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) + .clone() +} + +/// Like `dir_options`, but additionally request the ability to read the +/// directory entries. +pub(crate) fn readdir_options() -> OpenOptions { + dir_options().readdir_required(true).clone() +} + +/// Open a directory named by a bare path, using the host process' ambient +/// authority. +/// +/// # Ambient Authority +/// +/// This function is not sandboxed and may trivially access any path that the +/// host process has access to. +pub(crate) fn open_ambient_dir_impl(path: &Path) -> io::Result { + use std::os::windows::fs::OpenOptionsExt; + + // Set `FILE_FLAG_BACKUP_SEMANTICS` so that we can open directories. Unset + // `FILE_SHARE_DELETE` so that directories can't be renamed or deleted + // underneath us, since we use paths to implement many directory operations. + let dir = fs::OpenOptions::new() + .read(true) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) + .open(path)?; + + // Require a directory. It may seem possible to eliminate this `metadata()` + // call by appending a slash to the path before opening it so that the OS + // requires a directory for us, however on Windows in some circumstances + // this leads to "The filename, directory name, or volume label syntax is + // incorrect." errors. + if !dir.metadata()?.is_dir() { + return Err(errors::is_not_directory()); + } + + Ok(dir) +} + +#[test] +fn strip_dir_suffix_tests() { + assert_eq!(&*strip_dir_suffix(Path::new("/foo//")), Path::new("/foo")); + assert_eq!(&*strip_dir_suffix(Path::new("/foo/")), Path::new("/foo")); + assert_eq!(&*strip_dir_suffix(Path::new("foo/")), Path::new("foo")); + assert_eq!(&*strip_dir_suffix(Path::new("foo")), Path::new("foo")); + assert_eq!(&*strip_dir_suffix(Path::new("/")), Path::new("/")); + assert_eq!(&*strip_dir_suffix(Path::new("//")), Path::new("/")); + + assert_eq!( + &*strip_dir_suffix(Path::new("\\foo\\\\")), + Path::new("\\foo") + ); + assert_eq!(&*strip_dir_suffix(Path::new("\\foo\\")), Path::new("\\foo")); + assert_eq!(&*strip_dir_suffix(Path::new("foo\\")), Path::new("foo")); + assert_eq!(&*strip_dir_suffix(Path::new("foo")), Path::new("foo")); + assert_eq!(&*strip_dir_suffix(Path::new("\\")), Path::new("\\")); + assert_eq!(&*strip_dir_suffix(Path::new("\\\\")), Path::new("\\")); +} + +#[test] +fn test_path_requires_dir() { + assert!(!path_requires_dir(Path::new("."))); + assert!(path_requires_dir(Path::new("/"))); + assert!(path_requires_dir(Path::new("//"))); + assert!(path_requires_dir(Path::new("/./."))); + assert!(path_requires_dir(Path::new("foo/"))); + assert!(path_requires_dir(Path::new("foo//"))); + assert!(path_requires_dir(Path::new("foo//."))); + assert!(path_requires_dir(Path::new("foo/./."))); + assert!(path_requires_dir(Path::new("foo/./"))); + assert!(path_requires_dir(Path::new("foo/.//"))); + + assert!(path_requires_dir(Path::new("\\"))); + assert!(path_requires_dir(Path::new("\\\\"))); + assert!(path_requires_dir(Path::new("\\.\\."))); + assert!(path_requires_dir(Path::new("foo\\"))); + assert!(path_requires_dir(Path::new("foo\\\\"))); + assert!(path_requires_dir(Path::new("foo\\\\."))); + assert!(path_requires_dir(Path::new("foo\\.\\."))); + assert!(path_requires_dir(Path::new("foo\\.\\"))); + assert!(path_requires_dir(Path::new("foo\\.\\\\"))); +} + +#[test] +fn test_path_has_trailing_slash() { + assert!(path_has_trailing_slash(Path::new("/"))); + assert!(path_has_trailing_slash(Path::new("//"))); + assert!(path_has_trailing_slash(Path::new("foo/"))); + assert!(path_has_trailing_slash(Path::new("foo//"))); + assert!(path_has_trailing_slash(Path::new("foo/./"))); + assert!(path_has_trailing_slash(Path::new("foo/.//"))); + + assert!(path_has_trailing_slash(Path::new("\\"))); + assert!(path_has_trailing_slash(Path::new("\\\\"))); + assert!(path_has_trailing_slash(Path::new("foo\\"))); + assert!(path_has_trailing_slash(Path::new("foo\\\\"))); + assert!(path_has_trailing_slash(Path::new("foo\\.\\"))); + assert!(path_has_trailing_slash(Path::new("foo\\.\\\\"))); + + assert!(!path_has_trailing_slash(Path::new("foo"))); + assert!(!path_has_trailing_slash(Path::new("foo."))); + + assert!(!path_has_trailing_slash(Path::new("/./foo"))); + assert!(!path_has_trailing_slash(Path::new(".."))); + assert!(!path_has_trailing_slash(Path::new("/.."))); + + assert!(!path_has_trailing_slash(Path::new("\\.\\foo"))); + assert!(!path_has_trailing_slash(Path::new(".."))); + assert!(!path_has_trailing_slash(Path::new("\\.."))); + + assert!(!path_has_trailing_slash(Path::new("/./."))); + assert!(!path_has_trailing_slash(Path::new("foo//."))); + assert!(!path_has_trailing_slash(Path::new("foo/./."))); + + assert!(!path_has_trailing_slash(Path::new("."))); + + assert!(!path_has_trailing_slash(Path::new("\\.\\."))); + assert!(!path_has_trailing_slash(Path::new("foo\\\\."))); + assert!(!path_has_trailing_slash(Path::new("foo\\.\\."))); +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/errors.rs b/crates/wasi/src/filesystem/primitives/windows/fs/errors.rs new file mode 100644 index 000000000000..f4bffc9868ab --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/errors.rs @@ -0,0 +1,22 @@ +use std::io; +use windows_sys::Win32::Foundation; + +#[cold] +pub(crate) fn no_such_file_or_directory() -> io::Error { + io::Error::from_raw_os_error(Foundation::ERROR_FILE_NOT_FOUND as i32) +} + +#[cold] +pub(crate) fn is_directory() -> io::Error { + io::Error::from_raw_os_error(Foundation::ERROR_DIRECTORY_NOT_SUPPORTED as i32) +} + +#[cold] +pub(crate) fn is_not_directory() -> io::Error { + io::Error::from_raw_os_error(Foundation::ERROR_DIRECTORY as i32) +} + +#[cold] +pub(crate) fn too_many_symlinks() -> io::Error { + io::Error::from_raw_os_error(Foundation::ERROR_TOO_MANY_LINKS as i32) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/file_type_ext.rs b/crates/wasi/src/filesystem/primitives/windows/fs/file_type_ext.rs new file mode 100644 index 000000000000..084b805dc188 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/file_type_ext.rs @@ -0,0 +1,92 @@ +use crate::filesystem::primitives::FileType; +use std::{fs, io}; + +/// A type that implements `FileTypeExt` for this platform. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub(crate) enum ImplFileTypeExt { + CharacterDevice, + Fifo, + SymlinkUnknown, +} + +impl ImplFileTypeExt { + /// Constructs a new instance of `Self` from the given [`std::fs::File`] + /// and [`std::fs::Metadata`]. + pub(crate) fn from(file: &fs::File, metadata: &fs::Metadata) -> io::Result { + // Check for the things we can do with just metadata. + let file_type = Self::from_just_metadata(metadata); + if file_type != FileType::unknown() { + return Ok(file_type); + } + + // Use the open file to check for one of the exotic file types. + let file_type = winx::winapi_util::file::typ(file)?; + if file_type.is_char() { + return Ok(FileType::ext(ImplFileTypeExt::CharacterDevice)); + } + if file_type.is_pipe() { + return Ok(FileType::ext(ImplFileTypeExt::Fifo)); + } + + Ok(FileType::unknown()) + } + + /// Constructs a new instance of `Self` from the given + /// [`std::fs::Metadata`]. + #[inline] + pub(crate) fn from_just_metadata(metadata: &fs::Metadata) -> FileType { + let std = metadata.file_type(); + Self::from_std(std) + } + + /// Constructs a new instance of `Self` from the given + /// [`std::fs::FileType`]. + #[inline] + pub(crate) fn from_std(std: fs::FileType) -> FileType { + if std.is_file() { + return FileType::file(); + } + if std.is_dir() { + return FileType::dir(); + } + + if std.is_symlink() { + return FileType::ext(Self::SymlinkUnknown); + } + + FileType::unknown() + } + + #[inline] + pub(crate) fn is_symlink(&self) -> bool { + match self { + Self::SymlinkUnknown => true, + _ => false, + } + } +} + +#[doc(hidden)] +impl crate::filesystem::primitives::_WindowsFileTypeExt + for crate::filesystem::primitives::FileType +{ + #[inline] + fn is_block_device(&self) -> bool { + false + } + + #[inline] + fn is_char_device(&self) -> bool { + *self == FileType::ext(ImplFileTypeExt::CharacterDevice) + } + + #[inline] + fn is_fifo(&self) -> bool { + *self == FileType::ext(ImplFileTypeExt::Fifo) + } + + #[inline] + fn is_socket(&self) -> bool { + false + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/get_path.rs b/crates/wasi/src/filesystem/primitives/windows/fs/get_path.rs new file mode 100644 index 000000000000..c2b44ad8837d --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/get_path.rs @@ -0,0 +1,31 @@ +use std::ffi::OsString; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// Calculates system path of `file`. +/// +/// This function will automatically strip the extended prefix from the +/// resultant path to allow for joining this resultant path with relative +/// components. +pub(crate) fn get_path(file: &fs::File) -> io::Result { + // get system path to the handle + let path = winx::file::get_file_path(file)?; + + // strip extended prefix; otherwise we will error out on any relative + // components with `out_path` + let wide: Vec<_> = path.as_os_str().encode_wide().collect(); + let wide_final = if wide.starts_with(&['\\' as u16, '\\' as _, '?' as _, '\\' as _]) { + &wide[4..] + } else { + &wide + }; + Ok(PathBuf::from(OsString::from_wide(wide_final))) +} + +/// Convenience function for calling `get_path` and concatenating the result +/// with `path`. +pub(super) fn concatenate(file: &fs::File, path: &Path) -> io::Result { + let file_path = get_path(file)?; + Ok(file_path.join(path)) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/hard_link_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/hard_link_unchecked.rs new file mode 100644 index 000000000000..68408d06c0d1 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/hard_link_unchecked.rs @@ -0,0 +1,16 @@ +use super::get_path::concatenate; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `hard_link`, but which does not perform +/// sandboxing. +pub(crate) fn hard_link_unchecked( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + let old_full_path = concatenate(old_start, old_path)?; + let new_full_path = concatenate(new_start, new_path)?; + fs::hard_link(old_full_path, new_full_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/metadata_ext.rs b/crates/wasi/src/filesystem/primitives/windows/fs/metadata_ext.rs new file mode 100644 index 000000000000..4aaf4cb0f521 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/metadata_ext.rs @@ -0,0 +1,65 @@ +#![allow(clippy::useless_conversion)] + +use crate::filesystem::primitives::MetadataExt; +use std::{fs, io}; + +#[derive(Debug, Clone)] +pub(crate) struct ImplMetadataExt { + file_attributes: u32, + number_of_links: Option, +} + +impl ImplMetadataExt { + /// Constructs a new instance of `Self` from the given [`std::fs::File`] + /// and [`std::fs::Metadata`]. + #[inline] + pub(crate) fn from(file: &fs::File, std: &fs::Metadata) -> io::Result { + let fileinfo = winx::winapi_util::file::information(file)?; + let t64: u64 = fileinfo.number_of_links(); + let t32: u32 = t64.try_into().unwrap(); + + Ok(Self::from_parts(std, Some(t32))) + } + + /// Constructs a new instance of `Self` from the given + /// [`std::fs::Metadata`]. + /// + /// As with the comments in [`std::fs::Metadata::volume_serial_number`] and + /// nearby functions, some fields of the resulting metadata will be `None`. + /// + /// [`std::fs::Metadata::volume_serial_number`]: https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html#tymethod.volume_serial_number + #[inline] + pub(crate) fn from_just_metadata(std: &fs::Metadata) -> Self { + Self::from_parts(std, None) + } + + #[inline] + fn from_parts(std: &fs::Metadata, number_of_links: Option) -> Self { + use std::os::windows::fs::MetadataExt; + Self { + file_attributes: std.file_attributes(), + number_of_links, + } + } + + /// `MetadataExt` requires nightly to be implemented, but we sometimes + /// just need the file attributes. + #[inline] + pub(crate) fn file_attributes(&self) -> u32 { + self.file_attributes + } +} + +impl MetadataExt for ImplMetadataExt { + fn file_attributes(&self) -> u32 { + self.file_attributes + } +} + +#[doc(hidden)] +impl crate::filesystem::primitives::_WindowsByHandle for crate::filesystem::primitives::Metadata { + #[inline] + fn number_of_links(&self) -> Option { + self.ext.number_of_links + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/mod.rs b/crates/wasi/src/filesystem/primitives/windows/fs/mod.rs new file mode 100644 index 000000000000..75ff00fbe8ba --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/mod.rs @@ -0,0 +1,66 @@ +mod create_dir_unchecked; +mod create_file_at_w; +mod dir_entry_inner; +mod dir_options_ext; +mod dir_utils; +mod file_type_ext; +mod get_path; +mod hard_link_unchecked; +mod metadata_ext; +mod oflags; +mod open_impl; +mod open_options_ext; +mod open_unchecked; +mod read_dir_inner; +mod read_link_impl; +mod read_link_unchecked; +mod remove_dir_unchecked; +mod remove_file_unchecked; +mod rename_unchecked; +mod set_times_impl; +mod stat_unchecked; +mod symlink_unchecked; + +pub(crate) mod errors; + +#[rustfmt::skip] +pub(crate) use crate::filesystem::primitives::{ + via_parent::hard_link as hard_link_impl, + via_parent::create_dir as create_dir_impl, + via_parent::rename as rename_impl, + via_parent::remove_dir as remove_dir_impl, + manually::stat as stat_impl, + via_parent::symlink_dir as symlink_dir_impl, + via_parent::symlink_file as symlink_file_impl, + via_parent::remove_file as remove_file_impl, +}; + +pub(crate) use create_dir_unchecked::*; +pub(crate) use dir_entry_inner::*; +pub(crate) use dir_options_ext::*; +pub(crate) use dir_utils::*; +pub(crate) use file_type_ext::*; +pub(crate) use hard_link_unchecked::*; +pub(crate) use metadata_ext::*; +pub(crate) use open_impl::open_impl; +pub(crate) use open_options_ext::*; +pub(crate) use open_unchecked::*; +pub(crate) use read_dir_inner::*; +pub(crate) use read_link_impl::*; +pub(crate) use read_link_unchecked::*; +pub(crate) use remove_dir_unchecked::*; +pub(crate) use remove_file_unchecked::*; +pub(crate) use rename_unchecked::*; +pub(crate) use set_times_impl::*; +pub(crate) use stat_unchecked::*; +pub(crate) use symlink_unchecked::*; + +// On Windows, there is a limit of 63 reparse points on any given path. +// +pub(crate) const MAX_SYMLINK_EXPANSIONS: u8 = 63; + +pub(crate) fn file_path(file: &std::fs::File) -> Option { + get_path::get_path(file).ok() +} + +pub(super) use oflags::*; diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/oflags.rs b/crates/wasi/src/filesystem/primitives/windows/fs/oflags.rs new file mode 100644 index 000000000000..30d80197456c --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/oflags.rs @@ -0,0 +1,48 @@ +use crate::filesystem::primitives::{FollowSymlinks, OpenOptions, OpenOptionsExt}; +use windows_sys::Win32::Storage::FileSystem::{ + FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_WRITE_THROUGH, + FILE_SHARE_DELETE, +}; + +/// Adjust an `OpenOptions` after all the flags are set, in preparation +/// for the to call a Windows API `open` function. Also return a bool +/// indicating that the `trunc` flag was requested but could not be set, +/// so the file should be truncated manually after opening. +pub(in super::super) fn prepare_open_options_for_open(opts: &mut OpenOptions) -> bool { + let mut trunc = opts.truncate; + let mut manually_trunc = false; + + let mut custom_flags = match opts.follow { + FollowSymlinks::Yes => opts.ext.custom_flags, + FollowSymlinks::No => { + if trunc && !opts.create_new && !opts.append && opts.write { + // On Windows, truncating overwrites a symlink with a + // non-symlink. + manually_trunc = true; + trunc = false; + } + opts.ext.custom_flags | FILE_FLAG_OPEN_REPARSE_POINT + } + }; + let mut share_mode = opts.ext.share_mode; + if opts.maybe_dir { + custom_flags |= FILE_FLAG_BACKUP_SEMANTICS; + + // Only allow `FILE_SHARE_READ` and `FILE_SHARE_WRITE`; this mirrors + // the values in `dir_options()` and is done to prevent directories + // from being deleted or renamed underneath cap-std's sandboxed path + // lookups on Windows. + share_mode &= !FILE_SHARE_DELETE; + } + // This matches system-interface's `set_fd_flags` interpretation of these + // flags on Windows. + if opts.sync || opts.dsync { + custom_flags |= FILE_FLAG_WRITE_THROUGH; + } + + opts.truncate(trunc) + .share_mode(share_mode) + .custom_flags(custom_flags); + + manually_trunc +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/open_impl.rs b/crates/wasi/src/filesystem/primitives/windows/fs/open_impl.rs new file mode 100644 index 000000000000..95c94f710a1b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/open_impl.rs @@ -0,0 +1,66 @@ +use crate::filesystem::primitives::{OpenOptions, manually}; +use std::ffi::OsStr; +use std::path::Path; +use std::{fs, io}; +use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND; + +pub(crate) fn open_impl( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> io::Result { + // Windows reserves several special device paths. Disallow opening any + // of them. + // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + if let Some(stem) = file_prefix(path) { + if let Some(stemstr) = stem.to_str() { + match stemstr.trim_end().to_uppercase().as_str() { + "CON" | "PRN" | "AUX" | "NUL" | "COM0" | "COM1" | "COM2" | "COM3" | "COM4" + | "COM5" | "COM6" | "COM7" | "COM8" | "COM9" | "COM¹" | "COM²" | "COM³" + | "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" + | "LPT8" | "LPT9" | "LPT¹" | "LPT²" | "LPT³" => { + return Err(io::Error::from_raw_os_error(ERROR_FILE_NOT_FOUND as i32)); + } + _ => {} + } + } + } + + manually::open(start, path, options) +} + +// TODO: Replace this with `Path::file_prefix` once that's stable. For now, +// we use a copy of the code. This code is derived from +// https://github.com/rust-lang/rust/blob/9fe9041cc8eddaed402d17aa4facb2ce8f222e95/library/std/src/path.rs#L2648 +fn file_prefix(path: &Path) -> Option<&OsStr> { + path.file_name() + .map(split_file_at_dot) + .and_then(|(before, _after)| Some(before)) +} + +// This code is derived from +// https://github.com/rust-lang/rust/blob/9fe9041cc8eddaed402d17aa4facb2ce8f222e95/library/std/src/path.rs#L340 +#[allow(unsafe_code)] +fn split_file_at_dot(file: &OsStr) -> (&OsStr, Option<&OsStr>) { + let slice = file.as_encoded_bytes(); + if slice == b".." { + return (file, None); + } + + // The unsafety here stems from converting between &OsStr and &[u8] + // and back. This is safe to do because (1) we only look at ASCII + // contents of the encoding and (2) new &OsStr values are produced + // only from ASCII-bounded slices of existing &OsStr values. + let i = match slice[1..].iter().position(|b| *b == b'.') { + Some(i) => i + 1, + None => return (file, None), + }; + let before = &slice[..i]; + let after = &slice[i + 1..]; + unsafe { + ( + OsStr::from_encoded_bytes_unchecked(before), + Some(OsStr::from_encoded_bytes_unchecked(after)), + ) + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/open_options_ext.rs b/crates/wasi/src/filesystem/primitives/windows/fs/open_options_ext.rs new file mode 100644 index 000000000000..af8361d463ea --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/open_options_ext.rs @@ -0,0 +1,111 @@ +#![allow(unsafe_code)] + +use crate::filesystem::primitives::OpenOptions; +use std::io; +use std::ptr::null_mut; +use windows_sys::Win32::Foundation::{ERROR_INVALID_PARAMETER, GENERIC_READ, GENERIC_WRITE}; +use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; +use windows_sys::Win32::Storage::FileSystem::{ + CREATE_ALWAYS, CREATE_NEW, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_WRITE, FILE_SHARE_DELETE, + FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_WRITE_DATA, OPEN_ALWAYS, OPEN_EXISTING, + TRUNCATE_EXISTING, +}; + +#[derive(Debug, Clone)] +pub(crate) struct ImplOpenOptionsExt { + pub(super) access_mode: Option, + pub(super) share_mode: u32, + pub(super) custom_flags: u32, + pub(super) attributes: u32, + pub(super) security_attributes: *mut SECURITY_ATTRIBUTES, + pub(super) security_qos_flags: u32, +} + +unsafe impl Send for ImplOpenOptionsExt {} +unsafe impl Sync for ImplOpenOptionsExt {} + +impl ImplOpenOptionsExt { + pub(crate) const fn new() -> Self { + Self { + access_mode: None, + share_mode: FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + custom_flags: 0, + attributes: 0, + security_attributes: null_mut(), + security_qos_flags: 0, + } + } + + pub(crate) fn access_mode(&mut self, mode: u32) -> &mut Self { + self.access_mode = Some(mode); + self + } + + pub(crate) fn share_mode(&mut self, share: u32) -> &mut Self { + self.share_mode = share; + self + } + + pub(crate) fn custom_flags(&mut self, flags: u32) -> &mut Self { + self.custom_flags = flags; + self + } +} + +pub(crate) fn get_access_mode(options: &OpenOptions) -> io::Result { + match ( + options.read, + options.write, + options.append, + options.ext.access_mode, + ) { + (.., Some(mode)) => Ok(mode), + (true, false, false, None) => Ok(GENERIC_READ), + (false, true, false, None) => Ok(GENERIC_WRITE), + (true, true, false, None) => Ok(GENERIC_READ | GENERIC_WRITE), + (false, _, true, None) => Ok(FILE_GENERIC_WRITE & !FILE_WRITE_DATA), + (true, _, true, None) => Ok(GENERIC_READ | (FILE_GENERIC_WRITE & !FILE_WRITE_DATA)), + (false, false, false, None) => { + Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)) + } + } +} + +pub(crate) fn get_flags_and_attributes(options: &OpenOptions) -> u32 { + options.ext.custom_flags + | options.ext.attributes + | options.ext.security_qos_flags + | if options.create_new { + FILE_FLAG_OPEN_REPARSE_POINT + } else { + 0 + } +} + +pub(crate) fn get_creation_mode(options: &OpenOptions) -> io::Result { + const ERROR_INVALID_PARAMETER: i32 = 87; + + match (options.write, options.append) { + (true, false) => {} + (false, false) => { + if options.truncate || options.create || options.create_new { + return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)); + } + } + (_, true) => { + if options.truncate && !options.create_new { + return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)); + } + } + } + + Ok( + match (options.create, options.truncate, options.create_new) { + (false, false, false) => OPEN_EXISTING, + (true, false, false) => OPEN_ALWAYS, + (false, true, false) => TRUNCATE_EXISTING, + (true, true, false) => CREATE_ALWAYS, + (_, _, true) => CREATE_NEW, + }, + ) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/open_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/open_unchecked.rs new file mode 100644 index 000000000000..9adb1ec84872 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/open_unchecked.rs @@ -0,0 +1,215 @@ +//! Windows implementation of `openat` functionality. + +#![allow(unsafe_code)] + +use super::create_file_at_w::CreateFileAtW; +use super::prepare_open_options_for_open; +use crate::filesystem::primitives::{ + FollowSymlinks, OpenOptions, OpenUncheckedError, SymlinkKind, errors, file_path, + get_access_mode, get_creation_mode, get_flags_and_attributes, +}; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::fs::MetadataExt; +use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; +use std::path::{Component, Path, PathBuf}; +use std::{fs, io}; +use windows_sys::Win32::Foundation::{self, ERROR_ACCESS_DENIED, HANDLE, INVALID_HANDLE_VALUE}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_OPEN_REPARSE_POINT, +}; + +/// *Unsandboxed* function similar to `open`, but which does not perform +/// sandboxing. +pub(crate) fn open_unchecked( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> Result { + // We have the final `OpenOptions`; now prepare it for an `open`. + let mut prepared_opts = options.clone(); + let manually_trunc = prepare_open_options_for_open(&mut prepared_opts); + + handle_open_result( + open_at(start, path, &prepared_opts), + options, + manually_trunc, + ) +} + +// The following is derived from Rust's library/std/src/sys/windows/fs.rs +// at revision 56888c1e9b4135b511abd2d8e907099003d12281, except with a +// directory `start` parameter added and using `CreateFileAtW` instead of +// `CreateFileW`. + +fn open_at(start: &fs::File, path: &Path, opts: &OpenOptions) -> io::Result { + let mut dir = start.as_raw_handle() as HANDLE; + + // `PathCchCanonicalizeEx` and friends don't seem to work with relative + // paths. Or at least, when I tried it, they canonicalized "a" to "", + // which isn't what we want. So we manually canonicalize `..` and `.`. + // Hopefully there aren't other mysterious Windows path conventions that + // we're missing here. + let mut rebuilt = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir => { + rebuilt.push(component); + dir = 0 as HANDLE; + } + Component::Normal(_) => { + rebuilt.push(component); + } + Component::ParentDir => { + if !rebuilt.pop() { + // We popped past the beginning of `path`. Substitute in + // the path of `start` and convert this to an ambient + // path by dropping the directory base. It's ok to do + // this because we're not sandboxing at this level of the + // code. + if dir == 0 as HANDLE { + return Err(io::Error::from_raw_os_error(ERROR_ACCESS_DENIED as _)); + } + rebuilt = match file_path(start) { + Some(path) => path, + None => { + return Err(io::Error::from_raw_os_error(ERROR_ACCESS_DENIED as _)); + } + }; + dir = 0 as HANDLE; + // And then pop the last component of that. + let _ = rebuilt.pop(); + } + } + Component::CurDir => (), + } + } + + let mut wide = OsStr::encode_wide(rebuilt.as_os_str()).collect::>(); + + // If we ended up re-rooting, use Windows' `CreateFileW` instead of our + // own `CreateFileAtW` so that it does the requisite magic for absolute + // paths. + if dir == 0 as HANDLE { + // We're calling the windows-sys `CreateFileW` which expects a + // NUL-terminated filename, so add a NUL terminator. + wide.push(0); + + let handle = unsafe { + CreateFileW( + wide.as_ptr(), + get_access_mode(opts)?, + opts.ext.share_mode, + opts.ext.security_attributes, + get_creation_mode(opts)?, + get_flags_and_attributes(opts), + 0 as HANDLE, + ) + }; + if handle != INVALID_HANDLE_VALUE { + Ok(unsafe { fs::File::from_raw_handle(handle as _) }) + } else { + Err(io::Error::last_os_error()) + } + } else { + // Our own `CreateFileAtW` is similar to `CreateFileW` except it + // takes the filename as a Rust slice directly, so we can skip + // the NUL terminator. + let handle = unsafe { + CreateFileAtW( + dir, + &wide, + get_access_mode(opts)?, + opts.ext.share_mode, + opts.ext.security_attributes, + get_creation_mode(opts)?, + get_flags_and_attributes(opts), + 0 as HANDLE, + ) + }; + + if let Ok(handle) = handle.try_into() { + Ok(>::from(handle)) + } else { + Err(io::Error::last_os_error()) + } + } +} + +fn handle_open_result( + result: io::Result, + options: &OpenOptions, + manually_trunc: bool, +) -> Result { + match result { + Ok(f) => { + let enforce_dir = options.dir_required; + let enforce_nofollow = options.follow == FollowSymlinks::No + && (options.ext.custom_flags & FILE_FLAG_OPEN_REPARSE_POINT) == 0; + + if enforce_dir || enforce_nofollow { + let metadata = f.metadata().map_err(OpenUncheckedError::Other)?; + + if enforce_dir { + // Require a directory. It may seem possible to eliminate + // this `metadata()` call by appending a slash to the path + // before opening it so that the OS requires a directory + // for us, however on Windows in some circumstances this + // leads to "The filename, directory name, or volume label + // syntax is incorrect." errors. + // + // We check `file_attributes()` instead of using `is_dir()` + // since the latter returns false if we're looking at a + // directory symlink. + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY == 0 { + return Err(OpenUncheckedError::Other(errors::is_not_directory())); + } + } + + if enforce_nofollow { + // Windows doesn't have a way to return errors like + // `O_NOFOLLOW`, so if we're not following symlinks and + // we're not using `FILE_FLAG_OPEN_REPARSE_POINT` manually + // to open a symlink itself, check for symlinks and report + // them as a distinct error. + if metadata.file_type().is_symlink() { + return Err(OpenUncheckedError::Symlink( + io::Error::from_raw_os_error( + Foundation::ERROR_STOPPED_ON_SYMLINK as i32, + ), + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY + == FILE_ATTRIBUTE_DIRECTORY + { + SymlinkKind::Dir + } else { + SymlinkKind::File + }, + )); + } + } + } + + // Windows truncates symlinks into normal files, so truncation + // may be disabled above; do it manually if needed. Note that this + // is expected to always succeed for normal files, but this will + // fail if a directory was opened as directories don't support + // truncation. + if manually_trunc { + if let Err(e) = f.set_len(0) { + return Err(OpenUncheckedError::Other(e)); + } + } + Ok(f) + } + Err(e) if e.kind() == io::ErrorKind::NotFound => Err(OpenUncheckedError::NotFound(e)), + Err(e) => match e.raw_os_error() { + Some(code) => match code as u32 { + Foundation::ERROR_FILE_NOT_FOUND | Foundation::ERROR_PATH_NOT_FOUND => { + Err(OpenUncheckedError::NotFound(e)) + } + _ => Err(OpenUncheckedError::Other(e)), + }, + None => Err(OpenUncheckedError::Other(e)), + }, + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/read_dir_inner.rs b/crates/wasi/src/filesystem/primitives/windows/fs/read_dir_inner.rs new file mode 100644 index 000000000000..5d8941804cf6 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/read_dir_inner.rs @@ -0,0 +1,41 @@ +use super::get_path::concatenate; +use crate::filesystem::primitives::DirEntryInner; +use std::path::{Component, Path}; +use std::{fmt, fs, io}; + +pub(crate) struct ReadDirInner { + std: fs::ReadDir, +} + +impl ReadDirInner { + pub(crate) fn read_base_dir(start: &fs::File) -> io::Result { + Self::new_unchecked(&start, Component::CurDir.as_ref()) + } + + pub(crate) fn new_unchecked(start: &fs::File, path: &Path) -> io::Result { + let full_path = concatenate(start, path)?; + Ok(Self { + std: fs::read_dir(full_path)?, + }) + } +} + +impl Iterator for ReadDirInner { + type Item = io::Result; + + fn next(&mut self) -> Option { + self.std + .next() + .map(|result| result.map(DirEntryInner::from_std)) + } +} + +impl fmt::Debug for ReadDirInner { + // Like libstd's version, but doesn't print the path. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut b = f.debug_struct("ReadDir"); + // `fs::ReadDir`'s `Debug` just prints the path, and since we're not + // printing that, we don't have anything else to print. + b.finish() + } +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/read_link_impl.rs b/crates/wasi/src/filesystem/primitives/windows/fs/read_link_impl.rs new file mode 100644 index 000000000000..b93086268a29 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/read_link_impl.rs @@ -0,0 +1,20 @@ +use crate::filesystem::primitives::{FollowSymlinks, OpenOptions, OpenOptionsExt, open}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; +use windows_sys::Win32::Storage::FileSystem::{ + FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, +}; + +/// *Unsandboxed* function similar to `read_link`, but which does not perform +/// sandboxing. +pub(crate) fn read_link_impl(start: &fs::File, path: &Path) -> io::Result { + // Open the link with no access mode, instead of generic read. + // By default FILE_LIST_DIRECTORY is denied for the junction "C:\Documents and + // Settings", so this is needed for a common case. + let mut opts = OpenOptions::new(); + opts.access_mode(0); + opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS); + opts.follow(FollowSymlinks::No); + let file = open(start, path, &opts)?; + winx::file::read_link(&file) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/read_link_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/read_link_unchecked.rs new file mode 100644 index 000000000000..5d5bf218f19f --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/read_link_unchecked.rs @@ -0,0 +1,14 @@ +use super::get_path::concatenate; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `read_link`, but which does not perform +/// sandboxing. +pub(crate) fn read_link_unchecked( + start: &fs::File, + path: &Path, + _reuse: PathBuf, +) -> io::Result { + let full_path = concatenate(start, path)?; + fs::read_link(full_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/remove_dir_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/remove_dir_unchecked.rs new file mode 100644 index 000000000000..3b80cac3e913 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/remove_dir_unchecked.rs @@ -0,0 +1,10 @@ +use super::get_path::concatenate; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `remove_dir`, but which does not perform +/// sandboxing. +pub(crate) fn remove_dir_unchecked(start: &fs::File, path: &Path) -> io::Result<()> { + let full_path = concatenate(start, path)?; + fs::remove_dir(full_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/remove_file_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/remove_file_unchecked.rs new file mode 100644 index 000000000000..9486e8d5e399 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/remove_file_unchecked.rs @@ -0,0 +1,10 @@ +use super::get_path::concatenate; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `remove_file`, but which does not perform +/// sandboxing. +pub(crate) fn remove_file_unchecked(start: &fs::File, path: &Path) -> io::Result<()> { + let full_path = concatenate(start, path)?; + fs::remove_file(full_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/rename_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/rename_unchecked.rs new file mode 100644 index 000000000000..5afbdb5c5b53 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/rename_unchecked.rs @@ -0,0 +1,16 @@ +use super::get_path::concatenate; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `rename`, but which does not perform +/// sandboxing. +pub(crate) fn rename_unchecked( + old_start: &fs::File, + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + let old_full_path = concatenate(old_start, old_path)?; + let new_full_path = concatenate(new_start, new_path)?; + fs::rename(old_full_path, new_full_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/set_times_impl.rs b/crates/wasi/src/filesystem/primitives/windows/fs/set_times_impl.rs new file mode 100644 index 000000000000..c0acd4c9cc9b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/set_times_impl.rs @@ -0,0 +1,52 @@ +use crate::filesystem::primitives::{OpenOptions, OpenOptionsExt, open}; +use std::path::Path; +use std::time::SystemTime; +use std::{fs, io}; +use windows_sys::Win32::Storage::FileSystem::{ + FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, +}; + +#[inline] +pub(crate) fn set_times_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + set_times_inner(start, path, atime, mtime, 0) +} + +#[inline] +pub(crate) fn set_times_nofollow_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + set_times_inner(start, path, atime, mtime, FILE_FLAG_OPEN_REPARSE_POINT) +} + +fn set_times_inner( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, + custom_flags: u32, +) -> io::Result<()> { + let custom_flags = custom_flags | FILE_FLAG_BACKUP_SEMANTICS; + + // On Windows, `set_times` requires write permissions. + let file = open( + start, + path, + OpenOptions::new().write(true).custom_flags(custom_flags), + )?; + let mut times = fs::FileTimes::new(); + if let Some(atime) = atime { + times = times.set_accessed(atime); + } + if let Some(mtime) = mtime { + times = times.set_modified(mtime); + } + file.set_times(times) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/stat_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/stat_unchecked.rs new file mode 100644 index 000000000000..b39ce5a44db4 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/stat_unchecked.rs @@ -0,0 +1,36 @@ +use crate::filesystem::primitives::OpenOptionsExt; +use crate::filesystem::primitives::{FollowSymlinks, Metadata, OpenOptions, open_unchecked}; +use std::path::Path; +use std::{fs, io}; +use windows_sys::Win32::Storage::FileSystem::{ + FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, +}; + +/// *Unsandboxed* function similar to `stat`, but which does not perform +/// sandboxing. +pub(crate) fn stat_unchecked( + start: &fs::File, + path: &Path, + follow: FollowSymlinks, +) -> io::Result { + // Attempt to open the file to get the metadata that way, as that gives + // us all the info. + let mut opts = OpenOptions::new(); + + // Explicitly request no access, because we're just querying metadata. + opts.access_mode(0); + + match follow { + FollowSymlinks::Yes => { + opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); + opts.follow(FollowSymlinks::Yes); + } + FollowSymlinks::No => { + opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS); + opts.follow(FollowSymlinks::No); + } + } + + let file = open_unchecked(start, path, &opts)?; + Metadata::from_file(&file) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/fs/symlink_unchecked.rs b/crates/wasi/src/filesystem/primitives/windows/fs/symlink_unchecked.rs new file mode 100644 index 000000000000..73c6794da31b --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/fs/symlink_unchecked.rs @@ -0,0 +1,25 @@ +use super::get_path::concatenate; +use std::path::Path; +use std::{fs, io}; + +/// *Unsandboxed* function similar to `symlink_file`, but which does not +/// perform sandboxing. +pub(crate) fn symlink_file_unchecked( + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + let new_full_path = concatenate(new_start, new_path)?; + std::os::windows::fs::symlink_file(old_path, new_full_path) +} + +/// *Unsandboxed* function similar to `symlink_dir`, but which does not perform +/// sandboxing. +pub(crate) fn symlink_dir_unchecked( + old_path: &Path, + new_start: &fs::File, + new_path: &Path, +) -> io::Result<()> { + let new_full_path = concatenate(new_start, new_path)?; + std::os::windows::fs::symlink_dir(old_path, new_full_path) +} diff --git a/crates/wasi/src/filesystem/primitives/windows/mod.rs b/crates/wasi/src/filesystem/primitives/windows/mod.rs new file mode 100644 index 000000000000..5553d02ec536 --- /dev/null +++ b/crates/wasi/src/filesystem/primitives/windows/mod.rs @@ -0,0 +1,4 @@ +//! The `winx` module contains code specific to Windows, supported by the +//! `winx` crate. + +pub(crate) mod fs; diff --git a/crates/wasi/src/filesystem/unix.rs b/crates/wasi/src/filesystem/unix.rs index ade351c2a643..9a14680d5c32 100644 --- a/crates/wasi/src/filesystem/unix.rs +++ b/crates/wasi/src/filesystem/unix.rs @@ -1,9 +1,9 @@ +use crate::filesystem::primitives::{ + FileType, FileTypeExt, FollowSymlinks, Metadata, MetadataExt, OpenOptions, +}; use crate::filesystem::{ Advice, DescriptorFlags, DescriptorStat, DescriptorType, MetadataHashValue, }; -use cap_primitives::fs::{ - FileType, FileTypeExt, FollowSymlinks, Metadata, MetadataExt, OpenOptions, -}; use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl}; use rustix::io::write; use std::fs::File; @@ -11,8 +11,8 @@ use std::io; use std::os::unix::fs::FileExt; use std::path::Path; -pub use cap_primitives::fs::remove_file as remove_file_or_symlink; -pub use cap_primitives::fs::symlink; +pub use crate::filesystem::primitives::remove_file as remove_file_or_symlink; +pub use crate::filesystem::primitives::symlink; pub(crate) fn get_flags(file: &File) -> io::Result { let flags = fcntl_getfl(file)?; @@ -122,7 +122,7 @@ pub(crate) fn metadata_hash_at( path: &Path, follow: FollowSymlinks, ) -> io::Result { - let meta = cap_primitives::fs::stat(start, path, follow)?; + let meta = crate::filesystem::primitives::stat(start, path, follow)?; Ok(MetadataHashValue::new(meta_identity(&meta))) } @@ -140,7 +140,7 @@ pub(crate) fn stat_at( path: &Path, follow: FollowSymlinks, ) -> io::Result { - let meta = cap_primitives::fs::stat(start, path, follow)?; + let meta = crate::filesystem::primitives::stat(start, path, follow)?; Ok(DescriptorStat::new(&meta, meta.nlink())) } diff --git a/crates/wasi/src/filesystem/windows.rs b/crates/wasi/src/filesystem/windows.rs index f0760d85c9a0..ab5d9209b705 100644 --- a/crates/wasi/src/filesystem/windows.rs +++ b/crates/wasi/src/filesystem/windows.rs @@ -1,7 +1,9 @@ +use crate::filesystem::primitives::{ + FileType, FollowSymlinks, Metadata, OpenOptions, OpenOptionsExt, +}; use crate::filesystem::{ Advice, DescriptorFlags, DescriptorStat, DescriptorType, MetadataHashValue, }; -use cap_primitives::fs::{FileType, FollowSymlinks, Metadata, OpenOptions, OpenOptionsExt}; use std::fs::File; use std::io::{self, Write}; use std::mem::{self, MaybeUninit}; @@ -122,18 +124,15 @@ fn open_metadata_handle(start: &File, path: &Path, follow: FollowSymlinks) -> io opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT); } } - cap_primitives::fs::open(start, path, &opts) + crate::filesystem::primitives::open(start, path, &opts) } pub(crate) fn stat(f: &std::fs::File) -> io::Result { let meta = Metadata::from_file(f)?; - // Note that this is intentionally scoped to a separate block to - // minimize the surface area that is depended on by cap-fs-ext. - let link_count = { - use cap_fs_ext_avoid_using_this::MetadataExt; - meta.nlink() - }; + let link_count = crate::filesystem::primitives::_WindowsByHandle::number_of_links(&meta) + .unwrap() + .into(); Ok(DescriptorStat::new(&meta, link_count)) } @@ -179,10 +178,10 @@ fn is_char_device(ft: FileType) -> bool { } pub(crate) fn symlink(original: &Path, start: &File, link: &Path) -> io::Result<()> { - if cap_primitives::fs::stat(start, original, FollowSymlinks::Yes)?.is_dir() { - cap_primitives::fs::symlink_dir(original, start, link) + if crate::filesystem::primitives::stat(start, original, FollowSymlinks::Yes)?.is_dir() { + crate::filesystem::primitives::symlink_dir(original, start, link) } else { - cap_primitives::fs::symlink_file(original, start, link) + crate::filesystem::primitives::symlink_file(original, start, link) } } @@ -193,16 +192,17 @@ pub(crate) fn remove_file_or_symlink(start: &File, path: &Path) -> io::Result<() let mut opts = OpenOptions::new(); opts.access_mode(DELETE); opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS); - let file = cap_primitives::fs::open(start, path, &opts)?; + let file = crate::filesystem::primitives::open(start, path, &opts)?; let meta = Metadata::from_file(&file)?; if meta.file_type().is_symlink() - && cap_primitives::fs::MetadataExt::file_attributes(&meta) & FILE_ATTRIBUTE_DIRECTORY + && crate::filesystem::primitives::MetadataExt::file_attributes(&meta) + & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY { - cap_primitives::fs::remove_dir(start, path)?; + crate::filesystem::primitives::remove_dir(start, path)?; } else { - cap_primitives::fs::remove_file(start, path)?; + crate::filesystem::primitives::remove_file(start, path)?; } // Drop the file after calling `remove_file` or `remove_dir`, since diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index 6dfd0b83d63b..005d8155d5eb 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -2,10 +2,10 @@ //! # Wasmtime's WASI Implementation //! -//! This crate provides a Wasmtime host implementations of different versions of WASI. -//! WASI is implemented with the Rust crates [`tokio`] and -//! [`cap-primitives`](cap_primitives) primarily, meaning that operations are -//! implemented in terms of their native platform equivalents by default. +//! This crate provides a Wasmtime host implementations of different versions of +//! WASI. WASI is implemented with the Rust crate [`tokio`] combined with custom +//! implementations in this crate, and operations are implemented in terms of +//! their native platform equivalents by default. //! //! For components and WASIp2, see [`p2`]. //! For WASIp1 and core modules, see the [`p1`] module documentation. @@ -59,7 +59,7 @@ pub use self::view::{WasiCtxView, WasiView}; #[doc(no_inline)] pub use async_trait::async_trait; #[doc(no_inline)] -pub use cap_primitives::fs::SystemTimeSpec; +pub use public_cap_primitives::fs::SystemTimeSpec; #[doc(no_inline)] pub use rand::Rng; #[doc(no_inline)] diff --git a/crates/wasi/src/p2/host/filesystem.rs b/crates/wasi/src/p2/host/filesystem.rs index 579c74129a05..7f5ef8aec74c 100644 --- a/crates/wasi/src/p2/host/filesystem.rs +++ b/crates/wasi/src/p2/host/filesystem.rs @@ -171,7 +171,7 @@ impl HostDescriptor for WasiFilesystemCtxView<'_> { // within this `block` call, rather than delay calculating the metadata // for entries when they're demanded later in the iterator chain. Ok::<_, std::io::Error>( - cap_primitives::fs::read_base_dir(d)? + crate::filesystem::primitives::read_base_dir(d)? .map(|entry| { let entry = entry?; let meta = entry.metadata()?; @@ -713,7 +713,7 @@ impl From for ErrorCode { } } -fn descriptortype_from(ft: cap_primitives::fs::FileType) -> types::DescriptorType { +fn descriptortype_from(ft: crate::filesystem::primitives::FileType) -> types::DescriptorType { crate::filesystem::DescriptorType::from(ft).into() } diff --git a/crates/wasi/src/p3/filesystem/host.rs b/crates/wasi/src/p3/filesystem/host.rs index f71e61d810ae..6ea173bbe43b 100644 --- a/crates/wasi/src/p3/filesystem/host.rs +++ b/crates/wasi/src/p3/filesystem/host.rs @@ -243,7 +243,7 @@ impl StreamProducer for ReadStreamProducer { } fn map_dir_entry( - entry: std::io::Result, + entry: std::io::Result, ) -> Result, ErrorCode> { match entry { Ok(entry) => { @@ -289,7 +289,7 @@ impl ReadDirStream { let (tx, rx) = mpsc::channel(1); ReadDirStream { task: spawn_blocking(move || { - let entries = cap_primitives::fs::read_base_dir(&dir)?; + let entries = crate::filesystem::primitives::read_base_dir(&dir)?; for entry in entries { if let Some(entry) = map_dir_entry(entry)? { if let Err(_) = tx.blocking_send(entry) { @@ -676,7 +676,7 @@ impl types::HostDescriptorWithStore for WasiFilesystem { let allow_blocking_current_thread = dir.allow_blocking_current_thread; let dir = Arc::clone(dir.as_dir()); if allow_blocking_current_thread { - match cap_primitives::fs::read_base_dir(&dir) { + match crate::filesystem::primitives::read_base_dir(&dir) { Ok(readdir) => StreamReader::new( &mut store, FallibleIteratorProducer::new( diff --git a/crates/wasi/src/p3/filesystem/mod.rs b/crates/wasi/src/p3/filesystem/mod.rs index 11816c12ea3d..31ccb52ed67b 100644 --- a/crates/wasi/src/p3/filesystem/mod.rs +++ b/crates/wasi/src/p3/filesystem/mod.rs @@ -265,8 +265,8 @@ impl From for types::DescriptorType { } } -impl From for types::DescriptorType { - fn from(ft: cap_primitives::fs::FileType) -> Self { +impl From for types::DescriptorType { + fn from(ft: crate::filesystem::primitives::FileType) -> Self { crate::filesystem::DescriptorType::from(ft).into() } } diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 2f8b2855d62e..5532c77ffa05 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -340,13 +340,6 @@ user-id = 1 user-login = "alexcrichton" user-name = "Alex Crichton" -[[publisher.cap-fs-ext]] -version = "4.0.3" -when = "2026-08-20" -user-id = 6825 -user-login = "sunfishcode" -user-name = "Dan Gohman" - [[publisher.cap-primitives]] version = "4.0.3" when = "2026-08-20" @@ -354,13 +347,6 @@ user-id = 6825 user-login = "sunfishcode" user-name = "Dan Gohman" -[[publisher.cap-std]] -version = "4.0.3" -when = "2026-08-20" -user-id = 6825 -user-login = "sunfishcode" -user-name = "Dan Gohman" - [[publisher.cc]] version = "1.0.89" when = "2024-03-04"