dirx 0.1.0

Creates an in-memory index of all the files in a directory tree, and allows efficient scanning of only those files that have been modified since the index got created
Documentation
use crate::scan::error::Error;
use crate::scan::error::ErrorOperation;
use crate::scan::error::IntoScanError;
use crate::scan::Result;
use nix::fcntl::AtFlags;
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
use nix::sys::stat::SFlag;
use std::ffi::CStr;
use std::ffi::OsStr;
use std::fs::File;
use std::fs::Metadata;
use std::fs::OpenOptions;
use std::io;
use std::os::fd::AsRawFd;
use std::os::fd::FromRawFd;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::path::PathBuf;

fn c_str_to_os_str(cstr: &CStr) -> &OsStr {
    OsStr::from_bytes(cstr.to_bytes())
}

pub(super) fn open_at<P: AsRef<Path>>(dir: &ReadDir, path: P) -> Result<File> {
    let path = path.as_ref();
    let dirfd = dir.inner.as_raw_fd();
    let fd = nix::fcntl::openat(
        dirfd,
        path,
        OFlag::O_RDONLY | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
        Mode::empty(),
    );
    match fd {
        // SAFETY: this is a valid file descriptor returned from a successful call to `openat`
        Ok(fd) => Ok(unsafe { File::from_raw_fd(fd) }),
        Err(_) => Err(Error::last_os_error(
            dir.path.join(path),
            ErrorOperation::OpenFile,
        )),
    }
}

pub(super) fn read_dir<P: AsRef<Path>>(path: P) -> io::Result<ReadDir> {
    let dir = OpenOptions::new().read(true).open(path)?;
    ReadDir::from_file(PathBuf::new(), dir).map_err(Error::into_source)
}

pub(super) fn read_dir_at<P: AsRef<Path>>(dir: &ReadDir, path: P) -> Result<ReadDir> {
    let path = path.as_ref();
    let full_path = dir.path.join(path);
    let dirfd = dir.inner.as_raw_fd();
    let fd = nix::fcntl::openat(
        dirfd,
        path,
        OFlag::O_RDONLY | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW | OFlag::O_DIRECTORY,
        Mode::empty(),
    );
    let subdir = match fd {
        // SAFETY: this is a valid file descriptor returned from a successful call to `openat`
        Ok(fd) => unsafe { File::from_raw_fd(fd) },
        Err(_) => {
            return Err(Error::last_os_error(
                full_path,
                ErrorOperation::OpenDirectory,
            ))
        }
    };
    ReadDir::from_file(full_path, subdir)
}

fn file_type(dir: &ReadDir, dir_entry: &nix::dir::Entry) -> io::Result<FileType> {
    if let Some(file_type) = dir_entry.file_type() {
        Ok(match file_type {
            nix::dir::Type::File => FileType::File,
            nix::dir::Type::Directory => FileType::Directory,
            _ => FileType::Other,
        })
    } else {
        let dirfd = dir.inner.as_raw_fd();
        let stat =
            nix::sys::stat::fstatat(dirfd, dir_entry.file_name(), AtFlags::AT_SYMLINK_NOFOLLOW)?;
        let file_type = SFlag::S_IFMT & SFlag::from_bits_truncate(stat.st_mode);
        Ok(match file_type {
            SFlag::S_IFREG => FileType::File,
            SFlag::S_IFDIR => FileType::Directory,
            _ => FileType::Other,
        })
    }
}

pub(super) struct ReadDir {
    inner: nix::dir::OwningIter,
    pub(super) path: PathBuf,
    pub(super) metadata: Metadata,
}

impl ReadDir {
    fn from_file(path: PathBuf, file: File) -> Result<Self> {
        let metadata = match file.metadata() {
            Ok(metadata) => metadata,
            Err(err) => return Err(err.into_scan_error(path, ErrorOperation::ReadMetadata)),
        };
        let inner = match nix::dir::Dir::from(file) {
            Ok(dir) => dir.into_iter(),
            Err(err) => return Err(err.into_scan_error(path, ErrorOperation::ReadDirectory)),
        };
        Ok(Self {
            inner,
            path,
            metadata,
        })
    }
}

impl Iterator for ReadDir {
    type Item = Result<DirEntry>;

    fn next(&mut self) -> Option<Self::Item> {
        let dir_entry = match self.inner.next() {
            Some(Ok(dir_entry)) => dir_entry,
            Some(Err(_)) => {
                return Some(Err(Error::last_os_error(
                    self.path.clone(),
                    ErrorOperation::ReadDirectory,
                )))
            }
            None => return None,
        };

        let file_type = match file_type(self, &dir_entry) {
            Ok(file_type) => file_type,
            Err(err) => {
                let dir_entry_path = self.path.join(c_str_to_os_str(dir_entry.file_name()));
                return Some(Err(
                    err.into_scan_error(dir_entry_path, ErrorOperation::ReadMetadata)
                ));
            }
        };

        Some(Ok(DirEntry {
            inner: dir_entry,
            file_type,
        }))
    }
}

pub(super) struct DirEntry {
    inner: nix::dir::Entry,
    pub(super) file_type: FileType,
}

impl DirEntry {
    pub(super) fn file_name(&self) -> &OsStr {
        c_str_to_os_str(self.inner.file_name())
    }
}

pub(super) enum FileType {
    File,
    Directory,
    Other,
}

pub(super) trait SameFile {
    fn identifier(&self) -> (u64, u64);

    #[inline]
    fn is_same_as(&self, other: &Self) -> bool {
        self.identifier() == other.identifier()
    }
}

impl SameFile for Metadata {
    #[inline]
    fn identifier(&self) -> (u64, u64) {
        (self.dev(), self.ino())
    }
}

impl SameFile for ReadDir {
    #[inline]
    fn identifier(&self) -> (u64, u64) {
        self.metadata.identifier()
    }
}