#![allow(clippy::useless_conversion)]
use config::{CachePolicy, Config};
#[cfg(target_os = "linux")]
use file_handle::{FileHandle, OpenableFileHandle};
#[cfg(target_os = "macos")]
use self::statx::statx_timestamp;
use futures::executor::block_on;
use inode_store::{InodeId, InodeStore};
#[cfg(target_os = "linux")]
use libc::{self, statx_timestamp};
use moka::future::Cache;
use rfuse3::{Errno, raw::reply::ReplyEntry};
use uuid::Uuid;
use crate::passthrough::mmap::{MmapCachedValue, MmapChunkKey};
use crate::util::convert_stat64_to_file_attr;
#[cfg(target_os = "linux")]
use mount_fd::MountFds;
use statx::StatExt;
use std::cmp;
use std::io::Result;
use std::ops::DerefMut;
#[cfg(target_os = "macos")]
use std::os::fd::FromRawFd;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
#[cfg(target_os = "macos")]
use std::sync::Mutex as StdMutex;
use tracing::error;
use tracing::{debug, warn};
#[cfg(target_os = "macos")]
use std::num::NonZeroUsize;
#[cfg(target_os = "macos")]
use std::sync::Weak;
use std::sync::atomic::{AtomicBool, AtomicU32};
use std::{
collections::{BTreeMap, btree_map},
ffi::{CStr, CString, OsString},
fs::File,
io::{self, Error},
marker::PhantomData,
os::{
fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
unix::ffi::OsStringExt,
},
path::PathBuf,
sync::Arc,
sync::atomic::{AtomicU64, Ordering},
time::Duration,
};
use util::{
UniqueInodeGenerator, ebadf, is_dir, openat, reopen_fd_through_proc, stat_fd,
validate_path_component,
};
use vm_memory::bitmap::BitmapSlice;
use nix::sys::resource::{Resource, getrlimit, setrlimit};
pub mod async_io;
pub mod config;
#[cfg(target_os = "linux")]
mod file_handle;
mod inode_store;
mod mmap;
#[cfg(target_os = "linux")]
mod mount_fd;
mod os_compat;
mod statx;
pub mod util;
pub const CURRENT_DIR_CSTR: &[u8] = b".\0";
pub const PARENT_DIR_CSTR: &[u8] = b"..\0";
pub const VFS_MAX_INO: u64 = 0xff_ffff_ffff_ffff;
#[cfg(target_os = "linux")]
const MOUNT_INFO_FILE: &str = "/proc/self/mountinfo";
pub const EMPTY_CSTR: &[u8] = b"\0";
#[cfg(target_os = "linux")]
pub const PROC_SELF_FD_CSTR: &[u8] = b"/proc/self/fd\0";
#[cfg(target_os = "macos")]
pub const PROC_SELF_FD_CSTR: &[u8] = b"/dev/fd\0";
pub const ROOT_ID: u64 = 1;
use tokio::sync::{Mutex, MutexGuard, RwLock};
const MIN_PASSTHROUGH_NOFILE_SOFT_LIMIT: u64 = 8192;
const RESERVED_FILE_DESCRIPTORS: u64 = 64;
#[cfg(target_os = "macos")]
fn recover_std_mutex<T>(mutex: &StdMutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[derive(Debug, Clone)]
pub struct PassthroughArgs<P, M>
where
P: AsRef<Path>,
M: AsRef<str>,
{
pub root_dir: P,
pub mapping: Option<M>,
}
pub async fn new_passthroughfs_layer<P: AsRef<Path>, M: AsRef<str>>(
args: PassthroughArgs<P, M>,
) -> Result<PassthroughFs> {
let mut config = Config {
root_dir: args.root_dir.as_ref().to_path_buf(),
xattr: true,
do_import: true,
..Default::default()
};
#[cfg(target_os = "macos")]
if !config.macos_lazy_inode_fd {
config.entry_timeout = Duration::ZERO;
config.attr_timeout = Duration::ZERO;
config.dir_entry_timeout = Some(Duration::ZERO);
config.dir_attr_timeout = Some(Duration::ZERO);
}
if let Some(mapping) = args.mapping {
config.mapping = mapping
.as_ref()
.parse()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
}
let fs = PassthroughFs::<()>::new(config)?;
#[cfg(target_os = "linux")]
if fs.cfg.do_import {
fs.import().await?;
}
#[cfg(target_os = "macos")]
{
fs.import().await?;
}
Ok(fs)
}
type Inode = u64;
type Handle = u64;
fn desired_nofile_soft_limit(soft: u64, hard: u64, minimum: u64) -> Option<u64> {
if soft >= minimum || hard <= soft {
return None;
}
Some(cmp::min(minimum, hard))
}
fn raise_nofile_soft_limit(minimum: u64) -> u64 {
let Ok((soft, hard)) = getrlimit(Resource::RLIMIT_NOFILE) else {
return minimum;
};
if let Some(target) = desired_nofile_soft_limit(soft, hard, minimum) {
match setrlimit(Resource::RLIMIT_NOFILE, target, hard) {
Ok(()) => return target,
Err(err) => {
warn!(
"passthroughfs: failed to raise RLIMIT_NOFILE from {soft} to {target}: {err}"
);
}
}
}
soft
}
const MAX_HOST_INO: u64 = 0x7fff_ffff_ffff;
#[derive(Debug)]
enum InodeFile<'a> {
#[cfg(target_os = "linux")]
Owned(File),
Ref(&'a File),
#[cfg(target_os = "macos")]
Arc(Arc<File>),
}
impl AsRawFd for InodeFile<'_> {
fn as_raw_fd(&self) -> RawFd {
match self {
#[cfg(target_os = "linux")]
Self::Owned(file) => file.as_raw_fd(),
Self::Ref(file_ref) => file_ref.as_raw_fd(),
#[cfg(target_os = "macos")]
Self::Arc(arc) => arc.as_raw_fd(),
}
}
}
impl AsFd for InodeFile<'_> {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
#[cfg(target_os = "linux")]
Self::Owned(file) => file.as_fd(),
Self::Ref(file_ref) => file_ref.as_fd(),
#[cfg(target_os = "macos")]
Self::Arc(arc) => arc.as_fd(),
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
enum InodeHandle {
File(File),
#[cfg(target_os = "linux")]
Handle(Arc<OpenableFileHandle>),
#[cfg(target_os = "macos")]
Reopenable {
state: Arc<StdMutex<ReopenableState>>,
},
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct ReopenableState {
path: PathBuf,
cached: Option<Arc<File>>,
lazy_fd_lru: Option<Arc<LazyFdLru>>,
}
#[cfg(target_os = "macos")]
pub(crate) struct LazyFdLru {
inner: StdMutex<lru::LruCache<Inode, Weak<StdMutex<ReopenableState>>>>,
reopen_count: AtomicU64,
cap: NonZeroUsize,
}
#[cfg(target_os = "macos")]
impl LazyFdLru {
fn new(cap: NonZeroUsize) -> Self {
LazyFdLru {
inner: StdMutex::new(lru::LruCache::new(cap)),
reopen_count: AtomicU64::new(0),
cap,
}
}
fn touch(&self, inode: Inode, weak: Weak<StdMutex<ReopenableState>>) {
let mut guard = recover_std_mutex(&self.inner);
if let Some((_evicted_inode, evicted_weak)) = guard.push(inode, weak) {
drop(guard);
if let Some(state) = evicted_weak.upgrade() {
let mut s = recover_std_mutex(&state);
s.cached = None;
}
}
}
fn remove(&self, inode: Inode) {
let mut guard = recover_std_mutex(&self.inner);
let _ = guard.pop(&inode);
}
pub(crate) fn reopen_count(&self) -> u64 {
self.reopen_count.load(Ordering::Relaxed)
}
pub(crate) fn cap(&self) -> usize {
self.cap.get()
}
pub(crate) fn len(&self) -> usize {
recover_std_mutex(&self.inner).len()
}
fn bump_reopen(&self) {
self.reopen_count.fetch_add(1, Ordering::Relaxed);
}
}
#[cfg(target_os = "macos")]
impl std::fmt::Debug for LazyFdLru {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LazyFdLru")
.field("cap", &self.cap.get())
.field("reopen_count", &self.reopen_count())
.finish()
}
}
impl InodeHandle {
#[cfg(target_os = "linux")]
fn file_handle(&self) -> Option<&FileHandle> {
match self {
InodeHandle::File(_) => None,
InodeHandle::Handle(h) => Some(h.file_handle()),
}
}
fn get_file(&self) -> Result<InodeFile<'_>> {
match self {
InodeHandle::File(f) => Ok(InodeFile::Ref(f)),
#[cfg(target_os = "linux")]
InodeHandle::Handle(h) => {
let f = h.open(libc::O_PATH)?;
Ok(InodeFile::Owned(f))
}
#[cfg(target_os = "macos")]
InodeHandle::Reopenable { .. } => {
#[cfg(debug_assertions)]
panic!(
"InodeHandle::get_file called on Reopenable; \
use InodeData::get_file instead"
);
#[cfg(not(debug_assertions))]
{
Err(io::Error::other(
"InodeHandle::get_file called on Reopenable; \
use InodeData::get_file instead",
))
}
}
}
}
fn open_file(&self, flags: libc::c_int, proc_self_fd: &File) -> Result<File> {
match self {
InodeHandle::File(f) => reopen_fd_through_proc(f, flags, proc_self_fd),
#[cfg(target_os = "linux")]
InodeHandle::Handle(h) => h.open(flags),
#[cfg(target_os = "macos")]
InodeHandle::Reopenable { state } => {
let mut guard = recover_std_mutex(state);
let path = CString::new(guard.path.as_os_str().as_bytes())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let fd = lazy_open_path(&path, flags)?;
let f = unsafe { File::from_raw_fd(fd) };
if guard.cached.is_none() && flags == libc::O_RDONLY {
guard.cached = Some(Arc::new(f.try_clone()?));
}
Ok(f)
}
}
}
#[cfg(target_os = "linux")]
fn stat(&self) -> Result<libc::stat64> {
self.do_stat()
}
#[cfg(target_os = "macos")]
fn stat(&self) -> Result<libc::stat> {
self.do_stat()
}
#[cfg(target_os = "linux")]
fn do_stat(&self) -> Result<libc::stat64> {
match self {
InodeHandle::File(f) => stat_fd(f, None),
InodeHandle::Handle(_h) => {
let file = self.get_file()?;
stat_fd(&file, None)
}
}
}
#[cfg(target_os = "macos")]
fn do_stat(&self) -> Result<libc::stat> {
match self {
InodeHandle::File(f) => stat_fd(f, None),
InodeHandle::Reopenable { state } => {
let path = {
let guard = recover_std_mutex(state);
CString::new(guard.path.as_os_str().as_bytes())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
};
let mut st = std::mem::MaybeUninit::<libc::stat>::zeroed();
let res = unsafe {
libc::fstatat(
libc::AT_FDCWD,
path.as_ptr(),
st.as_mut_ptr(),
libc::AT_SYMLINK_NOFOLLOW,
)
};
if res != 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { st.assume_init() })
}
}
}
}
#[derive(Debug)]
pub struct InodeData {
inode: Inode,
handle: InodeHandle,
id: InodeId,
refcount: AtomicU64,
mode: u32,
#[cfg_attr(target_os = "macos", allow(dead_code))]
btime: statx_timestamp,
}
#[cfg(target_os = "macos")]
fn lazy_open_path(path: &CStr, flags: libc::c_int) -> io::Result<libc::c_int> {
let base = (flags & !libc::O_CREAT & !libc::O_DIRECTORY) | libc::O_CLOEXEC;
let with_nofollow = base | libc::O_NOFOLLOW;
let fd = unsafe { libc::open(path.as_ptr(), with_nofollow) };
if fd >= 0 {
return Ok(fd);
}
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ELOOP) {
let symlink_flags = (base & !libc::O_NOFOLLOW) | libc::O_SYMLINK;
let fd = unsafe { libc::open(path.as_ptr(), symlink_flags) };
if fd >= 0 {
return Ok(fd);
}
return Err(io::Error::last_os_error());
}
Err(err)
}
impl InodeData {
fn new(
inode: Inode,
f: InodeHandle,
refcount: u64,
id: InodeId,
mode: u32,
btime: statx_timestamp,
) -> Self {
InodeData {
inode,
handle: f,
id,
refcount: AtomicU64::new(refcount),
mode,
btime,
}
}
fn get_file(&self) -> Result<InodeFile<'_>> {
#[cfg(target_os = "macos")]
if let InodeHandle::Reopenable { state } = &self.handle {
let mut guard = recover_std_mutex(state);
let mut touched_lru = None;
if guard.cached.is_none() {
let path = CString::new(guard.path.as_os_str().as_bytes())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let fd = lazy_open_path(&path, libc::O_RDONLY)?;
guard.cached = Some(Arc::new(unsafe { File::from_raw_fd(fd) }));
touched_lru = guard.lazy_fd_lru.clone();
}
let arc = Arc::clone(guard.cached.as_ref().unwrap());
drop(guard);
if let Some(lru) = touched_lru {
lru.bump_reopen();
lru.touch(self.inode, Arc::downgrade(state));
}
return Ok(InodeFile::Arc(arc));
}
self.handle.get_file()
}
fn open_file(&self, flags: libc::c_int, proc_self_fd: &File) -> Result<File> {
let f = self.handle.open_file(flags, proc_self_fd)?;
#[cfg(target_os = "macos")]
if let InodeHandle::Reopenable { state } = &self.handle {
let (had_cache, lru_opt) = {
let guard = recover_std_mutex(state);
(guard.cached.is_some(), guard.lazy_fd_lru.clone())
};
if had_cache && let Some(lru) = lru_opt {
if flags == libc::O_RDONLY {
lru.bump_reopen();
}
lru.touch(self.inode, Arc::downgrade(state));
}
}
Ok(f)
}
#[cfg(target_os = "macos")]
fn update_lazy_path(&self, new_path: PathBuf) {
if let InodeHandle::Reopenable { state } = &self.handle {
let mut guard = recover_std_mutex(state);
guard.path = new_path;
guard.cached = None;
}
}
#[cfg(target_os = "macos")]
fn lazy_path(&self) -> Option<PathBuf> {
match &self.handle {
InodeHandle::Reopenable { state } => Some(recover_std_mutex(state).path.clone()),
_ => None,
}
}
}
struct InodeMap {
pub inodes: RwLock<InodeStore>,
}
impl InodeMap {
fn new() -> Self {
InodeMap {
inodes: RwLock::new(Default::default()),
}
}
async fn clear(&self) {
self.inodes.write().await.clear();
}
async fn get(&self, inode: Inode) -> Result<Arc<InodeData>> {
self.inodes
.read()
.await
.get(&inode)
.cloned()
.ok_or_else(ebadf)
}
fn get_inode_locked(
inodes: &InodeStore,
#[cfg_attr(target_os = "macos", allow(unused_variables))] handle: &InodeHandle,
) -> Option<Inode> {
#[cfg(target_os = "linux")]
if let Some(h) = handle.file_handle() {
return inodes.inode_by_handle(h).copied();
}
#[cfg(target_os = "macos")]
let _ = inodes;
None
}
async fn get_alt(&self, id: &InodeId, handle: &InodeHandle) -> Option<Arc<InodeData>> {
let inodes = self.inodes.read().await;
Self::get_alt_locked(&inodes, id, handle)
}
fn get_alt_locked(
inodes: &InodeStore,
id: &InodeId,
#[cfg_attr(target_os = "macos", allow(unused_variables))] handle: &InodeHandle,
) -> Option<Arc<InodeData>> {
#[cfg(target_os = "linux")]
let by_handle = handle.file_handle().and_then(|h| inodes.get_by_handle(h));
#[cfg(target_os = "macos")]
let by_handle: Option<&Arc<InodeData>> = None;
by_handle
.or_else(|| {
inodes.get_by_id(id).filter(|_data| {
#[cfg(target_os = "linux")]
{
_data.handle.file_handle().is_none()
}
#[cfg(target_os = "macos")]
{
true
}
})
})
.cloned()
}
async fn insert(&self, data: Arc<InodeData>) {
let mut inodes = self.inodes.write().await;
Self::insert_locked(&mut inodes, data)
}
fn insert_locked(inodes: &mut InodeStore, data: Arc<InodeData>) {
inodes.insert(data);
}
}
struct HandleData {
inode: Inode,
file: File,
lock: Mutex<()>,
open_flags: AtomicU32,
}
impl HandleData {
fn new(inode: Inode, file: File, flags: u32) -> Self {
HandleData {
inode,
file,
lock: Mutex::new(()),
open_flags: AtomicU32::new(flags),
}
}
fn get_file(&self) -> &File {
&self.file
}
async fn get_file_mut(&self) -> (MutexGuard<'_, ()>, &File) {
(self.lock.lock().await, &self.file)
}
fn borrow_fd(&self) -> BorrowedFd<'_> {
self.file.as_fd()
}
async fn get_flags(&self) -> u32 {
self.open_flags.load(Ordering::Relaxed)
}
async fn set_flags(&self, flags: u32) {
self.open_flags.store(flags, Ordering::Relaxed);
}
}
struct HandleMap {
handles: RwLock<BTreeMap<Handle, Arc<HandleData>>>,
}
impl HandleMap {
fn new() -> Self {
HandleMap {
handles: RwLock::new(BTreeMap::new()),
}
}
async fn clear(&self) {
self.handles.write().await.clear();
}
async fn insert(&self, handle: Handle, data: HandleData) {
self.handles.write().await.insert(handle, Arc::new(data));
}
async fn release(&self, handle: Handle, inode: Inode) -> Result<()> {
let mut handles = self.handles.write().await;
if let btree_map::Entry::Occupied(e) = handles.entry(handle)
&& e.get().inode == inode
{
e.remove();
return Ok(());
}
Err(ebadf())
}
async fn get(&self, handle: Handle, inode: Inode) -> Result<Arc<HandleData>> {
self.handles
.read()
.await
.get(&handle)
.filter(|hd| hd.inode == inode)
.cloned()
.ok_or_else(ebadf)
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Hash, Eq, PartialEq)]
struct FileUniqueKey(u64, statx_timestamp);
pub struct PassthroughFs<S: BitmapSlice + Send + Sync = ()> {
inode_map: InodeMap,
next_inode: AtomicU64,
handle_map: HandleMap,
next_handle: AtomicU64,
ino_allocator: UniqueInodeGenerator,
#[cfg(target_os = "linux")]
mount_fds: MountFds,
proc_self_fd: File,
writeback: AtomicBool,
no_open: AtomicBool,
no_opendir: AtomicBool,
no_readdir: AtomicBool,
seal_size: AtomicBool,
dir_entry_timeout: Duration,
dir_attr_timeout: Duration,
cfg: Config,
_uuid: Uuid,
phantom: PhantomData<S>,
#[cfg(target_os = "linux")]
handle_cache: Cache<FileUniqueKey, Arc<FileHandle>>,
mmap_chunks: Cache<MmapChunkKey, Arc<RwLock<mmap::MmapCachedValue>>>,
#[cfg(target_os = "macos")]
lazy_fd_lru: Option<Arc<LazyFdLru>>,
}
impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
pub fn new(mut cfg: Config) -> Result<PassthroughFs<S>> {
if cfg.no_open && cfg.cache_policy != CachePolicy::Always {
warn!("passthroughfs: no_open only work with cache=always, reset to open mode");
cfg.no_open = false;
}
if cfg.writeback && cfg.cache_policy == CachePolicy::Never {
warn!(
"passthroughfs: writeback cache conflicts with cache=none, reset to no_writeback"
);
cfg.writeback = false;
}
#[cfg(target_os = "macos")]
if cfg.macos_lazy_inode_fd {
cfg.root_dir = std::fs::canonicalize(&cfg.root_dir)?;
}
let proc_self_fd_cstr = unsafe { CStr::from_bytes_with_nul_unchecked(PROC_SELF_FD_CSTR) };
#[cfg(target_os = "linux")]
let flags = libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC;
#[cfg(target_os = "macos")]
let flags = libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC;
let proc_self_fd = Self::open_file(&libc::AT_FDCWD, proc_self_fd_cstr, flags, 0)?;
let (dir_entry_timeout, dir_attr_timeout) =
match (cfg.dir_entry_timeout, cfg.dir_attr_timeout) {
(Some(e), Some(a)) => (e, a),
(Some(e), None) => (e, cfg.attr_timeout),
(None, Some(a)) => (cfg.entry_timeout, a),
(None, None) => (cfg.entry_timeout, cfg.attr_timeout),
};
#[cfg(target_os = "linux")]
let mount_fds = MountFds::new(None)?;
let fd_limit = raise_nofile_soft_limit(MIN_PASSTHROUGH_NOFILE_SOFT_LIMIT);
#[cfg(target_os = "macos")]
let lazy_fd_lru: Option<Arc<LazyFdLru>> = if cfg.macos_lazy_inode_fd {
let cap = match cfg.macos_lazy_fd_lru_max {
Some(n) => n,
None => {
let auto = fd_limit.saturating_sub(RESERVED_FILE_DESCRIPTORS).max(2) / 2;
NonZeroUsize::new(auto.try_into().unwrap_or(usize::MAX))
.unwrap_or(NonZeroUsize::new(1).unwrap())
}
};
Some(Arc::new(LazyFdLru::new(cap)))
} else {
None
};
let max_mmap_size = if cfg.use_mmap { cfg.max_mmap_size } else { 0 };
let mmap_cache_builder = Cache::builder()
.max_capacity(max_mmap_size)
.weigher(
|_key: &MmapChunkKey, value: &Arc<RwLock<mmap::MmapCachedValue>>| -> u32 {
let guard = block_on(value.read());
match &*guard {
MmapCachedValue::Mmap(mmap) => mmap.len() as u32,
MmapCachedValue::MmapMut(mmap_mut) => mmap_mut.len() as u32,
}
},
)
.time_to_idle(Duration::from_millis(60));
Ok(PassthroughFs {
inode_map: InodeMap::new(),
next_inode: AtomicU64::new(ROOT_ID + 1),
ino_allocator: UniqueInodeGenerator::new(),
handle_map: HandleMap::new(),
next_handle: AtomicU64::new(1),
#[cfg(target_os = "linux")]
mount_fds,
proc_self_fd,
writeback: AtomicBool::new(false),
no_open: AtomicBool::new(false),
no_opendir: AtomicBool::new(false),
no_readdir: AtomicBool::new(cfg.no_readdir),
seal_size: AtomicBool::new(cfg.seal_size),
dir_entry_timeout,
dir_attr_timeout,
cfg,
_uuid: Uuid::new_v4(),
phantom: PhantomData,
#[cfg(target_os = "linux")]
handle_cache: moka::future::Cache::new(
fd_limit.saturating_sub(RESERVED_FILE_DESCRIPTORS).max(1),
),
mmap_chunks: mmap_cache_builder.build(),
#[cfg(target_os = "macos")]
lazy_fd_lru,
})
}
#[cfg(target_os = "macos")]
pub fn macos_lazy_fd_reopen_count(&self) -> Option<u64> {
self.lazy_fd_lru.as_ref().map(|l| l.reopen_count())
}
#[cfg(target_os = "macos")]
pub fn macos_lazy_fd_cache_len(&self) -> Option<usize> {
self.lazy_fd_lru.as_ref().map(|l| l.len())
}
#[cfg(target_os = "macos")]
pub fn macos_lazy_fd_cap(&self) -> Option<usize> {
self.lazy_fd_lru.as_ref().map(|l| l.cap())
}
pub async fn passthrough_host_path(&self, inode: Inode) -> Option<PathBuf> {
let data = self.inode_map.get(inode).await.ok()?;
#[cfg(target_os = "macos")]
if let Some(p) = data.lazy_path() {
return Some(p);
}
let file = data.get_file().ok()?;
let cstr = util::fd_path_cstr(file.as_raw_fd()).ok()?;
Some(PathBuf::from(std::ffi::OsStr::from_bytes(cstr.to_bytes())))
}
pub async fn import(&self) -> Result<()> {
let root = CString::new(self.cfg.root_dir.as_os_str().as_bytes())
.map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
let (handle, st) = Self::open_file_and_handle(
self,
&libc::AT_FDCWD,
&root,
#[cfg(target_os = "macos")]
Some(self.cfg.root_dir.clone()),
)
.await
.map_err(|e| {
error!("fuse: import: failed to get file or handle: {e:?}");
e
})?;
let id = InodeId::from_stat(&st);
unsafe { libc::umask(0o000) };
self.inode_map
.insert(Arc::new(InodeData::new(
ROOT_ID,
handle,
2,
id,
st.st.st_mode.into(),
st.btime
.ok_or_else(|| io::Error::other("birth time not available"))?,
)))
.await;
Ok(())
}
pub fn keep_fds(&self) -> Vec<RawFd> {
vec![self.proc_self_fd.as_raw_fd()]
}
pub fn config(&self) -> &Config {
&self.cfg
}
fn readlinkat(dfd: i32, pathname: &CStr) -> Result<PathBuf> {
let mut buf = Vec::with_capacity(libc::PATH_MAX as usize);
let buf_read = unsafe {
libc::readlinkat(
dfd,
pathname.as_ptr(),
buf.as_mut_ptr() as *mut libc::c_char,
buf.capacity(),
)
};
if buf_read < 0 {
error!("fuse: readlinkat error");
return Err(Error::last_os_error());
}
unsafe { buf.set_len(buf_read as usize) };
buf.shrink_to_fit();
Ok(PathBuf::from(OsString::from_vec(buf)))
}
pub async fn readlinkat_proc_file(&self, inode: Inode) -> Result<PathBuf> {
let data = self.inode_map.get(inode).await?;
let file = data.get_file()?;
let pathname = CString::new(format!("{}", file.as_raw_fd()))
.map_err(|e| Error::new(io::ErrorKind::InvalidData, e))?;
Self::readlinkat(self.proc_self_fd.as_raw_fd(), &pathname)
}
fn create_file_excl(
dir: &impl AsRawFd,
pathname: &CStr,
flags: i32,
mode: u32,
) -> io::Result<Option<File>> {
match openat(dir, pathname, flags | libc::O_CREAT | libc::O_EXCL, mode) {
Ok(file) => Ok(Some(file)),
Err(err) => {
if err.kind() == io::ErrorKind::AlreadyExists {
if (flags & libc::O_EXCL) != 0 {
return Err(err);
}
return Ok(None);
}
Err(err)
}
}
}
fn open_file(dfd: &impl AsRawFd, pathname: &CStr, flags: i32, mode: u32) -> io::Result<File> {
openat(dfd, pathname, flags, mode)
}
fn open_file_restricted(
&self,
dir: &impl AsRawFd,
pathname: &CStr,
flags: i32,
mode: u32,
) -> io::Result<File> {
let flags = libc::O_NOFOLLOW | libc::O_CLOEXEC | flags;
#[cfg(target_os = "macos")]
{
match openat(dir, pathname, flags, mode) {
Err(err) if err.raw_os_error() == Some(libc::ELOOP) => {
let symlink_flags = (flags & !libc::O_NOFOLLOW) | libc::O_SYMLINK;
openat(dir, pathname, symlink_flags, mode)
}
result => result,
}
}
#[cfg(not(target_os = "macos"))]
{
openat(dir, pathname, flags, mode)
}
}
async fn open_file_and_handle(
&self,
dir: &impl AsRawFd,
name: &CStr,
#[cfg(target_os = "macos")] lazy_abs_path: Option<PathBuf>,
) -> io::Result<(InodeHandle, StatExt)> {
#[cfg(target_os = "macos")]
if self.cfg.macos_lazy_inode_fd
&& let Some(abs_path) = lazy_abs_path
{
let st = statx::statx(dir, Some(name))?;
return Ok((
InodeHandle::Reopenable {
state: Arc::new(StdMutex::new(ReopenableState {
path: abs_path,
cached: None,
lazy_fd_lru: self.lazy_fd_lru.clone(),
})),
},
st,
));
}
#[cfg(target_os = "linux")]
{
let path_file = self.open_file_restricted(dir, name, libc::O_PATH, 0)?;
let st = statx::statx(&path_file, None)?;
let btime_is_valid = match st.btime {
Some(ts) => ts.tv_sec != 0 || ts.tv_nsec != 0,
None => false,
};
if btime_is_valid {
let key = FileUniqueKey(st.st.st_ino, st.btime.unwrap());
let cache = self.handle_cache.clone();
if let Some(h) = cache.get(&key).await {
let openable = self.to_openable_handle(h)?;
Ok((InodeHandle::Handle(openable), st))
} else if let Some(handle_from_fd) = FileHandle::from_fd(&path_file)? {
let handle_arc = Arc::new(handle_from_fd);
cache.insert(key, Arc::clone(&handle_arc)).await;
let openable = self.to_openable_handle(handle_arc)?;
Ok((InodeHandle::Handle(openable), st))
} else {
Ok((InodeHandle::File(path_file), st))
}
} else if let Some(handle_from_fd) = FileHandle::from_fd(&path_file)? {
let handle_arc = Arc::new(handle_from_fd);
let openable = self.to_openable_handle(handle_arc)?;
Ok((InodeHandle::Handle(openable), st))
} else {
Ok((InodeHandle::File(path_file), st))
}
}
#[cfg(target_os = "macos")]
{
let path_file = self.open_file_restricted(dir, name, libc::O_RDONLY, 0)?;
let st = statx::statx(&path_file, None)?;
Ok((InodeHandle::File(path_file), st))
}
}
#[cfg(target_os = "linux")]
fn to_openable_handle(&self, fh: Arc<FileHandle>) -> io::Result<Arc<OpenableFileHandle>> {
(*Arc::as_ref(&fh))
.clone()
.into_openable(&self.mount_fds, |fd, flags, _mode| {
reopen_fd_through_proc(&fd, flags, &self.proc_self_fd)
})
.map(Arc::new)
.map_err(|e| {
if !e.silent() {
error!("{e}");
}
e.into_inner()
})
}
async fn allocate_inode(
&self,
inodes: &InodeStore,
id: &InodeId,
handle: &InodeHandle,
) -> io::Result<Inode> {
if !self.cfg.use_host_ino {
match InodeMap::get_inode_locked(inodes, handle) {
Some(a) => Ok(a),
None => Ok(self.next_inode.fetch_add(1, Ordering::Relaxed)),
}
} else {
let inode = if id.ino > MAX_HOST_INO {
match InodeMap::get_inode_locked(inodes, handle) {
Some(ino) => ino,
None => self.ino_allocator.get_unique_inode(id)?,
}
} else {
self.ino_allocator.get_unique_inode(id)?
};
Ok(inode)
}
}
async fn do_lookup(
&self,
parent: Inode,
name: &CStr,
) -> std::result::Result<ReplyEntry, Errno> {
let name = if parent == ROOT_ID && name.to_bytes_with_nul().starts_with(PARENT_DIR_CSTR) {
CStr::from_bytes_with_nul(CURRENT_DIR_CSTR).unwrap()
} else {
name
};
let dir = self.inode_map.get(parent).await?;
let dir_file = dir.get_file()?;
#[cfg(target_os = "macos")]
let lazy_abs_path = if self.cfg.macos_lazy_inode_fd {
dir.lazy_path().map(|parent_path| {
let name_os = std::ffi::OsStr::from_bytes(name.to_bytes());
parent_path.join(name_os)
})
} else {
None
};
let (inode_handle, st) = self
.open_file_and_handle(
&dir_file,
name,
#[cfg(target_os = "macos")]
lazy_abs_path,
)
.await?;
let id = InodeId::from_stat(&st);
debug!(
"do_lookup: parent: {}, name: {}, handle: {:?}, id: {:?}",
parent,
name.to_string_lossy(),
inode_handle,
id
);
let mut found = None;
'search: loop {
match self.inode_map.get_alt(&id, &inode_handle).await {
None => break 'search,
Some(data) => {
let curr = data.refcount.load(Ordering::Acquire);
if curr == 0 {
continue 'search;
}
let new = curr.saturating_add(1);
if data
.refcount
.compare_exchange(curr, new, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
found = Some(data.inode);
break;
}
}
}
}
let inode = if let Some(v) = found {
v
} else {
let mut inodes = self.inode_map.inodes.write().await;
match InodeMap::get_alt_locked(&inodes, &id, &inode_handle) {
Some(data) => {
data.refcount.fetch_add(1, Ordering::Relaxed);
data.inode
}
None => {
let inode = self.allocate_inode(&inodes, &id, &inode_handle).await?;
if inode > VFS_MAX_INO {
error!("fuse: max inode number reached: {VFS_MAX_INO}");
return Err(io::Error::other(format!(
"max inode number reached: {VFS_MAX_INO}"
))
.into());
}
InodeMap::insert_locked(
inodes.deref_mut(),
Arc::new(InodeData::new(
inode,
inode_handle,
1,
id,
st.st.st_mode.into(),
st.btime
.ok_or_else(|| io::Error::other("birth time not available"))?,
)),
);
inode
}
}
};
let (entry_timeout, _) = if is_dir(st.st.st_mode.into()) {
(self.dir_entry_timeout, self.dir_attr_timeout)
} else {
(self.cfg.entry_timeout, self.cfg.attr_timeout)
};
let mut attr_temp = convert_stat64_to_file_attr(st.st);
attr_temp.ino = inode;
attr_temp.uid = self.cfg.mapping.find_mapping(attr_temp.uid, true, true);
attr_temp.gid = self.cfg.mapping.find_mapping(attr_temp.gid, true, false);
Ok(ReplyEntry {
ttl: entry_timeout,
attr: attr_temp,
generation: 0,
})
}
async fn forget_one(&self, inodes: &mut InodeStore, inode: Inode, count: u64) {
if inode == ROOT_ID {
return;
}
if let Some(data) = inodes.get(&inode) {
loop {
let curr = data.refcount.load(Ordering::Acquire);
let new = curr.saturating_sub(count);
if data
.refcount
.compare_exchange(curr, new, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
if new == 0 {
#[cfg(target_os = "linux")]
if data.handle.file_handle().is_some()
&& (data.btime.tv_sec != 0 || data.btime.tv_nsec != 0)
{
let key = FileUniqueKey(data.id.ino, data.btime);
let cache = self.handle_cache.clone();
cache.invalidate(&key).await;
}
#[cfg(target_os = "macos")]
if let Some(lru) = self.lazy_fd_lru.as_ref() {
lru.remove(inode);
}
let keep_mapping = !self.cfg.use_host_ino || data.id.ino > MAX_HOST_INO;
inodes.remove(&inode, keep_mapping);
}
break;
}
}
}
}
async fn do_release(&self, inode: Inode, handle: Handle) -> io::Result<()> {
self.handle_map.release(handle, inode).await
}
fn validate_path_component(&self, name: &CStr) -> io::Result<()> {
if !self.cfg.do_import {
return Ok(());
}
validate_path_component(name)
}
async fn get_writeback_open_flags(&self, flags: i32) -> i32 {
let mut new_flags = flags;
let writeback = self.writeback.load(Ordering::Relaxed);
if writeback && flags & libc::O_ACCMODE == libc::O_WRONLY {
new_flags &= !libc::O_ACCMODE;
new_flags |= libc::O_RDWR;
}
if writeback && flags & libc::O_APPEND != 0 {
new_flags &= !libc::O_APPEND;
}
new_flags
}
async fn get_mmap(
&self,
inode: Inode,
offset: u64,
file: &File,
) -> Option<(Arc<RwLock<mmap::MmapCachedValue>>, u64)> {
let file_size = file.metadata().unwrap().len();
let key = MmapChunkKey::new(inode, offset, file_size);
let aligned_offset = key.aligned_offset;
if let Some(cached) = self.mmap_chunks.get(&key).await {
let guard = cached.read().await;
let cache_len = match &*guard {
MmapCachedValue::Mmap(mmap) => mmap.len() as u64,
MmapCachedValue::MmapMut(mmap_mut) => mmap_mut.len() as u64,
};
if offset < key.aligned_offset + cache_len {
return Some((cached.clone(), key.aligned_offset));
}
}
let mmap = match mmap::create_mmap(offset, file).await {
Ok(v) => v,
Err(e) => {
error!("Failed to create mmap:{e}");
return None;
}
};
self.mmap_chunks.insert(key, mmap.clone()).await;
Some((mmap, aligned_offset))
}
async fn read_from_mmap(
&self,
inode: Inode,
offset: u64,
size: u64,
file: &File,
buf: &mut [u8],
) -> Result<usize> {
if buf.len() < size as usize {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Buffer too small: {} < {}", buf.len(), size),
));
}
let file_size = file.metadata()?.len();
if offset >= file_size {
return Ok(0); }
let max_readable = file_size - offset;
let actual_size = cmp::min(size, max_readable) as usize;
let mut len = actual_size;
let mut current_offset = offset;
let mut buf_offset = 0;
while len > 0 {
let (chunk, chunk_start_offset) = match self.get_mmap(inode, current_offset, file).await
{
Some((chunk, aligned_offset)) => (chunk, aligned_offset),
None => {
return Err(std::io::Error::other("Failed to get mmap chunk"));
}
};
let chunk_guard = chunk.read().await;
match &*chunk_guard {
MmapCachedValue::Mmap(mmap) => {
let chunk_len = mmap.len();
let copy_start = (current_offset - chunk_start_offset) as usize;
let remaining_in_chunk = chunk_len - copy_start;
let copy_len = cmp::min(len, remaining_in_chunk);
let copy_len = cmp::min(copy_len, buf.len() - buf_offset);
if copy_len == 0 {
break; }
buf[buf_offset..buf_offset + copy_len]
.copy_from_slice(&mmap[copy_start..copy_start + copy_len]);
buf_offset += copy_len;
len -= copy_len;
current_offset += copy_len as u64;
}
MmapCachedValue::MmapMut(mmap_mut) => {
let chunk_len = mmap_mut.len();
let copy_start = (current_offset - chunk_start_offset) as usize;
let remaining_in_chunk = chunk_len - copy_start;
let copy_len = cmp::min(len, remaining_in_chunk);
let copy_len = cmp::min(copy_len, buf.len() - buf_offset);
if copy_len == 0 {
break; }
buf[buf_offset..buf_offset + copy_len]
.copy_from_slice(&mmap_mut[copy_start..copy_start + copy_len]);
buf_offset += copy_len;
len -= copy_len;
current_offset += copy_len as u64;
}
}
}
Ok(buf_offset)
}
async fn write_to_mmap(
&self,
inode: Inode,
offset: u64,
data: &[u8],
file: &File,
) -> Result<usize> {
let file_size = file.metadata()?.len();
let len = data.len();
if offset + len as u64 > file_size {
let raw_fd = file.as_raw_fd();
let res = unsafe { libc::ftruncate(raw_fd, (offset + len as u64) as i64) };
if res < 0 {
return Err(std::io::Error::other("error to ftruncate"));
}
self.invalidate_mmap_cache(inode, file_size).await;
}
let mut remaining = len;
let mut current_offset = offset;
let mut data_offset = 0;
while remaining > 0 {
let (chunk, chunk_start_offset) = match self.get_mmap(inode, current_offset, file).await
{
Some((chunk, aligned_offset)) => (chunk, aligned_offset),
None => {
return Err(std::io::Error::other("Failed to get mmap chunk"));
}
};
let mut chunk_guard = chunk.write().await;
match &mut *chunk_guard {
MmapCachedValue::Mmap(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Cannot write to read-only mmap",
));
}
MmapCachedValue::MmapMut(mmap_mut) => {
let chunk_len = mmap_mut.len();
let copy_start = (current_offset - chunk_start_offset) as usize;
let remaining_in_chunk = chunk_len - copy_start;
let copy_len = cmp::min(remaining, remaining_in_chunk);
let copy_len = cmp::min(copy_len, data.len() - data_offset);
if copy_len == 0 {
break; }
mmap_mut[copy_start..copy_start + copy_len]
.copy_from_slice(&data[data_offset..data_offset + copy_len]);
data_offset += copy_len;
remaining -= copy_len;
current_offset += copy_len as u64;
mmap_mut.flush_async_range(copy_start, copy_len)?;
}
}
}
Ok(data_offset)
}
async fn invalidate_mmap_cache(&self, inode: Inode, old_size: u64) {
let keys_to_remove: Vec<_> = self
.mmap_chunks
.iter()
.filter(|item| {
let key = item.0.clone();
key.inode == inode && key.aligned_offset + mmap::MAX_WINDOW_SIZE as u64 >= old_size
})
.collect();
for item in keys_to_remove {
self.mmap_chunks.invalidate(item.0.as_ref()).await;
}
}
}
#[cfg(test)]
#[allow(unused_imports)]
#[allow(clippy::useless_conversion)]
mod tests {
use crate::{
passthrough::{PassthroughArgs, PassthroughFs, ROOT_ID, new_passthroughfs_layer},
unwrap_or_skip_eperm, unwrap_or_skip_mount_error,
};
use std::ffi::{CStr, OsStr, OsString};
use nix::unistd::{Gid, Uid, getgid, getuid};
use rfuse3::{
MountOptions,
raw::{Filesystem, Request, Session},
};
macro_rules! pass {
() => {
()
};
($($tt:tt)*) => {
()
};
}
#[test]
fn nofile_limit_raise_is_capped_by_hard_limit() {
assert_eq!(
super::desired_nofile_soft_limit(256, 4096, 8192),
Some(4096)
);
assert_eq!(
super::desired_nofile_soft_limit(256, 16384, 8192),
Some(8192)
);
assert_eq!(super::desired_nofile_soft_limit(8192, 16384, 8192), None);
}
#[cfg(target_os = "macos")]
struct MacFuseMountCleanup {
mount_dir: std::path::PathBuf,
}
#[cfg(target_os = "macos")]
impl Drop for MacFuseMountCleanup {
fn drop(&mut self) {
let _ = std::process::Command::new("umount")
.arg(&self.mount_dir)
.status();
let _ = std::process::Command::new("diskutil")
.arg("unmount")
.arg("force")
.arg(&self.mount_dir)
.status();
}
}
#[tokio::test]
async fn test_passthrough() {
if std::env::var("RUN_MACFUSE_TESTS").ok().as_deref() != Some("1") {
eprintln!("skip test_passthrough: RUN_MACFUSE_TESTS!=1");
return;
}
let temp_dir = tempfile::tempdir().expect("tempdir");
let source_dir = temp_dir.path().join("src");
let mount_dir = temp_dir.path().join("mnt");
std::fs::create_dir_all(&source_dir).expect("create source dir");
std::fs::create_dir_all(&mount_dir).expect("create mount dir");
#[cfg(target_os = "macos")]
let _cleanup = MacFuseMountCleanup {
mount_dir: mount_dir.clone(),
};
let args = PassthroughArgs {
root_dir: source_dir.clone(),
mapping: None::<&str>,
};
let fs = match super::new_passthroughfs_layer(args).await {
Ok(fs) => fs,
Err(e) => {
eprintln!("skip test_passthrough: init failed: {e:?}");
return;
}
};
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
let mut mount_options = MountOptions::default();
#[cfg(target_os = "linux")]
mount_options.force_readdir_plus(true);
mount_options.uid(uid).gid(gid);
let mount_path = OsString::from(mount_dir.as_os_str());
let session = Session::new(mount_options);
let mount_handle = unwrap_or_skip_mount_error!(
session.mount(fs, mount_path).await,
"mount passthrough fs"
);
let _ = mount_handle.unmount().await; }
#[tokio::test]
async fn lookup_rejects_nul_name_without_panicking() {
use rfuse3::raw::{Filesystem, Request};
use std::os::unix::ffi::OsStrExt;
let temp_dir = tempfile::tempdir().unwrap();
let fs = new_passthroughfs_layer(PassthroughArgs {
root_dir: temp_dir.path(),
mapping: None::<&str>,
})
.await
.unwrap();
let err = fs
.lookup(Request::default(), ROOT_ID, OsStr::from_bytes(b"bad\0name"))
.await
.unwrap_err();
let ioerr = std::io::Error::from(err);
assert_eq!(ioerr.raw_os_error(), Some(libc::EINVAL));
}
#[cfg(target_os = "macos")]
#[test]
fn macos_lazy_new_canonicalizes_root_dir() {
use super::Config;
use std::os::unix::fs::symlink;
let temp_dir = tempfile::tempdir().unwrap();
let real_root = temp_dir.path().join("real-root");
let link_root = temp_dir.path().join("link-root");
std::fs::create_dir(&real_root).unwrap();
symlink(&real_root, &link_root).unwrap();
let cfg = Config {
root_dir: link_root.clone(),
macos_lazy_inode_fd: true,
..Default::default()
};
let fs = PassthroughFs::<()>::new(cfg).expect("new fs");
assert_eq!(fs.cfg.root_dir, real_root.canonicalize().unwrap());
assert_ne!(fs.cfg.root_dir, link_root);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_lookup_symlink_entry_does_not_return_eloop() {
use std::os::unix::fs::symlink;
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(temp_dir.path().join("target.txt"), "target").unwrap();
symlink("target.txt", temp_dir.path().join("link.txt")).unwrap();
let fs = new_passthroughfs_layer(PassthroughArgs {
root_dir: temp_dir.path(),
mapping: None::<&str>,
})
.await
.unwrap();
let name = c"link.txt";
let entry = fs.do_lookup(ROOT_ID, name).await.unwrap();
assert_eq!(entry.attr.kind, rfuse3::FileType::Symlink);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_lazy_open_path_two_step_works() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("file.txt"), b"PR93").unwrap();
symlink("file.txt", temp.path().join("link.txt")).unwrap();
let file_c = CString::new(temp.path().join("file.txt").as_os_str().as_bytes()).unwrap();
let fd = super::lazy_open_path(&file_c, libc::O_RDONLY).expect("regular open failed");
assert!(fd >= 0);
unsafe { libc::close(fd) };
let link_c = CString::new(temp.path().join("link.txt").as_os_str().as_bytes()).unwrap();
let fd = super::lazy_open_path(&link_c, libc::O_RDONLY).expect("symlink retry path failed");
assert!(fd >= 0);
unsafe { libc::close(fd) };
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_lazy_dir_rename_rewrites_descendants() {
use super::Config;
use rfuse3::raw::Request;
use std::ffi::OsStr;
let temp_dir = tempfile::tempdir().unwrap();
std::fs::create_dir(temp_dir.path().join("a")).unwrap();
std::fs::create_dir(temp_dir.path().join("a/sub")).unwrap();
std::fs::write(temp_dir.path().join("a/sub/file.txt"), b"hi").unwrap();
let cfg = Config {
root_dir: temp_dir.path().to_path_buf(),
xattr: true,
do_import: true,
macos_lazy_inode_fd: true,
..Default::default()
};
let fs = PassthroughFs::<()>::new(cfg).expect("new fs");
fs.import().await.unwrap();
let a_entry = fs.do_lookup(ROOT_ID, c"a").await.unwrap();
let sub_entry = fs.do_lookup(a_entry.attr.ino, c"sub").await.unwrap();
let file_entry = fs.do_lookup(sub_entry.attr.ino, c"file.txt").await.unwrap();
use rfuse3::raw::Filesystem;
fs.rename(
Request::default(),
ROOT_ID,
OsStr::new("a"),
ROOT_ID,
OsStr::new("b"),
)
.await
.unwrap();
let new_root = temp_dir.path().canonicalize().unwrap();
for ino in [a_entry.attr.ino, sub_entry.attr.ino, file_entry.attr.ino] {
let data = fs.inode_map.get(ino).await.unwrap();
let path = data.lazy_path().expect("Reopenable on macOS lazy mode");
assert!(
path.starts_with(new_root.join("b")),
"inode {ino} path {path:?} should be under {:?} after rename",
new_root.join("b"),
);
}
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_lazy_fd_lru_bounds_cache() {
use super::Config;
use std::num::NonZeroUsize;
let temp_dir = tempfile::tempdir().unwrap();
for i in 0..4 {
std::fs::write(temp_dir.path().join(format!("f{i}.txt")), b"x").unwrap();
}
let cfg = Config {
root_dir: temp_dir.path().to_path_buf(),
xattr: true,
do_import: true,
macos_lazy_inode_fd: true,
macos_lazy_fd_lru_max: Some(NonZeroUsize::new(2).unwrap()),
..Default::default()
};
let fs = PassthroughFs::<()>::new(cfg).expect("new fs");
fs.import().await.unwrap();
assert_eq!(fs.macos_lazy_fd_cap(), Some(2));
for i in 0..4 {
let name = OsString::from(format!("f{i}.txt"));
let bytes: Vec<u8> = name
.as_os_str()
.as_encoded_bytes()
.iter()
.copied()
.chain(std::iter::once(0))
.collect();
let cname = CStr::from_bytes_with_nul(&bytes).unwrap();
let entry = fs.do_lookup(ROOT_ID, cname).await.unwrap();
let inode = entry.attr.ino;
let data = fs.inode_map.get(inode).await.unwrap();
let _ = data.get_file().unwrap();
}
let len = fs.macos_lazy_fd_cache_len().expect("lru enabled");
let reopens = fs.macos_lazy_fd_reopen_count().expect("lru enabled");
assert!(
len <= 2,
"cache length {len} exceeded cap 2 — LRU eviction is broken",
);
assert!(
reopens >= 4,
"expected ≥4 reopens, saw {reopens} — counter not bumping",
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_lazy_fd_pressure_caps_real_fds() {
use super::Config;
use std::num::NonZeroUsize;
const FILES: usize = 200;
const CAP: usize = 8;
const FD_SLACK: usize = 32;
let temp_dir = tempfile::tempdir().unwrap();
for i in 0..FILES {
std::fs::write(temp_dir.path().join(format!("f{i:04}.txt")), b"x").unwrap();
}
let cfg = Config {
root_dir: temp_dir.path().to_path_buf(),
xattr: true,
do_import: true,
macos_lazy_inode_fd: true,
macos_lazy_fd_lru_max: Some(NonZeroUsize::new(CAP).unwrap()),
..Default::default()
};
let fs = PassthroughFs::<()>::new(cfg).expect("new fs");
fs.import().await.unwrap();
let baseline_fds = count_open_fds();
let mut inodes = Vec::with_capacity(FILES);
for i in 0..FILES {
let name = format!("f{i:04}.txt");
let bytes: Vec<u8> = name
.as_bytes()
.iter()
.copied()
.chain(std::iter::once(0))
.collect();
let cname = CStr::from_bytes_with_nul(&bytes).unwrap();
let entry = fs.do_lookup(ROOT_ID, cname).await.unwrap();
let inode = entry.attr.ino;
let data = fs.inode_map.get(inode).await.unwrap();
let _ = data.get_file().unwrap();
inodes.push(inode);
}
let after_lookup_fds = count_open_fds();
let cache_len = fs.macos_lazy_fd_cache_len().unwrap();
let reopens = fs.macos_lazy_fd_reopen_count().unwrap();
assert_eq!(
cache_len, CAP,
"cache should saturate at cap={CAP}, saw {cache_len}",
);
assert!(
reopens as usize >= FILES,
"expected ≥ {FILES} reopens (one per lookup), saw {reopens}",
);
assert!(
after_lookup_fds <= baseline_fds + CAP + FD_SLACK,
"fd usage exploded: baseline={baseline_fds}, after={after_lookup_fds}, \
cap={CAP}, slack={FD_SLACK}",
);
let mut store = fs.inode_map.inodes.write().await;
for inode in &inodes {
fs.forget_one(&mut store, *inode, 1).await;
}
drop(store);
let after_forget_fds = count_open_fds();
let final_cache_len = fs.macos_lazy_fd_cache_len().unwrap();
assert_eq!(
final_cache_len, 0,
"LRU should drain after forget-all, saw {final_cache_len}",
);
assert!(
after_forget_fds <= baseline_fds + FD_SLACK,
"fd usage didn't drop after forget-all: baseline={baseline_fds}, \
after_forget={after_forget_fds}, slack={FD_SLACK}",
);
}
#[cfg(target_os = "macos")]
fn count_open_fds() -> usize {
std::fs::read_dir("/dev/fd")
.map(|d| d.filter_map(|e| e.ok()).count())
.unwrap_or(0)
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_setvolname_accepts_and_returns_ok() {
use rfuse3::raw::{Filesystem, Request};
use std::ffi::OsStr;
let temp_dir = tempfile::tempdir().unwrap();
let fs = new_passthroughfs_layer(PassthroughArgs {
root_dir: temp_dir.path(),
mapping: None::<&str>,
})
.await
.unwrap();
let res = fs
.setvolname(Request::default(), OsStr::new("MyVolume"))
.await;
assert!(
res.is_ok(),
"setvolname must not return ENOSYS, got {res:?}"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_getxtimes_reports_creation_time() {
use rfuse3::raw::{Filesystem, Request};
let temp_dir = tempfile::tempdir().unwrap();
let target = temp_dir.path().join("birthcheck.txt");
std::fs::write(&target, b"hi").unwrap();
let fs = new_passthroughfs_layer(PassthroughArgs {
root_dir: temp_dir.path(),
mapping: None::<&str>,
})
.await
.unwrap();
let cname = c"birthcheck.txt";
let entry = fs.do_lookup(ROOT_ID, cname).await.unwrap();
let times = fs
.getxtimes(Request::default(), entry.attr.ino)
.await
.expect("getxtimes must not return ENOSYS");
assert_eq!(times.bkuptime, times.crtime);
assert!(
times.crtime.sec > 0,
"crtime should be a real birthtime, got {:?}",
times.crtime,
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_exchange_swaps_two_siblings() {
use rfuse3::raw::{Filesystem, Request};
use std::ffi::OsStr;
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(temp_dir.path().join("a.txt"), b"A_PAYLOAD").unwrap();
std::fs::write(temp_dir.path().join("b.txt"), b"B_PAYLOAD").unwrap();
let fs = new_passthroughfs_layer(PassthroughArgs {
root_dir: temp_dir.path(),
mapping: None::<&str>,
})
.await
.unwrap();
fs.exchange(
Request::default(),
ROOT_ID,
OsStr::new("a.txt"),
ROOT_ID,
OsStr::new("b.txt"),
0,
)
.await
.expect("exchange must not return ENOSYS");
let after_a = std::fs::read(temp_dir.path().join("a.txt")).unwrap();
let after_b = std::fs::read(temp_dir.path().join("b.txt")).unwrap();
assert_eq!(
after_a, b"B_PAYLOAD",
"exchange did not move B's content to a.txt"
);
assert_eq!(
after_b, b"A_PAYLOAD",
"exchange did not move A's content to b.txt"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_resource_fork_xattr_honors_position() {
use rfuse3::raw::{Filesystem, Request};
use std::ffi::OsStr;
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(temp_dir.path().join("forked.txt"), b"data").unwrap();
let fs = new_passthroughfs_layer(PassthroughArgs {
root_dir: temp_dir.path(),
mapping: None::<&str>,
})
.await
.unwrap();
let entry = fs.do_lookup(ROOT_ID, c"forked.txt").await.unwrap();
let attr = OsStr::new("com.apple.ResourceFork");
fs.setxattr(Request::default(), entry.attr.ino, attr, b"abcd", 0, 0)
.await
.unwrap();
fs.setxattr(Request::default(), entry.attr.ino, attr, b"EF", 0, 2)
.await
.unwrap();
let data = fs
.getxattr(Request::default(), entry.attr.ino, attr, 4)
.await
.unwrap();
match data {
rfuse3::raw::reply::ReplyXAttr::Data(bytes) => assert_eq!(&bytes[..], b"abEF"),
other => panic!("expected resource-fork data, got {other:?}"),
}
}
#[tokio::test]
async fn test_lookup_and_getattr() {
pass!()
}
#[tokio::test]
async fn test_create() {
pass!()
}
}