openat_ct 0.2.0-pre9

A wrapper around openat, symlinkat, and similar system calls forked and improved from https://crates.io/crates/openat
Documentation
use std::ffi::{CStr, OsString};
use std::fs::{read_link, File};
use std::io;
use std::mem;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use std::path::{Path, PathBuf};

use crate::list::{open_dirfd, DirIter};
use crate::metadata::{self, Metadata};
use crate::{AsPath, DirFlags, DirMethodFlags, SimpleType};

/// Value if the libc::O_DIRECTORY flag when supported by the system, otherwise 0
#[cfg(feature = "o_directory")]
pub const O_DIRECTORY: libc::c_int = libc::O_DIRECTORY;
/// Value if the libc::O_DIRECTORY flag when supported by the system, otherwise 0
#[cfg(not(feature = "o_directory"))]
pub const O_DIRECTORY: libc::c_int = 0;

/// Value if the libc::O_PATH flag when supported by the system, otherwise 0
#[cfg(feature = "o_path")]
pub const O_PATH: libc::c_int = libc::O_PATH;
/// Value if the libc::O_PATH flag when supported by the system, otherwise 0
#[cfg(not(feature = "o_path"))]
pub const O_PATH: libc::c_int = 0;

/// Value if the libc::O_SEARCH flag when supported by the system, otherwise 0
#[cfg(feature = "o_search")]
pub const O_SEARCH: libc::c_int = libc::O_SEARCH;
/// Value if the libc::O_SEARCH flag when supported by the system, otherwise 0
#[cfg(not(feature = "o_search"))]
pub const O_SEARCH: libc::c_int = 0;

