use super::{DirHandle, Excl, GuardIo, NodeId, NodeKind, OpenMode};
use std::ffi::{CString, OsStr};
use std::fs::File;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::Path;
pub struct UnixDirHandle(File);
impl crate::sealed::Sealed for UnixDirHandle {}
const DIR_FLAGS: libc::c_int =
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_DIRECTORY;
const FILE_FLAGS: libc::c_int =
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK;
fn cstr(name: &OsStr) -> Result<CString, GuardIo> {
CString::new(name.as_bytes())
.map_err(|_| GuardIo::io(io::Error::from(io::ErrorKind::InvalidInput)))
}
fn classify_failure(dir: &File, name: &OsStr, err: io::Error) -> GuardIo {
let looks_like_a_link = matches!(err.raw_os_error(), Some(libc::ELOOP) | Some(libc::ENOTDIR))
&& matches!(
kind_at(dir, name),
Some(NodeKind::NotFollowable { .. }) | Some(NodeKind::NotAFile)
);
if looks_like_a_link {
GuardIo::link(err)
} else {
GuardIo::io(err)
}
}
fn stat_at(dir: &File, name: &OsStr) -> Option<libc::stat> {
let c = cstr(name).ok()?;
let mut st: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::fstatat(
dir.as_raw_fd(),
c.as_ptr(),
&mut st,
libc::AT_SYMLINK_NOFOLLOW,
)
};
(rc == 0).then_some(st)
}
fn kind_at(dir: &File, name: &OsStr) -> Option<NodeKind> {
let st = stat_at(dir, name)?;
Some(match st.st_mode & libc::S_IFMT {
libc::S_IFDIR => NodeKind::Dir,
libc::S_IFREG => NodeKind::RegularFile,
libc::S_IFLNK => NodeKind::NotFollowable { tag: None },
_ => NodeKind::NotAFile,
})
}
pub(super) fn identity_of(file: &File) -> io::Result<NodeId> {
use std::os::unix::fs::MetadataExt;
let m = file.metadata()?;
Ok(NodeId {
volume: m.dev(),
file: m.ino() as u128,
})
}
pub(super) fn identity_at(path: &Path) -> io::Result<NodeId> {
use std::os::unix::fs::MetadataExt;
let m = std::fs::symlink_metadata(path)?;
Ok(NodeId {
volume: m.dev(),
file: m.ino() as u128,
})
}
pub(super) fn clear_nonblock(file: &File) -> io::Result<()> {
let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETFL, flags & !libc::O_NONBLOCK) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
impl DirHandle for UnixDirHandle {
type Attrs = libc::mode_t;
fn open_root(path: &Path) -> Result<Self, GuardIo> {
let c = CString::new(path.as_os_str().as_bytes())
.map_err(|_| GuardIo::io(io::Error::from(io::ErrorKind::InvalidInput)))?;
let fd = unsafe {
libc::open(
c.as_ptr(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY,
)
};
if fd < 0 {
return Err(GuardIo::last_os_error());
}
Ok(Self(unsafe { File::from_raw_fd(fd) }))
}
fn open_child_dir(&self, name: &OsStr) -> Result<Self, GuardIo> {
let c = cstr(name)?;
let fd = unsafe { libc::openat(self.0.as_raw_fd(), c.as_ptr(), DIR_FLAGS) };
if fd < 0 {
return Err(classify_failure(&self.0, name, io::Error::last_os_error()));
}
Ok(Self(unsafe { File::from_raw_fd(fd) }))
}
fn make_child_dir(&self, name: &OsStr) -> Result<(), GuardIo> {
let c = cstr(name)?;
if unsafe { libc::mkdirat(self.0.as_raw_fd(), c.as_ptr(), 0o755) } < 0 {
return Err(GuardIo::last_os_error());
}
Ok(())
}
fn open_child_file(&self, name: &OsStr, mode: OpenMode) -> Result<File, GuardIo> {
let flags = match mode {
OpenMode::File => FILE_FLAGS,
OpenMode::Dir => DIR_FLAGS,
};
let c = cstr(name)?;
let fd = unsafe { libc::openat(self.0.as_raw_fd(), c.as_ptr(), flags) };
if fd < 0 {
return Err(classify_failure(&self.0, name, io::Error::last_os_error()));
}
Ok(unsafe { File::from_raw_fd(fd) })
}
fn create_child_file(&self, name: &OsStr, excl: Excl) -> Result<File, GuardIo> {
let extra = match excl {
Excl::Truncate => libc::O_TRUNC,
Excl::MustNotExist => libc::O_EXCL,
};
let flags = libc::O_WRONLY | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW | extra;
let c = cstr(name)?;
let fd =
unsafe { libc::openat(self.0.as_raw_fd(), c.as_ptr(), flags, 0o644 as libc::c_uint) };
if fd < 0 {
return Err(classify_failure(&self.0, name, io::Error::last_os_error()));
}
Ok(unsafe { File::from_raw_fd(fd) })
}
fn rename_child(&self, from: &OsStr, to: &OsStr) -> Result<(), GuardIo> {
let (old, new) = (cstr(from)?, cstr(to)?);
if unsafe {
libc::renameat(
self.0.as_raw_fd(),
old.as_ptr(),
self.0.as_raw_fd(),
new.as_ptr(),
)
} < 0
{
return Err(GuardIo::last_os_error());
}
Ok(())
}
fn unlink_child(&self, name: &OsStr) {
let Ok(c) = cstr(name) else { return };
unsafe {
libc::unlinkat(self.0.as_raw_fd(), c.as_ptr(), 0);
}
}
fn child_kind(&self, name: &OsStr) -> Option<NodeKind> {
kind_at(&self.0, name)
}
fn child_attrs(&self, name: &OsStr) -> Option<Self::Attrs> {
Some(stat_at(&self.0, name)?.st_mode)
}
fn apply_attrs(&self, file: &File, attrs: Self::Attrs) -> io::Result<()> {
if unsafe { libc::fchmod(file.as_raw_fd(), attrs & 0o7777) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn identity(&self) -> Result<NodeId, GuardIo> {
use std::os::unix::fs::MetadataExt;
let meta = self.0.metadata().map_err(GuardIo::io)?;
Ok(NodeId {
volume: meta.dev(),
file: meta.ino() as u128,
})
}
#[cfg(target_os = "linux")]
fn resolve_beneath(&self, rel: &Path, mode: OpenMode) -> Result<Option<File>, GuardIo> {
if std::env::var("HOTL_FSGUARD_FORCE_DESCEND").is_ok_and(|v| !v.is_empty() && v != "0") {
return Ok(None);
}
let c = CString::new(rel.as_os_str().as_bytes())
.map_err(|_| GuardIo::io(io::Error::from(io::ErrorKind::InvalidInput)))?;
let flags = match mode {
OpenMode::File => FILE_FLAGS,
OpenMode::Dir => DIR_FLAGS,
};
let mut how: libc::open_how = unsafe { std::mem::zeroed() };
how.flags = (flags & !libc::O_NOFOLLOW) as u64;
how.resolve =
libc::RESOLVE_BENEATH | libc::RESOLVE_NO_MAGICLINKS | libc::RESOLVE_NO_SYMLINKS;
let ret = unsafe {
libc::syscall(
libc::SYS_openat2,
self.0.as_raw_fd(),
c.as_ptr(),
&how as *const libc::open_how,
std::mem::size_of::<libc::open_how>(),
)
};
if ret >= 0 {
return Ok(Some(unsafe {
File::from_raw_fd(ret as std::os::unix::io::RawFd)
}));
}
let err = io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::ENOSYS) | Some(libc::EPERM) => Ok(None),
Some(libc::ELOOP) | Some(libc::EXDEV) => Err(GuardIo::link(err)),
_ => Err(GuardIo::io(err)),
}
}
#[cfg(not(target_os = "linux"))]
fn resolve_beneath(&self, _rel: &Path, _mode: OpenMode) -> Result<Option<File>, GuardIo> {
Ok(None)
}
fn into_file(self) -> File {
self.0
}
fn sync_name_durability(&self) -> Result<(), GuardIo> {
self.0.sync_all().map_err(GuardIo::io)
}
}