Skip to content

Commit b8e430b

Browse files
authored
Add set_symlink_permissions and access functions to DirExt (#337)
`set_symlink_permissions` is to `set_permissions` as `symlink_metadata` is to `metadata`; it doesn't follow symlinks. `access` follows the POSIX `faccessat` behavior.
1 parent 9c1e426 commit b8e430b

File tree

17 files changed

+473
-20
lines changed

17 files changed

+473
-20
lines changed

cap-fs-ext/src/dir_ext.rs

Lines changed: 135 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
#[cfg(feature = "fs_utf8")]
22
use camino::Utf8Path;
3+
#[cfg(all(windows, feature = "async_std", feature = "fs_utf8"))]
4+
use cap_primitives::fs::stat;
35
#[cfg(not(windows))]
46
use cap_primitives::fs::symlink;
5-
use cap_primitives::fs::{open_dir_nofollow, set_times, set_times_nofollow};
6-
#[cfg(all(windows, feature = "async_std", feature = "fs_utf8"))]
7-
use cap_primitives::fs::{stat, FollowSymlinks};
7+
use cap_primitives::fs::{
8+
access, open_dir_nofollow, set_symlink_permissions, set_times, set_times_nofollow,
9+
FollowSymlinks, Permissions,
10+
};
811
#[cfg(windows)]
912
use cap_primitives::fs::{symlink_dir, symlink_file};
1013
use io_lifetimes::AsFilelike;
@@ -13,7 +16,7 @@ use std::path::Path;
1316
#[cfg(feature = "async_std")]
1417
use {async_std::task::spawn_blocking, async_trait::async_trait};
1518

16-
pub use cap_primitives::fs::SystemTimeSpec;
19+
pub use cap_primitives::fs::{AccessType, SystemTimeSpec};
1720

1821
/// Extension trait for `Dir`.
1922
pub trait DirExt {
@@ -95,6 +98,18 @@ pub trait DirExt {
9598
/// points to a directory, it cannot be removed with the `remove_file`
9699
/// operation. This method will remove files and all symlinks.
97100
fn remove_file_or_symlink<P: AsRef<Path>>(&self, path: P) -> io::Result<()>;
101+
102+
/// Test for accessibility or existence of a filesystem object.
103+
fn access<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;
104+
105+
/// Test for accessibility or existence of a filesystem object, without
106+
/// following symbolic links.
107+
fn access_symlink<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;
108+
109+
/// Changes the permissions found on a file or a directory, without following
110+
/// symbolic links.
111+
fn set_symlink_permissions<P: AsRef<Path>>(&self, path: P, perm: Permissions)
112+
-> io::Result<()>;
98113
}
99114

100115
/// Extension trait for `Dir`, async.
@@ -216,6 +231,17 @@ pub trait AsyncDirExt {
216231
&self,
217232
path: P,
218233
) -> io::Result<()>;
234+
235+
/// Test for accessibility or existence of a filesystem object.
236+
async fn access<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;
237+
238+
/// Changes the permissions found on a file or a directory, without following
239+
/// symbolic links.
240+
async fn set_symlink_permissions<P: AsRef<Path>>(
241+
&self,
242+
path: P,
243+
perm: Permissions,
244+
) -> io::Result<()>;
219245
}
220246

221247
/// `fs_utf8` version of `DirExt`.
@@ -304,6 +330,21 @@ pub trait DirExtUtf8 {
304330
/// on symlinks to directories on Windows, similar to how `unlink` works
305331
/// on symlinks to directories on Posix-ish platforms.
306332
fn remove_file_or_symlink<P: AsRef<Utf8Path>>(&self, path: P) -> io::Result<()>;
333+
334+
/// Test for accessibility or existence of a filesystem object.
335+
fn access<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;
336+
337+
/// Test for accessibility or existence of a filesystem object, without
338+
/// following symbolic links.
339+
fn access_symlink<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;
340+
341+
/// Changes the permissions found on a file or a directory, without following
342+
/// symbolic links.
343+
fn set_symlink_permissions<P: AsRef<Utf8Path>>(
344+
&self,
345+
path: P,
346+
perm: Permissions,
347+
) -> io::Result<()>;
307348
}
308349

309350
/// `fs_utf8` version of `DirExt`.
@@ -410,6 +451,25 @@ pub trait AsyncDirExtUtf8 {
410451
/// points to a directory, it cannot be removed with the `remove_file`
411452
/// operation. This method will remove files and all symlinks.
412453
async fn remove_file_or_symlink<P: AsRef<Utf8Path> + Send>(&self, path: P) -> io::Result<()>;
454+
455+
/// Test for accessibility or existence of a filesystem object.
456+
async fn access<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()>;
457+
458+
/// Test for accessibility or existence of a filesystem object, without
459+
/// following symbolic links.
460+
async fn access_symlink<P: AsRef<Utf8Path>>(
461+
&self,
462+
path: P,
463+
type_: AccessType,
464+
) -> io::Result<()>;
465+
466+
/// Changes the permissions found on a file or a directory, without following
467+
/// symbolic links.
468+
async fn set_symlink_permissions<P: AsRef<Utf8Path>>(
469+
&self,
470+
path: P,
471+
perm: Permissions,
472+
) -> io::Result<()>;
413473
}
414474

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

567627
Ok(())
568628
}
629+
630+
/// Test for accessibility or existence of a filesystem object.
631+
fn access<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
632+
access(
633+
&self.as_filelike_view::<std::fs::File>(),
634+
path.as_ref(),
635+
type_,
636+
FollowSymlinks::Yes,
637+
)
638+
}
639+
640+
/// Test for accessibility or existence of a filesystem object.
641+
fn access_symlink<P: AsRef<Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
642+
access(
643+
&self.as_filelike_view::<std::fs::File>(),
644+
path.as_ref(),
645+
type_,
646+
FollowSymlinks::No,
647+
)
648+
}
649+
650+
/// Changes the permissions found on a file or a directory, without following
651+
/// symbolic links.
652+
fn set_symlink_permissions<P: AsRef<Path>>(
653+
&self,
654+
path: P,
655+
perm: Permissions,
656+
) -> io::Result<()> {
657+
set_symlink_permissions(
658+
&self.as_filelike_view::<std::fs::File>(),
659+
path.as_ref(),
660+
perm,
661+
)
662+
}
569663
}
570664

