diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c58b56cf..4420331f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -376,6 +376,8 @@ jobs: run: | cargo update --package=once_cell --precise=1.20.3 cargo update --package=uuid --precise=1.20.0 + cargo update --package=async-io --precise=2.5.0 + cargo update --package=polling --precise=3.10.0 # Don't use --all-features because some of the features have dependencies # that don't work on newer Rust versions. diff --git a/Cargo.toml b/Cargo.toml index b68eaac5..6858f7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ rust-version = "1.70" [dev-dependencies] anyhow = "1.0.37" +cap-tokio = { path = "cap-tokio", version = "4.0.2" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } cap-fs-ext = { path = "cap-fs-ext", version = "4.0.2" } cap-net-ext = { path = "cap-net-ext", version = "4.0.2" } cap-directories = { path = "cap-directories", version = "4.0.2" } @@ -54,14 +56,23 @@ fs_utf8 = [ "cap-fs-ext/fs_utf8", "cap-tempfile/fs_utf8", ] +async_std_fs_utf8 = [ + "cap-tokio/fs_utf8", + "cap-fs-ext/async_std_fs_utf8" +] arf_strings = [ "cap-std/arf_strings", "cap-fs-ext/arf_strings", "cap-tempfile/arf_strings", ] +async_std_arf_strings = [ + "cap-tokio/arf_strings", + "cap-fs-ext/async_std_arf_strings" +] [workspace] members = [ + "cap-tokio", "cap-fs-ext", "cap-net-ext", "cap-directories", diff --git a/README.md b/README.md index b426cff1..95269c0a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The `cap-std` project is organized around the eponymous [`cap-std`] crate, and develops libraries to make it easy to write capability-based code, including: - [`cap-std`] itself, which provides capability-based versions of `std` APIs + - [`cap-tokio`], which is to [`tokio`] what `cap-std` is to `std` - [`cap-directories`] which provides capability-based access to [standard application directories] - [`cap-tempfile`], which provides capability-based access to @@ -44,6 +45,7 @@ Linux. [CWE-22]: https://cwe.mitre.org/data/definitions/22.html [2021 CWE Top 25 Most Dangerous Software Weaknesses]: https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html [`cap-std`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-std/README.md +[`cap-tokio`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-tokio/README.md [`cap-directories`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-directories/README.md [`cap-tempfile`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-tempfile/README.md [`cap-fs-ext`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-fs-ext/README.md @@ -51,6 +53,7 @@ Linux. [`cap-rand`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-rand/README.md [`cap-net-ext`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-net-ext/README.md [`cap_std::fs`]: https://docs.rs/cap-std/latest/cap_std/fs/index.html +[`tokio`]: https://docs.rs/tokio/ [standard application directories]: https://docs.rs/directories/ [temporary directories]: https://docs.rs/tempfile/ [random number generators]: https://docs.rs/rand/ diff --git a/cap-fs-ext/Cargo.toml b/cap-fs-ext/Cargo.toml index 140cd65b..7c064555 100644 --- a/cap-fs-ext/Cargo.toml +++ b/cap-fs-ext/Cargo.toml @@ -14,9 +14,12 @@ edition = "2021" [dependencies] arf-strings = { version = "0.7.0", optional = true } +cap-tokio = { path = "../cap-tokio", optional = true, version = "4.0.2" } cap-std = { path = "../cap-std", optional = true, version = "4.0.2" } cap-primitives = { path = "../cap-primitives", version = "4.0.2" } io-lifetimes = { version = "3.0.1", default-features = false } +async-std = { version = "1.13.0", features = ["io_safety", "attributes"], optional = true } +async-trait = { version = "0.1.42", optional = true } camino = { version = "1.0.5", optional = true } [features] @@ -24,6 +27,9 @@ default = ["std"] fs_utf8 = ["cap-std/fs_utf8", "camino"] arf_strings = ["cap-std/arf_strings", "fs_utf8", "arf-strings"] std = ["cap-std"] +async_std = ["cap-tokio", "async-std", "io-lifetimes/async-std", "async-trait"] +async_std_fs_utf8 = ["cap-tokio/fs_utf8", "camino"] +async_std_arf_strings = ["cap-tokio/arf_strings", "async_std_fs_utf8", "arf-strings"] [target.'cfg(windows)'.dependencies.windows-sys] version = ">=0.60, <0.62" diff --git a/cap-fs-ext/src/dir_entry_ext.rs b/cap-fs-ext/src/dir_entry_ext.rs index 59abf6fb..356a5893 100644 --- a/cap-fs-ext/src/dir_entry_ext.rs +++ b/cap-fs-ext/src/dir_entry_ext.rs @@ -39,6 +39,22 @@ impl DirEntryExt for cap_std::fs::DirEntry { } } +#[cfg(all(not(windows), feature = "async_std"))] +impl DirEntryExt for cap_tokio::fs::DirEntry { + #[inline] + fn full_metadata(&self) -> io::Result { + self.metadata() + } +} + +#[cfg(all(windows, feature = "async_std"))] +impl DirEntryExt for cap_tokio::fs::DirEntry { + #[inline] + fn full_metadata(&self) -> io::Result { + _WindowsDirEntryExt::full_metadata(self) + } +} + #[cfg(all(not(windows), feature = "std", feature = "fs_utf8"))] impl DirEntryExt for cap_std::fs_utf8::DirEntry { #[inline] @@ -54,3 +70,19 @@ impl DirEntryExt for cap_std::fs_utf8::DirEntry { _WindowsDirEntryExt::full_metadata(self) } } + +#[cfg(all(not(windows), feature = "async_std", feature = "fs_utf8"))] +impl DirEntryExt for cap_tokio::fs_utf8::DirEntry { + #[inline] + fn full_metadata(&self) -> io::Result { + self.metadata() + } +} + +#[cfg(all(windows, feature = "async_std", feature = "fs_utf8"))] +impl DirEntryExt for cap_tokio::fs_utf8::DirEntry { + #[inline] + fn full_metadata(&self) -> io::Result { + _WindowsDirEntryExt::full_metadata(self) + } +} diff --git a/cap-fs-ext/src/dir_ext.rs b/cap-fs-ext/src/dir_ext.rs index afc31385..0fc7cf79 100644 --- a/cap-fs-ext/src/dir_ext.rs +++ b/cap-fs-ext/src/dir_ext.rs @@ -1,5 +1,7 @@ #[cfg(feature = "fs_utf8")] use camino::Utf8Path; +#[cfg(all(windows, feature = "async_std", feature = "fs_utf8"))] +use cap_primitives::fs::stat; #[cfg(not(windows))] use cap_primitives::fs::symlink; use cap_primitives::fs::{ @@ -11,6 +13,8 @@ use cap_primitives::fs::{symlink_dir, symlink_file}; use io_lifetimes::AsFilelike; use std::io; use std::path::Path; +#[cfg(feature = "async_std")] +use {async_std::task::spawn_blocking, async_trait::async_trait}; pub use cap_primitives::fs::{AccessType, SystemTimeSpec}; @@ -108,8 +112,152 @@ pub trait DirExt { -> io::Result<()>; } +/// Extension trait for `Dir`, async. +/// +/// The path parameters include `Send` for the `async_trait` macro. +#[cfg(feature = "async_std")] +#[async_trait] +pub trait AsyncDirExt { + /// Set the last access time for a file on a filesystem. + /// + /// This corresponds to [`filetime::set_file_atime`]. + /// + /// [`filetime::set_file_atime`]: https://docs.rs/filetime/latest/filetime/fn.set_file_atime.html + async fn set_atime + Send>( + &self, + path: P, + atime: SystemTimeSpec, + ) -> io::Result<()>; + + /// Set the last modification time for a file on a filesystem. + /// + /// This corresponds to [`filetime::set_file_mtime`]. + /// + /// [`filetime::set_file_mtime`]: https://docs.rs/filetime/latest/filetime/fn.set_file_mtime.html + async fn set_mtime + Send>( + &self, + path: P, + mtime: SystemTimeSpec, + ) -> io::Result<()>; + + /// Set the last access and modification times for a file on a filesystem. + /// + /// This corresponds to [`filetime::set_file_times`]. + /// + /// [`filetime::set_file_times`]: https://docs.rs/filetime/latest/filetime/fn.set_file_times.html + async fn set_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()>; + + /// Set the last access and modification times for a file on a filesystem. + /// This function does not follow symlink. + /// + /// This corresponds to [`filetime::set_symlink_file_times`]. + /// + /// [`filetime::set_symlink_file_times`]: https://docs.rs/filetime/latest/filetime/fn.set_symlink_file_times.html + async fn set_symlink_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()>; + + /// Creates a new symbolic link on a filesystem. + /// + /// This corresponds to [`std::os::unix::fs::symlink`], except that + /// it's supported on non-Unix platforms as well, and it's not guaranteed + /// to be atomic. + /// + /// [`std::os::unix::fs::symlink`]: https://doc.rust-lang.org/std/os/unix/fs/fn.symlink.html + async fn symlink< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()>; + + /// Creates a new file symbolic link on a filesystem. + /// + /// This corresponds to [`std::os::windows::fs::symlink_file`], except that + /// it's supported on non-Windows platforms as well, and it's not + /// guaranteed to fail if the target is not a file. + /// + /// [`std::os::windows::fs::symlink_file`]: https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_file.html + async fn symlink_file< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()>; + + /// Creates a new directory symbolic link on a filesystem. + /// + /// This corresponds to [`std::os::windows::fs::symlink_dir`], except that + /// it's supported on non-Windows platforms as well, and it's not + /// guaranteed to fail if the target is not a directory. + /// + /// [`std::os::windows::fs::symlink_dir`]: https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_dir.html + async fn symlink_dir< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()>; + + /// Similar to `cap_std::fs::Dir::open_dir`, but fails if the path names a + /// symlink. + async fn open_dir_nofollow + Send>( + &self, + path: P, + ) -> io::Result + where + Self: Sized; + + /// Removes a file or symlink from a filesystem. + /// + /// Removal of symlinks has different behavior under Windows - if a symlink + /// points to a directory, it cannot be removed with the `remove_file` + /// operation. This method will remove files and all symlinks. + async fn remove_file_or_symlink + Send>( + &self, + path: P, + ) -> io::Result<()>; + + /// Test for accessibility or existence of a filesystem object. + async fn access + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()>; + + /// Test for accessibility or existence of a filesystem object, without + /// following symbolic links. + async fn access_symlink + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()>; + + /// Changes the permissions found on a file or a directory, without following + /// symbolic links. + async fn set_symlink_permissions + Send>( + &self, + path: P, + perm: Permissions, + ) -> io::Result<()>; +} + /// `fs_utf8` version of `DirExt`. -#[cfg(all(feature = "std", feature = "fs_utf8"))] +#[cfg(all(any(feature = "std", feature = "async_std"), feature = "fs_utf8"))] pub trait DirExtUtf8 { /// Set the last access time for a file on a filesystem. /// @@ -211,6 +359,132 @@ pub trait DirExtUtf8 { ) -> io::Result<()>; } +/// `fs_utf8` version of `DirExt`. +/// +/// The path parameters include `Send` for the `async_trait` macro. +#[cfg(all(feature = "async_std", feature = "fs_utf8"))] +#[async_trait] +pub trait AsyncDirExtUtf8 { + /// Set the last access time for a file on a filesystem. + /// + /// This corresponds to [`filetime::set_file_atime`]. + /// + /// [`filetime::set_file_atime`]: https://docs.rs/filetime/latest/filetime/fn.set_file_atime.html + async fn set_atime + Send>( + &self, + path: P, + atime: SystemTimeSpec, + ) -> io::Result<()>; + + /// Set the last modification time for a file on a filesystem. + /// + /// This corresponds to [`filetime::set_file_mtime`]. + /// + /// [`filetime::set_file_mtime`]: https://docs.rs/filetime/latest/filetime/fn.set_file_mtime.html + async fn set_mtime + Send>( + &self, + path: P, + mtime: SystemTimeSpec, + ) -> io::Result<()>; + + /// Set the last access and modification times for a file on a filesystem. + /// + /// This corresponds to [`filetime::set_file_times`]. + /// + /// [`filetime::set_file_times`]: https://docs.rs/filetime/latest/filetime/fn.set_file_times.html + async fn set_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()>; + + /// Set the last access and modification times for a file on a filesystem. + /// This function does not follow symlink. + /// + /// This corresponds to [`filetime::set_symlink_file_times`]. + /// + /// [`filetime::set_symlink_file_times`]: https://docs.rs/filetime/latest/filetime/fn.set_symlink_file_times.html + async fn set_symlink_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()>; + + /// Creates a new symbolic link on a filesystem. + /// + /// This corresponds to [`std::os::unix::fs::symlink`], except that + /// it's supported on non-Unix platforms as well, and it's not guaranteed + /// to be atomic. + /// + /// [`std::os::unix::fs::symlink`]: https://doc.rust-lang.org/std/os/unix/fs/fn.symlink.html + async fn symlink + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()>; + + /// Creates a new file symbolic link on a filesystem. + /// + /// This corresponds to [`std::os::windows::fs::symlink_file`], except that + /// it's supported on non-Windows platforms as well, and it's not + /// guaranteed to fail if the target is not a file. + /// + /// [`std::os::windows::fs::symlink_file`]: https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_file.html + async fn symlink_file + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()>; + + /// Creates a new directory symbolic link on a filesystem. + /// + /// This corresponds to [`std::os::windows::fs::symlink_dir`], except that + /// it's supported on non-Windows platforms as well, and it's not + /// guaranteed to fail if the target is not a directory. + /// + /// [`std::os::windows::fs::symlink_dir`]: https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_dir.html + async fn symlink_dir + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()>; + + /// Similar to `cap_std::fs::Dir::open_dir`, but fails if the path names a + /// symlink. + async fn open_dir_nofollow + Send>(&self, path: P) -> io::Result + where + Self: Sized; + + /// Removes a file or symlink from a filesystem. + /// + /// Removal of symlinks has different behavior under Windows - if a symlink + /// points to a directory, it cannot be removed with the `remove_file` + /// operation. This method will remove files and all symlinks. + async fn remove_file_or_symlink + Send>(&self, path: P) -> io::Result<()>; + + /// Test for accessibility or existence of a filesystem object. + async fn access + Send>(&self, path: P, type_: AccessType) + -> io::Result<()>; + + /// Test for accessibility or existence of a filesystem object, without + /// following symbolic links. + async fn access_symlink + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()>; + + /// Changes the permissions found on a file or a directory, without following + /// symbolic links. + async fn set_symlink_permissions + Send>( + &self, + path: P, + perm: Permissions, + ) -> io::Result<()>; +} + #[cfg(feature = "std")] impl DirExt for cap_std::fs::Dir { #[inline] @@ -400,6 +674,359 @@ impl DirExt for cap_std::fs::Dir { } } +#[cfg(feature = "async_std")] +#[async_trait] +impl AsyncDirExt for cap_tokio::fs::Dir { + #[inline] + async fn set_atime + Send>( + &self, + path: P, + atime: SystemTimeSpec, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + set_times( + &clone.as_filelike_view::(), + path.as_ref(), + Some(atime), + None, + ) + }) + .await + } + + #[inline] + async fn set_mtime + Send>( + &self, + path: P, + mtime: SystemTimeSpec, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + set_times( + &clone.as_filelike_view::(), + path.as_ref(), + None, + Some(mtime), + ) + }) + .await + } + + #[inline] + async fn set_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + set_times( + &clone.as_filelike_view::(), + path.as_ref(), + atime, + mtime, + ) + }) + .await + } + + #[inline] + async fn set_symlink_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + set_times_nofollow( + &clone.as_filelike_view::(), + path.as_ref(), + atime, + mtime, + ) + }) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn symlink< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = src.as_ref().to_path_buf(); + let dst = dst.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn symlink_file< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = src.as_ref().to_path_buf(); + let dst = dst.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn symlink_dir< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = src.as_ref().to_path_buf(); + let dst = dst.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } + + #[cfg(windows)] + #[inline] + async fn symlink< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = src.as_ref().to_path_buf(); + let dst = dst.as_ref().to_path_buf(); + let clone = self.clone(); + if self.metadata(&src).await?.is_dir() { + spawn_blocking(move || { + symlink_dir( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } else { + spawn_blocking(move || { + symlink_file( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } + } + + #[cfg(windows)] + #[inline] + async fn symlink_file< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = src.as_ref().to_path_buf(); + let dst = dst.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink_file( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } + + #[cfg(windows)] + #[inline] + async fn symlink_dir< + P: AsRef + Send, + Q: AsRef + Send, + >( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = src.as_ref().to_path_buf(); + let dst = dst.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink_dir( + src.as_ref(), + &clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await + } + + #[inline] + async fn open_dir_nofollow + Send>( + &self, + path: P, + ) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + match open_dir_nofollow(&clone.as_filelike_view::(), path.as_ref()) { + Ok(file) => Ok(Self::from_std_file(file.into())), + Err(e) => Err(e), + } + }) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn remove_file_or_symlink + Send>( + &self, + path: P, + ) -> io::Result<()> { + self.remove_file(path).await + } + + #[cfg(windows)] + #[inline] + async fn remove_file_or_symlink + Send>( + &self, + path: P, + ) -> io::Result<()> { + use crate::OpenOptionsFollowExt; + use cap_primitives::fs::_WindowsByHandle; + use cap_std::fs::{OpenOptions, OpenOptionsExt}; + use windows_sys::Win32::Storage::FileSystem::{ + DELETE, FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_OPEN_REPARSE_POINT, + }; + let path = path.as_ref(); + + let mut opts = OpenOptions::new(); + opts.access_mode(DELETE); + opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS); + opts.follow(FollowSymlinks::No); + let file = self.open_with(path, &opts).await?; + + let meta = file.metadata().await?; + if meta.file_type().is_symlink() + && meta.file_attributes() & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY + { + self.remove_dir(path).await?; + } else { + self.remove_file(path).await?; + } + + // Drop the file after calling `remove_file` or `remove_dir`, since + // Windows doesn't actually remove the file until after the last open + // handle is closed, and this protects us from race conditions where + // other processes replace the file out from underneath us. + drop(file); + + Ok(()) + } + + /// Test for accessibility or existence of a filesystem object. + async fn access + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + access( + &clone.as_filelike_view::(), + path.as_ref(), + type_, + FollowSymlinks::Yes, + ) + }) + .await + } + + /// Test for accessibility or existence of a filesystem object, without + /// following symbolic links. + async fn access_symlink + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + access( + &clone.as_filelike_view::(), + path.as_ref(), + type_, + FollowSymlinks::No, + ) + }) + .await + } + + /// Changes the permissions found on a file or a directory, without following + /// symbolic links. + async fn set_symlink_permissions + Send>( + &self, + path: P, + perm: Permissions, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + set_symlink_permissions( + &clone.as_filelike_view::(), + path.as_ref(), + perm, + ) + }) + .await + } +} + #[cfg(all(feature = "std", feature = "fs_utf8"))] impl DirExtUtf8 for cap_std::fs_utf8::Dir { #[inline] @@ -600,13 +1227,301 @@ impl DirExtUtf8 for cap_std::fs_utf8::Dir { } } -#[cfg(all(feature = "std", feature = "fs_utf8"))] +#[cfg(all(feature = "async_std", feature = "fs_utf8"))] +#[async_trait] +impl AsyncDirExtUtf8 for cap_tokio::fs_utf8::Dir { + #[inline] + async fn set_atime + Send>( + &self, + path: P, + atime: SystemTimeSpec, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + set_times( + &clone.as_filelike_view::(), + &path, + Some(atime), + None, + ) + }) + .await + } + + #[inline] + async fn set_mtime + Send>( + &self, + path: P, + mtime: SystemTimeSpec, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + set_times( + &clone.as_filelike_view::(), + &path, + None, + Some(mtime), + ) + }) + .await + } + + #[inline] + async fn set_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + set_times( + &clone.as_filelike_view::(), + &path, + atime, + mtime, + ) + }) + .await + } + + #[inline] + async fn set_symlink_times + Send>( + &self, + path: P, + atime: Option, + mtime: Option, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + set_times_nofollow( + &clone.as_filelike_view::(), + &path, + atime, + mtime, + ) + }) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn symlink + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = from_utf8(src.as_ref())?; + let dst = from_utf8(dst.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || symlink(&src, &clone.as_filelike_view::(), &dst)) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn symlink_file + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + self.symlink(src, dst).await + } + + #[cfg(not(windows))] + #[inline] + async fn symlink_dir + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + self.symlink(src, dst).await + } + + #[cfg(windows)] + #[inline] + async fn symlink + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = from_utf8(src.as_ref())?; + let src_ = src.clone(); + let dst = from_utf8(dst.as_ref())?; + let clone = self.clone(); + // Call `stat` directly to avoid `async_trait` capturing `self`. + let metadata = spawn_blocking(move || { + stat( + &clone.as_filelike_view::(), + &src_, + FollowSymlinks::Yes, + ) + }) + .await?; + let clone = self.clone(); + if metadata.is_dir() { + spawn_blocking(move || { + symlink_dir(&src, &clone.as_filelike_view::(), &dst) + }) + .await + } else { + spawn_blocking(move || { + symlink_file(&src, &clone.as_filelike_view::(), &dst) + }) + .await + } + } + + #[cfg(windows)] + #[inline] + async fn symlink_file + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = from_utf8(src.as_ref())?; + let dst = from_utf8(dst.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || symlink_file(&src, &clone.as_filelike_view::(), &dst)) + .await + } + + #[cfg(windows)] + #[inline] + async fn symlink_dir + Send, Q: AsRef + Send>( + &self, + src: P, + dst: Q, + ) -> io::Result<()> { + let src = from_utf8(src.as_ref())?; + let dst = from_utf8(dst.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || symlink_dir(&src, &clone.as_filelike_view::(), &dst)) + .await + } + + #[inline] + async fn open_dir_nofollow + Send>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + match open_dir_nofollow(&clone.as_filelike_view::(), path.as_ref()) { + Ok(file) => Ok(Self::from_std_file(file.into())), + Err(e) => Err(e), + } + }) + .await + } + + #[cfg(not(windows))] + #[inline] + async fn remove_file_or_symlink + Send>(&self, path: P) -> io::Result<()> { + self.remove_file(path).await + } + + #[cfg(windows)] + #[inline] + async fn remove_file_or_symlink + Send>(&self, path: P) -> io::Result<()> { + use crate::{FollowSymlinks, OpenOptionsFollowExt}; + use cap_primitives::fs::_WindowsByHandle; + use cap_std::fs::{OpenOptions, OpenOptionsExt}; + use windows_sys::Win32::Storage::FileSystem::{ + DELETE, FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_OPEN_REPARSE_POINT, + }; + let path = path.as_ref(); + + let mut opts = OpenOptions::new(); + opts.access_mode(DELETE); + opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS); + opts.follow(FollowSymlinks::No); + let file = self.open_with(path, &opts).await?; + + let meta = file.metadata().await?; + if meta.file_type().is_symlink() + && meta.file_attributes() & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY + { + self.remove_dir(path).await?; + } else { + self.remove_file(path).await?; + } + + // Drop the file after calling `remove_file` or `remove_dir`, since + // Windows doesn't actually remove the file until after the last open + // handle is closed, and this protects us from race conditions where + // other processes replace the file out from underneath us. + drop(file); + + Ok(()) + } + + async fn access + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + access( + &clone.as_filelike_view::(), + path.as_ref(), + type_, + FollowSymlinks::Yes, + ) + }) + .await + } + + async fn access_symlink + Send>( + &self, + path: P, + type_: AccessType, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + access( + &clone.as_filelike_view::(), + path.as_ref(), + type_, + FollowSymlinks::No, + ) + }) + .await + } + + /// Changes the permissions found on a file or a directory, without following + /// symbolic links. + async fn set_symlink_permissions + Send>( + &self, + path: P, + perm: Permissions, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + let clone = self.clone(); + spawn_blocking(move || { + set_symlink_permissions( + &clone.as_filelike_view::(), + path.as_ref(), + perm, + ) + }) + .await + } +} + +#[cfg(all(any(feature = "std", feature = "async_std"), feature = "fs_utf8"))] #[cfg(not(feature = "arf_strings"))] fn from_utf8<'a>(path: &'a Utf8Path) -> io::Result<&'a std::path::Path> { Ok(path.as_std_path()) } -#[cfg(all(feature = "std", feature = "fs_utf8"))] +#[cfg(all(any(feature = "std", feature = "async_std"), feature = "fs_utf8"))] #[cfg(feature = "arf_strings")] fn from_utf8<'a>(path: &'a Utf8Path) -> io::Result { #[cfg(not(windows))] diff --git a/cap-fs-ext/src/file_type_ext.rs b/cap-fs-ext/src/file_type_ext.rs index 0a23569f..7598944d 100644 --- a/cap-fs-ext/src/file_type_ext.rs +++ b/cap-fs-ext/src/file_type_ext.rs @@ -63,7 +63,7 @@ impl FileTypeExt for std::fs::FileType { } } -#[cfg(all(not(windows), feature = "std"))] +#[cfg(all(not(windows), any(feature = "std", feature = "async_std")))] impl FileTypeExt for cap_primitives::fs::FileType { #[inline] fn is_block_device(&self) -> bool { @@ -86,7 +86,7 @@ impl FileTypeExt for cap_primitives::fs::FileType { } } -#[cfg(all(windows, feature = "std"))] +#[cfg(all(windows, any(feature = "std", feature = "async_std")))] impl FileTypeExt for cap_primitives::fs::FileType { #[inline] fn is_block_device(&self) -> bool { diff --git a/cap-fs-ext/src/is_file_read_write.rs b/cap-fs-ext/src/is_file_read_write.rs index 15381acd..2b6ae067 100644 --- a/cap-fs-ext/src/is_file_read_write.rs +++ b/cap-fs-ext/src/is_file_read_write.rs @@ -35,3 +35,27 @@ impl IsFileReadWrite for cap_std::fs_utf8::File { is_file_read_write(&self.as_filelike_view::()) } } + +#[cfg(feature = "async_std")] +impl IsFileReadWrite for async_std::fs::File { + #[inline] + fn is_file_read_write(&self) -> io::Result<(bool, bool)> { + is_file_read_write(&self.as_filelike_view::()) + } +} + +#[cfg(feature = "async_std")] +impl IsFileReadWrite for cap_tokio::fs::File { + #[inline] + fn is_file_read_write(&self) -> io::Result<(bool, bool)> { + is_file_read_write(&self.as_filelike_view::()) + } +} + +#[cfg(all(feature = "async_std", feature = "fs_utf8"))] +impl IsFileReadWrite for cap_tokio::fs_utf8::File { + #[inline] + fn is_file_read_write(&self) -> io::Result<(bool, bool)> { + is_file_read_write(&self.as_filelike_view::()) + } +} diff --git a/cap-fs-ext/src/lib.rs b/cap-fs-ext/src/lib.rs index 2ed80ee7..22ea2b1e 100644 --- a/cap-fs-ext/src/lib.rs +++ b/cap-fs-ext/src/lib.rs @@ -21,7 +21,11 @@ mod open_options_sync_ext; mod reopen; pub use dir_entry_ext::DirEntryExt; -#[cfg(all(feature = "std", feature = "fs_utf8"))] +#[cfg(feature = "async_std")] +pub use dir_ext::AsyncDirExt; +#[cfg(all(feature = "async_std", feature = "fs_utf8"))] +pub use dir_ext::AsyncDirExtUtf8; +#[cfg(all(any(feature = "std", feature = "async_std"), feature = "fs_utf8"))] pub use dir_ext::DirExtUtf8; pub use dir_ext::{AccessType, DirExt, SystemTimeSpec}; pub use file_type_ext::FileTypeExt; diff --git a/cap-fs-ext/src/metadata_ext.rs b/cap-fs-ext/src/metadata_ext.rs index f06791c6..fbacfcf7 100644 --- a/cap-fs-ext/src/metadata_ext.rs +++ b/cap-fs-ext/src/metadata_ext.rs @@ -71,7 +71,7 @@ impl MetadataExt for std::fs::Metadata { } } -#[cfg(all(not(windows), feature = "std"))] +#[cfg(all(not(windows), any(feature = "std", feature = "async_std")))] impl MetadataExt for cap_primitives::fs::Metadata { #[inline] fn dev(&self) -> u64 { @@ -89,7 +89,7 @@ impl MetadataExt for cap_primitives::fs::Metadata { } } -#[cfg(all(windows, feature = "std"))] +#[cfg(all(windows, any(feature = "std", feature = "async_std")))] impl MetadataExt for cap_primitives::fs::Metadata { fn dev(&self) -> u64 { _WindowsByHandle::volume_serial_number(self) diff --git a/cap-fs-ext/src/reopen.rs b/cap-fs-ext/src/reopen.rs index 32f3acc2..b61e7dd7 100644 --- a/cap-fs-ext/src/reopen.rs +++ b/cap-fs-ext/src/reopen.rs @@ -1,6 +1,8 @@ use cap_primitives::fs::{reopen, OpenOptions}; -#[cfg(feature = "std")] +#[cfg(any(feature = "std", feature = "async_std"))] use io_lifetimes::AsFilelike; +#[cfg(feature = "async_std")] +use io_lifetimes::FromFilelike; use std::io; /// A trait for the `reopen` function. @@ -53,3 +55,32 @@ impl Reopen for cap_std::fs_utf8::File { Ok(Self::from_std(file)) } } + +#[cfg(feature = "async_std")] +impl Reopen for async_std::fs::File { + #[inline] + fn reopen(&self, options: &OpenOptions) -> io::Result { + let file = reopen(&self.as_filelike_view::(), options)?; + Ok(async_std::fs::File::from_into_filelike(file)) + } +} + +#[cfg(feature = "async_std")] +impl Reopen for cap_tokio::fs::File { + #[inline] + fn reopen(&self, options: &OpenOptions) -> io::Result { + let file = reopen(&self.as_filelike_view::(), options)?; + let std = async_std::fs::File::from_into_filelike(file); + Ok(Self::from_std(std)) + } +} + +#[cfg(all(feature = "async_std", feature = "fs_utf8"))] +impl Reopen for cap_tokio::fs_utf8::File { + #[inline] + fn reopen(&self, options: &OpenOptions) -> io::Result { + let file = reopen(&self.as_filelike_view::(), options)?; + let std = async_std::fs::File::from_into_filelike(file); + Ok(Self::from_std(std)) + } +} diff --git a/cap-primitives/README.md b/cap-primitives/README.md index 014ea021..a584f6f5 100644 --- a/cap-primitives/README.md +++ b/cap-primitives/README.md @@ -13,7 +13,7 @@ The `cap-primitives` crate provides primitive sandboxing operations that -[`cap-std`] is built on. +[`cap-std`] and [`cap-tokio`] are built on. The filesystem module [`cap_primitives::fs`], the networking module [`cap_primitives::net`], and time module [`cap_primitives::time`] currently @@ -21,6 +21,7 @@ support Linux, macOS, FreeBSD, and Windows. WASI support is in development, though not yet usable. [`cap-std`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-std/README.md +[`cap-tokio`]: https://github.com/bytecodealliance/cap-std/blob/main/cap-tokio/README.md [`cap_primitives::fs`]: https://docs.rs/cap-primitives/latest/cap_primitives/fs/index.html [`cap_primitives::net`]: https://docs.rs/cap-primitives/latest/cap_primitives/net/index.html [`cap_primitives::time`]: https://docs.rs/cap-primitives/latest/cap_primitives/time/index.html diff --git a/cap-tokio/COPYRIGHT b/cap-tokio/COPYRIGHT new file mode 100644 index 00000000..a53c7c42 --- /dev/null +++ b/cap-tokio/COPYRIGHT @@ -0,0 +1,29 @@ +Short version for non-lawyers: + +`cap-tokio` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `cap-tokio` project are retained by their contributors. +No copyright assignment is required to contribute to the `cap-tokio` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `cap-tokio` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. diff --git a/cap-tokio/Cargo.toml b/cap-tokio/Cargo.toml new file mode 100644 index 00000000..07e50a8f --- /dev/null +++ b/cap-tokio/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cap-tokio" +version = "4.0.2" +description = "Capability-based version of tokio" +authors = [ + "Dan Gohman ", + "Jakub Konka ", +] +license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" +keywords = ["network", "file", "async", "future", "await"] +categories = ["filesystem", "network-programming", "asynchronous", "concurrency"] +repository = "https://github.com/bytecodealliance/cap-std" +edition = "2021" + +[dependencies] +arf-strings = { version = "0.7.0", optional = true } +futures-core = "0.3" +tokio = { version = "1", features = ["fs", "io-util", "rt", "net"] } +cap-primitives = { path = "../cap-primitives", version = "^4.0.2" } +io-lifetimes = { version = "2.0.0", default-features = false } +io-extras = { version = "0.18.3" } +socket2 = { version = "0.5" } +camino = { version = "1.0.5", optional = true } + +[target.'cfg(not(windows))'.dependencies] +rustix = { version = "1.0.0", features = ["fs"] } + +[features] +default = [] +fs_utf8 = ["camino"] +arf_strings = ["fs_utf8", "arf-strings"] diff --git a/cap-tokio/LICENSE-APACHE b/cap-tokio/LICENSE-APACHE new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/cap-tokio/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/cap-tokio/LICENSE-Apache-2.0_WITH_LLVM-exception b/cap-tokio/LICENSE-Apache-2.0_WITH_LLVM-exception new file mode 100644 index 00000000..f9d81955 --- /dev/null +++ b/cap-tokio/LICENSE-Apache-2.0_WITH_LLVM-exception @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/cap-tokio/LICENSE-MIT b/cap-tokio/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/cap-tokio/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/cap-tokio/README.md b/cap-tokio/README.md new file mode 100644 index 00000000..73f33873 --- /dev/null +++ b/cap-tokio/README.md @@ -0,0 +1,19 @@ +
+

