diff --git a/Cargo.toml b/Cargo.toml index a5b5b3f..88f5a15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,28 @@ version = "0.1.21" authors = ["paul@colomiets.name"] edition = "2018" +[features] +default = [] +linux = ["o_path", "o_directory", "o_tmpfile", "statx", "proc_self_fd", "link_file_at", "rename_exchange", "renameat_flags"] +#NOTE(cehteh): eventually provide some baseline configs for other OS'es (for cross compilation) +o_path = [] +o_directory = [] +o_tmpfile = [] +o_search = [] +fcntl_o_directory = [] +proc_self_fd = [] +link_file_at = [] +renameat_flags = [] +rename_exchange = [] +statx = [] + [dependencies] libc = "0.2.34" +[build-dependencies] +libc = "0.2.34" +conf_test = "0.2" + [dev-dependencies] argparse = "0.2.1" tempfile = "3.0.3" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..851c014 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +use conf_test::ConfTest; + +fn main() { + ConfTest::run(); +} diff --git a/conf_tests/fcntl_o_directory.rs b/conf_tests/fcntl_o_directory.rs new file mode 100644 index 0000000..5a135f0 --- /dev/null +++ b/conf_tests/fcntl_o_directory.rs @@ -0,0 +1,16 @@ +extern crate libc; + +fn main() { + unsafe { + let conf_tests = std::ffi::CString::new("conf_tests").unwrap(); + let fd = libc::open(conf_tests.as_ptr(), libc::O_DIRECTORY | libc::O_RDONLY); + + let flags = libc::fcntl(fd, libc::F_GETFL); + + if flags != -1 && flags & libc::O_DIRECTORY != 0 { + std::process::exit(0); + } else { + std::process::exit(1); + } + } +} diff --git a/conf_tests/link_file_at.rs b/conf_tests/link_file_at.rs new file mode 100644 index 0000000..0dd292d --- /dev/null +++ b/conf_tests/link_file_at.rs @@ -0,0 +1,13 @@ +extern crate libc; + +fn main() { + //NOTE(cehteh): same as the proc_self_fd test, maybe we need something smarter in future + unsafe { + let conf_tests = std::ffi::CString::new("/proc/self/fd/0").unwrap(); + if libc::open(conf_tests.as_ptr(), libc::O_RDONLY) != -1 { + std::process::exit(0); + } else { + std::process::exit(1); + } + } +} diff --git a/conf_tests/o_directory.rs b/conf_tests/o_directory.rs new file mode 100644 index 0000000..07c0f61 --- /dev/null +++ b/conf_tests/o_directory.rs @@ -0,0 +1,8 @@ +extern crate libc; + +fn main() { + unsafe { + let conf_tests = std::ffi::CString::new("conf_tests").unwrap(); + libc::open(conf_tests.as_ptr(), libc::O_DIRECTORY); + } +} diff --git a/conf_tests/o_path.rs b/conf_tests/o_path.rs new file mode 100644 index 0000000..e454460 --- /dev/null +++ b/conf_tests/o_path.rs @@ -0,0 +1,8 @@ +extern crate libc; + +fn main () { + unsafe { + let conf_tests = std::ffi::CString::new("conf_tests").unwrap(); + libc::open(conf_tests.as_ptr(), libc::O_PATH); + } +} diff --git a/conf_tests/o_search.rs b/conf_tests/o_search.rs new file mode 100644 index 0000000..60a3250 --- /dev/null +++ b/conf_tests/o_search.rs @@ -0,0 +1,8 @@ +extern crate libc; + +fn main() { + unsafe { + let conf_tests = std::ffi::CString::new("conf_tests").unwrap(); + libc::open(conf_tests.as_ptr(), libc::O_SEARCH); + } +} diff --git a/conf_tests/o_tmpfile.rs b/conf_tests/o_tmpfile.rs new file mode 100644 index 0000000..8c0ad16 --- /dev/null +++ b/conf_tests/o_tmpfile.rs @@ -0,0 +1,10 @@ +extern crate libc; + +fn main () { + unsafe { + let conf_tests = std::ffi::CString::new("conf_tests").unwrap(); + libc::open(conf_tests.as_ptr(), + libc::O_TMPFILE | libc::O_RDWR, + libc::S_IRUSR | libc::S_IWUSR); + } +} diff --git a/conf_tests/proc_self_fd.rs b/conf_tests/proc_self_fd.rs new file mode 100644 index 0000000..7983b5c --- /dev/null +++ b/conf_tests/proc_self_fd.rs @@ -0,0 +1,12 @@ +extern crate libc; + +fn main() { + unsafe { + let conf_tests = std::ffi::CString::new("/proc/self/fd/0").unwrap(); + if libc::open(conf_tests.as_ptr(), libc::O_RDONLY) != -1 { + std::process::exit(0); + } else { + std::process::exit(1); + } + } +} diff --git a/conf_tests/rename_exchange.rs b/conf_tests/rename_exchange.rs new file mode 100644 index 0000000..76f69f9 --- /dev/null +++ b/conf_tests/rename_exchange.rs @@ -0,0 +1,5 @@ +extern crate libc; + +fn main() { + let does_rename_exchange_exist = libc::RENAME_EXCHANGE; +} diff --git a/conf_tests/renameat_flags.rs b/conf_tests/renameat_flags.rs new file mode 100644 index 0000000..392ba1c --- /dev/null +++ b/conf_tests/renameat_flags.rs @@ -0,0 +1,5 @@ +extern crate libc; + +fn main() { + let does_renameat2_exist = libc::SYS_renameat2; +} diff --git a/examples/exchange.rs b/examples/exchange.rs index 21fdf0a..fdd651d 100644 --- a/examples/exchange.rs +++ b/examples/exchange.rs @@ -32,8 +32,10 @@ fn main() { } let parent = path1.parent().expect("path must have parent directory"); let dir = Dir::open(parent).expect("can open directory"); + #[cfg(feature = "rename_exchange")] dir.local_exchange( path1.file_name().expect("path1 must have filename"), path2.file_name().expect("path2 must have filename"), - ).expect("can rename"); + ) + .expect("can rename"); } diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..909d798 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,155 @@ +use std::ffi::CStr; +use std::fs::File; +use std::io; + +use crate::dir::{clone_dirfd_upgrade, to_cstr}; +use crate::{AsPath, Dir}; + +/// 'Dir::new()' creates a new DirFlags object with default (O_CLOEXEC) flags. One can then +/// freely add/remove flags to the set. The final open calls will add O_DIRECTORY and O_PATH +/// as applicable/supported but not verify or remove any defined flags. This allows passing +/// flags the 'openat' implementation is not even aware about. Thus the open call may fail +/// with some error when one constructed an invalid flag set. +#[derive(Copy, Clone)] +pub struct DirFlags { + flags: libc::c_int, +} + +impl DirFlags { + #[inline] + pub(crate) fn new(flags: libc::c_int) -> DirFlags { + DirFlags { flags } + } + + /// Sets the given flags + #[inline] + pub fn with(self, flags: libc::c_int) -> DirFlags { + DirFlags { + flags: self.flags | flags, + } + } + + /// Clears the given flags + #[inline] + pub fn without(self, flags: libc::c_int) -> DirFlags { + DirFlags { + flags: self.flags & !flags, + } + } + + /// Queries current flags + #[inline] + pub fn get_flags(&self) -> libc::c_int { + self.flags + } + + /// Open a directory descriptor at specified path + #[inline] + pub fn open(&self, path: P) -> io::Result { + Dir::_open(to_cstr(path)?.as_ref(), self.flags) + } +} + +/// 'Dir::with(&self)'/'Dir::with(&self)' creates a new DirMethodsFlags object with default +/// (O_CLOEXEC|O_NOFOLLOW) flags. One can then freely add/remove flags to the set. +/// Implements proxies for the Dir:: methods that open contained objects. +#[derive(Copy, Clone)] +pub struct DirMethodFlags<'a> { + object: &'a Dir, + flags: libc::c_int, +} + +impl<'a> DirMethodFlags<'a> { + #[inline] + pub(crate) fn new(object: &'a Dir, flags: libc::c_int) -> Self { + Self { object, flags } + } + + /// Sets the given flags + #[inline] + pub fn with(self, flags: libc::c_int) -> Self { + Self { + object: self.object, + flags: self.flags | flags, + } + } + + /// Clears the given flags + #[inline] + pub fn without(self, flags: libc::c_int) -> Self { + Self { + object: self.object, + flags: self.flags & !flags, + } + } + + /// Open subdirectory + #[inline] + pub fn sub_dir(&self, path: P) -> io::Result { + self.object._sub_dir(to_cstr(path)?.as_ref(), self.flags) + } + + /// Open file for reading in this directory + #[inline] + pub fn open_file(&self, path: P) -> io::Result { + self.object + ._open_file(to_cstr(path)?.as_ref(), self.flags | libc::O_RDONLY, 0) + } + + /// Open file for writing, create if necessary, truncate on open + #[inline] + pub fn write_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self.object._open_file( + to_cstr(path)?.as_ref(), + self.flags | libc::O_CREAT | libc::O_WRONLY | libc::O_TRUNC, + mode, + ) + } + + /// Open file for append, create if necessary + #[inline] + pub fn append_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self.object._open_file( + to_cstr(path)?.as_ref(), + self.flags | libc::O_CREAT | libc::O_WRONLY | libc::O_APPEND, + mode, + ) + } + + /// Create file for writing (and truncate) in this directory + #[deprecated(since = "0.1.7", note = "please use `write_file` instead")] + #[inline] + pub fn create_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self.object._open_file( + to_cstr(path)?.as_ref(), + self.flags | libc::O_CREAT | libc::O_WRONLY | libc::O_TRUNC, + mode, + ) + } + + /// Create a tmpfile in this directory which isn't linked to any filename + #[cfg(feature = "o_tmpfile")] + #[inline] + pub fn new_unnamed_file(&self, mode: libc::mode_t) -> io::Result { + self.object._open_file( + unsafe { CStr::from_bytes_with_nul_unchecked(b".\0") }, + self.flags | libc::O_TMPFILE | libc::O_WRONLY, + mode, + ) + } + + /// Create file if not exists, fail if exists + #[inline] + pub fn new_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self.object._open_file( + to_cstr(path)?.as_ref(), + self.flags | libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY, + mode, + ) + } + + /// Creates a new 'Normal' independently owned handle to the underlying directory. + pub fn clone_upgrade(&self) -> io::Result { + Ok(Dir(clone_dirfd_upgrade(self.object.0, self.flags)?)) + } +} diff --git a/src/dir.rs b/src/dir.rs index 3bed451..06f79ec 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -7,17 +7,32 @@ use std::os::unix::ffi::{OsStringExt}; use std::path::{PathBuf}; use libc; +use crate::list::{open_dirfd, DirIter}; use crate::metadata::{self, Metadata}; -use crate::list::{DirIter, open_dir, open_dirfd}; -use crate::{Dir, AsPath}; +use crate::{Dir, AsPath, DirFlags, DirMethodFlags}; -#[cfg(target_os="linux")] -const BASE_OPEN_FLAGS: libc::c_int = libc::O_PATH|libc::O_CLOEXEC; -#[cfg(target_os="freebsd")] -const BASE_OPEN_FLAGS: libc::c_int = libc::O_DIRECTORY|libc::O_CLOEXEC; -#[cfg(not(any(target_os="linux", target_os="freebsd")))] -const BASE_OPEN_FLAGS: libc::c_int = libc::O_CLOEXEC; + +/// Value if the libc::O_DIRECTORY flag when supported by the system, otherwise 0 +#[cfg(feature = "o_directory")] +pub const O_DIRECTORY: libc::c_int = libc::O_DIRECTORY; +/// Value if the libc::O_DIRECTORY flag when supported by the system, otherwise 0 +#[cfg(not(feature = "o_directory"))] +pub const O_DIRECTORY: libc::c_int = 0; + +/// Value if the libc::O_PATH flag when supported by the system, otherwise 0 +#[cfg(feature = "o_path")] +pub const O_PATH: libc::c_int = libc::O_PATH; +/// Value if the libc::O_PATH flag when supported by the system, otherwise 0 +#[cfg(not(feature = "o_path"))] +pub const O_PATH: libc::c_int = 0; + +/// Value if the libc::O_SEARCH flag when supported by the system, otherwise 0 +#[cfg(feature = "o_search")] +pub const O_SEARCH: libc::c_int = libc::O_SEARCH; +/// Value if the libc::O_SEARCH flag when supported by the system, otherwise 0 +#[cfg(not(feature = "o_search"))] +pub const O_SEARCH: libc::c_int = 0; impl Dir { /// Creates a directory descriptor that resolves paths relative to current @@ -33,20 +48,47 @@ impl Dir { Dir(libc::AT_FDCWD) } - /// Open a directory descriptor at specified path - // TODO(tailhook) maybe accept only absolute paths? - pub fn open(path: P) -> io::Result { - Dir::_open(to_cstr(path)?.as_ref()) + /// Create a flags builder for Dir objects. Initial flags default to `O_CLOEXEC'. More + /// flags can be set added by 'with()' and existing/default flags can be removed by + /// 'without()'. The flags builder can the be used to 'open()' to create + /// a Dir handle. + #[inline] + pub fn flags() -> DirFlags { + DirFlags::new(libc::O_CLOEXEC) } - fn _open(path: &CStr) -> io::Result { - let fd = unsafe { - libc::open(path.as_ptr(), BASE_OPEN_FLAGS) - }; - if fd < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(Dir(fd)) + /// Open a directory descriptor at specified path with O_PATH when available. + /// A descriptor obtained with this flag is restricted to do only certain operations: + /// - It may be used as anchor for opening sub-objects + /// - One can query metadata of this directory + /// + /// This handle is not suitable for a 'Dir::list()' call and may yield a runtime error. + /// Use 'Dir::flags().open()' to get a handle without O_PATH defined or use + /// 'Dir::list_self()' which clone-upgrades the handle it used for the iteration. + // TODO(tailhook) maybe accept only absolute paths? + pub fn open(path: P) -> io::Result { + Dir::_open(to_cstr(path)?.as_ref(), O_PATH | libc::O_CLOEXEC) + } + + pub(crate) fn _open(path: &CStr, flags: libc::c_int) -> io::Result { + let fd = unsafe { libc_ok(libc::open(path.as_ptr(), O_DIRECTORY | flags))? }; + Ok(Dir(fd)) + } + + /// Checks if the fd associated with the Dir object is really a directory. + /// There are subtle differences in how directories can be opened and what properties the + /// resulting file handles have. On some platforms it is possible that + /// Dir::open("somefile") succeeds. This will usually raise errors later when one tries to + /// do Directory operations on this. While checking if such an handle comes with cost of a + /// potential expensive 'stat()' operation. This library makes the assumption that in the + /// 'usual' case Dir objects are only created on directories and operations on Dir handles + /// handle errors properly. Still in some cases one may check a freshly created handle + /// explicitly. Thats what 'is_dir()' is for. Returns 'true' when the underlying handles + /// represents a directory and false otherwise. + pub fn is_dir(&self) -> bool { + match fd_type(self.0).unwrap_or(FdType::Other) { + FdType::NormalDir | FdType::OPathDir => true, + FdType::Other => false, } } @@ -54,37 +96,64 @@ impl Dir { /// /// You can list directory itself with `list_self`. pub fn list_dir(&self, path: P) -> io::Result { - open_dir(self, to_cstr(path)?.as_ref()) + self.with(O_SEARCH).sub_dir(path)?.list() } /// List this dir pub fn list_self(&self) -> io::Result { - unsafe { - open_dirfd(libc::dup(self.0)) - } + self.with(O_SEARCH).clone_upgrade()?.list() + } + + /// Create a DirIter from a Dir + /// Dir must not be a handle opened with O_PATH. + pub fn list(self) -> io::Result { + let fd = self.0; + std::mem::forget(self); + open_dirfd(fd) + } + + /// Create a flags builder for member methods. Defaults to `O_CLOEXEC | O_NOFOLLOW` plus + /// the given flags. Further flags can be added/removed by the 'with()'/'without()' + /// members. And finally be used by 'sub_dir()' and the different 'open()' calls. + #[inline] + pub fn with<'a>(&'a self, flags: libc::c_int) -> DirMethodFlags<'a> { + DirMethodFlags::new(self, libc::O_CLOEXEC | libc::O_NOFOLLOW | flags) } - /// Open subdirectory + /// Create a flags builder for member methods. Defaults to `O_CLOEXEC | O_NOFOLLOW` with + /// the given flags (which may O_CLOEXEC or O_NOFOLLOW) removed. Further flags can be + /// added/removed by the 'with()'/'without()' members. And finally be used by 'sub_dir()' + /// and the different 'open()' calls. + #[inline] + pub fn without<'a>(&'a self, flags: libc::c_int) -> DirMethodFlags<'a> { + DirMethodFlags::new(self, (libc::O_CLOEXEC | libc::O_NOFOLLOW) & !flags) + } + + /// Open subdirectory with O_PATH when available. + /// A descriptor obtained with this flag is restricted to do only certain operations: + /// - It may be used as anchor for opening sub-objects + /// - One can query metadata of this directory + /// + /// This handle is not suitable for a 'Dir::list()' call and may yield a runtime error. + /// Use 'Dir::with(0).sub_dir()' to get a handle without O_PATH defined or use + /// 'Dir::list_dir()' which clone-upgrades the handle it used for the iteration. /// /// Note that this method does not resolve symlinks by default, so you may have to call - /// [`read_link`] to resolve the real path first. + /// [`read_link`] to resolve the real path first or create a handle + /// 'without(libc::O_NOFOLLOW)'. /// /// [`read_link`]: #method.read_link pub fn sub_dir(&self, path: P) -> io::Result { - self._sub_dir(to_cstr(path)?.as_ref()) + self._sub_dir( + to_cstr(path)?.as_ref(), + O_PATH | libc::O_CLOEXEC | libc::O_NOFOLLOW, + ) } - fn _sub_dir(&self, path: &CStr) -> io::Result { - let fd = unsafe { - libc::openat(self.0, - path.as_ptr(), - BASE_OPEN_FLAGS|libc::O_NOFOLLOW) - }; - if fd < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(Dir(fd)) - } + pub(crate) fn _sub_dir(&self, path: &CStr, flags: libc::c_int) -> io::Result { + Ok(Dir(unsafe { + libc_ok(libc::openat(self.0, path.as_ptr(), flags | O_DIRECTORY))? + })) } /// Read link in this directory @@ -190,7 +259,7 @@ impl Dir { /// Currently, we recommend to fallback on any error if this operation /// can't be accomplished rather than relying on specific error codes, /// because semantics of errors are very ugly. - #[cfg(target_os="linux")] + #[cfg(feature = "o_tmpfile")] pub fn new_unnamed_file(&self, mode: libc::mode_t) -> io::Result { @@ -219,7 +288,7 @@ impl Dir { /// Currently, we recommend to fallback on any error if this operation /// can't be accomplished rather than relying on specific error codes, /// because semantics of errors are very ugly. - #[cfg(not(target_os="linux"))] + #[cfg(not(feature = "o_tmpfile"))] pub fn new_unnamed_file(&self, _mode: libc::mode_t) -> io::Result { @@ -238,7 +307,7 @@ impl Dir { /// fails. But in obscure scenarios where `/proc` is not mounted this /// method may fail even on linux. So your code should be able to fallback /// to a named file if this method fails too. - #[cfg(target_os="linux")] + #[cfg(feature = "link_file_at")] pub fn link_file_at(&self, file: &F, path: P) -> io::Result<()> { @@ -259,7 +328,10 @@ impl Dir { /// fails. But in obscure scenarios where `/proc` is not mounted this /// method may fail even on linux. So your code should be able to fallback /// to a named file if this method fails too. - #[cfg(not(target_os="linux"))] + //NOTE(cehteh): would it make sense to remove this function (for non linux), this will + // generate a compile time error rather than a runtime error, which most likely is + // favorable since the semantic cant easily emulated. + #[cfg(not(feature = "link_file_at"))] pub fn link_file_at(&self, _file: F, _path: P) -> io::Result<()> { @@ -295,7 +367,7 @@ impl Dir { mode) } - fn _open_file(&self, path: &CStr, flags: libc::c_int, mode: libc::mode_t) + pub(crate) fn _open_file(&self, path: &CStr, flags: libc::c_int, mode: libc::mode_t) -> io::Result { unsafe { @@ -304,14 +376,12 @@ impl Dir { // variadic in the signature. Since integers are not implicitly // promoted as they are in C this would break on Freebsd where // *mode_t* is an alias for `uint16_t`. - let res = libc::openat(self.0, path.as_ptr(), - flags|libc::O_CLOEXEC|libc::O_NOFOLLOW, - mode as libc::c_uint); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(File::from_raw_fd(res)) - } + let res = libc_ok( + libc::openat(self.0, path.as_ptr(), + flags|libc::O_CLOEXEC|libc::O_NOFOLLOW, + mode as libc::c_uint) + )?; + Ok(File::from_raw_fd(res)) } } @@ -343,13 +413,9 @@ impl Dir { } fn _create_dir(&self, path: &CStr, mode: libc::mode_t) -> io::Result<()> { unsafe { - let res = libc::mkdirat(self.0, path.as_ptr(), mode); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } + libc_ok(libc::mkdirat(self.0, path.as_ptr(), mode))?; } + Ok(()) } /// Rename a file in this directory to another name (keeping same dir) @@ -362,7 +428,7 @@ impl Dir { /// Similar to `local_rename` but atomically swaps both paths /// /// Only supported on Linux. - #[cfg(target_os="linux")] + #[cfg(feature = "rename_exchange")] pub fn local_exchange(&self, old: P, new: R) -> io::Result<()> { @@ -392,19 +458,16 @@ impl Dir { } fn _unlink(&self, path: &CStr, flags: libc::c_int) -> io::Result<()> { unsafe { - let res = libc::unlinkat(self.0, path.as_ptr(), flags); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } + libc_ok(libc::unlinkat(self.0, path.as_ptr(), flags))?; } + Ok(()) } /// Get the path of this directory (if possible) /// /// This uses symlinks in `/proc/self`, they sometimes may not be /// available so use with care. + #[cfg(feature = "proc_self_fd")] pub fn recover_path(&self) -> io::Result { let fd = self.0; if fd != libc::AT_FDCWD { @@ -426,27 +489,18 @@ impl Dir { } fn _stat(&self, path: &CStr, flags: libc::c_int) -> io::Result { unsafe { - let mut stat = mem::zeroed(); - let res = libc::fstatat(self.0, path.as_ptr(), - &mut stat, flags); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(metadata::new(stat)) - } + let mut stat = mem::zeroed(); // TODO(cehteh): uninit + libc_ok(libc::fstatat(self.0, path.as_ptr(), &mut stat, flags))?; + Ok(metadata::new(stat)) } } /// Returns the metadata of the directory itself. pub fn self_metadata(&self) -> io::Result { unsafe { - let mut stat = mem::zeroed(); - let res = libc::fstat(self.0, &mut stat); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(metadata::new(stat)) - } + let mut stat = mem::zeroed(); // TODO(cehteh): uninit + libc_ok(libc::fstat(self.0, &mut stat))?; + Ok(metadata::new(stat)) } } @@ -457,26 +511,133 @@ impl Dir { /// descriptor. The returned `Dir` will take responsibility for /// closing it when it goes out of scope. pub unsafe fn from_raw_fd_checked(fd: RawFd) -> io::Result { - let mut stat = mem::zeroed(); - let res = libc::fstat(fd, &mut stat); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - match stat.st_mode & libc::S_IFMT { - libc::S_IFDIR => Ok(Dir(fd)), - _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)) - } + match fd_type(fd)? { + FdType::NormalDir | FdType::OPathDir => Ok(Dir(fd)), + FdType::Other => Err(io::Error::from_raw_os_error(libc::ENOTDIR)), } } /// Creates a new independently owned handle to the underlying directory. + /// The new handle has the same (Normal/O_PATH) semantics as the original handle. pub fn try_clone(&self) -> io::Result { - let fd = unsafe { libc::dup(self.0) }; - if fd == -1 { - Err(io::Error::last_os_error()) + Ok(Dir(clone_dirfd(self.0)?)) + } + + /// Creates a new 'Normal' independently owned handle to the underlying directory. + pub fn clone_upgrade(&self) -> io::Result { + Ok(Dir(clone_dirfd_upgrade(self.0, 0)?)) + } + + /// Creates a new 'O_PATH' restricted independently owned handle to the underlying directory. + pub fn clone_downgrade(&self) -> io::Result { + Ok(Dir(clone_dirfd_downgrade(self.0)?)) + } +} + +const CURRENT_DIRECTORY: [libc::c_char; 2] = [b'.' as libc::c_char, 0]; + +//TODO(cehteh): eventually the clone calls should replicate O_SEARCH, maybe other file flags +fn clone_dirfd(fd: libc::c_int) -> io::Result { + unsafe { + match fd_type(fd)? { + FdType::NormalDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + O_DIRECTORY | libc::O_CLOEXEC, + )), + #[cfg(feature = "o_path")] + FdType::OPathDir => libc_ok(libc::dup(fd)), + _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)), + } + } +} + +pub(crate) fn clone_dirfd_upgrade(fd: libc::c_int, flags: libc::c_int) -> io::Result { + unsafe { + match fd_type(fd)? { + FdType::NormalDir | FdType::OPathDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + flags | O_DIRECTORY | libc::O_CLOEXEC, + )), + _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)), + } + } +} + +fn clone_dirfd_downgrade(fd: libc::c_int) -> io::Result { + unsafe { + match fd_type(fd)? { + #[cfg(feature = "o_path")] + FdType::NormalDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + libc::O_PATH | O_DIRECTORY | libc::O_CLOEXEC, + )), + #[cfg(not(feature = "o_path"))] + FdType::NormalDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + O_DIRECTORY | libc::O_CLOEXEC, + )), + #[cfg(feature = "o_path")] + FdType::OPathDir => libc_ok(libc::dup(fd)), + _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)), + } + } +} + +enum FdType { + NormalDir, + OPathDir, + Other, +} + +// OSes with fcntl() that returns O_DIRECTORY +// Linux has O_PATH +#[cfg(all(feature = "o_path", feature = "fcntl_o_directory"))] +fn fd_type(fd: libc::c_int) -> io::Result { + let flags = unsafe { libc_ok(libc::fcntl(fd, libc::F_GETFL))? }; + if flags & libc::O_DIRECTORY != 0 { + if flags & libc::O_PATH != 0 { + Ok(FdType::OPathDir) } else { - unsafe { Self::from_raw_fd_checked(fd) } + Ok(FdType::NormalDir) } + } else { + Ok(FdType::Other) + } +} + +#[cfg(all(not(feature = "o_path"), feature = "fcntl_o_directory"))] +fn fd_type(fd: libc::c_int) -> io::Result { + let flags = unsafe { libc_ok(libc::fcntl(fd, libc::F_GETFL))? }; + if flags & libc::O_DIRECTORY != 0 { + Ok(FdType::NormalDir) + } else { + Ok(FdType::Other) + } +} + +// OSes where fcntl won't return O_DIRECTORY use stat() +#[cfg(not(feature = "fcntl_o_directory"))] +fn fd_type(fd: libc::c_int) -> io::Result { + unsafe { + let mut stat = mem::zeroed(); // TODO(cehteh): uninit + libc_ok(libc::fstat(fd, &mut stat))?; + match stat.st_mode & libc::S_IFMT { + libc::S_IFDIR => Ok(FdType::NormalDir), + _ => Ok(FdType::Other), + } + } +} + +#[inline] +fn libc_ok(ret: libc::c_int) -> io::Result { + if ret != -1 { + Ok(ret) + } else { + Err(io::Error::last_os_error()) } } @@ -491,18 +652,11 @@ pub fn rename(old_dir: &Dir, old: P, new_dir: &Dir, new: R) _rename(old_dir, to_cstr(old)?.as_ref(), new_dir, to_cstr(new)?.as_ref()) } -fn _rename(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr) - -> io::Result<()> -{ +fn _rename(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr) -> io::Result<()> { unsafe { - let res = libc::renameat(old_dir.0, old.as_ptr(), - new_dir.0, new.as_ptr()); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } + libc_ok(libc::renameat(old_dir.0, old.as_ptr(), new_dir.0, new.as_ptr()))?; } + Ok(()) } /// Create a hardlink to a file @@ -522,19 +676,17 @@ pub fn hardlink(old_dir: &Dir, old: P, new_dir: &Dir, new: R) 0) } -fn _hardlink(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr, - flags: libc::c_int) - -> io::Result<()> -{ +fn _hardlink( + old_dir: &Dir, + old: &CStr, + new_dir: &Dir, + new: &CStr, + flags: libc::c_int, +) -> io::Result<()> { unsafe { - let res = libc::linkat(old_dir.0, old.as_ptr(), - new_dir.0, new.as_ptr(), flags); - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } + libc_ok(libc::linkat(old_dir.0, old.as_ptr(), new_dir.0, new.as_ptr(), flags))?; } + Ok(()) } /// Rename (move) a file between directories with flags @@ -543,7 +695,7 @@ fn _hardlink(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr, /// fallback to copying if needed. /// /// Only supported on Linux. -#[cfg(target_os="linux")] +#[cfg(feature = "renameat_flags")] pub fn rename_flags(old_dir: &Dir, old: P, new_dir: &Dir, new: R, flags: libc::c_int) -> io::Result<()> @@ -554,7 +706,7 @@ pub fn rename_flags(old_dir: &Dir, old: P, new_dir: &Dir, new: R, flags) } -#[cfg(target_os="linux")] +#[cfg(feature = "renameat_flags")] fn _rename_flags(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr, flags: libc::c_int) -> io::Result<()> @@ -608,7 +760,7 @@ impl Drop for Dir { } } -fn to_cstr(path: P) -> io::Result { +pub(crate) fn to_cstr(path: P) -> io::Result { path.to_path() .ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, @@ -628,12 +780,6 @@ mod test { assert!(Dir::open("src").is_ok()); } - #[test] - #[cfg_attr(target_os="freebsd", should_panic(expected="Not a directory"))] - fn test_open_file() { - Dir::open("src/lib.rs").unwrap(); - } - #[test] fn test_read_file() { let dir = Dir::open("src").unwrap(); @@ -661,13 +807,64 @@ mod test { #[test] fn test_list() { + let dir = Dir::flags().open("src").unwrap(); + let me = dir.list().unwrap(); + assert!(me + .collect::, _>>() + .unwrap() + .iter() + .find(|x| { x.file_name() == Path::new("lib.rs").as_os_str() }) + .is_some()); + } + + #[test] + #[cfg(feature = "o_path")] + #[should_panic(expected = "Bad file descriptor")] + fn test_list_opath_fail() { + let dir = Dir::open("src").unwrap(); + let me = dir.list().unwrap(); + assert!(me + .collect::, _>>() + .unwrap() + .iter() + .find(|x| { x.file_name() == Path::new("lib.rs").as_os_str() }) + .is_some()); + } + + #[test] + fn test_list_self() { + let dir = Dir::open("src").unwrap(); + let me = dir.list_self().unwrap(); + assert!(me + .collect::, _>>() + .unwrap() + .iter() + .find(|x| { x.file_name() == Path::new("lib.rs").as_os_str() }) + .is_some()); + } + + #[test] + fn test_list_dot() { let dir = Dir::open("src").unwrap(); let me = dir.list_dir(".").unwrap(); - assert!(me.collect::, _>>().unwrap() - .iter().find(|x| { - x.file_name() == Path::new("lib.rs").as_os_str() - }) - .is_some()); + assert!(me + .collect::, _>>() + .unwrap() + .iter() + .find(|x| { x.file_name() == Path::new("lib.rs").as_os_str() }) + .is_some()); + } + + #[test] + fn test_list_dir() { + let dir = Dir::open(".").unwrap(); + let me = dir.list_dir("src").unwrap(); + assert!(me + .collect::, _>>() + .unwrap() + .iter() + .find(|x| { x.file_name() == Path::new("lib.rs").as_os_str() }) + .is_some()); } #[test] diff --git a/src/filetype.rs b/src/filetype.rs index efaedbe..4ee98e4 100644 --- a/src/filetype.rs +++ b/src/filetype.rs @@ -7,6 +7,8 @@ use std::fs::Metadata; /// this simplified enum that works for many appalications. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SimpleType { + /// Entry is of unknown type + Unknown, /// Entry is a symlink Symlink, /// Entry is a directory diff --git a/src/lib.rs b/src/lib.rs index a1a2feb..89fdfbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,12 +51,14 @@ mod list; mod name; mod filetype; mod metadata; +mod builder; pub use crate::list::DirIter; pub use crate::name::AsPath; -pub use crate::dir::{rename, hardlink}; +pub use crate::dir::{hardlink, rename, O_DIRECTORY, O_PATH, O_SEARCH}; pub use crate::filetype::SimpleType; pub use crate::metadata::Metadata; +pub use crate::builder::{DirFlags, DirMethodFlags}; use std::ffi::CString; use std::os::unix::io::RawFd; @@ -73,6 +75,7 @@ pub struct Dir(RawFd); pub struct Entry { name: CString, file_type: Option, + ino: libc::ino_t, } #[cfg(test)] diff --git a/src/list.rs b/src/list.rs index 5b4d3cd..20c2561 100644 --- a/src/list.rs +++ b/src/list.rs @@ -5,8 +5,7 @@ use std::os::unix::ffi::OsStrExt; use libc; -use crate::{Dir, Entry, SimpleType}; - +use crate::{Entry, SimpleType}; // We have such weird constants because C types are ugly const DOT: [libc::c_char; 2] = [b'.' as libc::c_char, 0]; @@ -37,6 +36,10 @@ impl Entry { pub fn simple_type(&self) -> Option { self.file_type } + /// Returns the inode number of this entry + pub fn inode(&self) -> libc::ino_t { + self.ino + } } #[cfg(any(target_os="linux", target_os="fuchsia"))] @@ -104,17 +107,6 @@ pub fn open_dirfd(fd: libc::c_int) -> io::Result { } } -pub fn open_dir(dir: &Dir, path: &CStr) -> io::Result { - let dir_fd = unsafe { - libc::openat(dir.0, path.as_ptr(), libc::O_DIRECTORY|libc::O_CLOEXEC) - }; - if dir_fd < 0 { - Err(io::Error::last_os_error()) - } else { - open_dirfd(dir_fd) - } -} - impl Iterator for DirIter { type Item = io::Result; fn next(&mut self) -> Option { @@ -136,6 +128,7 @@ impl Iterator for DirIter { libc::DT_LNK => Some(SimpleType::Symlink), _ => Some(SimpleType::Other), }, + ino: e.d_ino, })); } } diff --git a/src/metadata.rs b/src/metadata.rs index de7cc22..7b5efb5 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -2,6 +2,7 @@ use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; use libc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::SimpleType; @@ -17,11 +18,11 @@ pub struct Metadata { impl Metadata { /// Returns simplified type of the directory entry pub fn simple_type(&self) -> SimpleType { - let typ = self.stat.st_mode & libc::S_IFMT; - match typ { + match self.file_type().unwrap_or(0) as libc::mode_t { libc::S_IFREG => SimpleType::File, libc::S_IFDIR => SimpleType::Dir, libc::S_IFLNK => SimpleType::Symlink, + 0 => SimpleType::Unknown, _ => SimpleType::Other, } } @@ -45,12 +46,112 @@ impl Metadata { pub fn len(&self) -> u64 { self.stat.st_size as u64 } + /// Return low level file type, if available + pub fn file_type(&self) -> Option { + Some(self.stat.st_mode & libc::S_IFMT) + } + /// Return device node, if available + pub fn ino(&self) -> Option { + Some(self.stat.st_ino) + } + /// Return device node major of the file, if available + pub fn dev_major(&self) -> Option { + Some(major(self.stat.st_dev)) + } + /// Return device node minor of the file, if available + pub fn dev_minor(&self) -> Option { + Some(minor(self.stat.st_dev)) + } + /// Return device node major of an device descriptor, if available + pub fn rdev_major(&self) -> Option { + match self.file_type()? as libc::mode_t { + libc::S_IFBLK | libc::S_IFCHR => Some(major(self.stat.st_rdev)), + _ => None, + } + } + /// Return device node minor of an device descriptor, if available + pub fn rdev_minor(&self) -> Option { + match self.file_type()? as libc::mode_t { + libc::S_IFBLK | libc::S_IFCHR => Some(minor(self.stat.st_rdev)), + _ => None, + } + } + /// Return preferered I/O Blocksize, if available + pub fn blksize(&self) -> Option { + Some(self.stat.st_blksize) + } + /// Return the number of 512 bytes blocks, if available + pub fn blocks(&self) -> Option { + Some(self.stat.st_blocks) + } + /// Returns file size (same as len() but Option), if available + pub fn size(&self) -> Option { + Some(self.stat.st_size) + } + /// Returns number of hard-links, if available + pub fn nlink(&self) -> Option { + Some(self.stat.st_nlink) + } + /// Returns user id, if available + pub fn uid(&self) -> Option { + Some(self.stat.st_uid) + } + /// Returns group id, if available + pub fn gid(&self) -> Option { + Some(self.stat.st_gid) + } + /// Returns mode bits, if available + pub fn file_mode(&self) -> Option { + Some(self.stat.st_mode & 0o7777) + } + /// Returns last access time, if available + pub fn atime(&self) -> Option { + Some(unix_systemtime(self.stat.st_atime, self.stat.st_atime_nsec)) + } + /// Returns creation, if available + pub fn btime(&self) -> Option { + None + } + /// Returns last status change time, if available + pub fn ctime(&self) -> Option { + Some(unix_systemtime(self.stat.st_ctime, self.stat.st_ctime_nsec)) + } + /// Returns last modification time, if available + pub fn mtime(&self) -> Option { + Some(unix_systemtime(self.stat.st_mtime, self.stat.st_mtime_nsec)) + } } pub fn new(stat: libc::stat) -> Metadata { Metadata { stat: stat } } +fn unix_systemtime(sec: libc::time_t, nsec: i64) -> SystemTime { + UNIX_EPOCH + Duration::from_secs(sec as u64) + Duration::from_nanos(nsec as u64) +} + +#[cfg(not(target_os = "macos"))] +pub fn major(dev: libc::dev_t) -> u32 { + unsafe { libc::major(dev) } +} + +#[cfg(not(target_os = "macos"))] +pub fn minor(dev: libc::dev_t) -> u32 { + unsafe { libc::minor(dev) } +} + +// major/minor are not in rust's darwin libc (why) +// see https://github.com/apple/darwin-xnu/blob/main/bsd/sys/types.h +#[cfg(target_os = "macos")] +pub fn major(dev: libc::dev_t) -> u32 { + (dev as u32 >> 24) & 0xff +} + +#[cfg(target_os = "macos")] +pub fn minor(dev: libc::dev_t) -> u32 { + dev as u32 & 0xffffff +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/flagsbuilder.rs b/tests/flagsbuilder.rs new file mode 100644 index 0000000..4ca0b50 --- /dev/null +++ b/tests/flagsbuilder.rs @@ -0,0 +1,51 @@ +extern crate openat; +use openat::{Dir}; + +#[test] +fn dir_flags_builder_basic() { + let dir = Dir::flags() + .without(libc::O_CLOEXEC) + .with(libc::O_NOFOLLOW) + .open("src"); + + assert!(dir.is_ok()); +} + +#[test] +fn dir_flags_builder_reuse() { + let dir_flags = Dir::flags().without(libc::O_CLOEXEC).with(libc::O_NOFOLLOW); + + let src_dir = dir_flags.open("src"); + let tests_dir = dir_flags.open("tests"); + + assert!(src_dir.is_ok()); + assert!(tests_dir.is_ok()); +} + +#[test] +fn method_flags_builder_basic() { + let dir = Dir::open("src").unwrap(); + let file = dir.without(libc::O_NOFOLLOW).open_file("dir.rs"); + assert!(file.is_ok()); +} + +#[test] +fn method_flags_builder_reuse() { + let dir = Dir::open("src").unwrap(); + let dir_flags = dir.without(libc::O_NOFOLLOW); + + let file1 = dir_flags.open_file("dir.rs"); + let file2 = dir_flags.open_file("builder.rs"); + + assert!(file1.is_ok()); + assert!(file2.is_ok()); +} + +#[test] +fn method_flags_exported() { + let dir = Dir::flags() + .with(openat::O_PATH) + .open("src"); + + assert!(dir.is_ok()); +} diff --git a/tests/tmpfile.rs b/tests/tmpfile.rs index 4fa0f0d..73e8c36 100644 --- a/tests/tmpfile.rs +++ b/tests/tmpfile.rs @@ -6,7 +6,7 @@ use std::os::unix::fs::PermissionsExt; use openat::Dir; #[test] -#[cfg(target_os="linux")] +#[cfg(all(feature = "o_tmpfile", feature = "link_file_at"))] fn unnamed_tmp_file_link() -> Result<(), io::Error> { let tmp = tempfile::tempdir()?; let dir = Dir::open(tmp.path())?;