#![allow(clippy::unnecessary_cast)]
#![allow(clippy::useless_conversion)]
use std::collections::{BTreeMap, btree_map};
use std::ffi::{CStr, CString, OsStr};
use std::fs::File;
use std::io;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::sync::Mutex;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use rfuse3::{FileType, Timestamp, raw::reply::FileAttr};
use tracing::error;
#[cfg(target_os = "macos")]
#[allow(non_camel_case_types)]
pub type stat64 = libc::stat;
#[cfg(target_os = "macos")]
pub const AT_EMPTY_PATH: i32 = 0;
#[cfg(target_os = "linux")]
pub use libc::{AT_EMPTY_PATH, stat64};
use super::inode_store::InodeId;
use super::{CURRENT_DIR_CSTR, EMPTY_CSTR, MAX_HOST_INO, PARENT_DIR_CSTR};
const VIRTUAL_INODE_FLAG: u64 = 1 << 55;
#[derive(Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq, Debug)]
struct DevMntIDPair(libc::dev_t, u64);
pub struct UniqueInodeGenerator {
dev_mntid_map: Mutex<BTreeMap<DevMntIDPair, u8>>,
next_unique_id: AtomicU8,
next_virtual_inode: AtomicU64,
}
impl Default for UniqueInodeGenerator {
fn default() -> Self {
Self::new()
}
}
impl UniqueInodeGenerator {
pub fn new() -> Self {
UniqueInodeGenerator {
dev_mntid_map: Mutex::new(Default::default()),
next_unique_id: AtomicU8::new(1),
next_virtual_inode: AtomicU64::new(1),
}
}
#[cfg(target_os = "linux")]
pub fn get_unique_inode(&self, id: &InodeId) -> io::Result<libc::ino64_t> {
self.get_unique_inode_impl(id)
}
#[cfg(target_os = "macos")]
pub fn get_unique_inode(&self, id: &InodeId) -> io::Result<libc::ino_t> {
self.get_unique_inode_impl(id)
}
fn get_unique_inode_impl(&self, id: &InodeId) -> io::Result<u64> {
let unique_id = {
let id: DevMntIDPair = DevMntIDPair(id.dev, id.mnt);
let mut id_map_guard = self.dev_mntid_map.lock().unwrap();
match id_map_guard.entry(id) {
btree_map::Entry::Occupied(v) => *v.get(),
btree_map::Entry::Vacant(v) => {
if self.next_unique_id.load(Ordering::Relaxed) == u8::MAX {
return Err(io::Error::other(
"the number of combinations of dev and mntid exceeds 255",
));
}
let next_id = self.next_unique_id.fetch_add(1, Ordering::Relaxed);
v.insert(next_id);
next_id
}
}
};
let inode = if id.ino <= MAX_HOST_INO {
id.ino
} else {
if self.next_virtual_inode.load(Ordering::Relaxed) > MAX_HOST_INO {
return Err(io::Error::other(format!(
"the virtual inode excess {MAX_HOST_INO}"
)));
}
self.next_virtual_inode.fetch_add(1, Ordering::Relaxed) | VIRTUAL_INODE_FLAG
};
Ok(((unique_id as u64) << 47) | inode)
}
#[cfg(test)]
fn decode_unique_inode(&self, inode: u64) -> io::Result<InodeId> {
use super::VFS_MAX_INO;
if inode > VFS_MAX_INO {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("the inode {inode} excess {VFS_MAX_INO}"),
));
}
let dev_mntid = (inode >> 47) as u8;
if dev_mntid == u8::MAX {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid dev and mntid {dev_mntid} excess 255"),
));
}
let mut dev: libc::dev_t = 0;
let mut mnt: u64 = 0;
let mut found = false;
let id_map_guard = self.dev_mntid_map.lock().unwrap();
for (k, v) in id_map_guard.iter() {
if *v == dev_mntid {
found = true;
dev = k.0;
mnt = k.1;
break;
}
}
if !found {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid dev and mntid {dev_mntid},there is no record in memory "),
));
}
Ok(InodeId {
ino: inode & MAX_HOST_INO,
dev,
mnt,
})
}
}
pub fn openat(
dir_fd: &impl AsRawFd,
path: &CStr,
flags: libc::c_int,
mode: u32,
) -> io::Result<File> {
let fd = if flags & libc::O_CREAT == libc::O_CREAT {
unsafe { libc::openat(dir_fd.as_raw_fd(), path.as_ptr(), flags, mode) }
} else {
unsafe { libc::openat(dir_fd.as_raw_fd(), path.as_ptr(), flags) }
};
if fd >= 0 {
Ok(unsafe { File::from_raw_fd(fd) })
} else {
Err(io::Error::last_os_error())
}
}
pub fn fd_path_cstr(fd: std::os::unix::io::RawFd) -> io::Result<CString> {
#[cfg(target_os = "linux")]
{
CString::new(format!("/proc/self/fd/{fd}"))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
#[cfg(target_os = "macos")]
{
let mut buf = [0u8; libc::MAXPATHLEN as usize];
let res = unsafe { libc::fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr()) };
if res < 0 {
return Err(io::Error::last_os_error());
}
let path = unsafe { CStr::from_ptr(buf.as_ptr() as *const libc::c_char) };
Ok(path.to_owned())
}
}
pub fn join_dir_and_name(dir: &CStr, name: &CStr) -> io::Result<CString> {
let dir_bytes = dir.to_bytes();
let name_bytes = name.to_bytes();
let mut out = Vec::with_capacity(dir_bytes.len() + 1 + name_bytes.len() + 1);
out.extend_from_slice(dir_bytes);
if !dir_bytes.ends_with(b"/") {
out.push(b'/');
}
out.extend_from_slice(name_bytes);
CString::new(out).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn reopen_fd_through_proc(
fd: &impl AsRawFd,
flags: libc::c_int,
proc_self_fd: &impl AsRawFd,
) -> io::Result<File> {
#[cfg(target_os = "macos")]
{
let mut buf = [0u8; libc::MAXPATHLEN as usize];
let res = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, buf.as_mut_ptr()) };
if res < 0 {
return Err(io::Error::last_os_error());
}
let path = unsafe { CStr::from_ptr(buf.as_ptr() as *const libc::c_char) };
let flags = flags & !libc::O_NOFOLLOW & !libc::O_CREAT & !libc::O_DIRECTORY;
openat(proc_self_fd, path, flags, 0)
}
#[cfg(target_os = "linux")]
{
let name = CString::new(format!("{}", fd.as_raw_fd()).as_str())?;
let flags = flags & !libc::O_NOFOLLOW & !libc::O_CREAT;
openat(proc_self_fd, &name, flags, 0)
}
}
pub fn stat_fd(dir: &impl AsRawFd, path: Option<&CStr>) -> io::Result<stat64> {
let pathname =
path.unwrap_or_else(|| unsafe { CStr::from_bytes_with_nul_unchecked(EMPTY_CSTR) });
let mut stat = MaybeUninit::<stat64>::zeroed();
let dir_fd = dir.as_raw_fd();
let res = match () {
#[cfg(target_os = "linux")]
() => unsafe {
libc::fstatat64(
dir_fd,
pathname.as_ptr(),
stat.as_mut_ptr(),
libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW,
)
},
#[cfg(target_os = "macos")]
() => unsafe {
if pathname.to_bytes().is_empty() {
libc::fstat(dir_fd, stat.as_mut_ptr())
} else {
libc::fstatat(
dir_fd,
pathname.as_ptr(),
stat.as_mut_ptr(),
AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW,
)
}
},
};
if res >= 0 {
Ok(unsafe { stat.assume_init() })
} else {
Err(io::Error::last_os_error())
}
}
pub fn is_safe_inode(mode: u32) -> bool {
let kind = mode & (libc::S_IFMT as u32);
kind == (libc::S_IFREG as u32) || kind == (libc::S_IFDIR as u32)
}
pub fn is_dir(mode: u32) -> bool {
(mode & (libc::S_IFMT as u32)) == (libc::S_IFDIR as u32)
}
pub fn ebadf() -> io::Error {
io::Error::from_raw_os_error(libc::EBADF)
}
pub fn einval() -> io::Error {
io::Error::from_raw_os_error(libc::EINVAL)
}
pub fn enosys() -> io::Error {
io::Error::from_raw_os_error(libc::ENOSYS)
}
#[cfg(target_os = "macos")]
pub fn is_linux_only_xattr(name: &CStr) -> bool {
let bytes = name.to_bytes();
bytes.starts_with(b"security.")
|| bytes.starts_with(b"trusted.")
|| bytes.starts_with(b"system.")
}
#[allow(unused)]
pub fn eperm() -> io::Error {
io::Error::from_raw_os_error(libc::EPERM)
}
#[allow(unused)]
pub fn convert_stat64_to_file_attr(stat: stat64) -> FileAttr {
FileAttr {
ino: stat.st_ino,
size: stat.st_size as u64,
blocks: stat.st_blocks as u64,
atime: Timestamp::new(stat.st_atime, stat.st_atime_nsec.try_into().unwrap()),
mtime: Timestamp::new(stat.st_mtime, stat.st_mtime_nsec.try_into().unwrap()),
ctime: Timestamp::new(stat.st_ctime, stat.st_ctime_nsec.try_into().unwrap()),
#[cfg(target_os = "macos")]
crtime: Timestamp::new(0, 0), kind: filetype_from_mode(stat.st_mode.into()),
perm: (stat.st_mode & 0o7777) as u16,
nlink: stat.st_nlink as u32,
uid: stat.st_uid,
gid: stat.st_gid,
rdev: stat.st_rdev as u32,
#[cfg(target_os = "macos")]
flags: 0, blksize: stat.st_blksize as u32,
}
}
pub fn filetype_from_mode(st_mode: u32) -> FileType {
let st_mode = st_mode & (libc::S_IFMT as u32);
if st_mode == (libc::S_IFIFO as u32) {
return FileType::NamedPipe;
}
if st_mode == (libc::S_IFCHR as u32) {
return FileType::CharDevice;
}
if st_mode == (libc::S_IFBLK as u32) {
return FileType::BlockDevice;
}
if st_mode == (libc::S_IFDIR as u32) {
return FileType::Directory;
}
if st_mode == (libc::S_IFREG as u32) {
return FileType::RegularFile;
}
if st_mode == (libc::S_IFLNK as u32) {
return FileType::Symlink;
}
if st_mode == (libc::S_IFSOCK as u32) {
return FileType::Socket;
}
error!("wrong st mode : {st_mode}");
unreachable!();
}
#[inline]
pub fn validate_path_component(name: &CStr) -> io::Result<()> {
match is_safe_path_component(name) {
true => Ok(()),
false => Err(io::Error::from_raw_os_error(libc::EINVAL)),
}
}
pub const SLASH_ASCII: u8 = 47;
fn is_safe_path_component(name: &CStr) -> bool {
let bytes = name.to_bytes_with_nul();
if bytes.contains(&SLASH_ASCII) {
return false;
}
!is_dot_or_dotdot(name)
}
#[inline]
fn is_dot_or_dotdot(name: &CStr) -> bool {
let bytes = name.to_bytes_with_nul();
bytes.starts_with(CURRENT_DIR_CSTR) || bytes.starts_with(PARENT_DIR_CSTR)
}
pub fn osstr_to_cstr(os_str: &OsStr) -> Result<CString, std::ffi::NulError> {
let bytes = os_str.as_bytes();
let c_string = CString::new(bytes)?;
Ok(c_string)
}
#[cfg(target_os = "linux")]
macro_rules! scoped_cred {
($name:ident, $ty:ty, $syscall_nr:expr) => {
#[derive(Debug)]
pub struct $name;
impl $name {
fn new(val: $ty) -> io::Result<Option<$name>> {
if val == 0 {
return Ok(None);
}
let res = unsafe { libc::syscall($syscall_nr, -1, val, -1) };
if res == 0 {
Ok(Some($name))
} else {
Err(io::Error::last_os_error())
}
}
}
impl Drop for $name {
fn drop(&mut self) {
let res = unsafe { libc::syscall($syscall_nr, -1, 0, -1) };
if res < 0 {
error!(
"fuse: failed to change credentials back to root: {}",
io::Error::last_os_error(),
);
}
}
}
};
}
#[cfg(target_os = "linux")]
scoped_cred!(ScopedUid, libc::uid_t, libc::SYS_setresuid);
#[cfg(target_os = "linux")]
scoped_cred!(ScopedGid, libc::gid_t, libc::SYS_setresgid);
#[cfg(target_os = "macos")]
pub struct ScopedUid;
#[cfg(target_os = "macos")]
impl ScopedUid {
fn new(_: libc::uid_t) -> io::Result<Option<Self>> {
Ok(None)
}
}
#[cfg(target_os = "macos")]
pub struct ScopedGid;
#[cfg(target_os = "macos")]
impl ScopedGid {
fn new(_: libc::gid_t) -> io::Result<Option<Self>> {
Ok(None)
}
}
pub fn set_creds(
uid: libc::uid_t,
gid: libc::gid_t,
) -> io::Result<(Option<ScopedUid>, Option<ScopedGid>)> {
ScopedGid::new(gid).and_then(|gid| Ok((ScopedUid::new(uid)?, gid)))
}
#[cfg(target_os = "macos")]
pub fn try_apfs_clonefile(src: &CStr, dst: &CStr) -> io::Result<bool> {
let res = unsafe { libc::clonefile(src.as_ptr(), dst.as_ptr(), 0) };
if res == 0 {
return Ok(true);
}
let err = io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::ENOTSUP) | Some(libc::EXDEV) => Ok(false),
_ => Err(err),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_safe_inode() {
let mut mode = (libc::S_IFDIR as u32) | 0o755;
assert!(is_safe_inode(mode));
mode = (libc::S_IFREG as u32) | 0o755;
assert!(is_safe_inode(mode));
mode = (libc::S_IFLNK as u32) | 0o755;
assert!(!is_safe_inode(mode));
mode = (libc::S_IFCHR as u32) | 0o755;
assert!(!is_safe_inode(mode));
mode = (libc::S_IFBLK as u32) | 0o755;
assert!(!is_safe_inode(mode));
mode = (libc::S_IFIFO as u32) | 0o755;
assert!(!is_safe_inode(mode));
mode = (libc::S_IFSOCK as u32) | 0o755;
assert!(!is_safe_inode(mode));
assert_eq!(
filetype_from_mode((libc::S_IFIFO as u32) | 0o755),
FileType::NamedPipe
);
assert_eq!(
filetype_from_mode((libc::S_IFCHR as u32) | 0o755),
FileType::CharDevice
);
assert_eq!(
filetype_from_mode((libc::S_IFBLK as u32) | 0o755),
FileType::BlockDevice
);
assert_eq!(
filetype_from_mode((libc::S_IFDIR as u32) | 0o755),
FileType::Directory
);
assert_eq!(
filetype_from_mode((libc::S_IFREG as u32) | 0o755),
FileType::RegularFile
);
assert_eq!(
filetype_from_mode((libc::S_IFLNK as u32) | 0o755),
FileType::Symlink
);
assert_eq!(
filetype_from_mode((libc::S_IFSOCK as u32) | 0o755),
FileType::Socket
);
}
#[test]
fn test_is_dir() {
let mode = libc::S_IFREG as u32;
assert!(!is_dir(mode));
let mode = libc::S_IFDIR as u32;
assert!(is_dir(mode));
}
#[test]
fn test_generate_unique_inode() {
{
let generator = UniqueInodeGenerator::new();
let inode_alt_key = InodeId {
ino: 1,
dev: 0,
mnt: 0,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x00800000000001);
let expect_inode_alt_key = generator.decode_unique_inode(unique_inode).unwrap();
assert_eq!(expect_inode_alt_key, inode_alt_key);
let inode_alt_key = InodeId {
ino: 1,
dev: 0,
mnt: 1,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x01000000000001);
let expect_inode_alt_key = generator.decode_unique_inode(unique_inode).unwrap();
assert_eq!(expect_inode_alt_key, inode_alt_key);
let inode_alt_key = InodeId {
ino: 2,
dev: 0,
mnt: 1,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x01000000000002);
let expect_inode_alt_key = generator.decode_unique_inode(unique_inode).unwrap();
assert_eq!(expect_inode_alt_key, inode_alt_key);
let inode_alt_key = InodeId {
ino: MAX_HOST_INO,
dev: 0,
mnt: 1,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x017fffffffffff);
let expect_inode_alt_key = generator.decode_unique_inode(unique_inode).unwrap();
assert_eq!(expect_inode_alt_key, inode_alt_key);
}
{
let generator = UniqueInodeGenerator::new();
let inode_alt_key = InodeId {
ino: MAX_HOST_INO + 1,
dev: u64::MAX as libc::dev_t,
mnt: u64::MAX,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x80800000000001);
let inode_alt_key = InodeId {
ino: MAX_HOST_INO + 2,
dev: u64::MAX as libc::dev_t,
mnt: u64::MAX,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x80800000000002);
let inode_alt_key = InodeId {
ino: MAX_HOST_INO + 3,
dev: u64::MAX as libc::dev_t,
mnt: 0,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x81000000000003);
let inode_alt_key = InodeId {
ino: u64::MAX,
dev: u64::MAX as libc::dev_t,
mnt: u64::MAX,
};
let unique_inode = generator.get_unique_inode(&inode_alt_key).unwrap();
assert_eq!(unique_inode, 0x80800000000004);
}
}
#[test]
fn test_stat_fd() {
let topdir = std::env::current_dir().unwrap();
let dir = File::open(&topdir).unwrap();
let filename = CString::new("Cargo.toml").unwrap();
let st1 = stat_fd(&dir, None).unwrap();
let st2 = stat_fd(&dir, Some(&filename)).unwrap();
assert_eq!(st1.st_dev, st2.st_dev);
assert_ne!(st1.st_ino, st2.st_ino);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_apfs_clone_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src.bin");
let dst = dir.path().join("dst.bin");
let payload: Vec<u8> = (0..(4 * 1024 * 1024)).map(|i| (i % 251) as u8).collect();
std::fs::write(&src, &payload).unwrap();
let src_c = CString::new(src.as_os_str().as_bytes()).unwrap();
let dst_c = CString::new(dst.as_os_str().as_bytes()).unwrap();
let cloned = try_apfs_clonefile(&src_c, &dst_c).expect("clone failed");
assert!(
cloned,
"macOS tempdir defaults to APFS — clone should succeed"
);
let read_back = std::fs::read(&dst).unwrap();
assert_eq!(
read_back, payload,
"clone produced different bytes than source"
);
let again = try_apfs_clonefile(&src_c, &dst_c);
assert!(
matches!(&again, Err(e) if e.raw_os_error() == Some(libc::EEXIST)),
"second clone should EEXIST, got {again:?}",
);
}
}