diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..165e390 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" # Location of package manifests + schedule: + interval: "daily" + labels: + - "improvement" + assignees: + - "cehteh" + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..5d110a9 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,24 @@ +name: Rust + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Clippy + run: cargo clippy --verbose diff --git a/.gitignore b/.gitignore index afef0a7..a9272b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Cargo.lock /target /tmp +.tests-* diff --git a/Cargo.toml b/Cargo.toml index a5b5b3f..71d34c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,42 @@ [package] -name = "openat" +name = "openat_ct" description = """ A wrapper around openat, symlinkat, and similar system calls + forked and improved from https://crates.io/crates/openat """ license = "MIT/Apache-2.0" readme = "README.md" keywords = ["open", "openat", "filesystem", "fs"] categories = ["filesystem", "api-bindings"] -repository = "https://github.com/tailhook/openat" -homepage = "https://github.com/tailhook/openat" -documentation = "http://docs.rs/openat" -version = "0.1.21" -authors = ["paul@colomiets.name"] -edition = "2018" +repository = "https://github.com/cehteh/openat" +homepage = "https://github.com/cehteh/openat" +documentation = "http://docs.rs/openat_ct" +version = "0.2.0-pre10" +authors = ["paul@colomiets.name, ct@pipapo.org"] +edition = "2021" + +[features] +default = [] +linux = ["o_path", "o_directory", "o_tmpfile", "statx", "proc_self_fd", "link_file_at", "rename_exchange", "renameat_flags", "fcntl_f_dupfd_cloexec"] +#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 = [] +fcntl_f_dupfd_cloexec = [] +proc_self_fd = [] +link_file_at = [] +renameat_flags = [] +rename_exchange = [] +statx = [] [dependencies] -libc = "0.2.34" +libc = "0.2" + +[build-dependencies] +conf_test = "0.3.0" [dev-dependencies] -argparse = "0.2.1" -tempfile = "3.0.3" +argparse = "0.2" +tempfile = "3.0" diff --git a/README.md b/README.md index bf82045..c51b783 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,30 @@ -Openat Crate -============ +Openat (ct) Crate +================= **Status: Beta** -[Documentation](https://docs.rs/openat) | -[Github](https://github.com/tailhook/openat) | -[Crate](https://crates.io/crates/openat) +[Documentation](https://docs.rs/openat_ct) | +[Github](https://github.com/cehteh/openat) | +[Crate](https://crates.io/crates/openat_ct) The interface to ``openat``, ``symlinkat``, and other functions in ``*at`` family. +About this Fork +=============== + +This is a fork of the original 'openat' at https://github.com/tailhook/openat + +The objective is to fix existing Issues and add features as required. An eventual goal would +be to get this merged back into upstream. In most cases the API should stay backward +compatible with the original openat (Unless functionality and fixes dictate changes). + +It can be used as drop in replacement by aliasing the import: + + use openat_ct as openat; + + Dependent crates ================ diff --git a/benches/count_processes.rs b/benches/count_processes.rs index aa9f856..1b4fc4f 100644 --- a/benches/count_processes.rs +++ b/benches/count_processes.rs @@ -3,37 +3,51 @@ extern crate openat; extern crate test; - use std::fs::read_dir; -use std::str::from_utf8; use std::os::unix::ffi::OsStrExt; -use test::Bencher; +use std::str::from_utf8; +use test::Bencher; use openat::Dir; - #[bench] fn procs_stdlib(b: &mut Bencher) { b.iter(|| { - read_dir("/proc").unwrap().filter(|r| { - r.as_ref().ok() - .and_then(|e| from_utf8(e.file_name().as_bytes()).ok() - // pid is everything that can be parsed as a number - .and_then(|s| s.parse::().ok())) - .is_some() - }).count() + read_dir("/proc") + .unwrap() + .filter(|r| { + r.as_ref() + .ok() + .and_then(|e| { + from_utf8(e.file_name().as_bytes()) + .ok() + // pid is everything that can be parsed as a number + .and_then(|s| s.parse::().ok()) + }) + .is_some() + }) + .count() }); } #[bench] fn procs_openat(b: &mut Bencher) { b.iter(|| { - Dir::open("/proc").unwrap().list_dir(".").unwrap().filter(|r| { - r.as_ref().ok() - .and_then(|e| from_utf8(e.file_name().as_bytes()).ok() - // pid is everything that can be parsed as a number - .and_then(|s| s.parse::().ok())) - .is_some() - }).count() + Dir::open("/proc") + .unwrap() + .list_dir(".") + .unwrap() + .filter(|r| { + r.as_ref() + .ok() + .and_then(|e| { + from_utf8(e.file_name().as_bytes()) + .ok() + // pid is everything that can be parsed as a number + .and_then(|s| s.parse::().ok()) + }) + .is_some() + }) + .count() }); } 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_f_dupfd_cloexec.rs b/conf_tests/fcntl_f_dupfd_cloexec.rs new file mode 100644 index 0000000..7adc81b --- /dev/null +++ b/conf_tests/fcntl_f_dupfd_cloexec.rs @@ -0,0 +1,10 @@ +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 dup = libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0); + } +} 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..317acaf 100644 --- a/examples/exchange.rs +++ b/examples/exchange.rs @@ -1,18 +1,16 @@ -extern crate argparse; -extern crate openat; - -use std::process::exit; use std::path::PathBuf; +use std::process::exit; +use openat_ct as openat; use argparse::{ArgumentParser, Parse}; use openat::Dir; -#[cfg(not(target_os="linux"))] +#[cfg(not(target_os = "linux"))] fn main() { println!("Atomic exchange is not supported on this platform") } -#[cfg(target_os="linux")] +#[cfg(target_os = "linux")] fn main() { let mut path1 = PathBuf::new(); let mut path2 = PathBuf::new(); @@ -32,8 +30,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/rust.yml b/rust.yml new file mode 100644 index 0000000..3c13d1b --- /dev/null +++ b/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..eada66d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,17 @@ +version = "Two" +#max_width = 120 +#comment_width = 120 +enum_discrim_align_threshold = 40 +struct_field_align_threshold = 40 + +#license_template_path +format_macro_matchers = true +normalize_comments = true +overflow_delimited_expr = true +reorder_impl_items = true +reorder_imports = false +group_imports = "StdExternalCrate" +use_field_init_shorthand = true + +use_try_shorthand = true +normalize_doc_attributes = true diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..f7273c6 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,147 @@ +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 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::new(clone_dirfd_upgrade( + self.object.rawfd(), + self.flags, + )?)) + } +} diff --git a/src/dir.rs b/src/dir.rs index 3bed451..20b630b 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -1,52 +1,94 @@ +use std::ffi::{CStr, OsString}; +use std::fs::{read_link, File}; use std::io; use std::mem; -use std::ffi::{OsString, CStr}; -use std::fs::{File, read_link}; -use std::os::unix::io::{AsRawFd, RawFd, FromRawFd, IntoRawFd}; -use std::os::unix::ffi::{OsStringExt}; -use std::path::{PathBuf}; +use std::os::unix::ffi::OsStringExt; +use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; +use std::path::{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::{AsPath, DirFlags, DirMethodFlags, SimpleType}; -use crate::{Dir, AsPath}; +/// 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; -#[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_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; + +/// A safe wrapper around directory file descriptor +/// +/// Construct it either with ``Dir::cwd()`` or ``Dir::open(path)`` +#[derive(Debug)] +pub struct Dir(RawFd); impl Dir { - /// Creates a directory descriptor that resolves paths relative to current - /// working directory (AT_FDCWD) - #[deprecated(since="0.1.15", note="\ - Use `Dir::open(\".\")` instead. \ - Dir::cwd() doesn't open actual file descriptor and uses magic value \ - instead which resolves to current dir on any syscall invocation. \ - This is usually counter-intuitive and yields a broken \ - file descriptor when using `Dir::as_raw_fd`. \ - Will be removed in version v0.2 of the library.")] - pub fn cwd() -> Dir { - Dir(libc::AT_FDCWD) - } - - /// Open a directory descriptor at specified path + /// low level ctor, only used by builder and tests + pub(crate) fn new(fd: RawFd) -> Self { + Dir(fd) + } + + /// Gets the underlying RawFd from a Dir handle. + pub(crate) fn rawfd(&self) -> RawFd { + self.0 + } + + /// 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) + } + + /// 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()) + Dir::_open(to_cstr(path)?.as_ref(), O_PATH | 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)) + 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::new(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(&self, flags: libc::c_int) -> DirMethodFlags { + DirMethodFlags::new(self, libc::O_CLOEXEC | libc::O_NOFOLLOW | flags) + } + + /// 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(&self, flags: libc::c_int) -> DirMethodFlags { + DirMethodFlags::new(self, (libc::O_CLOEXEC | libc::O_NOFOLLOW) & !flags) } - /// Open subdirectory + /// 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::new(unsafe { + libc_ok(libc::openat(self.0, path.as_ptr(), flags | O_DIRECTORY))? + })) } /// Read link in this directory @@ -95,9 +164,12 @@ impl Dir { fn _read_link(&self, path: &CStr) -> io::Result { let mut buf = vec![0u8; 4096]; let res = unsafe { - libc::readlinkat(self.0, - path.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_char, buf.len()) + libc::readlinkat( + self.0, + path.as_ptr(), + buf.as_mut_ptr() as *mut libc::c_char, + buf.len(), + ) }; if res < 0 { Err(io::Error::last_os_error()) @@ -114,8 +186,7 @@ impl Dir { /// /// [`read_link`]: #method.read_link pub fn open_file(&self, path: P) -> io::Result { - self._open_file(to_cstr(path)?.as_ref(), - libc::O_RDONLY, 0) + self._open_file(to_cstr(path)?.as_ref(), libc::O_RDONLY, 0) } /// Open file for writing, create if necessary, truncate on open @@ -126,12 +197,12 @@ impl Dir { /// clobbering the symlink at the destination. /// /// [`new_unnamed_file`]: #method.new_unnamed_file - pub fn write_file(&self, path: P, mode: libc::mode_t) - -> io::Result - { - self._open_file(to_cstr(path)?.as_ref(), - libc::O_CREAT|libc::O_WRONLY|libc::O_TRUNC, - mode) + pub fn write_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self._open_file( + to_cstr(path)?.as_ref(), + libc::O_CREAT | libc::O_WRONLY | libc::O_TRUNC, + mode, + ) } /// Open file for append, create if necessary @@ -140,31 +211,12 @@ impl Dir { /// will need to call [`read_link`] to resolve the real path first. /// /// [`read_link`]: #method.read_link - pub fn append_file(&self, path: P, mode: libc::mode_t) - -> io::Result - { - self._open_file(to_cstr(path)?.as_ref(), - libc::O_CREAT|libc::O_WRONLY|libc::O_APPEND, - mode) - } - - /// Create file for writing (and truncate) in this directory - /// - /// Deprecated alias for `write_file` - /// - /// If there exists a symlink at the destination path, this method will fail. In that case, you - /// will need to remove the symlink before calling this method. If you are on Linux, you can - /// alternatively create an unnamed file with [`new_unnamed_file`] and then rename it, - /// clobbering the symlink at the destination. - /// - /// [`new_unnamed_file`]: #method.new_unnamed_file - #[deprecated(since="0.1.7", note="please use `write_file` instead")] - pub fn create_file(&self, path: P, mode: libc::mode_t) - -> io::Result - { - self._open_file(to_cstr(path)?.as_ref(), - libc::O_CREAT|libc::O_WRONLY|libc::O_TRUNC, - mode) + pub fn append_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self._open_file( + to_cstr(path)?.as_ref(), + libc::O_CREAT | libc::O_WRONLY | libc::O_APPEND, + mode, + ) } /// Create a tmpfile in this directory which isn't linked to any filename @@ -190,13 +242,13 @@ 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")] - pub fn new_unnamed_file(&self, mode: libc::mode_t) - -> io::Result - { - self._open_file(unsafe { CStr::from_bytes_with_nul_unchecked(b".\0") }, - libc::O_TMPFILE|libc::O_WRONLY, - mode) + #[cfg(feature = "o_tmpfile")] + pub fn new_unnamed_file(&self, mode: libc::mode_t) -> io::Result { + self._open_file( + unsafe { CStr::from_bytes_with_nul_unchecked(b".\0") }, + libc::O_TMPFILE | libc::O_WRONLY, + mode, + ) } /// Create a tmpfile in this directory which isn't linked to any filename @@ -219,12 +271,12 @@ 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"))] - pub fn new_unnamed_file(&self, _mode: libc::mode_t) - -> io::Result - { - Err(io::Error::new(io::ErrorKind::Other, - "creating unnamed tmpfiles is only supported on linux")) + #[cfg(not(feature = "o_tmpfile"))] + pub fn new_unnamed_file(&self, _mode: libc::mode_t) -> io::Result { + Err(io::Error::new( + io::ErrorKind::Other, + "creating unnamed tmpfiles is only supported on linux", + )) } /// Link open file to a specified path @@ -238,14 +290,16 @@ 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")] - pub fn link_file_at(&self, file: &F, path: P) - -> io::Result<()> - { + #[cfg(feature = "link_file_at")] + pub fn link_file_at(&self, file: &F, path: P) -> io::Result<()> { let fd_path = format!("/proc/self/fd/{}", file.as_raw_fd()); - _hardlink(&Dir(libc::AT_FDCWD), to_cstr(fd_path)?.as_ref(), - &self, to_cstr(path)?.as_ref(), - libc::AT_SYMLINK_FOLLOW) + _hardlink( + &Dir::new(libc::AT_FDCWD), + to_cstr(fd_path)?.as_ref(), + self, + to_cstr(path)?.as_ref(), + libc::AT_SYMLINK_FOLLOW, + ) } /// Link open file to a specified path @@ -259,12 +313,15 @@ 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"))] - pub fn link_file_at(&self, _file: F, _path: P) - -> io::Result<()> - { - Err(io::Error::new(io::ErrorKind::Other, - "linking unnamed fd to directories is only supported on 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<()> { + Err(io::Error::new( + io::ErrorKind::Other, + "linking unnamed fd to directories is only supported on linux", + )) } /// Create file if not exists, fail if exists @@ -273,12 +330,12 @@ impl Dir { /// respect to other threads and processes. /// /// Technically it means passing `O_EXCL` flag to open. - pub fn new_file(&self, path: P, mode: libc::mode_t) - -> io::Result - { - self._open_file(to_cstr(path)?.as_ref(), - libc::O_CREAT|libc::O_EXCL|libc::O_WRONLY, - mode) + pub fn new_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self._open_file( + to_cstr(path)?.as_ref(), + libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY, + mode, + ) } /// Open file for reading and writing without truncation, create if needed @@ -287,46 +344,42 @@ impl Dir { /// will need to call [`read_link`] to resolve the real path first. /// /// [`read_link`]: #method.read_link - pub fn update_file(&self, path: P, mode: libc::mode_t) - -> io::Result - { - self._open_file(to_cstr(path)?.as_ref(), - libc::O_CREAT|libc::O_RDWR, - mode) + pub fn update_file(&self, path: P, mode: libc::mode_t) -> io::Result { + self._open_file(to_cstr(path)?.as_ref(), libc::O_CREAT | libc::O_RDWR, mode) } - fn _open_file(&self, path: &CStr, flags: libc::c_int, mode: libc::mode_t) - -> io::Result - { + pub(crate) fn _open_file( + &self, + path: &CStr, + flags: libc::c_int, + mode: libc::mode_t, + ) -> io::Result { unsafe { // Note: In below call to `openat`, *mode* must be cast to // `unsigned` because the optional `mode` argument to `openat` is // 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)) } } /// Make a symlink in this directory /// /// Note: the order of arguments differ from `symlinkat` - pub fn symlink(&self, path: P, value: R) - -> io::Result<()> - { + pub fn symlink(&self, path: P, value: R) -> io::Result<()> { self._symlink(to_cstr(path)?.as_ref(), to_cstr(value)?.as_ref()) } + fn _symlink(&self, path: &CStr, link: &CStr) -> io::Result<()> { unsafe { - let res = libc::symlinkat(link.as_ptr(), - self.0, path.as_ptr()); + let res = libc::symlinkat(link.as_ptr(), self.0, path.as_ptr()); if res < 0 { Err(io::Error::last_os_error()) } else { @@ -336,77 +389,114 @@ impl Dir { } /// Create a subdirectory in this directory - pub fn create_dir(&self, path: P, mode: libc::mode_t) - -> io::Result<()> - { + pub fn create_dir(&self, path: P, mode: libc::mode_t) -> io::Result<()> { self._create_dir(to_cstr(path)?.as_ref(), mode) } + 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) - pub fn local_rename(&self, old: P, new: R) - -> io::Result<()> - { + pub fn local_rename(&self, old: P, new: R) -> io::Result<()> { rename(self, to_cstr(old)?.as_ref(), self, to_cstr(new)?.as_ref()) } /// Similar to `local_rename` but atomically swaps both paths /// /// Only supported on Linux. - #[cfg(target_os="linux")] - pub fn local_exchange(&self, old: P, new: R) - -> io::Result<()> - { + #[cfg(feature = "rename_exchange")] + pub fn local_exchange(&self, old: P, new: R) -> io::Result<()> { // Workaround https://github.com/tailhook/openat/issues/35 // AKA https://github.com/rust-lang/libc/pull/2116 // Unfortunately since we made this libc::c_int in our // public API, we can't easily change it right now. let flags = libc::RENAME_EXCHANGE as libc::c_int; - rename_flags(self, to_cstr(old)?.as_ref(), - self, to_cstr(new)?.as_ref(), - flags) + rename_flags( + self, + to_cstr(old)?.as_ref(), + self, + to_cstr(new)?.as_ref(), + flags, + ) } /// Remove a subdirectory in this directory /// /// Note only empty directory may be removed - pub fn remove_dir(&self, path: P) - -> io::Result<()> - { + pub fn remove_dir(&self, path: P) -> io::Result<()> { self._unlink(to_cstr(path)?.as_ref(), libc::AT_REMOVEDIR) } + /// Remove a file in this directory - pub fn remove_file(&self, path: P) - -> io::Result<()> - { + pub fn remove_file(&self, path: P) -> io::Result<()> { self._unlink(to_cstr(path)?.as_ref(), 0) } + 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(()) + } + + /// Removes a directory with all its contents + pub fn remove_recursive(&self, path: P) -> io::Result<()> { + self.list_dir(path)? + .try_for_each(|entry| -> io::Result<()> { + match entry { + Ok(entry) if entry.simple_type() == Some(SimpleType::Dir) => { + self.sub_dir(path)?.remove_recursive(entry.file_name()) + } + Ok(entry) => self.remove_file(entry.file_name()), + Err(err) => Err(err), + } + })?; + self.remove_dir(path) + } + + /// Removes a directory with all its contents in a atomic way. This is done by renaming + /// the 'path' to some unique name first. When tmp_dir is given as sub direcory of 'self' + /// on the same filesystem. the 'path' will be moved into that. When tmp_dir is "" then + /// the rename is done in place. The unique name is determined by changing the path + /// extension to an increasing number (.0 .1 .2 ... .65534) until the rename + /// succeeds. When renaming will not succeed this will eventually return an error. + pub fn remove_recursive_atomic(&self, path: P, tmp_dir: T) -> io::Result<()> + where + P: AsPath + Copy + std::convert::AsRef, + T: AsPath + std::convert::AsRef, + { + let mut to_delete = PathBuf::from(tmp_dir.as_ref()); + if to_delete.as_os_str().is_empty() { + // In place, full name + to_delete.push(path); + } else { + // use the tmp_dir and the file_name part + to_delete.push(Path::new(path.as_ref()).file_name().unwrap()); + } + + for i in 0u16..u16::MAX { + to_delete.set_extension(i.to_string()); + if self.local_rename(path, &to_delete).is_ok() { + return self.remove_recursive(&to_delete); } } + Err(io::Error::new( + io::ErrorKind::Other, + "Could not rename for remove", + )) } /// 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; + let fd = self.rawfd(); if fd != libc::AT_FDCWD { read_link(format!("/proc/self/fd/{}", fd)) } else { @@ -424,29 +514,21 @@ impl Dir { pub fn metadata(&self, path: P) -> io::Result { self._stat(to_cstr(path)?.as_ref(), libc::AT_SYMLINK_NOFOLLOW) } + 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)) } } @@ -456,27 +538,176 @@ impl Dir { /// This function **consumes ownership** of the specified file /// 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)) - } + pub fn from_raw_fd_checked(fd: RawFd) -> io::Result { + match fd_type(fd)? { + FdType::NormalDir | FdType::OPathDir => Ok(Dir::new(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::new(clone_dirfd(self.0)?)) + } + + /// Creates a new 'Normal' independently owned handle to the underlying directory. + pub fn clone_upgrade(&self) -> io::Result { + Ok(Dir::new(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::new(clone_dirfd_downgrade(self.0)?)) + } + + /// Flush recent changes to the contents of this directory to persistent storage. + /// + /// With some operating systems and/or file systems, you must do this after creating, + /// removing, or renaming files in a directory to ensure that your changes survive a + /// crash, _even if_ the operations you performed are documented as atomic (e.g. [`rename`]). + /// If you changed the contents of any files as well as renaming them, you must also + /// perform a [`sync_all`] on each modified file _before_ renaming it. + /// + /// The `Dir` object must have been opened with `O_SEARCH` or `O_RDONLY`, not `O_PATH`, + /// for this operation to succeed. Use `Dir::flags().open()` to create the `Dir` + /// appropriately, or use `clone_upgrade` to convert an existing `Dir`. + /// + /// [`rename`]: std::fs::rename + /// [`sync_all`]: std::fs::File::sync_all + pub fn sync(&self) -> io::Result<()> { + unsafe { + libc_ok(libc::fsync(self.0))?; + Ok(()) + } + } +} + +const CURRENT_DIRECTORY: [libc::c_char; 2] = [b'.' as libc::c_char, 0]; + +#[cfg(all(feature = "o_path", feature = "fcntl_f_dupfd_cloexec"))] +unsafe fn dup_cloexec(fd: libc::c_int) -> io::Result { + libc_ok(libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0)) +} + +#[cfg(all(feature = "o_path", not(feature = "fcntl_f_dupfd_cloexec")))] +unsafe fn dup_cloexec(fd: libc::c_int) -> io::Result { + libc_ok(libc::fcntl(fd, libc::F_DUPFD, 0))?; + libc_ok(libc::fcntl(fd, libc::F_SETFD, FD_CLOEXEC)) +} + +// 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, + )), + FdType::OPathDir => dup_cloexec(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, + )), + FdType::OPathDir => dup_cloexec(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: RawFd) -> io::Result { + if fd != -1 { + 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 { + Ok(FdType::NormalDir) + } + } else { + Ok(FdType::Other) + } + } else { + Err(io::Error::from_raw_os_error(libc::EBADF)) + } +} + +#[cfg(all(not(feature = "o_path"), feature = "fcntl_o_directory"))] +fn fd_type(fd: RawFd) -> io::Result { + if fd != -1 { + let flags = unsafe { libc_ok(libc::fcntl(fd, libc::F_GETFL))? }; + if flags & libc::O_DIRECTORY != 0 { + Ok(FdType::NormalDir) } else { - unsafe { Self::from_raw_fd_checked(fd) } + Ok(FdType::Other) } + } else { + Err(io::Error::from_raw_os_error(libc::EBADF)) + } +} + +// OSes where fcntl won't return O_DIRECTORY use stat() +#[cfg(not(feature = "fcntl_o_directory"))] +fn fd_type(fd: RawFd) -> io::Result { + if fd != -1 { + 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), + } + } + } else { + Err(io::Error::from_raw_os_error(libc::EBADF)) + } +} + +#[inline] +pub(crate) fn libc_ok(ret: libc::c_int) -> io::Result { + if ret != -1 { + Ok(ret) + } else { + Err(io::Error::last_os_error()) } } @@ -484,25 +715,29 @@ impl Dir { /// /// Files must be on a single filesystem anyway. This funtion does **not** /// fallback to copying if needed. -pub fn rename(old_dir: &Dir, old: P, new_dir: &Dir, new: R) - -> io::Result<()> - where P: AsPath, R: AsPath, +pub fn rename(old_dir: &Dir, old: P, new_dir: &Dir, new: R) -> io::Result<()> +where + P: AsPath, + R: AsPath, { - _rename(old_dir, to_cstr(old)?.as_ref(), new_dir, to_cstr(new)?.as_ref()) + _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.rawfd(), + old.as_ptr(), + new_dir.rawfd(), + new.as_ptr(), + ))?; } + Ok(()) } /// Create a hardlink to a file @@ -513,28 +748,37 @@ fn _rename(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr) /// Note: by default ``linkat`` syscall doesn't resolve symbolic links, and /// it's also behavior of this function. It's recommended to resolve symlinks /// manually if needed. -pub fn hardlink(old_dir: &Dir, old: P, new_dir: &Dir, new: R) - -> io::Result<()> - where P: AsPath, R: AsPath, +pub fn hardlink(old_dir: &Dir, old: P, new_dir: &Dir, new: R) -> io::Result<()> +where + P: AsPath, + R: AsPath, { - _hardlink(old_dir, to_cstr(old)?.as_ref(), - new_dir, to_cstr(new)?.as_ref(), - 0) + _hardlink( + old_dir, + to_cstr(old)?.as_ref(), + new_dir, + to_cstr(new)?.as_ref(), + 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.rawfd(), + old.as_ptr(), + new_dir.rawfd(), + new.as_ptr(), + flags, + ))?; } + Ok(()) } /// Rename (move) a file between directories with flags @@ -543,27 +787,44 @@ 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")] -pub fn rename_flags(old_dir: &Dir, old: P, new_dir: &Dir, new: R, - flags: libc::c_int) - -> io::Result<()> - where P: AsPath, R: AsPath, +#[cfg(feature = "renameat_flags")] +pub fn rename_flags( + old_dir: &Dir, + old: P, + new_dir: &Dir, + new: R, + flags: libc::c_int, +) -> io::Result<()> +where + P: AsPath, + R: AsPath, { - _rename_flags(old_dir, to_cstr(old)?.as_ref(), - new_dir, to_cstr(new)?.as_ref(), - flags) + _rename_flags( + old_dir, + to_cstr(old)?.as_ref(), + new_dir, + to_cstr(new)?.as_ref(), + flags, + ) } -#[cfg(target_os="linux")] -fn _rename_flags(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr, - flags: libc::c_int) - -> io::Result<()> -{ +#[cfg(feature = "renameat_flags")] +fn _rename_flags( + old_dir: &Dir, + old: &CStr, + new_dir: &Dir, + new: &CStr, + flags: libc::c_int, +) -> io::Result<()> { unsafe { let res = libc::syscall( libc::SYS_renameat2, - old_dir.0, old.as_ptr(), - new_dir.0, new.as_ptr(), flags); + old_dir.rawfd(), + old.as_ptr(), + new_dir.rawfd(), + new.as_ptr(), + flags, + ); if res < 0 { Err(io::Error::last_os_error()) } else { @@ -584,7 +845,7 @@ impl FromRawFd for Dir { /// a directory file descriptor. #[inline] unsafe fn from_raw_fd(fd: RawFd) -> Dir { - Dir(fd) + Dir::new(fd) } } @@ -593,53 +854,44 @@ impl IntoRawFd for Dir { fn into_raw_fd(self) -> RawFd { let result = self.0; mem::forget(self); - return result; + result } } impl Drop for Dir { fn drop(&mut self) { - let fd = self.0; - if fd != libc::AT_FDCWD { - unsafe { - libc::close(fd); - } + unsafe { + libc::close(self.0); } } } -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, - "nul byte in file name") - }) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "nul byte in file name")) } #[cfg(test)] mod test { - use std::io::{Read}; - use std::path::Path; + use std::io::Read; use std::os::unix::io::{FromRawFd, IntoRawFd}; - use crate::{Dir}; + use std::path::Path; + + use crate::Dir; #[test] fn test_open_ok() { 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(); let mut buf = String::new(); - dir.open_file("lib.rs").unwrap() - .read_to_string(&mut buf).unwrap(); + dir.open_file("lib.rs") + .unwrap() + .read_to_string(&mut buf) + .unwrap(); assert!(buf.find("extern crate libc;").is_some()); } @@ -648,36 +900,97 @@ mod test { let dir = Dir::open("src").unwrap(); let dir = unsafe { Dir::from_raw_fd(dir.into_raw_fd()) }; let mut buf = String::new(); - dir.open_file("lib.rs").unwrap() - .read_to_string(&mut buf).unwrap(); + dir.open_file("lib.rs") + .unwrap() + .read_to_string(&mut buf) + .unwrap(); assert!(buf.find("extern crate libc;").is_some()); } #[test] - #[should_panic(expected="No such file or directory")] + #[should_panic(expected = "No such file or directory")] fn test_open_no_dir() { Dir::open("src/some-non-existent-file").unwrap(); } #[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] fn test_from_raw_fd_checked() { let fd = Dir::open(".").unwrap().into_raw_fd(); - let dir = unsafe { Dir::from_raw_fd_checked(fd) }.unwrap(); + let dir = Dir::from_raw_fd_checked(fd).unwrap(); let filefd = dir.open_file("src/lib.rs").unwrap().into_raw_fd(); - match unsafe { Dir::from_raw_fd_checked(filefd) } { - Ok(_) => assert!(false, "from_raw_fd_checked succeeded on a non-directory fd!"), - Err(e) => assert_eq!(e.raw_os_error().unwrap(), libc::ENOTDIR) + match Dir::from_raw_fd_checked(filefd) { + Ok(_) => assert!( + false, + "from_raw_fd_checked succeeded on a non-directory fd!" + ), + Err(e) => assert_eq!(e.raw_os_error().unwrap(), libc::ENOTDIR), } } @@ -688,4 +1001,47 @@ mod test { drop(d); let _file = d2.open_file("src/lib.rs").unwrap(); } + + #[test] + fn test_remove_recursive() { + let d = Dir::open(".").unwrap(); + + d.create_dir("test_remove", 0o777).unwrap(); + d.create_dir("test_remove/foo", 0o777).unwrap(); + d.create_dir("test_remove/foo/bar", 0o777).unwrap(); + d.create_dir("test_remove/bar", 0o777).unwrap(); + + d.remove_recursive("test_remove").unwrap(); + } + + #[test] + fn test_remove_recursive_atomic_notmp() { + let d = Dir::open(".").unwrap(); + + d.create_dir("test_removeatomic", 0o777).unwrap(); + d.create_dir("test_removeatomic/foo", 0o777).unwrap(); + d.create_dir("test_removeatomic/foo/bar", 0o777).unwrap(); + d.create_dir("test_removeatomic/bar", 0o777).unwrap(); + + d.remove_recursive_atomic("test_removeatomic/foo", "") + .unwrap(); + d.remove_recursive_atomic("test_removeatomic", "").unwrap(); + } + + #[test] + fn test_remove_recursive_atomic_tmp() { + let d = Dir::open(".").unwrap(); + + d.create_dir("test_removetmp", 0o777).unwrap(); + d.create_dir("test_removeatomictmp", 0o777).unwrap(); + d.create_dir("test_removeatomictmp/foo", 0o777).unwrap(); + d.create_dir("test_removeatomictmp/foo/bar", 0o777).unwrap(); + d.create_dir("test_removeatomictmp/bar", 0o777).unwrap(); + + d.remove_recursive_atomic("test_removeatomictmp/foo", "test_removetmp") + .unwrap(); + d.remove_recursive_atomic("test_removeatomictmp", "test_removetmp") + .unwrap(); + d.remove_dir("test_removetmp").unwrap(); + } } 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..4a24a1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,9 +11,6 @@ //! also be unmounted or be out of chroot and you will still be able to //! access files relative to it.* //! -//! *Note2: The constructor `Dir::cwd()` is deprecated, and it's recommended -//! to use `Dir::open(".")` instead.* -//! //! *Note3: Some OS's (e.g., macOS) do not provide `O_PATH`, in which case the //! file descriptor is of regular type.* //! @@ -41,55 +38,43 @@ //! Also while all methods of dir accept any path if you want to prevent //! certain symlink attacks and race condition you should only use //! a single-component path. I.e. open one part of a chain at a time. -//! #![warn(missing_docs)] extern crate libc; +mod builder; mod dir; -mod list; -mod name; mod filetype; +mod list; mod metadata; +mod name; -pub use crate::list::DirIter; -pub use crate::name::AsPath; -pub use crate::dir::{rename, hardlink}; +pub use crate::builder::{DirFlags, DirMethodFlags}; +pub use crate::dir::{hardlink, rename, Dir, O_DIRECTORY, O_PATH, O_SEARCH}; pub use crate::filetype::SimpleType; -pub use crate::metadata::Metadata; - -use std::ffi::CString; -use std::os::unix::io::RawFd; - -/// A safe wrapper around directory file descriptor -/// -/// Construct it either with ``Dir::cwd()`` or ``Dir::open(path)`` -/// -#[derive(Debug)] -pub struct Dir(RawFd); - -/// Entry returned by iterating over `DirIter` iterator -#[derive(Debug)] -pub struct Entry { - name: CString, - file_type: Option, -} +pub use crate::list::{DirIter, Entry}; +pub use crate::metadata::{metadata_types, Metadata}; +pub use crate::name::AsPath; #[cfg(test)] mod test { use std::mem; + use super::Dir; - fn assert_sync(x: T) -> T { x } - fn assert_send(x: T) -> T { x } + fn assert_sync(x: T) -> T { + x + } + fn assert_send(x: T) -> T { + x + } #[test] fn test() { - let d = Dir(3); + let d = Dir::new(3); let d = assert_sync(d); let d = assert_send(d); // don't execute close for our fake RawFd mem::forget(d); } } - diff --git a/src/list.rs b/src/list.rs index 5b4d3cd..538c36f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,26 +1,36 @@ +use std::ffi::{CStr, CString, OsStr}; use std::io; -use std::ptr; -use std::ffi::{CStr, OsStr}; +use std::fmt; +use std::mem; use std::os::unix::ffi::OsStrExt; +use std::sync::Arc; -use libc; - -use crate::{Dir, Entry, SimpleType}; - +use crate::{dir::libc_ok, metadata, Metadata, SimpleType}; // We have such weird constants because C types are ugly const DOT: [libc::c_char; 2] = [b'.' as libc::c_char, 0]; const DOTDOT: [libc::c_char; 3] = [b'.' as libc::c_char, b'.' as libc::c_char, 0]; - /// Iterator over directory entries /// /// Created using `Dir::list_dir()` -#[derive(Debug)] pub struct DirIter { - dir: *mut libc::DIR, + // Needs Arc here to be shared with Entries, for metdata() + dir: Arc, } +// It may not be thread-safe to call readdir concurrently from multiple threads on a single +// `DIR*`, but all `Send` requires is that we can call it from different threads +// non-concurrently - so this is fine. +// +// `man readdir` says: +// +// > It is expected that a future version of POSIX.1 will require that readdir() be +// > thread-safe when concurrently employed on different directory streams. +// +// so in the future we may also be able to implement `Sync`. +unsafe impl Send for DirIter {} + /// Position in a DirIter as obtained by 'DirIter::current_position()' /// /// The position is only valid for the DirIter it was retrieved from. @@ -28,53 +38,96 @@ pub struct DirPosition { pos: libc::c_long, } +/// Entry returned by iterating over `DirIter` iterator +pub struct Entry { + dir: Arc, + pub(crate) name: CString, + file_type: Option, + ino: libc::ino_t, +} + impl Entry { /// Returns the file name of this entry pub fn file_name(&self) -> &OsStr { OsStr::from_bytes(self.name.to_bytes()) } + /// Returns the simplified type of this 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 + } + + /// Returns the metadata of this entry + pub fn metadata(&self) -> io::Result { + unsafe { + let mut stat = mem::zeroed(); // TODO(cehteh): uninit + libc_ok(libc::fstatat( + libc::dirfd(self.dir.raw()), + self.name.as_ptr(), + &mut stat, + libc::AT_SYMLINK_NOFOLLOW, + ))?; + Ok(metadata::new(stat)) + } + } } -#[cfg(any(target_os="linux", target_os="fuchsia"))] +impl fmt::Debug for Entry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Entry") + .field("dir", &Arc::as_ptr(&self.dir)) + .field("name", &self.name) + .field("file_type", &self.file_type) + .field("ino", &self.ino) + .finish() + } +} + +#[cfg(any(target_os = "linux", target_os = "fuchsia"))] unsafe fn errno_location() -> *mut libc::c_int { libc::__errno_location() } -#[cfg(any(target_os="openbsd", target_os="netbsd", target_os="android"))] +#[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "android"))] unsafe fn errno_location() -> *mut libc::c_int { libc::__errno() } -#[cfg(not(any(target_os="linux", target_os="openbsd", target_os="netbsd", target_os="android", target_os="fuchsia")))] +#[cfg(not(any( + target_os = "linux", + target_os = "openbsd", + target_os = "netbsd", + target_os = "android", + target_os = "fuchsia" +)))] unsafe fn errno_location() -> *mut libc::c_int { libc::__error() } impl DirIter { - - unsafe fn next_entry(&mut self) -> io::Result> - { + unsafe fn next_entry(&mut self) -> io::Result> { // Reset errno to detect if error occurred *errno_location() = 0; - let entry = libc::readdir(self.dir); - if entry == ptr::null_mut() { + let entry = libc::readdir(self.dir.raw()); + if entry.is_null() { if *errno_location() == 0 { - return Ok(None) + return Ok(None); } else { return Err(io::Error::last_os_error()); } } - return Ok(Some(&*entry)); + Ok(Some(&*entry)) } /// Returns the current directory iterator position. The result should be handled as opaque value pub fn current_position(&self) -> io::Result { - let pos = unsafe { libc::telldir(self.dir) }; + let pos = unsafe { libc::telldir(self.dir.raw()) }; if pos == -1 { Err(io::Error::last_os_error()) @@ -86,40 +139,33 @@ impl DirIter { // note the C-API does not report errors for seekdir/rewinddir, thus we don't do as well. /// Sets the current directory iterator position to some location queried by 'current_position()' pub fn seek(&self, position: DirPosition) { - unsafe { libc::seekdir(self.dir, position.pos) }; + unsafe { libc::seekdir(self.dir.raw(), position.pos) }; } /// Resets the current directory iterator position to the beginning pub fn rewind(&self) { - unsafe { libc::rewinddir(self.dir) }; + unsafe { libc::rewinddir(self.dir.raw()) }; } } pub fn open_dirfd(fd: libc::c_int) -> io::Result { let dir = unsafe { libc::fdopendir(fd) }; - if dir == std::ptr::null_mut() { + if dir.is_null() { Err(io::Error::last_os_error()) } else { - Ok(DirIter { dir: dir }) - } -} - -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) + Ok(DirIter { + dir: Arc::new(DirHandle::new(dir)), + }) } } impl Iterator for DirIter { type Item = io::Result; + fn next(&mut self) -> Option { unsafe { loop { + let dir = Arc::clone(&self.dir); match self.next_entry() { Err(e) => return Some(Err(e)), Ok(None) => return None, @@ -127,8 +173,8 @@ impl Iterator for DirIter { Ok(Some(e)) if e.d_name[..3] == DOTDOT => continue, Ok(Some(e)) => { return Some(Ok(Entry { - name: CStr::from_ptr((e.d_name).as_ptr()) - .to_owned(), + dir, + name: CStr::from_ptr((e.d_name).as_ptr()).to_owned(), file_type: match e.d_type { 0 => None, libc::DT_REG => Some(SimpleType::File), @@ -136,6 +182,7 @@ impl Iterator for DirIter { libc::DT_LNK => Some(SimpleType::Symlink), _ => Some(SimpleType::Other), }, + ino: e.d_ino, })); } } @@ -144,10 +191,40 @@ impl Iterator for DirIter { } } -impl Drop for DirIter { +//#[derive(Debug)] +struct DirHandle(*mut libc::DIR); + +impl DirHandle { + fn new(dir: *mut libc::DIR) -> Self { + DirHandle(dir) + } + + fn raw(&self) -> *mut libc::DIR { + self.0 + } +} + +impl Drop for DirHandle { fn drop(&mut self) { unsafe { - libc::closedir(self.dir); + libc::closedir(self.0); + } + } +} + +#[cfg(test)] +mod test { + use crate::Dir; + + #[test] + fn test() { + let d = Dir::open(".").unwrap(); + for e in d.list_self().unwrap() { + if let Ok(e) = e { + if let Ok(m) = e.metadata() { + eprintln!("{:?} : {:?}", e.file_name(), m); + } + } } } } diff --git a/src/metadata.rs b/src/metadata.rs index de7cc22..b6b2ccb 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,11 +1,10 @@ +use std::fmt; use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; - -use libc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::SimpleType; - /// A file metadata /// /// Because we can't freely create a `std::fs::Metadata` object we have to @@ -14,41 +13,216 @@ pub struct Metadata { stat: libc::stat, } +impl fmt::Debug for Metadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Metadata") + .field("st_dev", &self.stat.st_dev) + .field("st_ino", &self.stat.st_ino) + .field("st_nlink", &self.stat.st_nlink) + .field("st_mode", &self.stat.st_mode) + .field("st_uid", &self.stat.st_uid) + .field("st_gid", &self.stat.st_gid) + .field("st_size", &self.stat.st_size) + .field("st_blocks", &self.stat.st_blocks) + .finish() + } +} + +/// Implements and exports the used types here. Depending on feature flags and operating +/// system the underlying types may change. By redefining them here this will stay consistent +/// to an user of the library. +#[allow(non_camel_case_types)] +#[allow(missing_docs)] +pub mod metadata_types { + pub type mode_t = libc::mode_t; + pub type ino_t = libc::ino_t; + pub type dev_t = libc::dev_t; + pub type c_uint = libc::c_uint; + pub type blksize_t = libc::blksize_t; + pub type blkcnt_t = libc::blkcnt_t; + pub type off_t = libc::off_t; + pub type nlink_t = libc::nlink_t; + pub type uid_t = libc::uid_t; + pub type gid_t = libc::gid_t; +} + +use metadata_types::*; + 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, } } + /// Returns underlying stat structure + #[deprecated( + since = "0.2.0", + note = "future versions will use other underlying methods to gather metadata (statx on linux)." + )] pub fn stat(&self) -> &libc::stat { &self.stat } + /// Returns `true` if the entry is a regular file pub fn is_file(&self) -> bool { self.simple_type() == SimpleType::File } + /// Returns `true` if the entry is a directory pub fn is_dir(&self) -> bool { self.simple_type() == SimpleType::Dir } + /// Returns permissions of the entry pub fn permissions(&self) -> Permissions { Permissions::from_mode(self.stat.st_mode as u32) } + /// Returns file size + #[allow(clippy::len_without_is_empty)] + #[deprecated(since = "0.2.0", note = "use Metadata::size(&self)")] pub fn len(&self) -> u64 { self.stat.st_size as u64 } + + /// Return low level file mode, if available + pub fn mode(&self) -> Option { + Some(self.stat.st_mode) + } + + /// 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 of the file, if available + pub fn dev(&self) -> Option { + Some(self.stat.st_dev) + } + + /// 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 of an device descriptor, if available + pub fn rdev(&self) -> Option { + match self.mode()? { + libc::S_IFBLK | libc::S_IFCHR => Some(self.stat.st_rdev), + _ => None, + } + } + + /// Return device node major of an device descriptor, if available + pub fn rdev_major(&self) -> Option { + Some(major(self.rdev()?)) + } + + /// Return device node minor of an device descriptor, if available + pub fn rdev_minor(&self) -> Option { + Some(minor(self.rdev()?)) + } + + /// 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 } + Metadata { 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) -> libc::c_uint { + unsafe { libc::major(dev) } +} + +#[cfg(not(target_os = "macos"))] +pub fn minor(dev: libc::dev_t) -> libc::c_uint { + 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) -> libc::c_uint { + (dev as u32 >> 24) & 0xff +} + +#[cfg(target_os = "macos")] +pub fn minor(dev: libc::dev_t) -> libc::c_uint { + dev as u32 & 0xffffff } #[cfg(test)] diff --git a/src/name.rs b/src/name.rs index c181db1..236ae9d 100644 --- a/src/name.rs +++ b/src/name.rs @@ -1,9 +1,8 @@ -use std::ffi::{OsStr, CStr, CString}; -use std::path::{Path, PathBuf}; +use std::ffi::{CStr, CString, OsStr}; use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; -use crate::{Entry}; - +use crate::list::Entry; /// The purpose of this is similar to `AsRef` but it's optimized for /// things that can be directly used as `CStr` (which is type passed to @@ -21,6 +20,7 @@ pub trait AsPath { impl<'a> AsPath for &'a Path { type Buffer = CString; + fn to_path(self) -> Option { CString::new(self.as_os_str().as_bytes()).ok() } @@ -28,6 +28,7 @@ impl<'a> AsPath for &'a Path { impl<'a> AsPath for &'a PathBuf { type Buffer = CString; + fn to_path(self) -> Option { CString::new(self.as_os_str().as_bytes()).ok() } @@ -35,6 +36,7 @@ impl<'a> AsPath for &'a PathBuf { impl<'a> AsPath for &'a OsStr { type Buffer = CString; + fn to_path(self) -> Option { CString::new(self.as_bytes()).ok() } @@ -42,6 +44,7 @@ impl<'a> AsPath for &'a OsStr { impl<'a> AsPath for &'a str { type Buffer = CString; + fn to_path(self) -> Option { CString::new(self.as_bytes()).ok() } @@ -49,6 +52,7 @@ impl<'a> AsPath for &'a str { impl<'a> AsPath for &'a String { type Buffer = CString; + fn to_path(self) -> Option { CString::new(self.as_bytes()).ok() } @@ -56,6 +60,7 @@ impl<'a> AsPath for &'a String { impl<'a> AsPath for String { type Buffer = CString; + fn to_path(self) -> Option { CString::new(self).ok() } @@ -63,6 +68,7 @@ impl<'a> AsPath for String { impl<'a> AsPath for &'a CStr { type Buffer = &'a CStr; + fn to_path(self) -> Option<&'a CStr> { Some(self) } @@ -70,6 +76,7 @@ impl<'a> AsPath for &'a CStr { impl<'a> AsPath for &'a Entry { type Buffer = &'a CStr; + fn to_path(self) -> Option<&'a CStr> { Some(&self.name) } diff --git a/tests/flagsbuilder.rs b/tests/flagsbuilder.rs new file mode 100644 index 0000000..c3f212e --- /dev/null +++ b/tests/flagsbuilder.rs @@ -0,0 +1,49 @@ +use openat::Dir; +use openat_ct as openat; + +#[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..3195c76 100644 --- a/tests/tmpfile.rs +++ b/tests/tmpfile.rs @@ -1,12 +1,11 @@ -extern crate tempfile; -extern crate openat; - use std::io::{self, Read, Write}; use std::os::unix::fs::PermissionsExt; + +use openat_ct as openat; 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())?;