/// 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 {
    /// 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<P: AsPath>(path: P) -> io::Result<Dir> {
        Dir::_open(to_cstr(path)?.as_ref(), O_PATH | libc::O_CLOEXEC)
    }

    pub(crate) fn _open(path: &CStr, flags: libc::c_int) -> io::Result<Dir> {
        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,
        }
    }

    /// List subdirectory of this dir
    ///
    /// You can list directory itself with `list_self`.
    pub fn list_dir<P: AsPath>(&self, path: P) -> io::Result<DirIter> {
        self.with(O_SEARCH).sub_dir(path)?.list()
    }

    /// List this dir
    pub fn list_self(&self) -> io::Result<DirIter> {
        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<DirIter> {
        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 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 or create a handle
    /// 'without(libc::O_NOFOLLOW)'.
    ///
    /// [`read_link`]: #method.read_link
    pub fn sub_dir<P: AsPath>(&self, path: P) -> io::Result<Dir> {
        self._sub_dir(
            to_cstr(path)?.as_ref(),
            O_PATH | libc::O_CLOEXEC | libc::O_NOFOLLOW,
        )
    }

    pub(crate) fn _sub_dir(&self, path: &CStr, flags: libc::c_int) -> io::Result<Dir> {
        Ok(Dir::new(unsafe {
            libc_ok(libc::openat(self.0, path.as_ptr(), flags | O_DIRECTORY))?
        }))
    }

    /// Read link in this directory
    pub fn read_link<P: AsPath>(&self, path: P) -> io::Result<PathBuf> {
        self._read_link(to_cstr(path)?.as_ref())
    }

    fn _read_link(&self, path: &CStr) -> io::Result<PathBuf> {
        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<P: AsPath>(&self, path: P) -> io::Result<File> {
        self._open_file(to_cstr(path)?.as_ref(), libc::O_RDONLY, 0)
    }

    /// 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<P: AsPath>(&self, path: P, mode: libc::mode_t) -> io::Result<File> {
        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<P: AsPath>(&self, path: P, mode: libc::mode_t) -> io::Result<File> {
        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
    ///
    /// 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(feature = "o_tmpfile")]
    pub fn new_unnamed_file(&self, mode: libc::mode_t) -> io::Result<File> {
        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(feature = "o_tmpfile"))]
    pub fn new_unnamed_file<P: AsPath>(&self, _mode: libc::mode_t) -> io::Result<File> {
        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(feature = "link_file_at")]
    pub fn link_file_at<F: AsRawFd, P: AsPath>(&self, file: &F, path: P) -> io::Result<()> {
        let fd_path = format!("/proc/self/fd/{}", file.as_raw_fd());
        _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
    ///
    /// 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.
    // 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<F: AsRawFd, P: AsPath>(&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<P: AsPath>(&self, path: P, mode: libc::mode_t) -> io::Result<File> {
        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<P: AsPath>(&self, path: P, mode: libc::mode_t) -> io::Result<File> {
        self._open_file(to_cstr(path)?.as_ref(), libc::O_CREAT | libc::O_RDWR, mode)
    }

    pub(crate) fn _open_file(
        &self,
        path: &CStr,
        flags: libc::c_int,
        mode: libc::mode_t,
    ) -> io::Result<File> {
        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_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<P: AsPath, R: AsPath>(&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<P: AsPath>(&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 {
            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<P: AsPath, R: AsPath>(&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(feature = "rename_exchange")]
    pub fn local_exchange<P: AsPath, R: AsPath>(&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<P: AsPath>(&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<P: AsPath>(&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 {
            libc_ok(libc::unlinkat(self.0, path.as_ptr(), flags))?;
        }
        Ok(())
    }

    /// Removes a directory with all its contents
    pub fn remove_recursive<P: AsPath + Copy>(&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<P, T>(&self, path: P, tmp_dir: T) -> io::Result<()>
    where
        P: AsPath + Copy + std::convert::AsRef<std::path::Path>,
        T: AsPath + std::convert::AsRef<std::path::Path>,
    {
        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<PathBuf> {
        let fd = self.rawfd();
        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<P: AsPath>(&self, path: P) -> io::Result<Metadata> {
        self._stat(to_cstr(path)?.as_ref(), libc::AT_SYMLINK_NOFOLLOW)
    }

    fn _stat(&self, path: &CStr, flags: libc::c_int) -> io::Result<Metadata> {
        unsafe {
            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<Metadata> {
        unsafe {
            let mut stat = mem::zeroed(); // TODO(cehteh): uninit
            libc_ok(libc::fstat(self.0, &mut stat))?;
            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 fn from_raw_fd_checked(fd: RawFd) -> io::Result<Self> {
        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<Self> {
        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<Self> {
        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<Self> {
        Ok(Dir::new(clone_dirfd_downgrade(self.0)?))
    }
}

const CURRENT_DIRECTORY: [libc::c_char; 2] = [b'.' as libc::c_char, 0];

// TODO(cehteh): eventually the clone calls should replicate O_SEARCH, maybe other file flags
fn clone_dirfd(fd: libc::c_int) -> io::Result<libc::c_int> {
    unsafe {
        match fd_type(fd)? {
            FdType::NormalDir => libc_ok(libc::openat(
                fd,
                &CURRENT_DIRECTORY as *const libc::c_char,
                O_DIRECTORY | libc::O_CLOEXEC,
            )),
            #[cfg(feature = "o_path")]
            FdType::OPathDir => libc_ok(libc::dup(fd)),
            _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
        }
    }
}

pub(crate) fn clone_dirfd_upgrade(fd: libc::c_int, flags: libc::c_int) -> io::Result<libc::c_int> {
    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<libc::c_int> {
    unsafe {
        match fd_type(fd)? {
            #[cfg(feature = "o_path")]
            FdType::NormalDir => libc_ok(libc::openat(
                fd,
                &CURRENT_DIRECTORY as *const libc::c_char,
                libc::O_PATH | O_DIRECTORY | libc::O_CLOEXEC,
            )),
            #[cfg(not(feature = "o_path"))]
            FdType::NormalDir => libc_ok(libc::openat(
                fd,
                &CURRENT_DIRECTORY as *const libc::c_char,
                O_DIRECTORY | libc::O_CLOEXEC,
            )),
            #[cfg(feature = "o_path")]
            FdType::OPathDir => libc_ok(libc::dup(fd)),
            _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
        }
    }
}

enum FdType {
    NormalDir,
    OPathDir,
    Other,
}

// OSes with fcntl() that returns O_DIRECTORY
// Linux has O_PATH
#[cfg(all(feature = "o_path", feature = "fcntl_o_directory"))]
fn fd_type(fd: RawFd) -> io::Result<FdType> {
    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<FdType> {
    if fd != -1 {
        let flags = unsafe { libc_ok(libc::fcntl(fd, libc::F_GETFL))? };
        if flags & libc::O_DIRECTORY != 0 {
            Ok(FdType::NormalDir)
        } else {
            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<FdType> {
    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<libc::c_int> {
    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**
/// fallback to copying if needed.
pub fn rename<P, R>(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 {
        libc_ok(libc::renameat(
            old_dir.rawfd(),
            old.as_ptr(),
            new_dir.rawfd(),
            new.as_ptr(),
        ))?;
    }
    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<P, R>(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 {
        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
///
/// Files must be on a single filesystem anyway. This funtion does **not**
/// fallback to copying if needed.
///
/// Only supported on Linux.
#[cfg(feature = "renameat_flags")]
pub fn rename_flags<P, R>(
    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(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.rawfd(),
            old.as_ptr(),
            new_dir.rawfd(),
            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::new(fd)
    }
}

impl IntoRawFd for Dir {
    #[inline]
    fn into_raw_fd(self) -> RawFd {
        let result = self.0;
        mem::forget(self);
        result
    }
}

impl Drop for Dir {
    fn drop(&mut self) {
        unsafe {
            libc::close(self.0);
        }
    }
}

pub(crate) fn to_cstr<P: AsPath>(path: P) -> io::Result<P::Buffer> {
    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::os::unix::io::{FromRawFd, IntoRawFd};
    use std::path::Path;

    use crate::Dir;

    #[test]
    fn test_open_ok() {
        assert!(Dir::open("src").is_ok());
    }

    #[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::flags().open("src").unwrap();
        let me = dir.list().unwrap();
        assert!(
            me.collect::<Result<Vec<_>, _>>()
                .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::<Result<Vec<_>, _>>()
                .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::<Result<Vec<_>, _>>()
                .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::<Result<Vec<_>, _>>()
                .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::<Result<Vec<_>, _>>()
                .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 = Dir::from_raw_fd_checked(fd).unwrap();
        let filefd = dir.open_file("src/lib.rs").unwrap().into_raw_fd();
        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),
        }
    }

    #[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();
    }

    #[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();
    }
}