Skip to content

Commit

Permalink
Add set_symlink_permissions and access functions to DirExt
Browse files Browse the repository at this point in the history
`set_symlink_permissions` is to `set_permissions` as `symlink_metadata`
is to `metadata`; it doesn't follow symlinks.

`access` follows the POSIX `faccessat` behavior.
  • Loading branch information
sunfishcode committed Nov 1, 2023
1 parent 9c1e426 commit 37a3033
Show file tree
Hide file tree
Showing 17 changed files with 473 additions and 20 deletions.
142 changes: 135 additions & 7 deletions cap-fs-ext/src/dir_ext.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#[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::{open_dir_nofollow, set_times, set_times_nofollow};
#[cfg(all(windows, feature = "async_std", feature = "fs_utf8"))]
use cap_primitives::fs::{stat, FollowSymlinks};
use cap_primitives::fs::{
access, open_dir_nofollow, set_symlink_permissions, set_times, set_times_nofollow,
FollowSymlinks, Permissions,
};
#[cfg(windows)]
use cap_primitives::fs::{symlink_dir, symlink_file};
use io_lifetimes::AsFilelike;
Expand All @@ -13,7 +16,7 @@ use std::path::Path;
#[cfg(feature = "async_std")]
use {async_std::task::spawn_blocking, async_trait::async_trait};

pub use cap_primitives::fs::SystemTimeSpec;
pub use cap_primitives::fs::{AccessType, SystemTimeSpec};

/// Extension trait for `Dir`.
pub trait DirExt {
Expand Down Expand Up @@ -95,6 +98,18 @@ pub trait DirExt {
/// points to a directory, it cannot be removed with the `remove_file`
/// operation. This method will remove files and all symlinks.
fn remove_file_or_symlink<P: AsRef<Path>>(&self, path: P) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object.
fn access<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object, without
/// following symbolic links.
fn access_symlink<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;

/// Changes the permissions found on a file or a directory, without following
/// symbolic links.
fn set_symlink_permissions<P: AsRef<Path>>(&self, path: P, perm: Permissions)
-> io::Result<()>;
}

/// Extension trait for `Dir`, async.
Expand Down Expand Up @@ -216,6 +231,17 @@ pub trait AsyncDirExt {
&self,
path: P,
) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object.
async fn access<P: AsRef<Path>>(&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<P: AsRef<Path>>(
&self,
path: P,
perm: Permissions,
) -> io::Result<()>;
}

/// `fs_utf8` version of `DirExt`.
Expand Down Expand Up @@ -304,6 +330,21 @@ pub trait DirExtUtf8 {
/// on symlinks to directories on Windows, similar to how `unlink` works
/// on symlinks to directories on Posix-ish platforms.
fn remove_file_or_symlink<P: AsRef<Utf8Path>>(&self, path: P) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object.
fn access<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object, without
/// following symbolic links.
fn access_symlink<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;

/// Changes the permissions found on a file or a directory, without following
/// symbolic links.
fn set_symlink_permissions<P: AsRef<Utf8Path>>(
&self,
path: P,
perm: Permissions,
) -> io::Result<()>;
}

/// `fs_utf8` version of `DirExt`.
Expand Down Expand Up @@ -410,6 +451,25 @@ pub trait AsyncDirExtUtf8 {
/// 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<P: AsRef<Utf8Path> + Send>(&self, path: P) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object.
async fn access<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;

/// Test for accessibility or existence of a filesystem object, without
/// following symbolic links.
async fn access_symlink<P: AsRef<Utf8Path>>(
&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<P: AsRef<Utf8Path>>(
&self,
path: P,
perm: Permissions,
) -> io::Result<()>;
}

#[cfg(feature = "std")]
Expand Down Expand Up @@ -533,7 +593,7 @@ impl DirExt for cap_std::fs::Dir {
#[cfg(windows)]
#[inline]
fn remove_file_or_symlink<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
use crate::{FollowSymlinks, OpenOptionsFollowExt};
use crate::OpenOptionsFollowExt;
use cap_primitives::fs::_WindowsByHandle;
use cap_std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
Expand Down Expand Up @@ -566,6 +626,40 @@ impl DirExt for cap_std::fs::Dir {

Ok(())
}

/// Test for accessibility or existence of a filesystem object.
fn access<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
access(
&self.as_filelike_view::<std::fs::File>(),
path.as_ref(),
type_,
FollowSymlinks::Yes,
)
}

/// Test for accessibility or existence of a filesystem object.
fn access_symlink<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
access(
&self.as_filelike_view::<std::fs::File>(),
path.as_ref(),
type_,
FollowSymlinks::No,
)
}

/// Changes the permissions found on a file or a directory, without following
/// symbolic links.
fn set_symlink_permissions<P: AsRef<Path>>(
&self,
path: P,
perm: Permissions,
) -> io::Result<()> {
set_symlink_permissions(
&self.as_filelike_view::<std::fs::File>(),
path.as_ref(),
perm,
)
}
}

