#![allow(
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
clippy::cast_possible_truncation
)]
use crate::cache::{NegativeCache, NegativeCacheConfig};
use crate::error::{FsError, Result};
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[derive(Debug)]
#[allow(dead_code)]
struct InodeData {
path: PathBuf,
refcount: AtomicU64,
file_type: FileType,
kernel_ino: u64,
}
impl InodeData {
fn new(path: PathBuf, file_type: FileType, kernel_ino: u64) -> Self {
Self {
path,
refcount: AtomicU64::new(1),
file_type,
kernel_ino,
}
}
fn inc_ref(&self) {
self.refcount.fetch_add(1, Ordering::Relaxed);
}
fn dec_ref(&self) -> u64 {
self.refcount.fetch_sub(1, Ordering::Relaxed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
Regular,
Directory,
Symlink,
BlockDevice,
CharDevice,
Fifo,
Socket,
Unknown,
}
impl FileType {
fn from_mode(mode: u32) -> Self {
let file_type = mode & u32::from(libc::S_IFMT);
if file_type == u32::from(libc::S_IFREG) {
Self::Regular
} else if file_type == u32::from(libc::S_IFDIR) {
Self::Directory
} else if file_type == u32::from(libc::S_IFLNK) {
Self::Symlink
} else if file_type == u32::from(libc::S_IFBLK) {
Self::BlockDevice
} else if file_type == u32::from(libc::S_IFCHR) {
Self::CharDevice
} else if file_type == u32::from(libc::S_IFIFO) {
Self::Fifo
} else if file_type == u32::from(libc::S_IFSOCK) {
Self::Socket
} else {
Self::Unknown
}
}
#[allow(dead_code)]
fn is_dir(self) -> bool {
self == Self::Directory
}
#[must_use]
pub fn to_dirent_type(self) -> u32 {
match self {
Self::Regular => libc::DT_REG as u32,
Self::Directory => libc::DT_DIR as u32,
Self::Symlink => libc::DT_LNK as u32,
Self::BlockDevice => libc::DT_BLK as u32,
Self::CharDevice => libc::DT_CHR as u32,
Self::Fifo => libc::DT_FIFO as u32,
Self::Socket => libc::DT_SOCK as u32,
Self::Unknown => libc::DT_UNKNOWN as u32,
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
struct HandleData {
file: File,
inode: u64,
flags: u32,
}
#[derive(Debug)]
struct DirHandleData {
inode: u64,
entries: Vec<DirEntry>,
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: OsString,
pub ino: u64,
pub file_type: FileType,
}
#[derive(Debug, Clone)]
pub struct PassthroughConfig {
pub negative_cache_enabled: bool,
pub negative_cache_max_entries: usize,
pub negative_cache_timeout: Duration,
}
impl Default for PassthroughConfig {
fn default() -> Self {
Self::new()
}
}
impl PassthroughConfig {
#[must_use]
pub const fn new() -> Self {
Self {
negative_cache_enabled: true,
negative_cache_max_entries: 10_000,
negative_cache_timeout: Duration::from_secs(5),
}
}
}
pub struct PassthroughFs {
root: PathBuf,
inodes: RwLock<HashMap<u64, InodeData>>,
next_inode: AtomicU64,
handles: RwLock<HashMap<u64, HandleData>>,
dir_handles: RwLock<HashMap<u64, DirHandleData>>,
next_handle: AtomicU64,
negative_cache: Option<NegativeCache>,
#[allow(dead_code)]
config: PassthroughConfig,
}
impl PassthroughFs {
pub const ROOT_INODE: u64 = 1;
pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
Self::with_config(root, PassthroughConfig::default())
}
pub fn with_config(root: impl Into<PathBuf>, config: PassthroughConfig) -> Result<Self> {
let root = root.into();
if !root.is_dir() {
return Err(FsError::InvalidPath(format!(
"root path is not a directory: {}",
root.display()
)));
}
let negative_cache = if config.negative_cache_enabled {
Some(NegativeCache::new(NegativeCacheConfig {
max_entries: config.negative_cache_max_entries,
timeout: config.negative_cache_timeout,
adaptive_ttl: Some(crate::cache::AdaptiveTtlConfig::default()),
}))
} else {
None
};
let root_kernel_ino = {
use std::os::unix::fs::MetadataExt;
std::fs::symlink_metadata(&root)
.map(|m| m.ino())
.map_err(|e| {
FsError::io(std::io::Error::new(
e.kind(),
format!("failed to stat root path '{}': {e}", root.display()),
))
})?
};
let mut inodes = HashMap::new();
inodes.insert(
Self::ROOT_INODE,
InodeData::new(PathBuf::new(), FileType::Directory, root_kernel_ino),
);
Ok(Self {
root,
inodes: RwLock::new(inodes),
next_inode: AtomicU64::new(Self::ROOT_INODE + 1),
handles: RwLock::new(HashMap::new()),
dir_handles: RwLock::new(HashMap::new()),
next_handle: AtomicU64::new(1),
negative_cache,
config,
})
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn negative_cache(&self) -> Option<&NegativeCache> {
self.negative_cache.as_ref()
}
fn alloc_inode(&self) -> u64 {
self.next_inode.fetch_add(1, Ordering::Relaxed)
}
fn alloc_handle(&self) -> u64 {
self.next_handle.fetch_add(1, Ordering::Relaxed)
}
pub(crate) fn inode_path(&self, inode: u64) -> Result<PathBuf> {
if inode == Self::ROOT_INODE {
return Ok(self.root.clone());
}
let inodes = self
.inodes
.read()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
let data = inodes.get(&inode).ok_or(FsError::InvalidHandle(inode))?;
Ok(self.root.join(&data.path))
}
pub(crate) fn kernel_ino_for(&self, inode: u64) -> Option<u64> {
let inodes = self
.inodes
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inodes.get(&inode).map(|d| d.kernel_ino)
}
#[allow(clippy::significant_drop_tightening)]
fn get_path(&self, parent: u64, name: &OsStr) -> Result<PathBuf> {
if parent == Self::ROOT_INODE {
return Ok(self.root.join(name));
}
let inodes = self
.inodes
.read()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
let parent_data = inodes.get(&parent).ok_or(FsError::InvalidHandle(parent))?;
Ok(self.root.join(&parent_data.path).join(name))
}
fn relative_path(&self, path: &Path) -> PathBuf {
path.strip_prefix(&self.root)
.map_or_else(|_| path.to_path_buf(), Path::to_path_buf)
}
#[allow(clippy::cast_possible_truncation)]
fn metadata_to_attr(ino: u64, metadata: &std::fs::Metadata) -> crate::fuse::FuseAttr {
crate::fuse::FuseAttr {
ino,
size: metadata.len(),
blocks: metadata.blocks(),
atime: metadata.atime() as u64,
mtime: metadata.mtime() as u64,
ctime: metadata.ctime() as u64,
atimensec: metadata.atime_nsec() as u32,
mtimensec: metadata.mtime_nsec() as u32,
ctimensec: metadata.ctime_nsec() as u32,
mode: metadata.mode(),
nlink: metadata.nlink() as u32,
uid: metadata.uid(),
gid: metadata.gid(),
rdev: metadata.rdev() as u32,
blksize: metadata.blksize() as u32,
padding: 0,
}
}
fn invalidate_negative_cache(&self, path: &Path) {
if let Some(ref cache) = self.negative_cache {
tracing::trace!(path = %path.display(), "invalidating negative cache");
cache.invalidate(path);
}
}
pub fn lookup(&self, parent: u64, name: &OsStr) -> Result<(u64, crate::fuse::FuseAttr)> {
let path = self.get_path(parent, name)?;
if let Some(ref cache) = self.negative_cache {
if cache.contains(&path) {
tracing::trace!(path = %path.display(), "negative cache hit");
return Err(FsError::not_found(path.display().to_string()));
}
}
let metadata = match std::fs::symlink_metadata(&path) {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if let Some(ref cache) = self.negative_cache {
tracing::trace!(path = %path.display(), "adding to negative cache");
cache.insert(path.clone());
}
return Err(FsError::not_found(path.display().to_string()));
}
Err(e) => return Err(FsError::io(e)),
};
let file_type = FileType::from_mode(metadata.mode());
let relative = self.relative_path(&path);
{
let inodes = self
.inodes
.read()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
for (&ino, data) in inodes.iter() {
if data.path == relative {
data.inc_ref();
return Ok((ino, Self::metadata_to_attr(ino, &metadata)));
}
}
}
let kernel_ino = metadata.ino();
let inode = self.alloc_inode();
{
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
inodes.insert(inode, InodeData::new(relative, file_type, kernel_ino));
}
Ok((inode, Self::metadata_to_attr(inode, &metadata)))
}
pub fn forget(&self, inode: u64, nlookup: u64) {
if inode == Self::ROOT_INODE {
return;
}
let should_remove = {
let inodes = match self.inodes.read() {
Ok(i) => i,
Err(_) => return,
};
if let Some(data) = inodes.get(&inode) {
for _ in 0..nlookup {
if data.dec_ref() == 1 {
return;
}
}
data.refcount.load(Ordering::Relaxed) == 0
} else {
false
}
};
if should_remove {
if let Ok(mut inodes) = self.inodes.write() {
inodes.remove(&inode);
}
}
}
pub fn getattr(&self, inode: u64) -> Result<crate::fuse::FuseAttr> {
let path = self.inode_path(inode)?;
let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
Ok(Self::metadata_to_attr(inode, &metadata))
}
#[allow(clippy::too_many_arguments)]
pub fn setattr(
&self,
inode: u64,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<(i64, u32)>,
mtime: Option<(i64, u32)>,
) -> Result<crate::fuse::FuseAttr> {
let path = self.inode_path(inode)?;
if let Some(mode) = mode {
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
.map_err(FsError::io)?;
}
if uid.is_some() || gid.is_some() {
let uid = uid.map_or(-1_i32 as libc::uid_t, |u| u);
let gid = gid.map_or(-1_i32 as libc::gid_t, |g| g);
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let ret = unsafe { libc::chown(path_cstr.as_ptr(), uid, gid) };
if ret != 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
}
if let Some(size) = size {
let file = OpenOptions::new()
.write(true)
.open(&path)
.map_err(FsError::io)?;
file.set_len(size).map_err(FsError::io)?;
}
if atime.is_some() || mtime.is_some() {
let atime_spec = atime.map_or(
libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_OMIT,
},
|(sec, nsec)| libc::timespec {
tv_sec: sec,
tv_nsec: i64::from(nsec),
},
);
let mtime_spec = mtime.map_or(
libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_OMIT,
},
|(sec, nsec)| libc::timespec {
tv_sec: sec,
tv_nsec: i64::from(nsec),
},
);
let times = [atime_spec, mtime_spec];
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let ret =
unsafe { libc::utimensat(libc::AT_FDCWD, path_cstr.as_ptr(), times.as_ptr(), 0) };
if ret != 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
}
self.getattr(inode)
}
pub fn readlink(&self, inode: u64) -> Result<PathBuf> {
let path = self.inode_path(inode)?;
std::fs::read_link(&path).map_err(FsError::io)
}
pub fn create(
&self,
parent: u64,
name: &OsStr,
mode: u32,
flags: u32,
) -> Result<(u64, crate::fuse::FuseAttr, u64)> {
let path = self.get_path(parent, name)?;
let mut opts = OpenOptions::new();
Self::apply_flags(&mut opts, flags);
opts.create(true);
opts.mode(mode & 0o7777);
let file = opts.open(&path).map_err(FsError::io)?;
let metadata = file.metadata().map_err(FsError::io)?;
self.invalidate_negative_cache(&path);
let relative = self.relative_path(&path);
let kernel_ino = metadata.ino();
let inode = self.alloc_inode();
{
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
inodes.insert(
inode,
InodeData::new(relative, FileType::Regular, kernel_ino),
);
}
let handle = self.alloc_handle();
{
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
handles.insert(handle, HandleData { file, inode, flags });
}
Ok((inode, Self::metadata_to_attr(inode, &metadata), handle))
}
pub fn mkdir(
&self,
parent: u64,
name: &OsStr,
mode: u32,
) -> Result<(u64, crate::fuse::FuseAttr)> {
let path = self.get_path(parent, name)?;
std::fs::create_dir(&path).map_err(FsError::io)?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode & 0o7777))
.map_err(FsError::io)?;
self.invalidate_negative_cache(&path);
let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
let relative = self.relative_path(&path);
let kernel_ino = metadata.ino();
let inode = self.alloc_inode();
{
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
inodes.insert(
inode,
InodeData::new(relative, FileType::Directory, kernel_ino),
);
}
Ok((inode, Self::metadata_to_attr(inode, &metadata)))
}
pub fn symlink(
&self,
parent: u64,
name: &OsStr,
target: &Path,
) -> Result<(u64, crate::fuse::FuseAttr)> {
let path = self.get_path(parent, name)?;
std::os::unix::fs::symlink(target, &path).map_err(FsError::io)?;
self.invalidate_negative_cache(&path);
let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
let relative = self.relative_path(&path);
let kernel_ino = metadata.ino();
let inode = self.alloc_inode();
{
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
inodes.insert(
inode,
InodeData::new(relative, FileType::Symlink, kernel_ino),
);
}
Ok((inode, Self::metadata_to_attr(inode, &metadata)))
}
pub fn link(
&self,
inode: u64,
new_parent: u64,
new_name: &OsStr,
) -> Result<(u64, crate::fuse::FuseAttr)> {
let source_path = self.inode_path(inode)?;
let new_path = self.get_path(new_parent, new_name)?;
std::fs::hard_link(&source_path, &new_path).map_err(FsError::io)?;
self.invalidate_negative_cache(&new_path);
let metadata = std::fs::symlink_metadata(&new_path).map_err(FsError::io)?;
{
let inodes = self
.inodes
.read()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
if let Some(data) = inodes.get(&inode) {
data.inc_ref();
}
}
Ok((inode, Self::metadata_to_attr(inode, &metadata)))
}
#[allow(clippy::cast_possible_truncation)]
pub fn mknod(
&self,
parent: u64,
name: &OsStr,
mode: u32,
rdev: u64,
) -> Result<(u64, crate::fuse::FuseAttr)> {
let path = self.get_path(parent, name)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let ret = unsafe {
libc::mknod(
path_cstr.as_ptr(),
mode as libc::mode_t,
rdev as libc::dev_t,
)
};
if ret != 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
self.invalidate_negative_cache(&path);
let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
let file_type = FileType::from_mode(metadata.mode());
let relative = self.relative_path(&path);
let kernel_ino = metadata.ino();
let inode = self.alloc_inode();
{
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
inodes.insert(inode, InodeData::new(relative, file_type, kernel_ino));
}
Ok((inode, Self::metadata_to_attr(inode, &metadata)))
}
pub fn unlink(&self, parent: u64, name: &OsStr) -> Result<()> {
let path = self.get_path(parent, name)?;
std::fs::remove_file(&path).map_err(FsError::io)?;
Ok(())
}
pub fn rmdir(&self, parent: u64, name: &OsStr) -> Result<()> {
let path = self.get_path(parent, name)?;
std::fs::remove_dir(&path).map_err(FsError::io)
}
pub fn rename(
&self,
parent: u64,
name: &OsStr,
new_parent: u64,
new_name: &OsStr,
_flags: u32,
) -> Result<()> {
let old_path = self.get_path(parent, name)?;
let new_path = self.get_path(new_parent, new_name)?;
std::fs::rename(&old_path, &new_path).map_err(FsError::io)?;
self.invalidate_negative_cache(&old_path);
self.invalidate_negative_cache(&new_path);
let old_relative = self.relative_path(&old_path);
let new_relative = self.relative_path(&new_path);
{
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
for data in inodes.values_mut() {
if data.path == old_relative {
data.path = new_relative;
break;
}
}
}
Ok(())
}
fn apply_flags(opts: &mut OpenOptions, flags: u32) {
let access_mode = flags & libc::O_ACCMODE as u32;
match access_mode {
x if x == libc::O_RDONLY as u32 => {
opts.read(true);
}
x if x == libc::O_WRONLY as u32 => {
opts.write(true);
}
x if x == libc::O_RDWR as u32 => {
opts.read(true).write(true);
}
_ => {
opts.read(true);
}
}
if flags & libc::O_APPEND as u32 != 0 {
opts.append(true);
}
if flags & libc::O_TRUNC as u32 != 0 {
opts.truncate(true);
}
}
pub fn open(&self, inode: u64, flags: u32) -> Result<u64> {
let path = self.inode_path(inode)?;
let mut opts = OpenOptions::new();
Self::apply_flags(&mut opts, flags);
let file = opts.open(&path).map_err(FsError::io)?;
let handle = self.alloc_handle();
{
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
handles.insert(handle, HandleData { file, inode, flags });
}
Ok(handle)
}
pub fn read(&self, handle: u64, offset: u64, size: u32) -> Result<Vec<u8>> {
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let data = handles
.get_mut(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
data.file
.seek(SeekFrom::Start(offset))
.map_err(FsError::io)?;
let mut buf = vec![0u8; size as usize];
let n = data.file.read(&mut buf).map_err(FsError::io)?;
buf.truncate(n);
Ok(buf)
}
pub fn write(&self, handle: u64, offset: u64, data: &[u8], _flags: u32) -> Result<u32> {
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let handle_data = handles
.get_mut(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
handle_data
.file
.seek(SeekFrom::Start(offset))
.map_err(FsError::io)?;
let n = handle_data.file.write(data).map_err(FsError::io)?;
#[allow(clippy::cast_possible_truncation)]
Ok(n as u32)
}
pub fn flush(&self, handle: u64) -> Result<()> {
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let data = handles
.get_mut(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
data.file.flush().map_err(FsError::io)
}
pub fn fsync(&self, handle: u64, datasync: bool) -> Result<()> {
let handles = self
.handles
.read()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
if datasync {
data.file.sync_data().map_err(FsError::io)
} else {
data.file.sync_all().map_err(FsError::io)
}
}
pub fn release(&self, handle: u64) -> Result<()> {
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
handles.remove(&handle);
Ok(())
}
pub fn get_file_raw_fd(&self, handle: u64) -> Option<std::os::unix::io::RawFd> {
use std::os::unix::io::AsRawFd;
let handles = self.handles.read().ok()?;
handles.get(&handle).map(|h| h.file.as_raw_fd())
}
pub fn lseek(&self, handle: u64, offset: i64, whence: u32) -> Result<u64> {
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let data = handles
.get_mut(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
let seek_from = match whence {
0 => SeekFrom::Start(offset as u64), 1 => SeekFrom::Current(offset), 2 => SeekFrom::End(offset), _ => return Err(FsError::InvalidPath("invalid whence".to_string())),
};
data.file.seek(seek_from).map_err(FsError::io)
}
#[cfg(target_os = "linux")]
pub fn fallocate(&self, handle: u64, mode: u32, offset: u64, length: u64) -> Result<()> {
let handles = self
.handles
.read()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
let fd = data.file.as_raw_fd();
#[allow(clippy::cast_possible_wrap)]
let ret = unsafe { libc::fallocate(fd, mode as i32, offset as i64, length as i64) };
if ret != 0 {
Err(FsError::io(std::io::Error::last_os_error()))
} else {
Ok(())
}
}
#[cfg(target_os = "macos")]
pub fn fallocate(&self, handle: u64, _mode: u32, offset: u64, length: u64) -> Result<()> {
let mut handles = self
.handles
.write()
.map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
let data = handles
.get_mut(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
let new_size = offset + length;
data.file.set_len(new_size).map_err(FsError::io)
}
pub fn opendir(&self, inode: u64) -> Result<u64> {
let path = self.inode_path(inode)?;
let mut entries = Vec::new();
entries.push(DirEntry {
name: OsString::from("."),
ino: inode,
file_type: FileType::Directory,
});
entries.push(DirEntry {
name: OsString::from(".."),
ino: Self::ROOT_INODE,
file_type: FileType::Directory,
});
for entry in std::fs::read_dir(&path).map_err(FsError::io)? {
let entry = entry.map_err(FsError::io)?;
let metadata = entry.metadata().map_err(FsError::io)?;
let file_type = FileType::from_mode(metadata.mode());
let entry_path = entry.path();
let relative = self.relative_path(&entry_path);
let entry_ino = {
let inodes = self
.inodes
.read()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
let mut found_ino = None;
for (&ino, data) in inodes.iter() {
if data.path == relative {
found_ino = Some(ino);
break;
}
}
found_ino
};
let ino = if let Some(ino) = entry_ino {
ino
} else {
let kernel_ino = metadata.ino();
let new_ino = self.alloc_inode();
let mut inodes = self
.inodes
.write()
.map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
inodes.insert(new_ino, InodeData::new(relative, file_type, kernel_ino));
new_ino
};
entries.push(DirEntry {
name: entry.file_name(),
ino,
file_type,
});
}
let handle = self.alloc_handle();
{
let mut dir_handles = self
.dir_handles
.write()
.map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
dir_handles.insert(handle, DirHandleData { inode, entries });
}
Ok(handle)
}
pub fn readdir(&self, handle: u64, offset: u64) -> Result<Vec<DirEntry>> {
let dir_handles = self
.dir_handles
.read()
.map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
let data = dir_handles
.get(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
let entries: Vec<DirEntry> = data.entries.iter().skip(offset as usize).cloned().collect();
Ok(entries)
}
pub fn releasedir(&self, handle: u64) -> Result<()> {
let mut dir_handles = self
.dir_handles
.write()
.map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
dir_handles.remove(&handle);
Ok(())
}
pub fn fsyncdir(&self, handle: u64, _datasync: bool) -> Result<()> {
let dir_handles = self
.dir_handles
.read()
.map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
let data = dir_handles
.get(&handle)
.ok_or(FsError::InvalidHandle(handle))?;
let path = self.inode_path(data.inode)?;
let dir = File::open(&path).map_err(FsError::io)?;
dir.sync_all().map_err(FsError::io)
}
#[cfg(target_os = "linux")]
pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
use std::os::unix::ffi::OsStrExt;
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let name_cstr = std::ffi::CString::new(name.as_bytes())
.map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
if size == 0 {
let ret = unsafe {
libc::getxattr(
path_cstr.as_ptr(),
name_cstr.as_ptr(),
std::ptr::null_mut(),
0,
)
};
if ret < 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
Ok(vec![0u8; ret as usize])
} else {
let mut buf = vec![0u8; size as usize];
let ret = unsafe {
libc::getxattr(
path_cstr.as_ptr(),
name_cstr.as_ptr(),
buf.as_mut_ptr().cast(),
size as usize,
)
};
if ret < 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
buf.truncate(ret as usize);
Ok(buf)
}
}
#[cfg(target_os = "macos")]
pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let name_cstr = std::ffi::CString::new(name.as_bytes())
.map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
if size == 0 {
let ret = unsafe {
libc::getxattr(
path_cstr.as_ptr(),
name_cstr.as_ptr(),
std::ptr::null_mut(),
0,
0,
0,
)
};
if ret < 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
Ok(vec![0u8; ret as usize])
} else {
let mut buf = vec![0u8; size as usize];
let ret = unsafe {
libc::getxattr(
path_cstr.as_ptr(),
name_cstr.as_ptr(),
buf.as_mut_ptr().cast(),
size as usize,
0,
0,
)
};
if ret < 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
buf.truncate(ret as usize);
Ok(buf)
}
}
#[cfg(target_os = "linux")]
pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let name_cstr = std::ffi::CString::new(name.as_bytes())
.map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
let ret = unsafe {
libc::setxattr(
path_cstr.as_ptr(),
name_cstr.as_ptr(),
value.as_ptr().cast(),
value.len(),
flags as i32,
)
};
if ret != 0 {
Err(FsError::io(std::io::Error::last_os_error()))
} else {
Ok(())
}
}
#[cfg(target_os = "macos")]
pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let name_cstr = std::ffi::CString::new(name.as_bytes())
.map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
let ret = unsafe {
libc::setxattr(
path_cstr.as_ptr(),
name_cstr.as_ptr(),
value.as_ptr().cast(),
value.len(),
0,
flags as i32,
)
};
if ret != 0 {
Err(FsError::io(std::io::Error::last_os_error()))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let name_cstr = std::ffi::CString::new(name.as_bytes())
.map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr()) };
if ret != 0 {
Err(FsError::io(std::io::Error::last_os_error()))
} else {
Ok(())
}
}
#[cfg(target_os = "macos")]
pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let name_cstr = std::ffi::CString::new(name.as_bytes())
.map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr(), 0) };
if ret != 0 {
Err(FsError::io(std::io::Error::last_os_error()))
} else {
Ok(())
}
}
pub fn statfs(&self) -> Result<crate::fuse::StatFs> {
let path_cstr = std::ffi::CString::new(self.root.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::statfs(path_cstr.as_ptr(), &raw mut stat) };
if ret != 0 {
return Err(FsError::io(std::io::Error::last_os_error()));
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::unnecessary_cast // Field types differ between macOS (u32) and Linux (u64).
)]
Ok(crate::fuse::StatFs {
blocks: stat.f_blocks as u64,
bfree: stat.f_bfree as u64,
bavail: stat.f_bavail as u64,
files: stat.f_files as u64,
ffree: stat.f_ffree as u64,
bsize: stat.f_bsize as u32,
namelen: 255, frsize: stat.f_bsize as u32,
})
}
pub fn access(&self, inode: u64, mask: u32) -> Result<()> {
let path = self.inode_path(inode)?;
let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
#[allow(clippy::cast_possible_wrap)]
let ret = unsafe { libc::access(path_cstr.as_ptr(), mask as i32) };
if ret != 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EACCES) {
Err(FsError::permission_denied(path.display().to_string()))
} else {
Err(FsError::io(err))
}
} else {
Ok(())
}
}
}
#[allow(clippy::missing_fields_in_debug)]
impl std::fmt::Debug for PassthroughFs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PassthroughFs")
.field("root", &self.root)
.field("inodes", &self.inodes.read().map_or(0, |i| i.len()))
.field("handles", &self.handles.read().map_or(0, |h| h.len()))
.field(
"dir_handles",
&self.dir_handles.read().map_or(0, |h| h.len()),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
const S_IFDIR: u32 = libc::S_IFDIR as u32;
const S_IFREG: u32 = libc::S_IFREG as u32;
fn setup_test_fs() -> (TempDir, PassthroughFs) {
let temp = TempDir::new().expect("failed to create temp dir");
let fs = PassthroughFs::new(temp.path()).expect("failed to create fs");
(temp, fs)
}
#[test]
fn test_new_filesystem() {
let temp = TempDir::new().unwrap();
let fs = PassthroughFs::new(temp.path()).unwrap();
assert_eq!(fs.root(), temp.path());
}
#[test]
fn test_new_invalid_path() {
let result = PassthroughFs::new("/nonexistent/path/12345");
assert!(result.is_err());
}
#[test]
fn test_lookup_existing_file() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("test.txt");
std::fs::write(&file_path, "hello").unwrap();
let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("test.txt"));
assert!(result.is_ok());
let (inode, attr) = result.unwrap();
assert!(inode > PassthroughFs::ROOT_INODE);
assert_eq!(attr.size, 5);
}
#[test]
fn test_lookup_nonexistent_file() {
let (_temp, fs) = setup_test_fs();
let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("nonexistent.txt"));
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[test]
fn test_lookup_negative_cache() {
let (_temp, fs) = setup_test_fs();
let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
if let Some(cache) = fs.negative_cache() {
let stats = cache.stats();
assert!(stats.entries > 0 || stats.misses > 0);
}
let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[test]
fn test_getattr_root() {
let (_temp, fs) = setup_test_fs();
let result = fs.getattr(PassthroughFs::ROOT_INODE);
assert!(result.is_ok());
let attr = result.unwrap();
assert_eq!(attr.ino, PassthroughFs::ROOT_INODE);
assert!(attr.mode & S_IFDIR != 0);
}
#[test]
fn test_create_and_read_file() {
let (_temp, fs) = setup_test_fs();
let (inode, _attr, handle) = fs
.create(
PassthroughFs::ROOT_INODE,
OsStr::new("newfile.txt"),
0o644,
libc::O_RDWR as u32,
)
.unwrap();
assert!(inode > PassthroughFs::ROOT_INODE);
let data = b"hello world";
let written = fs.write(handle, 0, data, 0).unwrap();
assert_eq!(written, data.len() as u32);
let read_data = fs.read(handle, 0, 100).unwrap();
assert_eq!(read_data, data);
fs.release(handle).unwrap();
}
#[test]
fn test_mkdir_and_rmdir() {
let (_temp, fs) = setup_test_fs();
let (inode, attr) = fs
.mkdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"), 0o755)
.unwrap();
assert!(inode > PassthroughFs::ROOT_INODE);
assert!(attr.mode & S_IFDIR != 0);
fs.rmdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"))
.unwrap();
let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("testdir"));
assert!(result.is_err());
assert!(result.unwrap_err().is_not_found());
}
#[test]
fn test_symlink_and_readlink() {
let (temp, fs) = setup_test_fs();
let target = temp.path().join("target.txt");
std::fs::write(&target, "target content").unwrap();
let (inode, _attr) = fs
.symlink(
PassthroughFs::ROOT_INODE,
OsStr::new("link"),
Path::new("target.txt"),
)
.unwrap();
let link_target = fs.readlink(inode).unwrap();
assert_eq!(link_target, Path::new("target.txt"));
}
#[test]
fn test_hard_link() {
let (temp, fs) = setup_test_fs();
let original = temp.path().join("original.txt");
std::fs::write(&original, "content").unwrap();
let (orig_inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("original.txt"))
.unwrap();
let (link_inode, attr) = fs
.link(
orig_inode,
PassthroughFs::ROOT_INODE,
OsStr::new("hardlink.txt"),
)
.unwrap();
assert_eq!(link_inode, orig_inode);
assert!(attr.nlink >= 2);
}
#[test]
fn test_unlink() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("todelete.txt");
std::fs::write(&file_path, "delete me").unwrap();
fs.unlink(PassthroughFs::ROOT_INODE, OsStr::new("todelete.txt"))
.unwrap();
assert!(!file_path.exists());
}
#[test]
fn test_rename() {
let (temp, fs) = setup_test_fs();
let old_path = temp.path().join("old.txt");
std::fs::write(&old_path, "content").unwrap();
fs.rename(
PassthroughFs::ROOT_INODE,
OsStr::new("old.txt"),
PassthroughFs::ROOT_INODE,
OsStr::new("new.txt"),
0,
)
.unwrap();
assert!(!old_path.exists());
assert!(temp.path().join("new.txt").exists());
}
#[test]
fn test_opendir_readdir() {
let (temp, fs) = setup_test_fs();
std::fs::write(temp.path().join("file1.txt"), "1").unwrap();
std::fs::write(temp.path().join("file2.txt"), "2").unwrap();
std::fs::create_dir(temp.path().join("subdir")).unwrap();
let handle = fs.opendir(PassthroughFs::ROOT_INODE).unwrap();
let entries = fs.readdir(handle, 0).unwrap();
assert!(entries.len() >= 5);
let names: Vec<_> = entries
.iter()
.map(|e| e.name.to_string_lossy().to_string())
.collect();
assert!(names.contains(&".".to_string()));
assert!(names.contains(&"..".to_string()));
assert!(names.contains(&"file1.txt".to_string()));
assert!(names.contains(&"file2.txt".to_string()));
assert!(names.contains(&"subdir".to_string()));
fs.releasedir(handle).unwrap();
}
#[test]
fn test_setattr_size() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("truncate.txt");
std::fs::write(&file_path, "hello world").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("truncate.txt"))
.unwrap();
let attr = fs
.setattr(inode, None, None, None, Some(5), None, None)
.unwrap();
assert_eq!(attr.size, 5);
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "hello");
}
#[test]
fn test_setattr_mode() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("chmod.txt");
std::fs::write(&file_path, "test").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("chmod.txt"))
.unwrap();
let attr = fs
.setattr(inode, Some(0o600), None, None, None, None, None)
.unwrap();
assert_eq!(attr.mode & 0o777, 0o600);
}
#[test]
fn test_open_read_write() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("rw.txt");
std::fs::write(&file_path, "initial").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("rw.txt"))
.unwrap();
let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
let data = fs.read(handle, 0, 100).unwrap();
assert_eq!(data, b"initial");
fs.write(handle, 0, b"INITIAL", 0).unwrap();
let data = fs.read(handle, 0, 100).unwrap();
assert_eq!(data, b"INITIAL");
fs.release(handle).unwrap();
}
#[test]
fn test_fsync() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("sync.txt");
std::fs::write(&file_path, "test").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("sync.txt"))
.unwrap();
let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
fs.write(handle, 0, b"updated", 0).unwrap();
fs.fsync(handle, false).unwrap();
fs.fsync(handle, true).unwrap();
fs.release(handle).unwrap();
}
#[test]
fn test_flush() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("flush.txt");
std::fs::write(&file_path, "test").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("flush.txt"))
.unwrap();
let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
fs.write(handle, 0, b"updated", 0).unwrap();
fs.flush(handle).unwrap();
fs.release(handle).unwrap();
}
#[test]
fn test_lseek() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("seek.txt");
std::fs::write(&file_path, "0123456789").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("seek.txt"))
.unwrap();
let handle = fs.open(inode, libc::O_RDONLY as u32).unwrap();
let pos = fs.lseek(handle, 5, 0).unwrap(); assert_eq!(pos, 5);
let data = fs.read(handle, pos, 5).unwrap();
assert_eq!(data, b"56789");
fs.release(handle).unwrap();
}
#[test]
fn test_statfs() {
let (_temp, fs) = setup_test_fs();
let stat = fs.statfs().unwrap();
assert!(stat.blocks > 0);
assert!(stat.bsize > 0);
}
#[test]
fn test_access() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("access.txt");
std::fs::write(&file_path, "test").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("access.txt"))
.unwrap();
fs.access(inode, libc::R_OK as u32).unwrap();
fs.access(inode, libc::W_OK as u32).unwrap();
}
#[test]
fn test_forget() {
let (temp, fs) = setup_test_fs();
let file_path = temp.path().join("forget.txt");
std::fs::write(&file_path, "test").unwrap();
let (inode, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
.unwrap();
let (inode2, _) = fs
.lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
.unwrap();
assert_eq!(inode, inode2);
fs.forget(inode, 1);
assert!(fs.getattr(inode).is_ok());
fs.forget(inode, 1);
}
#[test]
#[ignore = "mknod requires elevated permissions on macOS"]
fn test_mknod_regular_file() {
let (_temp, fs) = setup_test_fs();
let (inode, attr) = fs
.mknod(
PassthroughFs::ROOT_INODE,
OsStr::new("mknod_file"),
S_IFREG | 0o644,
0,
)
.unwrap();
assert!(inode > PassthroughFs::ROOT_INODE);
assert!(attr.mode & S_IFREG != 0);
}
#[test]
fn test_concurrent_operations() {
use std::sync::Arc;
use std::thread;
let (temp, fs) = setup_test_fs();
let fs = Arc::new(fs);
for i in 0..10 {
std::fs::write(
temp.path().join(format!("file{i}.txt")),
format!("content{i}"),
)
.unwrap();
}
let mut handles = vec![];
for i in 0..4 {
let fs = Arc::clone(&fs);
handles.push(thread::spawn(move || {
for j in 0..100 {
let name = format!("file{}.txt", (i + j) % 10);
let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name));
}
}));
}
for i in 0..4 {
let fs = Arc::clone(&fs);
handles.push(thread::spawn(move || {
for j in 0..50 {
let name = format!("file{}.txt", (i + j) % 10);
if let Ok((inode, _)) = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name))
{
if let Ok(handle) = fs.open(inode, libc::O_RDONLY as u32) {
let _ = fs.read(handle, 0, 100);
let _ = fs.release(handle);
}
}
}
}));
}
for handle in handles {
handle.join().expect("Thread panicked");
}
}
}