From 6cef9f0fc159de4c9fd708050ec76adb4e74d390 Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Fri, 27 Jan 2023 13:58:10 +0000 Subject: openat variant --- openat/src/dir.rs | 696 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 696 insertions(+) create mode 100644 openat/src/dir.rs (limited to 'openat/src/dir.rs') diff --git a/openat/src/dir.rs b/openat/src/dir.rs new file mode 100644 index 0000000..eac2f38 --- /dev/null +++ b/openat/src/dir.rs @@ -0,0 +1,696 @@ +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 libc; +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")))] +const BASE_OPEN_FLAGS: libc::c_int = libc::O_CLOEXEC; + +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 + // TODO(tailhook) maybe accept only absolute paths? + pub fn open(path: P) -> io::Result { + Dir::_open(to_cstr(path)?.as_ref()) + } + + 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)) + } + } + + /// List subdirectory of this 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()) + } + + /// List this dir + pub fn list_self(&self) -> io::Result { + unsafe { + open_dirfd(libc::dup(self.0)) + } + } + + /// Open subdirectory + /// + /// 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 + pub fn sub_dir(&self, path: P) -> io::Result { + self._sub_dir(to_cstr(path)?.as_ref()) + } + + 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)) + } + } + + /// Read link in this directory + pub fn read_link(&self, path: P) -> io::Result { + self._read_link(to_cstr(path)?.as_ref()) + } + + 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()) + }; + if res < 0 { + Err(io::Error::last_os_error()) + } else { + buf.truncate(res as usize); + Ok(OsString::from_vec(buf).into()) + } + } + + /// Open file for reading in this directory + /// + /// 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 + pub fn open_file(&self, path: P) -> io::Result { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_RDONLY, 0) + } + + /// Open file with specified flags relative to this directory + pub fn open_file_ex(&self, path: P, flags: libc::c_int, mode: libc::mode_t) -> io::Result { + self._open_file(to_cstr(path)?.as_ref(), flags, mode) + } + + /// Open file for writing, create if necessary, truncate on open + /// + /// 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 + 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 + /// + /// If there exists a symlink at the destination path, this method will fail. In that case, you + /// 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) + } + + /// Create a tmpfile in this directory which isn't linked to any filename + /// + /// This works by passing `O_TMPFILE` into the openat call. The flag is + /// supported only on linux. So this function always returns error on + /// such systems. + /// + /// **WARNING!** On glibc < 2.22 file permissions of the newly created file + /// may be arbitrary. Consider chowning after creating a file. + /// + /// Note: It may be unclear why creating unnamed file requires a dir. There + /// are two reasons: + /// + /// 1. It's created (and occupies space) on a real filesystem, so the + /// directory is a way to find out which filesystem to attach file to + /// 2. This method is mostly needed to initialize the file then link it + /// using ``link_file_at`` to the real directory entry. When linking + /// it must be linked into the same filesystem. But because for most + /// programs finding out filesystem layout is an overkill the rule of + /// thumb is to create a file in the the target directory. + /// + /// 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) + } + + /// Create a tmpfile in this directory which isn't linked to any filename + /// + /// This works by passing `O_TMPFILE` into the openat call. The flag is + /// supported only on linux. So this function always returns error on + /// such systems. + /// + /// Note: It may be unclear why creating unnamed file requires a dir. There + /// are two reasons: + /// + /// 1. It's created (and occupies space) on a real filesystem, so the + /// directory is a way to find out which filesystem to attach file to + /// 2. This method is mostly needed to initialize the file then link it + /// using ``link_file_at`` to the real directory entry. When linking + /// it must be linked into the same filesystem. But because for most + /// programs finding out filesystem layout is an overkill the rule of + /// thumb is to create a file in the the target directory. + /// + /// 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")) + } + + /// Link open file to a specified path + /// + /// This is used with ``new_unnamed_file()`` to create and initialize the + /// file before linking it into a filesystem. This requires `/proc` to be + /// mounted and works **only on linux**. + /// + /// On systems other than linux this always returns error. It's expected + /// that in most cases this methos is not called if ``new_unnamed_file`` + /// 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<()> + { + 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) + } + + /// Link open file to a specified path + /// + /// This is used with ``new_unnamed_file()`` to create and initialize the + /// file before linking it into a filesystem. This requires `/proc` to be + /// mounted and works **only on linux**. + /// + /// On systems other than linux this always returns error. It's expected + /// that in most cases this methos is not called if ``new_unnamed_file`` + /// 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")) + } + + /// Create file if not exists, fail if exists + /// + /// This function checks existence and creates file atomically with + /// 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) + } + + /// Open file for reading and writing without truncation, create if needed + /// + /// If there exists a symlink at the destination path, this method will fail. In that case, you + /// 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) + } + + 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)) + } + } + } + + /// Make a symlink in this directory + /// + /// Note: the order of arguments differ from `symlinkat` + 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()); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + } + + /// Create a subdirectory in this directory + 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(()) + } + } + } + + /// Rename a file in this directory to another name (keeping same dir) + 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<()> + { + // 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) + } + + /// Remove a subdirectory in this directory + /// + /// Note only empty directory may be removed + 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<()> + { + 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(()) + } + } + } + + /// Get the path of this directory (if possible) + /// + /// This uses symlinks in `/proc/self`, they sometimes may not be + /// available so use with care. + pub fn recover_path(&self) -> io::Result { + let fd = self.0; + if fd != libc::AT_FDCWD { + read_link(format!("/proc/self/fd/{}", fd)) + } else { + read_link("/proc/self/cwd") + } + } + + /// Returns metadata of an entry in this directory + /// + /// If the destination path is a symlink, this will return the metadata of the symlink itself. + /// If you would like to follow the symlink and return the metadata of the target, you will + /// have to call [`read_link`] to resolve the real path first. + /// + /// [`read_link`]: #method.read_link + 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)) + } + } + } + + /// 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)) + } + } + } + + /// Constructs a new `Dir` from a given raw file descriptor, + /// ensuring it is a directory file descriptor first. + /// + /// 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)) + } + } + } + + /// Creates a new independently owned handle to the underlying directory. + pub fn try_clone(&self) -> io::Result { + let fd = unsafe { libc::dup(self.0) }; + if fd == -1 { + Err(io::Error::last_os_error()) + } else { + unsafe { Self::from_raw_fd_checked(fd) } + } + } +} + +/// Rename (move) a file between directories +/// +/// 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, +{ + _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<()> +{ + 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(()) + } + } +} + +/// Create a hardlink to a file +/// +/// Files must be on a single filesystem even if they are in different +/// directories. +/// +/// 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, +{ + _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<()> +{ + 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(()) + } + } +} + +/// Rename (move) a file between directories with flags +/// +/// Files must be on a single filesystem anyway. This funtion does **not** +/// 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, +{ + _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<()> +{ + unsafe { + let res = libc::syscall( + libc::SYS_renameat2, + old_dir.0, old.as_ptr(), + new_dir.0, new.as_ptr(), flags); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} + +impl AsRawFd for Dir { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.0 + } +} + +impl FromRawFd for Dir { + /// The user must guarantee that the passed in `RawFd` is in fact + /// a directory file descriptor. + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Dir { + Dir(fd) + } +} + +impl IntoRawFd for Dir { + #[inline] + fn into_raw_fd(self) -> RawFd { + let result = self.0; + mem::forget(self); + return result; + } +} + +impl Drop for Dir { + fn drop(&mut self) { + let fd = self.0; + if fd != libc::AT_FDCWD { + unsafe { + libc::close(fd); + } + } + } +} + +fn to_cstr(path: P) -> io::Result { + path.to_path() + .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::os::unix::io::{FromRawFd, IntoRawFd}; + 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(); + assert!(buf.find("extern crate libc;").is_some()); + } + + #[test] + fn test_from_into() { + 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(); + assert!(buf.find("extern crate libc;").is_some()); + } + + #[test] + #[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::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()); + } + + #[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 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) + } + } + + #[test] + fn test_try_clone() { + let d = Dir::open(".").unwrap(); + let d2 = d.try_clone().unwrap(); + drop(d); + let _file = d2.open_file("src/lib.rs").unwrap(); + } +} -- cgit v1.2.3-54-g00ecf