#[cfg(feature = "async_std")]
Expand Down Expand Up @@ -829,7 +923,7 @@ impl AsyncDirExt for cap_async_std::fs::Dir {
&self,
path: P,
) -> io::Result<()> {
use crate::{FollowSymlinks, OpenOptionsFollowExt};
use crate::OpenOptionsFollowExt;
use cap_primitives::fs::_WindowsByHandle;
use cap_std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
Expand Down Expand Up @@ -996,7 +1090,7 @@ impl DirExtUtf8 for cap_std::fs_utf8::Dir {
#[cfg(windows)]
#[inline]
fn remove_file_or_symlink<P: AsRef<Utf8Path>>(&self, path: P) -> io::Result<()> {
use crate::{FollowSymlinks, OpenOptionsFollowExt};
use crate::OpenOptionsFollowExt;
use cap_primitives::fs::_WindowsByHandle;
use cap_std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
Expand Down Expand Up @@ -1029,6 +1123,40 @@ impl DirExtUtf8 for cap_std::fs_utf8::Dir {

Ok(())
}

/// Test for accessibility or existence of a filesystem object.
fn access<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
access(
&self.as_filelike_view::<std::fs::File>(),
path.as_ref().as_ref(),
type_,
FollowSymlinks::Yes,
)
}

/// Test for accessibility or existence of a filesystem object.
fn access_symlink<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
access(
&self.as_filelike_view::<std::fs::File>(),
path.as_ref().as_ref(),
type_,
FollowSymlinks::No,
)
}

/// Changes the permissions found on a file or a directory, without following
/// symbolic links.
fn set_symlink_permissions<P: AsRef<Utf8Path>>(
&self,
path: P,
perm: Permissions,
) -> io::Result<()> {
set_symlink_permissions(
&self.as_filelike_view::<std::fs::File>(),
path.as_ref().as_ref(),
perm,
)
}
}

