use std::collections::BTreeMap;
use std::ffi::{CString, OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use super::backend::{
DirEntry, Entry, Errno, FsBackend, StatFs, EACCES, EBADF, EINVAL, ENOENT, ENOSPC, ENOTDIR,
EISDIR, EIO, EPERM,
};
use crate::vmm::resources::SymlinkPolicy;
use super::notify::Notifier;
use super::protocol::{
Attr, SetattrIn, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, DT_UNKNOWN,
FATTR_ATIME, FATTR_ATIME_NOW, FATTR_GID, FATTR_MODE, FATTR_MTIME, FATTR_MTIME_NOW,
FATTR_SIZE, FATTR_UID, FUSE_ROOT_ID, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT,
S_IFREG, S_IFSOCK,
};
#[derive(Clone, Copy, Debug)]
enum Kind {
File,
Dir,
Symlink,
Other,
}
#[derive(Clone)]
struct InodeInfo {
host_path: PathBuf,
kind: Kind,
}
struct Mmap {
ptr: *mut u8,
len: usize,
}
unsafe impl Send for Mmap {}
unsafe impl Sync for Mmap {}
struct State {
inodes: BTreeMap<u64, InodeInfo>,
children: BTreeMap<(u64, Vec<u8>), u64>,
handles: BTreeMap<u64, OwnedFd>,
dax_mmaps: BTreeMap<usize, Mmap>,
next_nodeid: u64,
next_fh: u64,
}
impl Drop for State {
fn drop(&mut self) {
for (_, m) in std::mem::take(&mut self.dax_mmaps) {
unsafe {
libc::munmap(m.ptr as *mut _, m.len);
}
}
}
}
pub struct PosixFs {
root: PathBuf,
symlinks: SymlinkPolicy,
st: Mutex<State>,
watcher: Mutex<Option<Watcher>>,
}
struct Watcher {
inner: Arc<WatcherInner>,
handle: Option<std::thread::JoinHandle<()>>,
}
struct WatcherInner {
kq: libc::c_int,
stop: AtomicBool,
notifier: Mutex<Option<Arc<dyn Notifier>>>,
watched: Mutex<BTreeMap<libc::c_int, WatchedEntry>>,
}
struct WatchedEntry {
nodeid: u64,
parent_nodeid: u64,
name: Vec<u8>,
_owned_fd: OwnedFd,
}
impl Drop for Watcher {
fn drop(&mut self) {
self.inner.stop.store(true, Ordering::Release);
let trigger = libc::kevent {
ident: 0,
filter: libc::EVFILT_USER,
flags: libc::EV_ADD | libc::EV_ONESHOT | libc::EV_RECEIPT,
fflags: libc::NOTE_TRIGGER,
data: 0,
udata: std::ptr::null_mut(),
};
let mut tr = trigger;
unsafe {
let _ = libc::kevent(self.inner.kq, &mut tr as *mut _, 1, std::ptr::null_mut(), 0, std::ptr::null());
}
if let Some(h) = self.handle.take() {
let _ = h.join();
}
unsafe {
libc::close(self.inner.kq);
}
}
}
impl PosixFs {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
Self::new_with_symlinks(root, SymlinkPolicy::Opaque)
}
pub fn new_unchecked(root: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
Self::new_with_symlinks(root, SymlinkPolicy::Follow)
}
pub fn new_with_symlinks(
root: impl Into<PathBuf>,
symlinks: SymlinkPolicy,
) -> Result<Self, std::io::Error> {
let root = root.into();
let md = std::fs::metadata(&root)?;
if !md.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotADirectory,
format!("posix-fs root is not a directory: {}", root.display()),
));
}
let root = std::fs::canonicalize(&root)?;
let mut inodes = BTreeMap::new();
inodes.insert(
FUSE_ROOT_ID,
InodeInfo {
host_path: root.clone(),
kind: Kind::Dir,
},
);
Ok(Self {
root,
symlinks,
st: Mutex::new(State {
inodes,
children: BTreeMap::new(),
handles: BTreeMap::new(),
dax_mmaps: BTreeMap::new(),
next_nodeid: FUSE_ROOT_ID + 1,
next_fh: 1,
}),
watcher: Mutex::new(None),
})
}
pub fn set_notifier(&self, notifier: Arc<dyn Notifier>) -> Result<(), std::io::Error> {
let mut watcher_slot = self.watcher.lock().unwrap();
if watcher_slot.is_some() {
let w = watcher_slot.as_ref().unwrap();
*w.inner.notifier.lock().unwrap() = Some(notifier);
return Ok(());
}
let kq = unsafe { libc::kqueue() };
if kq < 0 {
return Err(std::io::Error::last_os_error());
}
let inner = Arc::new(WatcherInner {
kq,
stop: AtomicBool::new(false),
notifier: Mutex::new(Some(notifier)),
watched: Mutex::new(BTreeMap::new()),
});
let thread_inner = inner.clone();
let handle = std::thread::Builder::new()
.name("supermachine-posixfs-watch".to_owned())
.spawn(move || run_watcher(thread_inner))
.map_err(|e| std::io::Error::other(e.to_string()))?;
*watcher_slot = Some(Watcher {
inner,
handle: Some(handle),
});
Ok(())
}
fn watch_inode(&self, nodeid: u64, path: &std::path::Path) {
let watcher = self.watcher.lock().unwrap();
let Some(w) = watcher.as_ref() else { return };
let c = match CString::new(path.as_os_str().as_bytes()) {
Ok(c) => c,
Err(_) => return,
};
let fd = unsafe { libc::open(c.as_ptr(), libc::O_RDONLY | libc::O_EVTONLY) };
if fd < 0 {
return;
}
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let (parent_nodeid, name) = {
let st = self.st.lock().unwrap();
let entry = st
.children
.iter()
.find(|(_, id)| **id == nodeid)
.map(|((p, n), _)| (*p, n.clone()));
match entry {
Some(e) => e,
None => return, }
};
let mut watched = w.inner.watched.lock().unwrap();
watched.retain(|_, e| e.nodeid != nodeid);
let ev = libc::kevent {
ident: fd as libc::uintptr_t,
filter: libc::EVFILT_VNODE,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: libc::NOTE_DELETE
| libc::NOTE_RENAME
| libc::NOTE_WRITE
| libc::NOTE_EXTEND
| libc::NOTE_ATTRIB,
data: 0,
udata: std::ptr::null_mut(),
};
let mut event = ev;
let rc = unsafe {
libc::kevent(
w.inner.kq,
&mut event as *mut _,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
)
};
if rc < 0 {
return;
}
watched.insert(
fd,
WatchedEntry {
nodeid,
parent_nodeid,
name,
_owned_fd: owned,
},
);
}
fn host_path_of(&self, nodeid: u64) -> Result<PathBuf, Errno> {
let st = self.st.lock().unwrap();
st.inodes.get(&nodeid).map(|i| i.host_path.clone()).ok_or(ENOENT)
}
fn kind_of(&self, nodeid: u64) -> Result<Kind, Errno> {
let st = self.st.lock().unwrap();
st.inodes.get(&nodeid).map(|i| i.kind).ok_or(ENOENT)
}
}
#[cfg(target_os = "macos")]
fn host_to_linux_errno(host: i32) -> i32 {
match host {
35 => 11, 11 => 35, 63 => 36, 77 => 37, 78 => 38, 66 => 39, 62 => 40, 91 => 42, 90 => 43, 96 => 61, 84 => 75, 94 => 74, 92 => 84, 38 => 88, 39 => 89, 40 => 90, 41 => 91, 42 => 92, 43 => 93, 44 => 94, 102 => 95, 46 => 96, 47 => 97, 48 => 98, 49 => 99, 50 => 100, 51 => 101, 52 => 102, 53 => 103, 54 => 104, 55 => 105, 56 => 106, 57 => 107, 58 => 108, 59 => 109, 60 => 110, 61 => 111, 64 => 112, 65 => 113, 37 => 114, 36 => 115, 70 => 116, 69 => 122, 89 => 125, 105 => 130, 104 => 131, _ => host,
}
}
#[cfg(not(target_os = "macos"))]
#[inline]
fn host_to_linux_errno(host: i32) -> i32 {
host
}
fn io_err_to_linux(e: &std::io::Error) -> Errno {
-host_to_linux_errno(e.raw_os_error().unwrap_or(libc::EIO))
}
fn errno_now() -> Errno {
-host_to_linux_errno(
std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(libc::EIO),
)
}
fn attr_from_meta(ino: u64, md: &std::fs::Metadata) -> Attr {
let mode_full = md.mode();
let perm_bits = mode_full & 0o7777;
let typ_bits = if md.is_dir() {
S_IFDIR
} else if md.is_file() {
S_IFREG
} else if md.file_type().is_symlink() {
S_IFLNK
} else {
match mode_full & S_IFMT {
S_IFBLK => S_IFBLK,
S_IFCHR => S_IFCHR,
S_IFIFO => S_IFIFO,
S_IFSOCK => S_IFSOCK,
_ => 0,
}
};
Attr {
ino,
size: md.size(),
blocks: md.blocks(),
atime: md.atime() as u64,
mtime: md.mtime() as u64,
ctime: md.ctime() as u64,
atimensec: md.atime_nsec() as u32,
mtimensec: md.mtime_nsec() as u32,
ctimensec: md.ctime_nsec() as u32,
mode: typ_bits | perm_bits,
nlink: md.nlink() as u32,
uid: md.uid(),
gid: md.gid(),
rdev: md.rdev() as u32,
blksize: md.blksize() as u32,
flags: 0,
}
}
fn attr_from_stat(ino: u64, st: &libc::stat) -> Attr {
let mode_full = st.st_mode as u32;
let perm_bits = mode_full & 0o7777;
let typ_bits = match mode_full & S_IFMT {
S_IFDIR => S_IFDIR,
S_IFREG => S_IFREG,
S_IFLNK => S_IFLNK,
S_IFBLK => S_IFBLK,
S_IFCHR => S_IFCHR,
S_IFIFO => S_IFIFO,
S_IFSOCK => S_IFSOCK,
_ => 0,
};
#[cfg(target_os = "macos")]
let (a, an, m, mn, c, cn) = (
st.st_atime as u64,
st.st_atime_nsec as u32,
st.st_mtime as u64,
st.st_mtime_nsec as u32,
st.st_ctime as u64,
st.st_ctime_nsec as u32,
);
#[cfg(not(target_os = "macos"))]
let (a, an, m, mn, c, cn) = (
st.st_atime as u64,
st.st_atime_nsec as u32,
st.st_mtime as u64,
st.st_mtime_nsec as u32,
st.st_ctime as u64,
st.st_ctime_nsec as u32,
);
Attr {
ino,
size: st.st_size as u64,
blocks: st.st_blocks as u64,
atime: a,
mtime: m,
ctime: c,
atimensec: an,
mtimensec: mn,
ctimensec: cn,
mode: typ_bits | perm_bits,
nlink: st.st_nlink as u32,
uid: st.st_uid,
gid: st.st_gid,
rdev: st.st_rdev as u32,
blksize: st.st_blksize as u32,
flags: 0,
}
}
fn open_dirfd(path: &std::path::Path) -> Result<OwnedFd, Errno> {
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let fd = unsafe {
libc::open(c.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW)
};
if fd < 0 {
return Err(errno_now());
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
fn kind_from_meta(md: &std::fs::Metadata) -> Kind {
if md.is_dir() {
Kind::Dir
} else if md.is_file() {
Kind::File
} else if md.file_type().is_symlink() {
Kind::Symlink
} else {
Kind::Other
}
}
fn name_safe(name: &OsStr) -> Result<(), Errno> {
let bytes = name.as_bytes();
if bytes.is_empty() || bytes == b"." || bytes == b".." {
return Err(EINVAL);
}
if bytes.contains(&b'/') {
return Err(EINVAL);
}
if bytes.contains(&0u8) {
return Err(EINVAL);
}
Ok(())
}
impl FsBackend for PosixFs {
fn lookup(&self, parent: u64, name: &OsStr) -> Result<Entry, Errno> {
name_safe(name)?;
let parent_path = self.host_path_of(parent)?;
let path = parent_path.join(name);
let md = std::fs::symlink_metadata(&path)
.map_err(|e| io_err_to_linux(&e))?;
if self.symlinks != SymlinkPolicy::Follow && md.file_type().is_symlink() {
if let Ok(canonical) = std::fs::canonicalize(&path) {
if !canonical.starts_with(&self.root) {
return Err(EACCES);
}
}
}
let mut st = self.st.lock().unwrap();
let key = (parent, name.as_bytes().to_vec());
let nodeid = match st.children.get(&key) {
Some(&id) => id,
None => {
let id = st.next_nodeid;
st.next_nodeid += 1;
st.inodes.insert(
id,
InodeInfo {
host_path: path.clone(),
kind: kind_from_meta(&md),
},
);
st.children.insert(key, id);
id
}
};
let attr = attr_from_meta(nodeid, &md);
Ok(Entry {
nodeid,
generation: 0,
attr,
entry_valid: 1,
attr_valid: 1,
})
}
fn forget(&self, _nodeid: u64, _nlookup: u64) {
}
fn getattr(&self, nodeid: u64, _fh: Option<u64>) -> Result<Attr, Errno> {
let path = self.host_path_of(nodeid)?;
let md = std::fs::symlink_metadata(&path)
.map_err(|e| io_err_to_linux(&e))?;
Ok(attr_from_meta(nodeid, &md))
}
fn open(&self, nodeid: u64, flags: u32) -> Result<u64, Errno> {
let path = self.host_path_of(nodeid)?;
match self.kind_of(nodeid)? {
Kind::Dir => return Err(EISDIR),
_ => {}
}
self.watch_inode(nodeid, &path);
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let access = flags as i32 & libc::O_ACCMODE;
let fd = unsafe { libc::open(c.as_ptr(), access) };
if fd < 0 {
return Err(errno_now());
}
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let mut st = self.st.lock().unwrap();
let fh = st.next_fh;
st.next_fh += 1;
st.handles.insert(fh, owned);
Ok(fh)
}
fn read(&self, _nodeid: u64, fh: u64, offset: u64, size: u32) -> Result<Vec<u8>, Errno> {
let st = self.st.lock().unwrap();
let raw = st.handles.get(&fh).ok_or(EBADF)?.as_raw_fd();
drop(st);
let mut buf = vec![0u8; size as usize];
let n = unsafe {
libc::pread(raw, buf.as_mut_ptr() as *mut _, buf.len(), offset as libc::off_t)
};
if n < 0 {
return Err(errno_now());
}
buf.truncate(n as usize);
Ok(buf)
}
fn release(&self, _nodeid: u64, fh: u64) -> Result<(), Errno> {
let mut st = self.st.lock().unwrap();
st.handles.remove(&fh).ok_or(EBADF).map(|_| ())
}
fn write(&self, _nodeid: u64, fh: u64, offset: u64, data: &[u8]) -> Result<u32, Errno> {
let st = self.st.lock().unwrap();
let raw = st.handles.get(&fh).ok_or(EBADF)?.as_raw_fd();
drop(st);
let n = unsafe {
libc::pwrite(
raw,
data.as_ptr() as *const _,
data.len(),
offset as libc::off_t,
)
};
if n < 0 {
return Err(errno_now());
}
Ok(n as u32)
}
fn fsync(&self, _nodeid: u64, fh: u64, datasync: bool) -> Result<(), Errno> {
let st = self.st.lock().unwrap();
let raw = st.handles.get(&fh).ok_or(EBADF)?.as_raw_fd();
drop(st);
let rc = unsafe {
if datasync {
libc::fsync(raw) } else {
libc::fsync(raw)
}
};
if rc != 0 {
return Err(errno_now());
}
Ok(())
}
fn opendir(&self, nodeid: u64, _flags: u32) -> Result<u64, Errno> {
let path = self.host_path_of(nodeid)?;
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let fd = unsafe { libc::open(c.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY) };
if fd < 0 {
return Err(errno_now());
}
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let mut st = self.st.lock().unwrap();
let fh = st.next_fh;
st.next_fh += 1;
st.handles.insert(fh, owned);
Ok(fh)
}
fn readdir(
&self,
nodeid: u64,
_fh: u64,
offset: u64,
_size: u32,
) -> Result<Vec<DirEntry>, Errno> {
let path = self.host_path_of(nodeid)?;
let rd = std::fs::read_dir(&path).map_err(|e| io_err_to_linux(&e))?;
let mut out = Vec::new();
for (i, entry_res) in rd.enumerate() {
if (i as u64) < offset {
continue;
}
let entry = match entry_res {
Ok(e) => e,
Err(_) => continue,
};
let typ = match entry.file_type() {
Ok(t) if t.is_dir() => DT_DIR,
Ok(t) if t.is_file() => DT_REG,
Ok(t) if t.is_symlink() => DT_LNK,
Ok(t) => match t {
t if t.is_block_device() => DT_BLK,
t if t.is_char_device() => DT_CHR,
t if t.is_fifo() => DT_FIFO,
t if t.is_socket() => DT_SOCK,
_ => DT_UNKNOWN,
},
Err(_) => DT_UNKNOWN,
};
let ino = entry.metadata().map(|m| m.ino()).unwrap_or(0);
out.push(DirEntry {
ino,
name: entry.file_name().as_bytes().to_vec(),
typ,
});
}
Ok(out)
}
fn releasedir(&self, _nodeid: u64, fh: u64) -> Result<(), Errno> {
let mut st = self.st.lock().unwrap();
st.handles.remove(&fh).ok_or(EBADF).map(|_| ())
}
fn statfs(&self, nodeid: u64) -> Result<StatFs, Errno> {
let path = self.host_path_of(nodeid)?;
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let mut s: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(c.as_ptr(), &mut s) } < 0 {
return Err(errno_now());
}
Ok(StatFs {
blocks: s.f_blocks,
bfree: s.f_bfree,
bavail: s.f_bavail,
files: s.f_files,
ffree: s.f_ffree,
bsize: s.f_bsize as u32,
namelen: 255,
frsize: s.f_bsize as u32,
})
}
fn create(
&self,
parent: u64,
name: &OsStr,
mode: u32,
flags: u32,
) -> Result<(crate::fuse::backend::Entry, u64), Errno> {
name_safe(name)?;
let parent_path = self.host_path_of(parent)?;
let full = parent_path.join(name);
let c = CString::new(full.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let access = flags as i32 & libc::O_ACCMODE;
let fd = unsafe {
libc::open(
c.as_ptr(),
access | libc::O_CREAT | libc::O_EXCL,
mode as libc::c_uint,
)
};
if fd < 0 {
return Err(errno_now());
}
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let md = std::fs::metadata(&full).map_err(|e| io_err_to_linux(&e))?;
let mut st = self.st.lock().unwrap();
let nodeid = st.next_nodeid;
st.next_nodeid += 1;
st.inodes.insert(
nodeid,
InodeInfo {
host_path: full.clone(),
kind: kind_from_meta(&md),
},
);
st.children.insert((parent, name.as_bytes().to_vec()), nodeid);
let fh = st.next_fh;
st.next_fh += 1;
st.handles.insert(fh, owned);
let attr = attr_from_meta(nodeid, &md);
Ok((
crate::fuse::backend::Entry {
nodeid,
generation: 0,
attr,
entry_valid: 1,
attr_valid: 1,
},
fh,
))
}
fn mkdir(&self, parent: u64, name: &OsStr, mode: u32) -> Result<crate::fuse::backend::Entry, Errno> {
name_safe(name)?;
let parent_path = self.host_path_of(parent)?;
let full = parent_path.join(name);
let c = CString::new(full.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let rc = unsafe { libc::mkdir(c.as_ptr(), mode as libc::mode_t) };
if rc != 0 {
return Err(errno_now());
}
let md = std::fs::metadata(&full).map_err(|e| io_err_to_linux(&e))?;
let mut st = self.st.lock().unwrap();
let nodeid = st.next_nodeid;
st.next_nodeid += 1;
st.inodes.insert(
nodeid,
InodeInfo {
host_path: full,
kind: Kind::Dir,
},
);
st.children.insert((parent, name.as_bytes().to_vec()), nodeid);
Ok(crate::fuse::backend::Entry {
nodeid,
generation: 0,
attr: attr_from_meta(nodeid, &md),
entry_valid: 1,
attr_valid: 1,
})
}
fn unlink(&self, parent: u64, name: &OsStr) -> Result<(), Errno> {
name_safe(name)?;
let parent_path = self.host_path_of(parent)?;
let full = parent_path.join(name);
let c = CString::new(full.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let rc = unsafe { libc::unlink(c.as_ptr()) };
if rc != 0 {
return Err(errno_now());
}
let mut st = self.st.lock().unwrap();
st.children.remove(&(parent, name.as_bytes().to_vec()));
Ok(())
}
fn rmdir(&self, parent: u64, name: &OsStr) -> Result<(), Errno> {
name_safe(name)?;
let parent_path = self.host_path_of(parent)?;
let full = parent_path.join(name);
let c = CString::new(full.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let rc = unsafe { libc::rmdir(c.as_ptr()) };
if rc != 0 {
return Err(errno_now());
}
let mut st = self.st.lock().unwrap();
st.children.remove(&(parent, name.as_bytes().to_vec()));
Ok(())
}
fn symlink(
&self,
parent: u64,
name: &OsStr,
target: &OsStr,
) -> Result<crate::fuse::backend::Entry, Errno> {
if self.symlinks == SymlinkPolicy::Deny {
return Err(EPERM);
}
name_safe(name)?;
if target.as_bytes().is_empty() || target.as_bytes().contains(&0u8) {
return Err(EINVAL);
}
let parent_path = self.host_path_of(parent)?;
let parent_dirfd = open_dirfd(&parent_path)?;
let target_c = CString::new(target.as_bytes()).map_err(|_| EINVAL)?;
let name_c = CString::new(name.as_bytes()).map_err(|_| EINVAL)?;
let rc = unsafe {
libc::symlinkat(target_c.as_ptr(), parent_dirfd.as_raw_fd(), name_c.as_ptr())
};
if rc != 0 {
return Err(errno_now());
}
let mut stb: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::fstatat(
parent_dirfd.as_raw_fd(),
name_c.as_ptr(),
&mut stb,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if rc != 0 {
return Err(errno_now());
}
let full = parent_path.join(name);
let mut st = self.st.lock().unwrap();
let nodeid = st.next_nodeid;
st.next_nodeid += 1;
st.inodes.insert(
nodeid,
InodeInfo {
host_path: full,
kind: Kind::Symlink,
},
);
st.children.insert((parent, name.as_bytes().to_vec()), nodeid);
Ok(crate::fuse::backend::Entry {
nodeid,
generation: 0,
attr: attr_from_stat(nodeid, &stb),
entry_valid: 1,
attr_valid: 1,
})
}
fn readlink(&self, nodeid: u64) -> Result<Vec<u8>, Errno> {
let path = self.host_path_of(nodeid)?;
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let mut buf = vec![0u8; 4096];
let n = unsafe { libc::readlink(c.as_ptr(), buf.as_mut_ptr() as *mut _, buf.len()) };
if n < 0 {
return Err(errno_now());
}
buf.truncate(n as usize);
Ok(buf)
}
fn link(
&self,
nodeid: u64,
new_parent: u64,
new_name: &OsStr,
) -> Result<crate::fuse::backend::Entry, Errno> {
if self.symlinks == SymlinkPolicy::Deny {
return Err(EPERM);
}
name_safe(new_name)?;
let src_path = self.host_path_of(nodeid)?;
let new_parent_path = self.host_path_of(new_parent)?;
let dst_path = new_parent_path.join(new_name);
let src_c = CString::new(src_path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let dst_c = CString::new(dst_path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let rc = unsafe { libc::link(src_c.as_ptr(), dst_c.as_ptr()) };
if rc != 0 {
return Err(errno_now());
}
let dst_c2 = CString::new(dst_path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let mut stb: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::lstat(dst_c2.as_ptr(), &mut stb) };
if rc != 0 {
return Err(errno_now());
}
let mut st = self.st.lock().unwrap();
let new_nodeid = st.next_nodeid;
st.next_nodeid += 1;
let kind = if (stb.st_mode as u32 & S_IFMT) == S_IFDIR {
Kind::Dir
} else if (stb.st_mode as u32 & S_IFMT) == S_IFREG {
Kind::File
} else if (stb.st_mode as u32 & S_IFMT) == S_IFLNK {
Kind::Symlink
} else {
Kind::Other
};
st.inodes.insert(
new_nodeid,
InodeInfo {
host_path: dst_path,
kind,
},
);
st.children
.insert((new_parent, new_name.as_bytes().to_vec()), new_nodeid);
Ok(crate::fuse::backend::Entry {
nodeid: new_nodeid,
generation: 0,
attr: attr_from_stat(new_nodeid, &stb),
entry_valid: 1,
attr_valid: 1,
})
}
fn setattr(
&self,
nodeid: u64,
fh: Option<u64>,
attr: SetattrIn,
) -> Result<Attr, Errno> {
let path = self.host_path_of(nodeid)?;
let fd_raw: Option<libc::c_int> = if let Some(handle) = fh {
let st = self.st.lock().unwrap();
Some(st.handles.get(&handle).ok_or(EBADF)?.as_raw_fd())
} else {
None
};
let path_c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
if attr.valid & FATTR_SIZE != 0 {
let rc = unsafe {
if let Some(fd) = fd_raw {
libc::ftruncate(fd, attr.size as libc::off_t)
} else {
libc::truncate(path_c.as_ptr(), attr.size as libc::off_t)
}
};
if rc != 0 {
return Err(errno_now());
}
}
if attr.valid & FATTR_MODE != 0 {
let mode_bits = (attr.mode & 0o7777) as libc::mode_t;
let rc = unsafe {
if let Some(fd) = fd_raw {
libc::fchmod(fd, mode_bits)
} else {
libc::chmod(path_c.as_ptr(), mode_bits)
}
};
if rc != 0 {
return Err(errno_now());
}
}
if attr.valid & (FATTR_UID | FATTR_GID) != 0 {
let uid = if attr.valid & FATTR_UID != 0 {
attr.uid as libc::uid_t
} else {
u32::MAX as libc::uid_t
};
let gid = if attr.valid & FATTR_GID != 0 {
attr.gid as libc::gid_t
} else {
u32::MAX as libc::gid_t
};
let rc = unsafe {
if let Some(fd) = fd_raw {
libc::fchown(fd, uid, gid)
} else {
libc::lchown(path_c.as_ptr(), uid, gid)
}
};
if rc != 0 {
return Err(errno_now());
}
}
let want_times = attr.valid
& (FATTR_ATIME | FATTR_MTIME | FATTR_ATIME_NOW | FATTR_MTIME_NOW)
!= 0;
if want_times {
let ts_atime = if attr.valid & FATTR_ATIME_NOW != 0 {
libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_NOW }
} else if attr.valid & FATTR_ATIME != 0 {
libc::timespec {
tv_sec: attr.atime as libc::time_t,
tv_nsec: attr.atimensec as libc::c_long,
}
} else {
libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_OMIT }
};
let ts_mtime = if attr.valid & FATTR_MTIME_NOW != 0 {
libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_NOW }
} else if attr.valid & FATTR_MTIME != 0 {
libc::timespec {
tv_sec: attr.mtime as libc::time_t,
tv_nsec: attr.mtimensec as libc::c_long,
}
} else {
libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_OMIT }
};
let times = [ts_atime, ts_mtime];
let rc = unsafe {
libc::utimensat(
libc::AT_FDCWD,
path_c.as_ptr(),
times.as_ptr(),
libc::AT_SYMLINK_NOFOLLOW,
)
};
if rc != 0 {
return Err(errno_now());
}
}
let mut stb: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::lstat(path_c.as_ptr(), &mut stb) };
if rc != 0 {
return Err(errno_now());
}
Ok(attr_from_stat(nodeid, &stb))
}
fn rename(
&self,
old_parent: u64,
old_name: &OsStr,
new_parent: u64,
new_name: &OsStr,
flags: u32,
) -> Result<(), Errno> {
name_safe(old_name)?;
name_safe(new_name)?;
if flags != 0 {
return Err(EINVAL);
}
let old_parent_path = self.host_path_of(old_parent)?;
let new_parent_path = self.host_path_of(new_parent)?;
let old_full = old_parent_path.join(old_name);
let new_full = new_parent_path.join(new_name);
let old_dirfd = open_dirfd(&old_parent_path)?;
let new_dirfd = open_dirfd(&new_parent_path)?;
let old_c = CString::new(old_name.as_bytes()).map_err(|_| EINVAL)?;
let new_c = CString::new(new_name.as_bytes()).map_err(|_| EINVAL)?;
let rc = unsafe {
libc::renameat(
old_dirfd.as_raw_fd(),
old_c.as_ptr(),
new_dirfd.as_raw_fd(),
new_c.as_ptr(),
)
};
if rc != 0 {
return Err(errno_now());
}
let mut st = self.st.lock().unwrap();
let old_key = (old_parent, old_name.as_bytes().to_vec());
let new_key = (new_parent, new_name.as_bytes().to_vec());
if let Some(nodeid) = st.children.remove(&old_key) {
st.children.remove(&new_key);
st.children.insert(new_key, nodeid);
if let Some(info) = st.inodes.get_mut(&nodeid) {
info.host_path = new_full.clone();
}
let old_prefix = old_full.clone();
for (id, info) in st.inodes.iter_mut() {
if *id == nodeid {
continue;
}
if let Ok(suffix) = info.host_path.strip_prefix(&old_prefix) {
info.host_path = new_full.join(suffix);
}
}
} else {
st.children.remove(&new_key);
}
Ok(())
}
fn flush(&self, _nodeid: u64, fh: u64) -> Result<(), Errno> {
let st = self.st.lock().unwrap();
st.handles.get(&fh).ok_or(EBADF)?;
Ok(())
}
fn fsyncdir(&self, _nodeid: u64, fh: u64, _datasync: bool) -> Result<(), Errno> {
let st = self.st.lock().unwrap();
st.handles.get(&fh).ok_or(EBADF)?;
Ok(())
}
fn dax_map(
&self,
nodeid: u64,
fh: u64,
foffset: u64,
len: u64,
prot: u32,
) -> Result<*mut u8, Errno> {
let opened_fresh: Option<OwnedFd>;
let raw = if fh == u64::MAX {
let path = self.host_path_of(nodeid)?;
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let mut fd = unsafe { libc::open(c.as_ptr(), libc::O_RDWR) };
if fd < 0 {
fd = unsafe { libc::open(c.as_ptr(), libc::O_RDONLY) };
}
if fd < 0 {
return Err(errno_now());
}
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let raw = owned.as_raw_fd();
opened_fresh = Some(owned);
raw
} else {
let st = self.st.lock().unwrap();
let raw = st.handles.get(&fh).ok_or(EBADF)?.as_raw_fd();
drop(st);
opened_fresh = None;
raw
};
let _ = &opened_fresh;
let _ = prot; let host_prot = libc::PROT_READ | libc::PROT_WRITE;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
len as usize,
host_prot,
libc::MAP_SHARED,
raw,
foffset as libc::off_t,
)
};
if ptr == libc::MAP_FAILED {
return Err(errno_now());
}
let mut st = self.st.lock().unwrap();
st.dax_mmaps.insert(
ptr as usize,
Mmap {
ptr: ptr as *mut u8,
len: len as usize,
},
);
Ok(ptr as *mut u8)
}
fn dax_unmap(&self, _nodeid: u64, host_va: *mut u8, _len: u64) -> Result<(), Errno> {
let mut st = self.st.lock().unwrap();
let m = st.dax_mmaps.remove(&(host_va as usize)).ok_or(EINVAL)?;
let rc = unsafe { libc::munmap(m.ptr as *mut _, m.len) };
if rc != 0 {
return Err(errno_now());
}
Ok(())
}
}
fn run_watcher(inner: Arc<WatcherInner>) {
let wakeup = libc::kevent {
ident: 0,
filter: libc::EVFILT_USER,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: 0,
data: 0,
udata: std::ptr::null_mut(),
};
let mut w = wakeup;
unsafe {
libc::kevent(
inner.kq,
&mut w as *mut _,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
);
}
let mut events: [libc::kevent; 16] = unsafe { std::mem::zeroed() };
loop {
if inner.stop.load(Ordering::Acquire) {
break;
}
let n = unsafe {
libc::kevent(
inner.kq,
std::ptr::null(),
0,
events.as_mut_ptr(),
events.len() as libc::c_int,
std::ptr::null(),
)
};
if n < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
eprintln!("[posix-fs watcher] kevent failed: {err}; thread exiting");
return;
}
if inner.stop.load(Ordering::Acquire) {
break;
}
for ev in events.iter().take(n as usize) {
if ev.filter == libc::EVFILT_USER {
continue;
}
let fd = ev.ident as libc::c_int;
let entry = inner
.watched
.lock()
.unwrap()
.get(&fd)
.map(|e| (e.nodeid, e.parent_nodeid, e.name.clone()));
let Some((nodeid, parent_nodeid, name)) = entry else { continue };
if let Some(n) = inner.notifier.lock().unwrap().as_ref() {
n.invalidate_inode(nodeid, 0, -1);
n.invalidate_inode(nodeid, 0, 0);
n.invalidate_entry(parent_nodeid, &name);
}
if ev.fflags & (libc::NOTE_DELETE | libc::NOTE_RENAME) != 0 {
inner.watched.lock().unwrap().remove(&fd);
}
}
}
}
use std::os::unix::fs::FileTypeExt;
#[allow(unused_imports)]
use std::convert::TryFrom;
#[allow(dead_code)]
const _: () = {
let _ = OsString::new;
let _ = ENOSPC;
let _ = ENOTDIR;
let _ = EACCES;
let _ = EIO;
};
#[cfg(test)]
mod tests {
use super::*;
fn tmpdir(name: &str) -> PathBuf {
let pid = unsafe { libc::getpid() };
let p = std::env::temp_dir().join(format!("posixfs-{pid}-{name}"));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn lookup_and_read_real_file() {
let dir = tmpdir("t1");
std::fs::write(dir.join("hello.txt"), b"hi from posix").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("hello.txt")).unwrap();
assert!(e.attr.size == 13);
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let buf = fs.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(buf, b"hi from posix");
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn readdir_lists_real_entries_with_types() {
let dir = tmpdir("t2");
std::fs::write(dir.join("a.txt"), b"a").unwrap();
std::fs::create_dir_all(dir.join("sub")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let dh = fs.opendir(FUSE_ROOT_ID, 0).unwrap();
let entries = fs.readdir(FUSE_ROOT_ID, dh, 0, 4096).unwrap();
let by_name: std::collections::HashMap<&[u8], u32> =
entries.iter().map(|e| (e.name.as_slice(), e.typ)).collect();
assert_eq!(by_name[&b"a.txt"[..]], DT_REG);
assert_eq!(by_name[&b"sub"[..]], DT_DIR);
fs.releasedir(FUSE_ROOT_ID, dh).unwrap();
}
#[test]
fn lookup_rejects_dotdot() {
let dir = tmpdir("t3");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("..")).unwrap_err();
assert_eq!(err, EINVAL);
}
#[test]
fn lookup_rejects_slash_in_name() {
let dir = tmpdir("t4");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("a/b")).unwrap_err();
assert_eq!(err, EINVAL);
}
#[test]
fn lookup_rejects_embedded_nul_in_name() {
let dir = tmpdir("t4n");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::from_bytes(b"foo\0bar")).unwrap_err();
assert_eq!(err, EINVAL);
}
#[test]
fn lookup_blocks_external_symlink_by_default() {
let dir = tmpdir("t-sym-default");
let outside = tmpdir("t-sym-outside");
std::fs::write(outside.join("secret.txt"), b"do not leak").unwrap();
std::os::unix::fs::symlink(&outside, dir.join("escape")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("escape")).unwrap_err();
assert_eq!(err, EACCES, "external symlink must be denied");
}
#[test]
fn lookup_allows_internal_symlink_by_default() {
let dir = tmpdir("t-sym-internal");
std::fs::create_dir_all(dir.join("real")).unwrap();
std::fs::write(dir.join("real/data.txt"), b"in-tree data").unwrap();
std::os::unix::fs::symlink("real/data.txt", dir.join("link.txt")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("link.txt")).unwrap();
assert!(e.attr.size > 0);
}
#[test]
fn lookup_succeeds_on_broken_symlink() {
let dir = tmpdir("t-sym-broken");
std::os::unix::fs::symlink(
"does-not-exist-anywhere.txt",
dir.join("dangling"),
)
.unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("dangling")).unwrap();
assert_eq!(e.attr.mode & S_IFMT, S_IFLNK, "attr must report symlink type");
}
#[test]
fn lookup_returns_symlink_attrs_not_target_attrs() {
let dir = tmpdir("t-sym-attrs");
std::fs::write(dir.join("real.txt"), b"twelve bytes").unwrap();
std::os::unix::fs::symlink("real.txt", dir.join("link")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("link")).unwrap();
assert_eq!(e.attr.mode & S_IFMT, S_IFLNK, "must report symlink type");
assert_eq!(e.attr.size, "real.txt".len() as u64);
}
#[test]
fn getattr_returns_symlink_attrs_not_target_attrs() {
let dir = tmpdir("t-sym-getattr");
std::fs::write(dir.join("target.bin"), vec![0u8; 4096]).unwrap();
std::os::unix::fs::symlink("target.bin", dir.join("alias")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("alias")).unwrap();
let attr = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(attr.mode & S_IFMT, S_IFLNK);
assert_eq!(attr.size, "target.bin".len() as u64);
}
#[test]
fn lookup_succeeds_on_recently_created_symlink() {
let dir = tmpdir("t-sym-recent");
std::fs::create_dir_all(dir.join("napi-postinstall/lib")).unwrap();
std::fs::write(dir.join("napi-postinstall/lib/cli.js"), b"console.log('hi')").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let bin_entry = fs
.symlink(
FUSE_ROOT_ID,
OsStr::new("napi-postinstall-bin"),
OsStr::new("../napi-postinstall/lib/cli.js"),
)
.unwrap();
assert_eq!(bin_entry.attr.mode & S_IFMT, S_IFLNK);
let e = fs
.lookup(FUSE_ROOT_ID, OsStr::new("napi-postinstall-bin"))
.unwrap();
assert_eq!(e.attr.mode & S_IFMT, S_IFLNK);
let attr = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(attr.mode & S_IFMT, S_IFLNK);
}
#[test]
fn lookup_allows_external_symlink_when_opted_in() {
let dir = tmpdir("t-sym-opt-in");
let outside = tmpdir("t-sym-target");
std::fs::write(outside.join("ok.txt"), b"opt-in data").unwrap();
std::os::unix::fs::symlink(outside.join("ok.txt"), dir.join("link")).unwrap();
let fs = PosixFs::new_unchecked(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("link")).unwrap();
assert!(e.attr.size > 0);
}
#[test]
fn dax_map_then_unmap_round_trip() {
let dir = tmpdir("t5");
let path = dir.join("data.bin");
let mut data = vec![0u8; 32 * 1024];
for (i, b) in data.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
std::fs::write(&path, &data).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("data.bin")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDWR as u32).unwrap();
let host_va = fs.dax_map(e.nodeid, fh, 0, 32 * 1024, 0).unwrap();
assert!(!host_va.is_null());
let host_slice = unsafe { std::slice::from_raw_parts(host_va, 32 * 1024) };
assert_eq!(host_slice, &data[..]);
fs.dax_unmap(e.nodeid, host_va, 32 * 1024).unwrap();
assert_eq!(fs.dax_unmap(e.nodeid, host_va, 32 * 1024).unwrap_err(), EINVAL);
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn read_eof_returns_empty() {
let dir = tmpdir("t6");
std::fs::write(dir.join("x"), b"short").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("x")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let eof = fs.read(e.nodeid, fh, 100, 10).unwrap();
assert!(eof.is_empty());
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn open_directory_returns_eisdir() {
let dir = tmpdir("t7");
std::fs::create_dir_all(dir.join("sub")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("sub")).unwrap();
let err = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap_err();
assert_eq!(err, EISDIR);
}
#[test]
fn symlink_create_inside_mount() {
let dir = tmpdir("t-sym-create");
let fs = PosixFs::new(&dir).unwrap();
let e = fs
.symlink(FUSE_ROOT_ID, OsStr::new("link"), OsStr::new("target"))
.unwrap();
assert_eq!(e.attr.mode & S_IFMT, S_IFLNK);
let target = fs.readlink(e.nodeid).unwrap();
assert_eq!(target, b"target");
let md = std::fs::symlink_metadata(dir.join("link")).unwrap();
assert!(md.file_type().is_symlink());
}
#[test]
fn symlink_create_external_target_stored_verbatim() {
let dir = tmpdir("t-sym-external");
let fs = PosixFs::new(&dir).unwrap();
let e = fs
.symlink(FUSE_ROOT_ID, OsStr::new("escape"), OsStr::new("/etc/passwd"))
.unwrap();
let target = fs.readlink(e.nodeid).unwrap();
assert_eq!(target, b"/etc/passwd");
let read = std::fs::read_link(dir.join("escape")).unwrap();
assert_eq!(read.as_os_str(), OsStr::new("/etc/passwd"));
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("escape")).unwrap_err();
assert_eq!(err, EACCES);
}
#[test]
fn symlink_create_blocked_by_deny() {
let dir = tmpdir("t-sym-deny");
let fs = PosixFs::new_with_symlinks(&dir, SymlinkPolicy::Deny).unwrap();
let err = fs
.symlink(FUSE_ROOT_ID, OsStr::new("link"), OsStr::new("target"))
.unwrap_err();
assert_eq!(err, EPERM);
assert!(std::fs::symlink_metadata(dir.join("link")).is_err());
}
#[test]
fn link_create_inside_mount() {
let dir = tmpdir("t-link");
std::fs::write(dir.join("orig"), b"hello").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let src = fs.lookup(FUSE_ROOT_ID, OsStr::new("orig")).unwrap();
let new_entry = fs
.link(src.nodeid, FUSE_ROOT_ID, OsStr::new("alias"))
.unwrap();
assert_eq!(new_entry.attr.nlink, 2);
let fh = fs.open(new_entry.nodeid, libc::O_RDONLY as u32).unwrap();
let buf = fs.read(new_entry.nodeid, fh, 0, 64).unwrap();
assert_eq!(buf, b"hello");
fs.release(new_entry.nodeid, fh).unwrap();
}
#[test]
fn link_blocked_by_deny() {
let dir = tmpdir("t-link-deny");
std::fs::write(dir.join("orig"), b"hi").unwrap();
let fs = PosixFs::new_with_symlinks(&dir, SymlinkPolicy::Deny).unwrap();
let src = fs.lookup(FUSE_ROOT_ID, OsStr::new("orig")).unwrap();
let err = fs
.link(src.nodeid, FUSE_ROOT_ID, OsStr::new("alias"))
.unwrap_err();
assert_eq!(err, EPERM);
assert!(std::fs::symlink_metadata(dir.join("alias")).is_err());
}
#[test]
fn lookup_with_opaque_blocks_external_symlink() {
let dir = tmpdir("t-sym-opaque-blocks");
let outside = tmpdir("t-sym-opaque-out");
std::fs::write(outside.join("x"), b"secret").unwrap();
std::os::unix::fs::symlink(&outside, dir.join("escape")).unwrap();
let fs = PosixFs::new_with_symlinks(&dir, SymlinkPolicy::Opaque).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("escape")).unwrap_err();
assert_eq!(err, EACCES);
}
#[test]
fn lookup_with_follow_allows_external_symlink() {
let dir = tmpdir("t-sym-follow");
let outside = tmpdir("t-sym-follow-out");
std::fs::write(outside.join("ok.txt"), b"out-of-mount data").unwrap();
std::os::unix::fs::symlink(outside.join("ok.txt"), dir.join("link")).unwrap();
let fs = PosixFs::new_with_symlinks(&dir, SymlinkPolicy::Follow).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("link")).unwrap();
assert!(e.attr.size > 0);
}
#[test]
fn rmdir_nonempty_returns_linux_enotempty() {
let dir = tmpdir("t-rmdir-nonempty");
std::fs::create_dir_all(dir.join("sub")).unwrap();
std::fs::write(dir.join("sub/inside"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("sub")).unwrap_err();
assert_eq!(err, -39, "rmdir non-empty must surface as Linux ENOTEMPTY (39), got {err}");
}
#[test]
fn rmdir_empty_succeeds() {
let dir = tmpdir("t-rmdir-empty");
std::fs::create_dir_all(dir.join("sub")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
fs.rmdir(FUSE_ROOT_ID, OsStr::new("sub")).unwrap();
assert!(!dir.join("sub").exists());
}
#[test]
fn host_to_linux_errno_table() {
#[cfg(target_os = "macos")]
{
assert_eq!(host_to_linux_errno(35), 11);
assert_eq!(host_to_linux_errno(11), 35);
assert_eq!(host_to_linux_errno(63), 36);
assert_eq!(host_to_linux_errno(78), 38);
assert_eq!(host_to_linux_errno(66), 39);
assert_eq!(host_to_linux_errno(62), 40);
assert_eq!(host_to_linux_errno(84), 75);
assert_eq!(host_to_linux_errno(38), 88);
assert_eq!(host_to_linux_errno(102), 95);
assert_eq!(host_to_linux_errno(60), 110);
assert_eq!(host_to_linux_errno(61), 111);
assert_eq!(host_to_linux_errno(36), 115);
assert_eq!(host_to_linux_errno(70), 116);
assert_eq!(host_to_linux_errno(89), 125);
assert_eq!(host_to_linux_errno(105), 130);
assert_eq!(host_to_linux_errno(104), 131);
}
assert_eq!(host_to_linux_errno(libc::EPERM), 1);
assert_eq!(host_to_linux_errno(libc::ENOENT), 2);
assert_eq!(host_to_linux_errno(libc::EINTR), 4);
assert_eq!(host_to_linux_errno(libc::EIO), 5);
assert_eq!(host_to_linux_errno(libc::EINVAL), 22);
assert_eq!(host_to_linux_errno(libc::ERANGE), 34);
}
fn make_setattr(valid: u32) -> SetattrIn {
SetattrIn {
valid,
padding: 0,
fh: 0,
size: 0,
lock_owner: 0,
atime: 0,
mtime: 0,
ctime: 0,
atimensec: 0,
mtimensec: 0,
ctimensec: 0,
mode: 0,
unused4: 0,
uid: 0,
gid: 0,
unused5: 0,
}
}
#[test]
fn setattr_chmod() {
let dir = tmpdir("t-setattr-chmod");
let path = dir.join("f");
std::fs::write(&path, b"data").unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o644);
std::fs::set_permissions(&path, perms).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_MODE);
req.mode = 0o600;
let attr = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(attr.mode & 0o7777, 0o600);
let md = std::fs::metadata(&path).unwrap();
let host_mode = std::os::unix::fs::PermissionsExt::mode(&md.permissions()) & 0o7777;
assert_eq!(host_mode, 0o600);
}
#[test]
fn setattr_truncate() {
let dir = tmpdir("t-setattr-truncate");
let path = dir.join("f");
std::fs::write(&path, vec![0xABu8; 1024]).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_SIZE);
req.size = 10;
let attr = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(attr.size, 10);
assert_eq!(std::fs::metadata(&path).unwrap().len(), 10);
}
#[test]
fn setattr_truncate_via_fh() {
let dir = tmpdir("t-setattr-truncate-fh");
let path = dir.join("f");
std::fs::write(&path, vec![0u8; 256]).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDWR as u32).unwrap();
let mut req = make_setattr(FATTR_SIZE);
req.size = 64;
let attr = fs.setattr(e.nodeid, Some(fh), req).unwrap();
assert_eq!(attr.size, 64);
assert_eq!(std::fs::metadata(&path).unwrap().len(), 64);
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn setattr_utimens() {
let dir = tmpdir("t-setattr-utimens");
let path = dir.join("f");
std::fs::write(&path, b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fixed_mtime: u64 = 1_500_000_000; let mut req = make_setattr(FATTR_MTIME);
req.mtime = fixed_mtime;
req.mtimensec = 0;
let attr = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(attr.mtime, fixed_mtime);
let md = std::fs::metadata(&path).unwrap();
assert_eq!(md.mtime() as u64, fixed_mtime);
}
#[test]
fn rename_within_dir() {
let dir = tmpdir("t-rename-same");
std::fs::write(dir.join("from"), b"payload").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let _ = fs.lookup(FUSE_ROOT_ID, OsStr::new("from")).unwrap();
fs.rename(
FUSE_ROOT_ID,
OsStr::new("from"),
FUSE_ROOT_ID,
OsStr::new("to"),
0,
)
.unwrap();
assert!(!dir.join("from").exists());
assert!(dir.join("to").exists());
assert_eq!(std::fs::read(dir.join("to")).unwrap(), b"payload");
let new_e = fs.lookup(FUSE_ROOT_ID, OsStr::new("to")).unwrap();
assert!(new_e.attr.size == 7);
}
#[test]
fn rename_across_dirs() {
let dir = tmpdir("t-rename-cross");
std::fs::create_dir_all(dir.join("a")).unwrap();
std::fs::create_dir_all(dir.join("b")).unwrap();
std::fs::write(dir.join("a/file"), b"cross-dir").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let a = fs.lookup(FUSE_ROOT_ID, OsStr::new("a")).unwrap();
let b = fs.lookup(FUSE_ROOT_ID, OsStr::new("b")).unwrap();
let _ = fs.lookup(a.nodeid, OsStr::new("file")).unwrap();
fs.rename(
a.nodeid,
OsStr::new("file"),
b.nodeid,
OsStr::new("file"),
0,
)
.unwrap();
assert!(!dir.join("a/file").exists());
assert_eq!(std::fs::read(dir.join("b/file")).unwrap(), b"cross-dir");
let new_e = fs.lookup(b.nodeid, OsStr::new("file")).unwrap();
assert_eq!(new_e.attr.size, 9);
}
#[test]
fn rename_atomic_write_pattern() {
let dir = tmpdir("t-rename-atomic");
std::fs::write(dir.join("file.tmp"), b"new contents").unwrap();
let fs = PosixFs::new(&dir).unwrap();
fs.rename(
FUSE_ROOT_ID,
OsStr::new("file.tmp"),
FUSE_ROOT_ID,
OsStr::new("file"),
0,
)
.unwrap();
assert!(!dir.join("file.tmp").exists());
assert_eq!(std::fs::read(dir.join("file")).unwrap(), b"new contents");
}
#[test]
fn rename_rejects_nonzero_flags() {
let dir = tmpdir("t-rename-flags");
std::fs::write(dir.join("a"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs
.rename(
FUSE_ROOT_ID,
OsStr::new("a"),
FUSE_ROOT_ID,
OsStr::new("b"),
1, )
.unwrap_err();
assert_eq!(err, EINVAL);
}
#[test]
fn flush_is_noop() {
let dir = tmpdir("t-flush");
std::fs::write(dir.join("f"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
fs.flush(e.nodeid, fh).unwrap();
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn flush_unknown_fh_returns_ebadf() {
let dir = tmpdir("t-flush-ebadf");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.flush(FUSE_ROOT_ID, 999_999).unwrap_err();
assert_eq!(err, EBADF);
}
#[test]
fn fsyncdir_is_noop() {
let dir = tmpdir("t-fsyncdir");
let fs = PosixFs::new(&dir).unwrap();
let dh = fs.opendir(FUSE_ROOT_ID, 0).unwrap();
fs.fsyncdir(FUSE_ROOT_ID, dh, false).unwrap();
fs.fsyncdir(FUSE_ROOT_ID, dh, true).unwrap();
fs.releasedir(FUSE_ROOT_ID, dh).unwrap();
}
#[test]
#[ignore = "requires a booted VM; turn this into a tsi_loopback-style integration test"]
fn symlink_inside_guest_via_python_image() {
}
}