571665
#[cfg(feature = "async_std")]
@@ -829,7 +923,7 @@ impl AsyncDirExt for cap_async_std::fs::Dir {
829923
&self,
830924
path: P,
831925
) -> io::Result<()> {
832-
use crate::{FollowSymlinks, OpenOptionsFollowExt};
926+
use crate::OpenOptionsFollowExt;
833927
use cap_primitives::fs::_WindowsByHandle;
834928
use cap_std::fs::OpenOptions;
835929
use std::os::windows::fs::OpenOptionsExt;
@@ -996,7 +1090,7 @@ impl DirExtUtf8 for cap_std::fs_utf8::Dir {
9961090
#[cfg(windows)]
9971091
#[inline]
9981092
fn remove_file_or_symlink<P: AsRef<Utf8Path>>(&self, path: P) -> io::Result<()> {
999-
use crate::{FollowSymlinks, OpenOptionsFollowExt};
1093+
use crate::OpenOptionsFollowExt;
10001094
use cap_primitives::fs::_WindowsByHandle;
10011095
use cap_std::fs::OpenOptions;
10021096
use std::os::windows::fs::OpenOptionsExt;
@@ -1029,6 +1123,40 @@ impl DirExtUtf8 for cap_std::fs_utf8::Dir {
10291123

10301124
Ok(())
10311125
}
1126+
1127+
/// Test for accessibility or existence of a filesystem object.
1128+
fn access<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
1129+
access(
1130+
&self.as_filelike_view::<std::fs::File>(),
1131+
path.as_ref().as_ref(),
1132+
type_,
1133+
FollowSymlinks::Yes,
1134+
)
1135+
}
1136+
1137+
/// Test for accessibility or existence of a filesystem object.
1138+
fn access_symlink<P: AsRef<Utf8Path>>(&self, path: P, type_: AccessType) -> io::Result<()> {
1139+
access(
1140+
&self.as_filelike_view::<std::fs::File>(),
1141+
path.as_ref().as_ref(),
1142+
type_,
1143+
FollowSymlinks::No,
1144+
)
1145+
}
1146+
1147+
/// Changes the permissions found on a file or a directory, without following
1148+
/// symbolic links.
1149+
fn set_symlink_permissions<P: AsRef<Utf8Path>>(
1150+
&self,
1151+
path: P,
1152+
perm: Permissions,
1153+
) -> io::Result<()> {
1154+
set_symlink_permissions(
1155+
&self.as_filelike_view::<std::fs::File>(),
1156+
path.as_ref().as_ref(),
1157+
perm,
1158+
)
1159+
}
10321160
}
10331161

10341162
#[cfg(all(feature = "async_std", feature = "fs_utf8"))]

cap-fs-ext/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod reopen;
2323
pub use dir_entry_ext::DirEntryExt;
2424
#[cfg(all(any(feature = "std", feature = "async_std"), feature = "fs_utf8"))]
2525
pub use dir_ext::DirExtUtf8;
26-
pub use dir_ext::{DirExt, SystemTimeSpec};
26+
pub use dir_ext::{AccessType, DirExt, SystemTimeSpec};
2727
pub use file_type_ext::FileTypeExt;
2828
pub use is_file_read_write::IsFileReadWrite;
2929
pub use metadata_ext::MetadataExt;

cap-net-ext/src/lib.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ pub trait PoolExt: private::Sealed {
244244
/// Initiate a TCP connection, converting a [`TcpListener`] to a
245245
/// [`TcpStream`].
246246
///
247-
/// This is simlar to to [`Pool::connect_tcp_stream`] in that it performs a
247+
/// This is simlar to [`Pool::connect_tcp_stream`] in that it performs a
248248
/// TCP connection, but instead of creating a new socket itself it takes a
249249
/// [`TcpListener`], such as one created with [`TcpListenerExt::new`].
250250
///
@@ -263,8 +263,8 @@ pub trait PoolExt: private::Sealed {
263263

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

280280
/// Initiate a UDP connection.
281281
///
282-
/// This is simlar to to [`Pool::connect_udp_socket`] in that it performs a
282+
/// This is simlar to [`Pool::connect_udp_socket`] in that it performs a
283283
/// UDP connection, but instead of creating a new socket itself it takes a
284284
/// [`UdpSocket`], such as one created with [`UdpSocketExt::new`].
285285
///
@@ -537,7 +537,7 @@ impl TcpConnecter {
537537
/// Initiate a TCP connection, converting a [`TcpListener`] to a
538538
/// [`TcpStream`].
539539
///
540-
/// This is simlar to to [`Pool::connect_tcp_stream`] in that it performs a
540+
/// This is simlar to [`Pool::connect_tcp_stream`] in that it performs a
541541
/// TCP connection, but instead of creating a new socket itself it takes a
542542
/// [`TcpListener`], such as one created with [`TcpListenerExt::new`].
543543
///
@@ -554,8 +554,8 @@ impl TcpConnecter {
554554

555555
/// Initiate a TCP connection on a socket.
556556
///
557-
/// This is simlar to to [`Pool::connect_into_tcp_stream`], however instead
558-
/// of converting a `TcpListener` to a `TcpStream`, it leaves fd in the
557+
/// This is simlar to [`Pool::connect_into_tcp_stream`], however instead of
558+
/// converting a `TcpListener` to a `TcpStream`, it leaves fd in the
559559
/// existing `TcpListener`.
560560
///
561561
/// This is similar to [`PoolExt::connect_existing_tcp_listener`] except
@@ -584,7 +584,7 @@ pub struct UdpConnecter(smallvec::SmallVec<[SocketAddr; 1]>);
584584
impl UdpConnecter {
585585
/// Initiate a UDP connection.
586586
///
587-
/// This is simlar to to [`Pool::connect_udp_socket`] in that it performs a
587+
/// This is simlar to [`Pool::connect_udp_socket`] in that it performs a
588588
/// UDP connection, but instead of creating a new socket itself it takes a
589589
/// [`UdpSocket`], such as one created with [`UdpSocketExt::new`].
590590
///

cap-primitives/src/fs/access.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//! Access test functions.
2+
3+
use crate::fs::{access_impl, FollowSymlinks};
4+
#[cfg(racy_asserts)]
5+
use crate::fs::{access_unchecked, file_path};
6+
use std::path::Path;
7+
use std::{fs, io};
8+
9+
/// Access modes for use with [`DirExt::access`].
10+
#[derive(Clone, Copy, Debug)]
11+
pub struct AccessModes {
12+
/// Is the object readable?
13+
pub readable: bool,
14+
/// Is the object writable?
15+
pub writable: bool,
16+
/// Is the object executable?
17+
pub executable: bool,
18+
}
19+
20+
/// Access modes for use with [`DirExt::access`].
21+
#[derive(Clone, Copy, Debug)]
22+
pub enum AccessType {
23+
/// Test whether the named object is accessible in the given modes.
24+
Access(AccessModes),
25+
26+
/// Test whether the named object exists.
27+
Exists,
28+
}
29+
30+
/// Canonicalize the given path, ensuring that the resolution of the path never
31+
/// escapes the directory tree rooted at `start`.
32+
#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
33+
pub fn access(
34+
start: &fs::File,
35+
path: &Path,
36+
type_: AccessType,
37+
follow: FollowSymlinks,
38+
) -> io::Result<()> {
39+
// Call the underlying implementation.
40+
let result = access_impl(start, path, type_, follow);
41+
42+
#[cfg(racy_asserts)]
43+
let unchecked = access_unchecked(start, path, type_, follow);
44+
45+
#[cfg(racy_asserts)]
46+
check_access(start, path, type_, follow, &result, &unchecked);
47+
48+
result
49+
}
50+
51+
#[cfg(racy_asserts)]
52+
#[allow(clippy::enum_glob_use)]
53+
fn check_access(
54+
start: &fs::File,
55+
path: &Path,
56+
_type_: AccessType,
57+
_follow: FollowSymlinks,
58+
result: &io::Result<()>,
59+
unchecked: &io::Result<()>,
60+
) {
61+
use io::ErrorKind::*;
62+
63+
match (map_result(result), map_result(stat)) {
64+
(Ok(()), Ok(())) => {}
65+
66+
(Err((PermissionDenied, message)), _) => {
67+
// TODO: Check that access in the no-follow case got the right
68+
// error.
69+
}
70+
71+
(Err((kind, message)), Err((unchecked_kind, unchecked_message))) => {
72+
assert_eq!(kind, unchecked_kind);
73+
assert_eq!(
74+
message,
75+
unchecked_message,
76+
"start='{:?}', path='{:?}'",
77+
start,
78+
path.display()
79+
);
80+
}
81+
82+
other => panic!(
83+
"unexpected result from access start='{:?}', path='{}':\n{:#?}",
84+
start,
85+
path.display(),
86+
other,
87+
),
88+
}
89+
}

0 commit comments

Comments
 (0)