diff --git a/src/dir.rs b/src/dir.rs index 3bed451..66bf82d 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -7,16 +7,18 @@ 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}; -#[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")))] +// NOTE(cehteh): removed O_PATH since it is linux only and highly unportable (semantics can't be emulated) +// but see open_path() below. +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +const BASE_OPEN_FLAGS: libc::c_int = libc::O_DIRECTORY | libc::O_CLOEXEC; +// NOTE(cehteh): O_DIRECTORY is defined in posix, what is the reason for not using it? +// TODO(cehteh): on systems that do not support O_DIRECTORY a runtime stat-check is required +#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] const BASE_OPEN_FLAGS: libc::c_int = libc::O_CLOEXEC; impl Dir { @@ -36,32 +38,50 @@ impl Dir { /// 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()) + Dir::_open(to_cstr(path)?.as_ref(), BASE_OPEN_FLAGS) } - 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 'lite' directory descriptor at specified path + /// 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 + /// Using this descriptor for iterating over the content is unspecified. + /// Uses O_PATH on Linux + #[cfg(any(target_os = "linux"))] + pub fn open_lite(path: P) -> io::Result { + Dir::_open(to_cstr(path)?.as_ref(), libc::O_PATH | BASE_OPEN_FLAGS) + } + + #[cfg(not(target_os = "linux"))] + pub fn open_lite(path: P) -> io::Result { + Dir::_open(to_cstr(path)?.as_ref(), BASE_OPEN_FLAGS) + } + + fn _open(path: &CStr, flags: libc::c_int) -> io::Result { + let fd = unsafe { libc_ok(libc::open(path.as_ptr(), flags))? }; + Ok(Dir(fd)) } /// List subdirectory of this dir /// /// You can list directory itself with `list_self`. + // TODO(cehteh): may be deprecated in favor of list() pub fn list_dir(&self, path: P) -> io::Result { - open_dir(self, to_cstr(path)?.as_ref()) + self.sub_dir(path)?.list() } /// List this dir + // TODO(cehteh): may be deprecated in favor of list() pub fn list_self(&self) -> io::Result { - unsafe { - open_dirfd(libc::dup(self.0)) - } + self.clone_upgrade()?.list() + } + + /// Create a DirIter from a Dir + /// Dir must not be a 'Lite' handle + pub fn list(self) -> io::Result { + let fd = self.0; + std::mem::forget(self); + open_dirfd(fd) } /// Open subdirectory @@ -71,20 +91,33 @@ impl Dir { /// /// [`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(), BASE_OPEN_FLAGS | 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)) - } + /// Open subdirectory with a 'lite' descriptor at specified path + /// 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 + /// Using this descriptor for iterating over the content is unspecified. + /// Uses O_PATH on Linux + /// + /// 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`]: #method.read_link + #[cfg(any(target_os = "linux"))] + pub fn sub_dir_lite(&self, path: P) -> io::Result { + self._sub_dir(to_cstr(path)?.as_ref(), BASE_OPEN_FLAGS | libc::O_NOFOLLOW | libc::O_PATH) + } + + #[cfg(not(target_os = "linux"))] + pub fn sub_dir_lite(&self, path: P) -> io::Result { + self._sub_dir(to_cstr(path)?.as_ref(), BASE_OPEN_FLAGS | libc::O_NOFOLLOW) + } + + 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))? })) } /// Read link in this directory @@ -304,14 +337,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 +374,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) @@ -392,19 +419,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. + // FIXME(cehteh): proc stuff isn't portable pub fn recover_path(&self) -> io::Result { let fd = self.0; if fd != libc::AT_FDCWD { @@ -426,27 +450,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,29 +472,136 @@ 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::LiteDir => Ok(Dir(fd)), + _ => 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/Lite) 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)?)) + } + + /// Creates a new 'Lite' 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]; + +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, + BASE_OPEN_FLAGS, + )), + #[cfg(target_os = "linux")] + FdType::LiteDir => libc_ok(libc::dup(fd)), + _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)), + } + } +} + +fn clone_dirfd_upgrade(fd: libc::c_int) -> io::Result { + unsafe { + match fd_type(fd)? { + FdType::NormalDir | FdType::LiteDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + BASE_OPEN_FLAGS, + )), + _ => 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(target_os = "linux")] + FdType::NormalDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + libc::O_PATH | BASE_OPEN_FLAGS, + )), + #[cfg(not(target_os = "linux"))] + FdType::NormalDir => libc_ok(libc::openat( + fd, + &CURRENT_DIRECTORY as *const libc::c_char, + BASE_OPEN_FLAGS, + )), + #[cfg(target_os = "linux")] + FdType::LiteDir => libc_ok(libc::dup(fd)), + _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)), + } + } +} + +enum FdType { + NormalDir, + LiteDir, + Other, +} + +// OSes with O_DIRECTORY can use fcntl() +// Linux hash O_PATH +#[cfg(target_os = "linux")] +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::LiteDir) } else { - unsafe { Self::from_raw_fd_checked(fd) } + Ok(FdType::NormalDir) + } + } else { + Ok(FdType::Other) + } +} + +#[cfg(target_os = "freebsd")] +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 without O_DIRECTORY use stat() +#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] +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()) + } +} + /// Rename (move) a file between directories /// /// Files must be on a single filesystem anyway. This funtion does **not** @@ -491,18 +613,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 +637,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 @@ -629,7 +742,8 @@ mod test { } #[test] - #[cfg_attr(target_os="freebsd", should_panic(expected="Not a directory"))] + #[cfg_attr(any(target_os = "freebsd", target_os = "linux"), should_panic(expected = "Not a directory"))] + // NOTE(cehteh): should fail in all cases! see O_DIRECTORY at the top fn test_open_file() { Dir::open("src/lib.rs").unwrap(); } @@ -661,13 +775,50 @@ mod test { #[test] fn test_list() { + 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/list.rs b/src/list.rs index 5b4d3cd..3d4889c 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]; @@ -104,17 +103,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 {