use crate::c_char;
use crate::fs::ReadDir;
use crate::fs::{FileDes, FileType, types::Result};
use crate::{DirEntryError, util::BytePath as _};
use chrono::{DateTime, Utc};
use core::cell::Cell;
use core::ffi::CStr;
use core::fmt;
use libc::{
AT_SYMLINK_FOLLOW, AT_SYMLINK_NOFOLLOW, F_OK, R_OK, W_OK, X_OK, access, fstatat, lstat,
realpath, stat,
};
use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _, path::Path};
#[derive(Clone)] pub struct DirEntry {
pub(crate) path: Box<CStr>,
pub(crate) file_type: FileType,
pub(crate) inode: u64, pub(crate) depth: u32,
pub(crate) file_name_index: usize, pub(crate) is_traversible_cache: Cell<Option<bool>>, }
impl core::ops::Deref for DirEntry {
type Target = [u8];
#[inline]
fn deref(&self) -> &[u8] {
self.as_bytes()
}
}
impl AsRef<[u8]> for DirEntry {
#[inline]
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl<'drnt> From<&'drnt DirEntry> for &'drnt CStr {
#[inline]
fn from(entry: &'drnt DirEntry) -> &'drnt CStr {
&entry.path
}
}
impl fmt::Display for DirEntry {
#[allow(clippy::missing_inline_in_public_items)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string_lossy())
}
}
impl From<DirEntry> for std::path::PathBuf {
#[inline]
fn from(entry: DirEntry) -> Self {
entry.as_os_str().into()
}
}
impl TryFrom<&[u8]> for DirEntry {
type Error = DirEntryError;
#[inline]
fn try_from(path: &[u8]) -> Result<Self> {
Self::new(OsStr::from_bytes(path))
}
}
impl TryFrom<&OsStr> for DirEntry {
type Error = DirEntryError;
#[inline]
fn try_from(path: &OsStr) -> Result<Self> {
Self::new(path)
}
}
impl AsRef<Path> for DirEntry {
#[inline]
fn as_ref(&self) -> &Path {
self.as_path()
}
}
impl fmt::Debug for DirEntry {
#[allow(clippy::missing_inline_in_public_items)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Dirent")
.field("path", &self.to_string_lossy())
.field("file_name", &String::from_utf8_lossy(self.file_name()))
.field("depth", &self.depth)
.field("file_type", &self.file_type)
.field("file_name_index", &self.file_name_index)
.field("inode", &self.inode)
.field("traversible_cache", &self.is_traversible_cache)
.finish()
}
}
impl DirEntry {
#[inline]
pub fn is_executable(&self) -> bool {
self.is_regular_file() && unsafe { access(self.as_ptr(), X_OK) == 0 }
}
#[inline]
pub const fn as_ptr(&self) -> *const c_char {
self.path.as_ptr() }
#[inline]
pub const fn as_path(&self) -> &Path {
unsafe { core::mem::transmute(self.as_bytes()) } }
#[inline]
pub const fn as_os_str(&self) -> &OsStr {
unsafe { core::mem::transmute(self.as_bytes()) }
}
#[inline]
#[must_use]
pub const fn is_block_device(&self) -> bool {
self.file_type.is_block_device()
}
#[inline]
pub(crate) fn open(&self) -> Result<FileDes> {
const FLAGS: i32 = libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NONBLOCK;
let fd = unsafe { libc::open(self.as_ptr(), FLAGS) };
if fd < 0 {
return_os_error!()
}
Ok(FileDes(fd))
}
#[inline]
#[must_use]
pub fn to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(self)
}
#[inline]
pub const fn as_str(&self) -> core::result::Result<&str, core::str::Utf8Error> {
core::str::from_utf8(self.as_bytes())
}
#[inline]
#[must_use]
pub const fn is_char_device(&self) -> bool {
self.file_type.is_char_device()
}
#[inline]
#[must_use]
pub const fn is_pipe(&self) -> bool {
self.file_type.is_pipe()
}
#[inline]
#[must_use]
pub const fn is_socket(&self) -> bool {
self.file_type.is_socket()
}
#[inline]
#[must_use]
pub const fn is_regular_file(&self) -> bool {
self.file_type.is_regular_file()
}
#[inline]
#[must_use]
pub const fn is_dir(&self) -> bool {
self.file_type.is_dir()
}
#[inline]
#[must_use]
pub const fn is_unknown(&self) -> bool {
self.file_type.is_unknown()
}
#[inline]
#[must_use]
pub const fn is_symlink(&self) -> bool {
self.file_type.is_symlink()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
match self.file_type {
FileType::RegularFile => self.file_size().is_ok_and(|size| size == 0),
FileType::Directory => {
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "netbsd",
target_os = "solaris",
target_os = "illumos"
))]
let result = self.is_empty_getdents();
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "netbsd",
target_os = "solaris",
target_os = "illumos"
)))]
let result = self.is_empty_posix(); result
}
_ => false,
}
}
#[inline]
#[must_use]
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "netbsd",
target_os = "solaris",
target_os = "illumos"
)))]
pub(crate) fn is_empty_posix(&self) -> bool {
use crate::readdir64;
use libc::closedir;
debug_assert!(
self.is_dir(),
"checking the only entries to this are directories"
);
let dir = unsafe { libc::opendir(self.as_ptr()) };
if dir.is_null() {
return false;
}
let result = unsafe {
readdir64(dir); readdir64(dir); readdir64(dir).is_null()
};
unsafe { closedir(dir) };
result
}
#[inline]
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "netbsd",
target_os = "solaris",
target_os = "illumos"
))]
pub(crate) fn is_empty_getdents(&self) -> bool {
use crate::fs::AlignedBuffer;
use crate::util::getdents64;
const BUF_SIZE: usize = 500; const MINIMUM_DIRENT_SIZE: isize = 32;
debug_assert!(
self.file_type == FileType::Directory || self.file_type == FileType::Symlink,
" Only expect dirs/symlinks to pass through this private func"
);
let dirfd = self.open();
if let Ok(fd) = dirfd {
let mut syscall_buffer = AlignedBuffer::<u8, BUF_SIZE>::new();
let dents = unsafe { getdents64(fd.0, syscall_buffer.as_mut_ptr().cast(), BUF_SIZE) };
unsafe { libc::close(fd.0) };
return dents <= 2 * MINIMUM_DIRENT_SIZE;
}
false }
#[inline]
pub const fn as_cstr(&self) -> &CStr {
&self.path
}
#[inline]
pub const fn file_name_cstr(&self) -> &CStr {
let path = self.path.to_bytes_with_nul();
let len = path.len() - self.file_name_index;
unsafe {
CStr::from_bytes_with_nul_unchecked(core::slice::from_raw_parts(
path.as_ptr().add(self.file_name_index),
len,
))
}
}
#[inline]
pub const fn file_name_ptr(&self) -> *const c_char {
unsafe { self.as_ptr().add(self.file_name_index) }
}
#[inline]
#[must_use]
pub const fn file_name(&self) -> &[u8] {
debug_assert!(
self.len() >= self.file_name_index(),
"this should always be equal or below (equal only when root)"
);
let path = self.as_bytes();
let len = path.len() - self.file_name_index;
unsafe {
core::slice::from_raw_parts(path.as_ptr().add(self.file_name_index), len)
}
}
#[inline]
pub fn to_inner(self) -> Box<CStr> {
self.path
}
#[inline]
pub(crate) fn get_realpath<F, T>(&self, f: F) -> Result<T>
where
F: Fn(&CStr) -> Result<T>,
{
let ptr = unsafe { realpath(self.as_ptr(), core::ptr::null_mut()) };
if ptr.is_null() {
return_os_error!()
}
let cstr = unsafe { CStr::from_ptr(ptr) };
let result = f(cstr);
unsafe { libc::free(ptr.cast()) }
result
}
#[inline]
#[allow(clippy::cast_possible_truncation)]
pub fn to_full_path(&self) -> Result<Self> {
self.get_realpath(|full_path| {
let file_name_index = full_path.to_bytes().file_name_index();
let (file_type, ino) = if self.is_symlink() {
let statted = self.get_stat()?; (FileType::from_stat(&statted), access_stat!(statted, st_ino))
} else {
(self.file_type(), self.ino())
};
Ok(Self {
path: full_path.into(),
file_type,
inode: ino,
depth: self.depth,
file_name_index,
is_traversible_cache: Cell::new(Some(file_type == FileType::Directory)),
})
})
}
#[inline]
pub fn parent(&self) -> Option<&[u8]> {
self.as_path()
.parent()
.map(|path| path.as_os_str().as_bytes())
}
#[inline]
pub fn is_readable(&self) -> bool {
unsafe { access(self.as_ptr(), R_OK) == 0 }
}
#[inline]
pub fn is_writable(&self) -> bool {
unsafe { access(self.as_ptr(), W_OK) == 0 }
}
#[inline]
pub fn exists(&self) -> bool {
unsafe { access(self.as_ptr(), F_OK) == 0 }
}
#[inline]
pub fn get_lstatat(&self, fd: &FileDes) -> Result<stat> {
stat_syscall!(fstatat, fd.0, self.file_name_ptr(), AT_SYMLINK_NOFOLLOW)
}
#[inline]
pub fn get_lstat(&self) -> Result<stat> {
stat_syscall!(lstat, self.as_ptr())
}
#[inline]
pub fn get_stat(&self) -> Result<stat> {
stat_syscall!(stat, self.as_ptr())
}
#[inline]
pub fn get_statat(&self, fd: &FileDes) -> Result<stat> {
stat_syscall!(fstatat, fd.0, self.file_name_ptr(), AT_SYMLINK_FOLLOW)
}
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] {
self.path.to_bytes()
}
#[inline]
pub const fn len(&self) -> usize {
self.path.count_bytes()
}
#[inline]
#[must_use]
pub const fn file_type(&self) -> FileType {
self.file_type
}
#[inline]
#[must_use]
pub const fn depth(&self) -> usize {
self.depth as _
}
#[inline]
#[must_use]
pub const fn ino(&self) -> u64 {
self.inode
}
#[inline]
#[must_use]
pub fn filter<F: Fn(&Self) -> bool>(&self, func: F) -> bool {
func(self)
}
#[inline]
#[must_use]
pub const fn file_name_index(&self) -> usize {
self.file_name_index
}
#[inline]
#[must_use]
pub fn is_traversible(&self) -> bool {
match self.file_type {
FileType::Directory => true,
FileType::Symlink => self.check_symlink_traversibility(), _ => false,
}
}
#[inline]
pub(crate) fn check_symlink_traversibility(&self) -> bool {
debug_assert!(
self.file_type() == FileType::Symlink,
"we only expect symlinks to use this function(hence private)"
);
if let Some(cached) = self.is_traversible_cache.get() {
return cached;
}
let is_traversible = self
.get_stat()
.is_ok_and(|entry| FileType::from_stat(&entry) == FileType::Directory);
self.is_traversible_cache.set(Some(is_traversible));
is_traversible
}
#[inline]
#[must_use]
pub const fn is_hidden(&self) -> bool {
unsafe { self.as_ptr().cast::<u8>().add(self.file_name_index).read() == b'.' }
}
#[inline]
#[must_use]
pub fn dirname(&self) -> &[u8] {
debug_assert!(
self.file_name_index() <= self.len(),
"Indexing should always be within bounds"
);
if self.len() == 1 && self.as_bytes() == b"/" {
return b"/";
}
unsafe {
self .get_unchecked(..self.file_name_index().saturating_sub(1))
.rsplit(|&b| b == b'/')
.next()
.unwrap_or(self.as_bytes())
}
}
#[inline]
pub fn extension(&self) -> Option<&[u8]> {
let filename = self.file_name();
let len = filename.len();
if len <= 1 {
return None;
}
let search_range = unsafe { &filename.get_unchecked(..len.saturating_sub(1)) };
crate::util::memrchr(b'.', search_range).map(|pos| {
unsafe { filename.get_unchecked(pos + 1..) }
})
}
#[inline]
pub fn new<T: AsRef<OsStr>>(path: T) -> Result<Self> {
let mut path_ref = path.as_ref().as_bytes();
if path_ref != b"/"
&& let Some(stripped) = path_ref.strip_suffix(b"/")
{
path_ref = stripped;
}
let cstring = std::ffi::CString::new(path_ref).map_err(DirEntryError::NulError)?;
let get_stat = stat_syscall!(lstat, cstring.as_ptr()).map_err(DirEntryError::IOError)?;
let inode = access_stat!(get_stat, st_ino);
let file_name_index = path_ref.file_name_index();
let file_type = FileType::from_stat(&get_stat);
Ok(Self {
path: cstring.into(),
file_type,
inode,
depth: 0,
file_name_index,
is_traversible_cache: Cell::new(None), })
}
#[inline]
#[expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "needs to be in u32 for chrono"
)]
pub fn modified_time(&self) -> Result<DateTime<Utc>> {
let statted = self.get_lstat()?;
DateTime::from_timestamp(
access_stat!(statted, st_mtime),
access_stat!(statted, st_mtimensec),
)
.ok_or(DirEntryError::TimeError)
}
#[inline]
#[expect(clippy::cast_sign_loss, reason = "Size is a u64")]
pub fn file_size(&self) -> Result<u64> {
self.get_lstat().map(|s| s.st_size as _)
}
#[inline]
pub fn readdir(&self) -> Result<ReadDir> {
ReadDir::new(self)
}
#[inline]
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "openbsd",
target_os = "netbsd",
target_os = "illumos",
target_os = "solaris"
))]
pub fn getdents(&self) -> Result<crate::fs::GetDents> {
crate::fs::GetDents::new(self)
}
#[inline]
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
pub fn getdirentries(&self) -> Result<crate::fs::GetDirEntries> {
crate::fs::GetDirEntries::new(self)
}
}