cap-tokio

+ +

+ Capability-based version of `tokio` +

+ +

+ Github Actions CI Status + crates.io page + docs.rs docs +

+
+ +This crate provides a capability-based version of [`tokio`]. See the +[toplevel README.md] for more information about capability-based security. + +[`tokio`]: https://crates.io/crates/tokio +[toplevel README.md]: https://github.com/bytecodealliance/cap-std/blob/main/README.md diff --git a/cap-tokio/build.rs b/cap-tokio/build.rs new file mode 100644 index 00000000..434559ec --- /dev/null +++ b/cap-tokio/build.rs @@ -0,0 +1,84 @@ +use std::env::var; +use std::io::Write; + +fn main() { + use_feature_or_nothing("windows_file_type_ext"); + + // Cfgs that users may set. + println!("cargo:rustc-check-cfg=cfg(io_lifetimes_use_std)"); + + // Don't rerun this on changes other than build.rs, as we only depend on + // the rustc version. + println!("cargo:rerun-if-changed=build.rs"); +} + +fn use_feature_or_nothing(feature: &str) { + if has_feature(feature) { + use_feature(feature); + } + println!("cargo:rustc-check-cfg=cfg({})", feature); +} + +fn use_feature(feature: &str) { + println!("cargo:rustc-cfg={}", feature); +} + +/// Test whether the rustc at `var("RUSTC")` supports the given feature. +fn has_feature(feature: &str) -> bool { + can_compile(&format!( + "#![allow(stable_features)]\n#![feature({})]", + feature + )) +} + +/// Test whether the rustc at `var("RUSTC")` can compile the given code. +fn can_compile>(test: T) -> bool { + use std::process::Stdio; + + let rustc = var("RUSTC").unwrap(); + let target = var("TARGET").unwrap(); + + // Use `RUSTC_WRAPPER` if it's set, unless it's set to an empty string, + // as documented [here]. + // [here]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads + let wrapper = var("RUSTC_WRAPPER") + .ok() + .and_then(|w| if w.is_empty() { None } else { Some(w) }); + + let mut cmd = if let Some(wrapper) = wrapper { + let mut cmd = std::process::Command::new(wrapper); + // The wrapper's first argument is supposed to be the path to rustc. + cmd.arg(rustc); + cmd + } else { + std::process::Command::new(rustc) + }; + + cmd.arg("--crate-type=rlib") // Don't require `main`. + .arg("--emit=metadata") // Do as little as possible but still parse. + .arg("--target") + .arg(target) + .arg("-o") + .arg("-") + .stdout(Stdio::null()); // We don't care about the output (only whether it builds or not) + + // If Cargo wants to set RUSTFLAGS, use that. + if let Ok(rustflags) = var("CARGO_ENCODED_RUSTFLAGS") { + if !rustflags.is_empty() { + for arg in rustflags.split('\x1f') { + cmd.arg(arg); + } + } + } + + let mut child = cmd + .arg("-") // Read from stdin. + .stdin(Stdio::piped()) // Stdin is a pipe. + .stderr(Stdio::null()) // Errors from feature detection aren't interesting and can be confusing. + .spawn() + .unwrap(); + + writeln!(child.stdin.take().unwrap(), "{}", test.as_ref()).unwrap(); + + child.wait().unwrap().success() +} diff --git a/cap-tokio/src/fs/dir.rs b/cap-tokio/src/fs/dir.rs new file mode 100644 index 00000000..18d85f55 --- /dev/null +++ b/cap-tokio/src/fs/dir.rs @@ -0,0 +1,1086 @@ +use crate::fs::{DirBuilder, File, Metadata, OpenOptions, ReadDir}; +use crate::try_into_os::*; +use cap_primitives::fs::{ + canonicalize, copy, create_dir, hard_link, open, open_ambient_dir, open_dir, open_parent_dir, + read_base_dir, read_dir, read_link, remove_dir, remove_dir_all, remove_file, remove_open_dir, + remove_open_dir_all, rename, set_permissions, stat, DirOptions, FollowSymlinks, Permissions, +}; +use cap_primitives::AmbientAuthority; +use io_lifetimes::raw::{AsRawFilelike, FromRawFilelike}; +use io_lifetimes::AsFilelike; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsHandle, BorrowedHandle, OwnedHandle}; +use std::fmt; +use std::mem::ManuallyDrop; +#[cfg(target_os = "wasi")] +use std::os::wasi::{ + fs::OpenOptionsExt, + io::{AsRawFd, IntoRawFd}, +}; +use std::path::{Path, PathBuf}; +use std::{fs, io}; +use tokio::task::spawn_blocking; +#[cfg(unix)] +use { + crate::os::unix::net::{UnixDatagram, UnixListener, UnixStream}, + cap_primitives::fs::symlink, + std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, +}; + +#[cfg(windows)] +use { + cap_primitives::fs::{symlink_dir, symlink_file}, + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle, RawHandle}, +}; + +/// A reference to an open directory on a filesystem. +/// +/// This does not directly correspond to anything in `tokio`, however its +/// methods correspond to the [functions in `tokio::fs`] and the +/// constructor methods for [`tokio::fs::File`]. +/// +/// Unlike `tokio::fs`, this API's `canonicalize` returns a relative path +/// since absolute paths don't interoperate well with the capability model. +/// +/// [functions in `tokio::fs`]: https://docs.rs/tokio/latest/tokio/fs/index.html#functions +/// We use `Arc` rather than `tokio::fs::File` because: +/// - `Dir` only needs the file descriptor, not async read/write +/// - All actual filesystem operations go through sync `cap_primitives` +/// calls wrapped in `spawn_blocking` +/// - `tokio::fs::File` doesn't implement `Clone`, so we use `Arc` to +/// enable cloning for use in `spawn_blocking` closures +#[derive(Clone)] +pub struct Dir { + std_file: std::sync::Arc, +} + +impl Dir { + /// Constructs a new instance of `Self` from the given + /// `std::fs::File`. + /// + /// To prevent race conditions on Windows, the file must be opened without + /// `FILE_SHARE_DELETE`. + /// + /// This grants access the resources the `std::fs::File` instance + /// already has access to. + #[inline] + pub fn from_std_file(std_file: fs::File) -> Self { + Self { + std_file: std::sync::Arc::new(std_file), + } + } + + /// Consumes `self` and returns an `std::fs::File`, if this is the + /// sole reference. Returns `Err(self)` if other clones exist. + #[inline] + pub fn into_std_file(self) -> Result { + match std::sync::Arc::try_unwrap(self.std_file) { + Ok(f) => Ok(f), + Err(arc) => Err(Self { std_file: arc }), + } + } + + /// Attempts to open a file in read-only mode. + /// + /// This corresponds to [`tokio::fs::File::open`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn open>(&self, path: P) -> io::Result { + self.open_with(path, OpenOptions::new().read(true)).await + } + + /// Opens a file at `path` with the options specified by `options`. + /// + /// This corresponds to [`std::fs::OpenOptions::open`]. + /// + /// Instead of being a method on `OpenOptions`, this is a method on `Dir`, + /// and it only accesses paths relative to `self`. + #[inline] + pub async fn open_with>( + &self, + path: P, + options: &OpenOptions, + ) -> io::Result { + self._open_with(path.as_ref(), options).await + } + + #[cfg(not(target_os = "wasi"))] + async fn _open_with(&self, path: &Path, options: &OpenOptions) -> io::Result { + let path = path.to_path_buf(); + let clone = self.clone(); + let options = options.clone(); + let file = spawn_blocking(move || { + open( + &*clone.as_filelike_view::(), + path.as_ref(), + &options, + ) + }) + .await? + .map(|f| f.into())?; + Ok(File::from_std(file)) + } + + #[cfg(target_os = "wasi")] + async fn _open_with( + file: &std::fs::File, + path: &Path, + options: &OpenOptions, + ) -> io::Result { + let file = options.open_at(&self.std_file, path)?.into(); + Ok(File::from_std(file)) + } + + /// Attempts to open a directory. + #[inline] + pub async fn open_dir>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + let dir = spawn_blocking(move || { + open_dir(&clone.as_filelike_view::(), path.as_ref()) + }) + .await??; + Ok(Self::from_std_file(dir.into())) + } + + /// Creates a new, empty directory at the provided path. + /// + /// This corresponds to [`tokio::fs::create_dir`], but only accesses + /// paths relative to `self`. + /// + /// TODO: async: fix this when we fix + #[inline] + pub fn create_dir>(&self, path: P) -> io::Result<()> { + self._create_dir_one(path.as_ref(), &DirOptions::new()) + } + + /// Recursively create a directory and all of its parent components if they + /// are missing. + /// + /// This corresponds to [`tokio::fs::create_dir_all`], but only + /// accesses paths relative to `self`. + /// + /// TODO: async: fix this when we fix + #[inline] + pub fn create_dir_all>(&self, path: P) -> io::Result<()> { + self._create_dir_all(path.as_ref(), &DirOptions::new()) + } + + /// Creates the specified directory with the options configured in this + /// builder. + /// + /// This corresponds to [`std::fs::DirBuilder::create`]. + /// + /// TODO: async: fix this when we fix + #[inline] + pub fn create_dir_with>( + &self, + path: P, + dir_builder: &DirBuilder, + ) -> io::Result<()> { + let options = dir_builder.options(); + if dir_builder.is_recursive() { + self._create_dir_all(path.as_ref(), options) + } else { + self._create_dir_one(path.as_ref(), options) + } + } + + #[inline] + fn _create_dir_one(&self, path: &Path, dir_options: &DirOptions) -> io::Result<()> { + create_dir( + &self.as_filelike_view::(), + path.as_ref(), + dir_options, + ) + } + + fn _create_dir_all(&self, path: &Path, dir_options: &DirOptions) -> io::Result<()> { + if path == Path::new("") { + return Ok(()); + } + + match self._create_dir_one(path, dir_options) { + Ok(()) => return Ok(()), + Err(ref e) if e.kind() == io::ErrorKind::NotFound => {} + Err(_) if self.is_dir_blocking(path) => return Ok(()), + Err(e) => return Err(e), + } + match path.parent() { + Some(p) => self._create_dir_all(p, dir_options)?, + None => { + return Err(io::Error::new( + io::ErrorKind::Other, + "failed to create whole tree", + )) + } + } + match self._create_dir_one(path, dir_options) { + Ok(()) => Ok(()), + Err(_) if self.is_dir_blocking(path) => Ok(()), + Err(e) => Err(e), + } + } + + /// Opens a file in write-only mode. + /// + /// This corresponds to [`tokio::fs::File::create`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn create>(&self, path: P) -> io::Result { + self.open_with( + path, + OpenOptions::new().write(true).create(true).truncate(true), + ) + .await + } + + /// Returns the canonical form of a path with all intermediate components + /// normalized and symbolic links resolved. + /// + /// This corresponds to [`tokio::fs::canonicalize`], but instead of + /// returning an absolute path, returns a path relative to the + /// directory represented by `self`. + #[inline] + pub async fn canonicalize>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + canonicalize(&clone.as_filelike_view::(), path.as_ref()) + }) + .await? + .map(PathBuf::from) + } + + /// Copies the contents of one file to another. This function will also + /// copy the permission bits of the original file to the destination + /// file. + /// + /// This corresponds to [`tokio::fs::copy`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn copy, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result { + let from = from.as_ref().to_path_buf(); + let to = to.as_ref().to_path_buf(); + let from_clone = self.clone(); + let to_clone = to_dir.clone(); + spawn_blocking(move || { + copy( + &from_clone.as_filelike_view::(), + from.as_ref(), + &to_clone.as_filelike_view::(), + to.as_ref(), + ) + }) + .await? + } + + /// Creates a new hard link on a filesystem. + /// + /// This corresponds to [`tokio::fs::hard_link`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn hard_link, Q: AsRef>( + &self, + src: P, + dst_dir: &Self, + dst: Q, + ) -> io::Result<()> { + let dst = dst.as_ref().to_path_buf(); + let src = src.as_ref().to_path_buf(); + let src_clone = self.clone(); + let dst_clone = dst_dir.clone(); + spawn_blocking(move || { + hard_link( + &src_clone.as_filelike_view::(), + src.as_ref(), + &dst_clone.as_filelike_view::(), + dst.as_ref(), + ) + }) + .await? + } + + /// Given a path, query the file system to get information about a file, + /// directory, etc. + /// + /// This corresponds to [`tokio::fs::metadata`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn metadata>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + stat( + &clone.as_filelike_view::(), + path.as_ref(), + FollowSymlinks::Yes, + ) + }) + .await? + } + + /// TODO: Remove this once `create_dir` and friends are async. + #[inline] + fn metadata_blocking>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + stat( + &self.as_filelike_view::(), + path.as_ref(), + FollowSymlinks::Yes, + ) + } + + /// Queries metadata about the underlying directory. + /// + /// This is similar to [`std::fs::File::metadata`], but for `Dir` rather + /// than for `File`. + #[inline] + pub async fn dir_metadata(&self) -> io::Result { + let clone = self.clone(); + spawn_blocking(move || metadata_from(&*clone.as_filelike_view::())).await? + } + + /// Returns an iterator over the entries within `self`. + #[inline] + pub async fn entries(&self) -> io::Result { + let clone = self.clone(); + spawn_blocking(move || read_base_dir(&clone.as_filelike_view::())) + .await? + .map(|inner| ReadDir { inner }) + } + + /// Returns an iterator over the entries within a directory. + /// + /// This corresponds to [`tokio::fs::read_dir`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn read_dir>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || read_dir(&clone.as_filelike_view::(), path.as_ref())) + .await? + .map(|inner| ReadDir { inner }) + } + + /// Read the entire contents of a file into a bytes vector. + /// + /// This corresponds to [`tokio::fs::read`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn read>(&self, path: P) -> io::Result> { + use tokio::io::AsyncReadExt; + let mut file = self.open(path).await?; + let mut bytes = Vec::with_capacity(initial_buffer_size(&file).await); + file.read_to_end(&mut bytes).await?; + Ok(bytes) + } + + /// Reads a symbolic link, returning the file that the link points to. + /// + /// This corresponds to [`tokio::fs::read_link`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn read_link>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || read_link(&clone.as_filelike_view::(), path.as_ref())) + .await? + .map(PathBuf::from) + } + + /// Read the entire contents of a file into a string. + /// + /// This corresponds to [`tokio::fs::read_to_string`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn read_to_string>(&self, path: P) -> io::Result { + use tokio::io::AsyncReadExt; + let mut s = String::new(); + self.open(path).await?.read_to_string(&mut s).await?; + Ok(s) + } + + /// Removes an empty directory. + /// + /// This corresponds to [`tokio::fs::remove_dir`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn remove_dir>(&self, path: P) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + remove_dir(&clone.as_filelike_view::(), path.as_ref()) + }) + .await? + } + + /// Removes a directory at this path, after removing all its contents. Use + /// carefully! + /// + /// This corresponds to [`tokio::fs::remove_dir_all`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn remove_dir_all>(&self, path: P) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + remove_dir_all(&clone.as_filelike_view::(), path.as_ref()) + }) + .await? + } + + /// Remove the directory referenced by `self` and consume `self`. + /// + /// Even though this implementation works in terms of handles as much as + /// possible, removal is not guaranteed to be atomic with respect to a + /// concurrent rename of the directory. + #[inline] + pub async fn remove_open_dir(self) -> io::Result<()> { + let file = std::sync::Arc::try_unwrap(self.std_file).map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "cannot remove open dir: other references exist", + ) + })?; + spawn_blocking(move || remove_open_dir(file)).await? + } + + /// Removes the directory referenced by `self`, after removing all its + /// contents, and consume `self`. Use carefully! + /// + /// Even though this implementation works in terms of handles as much as + /// possible, removal is not guaranteed to be atomic with respect to a + /// concurrent rename of the directory. + #[inline] + pub async fn remove_open_dir_all(self) -> io::Result<()> { + let file = std::sync::Arc::try_unwrap(self.std_file).map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "cannot remove open dir: other references exist", + ) + })?; + spawn_blocking(move || remove_open_dir_all(file)).await? + } + + /// Removes a file from a filesystem. + /// + /// This corresponds to [`tokio::fs::remove_file`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn remove_file>(&self, path: P) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + remove_file(&clone.as_filelike_view::(), path.as_ref()) + }) + .await? + } + + /// Rename a file or directory to a new name, replacing the original file + /// if to already exists. + /// + /// This corresponds to [`tokio::fs::rename`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn rename, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result<()> { + let from = from.as_ref().to_path_buf(); + let to = to.as_ref().to_path_buf(); + let clone = self.clone(); + let to_clone = to_dir.clone(); + spawn_blocking(move || { + rename( + &clone.as_filelike_view::(), + from.as_ref(), + &to_clone.as_filelike_view::(), + to.as_ref(), + ) + }) + .await? + } + + /// Changes the permissions found on a file or a directory. + /// + /// This corresponds to [`tokio::fs::set_permissions`], but only + /// accesses paths relative to `self`. Also, on some platforms, this + /// function may fail if the file or directory cannot be opened for + /// reading or writing first. + pub async fn set_permissions>( + &self, + path: P, + perm: Permissions, + ) -> io::Result<()> { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + set_permissions( + &clone.as_filelike_view::(), + path.as_ref(), + perm, + ) + }) + .await? + } + + /// Query the metadata about a file without following symlinks. + /// + /// This corresponds to [`tokio::fs::symlink_metadata`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn symlink_metadata>(&self, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + stat( + &clone.as_filelike_view::(), + path.as_ref(), + FollowSymlinks::No, + ) + }) + .await? + } + + /// Write a slice as the entire contents of a file. + /// + /// This corresponds to [`tokio::fs::write`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn write, C: AsRef<[u8]>>( + &self, + path: P, + contents: C, + ) -> io::Result<()> { + use tokio::io::AsyncWriteExt; + let mut file = self.create(path).await?; + file.write_all(contents.as_ref()).await + } + + /// Creates a new symbolic link on a filesystem. + /// + /// The `original` argument provides the target of the symlink. The `link` + /// argument provides the name of the created symlink. + /// + /// Despite the argument ordering, `original` is not resolved relative to + /// `self` here. `link` is resolved relative to `self`, and `original` is + /// not resolved within this function. + /// + /// The `link` path is resolved when the symlink is dereferenced, relative + /// to the directory that contains it. + /// + /// This corresponds to [`tokio::fs::symlink`] (on Unix). + #[cfg(not(windows))] + #[inline] + pub async fn symlink, Q: AsRef>( + &self, + original: P, + link: Q, + ) -> io::Result<()> { + let original = original.as_ref().to_path_buf(); + let link = link.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink( + original.as_ref(), + &clone.as_filelike_view::(), + link.as_ref(), + ) + }) + .await? + } + + /// Creates a new file symbolic link on a filesystem. + /// + /// The `original` argument provides the target of the symlink. The `link` + /// argument provides the name of the created symlink. + /// + /// Despite the argument ordering, `original` is not resolved relative to + /// `self` here. `link` is resolved relative to `self`, and `original` is + /// not resolved within this function. + /// + /// The `link` path is resolved when the symlink is dereferenced, relative + /// to the directory that contains it. + /// + /// This corresponds to [`tokio::fs::symlink_file`] (on Windows). + #[cfg(windows)] + #[inline] + pub async fn symlink_file, Q: AsRef>( + &self, + original: P, + link: Q, + ) -> io::Result<()> { + let original = original.as_ref().to_path_buf(); + let link = link.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink_file( + original.as_ref(), + &clone.as_filelike_view::(), + link.as_ref(), + ) + }) + .await? + } + + /// Creates a new directory symlink on a filesystem. + /// + /// The `original` argument provides the target of the symlink. The `link` + /// argument provides the name of the created symlink. + /// + /// Despite the argument ordering, `original` is not resolved relative to + /// `self` here. `link` is resolved relative to `self`, and `original` is + /// not resolved within this function. + /// + /// The `link` path is resolved when the symlink is dereferenced, relative + /// to the directory that contains it. + /// + /// This corresponds to [`tokio::fs::symlink_dir`] (on Windows). + #[cfg(windows)] + #[inline] + pub async fn symlink_dir, Q: AsRef>( + &self, + original: P, + link: Q, + ) -> io::Result<()> { + let original = original.as_ref().to_path_buf(); + let link = link.as_ref().to_path_buf(); + let clone = self.clone(); + spawn_blocking(move || { + symlink_dir( + original.as_ref(), + &clone.as_filelike_view::(), + link.as_ref(), + ) + }) + .await? + } + + /// Creates a new `UnixListener` bound to the specified socket. + /// + /// This corresponds to [`tokio::net::UnixListener::bind`], + /// but only accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + #[doc(alias = "bind")] + #[cfg(unix)] + #[inline] + pub async fn bind_unix_listener>(&self, path: P) -> io::Result { + todo!( + "Dir::bind_unix_listener({:?}, {})", + self.std_file, + path.as_ref().display() + ) + } + + /// Connects to the socket named by path. + /// + /// This corresponds to [`tokio::net::UnixStream::connect`], + /// but only accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + #[doc(alias = "connect")] + #[cfg(unix)] + #[inline] + pub async fn connect_unix_stream>(&self, path: P) -> io::Result { + todo!( + "Dir::connect_unix_stream({:?}, {})", + self.std_file, + path.as_ref().display() + ) + } + + /// Creates a Unix datagram socket bound to the given path. + /// + /// This corresponds to [`tokio::net::UnixDatagram::bind`], + /// but only accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + #[doc(alias = "bind")] + #[cfg(unix)] + #[inline] + pub async fn bind_unix_datagram>(&self, path: P) -> io::Result { + todo!( + "Dir::bind_unix_datagram({:?}, {})", + self.std_file, + path.as_ref().display() + ) + } + + /// Connects the socket to the specified address. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::connect`], but only + /// accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + #[doc(alias = "connect")] + #[cfg(unix)] + #[inline] + pub async fn connect_unix_datagram>( + &self, + _unix_datagram: &UnixDatagram, + path: P, + ) -> io::Result<()> { + todo!( + "Dir::connect_unix_datagram({:?}, {})", + self.std_file, + path.as_ref().display() + ) + } + + /// Sends data on the socket to the specified address. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::send_to`], but only + /// accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + #[doc(alias = "send_to")] + #[cfg(unix)] + #[inline] + pub async fn send_to_unix_datagram_addr>( + &self, + _unix_datagram: &UnixDatagram, + buf: &[u8], + path: P, + ) -> io::Result { + todo!( + "Dir::send_to_unix_datagram_addr({:?}, {:?}, {})", + self.std_file, + buf, + path.as_ref().display() + ) + } + + /// Returns `true` if the path points at an existing entity. + /// + /// This corresponds to [`std::path::Path::exists`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn exists>(&self, path: P) -> bool { + self.metadata(path).await.is_ok() + } + + /// Returns `true` if the path points at an existing entity. + /// + /// This is an asynchronous version of [`std::fs::try_exists`], and also + /// only accesses paths relative to `self`. + #[inline] + pub async fn try_exists>(&self, path: P) -> io::Result { + match self.metadata(path.as_ref()).await { + Ok(_) => Ok(true), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false), + Err(e) => Err(e), + } + } + + /// Returns `true` if the path exists on disk and is pointing at a regular + /// file. + /// + /// This corresponds to [`std::path::Path::is_file`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn is_file>(&self, path: P) -> bool { + self.metadata(path) + .await + .map(|m| m.is_file()) + .unwrap_or(false) + } + + /// Checks if `path` is a directory. + /// + /// This is similar to [`std::path::Path::is_dir`] in that it checks + /// if `path` relative to `Dir` is a directory. This function will traverse + /// symbolic links to query information about the destination file. In case + /// of broken symbolic links, this will return `false`. + #[inline] + pub async fn is_dir>(&self, path: P) -> bool { + self.metadata(path) + .await + .map(|m| m.is_dir()) + .unwrap_or(false) + } + + /// TODO: Remove this once `create_dir` and friends are async. + #[inline] + fn is_dir_blocking>(&self, path: P) -> bool { + self.metadata_blocking(path) + .map(|m| m.is_dir()) + .unwrap_or(false) + } + + /// Constructs a new instance of `Self` by opening the given path as a + /// directory using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn open_ambient_dir>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = path.as_ref().to_path_buf(); + spawn_blocking(move || open_ambient_dir(path.as_ref(), ambient_authority)) + .await? + .map(|f| Self::from_std_file(f.into())) + } + + /// Constructs a new instance of `Self` by opening the parent directory + /// (aka "..") of `self`, using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function accesses a directory outside of the `self` subtree. + #[inline] + pub async fn open_parent_dir(&self, ambient_authority: AmbientAuthority) -> io::Result { + let clone = self.clone(); + let dir = spawn_blocking(move || { + open_parent_dir( + &*clone.as_filelike_view::(), + ambient_authority, + ) + }) + .await??; + Ok(Self::from_std_file(dir.into())) + } + + /// Recursively create a directory and all of its parent components if they + /// are missing, using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn create_ambient_dir_all>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result<()> { + let _ = ambient_authority; + let path = path.as_ref().to_path_buf(); + tokio::fs::create_dir_all(path).await + } + + /// Construct a new instance of `Self` from existing directory file + /// descriptor. + /// + /// This can be useful when interacting with other libraries and or C/C++ + /// code which has invoked `openat(..., O_DIRECTORY)` external to this + /// crate. + pub async fn reopen_dir(dir: &Filelike) -> io::Result { + // Our public API has a `&Filelike` here, which prevents us from doing + // a `clone` as we usually do. So instead, we use the raw filelike, which + // we can clone and depend on it remaining open until we return. + let raw_filelike = dir.as_filelike_view::().as_raw_filelike(); + // SAFETY: `raw_filelike` remains open for the duration of the + // `reopen_dir` call. + let file = ManuallyDrop::new(unsafe { std::fs::File::from_raw_filelike(raw_filelike) }); + let dir = spawn_blocking(move || { + cap_primitives::fs::open_dir(&*file, std::path::Component::CurDir.as_ref()) + }) + .await??; + Ok(Self::from_std_file(dir.into())) + } +} + +#[cfg(not(target_os = "wasi"))] +#[inline] +fn metadata_from(file: &std::fs::File) -> io::Result { + Metadata::from_file(file) +} + +#[cfg(target_os = "wasi")] +#[inline] +fn metadata_from(file: &std::fs::File) -> io::Result { + file.metadata() +} + +#[cfg(not(windows))] +impl FromRawFd for Dir { + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Self { + Self::from_std_file(fs::File::from_raw_fd(fd)) + } +} + +#[cfg(not(windows))] +impl From for Dir { + #[inline] + fn from(fd: OwnedFd) -> Self { + Self::from_std_file(fs::File::from(fd)) + } +} + +#[cfg(windows)] +impl FromRawHandle for Dir { + /// To prevent race conditions on Windows, the handle must be opened + /// without `FILE_SHARE_DELETE`. + #[inline] + unsafe fn from_raw_handle(handle: RawHandle) -> Self { + Self::from_std_file(fs::File::from_raw_handle(handle)) + } +} + +#[cfg(windows)] +impl From for Dir { + #[inline] + fn from(handle: OwnedHandle) -> Self { + Self::from_std_file(fs::File::from(handle)) + } +} + +#[cfg(not(windows))] +impl AsRawFd for Dir { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std_file.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for Dir { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std_file.as_fd() + } +} + +#[cfg(windows)] +impl AsRawHandle for Dir { + #[inline] + fn as_raw_handle(&self) -> RawHandle { + self.std_file.as_raw_handle() + } +} + +#[cfg(windows)] +impl AsHandle for Dir { + #[inline] + fn as_handle(&self) -> BorrowedHandle<'_> { + self.std_file.as_handle() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for Dir { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.std_file.as_raw_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for Dir { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.std_file.as_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for Dir { + fn try_into_raw_fd(self) -> std::io::Result { + let file = std::sync::Arc::try_unwrap(self.std_file) + .map_err(|_| std::io::Error::other("Dir has other clones"))?; + Ok(file.into_raw_fd()) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(dir: Dir) -> io::Result { + match std::sync::Arc::try_unwrap(dir.std_file) { + Ok(f) => Ok(f.into()), + Err(arc) => Ok(arc.as_fd().try_clone_to_owned()?), + } + } +} + +#[cfg(windows)] +impl TryIntoRawHandle for Dir { + fn try_into_raw_handle(self) -> std::io::Result { + let file = std::sync::Arc::try_unwrap(self.std_file) + .map_err(|_| std::io::Error::other("Dir has other clones"))?; + Ok(file.into_raw_handle()) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedHandle { + type Error = io::Error; + #[inline] + fn try_from(dir: Dir) -> io::Result { + match std::sync::Arc::try_unwrap(dir.std_file) { + Ok(f) => Ok(f.into()), + Err(arc) => Ok(arc.as_handle().try_clone_to_owned()?), + } + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for Dir { + fn try_into_raw_handle_or_socket( + self, + ) -> io::Result { + use TryIntoRawHandle; + let raw = self.try_into_raw_handle()?; + Ok(io_extras::os::windows::RawHandleOrSocket::from_raw_handle( + raw, + )) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + #[inline] + fn try_from(dir: Dir) -> io::Result { + let handle: OwnedHandle = dir.try_into()?; + Ok(handle.into()) + } +} + +/// Indicates how large a buffer to pre-allocate before reading the entire +/// file. +/// +/// Derived from the function of the same name in Rust's library/std/src/fs.rs +/// at revision 108e90ca78f052c0c1c49c42a22c85620be19712. +async fn initial_buffer_size(file: &File) -> usize { + // Allocate one extra byte so the buffer doesn't need to grow before the + // final `read` call at the end of the file. Don't worry about `usize` + // overflow because reading will fail regardless in that case. + file.metadata() + .await + .map(|m| m.len() as usize + 1) + .unwrap_or(0) +} + +impl fmt::Debug for Dir { + // 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("Dir"); + #[cfg(not(windows))] + b.field("fd", &self.std_file.as_raw_fd()); + #[cfg(windows)] + b.field("handle", &self.std_file.as_raw_handle()); + b.finish() + } +} diff --git a/cap-tokio/src/fs/dir_entry.rs b/cap-tokio/src/fs/dir_entry.rs new file mode 100644 index 00000000..c1f18085 --- /dev/null +++ b/cap-tokio/src/fs/dir_entry.rs @@ -0,0 +1,114 @@ +use crate::fs::{Dir, File, FileType, Metadata, OpenOptions}; +#[cfg(unix)] +use rustix::fs::DirEntryExt; +#[cfg(target_os = "wasi")] +use rustix::fs::DirEntryExt; +use std::ffi::OsString; +use std::fmt; +use std::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. +/// +/// TODO: async +pub struct DirEntry { + pub(crate) inner: cap_primitives::fs::DirEntry, +} + +impl DirEntry { + /// Open the file for reading. + #[inline] + pub fn open(&self) -> io::Result { + let file = self.inner.open()?; + Ok(File::from_std(tokio::fs::File::from_std(file))) + } + + /// Open the file with the given options. + #[inline] + pub fn open_with(&self, options: &OpenOptions) -> io::Result { + let file = self.inner.open_with(options)?; + Ok(File::from_std(tokio::fs::File::from_std(file))) + } + + /// Open the entry as a directory. + #[inline] + pub fn open_dir(&self) -> io::Result { + let dir = self.inner.open_dir()?; + Ok(Dir::from_std_file(dir)) + } + + /// Removes the file from its filesystem. + #[inline] + pub fn remove_file(&self) -> io::Result<()> { + self.inner.remove_file() + } + + /// Removes the directory from its filesystem. + #[inline] + pub fn remove_dir(&self) -> io::Result<()> { + self.inner.remove_dir() + } + + /// Returns the metadata for the file that this entry points at. + /// + /// This corresponds to [`std::fs::DirEntry::metadata`]. + #[inline] + pub fn metadata(&self) -> io::Result { + // TODO: Make this async. + self.inner.metadata() + } + + /// Returns the file type for the file that this entry points at. + /// + /// This corresponds to [`std::fs::DirEntry::file_type`]. + #[inline] + pub async fn file_type(&self) -> io::Result { + // TODO: Make this actually async. + self.inner.file_type() + } + + /// 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() + } +} + +#[cfg(windows)] +#[doc(hidden)] +impl cap_primitives::fs::_WindowsDirEntryExt for DirEntry { + #[inline] + fn full_metadata(&self) -> io::Result { + self.inner.full_metadata() + } +} + +impl fmt::Debug for DirEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} diff --git a/cap-tokio/src/fs/file.rs b/cap-tokio/src/fs/file.rs new file mode 100644 index 00000000..93e8cfb0 --- /dev/null +++ b/cap-tokio/src/fs/file.rs @@ -0,0 +1,435 @@ +use crate::fs::{Metadata, OpenOptions, Permissions}; +use crate::try_into_os::*; +use cap_primitives::fs::open_ambient; +use cap_primitives::AmbientAuthority; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsHandle, BorrowedHandle, OwnedHandle}; +use std::fmt; +use std::io; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, FromRawFd, RawFd}; +use std::path::Path; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; +use tokio::task::spawn_blocking; +#[cfg(windows)] +use { + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle}, +}; + +/// A reference to an open file on a filesystem. +/// +/// This corresponds to [`tokio::fs::File`]. +/// +/// This `File` has no `open` or `create` methods. To open or create a file, +/// first obtain a [`Dir`] containing the path, and then call [`Dir::open`] or +/// [`Dir::create`]. +/// +/// [`Dir`]: crate::fs::Dir +/// [`Dir::open`]: crate::fs::Dir::open +/// [`Dir::create`]: crate::fs::Dir::create +pub struct File { + pub(crate) std: tokio::fs::File, +} + +impl File { + /// Constructs a new instance of `Self` from the given + /// `tokio::fs::File`. + /// + /// This grants access the resources the `tokio::fs::File` instance + /// already has access to. + #[inline] + pub fn from_std(std: tokio::fs::File) -> Self { + Self { std } + } + + /// Consumes `self` and returns a `tokio::fs::File`. + #[inline] + pub fn into_std(self) -> tokio::fs::File { + self.std + } + + /// Attempts to sync all OS-internal metadata to disk. + /// + /// This corresponds to [`tokio::fs::File::sync_all`]. + #[inline] + pub async fn sync_all(&self) -> io::Result<()> { + self.std.sync_all().await + } + + /// This function is similar to `sync_all`, except that it may not + /// synchronize file metadata to a filesystem. + /// + /// This corresponds to [`tokio::fs::File::sync_data`]. + #[inline] + pub async fn sync_data(&self) -> io::Result<()> { + self.std.sync_data().await + } + + /// Truncates or extends the underlying file, updating the size of this + /// file to become size. + /// + /// This corresponds to [`tokio::fs::File::set_len`]. + #[inline] + pub async fn set_len(&self, size: u64) -> io::Result<()> { + self.std.set_len(size).await + } + + /// Queries metadata about the underlying file. + /// + /// This corresponds to [`tokio::fs::File::metadata`]. + #[inline] + pub async fn metadata(&self) -> io::Result { + let std_file = self.std.try_clone().await?; + let std_file = std_file.into_std().await; + spawn_blocking(move || metadata_from(&std_file)).await? + } + + /// Changes the permissions on the underlying file. + /// + /// This corresponds to [`tokio::fs::File::set_permissions`]. + #[inline] + pub async fn set_permissions(&self, perm: Permissions) -> io::Result<()> { + let std_file = self.std.try_clone().await?; + let std_file = std_file.into_std().await; + spawn_blocking(move || { + let std_perm = permissions_into_std(&std_file, perm)?; + std_file.set_permissions(std_perm) + }) + .await? + } + + /// Constructs a new instance of `Self` in read-only mode by opening the + /// given path as a file using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn open_ambient>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = path.as_ref().to_path_buf(); + spawn_blocking(move || { + open_ambient( + path.as_ref(), + OpenOptions::new().read(true), + ambient_authority, + ) + }) + .await? + .map(|f| Self::from_std(tokio::fs::File::from_std(f))) + } + + /// Constructs a new instance of `Self` in write-only mode by opening, + /// creating or truncating, the given path as a file using the host + /// process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn create_ambient>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = path.as_ref().to_path_buf(); + spawn_blocking(move || { + open_ambient( + path.as_ref(), + OpenOptions::new().write(true).create(true).truncate(true), + ambient_authority, + ) + }) + .await? + .map(|f| Self::from_std(tokio::fs::File::from_std(f))) + } + + /// Constructs a new instance of `Self` with the options specified by + /// `options` by opening the given path as a file using the host process' + /// ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn open_ambient_with>( + path: P, + options: &OpenOptions, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = path.as_ref().to_path_buf(); + let options = options.clone(); + spawn_blocking(move || open_ambient(path.as_ref(), &options, ambient_authority)) + .await? + .map(|f| Self::from_std(tokio::fs::File::from_std(f))) + } + + /// Returns a new `OpenOptions` object. + /// + /// This corresponds to [`tokio::fs::File::options`]. + #[must_use] + #[inline] + pub fn options() -> OpenOptions { + OpenOptions::new() + } +} + +#[cfg(not(target_os = "wasi"))] +#[inline] +fn metadata_from(file: &std::fs::File) -> io::Result { + Metadata::from_file(file) +} + +#[cfg(target_os = "wasi")] +#[inline] +fn metadata_from(file: &std::fs::File) -> io::Result { + file.metadata() +} + +#[cfg(not(target_os = "wasi"))] +#[inline] +fn permissions_into_std( + file: &std::fs::File, + permissions: Permissions, +) -> io::Result { + permissions.into_std(file) +} + +#[cfg(target_os = "wasi")] +#[inline] +fn permissions_into_std( + _file: &std::fs::File, + permissions: Permissions, +) -> io::Result { + permissions +} + +#[cfg(not(windows))] +impl FromRawFd for File { + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from_raw_fd(fd))) + } +} + +#[cfg(not(windows))] +impl From for File { + #[inline] + fn from(fd: OwnedFd) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from(fd))) + } +} + +#[cfg(windows)] +impl FromRawHandle for File { + #[inline] + unsafe fn from_raw_handle(handle: RawHandle) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from_raw_handle( + handle, + ))) + } +} + +#[cfg(windows)] +impl From for File { + #[inline] + fn from(handle: OwnedHandle) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from(handle))) + } +} + +#[cfg(not(windows))] +impl AsRawFd for File { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for File { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +#[cfg(windows)] +impl AsRawHandle for File { + #[inline] + fn as_raw_handle(&self) -> RawHandle { + self.std.as_raw_handle() + } +} + +#[cfg(windows)] +impl AsHandle for File { + #[inline] + fn as_handle(&self) -> BorrowedHandle<'_> { + self.std.as_handle() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for File { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.std.as_raw_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for File { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.std.as_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for File { + fn try_into_raw_fd(self) -> io::Result { + use std::os::unix::io::IntoRawFd; + let std_file = self.std.try_into_std().map_err(|_| { + io::Error::other("cannot convert tokio File: background operation in progress") + })?; + Ok(std_file.into_raw_fd()) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(file: File) -> io::Result { + let raw = TryIntoRawFd::try_into_raw_fd(file)?; + Ok(unsafe { std::os::unix::io::OwnedFd::from_raw_fd(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandle for File { + fn try_into_raw_handle(self) -> io::Result { + use std::os::windows::io::IntoRawHandle; + let std_file = self.std.try_into_std().map_err(|_| { + io::Error::other("cannot convert tokio File: background operation in progress") + })?; + Ok(std_file.into_raw_handle()) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedHandle { + type Error = io::Error; + #[inline] + fn try_from(file: File) -> io::Result { + use std::os::windows::io::IntoRawHandle; + let raw = TryIntoRawHandle::try_into_raw_handle(file)?; + Ok(unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for File { + fn try_into_raw_handle_or_socket( + self, + ) -> std::io::Result { + use TryIntoRawHandle; + let raw = self.try_into_raw_handle()?; + Ok(io_extras::os::windows::RawHandleOrSocket::from_raw_handle( + raw, + )) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + fn try_from(file: File) -> io::Result { + let handle: OwnedHandle = file.try_into()?; + Ok(handle.into()) + } +} + +impl AsyncRead for File { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.std), cx, buf) + } +} + +impl AsyncWrite for File { + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.std), cx, buf) + } + + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.std), cx) + } + + #[inline] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.std), cx) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + AsyncWrite::poll_write_vectored(Pin::new(&mut self.std), cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.std.is_write_vectored() + } +} + +impl AsyncSeek for File { + #[inline] + fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + Pin::new(&mut self.std).start_seek(position) + } + + #[inline] + fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.std).poll_complete(cx) + } +} + +impl fmt::Debug for File { + // 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("File"); + #[cfg(not(windows))] + b.field("fd", &self.std.as_raw_fd()); + #[cfg(windows)] + b.field("handle", &self.std.as_raw_handle()); + b.finish() + } +} diff --git a/cap-tokio/src/fs/mod.rs b/cap-tokio/src/fs/mod.rs new file mode 100644 index 00000000..b769dfca --- /dev/null +++ b/cap-tokio/src/fs/mod.rs @@ -0,0 +1,47 @@ +//! A capability-based filesystem API modeled after [`tokio::fs`]. +//! +//! This corresponds to [`tokio::fs`]. +//! +//! Instead of [`tokio::fs`'s free functions] and [`tokio::fs::File`]'s +//! constructors which operate on bare paths, this crate has methods on [`Dir`] +//! which operate on paths which must be relative to the directory. +//! +//! Where `tokio` says "the filesystem", this API says "a filesystem", as +//! it doesn't assume that there's a single global filesystem namespace. +//! +//! Since all functions which expose raw file descriptors are `unsafe`, I/O +//! handles in this API are unforgeable (unsafe code notwithstanding). This +//! combined with a lack of absolute paths provides a natural capability-based +//! interface. +//! +//! This crate uses `std::path::Path` rather than having its own path type. +//! To preserve the capability-based interface, avoid using +//! `std::path::Path`'s `canonicalize`, `read_link`, `read_dir`, +//! `metadata`, and `symlink_metadata` functions. +//! +//! [`tokio::fs`'s free functions]: https://docs.rs/tokio/latest/tokio/fs/index.html#functions + +mod dir; +mod dir_entry; +mod file; +mod read_dir; + +pub use dir::Dir; +pub use dir_entry::DirEntry; +pub use file::File; +pub use read_dir::ReadDir; + +// Re-export things from `cap_primitives` that we can use as-is. +#[cfg(not(target_os = "wasi"))] +pub use cap_primitives::fs::{DirBuilder, FileType, Metadata, OpenOptions, Permissions}; + +// Re-export conditional types from `cap_primitives`. +#[cfg(any(unix, target_os = "vxworks", all(windows, windows_file_type_ext)))] +pub use cap_primitives::fs::FileTypeExt; +#[cfg(unix)] +pub use cap_primitives::fs::{DirBuilderExt, PermissionsExt}; +pub use cap_primitives::fs::{FileExt, MetadataExt, OpenOptionsExt}; + +// Re-export things from `std` that we can use as-is. +#[cfg(target_os = "wasi")] +pub use std::fs::{DirBuilder, FileType, Metadata, OpenOptions, Permissions}; diff --git a/cap-tokio/src/fs/read_dir.rs b/cap-tokio/src/fs/read_dir.rs new file mode 100644 index 00000000..cbff1110 --- /dev/null +++ b/cap-tokio/src/fs/read_dir.rs @@ -0,0 +1,31 @@ +use crate::fs::DirEntry; +use std::fmt; +use std::io; + +/// 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: cap_primitives::fs::ReadDir, +} + +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/cap-tokio/src/fs_utf8/dir.rs b/cap-tokio/src/fs_utf8/dir.rs new file mode 100644 index 00000000..cd5b1221 --- /dev/null +++ b/cap-tokio/src/fs_utf8/dir.rs @@ -0,0 +1,813 @@ +use crate::fs::{OpenOptions, Permissions}; +use crate::fs_utf8::{from_utf8, to_utf8, DirBuilder, File, Metadata, ReadDir}; +use crate::try_into_os::*; +use camino::{Utf8Path, Utf8PathBuf}; +use cap_primitives::AmbientAuthority; +use io_lifetimes::raw::{AsRawFilelike, FromRawFilelike}; +use io_lifetimes::AsFilelike; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsHandle, BorrowedHandle, OwnedHandle}; +use std::fmt; +use std::mem::ManuallyDrop; +use std::{fs, io}; +#[cfg(unix)] +use { + crate::os::unix::net::{UnixDatagram, UnixListener, UnixStream}, + std::os::unix::io::{AsRawFd, FromRawFd, RawFd}, +}; +#[cfg(windows)] +use { + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle}, +}; + +/// A reference to an open directory on a filesystem. +/// +/// This does not directly correspond to anything in `tokio`, however its +/// methods correspond to the [functions in `tokio::fs`] and the +/// constructor methods for [`tokio::fs::File`]. +/// +/// Unlike `tokio::fs`, this API's `canonicalize` returns a relative path +/// since absolute paths don't interoperate well with the capability model. +/// +/// [functions in `tokio::fs`]: https://docs.rs/tokio/latest/tokio/fs/index.html#functions +#[derive(Clone)] +pub struct Dir { + cap_std: crate::fs::Dir, +} + +impl Dir { + /// Constructs a new instance of `Self` from the given + /// `std::fs::File`. + /// + /// To prevent race conditions on Windows, the file must be opened without + /// `FILE_SHARE_DELETE`. + /// + /// This grants access the resources the `std::fs::File` instance + /// already has access to. + #[inline] + pub fn from_std_file(std_file: fs::File) -> Self { + Self::from_cap_std(crate::fs::Dir::from_std_file(std_file)) + } + + /// Constructs a new instance of `Self` from the given `cap_std::fs::Dir`. + #[inline] + pub fn from_cap_std(cap_std: crate::fs::Dir) -> Self { + Self { cap_std } + } + + /// Attempts to open a file in read-only mode. + /// + /// This corresponds to [`tokio::fs::File::open`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn open>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.open(path).await.map(File::from_cap_std) + } + + /// Opens a file at `path` with the options specified by `options`. + /// + /// This corresponds to [`tokio::fs::OpenOptions::open`]. + /// + /// Instead of being a method on `OpenOptions`, this is a method on `Dir`, + /// and it only accesses paths relative to `self`. + #[inline] + pub async fn open_with>( + &self, + path: P, + options: &OpenOptions, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std + .open_with(path, options) + .await + .map(File::from_cap_std) + } + + /// Attempts to open a directory. + #[inline] + pub async fn open_dir>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.open_dir(path).await.map(Self::from_cap_std) + } + + /// Creates a new, empty directory at the provided path. + /// + /// This corresponds to [`tokio::fs::create_dir`], but only accesses + /// paths relative to `self`. + /// + /// TODO: async: fix this when we fix https://github.com/bytecodealliance/cap-std/issues/51 + #[inline] + pub fn create_dir>(&self, path: P) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.create_dir(path) + } + + /// Recursively create a directory and all of its parent components if they + /// are missing. + /// + /// This corresponds to [`tokio::fs::create_dir_all`], but only + /// accesses paths relative to `self`. + /// + /// TODO: async: fix this when we fix https://github.com/bytecodealliance/cap-std/issues/51 + #[inline] + pub fn create_dir_all>(&self, path: P) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.create_dir_all(path) + } + + /// Creates the specified directory with the options configured in this + /// builder. + /// + /// This corresponds to [`tokio::fs::DirBuilder::create`]. + /// + /// TODO: async: fix this when we fix https://github.com/bytecodealliance/cap-std/issues/51 + #[inline] + pub fn create_dir_with>( + &self, + path: P, + dir_builder: &DirBuilder, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.create_dir_with(path, dir_builder) + } + + /// Opens a file in write-only mode. + /// + /// This corresponds to [`tokio::fs::File::create`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn create>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.create(path).await.map(File::from_cap_std) + } + + /// Returns the canonical form of a path with all intermediate components + /// normalized and symbolic links resolved. + /// + /// This corresponds to [`tokio::fs::canonicalize`], but instead of + /// returning an absolute path, returns a path relative to the + /// directory represented by `self`. + #[inline] + pub async fn canonicalize>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.canonicalize(path).await.and_then(to_utf8) + } + + /// Copies the contents of one file to another. This function will also + /// copy the permission bits of the original file to the destination + /// file. + /// + /// This corresponds to [`tokio::fs::copy`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn copy, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result { + let from = from_utf8(from.as_ref())?; + let to = from_utf8(to.as_ref())?; + self.cap_std.copy(from, &to_dir.cap_std, to).await + } + + /// Creates a new hard link on a filesystem. + /// + /// This corresponds to [`tokio::fs::hard_link`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn hard_link, Q: AsRef>( + &self, + src: P, + dst_dir: &Self, + dst: Q, + ) -> io::Result<()> { + let src = from_utf8(src.as_ref())?; + let dst = from_utf8(dst.as_ref())?; + self.cap_std.hard_link(src, &dst_dir.cap_std, dst).await + } + + /// Given a path, query the file system to get information about a file, + /// directory, etc. + /// + /// This corresponds to [`tokio::fs::metadata`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn metadata>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.metadata(path).await + } + + /// Queries metadata about the underlying directory. + /// + /// This is similar to [`std::fs::File::metadata`], but for `Dir` rather + /// than for `File`. + #[inline] + pub async fn dir_metadata(&self) -> io::Result { + self.cap_std.dir_metadata().await + } + + /// Returns an iterator over the entries within `self`. + #[inline] + pub async fn entries(&self) -> io::Result { + self.cap_std.entries().await.map(ReadDir::from_cap_std) + } + + /// Returns an iterator over the entries within a directory. + /// + /// This corresponds to [`tokio::fs::read_dir`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn read_dir>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.read_dir(path).await.map(ReadDir::from_cap_std) + } + + /// Read the entire contents of a file into a bytes vector. + /// + /// This corresponds to [`tokio::fs::read`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn read>(&self, path: P) -> io::Result> { + let path = from_utf8(path.as_ref())?; + self.cap_std.read(path).await + } + + /// Reads a symbolic link, returning the file that the link points to. + /// + /// This corresponds to [`tokio::fs::read_link`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn read_link>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.read_link(path).await.and_then(to_utf8) + } + + /// Read the entire contents of a file into a string. + /// + /// This corresponds to [`tokio::fs::read_to_string`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn read_to_string>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.read_to_string(path).await + } + + /// Removes an empty directory. + /// + /// This corresponds to [`tokio::fs::remove_dir`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn remove_dir>(&self, path: P) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.remove_dir(path).await + } + + /// Removes a directory at this path, after removing all its contents. Use + /// carefully! + /// + /// This corresponds to [`tokio::fs::remove_dir_all`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn remove_dir_all>(&self, path: P) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.remove_dir_all(path).await + } + + /// Remove the directory referenced by `self` and consume `self`. + /// + /// Even though this implementation works in terms of handles as much as + /// possible, removal is not guaranteed to be atomic with respect to a + /// concurrent rename of the directory. + #[inline] + pub async fn remove_open_dir(self) -> io::Result<()> { + self.cap_std.remove_open_dir().await + } + + /// Removes the directory referenced by `self`, after removing all its + /// contents, and consume `self`. Use carefully! + /// + /// Even though this implementation works in terms of handles as much as + /// possible, removal is not guaranteed to be atomic with respect to a + /// concurrent rename of the directory. + #[inline] + pub async fn remove_open_dir_all(self) -> io::Result<()> { + self.cap_std.remove_open_dir_all().await + } + + /// Removes a file from a filesystem. + /// + /// This corresponds to [`tokio::fs::remove_file`], but only accesses + /// paths relative to `self`. + #[inline] + pub async fn remove_file>(&self, path: P) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.remove_file(path).await + } + + /// Rename a file or directory to a new name, replacing the original file + /// if to already exists. + /// + /// This corresponds to [`tokio::fs::rename`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn rename, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result<()> { + let from = from_utf8(from.as_ref())?; + let to = from_utf8(to.as_ref())?; + self.cap_std.rename(from, &to_dir.cap_std, to).await + } + + /// Changes the permissions found on a file or a directory. + /// + /// This corresponds to [`tokio::fs::set_permissions`], but only + /// accesses paths relative to `self`. Also, on some platforms, this + /// function may fail if the file or directory cannot be opened for + /// reading or writing first. + pub async fn set_permissions>( + &self, + path: P, + perm: Permissions, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.set_permissions(path, perm).await + } + + /// Query the metadata about a file without following symlinks. + /// + /// This corresponds to [`tokio::fs::symlink_metadata`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn symlink_metadata>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.symlink_metadata(path).await + } + + /// Write a slice as the entire contents of a file. + /// + /// This corresponds to [`tokio::fs::write`], but only accesses paths + /// relative to `self`. + #[inline] + pub async fn write, C: AsRef<[u8]>>( + &self, + path: P, + contents: C, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std.write(path, contents).await + } + + /// Creates a new symbolic link on a filesystem. + /// + /// The `original` argument provides the target of the symlink. The `link` + /// argument provides the name of the created symlink. + /// + /// Despite the argument ordering, `original` is not resolved relative to + /// `self` here. `link` is resolved relative to `self`, and `original` is + /// not resolved within this function. + /// + /// The `link` path is resolved when the symlink is dereferenced, relative + /// to the directory that contains it. + /// + /// This corresponds to [`tokio::os::unix::fs::symlink`], but only + /// accesses paths relative to `self`. + /// + /// [`tokio::os::unix::fs::symlink`]: https://docs.rs/tokio/latest/tokio/os/unix/fs/fn.symlink.html + #[cfg(not(windows))] + #[inline] + pub async fn symlink, Q: AsRef>( + &self, + original: P, + link: Q, + ) -> io::Result<()> { + let original = from_utf8(original.as_ref())?; + let link = from_utf8(link.as_ref())?; + self.cap_std.symlink(original, link).await + } + + /// Creates a new file symbolic link on a filesystem. + /// + /// The `original` argument provides the target of the symlink. The `link` + /// argument provides the name of the created symlink. + /// + /// Despite the argument ordering, `original` is not resolved relative to + /// `self` here. `link` is resolved relative to `self`, and `original` is + /// not resolved within this function. + /// + /// The `link` path is resolved when the symlink is dereferenced, relative + /// to the directory that contains it. + /// + /// This corresponds to [`tokio::os::windows::fs::symlink_file`], but + /// only accesses paths relative to `self`. + /// + /// [`tokio::os::windows::fs::symlink_file`]: https://docs.rs/tokio/latest/tokio/os/windows/fs/fn.symlink_file.html + #[cfg(windows)] + #[inline] + pub async fn symlink_file, Q: AsRef>( + &self, + original: P, + link: Q, + ) -> io::Result<()> { + let original = from_utf8(original.as_ref())?; + let link = from_utf8(link.as_ref())?; + self.cap_std.symlink_file(original, link).await + } + + /// Creates a new directory symlink on a filesystem. + /// + /// The `original` argument provides the target of the symlink. The `link` + /// argument provides the name of the created symlink. + /// + /// Despite the argument ordering, `original` is not resolved relative to + /// `self` here. `link` is resolved relative to `self`, and `original` is + /// not resolved within this function. + /// + /// The `link` path is resolved when the symlink is dereferenced, relative + /// to the directory that contains it. + /// + /// This corresponds to [`tokio::os::windows::fs::symlink_dir`], but + /// only accesses paths relative to `self`. + /// + /// [`tokio::os::windows::fs::symlink_dir`]: https://docs.rs/tokio/latest/tokio/os/windows/fs/fn.symlink_dir.html + #[cfg(windows)] + #[inline] + pub async fn symlink_dir, Q: AsRef>( + &self, + original: P, + link: Q, + ) -> io::Result<()> { + let original = from_utf8(original.as_ref())?; + let link = from_utf8(link.as_ref())?; + self.cap_std.symlink_dir(original, link).await + } + + /// Creates a new `UnixListener` bound to the specified socket. + /// + /// This corresponds to [`tokio::os::unix::net::UnixListener::bind`], + /// but only accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + /// + /// [`tokio::os::unix::net::UnixListener::bind`]: https://docs.rs/tokio/latest/tokio/os/unix/net/struct.UnixListener.html#method.bind + #[doc(alias = "bind")] + #[cfg(unix)] + #[inline] + pub async fn bind_unix_listener>( + &self, + path: P, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.bind_unix_listener(path).await + } + + /// Connects to the socket named by path. + /// + /// This corresponds to [`tokio::os::unix::net::UnixStream::connect`], + /// but only accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + /// + /// [`tokio::os::unix::net::UnixStream::connect`]: https://docs.rs/tokio/latest/tokio/os/unix/net/struct.UnixStream.html#method.connect + #[doc(alias = "connect")] + #[cfg(unix)] + #[inline] + pub async fn connect_unix_stream>(&self, path: P) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.connect_unix_stream(path).await + } + + /// Creates a Unix datagram socket bound to the given path. + /// + /// This corresponds to [`tokio::os::unix::net::UnixDatagram::bind`], + /// but only accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + /// + /// [`tokio::os::unix::net::UnixDatagram::bind`]: https://docs.rs/tokio/latest/tokio/os/unix/net/struct.UnixDatagram.html#method.bind + #[doc(alias = "bind")] + #[cfg(unix)] + #[inline] + pub async fn bind_unix_datagram>( + &self, + path: P, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std.bind_unix_datagram(path).await + } + + /// Connects the socket to the specified address. + /// + /// This corresponds to + /// [`tokio::os::unix::net::UnixDatagram::connect`], but only + /// accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + /// + /// [`tokio::os::unix::net::UnixDatagram::connect`]: https://docs.rs/tokio/latest/tokio/os/unix/net/struct.UnixDatagram.html#method.connect + #[doc(alias = "connect")] + #[cfg(unix)] + #[inline] + pub async fn connect_unix_datagram>( + &self, + unix_datagram: &UnixDatagram, + path: P, + ) -> io::Result<()> { + let path = from_utf8(path.as_ref())?; + self.cap_std + .connect_unix_datagram(unix_datagram, path) + .await + } + + /// Sends data on the socket to the specified address. + /// + /// This corresponds to + /// [`tokio::os::unix::net::UnixDatagram::send_to`], but only + /// accesses paths relative to `self`. + /// + /// XXX: This function is not yet implemented. + /// + /// [`tokio::os::unix::net::UnixDatagram::send_to`]: https://docs.rs/tokio/latest/tokio/os/unix/net/struct.UnixDatagram.html#method.send_to + #[doc(alias = "send_to")] + #[cfg(unix)] + #[inline] + pub async fn send_to_unix_datagram_addr>( + &self, + unix_datagram: &UnixDatagram, + buf: &[u8], + path: P, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + self.cap_std + .send_to_unix_datagram_addr(unix_datagram, buf, path) + .await + } + + // tokio doesn't have `try_clone`. + + /// Returns `true` if the path points at an existing entity. + /// + /// This corresponds to [`tokio::path::Path::exists`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn exists>(&self, path: P) -> bool { + match from_utf8(path.as_ref()) { + Ok(path) => self.cap_std.exists(path).await, + Err(_) => false, + } + } + + /// Returns `true` if the path points at an existing entity. + /// + /// This corresponds to [`tokio::path::Path::exists`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn try_exists>(&self, path: P) -> io::Result { + self.cap_std.try_exists(from_utf8(path.as_ref())?).await + } + + /// Returns `true` if the path exists on disk and is pointing at a regular + /// file. + /// + /// This corresponds to [`tokio::path::Path::is_file`], but only + /// accesses paths relative to `self`. + #[inline] + pub async fn is_file>(&self, path: P) -> bool { + match from_utf8(path.as_ref()) { + Ok(path) => self.cap_std.is_file(path).await, + Err(_) => false, + } + } + + /// Checks if `path` is a directory. + /// + /// This is similar to [`tokio::path::Path::is_dir`] in that it checks + /// if `path` relative to `Dir` is a directory. This function will traverse + /// symbolic links to query information about the destination file. In case + /// of broken symbolic links, this will return `false`. + #[inline] + pub async fn is_dir>(&self, path: P) -> bool { + match from_utf8(path.as_ref()) { + Ok(path) => self.cap_std.is_dir(path).await, + Err(_) => false, + } + } + + /// Constructs a new instance of `Self` by opening the given path as a + /// directory using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn open_ambient_dir>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + crate::fs::Dir::open_ambient_dir(path, ambient_authority) + .await + .map(Self::from_cap_std) + } + + /// Constructs a new instance of `Self` by opening the parent directory + /// (aka "..") of `self`, using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function accesses a directory outside of the `self` subtree. + #[inline] + pub async fn open_parent_dir(&self, ambient_authority: AmbientAuthority) -> io::Result { + self.cap_std + .open_parent_dir(ambient_authority) + .await + .map(Self::from_cap_std) + } + + /// Recursively create a directory and all of its parent components if they + /// are missing, using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn create_ambient_dir_all>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result<()> { + let _ = ambient_authority; + let path = from_utf8(path.as_ref())?; + tokio::fs::create_dir_all(path).await + } + + /// Construct a new instance of `Self` from existing directory file + /// descriptor. + /// + /// This can be useful when interacting with other libraries and or C/C++ + /// code which has invoked `openat(..., O_DIRECTORY)` external to this + /// crate. + pub async fn reopen_dir(dir: &Filelike) -> io::Result { + // Our public API has a `&Filelike` here, which prevents us from doing + // a `clone` as we usually do. So instead, we use the raw fd, which we + // can clone and depend on it remaining open until we return. + let raw_filelike = dir.as_filelike_view::().as_raw_filelike(); + // SAFETY: `raw_filelike` remains open for the duration of the `reopen_dir` + // call. + let file = ManuallyDrop::new(unsafe { std::fs::File::from_raw_filelike(raw_filelike) }); + crate::fs::Dir::reopen_dir(&*file) + .await + .map(Self::from_cap_std) + } +} + +#[cfg(not(windows))] +impl FromRawFd for Dir { + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Self { + Self::from_std_file(fs::File::from_raw_fd(fd)) + } +} + +#[cfg(not(windows))] +impl From for Dir { + #[inline] + fn from(fd: OwnedFd) -> Self { + Self::from_std_file(fs::File::from(fd)) + } +} + +#[cfg(windows)] +impl FromRawHandle for Dir { + /// To prevent race conditions on Windows, the handle must be opened + /// without `FILE_SHARE_DELETE`. + #[inline] + unsafe fn from_raw_handle(handle: RawHandle) -> Self { + Self::from_std_file(fs::File::from_raw_handle(handle)) + } +} + +#[cfg(windows)] +impl From for Dir { + #[inline] + fn from(handle: OwnedHandle) -> Self { + Self::from_std_file(fs::File::from(handle)) + } +} + +#[cfg(not(windows))] +impl AsRawFd for Dir { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.cap_std.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for Dir { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.cap_std.as_fd() + } +} + +#[cfg(windows)] +impl AsRawHandle for Dir { + #[inline] + fn as_raw_handle(&self) -> RawHandle { + self.cap_std.as_raw_handle() + } +} + +#[cfg(windows)] +impl AsHandle for Dir { + #[inline] + fn as_handle(&self) -> BorrowedHandle<'_> { + self.cap_std.as_handle() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for Dir { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.cap_std.as_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for Dir { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.cap_std.as_raw_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for Dir { + fn try_into_raw_fd(self) -> std::io::Result { + TryIntoRawFd::try_into_raw_fd(self.cap_std) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(dir: Dir) -> io::Result { + let raw = TryIntoRawFd::try_into_raw_fd(dir)?; + Ok(unsafe { std::os::unix::io::OwnedFd::from_raw_fd(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandle for Dir { + fn try_into_raw_handle(self) -> std::io::Result { + TryIntoRawHandle::try_into_raw_handle(self.cap_std) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedHandle { + type Error = io::Error; + #[inline] + fn try_from(dir: Dir) -> io::Result { + use std::os::windows::io::IntoRawHandle; + let raw = TryIntoRawHandle::try_into_raw_handle(dir)?; + Ok(unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for Dir { + fn try_into_raw_handle_or_socket( + self, + ) -> std::io::Result { + TryIntoRawHandleOrSocket::try_into_raw_handle_or_socket(self.cap_std) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + fn try_from(dir: Dir) -> io::Result { + let handle: OwnedHandle = dir.try_into()?; + Ok(handle.into()) + } +} + +impl fmt::Debug for Dir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.cap_std.fmt(f) + } +} diff --git a/cap-tokio/src/fs_utf8/dir_entry.rs b/cap-tokio/src/fs_utf8/dir_entry.rs new file mode 100644 index 00000000..2674a5eb --- /dev/null +++ b/cap-tokio/src/fs_utf8/dir_entry.rs @@ -0,0 +1,119 @@ +use crate::fs::{FileType, Metadata, OpenOptions}; +use crate::fs_utf8::{to_utf8, Dir, File}; +#[cfg(not(windows))] +use rustix::fs::DirEntryExt; +use std::{fmt, io}; + +/// Entries returned by the `ReadDir` iterator. +/// +/// This corresponds to [`tokio::fs::DirEntry`]. +/// +/// Unlike `tokio::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 `tokio::fs::DirEntry` doesn't provide +/// a way to construct a `DirEntry` without opening directories by ambient +/// paths. +/// +/// TODO: async +pub struct DirEntry { + cap_std: crate::fs::DirEntry, +} + +impl DirEntry { + /// Constructs a new instance of `Self` from the given + /// `cap_std::fs::DirEntry`. + #[inline] + pub fn from_cap_std(cap_std: crate::fs::DirEntry) -> Self { + Self { cap_std } + } + + /// Open the file for reading. + #[inline] + pub fn open(&self) -> io::Result { + self.cap_std.open().map(File::from_cap_std) + } + + /// Open the file with the given options. + #[inline] + pub fn open_with(&self, options: &OpenOptions) -> io::Result { + self.cap_std.open_with(options).map(File::from_cap_std) + } + + /// Open the entry as a directory. + #[inline] + pub fn open_dir(&self) -> io::Result { + self.cap_std.open_dir().map(Dir::from_cap_std) + } + + /// Removes the file from its filesystem. + #[inline] + pub fn remove_file(&self) -> io::Result<()> { + self.cap_std.remove_file() + } + + /// Removes the directory from its filesystem. + #[inline] + pub fn remove_dir(&self) -> io::Result<()> { + self.cap_std.remove_dir() + } + + /// Returns the metadata for the file that this entry points at. + /// + /// This corresponds to [`tokio::fs::DirEntry::metadata`]. + #[inline] + pub fn metadata(&self) -> io::Result { + self.cap_std.metadata() + } + + /// Returns the file type for the file that this entry points at. + /// + /// This corresponds to [`tokio::fs::DirEntry::file_type`]. + #[inline] + pub async fn file_type(&self) -> io::Result { + self.cap_std.file_type().await + } + + /// Returns the bare file name of this directory entry without any other + /// leading path component. + /// + /// This function returns an `Err` in the case that the file name isn't + /// encodable as UTF-8. + /// + /// If the `arf_strings` feature is enabled, unencodable names are + /// translated to UTF-8 using `arf-strings`. + /// + /// This corresponds to [`tokio::fs::DirEntry::file_name`]. + #[inline] + pub fn file_name(&self) -> io::Result { + Ok(to_utf8(self.cap_std.file_name())?.into()) + } +} + +#[cfg(not(windows))] +impl DirEntryExt for DirEntry { + #[inline] + fn ino(&self) -> u64 { + self.cap_std.ino() + } +} + +#[cfg(windows)] +#[doc(hidden)] +impl cap_primitives::fs::_WindowsDirEntryExt for DirEntry { + #[inline] + fn full_metadata(&self) -> io::Result { + self.cap_std.full_metadata() + } +} + +impl fmt::Debug for DirEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.cap_std.fmt(f) + } +} diff --git a/cap-tokio/src/fs_utf8/file.rs b/cap-tokio/src/fs_utf8/file.rs new file mode 100644 index 00000000..8f1468cd --- /dev/null +++ b/cap-tokio/src/fs_utf8/file.rs @@ -0,0 +1,371 @@ +use crate::fs::{Metadata, OpenOptions, Permissions}; +use crate::fs_utf8::from_utf8; +use crate::try_into_os::*; +use camino::Utf8Path; +use cap_primitives::AmbientAuthority; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsHandle, BorrowedHandle, OwnedHandle}; +use std::fmt; +use std::io; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, FromRawFd, RawFd}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; +#[cfg(windows)] +use { + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle}, +}; + +/// A reference to an open file on a filesystem. +/// +/// This corresponds to [`tokio::fs::File`]. +/// +/// This `File` has no `open` or `create` methods. To open or create a file, +/// first obtain a [`Dir`] containing the path, and then call [`Dir::open`] or +/// [`Dir::create`]. +/// +/// [`Dir`]: crate::fs::Dir +/// [`Dir::open`]: crate::fs::Dir::open +/// [`Dir::create`]: crate::fs::Dir::create +pub struct File { + cap_std: crate::fs::File, +} + +impl File { + /// Constructs a new instance of `Self` from the given + /// `tokio::fs::File`. + /// + /// This grants access the resources the `tokio::fs::File` instance + /// already has access to. + #[inline] + pub fn from_std(std: tokio::fs::File) -> Self { + Self::from_cap_std(crate::fs::File::from_std(std)) + } + + /// Constructs a new instance of `Self` from the given `cap_std::fs::File`. + #[inline] + pub fn from_cap_std(cap_std: crate::fs::File) -> Self { + Self { cap_std } + } + + /// Consumes `self` and returns a `tokio::fs::File`. + #[inline] + pub fn into_std(self) -> tokio::fs::File { + self.cap_std.into_std() + } + + /// Attempts to sync all OS-internal metadata to disk. + /// + /// This corresponds to [`tokio::fs::File::sync_all`]. + #[inline] + pub async fn sync_all(&self) -> io::Result<()> { + self.cap_std.sync_all().await + } + + /// This function is similar to `sync_all`, except that it may not + /// synchronize file metadata to a filesystem. + /// + /// This corresponds to [`tokio::fs::File::sync_data`]. + #[inline] + pub async fn sync_data(&self) -> io::Result<()> { + self.cap_std.sync_data().await + } + + /// Truncates or extends the underlying file, updating the size of this + /// file to become size. + /// + /// This corresponds to [`tokio::fs::File::set_len`]. + #[inline] + pub async fn set_len(&self, size: u64) -> io::Result<()> { + self.cap_std.set_len(size).await + } + + /// Queries metadata about the underlying file. + /// + /// This corresponds to [`tokio::fs::File::metadata`]. + #[inline] + pub async fn metadata(&self) -> io::Result { + self.cap_std.metadata().await + } + + /// Changes the permissions on the underlying file. + /// + /// This corresponds to [`tokio::fs::File::set_permissions`]. + #[inline] + pub async fn set_permissions(&self, perm: Permissions) -> io::Result<()> { + self.cap_std.set_permissions(perm).await + } + + /// Constructs a new instance of `Self` in read-only mode by opening the + /// given path as a file using the host process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn open_ambient>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + crate::fs::File::open_ambient(path, ambient_authority) + .await + .map(Self::from_cap_std) + } + + /// Constructs a new instance of `Self` in write-only mode by opening, + /// creating or truncating, the given path as a file using the host + /// process' ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn create_ambient>( + path: P, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + crate::fs::File::create_ambient(path, ambient_authority) + .await + .map(Self::from_cap_std) + } + + /// Constructs a new instance of `Self` with the options specified by + /// `options` by opening the given path as a file using the host process' + /// ambient authority. + /// + /// # Ambient Authority + /// + /// This function is not sandboxed and may access any path that the host + /// process has access to. + #[inline] + pub async fn open_ambient_with>( + path: P, + options: &OpenOptions, + ambient_authority: AmbientAuthority, + ) -> io::Result { + let path = from_utf8(path.as_ref())?; + crate::fs::File::open_ambient_with(path, options, ambient_authority) + .await + .map(Self::from_cap_std) + } + + /// Returns a new `OpenOptions` object. + /// + /// This corresponds to [`tokio::fs::File::options`]. + #[must_use] + #[inline] + pub fn options() -> OpenOptions { + OpenOptions::new() + } +} + +#[cfg(not(windows))] +impl FromRawFd for File { + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from_raw_fd(fd))) + } +} + +#[cfg(not(windows))] +impl From for File { + #[inline] + fn from(fd: OwnedFd) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from(fd))) + } +} + +#[cfg(windows)] +impl FromRawHandle for File { + #[inline] + unsafe fn from_raw_handle(handle: RawHandle) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from_raw_handle( + handle, + ))) + } +} + +#[cfg(windows)] +impl From for File { + #[inline] + fn from(handle: OwnedHandle) -> Self { + Self::from_std(tokio::fs::File::from_std(std::fs::File::from(handle))) + } +} + +#[cfg(not(windows))] +impl AsRawFd for File { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.cap_std.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for File { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.cap_std.as_fd() + } +} + +#[cfg(windows)] +impl AsRawHandle for File { + #[inline] + fn as_raw_handle(&self) -> RawHandle { + self.cap_std.as_raw_handle() + } +} + +#[cfg(windows)] +impl AsHandle for File { + #[inline] + fn as_handle(&self) -> BorrowedHandle<'_> { + self.cap_std.as_handle() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for File { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.cap_std.as_raw_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for File { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.cap_std.as_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for File { + fn try_into_raw_fd(self) -> std::io::Result { + TryIntoRawFd::try_into_raw_fd(self.cap_std) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(file: File) -> io::Result { + let raw = TryIntoRawFd::try_into_raw_fd(file)?; + Ok(unsafe { std::os::unix::io::OwnedFd::from_raw_fd(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandle for File { + fn try_into_raw_handle(self) -> std::io::Result { + TryIntoRawHandle::try_into_raw_handle(self.cap_std) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedHandle { + type Error = io::Error; + #[inline] + fn try_from(file: File) -> io::Result { + use std::os::windows::io::IntoRawHandle; + let raw = TryIntoRawHandle::try_into_raw_handle(file)?; + Ok(unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for File { + fn try_into_raw_handle_or_socket( + self, + ) -> std::io::Result { + TryIntoRawHandleOrSocket::try_into_raw_handle_or_socket(self.cap_std) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + fn try_from(file: File) -> io::Result { + let handle: OwnedHandle = file.try_into()?; + Ok(handle.into()) + } +} + +impl AsyncRead for File { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.cap_std), cx, buf) + } +} + +impl AsyncWrite for File { + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.cap_std), cx, buf) + } + + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.cap_std), cx) + } + + #[inline] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.cap_std), cx) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + AsyncWrite::poll_write_vectored(Pin::new(&mut self.cap_std), cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.cap_std.is_write_vectored() + } +} + +impl AsyncSeek for File { + #[inline] + fn start_seek(mut self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + Pin::new(&mut self.cap_std).start_seek(position) + } + + #[inline] + fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.cap_std).poll_complete(cx) + } +} + +impl fmt::Debug for File { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.cap_std.fmt(f) + } +} diff --git a/cap-tokio/src/fs_utf8/mod.rs b/cap-tokio/src/fs_utf8/mod.rs new file mode 100644 index 00000000..bfd5e12e --- /dev/null +++ b/cap-tokio/src/fs_utf8/mod.rs @@ -0,0 +1,102 @@ +//! A fully UTF-8 filesystem API modeled after [`cap_tokio::fs`]. +//! +//! Where `cap_tokio::fs` would use `Path` and `PathBuf`, this `fs_utf8` +//! module uses [`Utf8Path`] and [`Utf8PathBuf`], meaning that all paths are +//! valid UTF-8. +//! +//! If you don't want this, use the regular [`cap_tokio::fs`] module +//! instead. +//! +//! [`cap_tokio::fs`]: ../fs/ + +mod dir; +mod dir_entry; +mod file; +mod read_dir; + +pub use dir::Dir; +pub use dir_entry::DirEntry; +pub use file::File; +pub use read_dir::ReadDir; + +// Re-export things from `cap_std::fs` that we can use as-is. +pub use crate::fs::{DirBuilder, FileType, Metadata, OpenOptions, Permissions}; + +// Re-export conditional types from `cap_primitives`. +#[cfg(any(unix, target_os = "vxworks", all(windows, windows_file_type_ext)))] +pub use cap_primitives::fs::FileTypeExt; +#[cfg(unix)] +pub use cap_primitives::fs::{DirBuilderExt, PermissionsExt}; +pub use cap_primitives::fs::{FileExt, MetadataExt, OpenOptionsExt}; + +// Re-export `camino` to make it easy for users to depend on the same +// version we do, because we use its types in our public API. +pub use camino; + +use camino::{Utf8Path, Utf8PathBuf}; + +#[cfg(not(feature = "arf_strings"))] +fn from_utf8<'a>(path: &'a Utf8Path) -> std::io::Result<&'a std::path::Path> { + Ok(path.as_std_path()) +} + +#[cfg(feature = "arf_strings")] +fn from_utf8<'a>(path: &'a Utf8Path) -> std::io::Result { + #[cfg(not(windows))] + let path = { + #[cfg(unix)] + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; + #[cfg(target_os = "wasi")] + use std::{ffi::OsString, os::wasi::ffi::OsStringExt}; + + let string = arf_strings::str_to_host(path.as_str())?; + OsString::from_vec(string.into_bytes()) + }; + + #[cfg(windows)] + let path = arf_strings::str_to_host(path.as_str())?; + + Ok(path.into()) +} + +fn to_utf8>(path: P) -> std::io::Result { + #[cfg(not(feature = "arf_strings"))] + #[cfg(not(windows))] + { + Ok(Utf8Path::from_path(path.as_ref()) + .ok_or_else(|| ::rustix::io::Errno::ILSEQ)? + .to_path_buf()) + } + + #[cfg(not(feature = "arf_strings"))] + #[cfg(windows)] + { + Ok(Utf8Path::from_path(path.as_ref()) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "filesystem path is not valid UTF-8", + ) + })? + .to_path_buf()) + } + + #[cfg(feature = "arf_strings")] + { + // For now, for WASI use the same logic as other OS's, but + // in the future, the idea is we could avoid this. + let osstr = path.as_ref().as_os_str(); + + #[cfg(not(windows))] + { + arf_strings::host_os_str_to_str(osstr) + .map(std::borrow::Cow::into_owned) + .map(Into::into) + } + + #[cfg(windows)] + { + arf_strings::host_to_str(osstr).map(Into::into) + } + } +} diff --git a/cap-tokio/src/fs_utf8/read_dir.rs b/cap-tokio/src/fs_utf8/read_dir.rs new file mode 100644 index 00000000..e9b37351 --- /dev/null +++ b/cap-tokio/src/fs_utf8/read_dir.rs @@ -0,0 +1,38 @@ +use crate::fs_utf8::DirEntry; +use std::{fmt, io}; + +/// Iterator over the entries in a directory. +/// +/// This corresponds to [`tokio::fs::ReadDir`]. +/// +/// There is no `from_std` method, as `tokio::fs::ReadDir` doesn't provide +/// a way to construct a `ReadDir` without opening directories by ambient +/// paths. +pub struct ReadDir { + cap_std: crate::fs::ReadDir, +} + +impl ReadDir { + /// Constructs a new instance of `Self` from the given `cap_std::fs::File`. + #[inline] + pub fn from_cap_std(cap_std: crate::fs::ReadDir) -> Self { + Self { cap_std } + } +} + +impl Iterator for ReadDir { + type Item = io::Result; + + #[inline] + fn next(&mut self) -> Option { + self.cap_std + .next() + .map(|result| result.map(DirEntry::from_cap_std)) + } +} + +impl fmt::Debug for ReadDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.cap_std.fmt(f) + } +} diff --git a/cap-tokio/src/lib.rs b/cap-tokio/src/lib.rs new file mode 100644 index 00000000..13e314ad --- /dev/null +++ b/cap-tokio/src/lib.rs @@ -0,0 +1,53 @@ +//! A capability-based API modeled after [`tokio`]. +//! +//! This corresponds to [`tokio`]. +//! +//! Capability-based APIs represent access to external resources as values +//! which can be passed around between different parts of a program. +//! +//! Two notable features are the [`Dir`] and [`Pool`] types: +//! - `Dir` represents an open directory in a filesystem. Instead of opening +//! files by absolute paths or paths relative to the current working +//! directory, files are opened via paths relative to a `Dir`. The concepts +//! of a process-wide "current working directory" and a single global +//! filesystem namespace are de-emphasized. +//! - `Pool` represents a set of network addresses. Instead of allowing +//! applications to request access to any address and then applying +//! process-wide filtering rules, filtering rules are built into pools which +//! may be passed through the program. +//! +//! On WASI, use of this library closely reflects the underlying system +//! API, so it avoids compatibility layers. +//! +//! [`Dir`]: fs::Dir +//! [`Pool`]: net::Pool + +#![deny(missing_docs)] +#![cfg_attr(target_os = "wasi", feature(wasi_ext))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/bytecodealliance/cap-std/main/media/cap-std.svg" +)] +#![doc( + html_favicon_url = "https://raw.githubusercontent.com/bytecodealliance/cap-std/main/media/cap-std.ico" +)] +#![cfg_attr(io_lifetimes_use_std, feature(io_safety))] + +pub mod try_into_os; + +pub mod fs; +#[cfg(feature = "fs_utf8")] +pub mod fs_utf8; +#[cfg(not(target_os = "wasi"))] // Disable `net` on WASI until it has networking support. +pub mod net; +pub mod os; +pub mod time; +#[doc(hidden)] +pub use cap_primitives::ambient_authority_known_at_compile_time; +pub use cap_primitives::{ambient_authority, AmbientAuthority}; + +// Re-export `tokio` to make it easy for users to depend on the same +// version we do, because we use its types in our public API. +pub use tokio; +// And these are also part of our public API +pub use cap_primitives::ipnet; +pub use io_lifetimes; diff --git a/cap-tokio/src/net/mod.rs b/cap-tokio/src/net/mod.rs new file mode 100644 index 00000000..3aea231c --- /dev/null +++ b/cap-tokio/src/net/mod.rs @@ -0,0 +1,27 @@ +//! A capability-based network API modeled after [`tokio::net`]. +//! +//! This corresponds to [`tokio::net`]. +//! +//! Instead of [`tokio::net`]'s constructor methods which take an address +//! to connect to, this crates has methods on [`Pool`] which operate on +//! addresses which must be present in the pool. +//! +//! [`Pool`]: struct.Pool.html + +mod pool; +mod tcp_listener; +mod tcp_stream; +mod udp_socket; + +pub use pool::*; +pub use tcp_listener::*; +pub use tcp_stream::*; +pub use udp_socket::*; + +// Re-export things from `std::net` that we can use as-is. +pub use std::net::{ + AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, SocketAddrV4, SocketAddrV6, + ToSocketAddrs, +}; + +// TODO: re-export experimental Ipv6MulticastScope? diff --git a/cap-tokio/src/net/pool.rs b/cap-tokio/src/net/pool.rs new file mode 100644 index 00000000..8f672207 --- /dev/null +++ b/cap-tokio/src/net/pool.rs @@ -0,0 +1,230 @@ +use crate::net::{TcpListener, TcpStream, ToSocketAddrs, UdpSocket}; +use cap_primitives::net::no_socket_addrs; +use cap_primitives::{ipnet, AmbientAuthority}; +use std::io; +use tokio::net; + +/// A pool of network addresses. +/// +/// This does not directly correspond to anything in `tokio`, however its +/// methods correspond to the several functions in [`tokio::net`]. +/// +/// `Pool` implements `Clone`, which creates new independent entities that +/// carry the full authority of the originals. This means that in a borrow +/// of a `Pool`, the scope of the authority is not necessarily limited to +/// the scope of the borrow. +/// +/// Similarly, the [`cap_net_ext::PoolExt`] class allows creating "binder" +/// and "connecter" objects which represent capabilities to bind and +/// connect to addresses. +/// +/// [`cap_net_ext::PoolExt`]: https://docs.rs/cap-net-ext/latest/cap_net_ext/trait.PoolExt.html +#[derive(Clone, Default)] +pub struct Pool { + cap: cap_primitives::net::Pool, +} + +impl Pool { + /// Construct a new empty pool. + pub fn new() -> Self { + Self { + cap: cap_primitives::net::Pool::new(), + } + } + + /// Add addresses to the pool. + /// + /// # Ambient Authority + /// + /// This function allows ambient access to any IP address. + pub fn insert( + &mut self, + addrs: A, + ambient_authority: AmbientAuthority, + ) -> io::Result<()> { + for addr in addrs.to_socket_addrs()? { + self.cap.insert(addr, ambient_authority)?; + } + Ok(()) + } + + /// Add a specific [`std::net::SocketAddr`] to the pool. + /// + /// # Ambient Authority + /// + /// This function allows ambient access to any IP address. + pub fn insert_socket_addr( + &mut self, + addr: std::net::SocketAddr, + ambient_authority: AmbientAuthority, + ) { + self.cap.insert_socket_addr(addr, ambient_authority) + } + + /// Add a range of network addresses, accepting any port, to the pool. + /// + /// Unlike `insert_ip_net`, this function grants access to any requested + /// port. + /// + /// # Ambient Authority + /// + /// This function allows ambient access to any IP address. + pub fn insert_ip_net_port_any( + &mut self, + ip_net: ipnet::IpNet, + ambient_authority: AmbientAuthority, + ) { + self.cap.insert_ip_net_port_any(ip_net, ambient_authority) + } + + /// Add a range of network addresses, accepting a range of ports, to the + /// pool. + /// + /// This grants access to the port range starting at `ports_start` and, + /// if `ports_end` is provided, ending before `ports_end`. + /// + /// # Ambient Authority + /// + /// This function allows ambient access to any IP address. + pub fn insert_ip_net_port_range( + &mut self, + ip_net: ipnet::IpNet, + ports_start: u16, + ports_end: Option, + ambient_authority: AmbientAuthority, + ) { + self.cap + .insert_ip_net_port_range(ip_net, ports_start, ports_end, ambient_authority) + } + + /// Add a range of network addresses with a specific port to the pool. + /// + /// # Ambient Authority + /// + /// This function allows ambient access to any IP address. + pub fn insert_ip_net( + &mut self, + ip_net: ipnet::IpNet, + port: u16, + ambient_authority: AmbientAuthority, + ) { + self.cap.insert_ip_net(ip_net, port, ambient_authority) + } + + /// Creates a new `TcpListener` which will be bound to the specified + /// address. + /// + /// This corresponds to [`tokio::net::TcpListener::bind`]. + #[doc(alias = "bind")] + #[inline] + pub async fn bind_tcp_listener(&self, addr: A) -> io::Result { + let addrs = addr.to_socket_addrs()?; + + let mut last_err = None; + for addr in addrs { + self.cap.check_addr(&addr)?; + // TODO: when compiling for WASI, use WASI-specific methods instead + match net::TcpListener::bind(addr).await { + Ok(tcp_listener) => return Ok(TcpListener::from_std(tcp_listener)), + Err(e) => last_err = Some(e), + } + } + match last_err { + Some(e) => Err(e), + None => Err(no_socket_addrs()), + } + } + + /// Creates a new TCP stream connected to the specified address. + /// + /// This corresponds to [`tokio::net::TcpStream::connect`]. + #[doc(alias = "connect")] + #[inline] + pub async fn connect_tcp_stream(&self, addr: A) -> io::Result { + let addrs = addr.to_socket_addrs()?; + + let mut last_err = None; + for addr in addrs { + self.cap.check_addr(&addr)?; + // TODO: when compiling for WASI, use WASI-specific methods instead + match net::TcpStream::connect(addr).await { + Ok(tcp_stream) => return Ok(TcpStream::from_std(tcp_stream)), + Err(e) => last_err = Some(e), + } + } + match last_err { + Some(e) => Err(e), + None => Err(no_socket_addrs()), + } + } + + /// Creates a UDP socket from the given address. + /// + /// This corresponds to [`tokio::net::UdpSocket::bind`]. + #[doc(alias = "bind")] + #[inline] + pub async fn bind_udp_socket(&self, addr: A) -> io::Result { + let addrs = addr.to_socket_addrs()?; + + let mut last_err = None; + for addr in addrs { + self.cap.check_addr(&addr)?; + match net::UdpSocket::bind(addr).await { + Ok(udp_socket) => return Ok(UdpSocket::from_std(udp_socket)), + Err(e) => last_err = Some(e), + } + } + match last_err { + Some(e) => Err(e), + None => Err(no_socket_addrs()), + } + } + + /// Sends data on the socket to the given address. + /// + /// This corresponds to [`tokio::net::UdpSocket::send_to`]. + #[doc(alias = "send_to")] + #[inline] + pub async fn send_to_udp_socket_addr( + &self, + udp_socket: &UdpSocket, + buf: &[u8], + addr: A, + ) -> io::Result { + let mut addrs = addr.to_socket_addrs()?; + + // `UdpSocket::send_to` only sends to the first address. + let addr = match addrs.next() { + None => return Err(no_socket_addrs()), + Some(addr) => addr, + }; + self.cap.check_addr(&addr)?; + udp_socket.std.send_to(buf, addr).await + } + + /// Connects the UDP socket to a remote address. + /// + /// This corresponds to [`tokio::net::UdpSocket::connect`]. + #[doc(alias = "connect")] + #[inline] + pub async fn connect_udp_socket( + &self, + udp_socket: &UdpSocket, + addr: A, + ) -> io::Result<()> { + let addrs = addr.to_socket_addrs()?; + + let mut last_err = None; + for addr in addrs { + self.cap.check_addr(&addr)?; + match udp_socket.std.connect(addr).await { + Ok(()) => return Ok(()), + Err(e) => last_err = Some(e), + } + } + match last_err { + Some(e) => Err(e), + None => Err(no_socket_addrs()), + } + } +} diff --git a/cap-tokio/src/net/tcp_listener.rs b/cap-tokio/src/net/tcp_listener.rs new file mode 100644 index 00000000..87583217 --- /dev/null +++ b/cap-tokio/src/net/tcp_listener.rs @@ -0,0 +1,221 @@ +use crate::net::{SocketAddr, TcpStream}; +use crate::try_into_os::*; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsSocket, BorrowedSocket, OwnedSocket}; +use std::fmt; +use std::io; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use tokio::net; +#[cfg(windows)] +use { + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawSocket, IntoRawSocket, RawSocket}, +}; + +/// A TCP socket server, listening for connections. +/// +/// This corresponds to [`tokio::net::TcpListener`]. +/// +/// This `TcpListener` has no `bind` method. To bind it to a socket address, +/// first obtain a [`Pool`] permitting the address, and then call +/// [`Pool::bind_tcp_listener`]. +/// +/// [`Pool`]: struct.Pool.html +/// [`Pool::bind_tcp_listener`]: struct.Pool.html#method.bind_tcp_listener +pub struct TcpListener { + std: net::TcpListener, +} + +impl TcpListener { + /// Constructs a new instance of `Self` from the given + /// `tokio::net::TcpListener`. + /// + /// This grants access the resources the `tokio::net::TcpListener` + /// instance already has access to. + #[inline] + pub fn from_std(std: net::TcpListener) -> Self { + Self { std } + } + + /// Returns the local socket address of this listener. + /// + /// This corresponds to [`tokio::net::TcpListener::local_addr`]. + #[inline] + pub fn local_addr(&self) -> io::Result { + self.std.local_addr() + } + + /// Accept a new incoming connection from this listener. + /// + /// This corresponds to [`tokio::net::TcpListener::accept`]. + #[inline] + pub async fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> { + self.std + .accept() + .await + .map(|(tcp_stream, addr)| (TcpStream::from_std(tcp_stream), addr)) + } +} + +#[cfg(not(windows))] +impl TryFrom for TcpListener { + type Error = io::Error; + #[inline] + fn try_from(fd: RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + let std_listener = unsafe { std::net::TcpListener::from_raw_fd(fd) }; + std_listener.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpListener::from_std(std_listener)?)) + } +} + +#[cfg(not(windows))] +impl TryFrom for TcpListener { + type Error = io::Error; + #[inline] + fn try_from(fd: OwnedFd) -> io::Result { + let std_listener = std::net::TcpListener::from(fd); + std_listener.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpListener::from_std(std_listener)?)) + } +} + +#[cfg(windows)] +impl TryFrom for TcpListener { + type Error = io::Error; + #[inline] + fn try_from(socket: RawSocket) -> io::Result { + use std::os::windows::io::FromRawSocket; + let std_listener = unsafe { std::net::TcpListener::from_raw_socket(socket) }; + std_listener.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpListener::from_std(std_listener)?)) + } +} + +#[cfg(windows)] +impl TryFrom for TcpListener { + type Error = io::Error; + #[inline] + fn try_from(socket: OwnedSocket) -> io::Result { + let std_listener = std::net::TcpListener::from(socket); + std_listener.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpListener::from_std(std_listener)?)) + } +} + +#[cfg(not(windows))] +impl AsRawFd for TcpListener { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for TcpListener { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +#[cfg(windows)] +impl AsRawSocket for TcpListener { + #[inline] + fn as_raw_socket(&self) -> RawSocket { + self.std.as_raw_socket() + } +} + +#[cfg(windows)] +impl AsSocket for TcpListener { + #[inline] + fn as_socket(&self) -> BorrowedSocket<'_> { + self.std.as_socket() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for TcpListener { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.std.as_raw_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for TcpListener { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.std.as_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for TcpListener { + fn try_into_raw_fd(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_fd()) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(val: TcpListener) -> io::Result { + use std::os::unix::io::FromRawFd; + let raw = TryIntoRawFd::try_into_raw_fd(val)?; + Ok(unsafe { OwnedFd::from_raw_fd(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawSocket for TcpListener { + fn try_into_raw_socket(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_socket()) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedSocket { + type Error = io::Error; + #[inline] + fn try_from(val: TcpListener) -> io::Result { + use std::os::windows::io::{FromRawSocket, IntoRawSocket}; + let raw = TryIntoRawSocket::try_into_raw_socket(val)?; + Ok(unsafe { OwnedSocket::from_raw_socket(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for TcpListener { + fn try_into_raw_handle_or_socket( + self, + ) -> std::io::Result { + use TryIntoRawSocket; + let raw = self.try_into_raw_socket()?; + Ok(io_extras::os::windows::RawHandleOrSocket::from_raw_socket( + raw, + )) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + fn try_from(val: TcpListener) -> io::Result { + let socket: OwnedSocket = val.try_into()?; + Ok(socket.into()) + } +} + +impl fmt::Debug for TcpListener { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.std.fmt(f) + } +} diff --git a/cap-tokio/src/net/tcp_stream.rs b/cap-tokio/src/net/tcp_stream.rs new file mode 100644 index 00000000..228d00d4 --- /dev/null +++ b/cap-tokio/src/net/tcp_stream.rs @@ -0,0 +1,308 @@ +use crate::net::SocketAddr; +use crate::try_into_os::*; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsSocket, BorrowedSocket, OwnedSocket}; +use std::fmt; +use std::io; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net; +#[cfg(windows)] +use { + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawSocket, IntoRawSocket, RawSocket}, +}; + +/// A TCP stream between a local and a remote socket. +/// +/// This corresponds to [`tokio::net::TcpStream`]. +/// +/// This `TcpStream` has no `connect` method. To create a `TcpStream`, first +/// obtain a [`Pool`] permitting the address, and then call +/// [`Pool::connect_tcp_stream`]. +/// +/// [`Pool`]: struct.Pool.html +/// [`Pool::connect_tcp_stream`]: struct.Pool.html#method.connect_tcp_stream +pub struct TcpStream { + std: net::TcpStream, +} + +impl TcpStream { + /// Constructs a new instance of `Self` from the given + /// `tokio::net::TcpStream`. + /// + /// This grants access the resources the `tokio::net::TcpStream` + /// instance already has access to. + #[inline] + pub fn from_std(std: net::TcpStream) -> Self { + Self { std } + } + + /// Returns the remote address that this stream is connected to. + /// + /// This corresponds to [`tokio::net::TcpStream::peer_addr`]. + #[inline] + pub fn peer_addr(&self) -> io::Result { + self.std.peer_addr() + } + + /// Returns the local socket address of this listener. + /// + /// This corresponds to [`tokio::net::TcpStream::local_addr`]. + #[inline] + pub fn local_addr(&self) -> io::Result { + self.std.local_addr() + } + + /// Receives data on the socket from the remote address to which it is + /// connected, without removing that data from the queue. + /// + /// This corresponds to [`tokio::net::TcpStream::peek`]. + #[inline] + pub async fn peek(&self, buf: &mut [u8]) -> io::Result { + self.std.peek(buf).await + } + + /// Sets the value of the `TCP_NODELAY` option on this socket. + /// + /// This corresponds to [`tokio::net::TcpStream::set_nodelay`]. + #[inline] + pub fn set_nodelay(&self, nodelay: bool) -> io::Result<()> { + self.std.set_nodelay(nodelay) + } + + /// Gets the value of the `TCP_NODELAY` option on this socket. + /// + /// This corresponds to [`tokio::net::TcpStream::nodelay`]. + #[inline] + pub fn nodelay(&self) -> io::Result { + self.std.nodelay() + } + + /// Sets the value for the `IP_TTL` option on this socket. + /// + /// This corresponds to [`tokio::net::TcpStream::set_ttl`]. + #[inline] + pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { + self.std.set_ttl(ttl) + } + + /// Gets the value of the `IP_TTL` option for this socket. + /// + /// This corresponds to [`tokio::net::TcpStream::ttl`]. + #[inline] + pub fn ttl(&self) -> io::Result { + self.std.ttl() + } +} + +#[cfg(not(windows))] +impl TryFrom for TcpStream { + type Error = io::Error; + #[inline] + fn try_from(fd: RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + let std_stream = unsafe { std::net::TcpStream::from_raw_fd(fd) }; + std_stream.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpStream::from_std(std_stream)?)) + } +} + +#[cfg(not(windows))] +impl TryFrom for TcpStream { + type Error = io::Error; + #[inline] + fn try_from(fd: OwnedFd) -> io::Result { + let std_stream = std::net::TcpStream::from(fd); + std_stream.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpStream::from_std(std_stream)?)) + } +} + +#[cfg(windows)] +impl TryFrom for TcpStream { + type Error = io::Error; + #[inline] + fn try_from(socket: RawSocket) -> io::Result { + use std::os::windows::io::FromRawSocket; + let std_stream = unsafe { std::net::TcpStream::from_raw_socket(socket) }; + std_stream.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpStream::from_std(std_stream)?)) + } +} + +#[cfg(windows)] +impl TryFrom for TcpStream { + type Error = io::Error; + #[inline] + fn try_from(socket: OwnedSocket) -> io::Result { + let std_stream = std::net::TcpStream::from(socket); + std_stream.set_nonblocking(true)?; + Ok(Self::from_std(net::TcpStream::from_std(std_stream)?)) + } +} + +#[cfg(not(windows))] +impl AsRawFd for TcpStream { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for TcpStream { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +#[cfg(windows)] +impl AsRawSocket for TcpStream { + #[inline] + fn as_raw_socket(&self) -> RawSocket { + self.std.as_raw_socket() + } +} + +#[cfg(windows)] +impl AsSocket for TcpStream { + #[inline] + fn as_socket(&self) -> BorrowedSocket<'_> { + self.std.as_socket() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for TcpStream { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.std.as_raw_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for TcpStream { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.std.as_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for TcpStream { + fn try_into_raw_fd(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_fd()) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(val: TcpStream) -> io::Result { + use std::os::unix::io::FromRawFd; + let raw = TryIntoRawFd::try_into_raw_fd(val)?; + Ok(unsafe { OwnedFd::from_raw_fd(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawSocket for TcpStream { + fn try_into_raw_socket(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_socket()) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedSocket { + type Error = io::Error; + #[inline] + fn try_from(val: TcpStream) -> io::Result { + use std::os::windows::io::{FromRawSocket, IntoRawSocket}; + let raw = TryIntoRawSocket::try_into_raw_socket(val)?; + Ok(unsafe { OwnedSocket::from_raw_socket(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for TcpStream { + fn try_into_raw_handle_or_socket( + self, + ) -> std::io::Result { + use TryIntoRawSocket; + let raw = self.try_into_raw_socket()?; + Ok(io_extras::os::windows::RawHandleOrSocket::from_raw_socket( + raw, + )) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + fn try_from(val: TcpStream) -> io::Result { + let socket: OwnedSocket = val.try_into()?; + Ok(socket.into()) + } +} + +impl AsyncRead for TcpStream { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.std), cx, buf) + } +} + +impl AsyncWrite for TcpStream { + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.std), cx, buf) + } + + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.std), cx) + } + + #[inline] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.std), cx) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + AsyncWrite::poll_write_vectored(Pin::new(&mut self.std), cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.std.is_write_vectored() + } +} + +impl fmt::Debug for TcpStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.std.fmt(f) + } +} diff --git a/cap-tokio/src/net/udp_socket.rs b/cap-tokio/src/net/udp_socket.rs new file mode 100644 index 00000000..76d76b20 --- /dev/null +++ b/cap-tokio/src/net/udp_socket.rs @@ -0,0 +1,376 @@ +use crate::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use crate::try_into_os::*; +#[cfg(not(windows))] +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(windows)] +use io_lifetimes::{AsSocket, BorrowedSocket, OwnedSocket}; +use std::fmt; +use std::io; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use tokio::net; +#[cfg(windows)] +use { + io_extras::os::windows::{ + AsHandleOrSocket, AsRawHandleOrSocket, BorrowedHandleOrSocket, RawHandleOrSocket, + }, + std::os::windows::io::{AsRawSocket, IntoRawSocket, RawSocket}, +}; + +/// A UDP socket. +/// +/// This corresponds to [`tokio::net::UdpSocket`]. +/// +/// This `UdpSocket` has no `bind`, `connect`, or `send_to` methods. To create +/// a `UdpSocket` bound to an address or to send a message to an address, first +/// obtain a [`Pool`] permitting the address, and then call +/// [`Pool::bind_udp_socket`], or [`Pool::connect_udp_socket`], or +/// [`Pool::send_to_udp_socket_addr`]. +/// +/// [`Pool`]: struct.Pool.html +/// [`Pool::bind_udp_socket`]: struct.Pool.html#method.bind_udp_socket +/// [`Pool::connect_udp_socket`]: struct.Pool.html#method.connect_udp_socket +/// [`Pool::send_to_udp_socket_addr`]: struct.Pool.html#method.send_to_udp_socket_addr +pub struct UdpSocket { + pub(crate) std: net::UdpSocket, +} + +impl UdpSocket { + /// Constructs a new instance of `Self` from the given + /// `tokio::net::UdpSocket`. + /// + /// This grants access the resources the `tokio::net::UdpSocket` + /// instance already has access to. + #[inline] + pub fn from_std(std: net::UdpSocket) -> Self { + Self { std } + } + + /// Receives a single datagram message on the socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::recv_from`]. + #[inline] + pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { + self.std.recv_from(buf).await + } + + /// Returns the socket address of the remote peer this socket was connected + /// to. + /// + /// This corresponds to [`tokio::net::UdpSocket::peer_addr`]. + #[inline] + pub fn peer_addr(&self) -> io::Result { + self.std.peer_addr() + } + + /// Returns the socket address that this socket was created from. + /// + /// This corresponds to [`tokio::net::UdpSocket::local_addr`]. + #[inline] + pub fn local_addr(&self) -> io::Result { + self.std.local_addr() + } + + /// Sets the value of the `SO_BROADCAST` option for this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::set_broadcast`]. + #[inline] + pub fn set_broadcast(&self, broadcast: bool) -> io::Result<()> { + self.std.set_broadcast(broadcast) + } + + /// Gets the value of the `SO_BROADCAST` option for this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::broadcast`]. + #[inline] + pub fn broadcast(&self) -> io::Result { + self.std.broadcast() + } + + /// Sets the value of the `IP_MULTICAST_LOOP` option for this socket. + /// + /// This corresponds to + /// [`tokio::net::UdpSocket::set_multicast_loop_v4`]. + #[inline] + pub fn set_multicast_loop_v4(&self, multicast_loop_v4: bool) -> io::Result<()> { + // tokio doesn't expose set_multicast_loop_v4 directly; + // use the underlying socket2 or std approach via raw fd + let socket = socket2::SockRef::from(&self.std); + socket.set_multicast_loop_v4(multicast_loop_v4) + } + + /// Gets the value of the `IP_MULTICAST_LOOP` option for this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::multicast_loop_v4`]. + #[inline] + pub fn multicast_loop_v4(&self) -> io::Result { + let socket = socket2::SockRef::from(&self.std); + socket.multicast_loop_v4() + } + + /// Sets the value of the `IP_MULTICAST_TTL` option for this socket. + /// + /// This corresponds to + /// [`tokio::net::UdpSocket::set_multicast_ttl_v4`]. + #[inline] + pub fn set_multicast_ttl_v4(&self, multicast_ttl_v4: u32) -> io::Result<()> { + let socket = socket2::SockRef::from(&self.std); + socket.set_multicast_ttl_v4(multicast_ttl_v4) + } + + /// Gets the value of the `IP_MULTICAST_TTL` option for this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::multicast_ttl_v4`]. + #[inline] + pub fn multicast_ttl_v4(&self) -> io::Result { + let socket = socket2::SockRef::from(&self.std); + socket.multicast_ttl_v4() + } + + /// Sets the value of the `IPV6_MULTICAST_LOOP` option for this socket. + /// + /// This corresponds to + /// [`tokio::net::UdpSocket::set_multicast_loop_v6`]. + #[inline] + pub fn set_multicast_loop_v6(&self, multicast_loop_v6: bool) -> io::Result<()> { + let socket = socket2::SockRef::from(&self.std); + socket.set_multicast_loop_v6(multicast_loop_v6) + } + + /// Gets the value of the `IPV6_MULTICAST_LOOP` option for this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::multicast_loop_v6`]. + #[inline] + pub fn multicast_loop_v6(&self) -> io::Result { + let socket = socket2::SockRef::from(&self.std); + socket.multicast_loop_v6() + } + + /// Sets the value for the `IP_TTL` option on this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::set_ttl`]. + #[inline] + pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { + self.std.set_ttl(ttl) + } + + /// Gets the value of the `IP_TTL` option for this socket. + /// + /// This corresponds to [`tokio::net::UdpSocket::ttl`]. + #[inline] + pub fn ttl(&self) -> io::Result { + self.std.ttl() + } + + /// Executes an operation of the `IP_ADD_MEMBERSHIP` type. + /// + /// This corresponds to [`tokio::net::UdpSocket::join_multicast_v4`]. + #[allow(clippy::trivially_copy_pass_by_ref)] + #[inline] + pub fn join_multicast_v4(&self, multiaddr: Ipv4Addr, interface: Ipv4Addr) -> io::Result<()> { + self.std.join_multicast_v4(multiaddr, interface) + } + + /// Executes an operation of the `IPV6_ADD_MEMBERSHIP` type. + /// + /// This corresponds to [`tokio::net::UdpSocket::join_multicast_v6`]. + #[allow(clippy::trivially_copy_pass_by_ref)] + #[inline] + pub fn join_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32) -> io::Result<()> { + self.std.join_multicast_v6(multiaddr, interface) + } + + /// Executes an operation of the `IP_DROP_MEMBERSHIP` type. + /// + /// This corresponds to [`tokio::net::UdpSocket::leave_multicast_v4`]. + #[allow(clippy::trivially_copy_pass_by_ref)] + #[inline] + pub fn leave_multicast_v4(&self, multiaddr: Ipv4Addr, interface: Ipv4Addr) -> io::Result<()> { + self.std.leave_multicast_v4(multiaddr, interface) + } + + /// Executes an operation of the `IPV6_DROP_MEMBERSHIP` type. + /// + /// This corresponds to [`tokio::net::UdpSocket::leave_multicast_v6`]. + #[allow(clippy::trivially_copy_pass_by_ref)] + #[inline] + pub fn leave_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32) -> io::Result<()> { + self.std.leave_multicast_v6(multiaddr, interface) + } + + /// Sends data on the socket to the remote address to which it is + /// connected. + /// + /// This corresponds to [`tokio::net::UdpSocket::send`]. + #[inline] + pub async fn send(&self, buf: &[u8]) -> io::Result { + self.std.send(buf).await + } + + /// Receives a single datagram message on the socket from the remote + /// address to which it is connected. + /// + /// This corresponds to [`tokio::net::UdpSocket::recv`]. + #[inline] + pub async fn recv(&self, buf: &mut [u8]) -> io::Result { + self.std.recv(buf).await + } +} + +#[cfg(not(windows))] +impl TryFrom for UdpSocket { + type Error = io::Error; + #[inline] + fn try_from(fd: RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + let std_socket = unsafe { std::net::UdpSocket::from_raw_fd(fd) }; + std_socket.set_nonblocking(true)?; + Ok(Self::from_std(net::UdpSocket::from_std(std_socket)?)) + } +} + +#[cfg(not(windows))] +impl TryFrom for UdpSocket { + type Error = io::Error; + #[inline] + fn try_from(fd: OwnedFd) -> io::Result { + let std_socket = std::net::UdpSocket::from(fd); + std_socket.set_nonblocking(true)?; + Ok(Self::from_std(net::UdpSocket::from_std(std_socket)?)) + } +} + +#[cfg(windows)] +impl TryFrom for UdpSocket { + type Error = io::Error; + #[inline] + fn try_from(socket: RawSocket) -> io::Result { + use std::os::windows::io::FromRawSocket; + let std_socket = unsafe { std::net::UdpSocket::from_raw_socket(socket) }; + std_socket.set_nonblocking(true)?; + Ok(Self::from_std(net::UdpSocket::from_std(std_socket)?)) + } +} + +#[cfg(windows)] +impl TryFrom for UdpSocket { + type Error = io::Error; + #[inline] + fn try_from(socket: OwnedSocket) -> io::Result { + let std_socket = std::net::UdpSocket::from(socket); + std_socket.set_nonblocking(true)?; + Ok(Self::from_std(net::UdpSocket::from_std(std_socket)?)) + } +} + +#[cfg(not(windows))] +impl AsRawFd for UdpSocket { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +#[cfg(not(windows))] +impl AsFd for UdpSocket { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +#[cfg(windows)] +impl AsRawSocket for UdpSocket { + #[inline] + fn as_raw_socket(&self) -> RawSocket { + self.std.as_raw_socket() + } +} + +#[cfg(windows)] +impl AsSocket for UdpSocket { + #[inline] + fn as_socket(&self) -> BorrowedSocket<'_> { + self.std.as_socket() + } +} + +#[cfg(windows)] +impl AsRawHandleOrSocket for UdpSocket { + #[inline] + fn as_raw_handle_or_socket(&self) -> RawHandleOrSocket { + self.std.as_raw_handle_or_socket() + } +} + +#[cfg(windows)] +impl AsHandleOrSocket for UdpSocket { + #[inline] + fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket<'_> { + self.std.as_handle_or_socket() + } +} + +#[cfg(not(windows))] +impl TryIntoRawFd for UdpSocket { + fn try_into_raw_fd(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_fd()) + } +} + +#[cfg(not(windows))] +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(val: UdpSocket) -> io::Result { + use std::os::unix::io::FromRawFd; + let raw = TryIntoRawFd::try_into_raw_fd(val)?; + Ok(unsafe { OwnedFd::from_raw_fd(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawSocket for UdpSocket { + fn try_into_raw_socket(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_socket()) + } +} + +#[cfg(windows)] +impl TryFrom for OwnedSocket { + type Error = io::Error; + #[inline] + fn try_from(val: UdpSocket) -> io::Result { + use std::os::windows::io::{FromRawSocket, IntoRawSocket}; + let raw = TryIntoRawSocket::try_into_raw_socket(val)?; + Ok(unsafe { OwnedSocket::from_raw_socket(raw) }) + } +} + +#[cfg(windows)] +impl TryIntoRawHandleOrSocket for UdpSocket { + fn try_into_raw_handle_or_socket( + self, + ) -> std::io::Result { + use TryIntoRawSocket; + let raw = self.try_into_raw_socket()?; + Ok(io_extras::os::windows::RawHandleOrSocket::from_raw_socket( + raw, + )) + } +} + +#[cfg(windows)] +impl TryFrom for io_extras::os::windows::OwnedHandleOrSocket { + type Error = io::Error; + fn try_from(val: UdpSocket) -> io::Result { + let socket: OwnedSocket = val.try_into()?; + Ok(socket.into()) + } +} + +impl fmt::Debug for UdpSocket { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.std.fmt(f) + } +} diff --git a/cap-tokio/src/os/mod.rs b/cap-tokio/src/os/mod.rs new file mode 100644 index 00000000..325de263 --- /dev/null +++ b/cap-tokio/src/os/mod.rs @@ -0,0 +1,6 @@ +//! OS-specific extensions. +//! +//! This corresponds to [`std::os`]. + +#[cfg(unix)] +pub mod unix; diff --git a/cap-tokio/src/os/unix/mod.rs b/cap-tokio/src/os/unix/mod.rs new file mode 100644 index 00000000..b9ca1fd0 --- /dev/null +++ b/cap-tokio/src/os/unix/mod.rs @@ -0,0 +1,5 @@ +//! Platform-specific extensions for Unix platforms. +//! +//! This corresponds to [`std::os::unix`]. + +pub mod net; diff --git a/cap-tokio/src/os/unix/net/mod.rs b/cap-tokio/src/os/unix/net/mod.rs new file mode 100644 index 00000000..0a7a1333 --- /dev/null +++ b/cap-tokio/src/os/unix/net/mod.rs @@ -0,0 +1,13 @@ +//! Unix-specific networking functionality +//! +//! This corresponds to [`tokio::net`] Unix socket types. + +mod unix_datagram; +mod unix_listener; +mod unix_stream; + +pub use unix_datagram::*; +pub use unix_listener::*; +pub use unix_stream::*; + +pub use tokio::net::unix::SocketAddr; diff --git a/cap-tokio/src/os/unix/net/unix_datagram.rs b/cap-tokio/src/os/unix/net/unix_datagram.rs new file mode 100644 index 00000000..4af51911 --- /dev/null +++ b/cap-tokio/src/os/unix/net/unix_datagram.rs @@ -0,0 +1,168 @@ +use crate::os::unix::net::SocketAddr; +use crate::try_into_os::*; +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +use std::fmt; +use std::io; +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use tokio::net; + +/// A Unix datagram socket. +/// +/// This corresponds to [`tokio::net::UnixDatagram`]. +/// +/// This `UnixDatagram` has no `bind`, `connect`, or `send_to` methods. To +/// create a `UnixDatagram`, first obtain a [`Dir`] containing the path, and +/// then call [`Dir::bind_unix_datagram`], [`Dir::connect_unix_datagram`], or +/// [`Dir::send_to_unix_datagram_addr`]. +/// +/// [`Dir`]: struct.Dir.html +/// [`Dir::connect_unix_datagram`]: struct.Dir.html#method.connect_unix_datagram +/// [`Dir::bind_unix_datagram`]: struct.Dir.html#method.bind_unix_datagram +/// [`Dir::send_to_unix_datagram_addr`]: struct.Dir.html#method.send_to_unix_datagram_addr +pub struct UnixDatagram { + std: net::UnixDatagram, +} + +impl UnixDatagram { + /// Constructs a new instance of `Self` from the given + /// `tokio::net::UnixDatagram`. + /// + /// This grants access the resources the + /// `tokio::net::UnixDatagram` instance already has access + /// to. + #[inline] + pub fn from_std(std: net::UnixDatagram) -> Self { + Self { std } + } + + /// Creates a Unix Datagram socket which is not bound to any address. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::unbound`]. + /// + /// TODO: should this require a capability? + #[inline] + pub fn unbound() -> io::Result { + let unix_datagram = net::UnixDatagram::unbound()?; + Ok(Self::from_std(unix_datagram)) + } + + /// Creates an unnamed pair of connected sockets. + /// + /// This corresponds to [`tokio::net::UnixDatagram::pair`]. + /// + /// TODO: should this require a capability? + #[inline] + pub fn pair() -> io::Result<(Self, Self)> { + net::UnixDatagram::pair().map(|(a, b)| (Self::from_std(a), Self::from_std(b))) + } + + /// Returns the address of this socket. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::local_addr`]. + #[inline] + pub fn local_addr(&self) -> io::Result { + self.std.local_addr() + } + + /// Returns the address of this socket's peer. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::peer_addr`]. + #[inline] + pub fn peer_addr(&self) -> io::Result { + self.std.peer_addr() + } + + /// Receives data from the socket. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::recv_from`]. + #[inline] + pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { + self.std.recv_from(buf).await + } + + /// Receives data from the socket. + /// + /// This corresponds to [`tokio::net::UnixDatagram::recv`]. + #[inline] + pub async fn recv(&self, buf: &mut [u8]) -> io::Result { + self.std.recv(buf).await + } + + /// Sends data on the socket to the socket's peer. + /// + /// This corresponds to [`tokio::net::UnixDatagram::send`]. + #[inline] + pub async fn send(&self, buf: &[u8]) -> io::Result { + self.std.send(buf).await + } + + /// Shut down the read, write, or both halves of this connection. + /// + /// This corresponds to + /// [`tokio::net::UnixDatagram::shutdown`]. + #[inline] + pub fn shutdown(&self, how: std::net::Shutdown) -> io::Result<()> { + self.std.shutdown(how) + } +} + +impl TryFrom for UnixDatagram { + type Error = io::Error; + #[inline] + fn try_from(fd: RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + let std_datagram = unsafe { std::os::unix::net::UnixDatagram::from_raw_fd(fd) }; + std_datagram.set_nonblocking(true)?; + Ok(Self::from_std(net::UnixDatagram::from_std(std_datagram)?)) + } +} + +impl TryFrom for UnixDatagram { + type Error = io::Error; + #[inline] + fn try_from(fd: OwnedFd) -> io::Result { + let std_datagram = std::os::unix::net::UnixDatagram::from(fd); + std_datagram.set_nonblocking(true)?; + Ok(Self::from_std(net::UnixDatagram::from_std(std_datagram)?)) + } +} + +impl AsRawFd for UnixDatagram { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +impl AsFd for UnixDatagram { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +impl TryIntoRawFd for UnixDatagram { + fn try_into_raw_fd(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_fd()) + } +} + +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(val: UnixDatagram) -> io::Result { + use std::os::unix::io::FromRawFd; + let raw = TryIntoRawFd::try_into_raw_fd(val)?; + Ok(unsafe { OwnedFd::from_raw_fd(raw) }) + } +} + +impl fmt::Debug for UnixDatagram { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.std.fmt(f) + } +} diff --git a/cap-tokio/src/os/unix/net/unix_listener.rs b/cap-tokio/src/os/unix/net/unix_listener.rs new file mode 100644 index 00000000..a9751980 --- /dev/null +++ b/cap-tokio/src/os/unix/net/unix_listener.rs @@ -0,0 +1,111 @@ +use crate::os::unix::net::{SocketAddr, UnixStream}; +use crate::try_into_os::*; +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +use std::fmt; +use std::io; +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use tokio::net; + +/// A structure representing a Unix domain socket server. +/// +/// This corresponds to [`tokio::net::UnixListener`]. +/// +/// This `UnixListener` has no `bind` method. To bind it to a socket address, +/// first obtain a [`Dir`] containing the path, and then call +/// [`Dir::bind_unix_listener`]. +/// +/// [`Dir`]: struct.Dir.html +/// [`Dir::bind_unix_listener`]: struct.Dir.html#method.bind_unix_listener +pub struct UnixListener { + std: net::UnixListener, +} + +impl UnixListener { + /// Constructs a new instance of `Self` from the given + /// `tokio::net::UnixListener`. + /// + /// This grants access the resources the + /// `tokio::net::UnixListener` instance already has access + /// to. + #[inline] + pub fn from_std(std: net::UnixListener) -> Self { + Self { std } + } + + /// Accepts a new incoming connection to this listener. + /// + /// This corresponds to [`tokio::net::UnixListener::accept`]. + #[inline] + pub async fn accept(&self) -> io::Result<(UnixStream, SocketAddr)> { + self.std + .accept() + .await + .map(|(unix_stream, addr)| (UnixStream::from_std(unix_stream), addr)) + } + + /// Returns the local socket address of this listener. + /// + /// This corresponds to + /// [`tokio::net::UnixListener::local_addr`]. + #[inline] + pub fn local_addr(&self) -> io::Result { + self.std.local_addr() + } +} + +impl TryFrom for UnixListener { + type Error = io::Error; + #[inline] + fn try_from(fd: RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + let std_listener = unsafe { std::os::unix::net::UnixListener::from_raw_fd(fd) }; + std_listener.set_nonblocking(true)?; + Ok(Self::from_std(net::UnixListener::from_std(std_listener)?)) + } +} + +impl TryFrom for UnixListener { + type Error = io::Error; + #[inline] + fn try_from(fd: OwnedFd) -> io::Result { + let std_listener = std::os::unix::net::UnixListener::from(fd); + std_listener.set_nonblocking(true)?; + Ok(Self::from_std(net::UnixListener::from_std(std_listener)?)) + } +} + +impl AsRawFd for UnixListener { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +impl AsFd for UnixListener { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +impl TryIntoRawFd for UnixListener { + fn try_into_raw_fd(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_fd()) + } +} + +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(val: UnixListener) -> io::Result { + use std::os::unix::io::FromRawFd; + let raw = TryIntoRawFd::try_into_raw_fd(val)?; + Ok(unsafe { OwnedFd::from_raw_fd(raw) }) + } +} + +impl fmt::Debug for UnixListener { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.std.fmt(f) + } +} diff --git a/cap-tokio/src/os/unix/net/unix_stream.rs b/cap-tokio/src/os/unix/net/unix_stream.rs new file mode 100644 index 00000000..9eb22377 --- /dev/null +++ b/cap-tokio/src/os/unix/net/unix_stream.rs @@ -0,0 +1,168 @@ +use crate::os::unix::net::SocketAddr; +use crate::try_into_os::*; +use io_lifetimes::{AsFd, BorrowedFd, OwnedFd}; +use std::fmt; +use std::io; +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net; + +/// A Unix stream socket. +/// +/// This corresponds to [`tokio::net::UnixStream`]. +/// +/// This `UnixStream` has no `connect` method. To create a `UnixStream`, first +/// obtain a [`Dir`] containing the path, and then call +/// [`Dir::connect_unix_stream`]. +/// +/// [`Dir`]: struct.Dir.html +/// [`Dir::connect_unix_stream`]: struct.Dir.html#method.connect_unix_stream +pub struct UnixStream { + std: net::UnixStream, +} + +impl UnixStream { + /// Constructs a new instance of `Self` from the given + /// `tokio::net::UnixStream`. + /// + /// This grants access the resources the + /// `tokio::net::UnixStream` instance already has access + /// to. + #[inline] + pub fn from_std(std: net::UnixStream) -> Self { + Self { std } + } + + /// Creates an unnamed pair of connected sockets. + /// + /// This corresponds to [`tokio::net::UnixStream::pair`]. + /// + /// TODO: should this require a capability? + #[inline] + pub fn pair() -> io::Result<(Self, Self)> { + net::UnixStream::pair().map(|(a, b)| (Self::from_std(a), Self::from_std(b))) + } + + /// Returns the socket address of the local half of this connection. + /// + /// This corresponds to + /// [`tokio::net::UnixStream::local_addr`]. + #[inline] + pub fn local_addr(&self) -> io::Result { + self.std.local_addr() + } + + /// Returns the socket address of the remote half of this connection. + /// + /// This corresponds to + /// [`tokio::net::UnixStream::peer_addr`]. + #[inline] + pub fn peer_addr(&self) -> io::Result { + self.std.peer_addr() + } +} + +impl TryFrom for UnixStream { + type Error = io::Error; + #[inline] + fn try_from(fd: RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd) }; + std_stream.set_nonblocking(true)?; + Ok(Self::from_std(net::UnixStream::from_std(std_stream)?)) + } +} + +impl TryFrom for UnixStream { + type Error = io::Error; + #[inline] + fn try_from(fd: OwnedFd) -> io::Result { + let std_stream = std::os::unix::net::UnixStream::from(fd); + std_stream.set_nonblocking(true)?; + Ok(Self::from_std(net::UnixStream::from_std(std_stream)?)) + } +} + +impl AsRawFd for UnixStream { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.std.as_raw_fd() + } +} + +impl AsFd for UnixStream { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.std.as_fd() + } +} + +impl TryIntoRawFd for UnixStream { + fn try_into_raw_fd(self) -> std::io::Result { + Ok(self.std.into_std()?.into_raw_fd()) + } +} + +impl TryFrom for OwnedFd { + type Error = io::Error; + #[inline] + fn try_from(val: UnixStream) -> io::Result { + use std::os::unix::io::FromRawFd; + let raw = TryIntoRawFd::try_into_raw_fd(val)?; + Ok(unsafe { OwnedFd::from_raw_fd(raw) }) + } +} + +impl AsyncRead for UnixStream { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.std), cx, buf) + } +} + +impl AsyncWrite for UnixStream { + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.std), cx, buf) + } + + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.std), cx) + } + + #[inline] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.std), cx) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + AsyncWrite::poll_write_vectored(Pin::new(&mut self.std), cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.std.is_write_vectored() + } +} + +impl fmt::Debug for UnixStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.std.fmt(f) + } +} diff --git a/cap-tokio/src/time/mod.rs b/cap-tokio/src/time/mod.rs new file mode 100644 index 00000000..ff5d788d --- /dev/null +++ b/cap-tokio/src/time/mod.rs @@ -0,0 +1,10 @@ +//! A capability-based clock API modeled after [`std::time`]. +//! +//! This corresponds to [`std::time`]. +//! +//! Instead of [`std::time`]'s methods which return the current time, this +//! crate has methods on [`SystemClock`] and [`MonotonicClock`]. + +pub use cap_primitives::time::{ + Duration, Instant, MonotonicClock, SystemClock, SystemTime, SystemTimeError, +}; diff --git a/cap-tokio/src/try_into_os.rs b/cap-tokio/src/try_into_os.rs new file mode 100644 index 00000000..9275d791 --- /dev/null +++ b/cap-tokio/src/try_into_os.rs @@ -0,0 +1,39 @@ +//! Fallible ownership-transfer traits for file descriptors/handles. +//! +//! These mirror [`std::os::unix::io::IntoRawFd`] and friends, but return +//! `Result` because tokio types must deregister from the reactor before +//! yielding the raw fd, and that deregistration can fail. + +use std::io; + +/// Fallible version of [`std::os::unix::io::IntoRawFd`]. +/// +/// Tokio types register with the async reactor; converting to a raw fd +/// requires deregistration, which can fail. +#[cfg(unix)] +pub trait TryIntoRawFd: Sized { + /// Consume this object, returning the raw underlying file descriptor. + fn try_into_raw_fd(self) -> io::Result; +} + +/// Fallible version of [`std::os::windows::io::IntoRawHandle`]. +#[cfg(windows)] +pub trait TryIntoRawHandle: Sized { + /// Consume this object, returning the raw underlying handle. + fn try_into_raw_handle(self) -> io::Result; +} + +/// Fallible version of [`std::os::windows::io::IntoRawSocket`]. +#[cfg(windows)] +pub trait TryIntoRawSocket: Sized { + /// Consume this object, returning the raw underlying socket. + fn try_into_raw_socket(self) -> io::Result; +} + +/// Fallible version of [`io_extras::os::windows::IntoRawHandleOrSocket`]. +#[cfg(windows)] +pub trait TryIntoRawHandleOrSocket: Sized { + /// Consume this object, returning the raw underlying handle or socket. + fn try_into_raw_handle_or_socket(self) + -> io::Result; +} diff --git a/examples/tokio_fs_misc.rs b/examples/tokio_fs_misc.rs new file mode 100644 index 00000000..ad8a0898 --- /dev/null +++ b/examples/tokio_fs_misc.rs @@ -0,0 +1,112 @@ +// Copied from https://doc.rust-lang.org/rust-by-example/std_misc/fs.html and +// adapted to use this crate instead. + +use cap_tokio::ambient_authority; +use cap_tokio::fs::{Dir, OpenOptions}; +use std::io; +use std::path::Path; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +// A simple implementation of `% cat path` +async fn cat(dir: &mut Dir, path: &Path) -> io::Result { + let mut f = dir.open(path).await?; + let mut s = String::new(); + match f.read_to_string(&mut s).await { + Ok(_) => Ok(s), + Err(e) => Err(e), + } +} + +// A simple implementation of `% echo s > path` +async fn echo(s: &str, dir: &mut Dir, path: &Path) -> io::Result<()> { + let mut f = dir.create(path).await?; + + f.write_all(s.as_bytes()).await +} + +// A simple implementation of `% touch path` (ignores existing files) +async fn touch(dir: &mut Dir, path: &Path) -> io::Result<()> { + match dir + .open_with(path, OpenOptions::new().create(true).write(true)) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } +} + +#[tokio::main] +async fn main() { + let mut cwd = Dir::open_ambient_dir(".", ambient_authority()) + .await + .expect("!"); + + println!("`mkdir a`"); + + // Create a directory, returns `io::Result<()>` + match cwd.create_dir("a") { + Err(why) => println!("! {:?}", why.kind()), + Ok(_) => {} + } + + println!("`echo hello > a/b.txt`"); + // The previous match can be simplified using the `unwrap_or_else` method + echo("hello", &mut cwd, Path::new("a/b.txt")) + .await + .unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`mkdir -p a/c/d`"); + // Recursively create a directory, returns `io::Result<()>` + cwd.create_dir_all("a/c/d").unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`touch a/c/e.txt`"); + touch(&mut cwd, Path::new("a/c/e.txt")) + .await + .unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + #[cfg(target_family = "unix")] + { + println!("`ln -s ../b.txt a/c/b.txt`"); + // Create a symbolic link, returns `io::Result<()>` + cwd.symlink("../b.txt", "a/c/b.txt") + .await + .unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + } + + println!("`cat a/c/b.txt`"); + match cat(&mut cwd, Path::new("a/c/b.txt")).await { + Err(why) => println!("! {:?}", why.kind()), + Ok(s) => println!("> {}", s), + } + + println!("`ls a`"); + // Read the contents of a directory, returns `io::Result>` + match cwd.read_dir("a").await { + Err(why) => println!("! {:?}", why.kind()), + Ok(paths) => { + for path in paths { + println!("! {:?}", path.unwrap().file_name()); + } + } + } + + println!("`rm a/c/e.txt`"); + // Remove a file, returns `io::Result<()>` + cwd.remove_file("a/c/e.txt").await.unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); + + println!("`rmdir a/c/d`"); + // Remove an empty directory, returns `io::Result<()>` + cwd.remove_dir("a/c/d").await.unwrap_or_else(|why| { + println!("! {:?}", why.kind()); + }); +}