#[cfg(all(feature = "async_std", feature = "fs_utf8"))]
Expand Down
2 changes: 1 addition & 1 deletion cap-fs-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ mod reopen;
pub use dir_entry_ext::DirEntryExt;
#[cfg(all(any(feature = "std", feature = "async_std"), feature = "fs_utf8"))]
pub use dir_ext::DirExtUtf8;
pub use dir_ext::{DirExt, SystemTimeSpec};
pub use dir_ext::{AccessType, DirExt, SystemTimeSpec};
pub use file_type_ext::FileTypeExt;
pub use is_file_read_write::IsFileReadWrite;
pub use metadata_ext::MetadataExt;
Expand Down
16 changes: 8 additions & 8 deletions cap-net-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ pub trait PoolExt: private::Sealed {
/// Initiate a TCP connection, converting a [`TcpListener`] to a
/// [`TcpStream`].
///
/// This is simlar to to [`Pool::connect_tcp_stream`] in that it performs a
/// This is simlar to [`Pool::connect_tcp_stream`] in that it performs a
/// TCP connection, but instead of creating a new socket itself it takes a
/// [`TcpListener`], such as one created with [`TcpListenerExt::new`].
///
Expand All @@ -263,8 +263,8 @@ pub trait PoolExt: private::Sealed {

/// Initiate a TCP connection on a socket.
///
/// This is simlar to to [`Self::connect_into_tcp_stream`], however instead
/// of converting a `TcpListener` to a `TcpStream`, it leaves fd in the
/// This is simlar to [`Self::connect_into_tcp_stream`], however instead of
/// converting a `TcpListener` to a `TcpStream`, it leaves fd in the
/// existing `TcpListener`.
///
/// This function ensures that the address to connect to is permitted by
Expand All @@ -279,7 +279,7 @@ pub trait PoolExt: private::Sealed {

/// Initiate a UDP connection.
///
/// This is simlar to to [`Pool::connect_udp_socket`] in that it performs a
/// This is simlar to [`Pool::connect_udp_socket`] in that it performs a
/// UDP connection, but instead of creating a new socket itself it takes a
/// [`UdpSocket`], such as one created with [`UdpSocketExt::new`].
///
Expand Down Expand Up @@ -537,7 +537,7 @@ impl TcpConnecter {
/// Initiate a TCP connection, converting a [`TcpListener`] to a
/// [`TcpStream`].
///
/// This is simlar to to [`Pool::connect_tcp_stream`] in that it performs a
/// This is simlar to [`Pool::connect_tcp_stream`] in that it performs a
/// TCP connection, but instead of creating a new socket itself it takes a
/// [`TcpListener`], such as one created with [`TcpListenerExt::new`].
///
Expand All @@ -554,8 +554,8 @@ impl TcpConnecter {

/// Initiate a TCP connection on a socket.
///
/// This is simlar to to [`Pool::connect_into_tcp_stream`], however instead
/// of converting a `TcpListener` to a `TcpStream`, it leaves fd in the
/// This is simlar to [`Pool::connect_into_tcp_stream`], however instead of
/// converting a `TcpListener` to a `TcpStream`, it leaves fd in the
/// existing `TcpListener`.
///
/// This is similar to [`PoolExt::connect_existing_tcp_listener`] except
Expand Down Expand Up @@ -584,7 +584,7 @@ pub struct UdpConnecter(smallvec::SmallVec<[SocketAddr; 1]>);
impl UdpConnecter {
/// Initiate a UDP connection.
///
/// This is simlar to to [`Pool::connect_udp_socket`] in that it performs a
/// This is simlar to [`Pool::connect_udp_socket`] in that it performs a
/// UDP connection, but instead of creating a new socket itself it takes a
/// [`UdpSocket`], such as one created with [`UdpSocketExt::new`].
///
Expand Down
89 changes: 89 additions & 0 deletions cap-primitives/src/fs/access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! Access test functions.
use crate::fs::{access_impl, FollowSymlinks};
#[cfg(racy_asserts)]
use crate::fs::{access_unchecked, file_path};
use std::path::Path;
use std::{fs, io};

/// Access modes for use with [`DirExt::access`].
#[derive(Clone, Copy, Debug)]
pub struct AccessModes {
/// Is the object readable?
pub readable: bool,
/// Is the object writable?
pub writable: bool,
/// Is the object executable?
pub executable: bool,
}

/// Access modes for use with [`DirExt::access`].
#[derive(Clone, Copy, Debug)]
pub enum AccessType {
/// Test whether the named object is accessible in the given modes.
Access(AccessModes),

/// Test whether the named object exists.
Exists,
}

/// Canonicalize the given path, ensuring that the resolution of the path never
/// escapes the directory tree rooted at `start`.
#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
pub fn access(
start: &fs::File,
path: &Path,
type_: AccessType,
follow: FollowSymlinks,
) -> io::Result<()> {
// Call the underlying implementation.
let result = access_impl(start, path, type_, follow);

#[cfg(racy_asserts)]
let unchecked = access_unchecked(start, path, type_, follow);

#[cfg(racy_asserts)]
check_access(start, path, type_, follow, &result, &unchecked);

result
}

#[cfg(racy_asserts)]
#[allow(clippy::enum_glob_use)]
fn check_access(
start: &fs::File,
path: &Path,
_type_: AccessType,
_follow: FollowSymlinks,
result: &io::Result<()>,
unchecked: &io::Result<()>,
) {
use io::ErrorKind::*;

match (map_result(result), map_result(stat)) {
(Ok(()), Ok(())) => {}

(Err((PermissionDenied, message)), _) => {
// TODO: Check that access in the no-follow case got the right
// error.
}

(Err((kind, message)), Err((unchecked_kind, unchecked_message))) => {
assert_eq!(kind, unchecked_kind);
assert_eq!(
message,
unchecked_message,
"start='{:?}', path='{:?}'",
start,
path.display()
);
}

other => panic!(
"unexpected result from access start='{:?}', path='{}':\n{:#?}",
start,
path.display(),
other,
),
}
}
Loading

0 comments on commit 37a3033

Please sign in to comment.