use std::collections::BTreeMap;
use std::ffi::{CString, OsStr, OsString};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use super::backend::{
DirEntry, Entry, Errno, FsBackend, StatFs, EACCES, EBADF, EEXIST as EEXIST_ERRNO, EINVAL, EIO,
EISDIR, ENOENT, ENOSPC, ENOTDIR, EPERM,
};
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,
};
use crate::vmm::resources::SymlinkPolicy;
const ENTRY_VALID_SECS: u64 = 60;
const ATTR_VALID_SECS: u64 = 60;
#[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 HandleEntry {
fd: Option<OwnedFd>,
nodeid: u64,
flags: u32,
dir_snapshot: Option<Arc<Vec<DirEntry>>>,
}
const REOPEN_FLAG_MASK: i32 = libc::O_ACCMODE | libc::O_NOCTTY | libc::O_NOFOLLOW | libc::O_CLOEXEC;
struct State {
inodes: BTreeMap<u64, InodeInfo>,
children: BTreeMap<(u64, Vec<u8>), u64>,
handles: BTreeMap<u64, HandleEntry>,
dax_mmaps: BTreeMap<usize, Mmap>,
partial_writes: BTreeMap<u64, PartialWrite>,
next_nodeid: u64,
next_fh: u64,
}
#[derive(Clone, Debug)]
struct PartialWrite {
final_path: PathBuf,
partial_path: PathBuf,
parent_nodeid: u64,
child_name: Vec<u8>,
}
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>>,
rosetta_share: bool,
rosetta_socket_name: String,
}
struct Watcher {
inner: Arc<WatcherInner>,
handle: Option<std::thread::JoinHandle<()>>,
}
struct WatcherInner {
#[cfg(target_os = "macos")]
kq: libc::c_int,
#[cfg(not(target_os = "macos"))]
inotify_fd: libc::c_int,
#[cfg(not(target_os = "macos"))]
wake_fd: 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>,
#[cfg(target_os = "macos")]
_owned_fd: OwnedFd,
}
enum WatchChange {
Changed,
Gone,
}
fn watcher_emit(inner: &WatcherInner, ident: libc::c_int, change: WatchChange) {
let entry = inner
.watched
.lock()
.unwrap()
.get(&ident)
.map(|e| (e.nodeid, e.parent_nodeid, e.name.clone()));
let Some((nodeid, parent_nodeid, name)) = entry else {
return;
};
if let Some(n) = inner.notifier.lock().unwrap().as_ref() {
n.invalidate_inode(nodeid, 0, -1);
n.invalidate_inode(nodeid, 0, 0);
if matches!(change, WatchChange::Gone) {
n.invalidate_entry(parent_nodeid, &name);
}
}
if matches!(change, WatchChange::Gone) {
inner.watched.lock().unwrap().remove(&ident);
}
}
impl Drop for Watcher {
fn drop(&mut self) {
self.inner.stop.store(true, Ordering::Release);
#[cfg(target_os = "macos")]
unsafe {
let mut tr = 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 _ = libc::kevent(
self.inner.kq,
&mut tr as *mut _,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
);
}
#[cfg(not(target_os = "macos"))]
unsafe {
let one: u64 = 1;
let _ = libc::write(
self.inner.wake_fd,
&one as *const u64 as *const libc::c_void,
8,
);
}
if let Some(h) = self.handle.take() {
let _ = h.join();
}
unsafe {
#[cfg(target_os = "macos")]
libc::close(self.inner.kq);
#[cfg(not(target_os = "macos"))]
{
libc::close(self.inner.inotify_fd);
libc::close(self.inner.wake_fd);
}
}
}
}
impl PosixFs {
fn create_finalize(
&self,
parent: u64,
name: &OsStr,
final_path: &Path,
fd: libc::c_int,
partial_info: Option<PartialWrite>,
open_flags: u32,
) -> Result<(crate::fuse::backend::Entry, u64), Errno> {
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let dup_fd = owned.try_clone().map_err(|e| io_err_to_linux(&e))?;
let md = std::fs::File::from(dup_fd)
.metadata()
.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: final_path.to_path_buf(),
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,
HandleEntry {
fd: Some(owned),
nodeid,
flags: open_flags,
dir_snapshot: None,
},
);
if let Some(info) = partial_info {
st.partial_writes.insert(fh, info);
}
let attr = attr_from_meta(nodeid, &md);
Ok((
crate::fuse::backend::Entry {
nodeid,
generation: 0,
attr,
entry_valid: ENTRY_VALID_SECS,
attr_valid: ATTR_VALID_SECS,
},
fh,
))
}
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(),
partial_writes: BTreeMap::new(),
next_nodeid: FUSE_ROOT_ID + 1,
next_fh: 1,
}),
watcher: Mutex::new(None),
rosetta_share: false,
rosetta_socket_name: {
use std::sync::atomic::{AtomicU64, Ordering};
static MOUNT_COUNTER: AtomicU64 = AtomicU64::new(0);
let n = MOUNT_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
format!("supermachine-rosettad-{pid:x}-{n:x}")
},
})
}
pub fn with_rosetta(mut self) -> Self {
self.rosetta_share = true;
self
}
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(());
}
#[cfg(target_os = "macos")]
let inner = {
let kq = unsafe { libc::kqueue() };
if kq < 0 {
return Err(std::io::Error::last_os_error());
}
Arc::new(WatcherInner {
kq,
stop: AtomicBool::new(false),
notifier: Mutex::new(Some(notifier)),
watched: Mutex::new(BTreeMap::new()),
})
};
#[cfg(not(target_os = "macos"))]
let inner = {
let inotify_fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
if inotify_fd < 0 {
return Err(std::io::Error::last_os_error());
}
let wake_fd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC) };
if wake_fd < 0 {
let err = std::io::Error::last_os_error();
unsafe { libc::close(inotify_fd) };
return Err(err);
}
Arc::new(WatcherInner {
inotify_fd,
wake_fd,
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 (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);
#[cfg(target_os = "macos")]
let (ident, owned) = {
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 mut event = 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 rc = unsafe {
libc::kevent(
w.inner.kq,
&mut event as *mut _,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
)
};
if rc < 0 {
return;
}
(fd, owned)
};
#[cfg(not(target_os = "macos"))]
let ident = {
let wd = unsafe {
libc::inotify_add_watch(
w.inner.inotify_fd,
c.as_ptr(),
libc::IN_MODIFY
| libc::IN_CLOSE_WRITE
| libc::IN_ATTRIB
| libc::IN_DELETE_SELF
| libc::IN_MOVE_SELF,
)
};
if wd < 0 {
return;
}
wd
};
watched.insert(
ident,
WatchedEntry {
nodeid,
parent_nodeid,
name,
#[cfg(target_os = "macos")]
_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)
}
pub fn snapshot_state(&self) -> Vec<u8> {
let st = self.st.lock().unwrap();
let mut out = Vec::with_capacity(
64 + st.inodes.len() * 64 + st.children.len() * 32 + st.handles.len() * 32,
);
out.extend_from_slice(b"PFSS");
out.extend_from_slice(&2u32.to_le_bytes());
out.extend_from_slice(&st.next_nodeid.to_le_bytes());
out.extend_from_slice(&(st.inodes.len() as u64).to_le_bytes());
for (nodeid, info) in &st.inodes {
out.extend_from_slice(&nodeid.to_le_bytes());
let kd: u8 = match info.kind {
Kind::File => 0,
Kind::Dir => 1,
Kind::Symlink => 2,
Kind::Other => 3,
};
out.push(kd);
let pb = info.host_path.as_os_str().as_bytes();
out.extend_from_slice(&(pb.len() as u32).to_le_bytes());
out.extend_from_slice(pb);
}
out.extend_from_slice(&(st.children.len() as u64).to_le_bytes());
for ((parent, name), child) in &st.children {
out.extend_from_slice(&parent.to_le_bytes());
out.extend_from_slice(&(name.len() as u32).to_le_bytes());
out.extend_from_slice(name);
out.extend_from_slice(&child.to_le_bytes());
}
out.extend_from_slice(&st.next_fh.to_le_bytes());
out.extend_from_slice(&(st.handles.len() as u64).to_le_bytes());
for (fh, entry) in &st.handles {
out.extend_from_slice(&fh.to_le_bytes());
out.extend_from_slice(&entry.nodeid.to_le_bytes());
out.extend_from_slice(&entry.flags.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); }
out
}
pub fn restore_state(&self, blob: &[u8]) -> Result<(), std::io::Error> {
let mut p = 0usize;
fn take<'a>(b: &'a [u8], p: &mut usize, n: usize) -> Result<&'a [u8], std::io::Error> {
if *p + n > b.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"posix-fs snapshot truncated",
));
}
let s = &b[*p..*p + n];
*p += n;
Ok(s)
}
fn read_u32(b: &[u8], p: &mut usize) -> Result<u32, std::io::Error> {
let s = take(b, p, 4)?;
Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn read_u64(b: &[u8], p: &mut usize) -> Result<u64, std::io::Error> {
let s = take(b, p, 8)?;
Ok(u64::from_le_bytes([
s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
]))
}
let magic = take(blob, &mut p, 4)?;
if magic != b"PFSS" {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"posix-fs snapshot bad magic",
));
}
let version = read_u32(blob, &mut p)?;
if version != 1 && version != 2 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("posix-fs snapshot version {version} unsupported"),
));
}
let next_nodeid = read_u64(blob, &mut p)?;
let inode_count = read_u64(blob, &mut p)? as usize;
let mut new_inodes: BTreeMap<u64, InodeInfo> = BTreeMap::new();
let mut root_match = false;
for _ in 0..inode_count {
let nodeid = read_u64(blob, &mut p)?;
let kd = take(blob, &mut p, 1)?[0];
let kind = match kd {
0 => Kind::File,
1 => Kind::Dir,
2 => Kind::Symlink,
3 => Kind::Other,
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("posix-fs snapshot bad kind {kd}"),
));
}
};
let path_len = read_u32(blob, &mut p)? as usize;
let path_bytes = take(blob, &mut p, path_len)?;
let host_path = PathBuf::from(OsStr::from_bytes(path_bytes));
if nodeid == FUSE_ROOT_ID {
if host_path != self.root {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"posix-fs snapshot root mismatch: snapshot={} mount={}",
host_path.display(),
self.root.display()
),
));
}
root_match = true;
}
new_inodes.insert(nodeid, InodeInfo { host_path, kind });
}
if !root_match {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"posix-fs snapshot missing FUSE_ROOT_ID entry",
));
}
let children_count = read_u64(blob, &mut p)? as usize;
let mut new_children: BTreeMap<(u64, Vec<u8>), u64> = BTreeMap::new();
for _ in 0..children_count {
let parent = read_u64(blob, &mut p)?;
let name_len = read_u32(blob, &mut p)? as usize;
let name = take(blob, &mut p, name_len)?.to_vec();
let child = read_u64(blob, &mut p)?;
new_children.insert((parent, name), child);
}
let (next_fh_restored, new_handles): (u64, BTreeMap<u64, HandleEntry>) = if version >= 2 {
let next_fh = read_u64(blob, &mut p)?;
let handle_count = read_u64(blob, &mut p)? as usize;
let mut handles = BTreeMap::new();
for _ in 0..handle_count {
let fh = read_u64(blob, &mut p)?;
let nodeid = read_u64(blob, &mut p)?;
let flags = read_u32(blob, &mut p)?;
let _reserved = read_u32(blob, &mut p)?;
handles.insert(
fh,
HandleEntry {
fd: None,
nodeid,
flags,
dir_snapshot: None,
},
);
}
(next_fh, handles)
} else {
(0, BTreeMap::new())
};
if p != blob.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("posix-fs snapshot has {} trailing bytes", blob.len() - p),
));
}
let mut st = self.st.lock().unwrap();
st.inodes = new_inodes;
st.children = new_children;
st.next_nodeid = next_nodeid.max(st.next_nodeid);
if version >= 2 {
st.handles = new_handles;
let max_fh = st.handles.keys().copied().max().unwrap_or(0);
st.next_fh = next_fh_restored.max(max_fh + 1).max(st.next_fh);
}
Ok(())
}
fn handle_raw_fd_locked(st: &mut State, fh: u64) -> Result<libc::c_int, Errno> {
let entry = st.handles.get_mut(&fh).ok_or(EBADF)?;
if let Some(ref fd) = entry.fd {
return Ok(fd.as_raw_fd());
}
let nodeid = entry.nodeid;
let flags = entry.flags;
let host_path = st
.inodes
.get(&nodeid)
.map(|i| i.host_path.clone())
.ok_or(EBADF)?;
let c = CString::new(host_path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let open_flags = (flags as i32) & REOPEN_FLAG_MASK;
let raw = unsafe { libc::open(c.as_ptr(), open_flags) };
if raw < 0 {
return Err(errno_now());
}
let owned = unsafe { OwnedFd::from_raw_fd(raw) };
let raw = owned.as_raw_fd();
let entry = st.handles.get_mut(&fh).ok_or(EBADF)?;
entry.fd = Some(owned);
Ok(raw)
}
}
#[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, 93 => 61, 96 => 61, 84 => 75, 94 => 74, 92 => 84, 38 => 88, 39 => 89, 40 => 90, 41 => 91, 42 => 92, 43 => 93, 44 => 94, 45 => 95, 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, 71 => 66, 68 => 87, 95 => 72, 97 => 67, 98 => 63, 99 => 60, 100 => 71, 101 => 62, 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,
}
}
#[allow(clippy::unnecessary_cast)]
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 = if self.symlinks == SymlinkPolicy::Follow {
std::fs::metadata(&path)
.or_else(|_| std::fs::symlink_metadata(&path))
.map_err(|e| io_err_to_linux(&e))?
} else {
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: ENTRY_VALID_SECS,
attr_valid: ATTR_VALID_SECS,
})
}
fn forget(&self, _nodeid: u64, _nlookup: u64) {
}
fn getattr(&self, nodeid: u64, fh: Option<u64>) -> Result<Attr, Errno> {
if let Some(fh) = fh {
let mut st = self.st.lock().unwrap();
if st.partial_writes.contains_key(&fh) {
let raw = Self::handle_raw_fd_locked(&mut st, fh)?;
drop(st);
let mut st_buf = std::mem::MaybeUninit::<libc::stat>::uninit();
let rc = unsafe { libc::fstat(raw, st_buf.as_mut_ptr()) };
if rc != 0 {
return Err(errno_now());
}
let s = unsafe { st_buf.assume_init() };
return Ok(attr_from_stat(nodeid, &s));
}
}
let path = self.host_path_of(nodeid)?;
let md = if self.symlinks == SymlinkPolicy::Follow {
std::fs::metadata(&path)
.or_else(|_| std::fs::symlink_metadata(&path))
.map_err(|e| io_err_to_linux(&e))?
} else {
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)?;
if let Kind::Dir = self.kind_of(nodeid)? {
return Err(EISDIR);
}
self.watch_inode(nodeid, &path);
let want_trunc = (flags as i32 & libc::O_TRUNC) != 0;
let is_aotcache_path = path.extension().and_then(|s| s.to_str()) == Some("aotcache");
if want_trunc && is_aotcache_path {
let (parent_path, name) = match (path.parent(), path.file_name()) {
(Some(p), Some(n)) => (p, n),
_ => return Err(EINVAL),
};
let mut bytes = Vec::with_capacity(name.as_bytes().len() + 10);
bytes.push(b'.');
bytes.extend_from_slice(name.as_bytes());
bytes.extend_from_slice(b".partial");
let partial_path = parent_path.join(OsStr::from_bytes(&bytes));
let partial_c =
CString::new(partial_path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
let access = flags as i32 & libc::O_ACCMODE;
let mode: libc::c_uint = 0o644;
let mut fd = unsafe {
libc::open(
partial_c.as_ptr(),
access | libc::O_CREAT | libc::O_EXCL,
mode,
)
};
if fd < 0 && errno_now() == EEXIST_ERRNO {
unsafe { libc::unlink(partial_c.as_ptr()) };
fd = unsafe {
libc::open(
partial_c.as_ptr(),
access | libc::O_CREAT | libc::O_EXCL,
mode,
)
};
}
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;
let aotcache_flags = (access | libc::O_CREAT | libc::O_EXCL) as u32;
st.handles.insert(
fh,
HandleEntry {
fd: Some(owned),
nodeid,
flags: aotcache_flags,
dir_snapshot: None,
},
);
let mut parent_nodeid = 0u64;
let mut child_name = Vec::new();
for (&(pn, ref cn), &nid) in &st.children {
if nid == nodeid {
parent_nodeid = pn;
child_name = cn.clone();
break;
}
}
st.partial_writes.insert(
fh,
PartialWrite {
final_path: path.clone(),
partial_path,
parent_nodeid,
child_name,
},
);
return Ok(fh);
}
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,
HandleEntry {
fd: Some(owned),
nodeid,
flags: access as u32,
dir_snapshot: None,
},
);
Ok(fh)
}
fn read(&self, _nodeid: u64, fh: u64, offset: u64, size: u32) -> Result<Vec<u8>, Errno> {
let mut st = self.st.lock().unwrap();
let raw = Self::handle_raw_fd_locked(&mut st, fh)?;
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 (entry, partial) = {
let mut st = self.st.lock().unwrap();
let entry = st.handles.remove(&fh).ok_or(EBADF)?;
let partial = st.partial_writes.remove(&fh);
(entry, partial)
};
let Some(info) = partial else {
drop(entry.fd);
return Ok(());
};
let owned = match entry.fd {
Some(fd) => fd,
None => {
let partial_c =
CString::new(info.partial_path.as_os_str().as_bytes()).map_err(|_| EINVAL)?;
unsafe { libc::unlink(partial_c.as_ptr()) };
return Ok(());
}
};
let raw = owned.as_raw_fd();
let mut st_buf = std::mem::MaybeUninit::<libc::stat>::uninit();
let st_rc = unsafe { libc::fstat(raw, st_buf.as_mut_ptr()) };
let size = if st_rc == 0 {
unsafe { st_buf.assume_init().st_size }
} else {
0
};
let final_c = match CString::new(info.final_path.as_os_str().as_bytes()) {
Ok(s) => s,
Err(_) => {
drop(owned);
return Err(EINVAL);
}
};
let partial_c = match CString::new(info.partial_path.as_os_str().as_bytes()) {
Ok(s) => s,
Err(_) => {
drop(owned);
return Err(EINVAL);
}
};
let result = if size > 0 {
let rc = unsafe { libc::rename(partial_c.as_ptr(), final_c.as_ptr()) };
if rc != 0 {
let e = errno_now();
unsafe { libc::unlink(partial_c.as_ptr()) };
Err(e)
} else {
Ok(())
}
} else {
unsafe { libc::unlink(partial_c.as_ptr()) };
Ok(())
};
drop(owned);
result
}
fn write(&self, _nodeid: u64, fh: u64, offset: u64, data: &[u8]) -> Result<u32, Errno> {
let mut st = self.st.lock().unwrap();
let raw = Self::handle_raw_fd_locked(&mut st, fh)?;
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 mut st = self.st.lock().unwrap();
let raw = Self::handle_raw_fd_locked(&mut st, fh)?;
drop(st);
let weak_fsync = std::env::var_os("SUPERMACHINE_FSYNC_WEAK").is_some();
let rc = if weak_fsync {
unsafe { libc::fsync(raw) }
} else {
#[cfg(target_os = "macos")]
{
let rc = unsafe { libc::fcntl(raw, libc::F_FULLFSYNC) };
if rc != 0 {
let e = std::io::Error::last_os_error();
if e.raw_os_error() == Some(libc::ENOTSUP)
|| e.raw_os_error() == Some(libc::EINVAL)
{
unsafe { libc::fsync(raw) }
} else {
rc
}
} else {
0
}
}
#[cfg(not(target_os = "macos"))]
{
unsafe { 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 dir_flags = libc::O_RDONLY | libc::O_DIRECTORY;
let fd = unsafe { libc::open(c.as_ptr(), dir_flags) };
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,
HandleEntry {
fd: Some(owned),
nodeid,
flags: dir_flags as u32,
dir_snapshot: None,
},
);
Ok(fh)
}
fn readdir(
&self,
nodeid: u64,
fh: u64,
offset: u64,
_size: u32,
) -> Result<Vec<DirEntry>, Errno> {
{
let st = self.st.lock().unwrap();
if let Some(entry) = st.handles.get(&fh) {
if let Some(snap) = entry.dir_snapshot.as_ref() {
let start = (offset as usize).min(snap.len());
return Ok(snap[start..].to_vec());
}
}
}
let path = self.host_path_of(nodeid)?;
let rd = std::fs::read_dir(&path).map_err(|e| io_err_to_linux(&e))?;
let mut entries: Vec<DirEntry> = Vec::new();
for entry_res in rd {
let entry = match entry_res {
Ok(e) => e,
Err(_) => continue,
};
let name_bytes_owned = entry.file_name().as_bytes().to_vec();
if name_bytes_owned.starts_with(b".")
&& name_bytes_owned.ends_with(b".aotcache.partial")
{
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);
entries.push(DirEntry {
ino,
name: name_bytes_owned,
typ,
});
}
let cached = Arc::new(entries);
{
let mut st = self.st.lock().unwrap();
if let Some(h) = st.handles.get_mut(&fh) {
if h.dir_snapshot.is_none() {
h.dir_snapshot = Some(Arc::clone(&cached));
}
}
}
let start = (offset as usize).min(cached.len());
Ok(cached[start..].to_vec())
}
fn releasedir(&self, _nodeid: u64, fh: u64) -> Result<(), Errno> {
let mut st = self.st.lock().unwrap();
st.handles.remove(&fh).ok_or(EBADF).map(|_| ())
}
#[allow(clippy::unnecessary_cast)]
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 final_path = parent_path.join(name);
let is_aotcache = name.as_bytes().ends_with(b".aotcache");
let (open_path, partial_info) = if is_aotcache {
let mut bytes = Vec::with_capacity(name.as_bytes().len() + 10);
bytes.push(b'.');
bytes.extend_from_slice(name.as_bytes());
bytes.extend_from_slice(b".partial");
let partial_name = OsStr::from_bytes(&bytes);
let partial_path = parent_path.join(partial_name);
let info = PartialWrite {
final_path: final_path.clone(),
partial_path: partial_path.clone(),
parent_nodeid: parent,
child_name: name.as_bytes().to_vec(),
};
(partial_path, Some(info))
} else {
(final_path.clone(), None)
};
let c = CString::new(open_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 | libc::O_CREAT | libc::O_EXCL,
mode as libc::c_uint,
)
};
if fd < 0 {
let e = errno_now();
if partial_info.is_some() && e == EEXIST_ERRNO {
unsafe { libc::unlink(c.as_ptr()) };
let fd2 = unsafe {
libc::open(
c.as_ptr(),
access | libc::O_CREAT | libc::O_EXCL,
mode as libc::c_uint,
)
};
if fd2 < 0 {
return Err(errno_now());
}
return self.create_finalize(
parent,
name,
&final_path,
fd2,
partial_info,
(access | libc::O_CREAT | libc::O_EXCL) as u32,
);
}
return Err(e);
}
self.create_finalize(
parent,
name,
&final_path,
fd,
partial_info,
(access | libc::O_CREAT | libc::O_EXCL) as u32,
)
}
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 = match open_dirfd(&parent_path) {
Ok(parent_dirfd) => {
let name_c = CString::new(name.as_bytes()).map_err(|_| EINVAL)?;
let mut sb: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe {
libc::fstatat(
parent_dirfd.as_raw_fd(),
name_c.as_ptr(),
&mut sb,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if rc != 0 {
return Err(errno_now());
}
std::fs::symlink_metadata(&full).map_err(|e| io_err_to_linux(&e))?
}
Err(_) => std::fs::symlink_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: ENTRY_VALID_SECS,
attr_valid: ATTR_VALID_SECS,
})
}
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: ENTRY_VALID_SECS,
attr_valid: ATTR_VALID_SECS,
})
}
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)
}
#[allow(clippy::unnecessary_cast)]
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::linkat(
libc::AT_FDCWD,
src_c.as_ptr(),
libc::AT_FDCWD,
dst_c.as_ptr(),
0, )
};
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: ENTRY_VALID_SECS,
attr_valid: ATTR_VALID_SECS,
})
}
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 mut st = self.st.lock().unwrap();
Some(Self::handle_raw_fd_locked(&mut st, handle)?)
} 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 mut st = self.st.lock().unwrap();
let raw = Self::handle_raw_fd_locked(&mut st, fh)?;
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 ioctl(
&self,
nodeid: u64,
fh: u64,
cmd: u32,
in_data: &[u8],
out_max: u32,
) -> Result<(i32, Vec<u8>), Errno> {
if !self.rosetta_share {
return Err(super::backend::ENOSYS);
}
if crate::trace::enabled("rosetta") {
eprintln!(
"[rosetta] ioctl nodeid={nodeid} fh={fh} cmd=0x{cmd:x} \
in_len={} out_max={}",
in_data.len(),
out_max
);
}
const ROSETTA_CACHE_SETTINGS_QUERY: u32 = 0x80806123;
const ROSETTA_VERIFY_HANDSHAKE: u32 = 0x80456125;
if cmd == ROSETTA_VERIFY_HANDSHAKE {
if crate::trace::enabled("rosetta") {
eprintln!("[rosetta] verify handshake ioctl 0x80456125, out_max={out_max}");
}
let resp: &[u8] =
b"Our hard work\nby these words guarded\nplease don't steal\n\xc2\xa9 Apple Inc\0";
debug_assert_eq!(resp.len(), 69);
let want = (out_max as usize).min(resp.len());
return Ok((0, resp[..want].to_vec()));
}
const ROSETTA_HV_PING: u32 = 0x00006124;
if cmd == ROSETTA_HV_PING {
if crate::trace::enabled("rosetta") {
eprintln!("[rosetta] hv-ping ioctl 0x00006124");
}
return Ok((0, Vec::new()));
}
if cmd != ROSETTA_CACHE_SETTINGS_QUERY {
return Err(super::backend::ENOSYS);
}
let want = out_max.min(128) as usize;
let fill = std::env::var("SUPERMACHINE_ROSETTA_FILL")
.ok()
.unwrap_or_else(|| "synth".to_owned());
let mut data = match fill.as_str() {
"ones" => vec![0xffu8; want],
"marker" => vec![0xaau8; want],
_ => vec![0x00u8; want],
};
if matches!(fill.as_str(), "sizes" | "synth") {
let size: u64 = 0x10000000;
let bytes = size.to_le_bytes();
for (i, b) in bytes.iter().enumerate() {
if 0x20 + i < want {
data[0x20 + i] = *b;
}
if 0x40 + i < want {
data[0x40 + i] = *b;
}
}
}
if fill.as_str() == "synth" {
data[0] = 1;
let six_c: u8 = std::env::var("SUPERMACHINE_ROSETTA_6C")
.ok()
.and_then(|s| s.parse::<u8>().ok())
.unwrap_or(1);
if 0x6c < want {
data[0x6c] = six_c;
}
const ROSETTAD_SOCK_PATH: &[u8] = b"/run/rosettad/rosetta.sock\0";
let pn = ROSETTAD_SOCK_PATH.len();
if pn < want {
data[1..1 + pn].copy_from_slice(ROSETTAD_SOCK_PATH);
}
let _unused = self.rosetta_socket_name.as_bytes();
if let Ok(spec) = std::env::var("SUPERMACHINE_ROSETTA_OVERRIDE") {
for chunk in spec.split(',').filter(|s| !s.is_empty()) {
if let Some((off_s, hex_s)) = chunk.split_once(':') {
let off = off_s
.trim()
.strip_prefix("0x")
.unwrap_or(off_s.trim())
.parse::<usize>()
.ok()
.or_else(|| {
usize::from_str_radix(off_s.trim().trim_start_matches("0x"), 16)
.ok()
});
let off = match off {
Some(o) => o,
None => continue,
};
let hex_s = hex_s.trim();
let mut i = 0;
while i + 1 < hex_s.len() {
let b = u8::from_str_radix(&hex_s[i..i + 2], 16);
if let Ok(b) = b {
let pos = off + (i / 2);
if pos < want {
data[pos] = b;
}
}
i += 2;
}
}
}
}
}
if crate::trace::enabled("rosetta") {
let hex: String = data.iter().take(96).map(|b| format!("{:02x}", b)).collect();
eprintln!("[rosetta] returning {} bytes: {}…", data.len(), hex);
let _ = std::fs::write("/tmp/supermachine_rosetta_response.bin", &data);
let _ = std::fs::write(
"/tmp/supermachine_rosetta_response.hex",
data.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(""),
);
}
Ok((0, data))
}
fn snapshot_state(&self) -> Option<Vec<u8>> {
Some(PosixFs::snapshot_state(self))
}
fn restore_state(&self, blob: &[u8]) -> Result<(), std::io::Error> {
PosixFs::restore_state(self, blob)
}
}
#[cfg(target_os = "macos")]
fn run_watcher(inner: Arc<WatcherInner>) {
let mut w = libc::kevent {
ident: 0,
filter: libc::EVFILT_USER,
flags: libc::EV_ADD | libc::EV_CLEAR,
fflags: 0,
data: 0,
udata: std::ptr::null_mut(),
};
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 ident = ev.ident as libc::c_int;
let change = if ev.fflags & (libc::NOTE_DELETE | libc::NOTE_RENAME) != 0 {
WatchChange::Gone
} else {
WatchChange::Changed
};
watcher_emit(&inner, ident, change);
}
}
}
#[cfg(not(target_os = "macos"))]
fn run_watcher(inner: Arc<WatcherInner>) {
let mut fds = [
libc::pollfd {
fd: inner.inotify_fd,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: inner.wake_fd,
events: libc::POLLIN,
revents: 0,
},
];
let mut buf = [0u8; 8192];
const HDR: usize = 16; loop {
if inner.stop.load(Ordering::Acquire) {
break;
}
let n = unsafe { libc::poll(fds.as_mut_ptr(), 2, -1) };
if n < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
eprintln!("[posix-fs watcher] poll failed: {err}; thread exiting");
return;
}
if inner.stop.load(Ordering::Acquire) {
break;
}
if fds[1].revents & libc::POLLIN != 0 {
let mut sink = [0u8; 8];
let _ = unsafe { libc::read(inner.wake_fd, sink.as_mut_ptr() as *mut libc::c_void, 8) };
}
if fds[0].revents & libc::POLLIN == 0 {
continue;
}
let got = unsafe {
libc::read(
inner.inotify_fd,
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
if got <= 0 {
continue; }
let got = got as usize;
let mut off = 0usize;
while off + HDR <= got {
let wd = i32::from_ne_bytes(buf[off..off + 4].try_into().unwrap());
let mask = u32::from_ne_bytes(buf[off + 4..off + 8].try_into().unwrap());
let len = u32::from_ne_bytes(buf[off + 12..off + 16].try_into().unwrap()) as usize;
let gone = mask & (libc::IN_DELETE_SELF | libc::IN_MOVE_SELF | libc::IN_IGNORED) != 0;
let change = if gone {
WatchChange::Gone
} else {
WatchChange::Changed
};
watcher_emit(&inner, wd, change);
off += HDR + len;
}
}
}
#[allow(unused_imports)]
use std::convert::TryFrom;
use std::os::unix::fs::FileTypeExt;
#[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 readdir_snapshot_is_stable_across_concurrent_unlink() {
let dir = tmpdir("readdir_concurrent_unlink");
for i in 0..50 {
std::fs::write(dir.join(format!("f{i:03}.txt")), b"x").unwrap();
}
let fs = PosixFs::new(&dir).unwrap();
let dh = fs.opendir(FUSE_ROOT_ID, 0).unwrap();
let first = fs.readdir(FUSE_ROOT_ID, dh, 0, 65536).unwrap();
assert_eq!(first.len(), 50, "all 50 entries in first batch");
for entry in &first[0..10] {
let name = std::str::from_utf8(&entry.name).unwrap();
std::fs::remove_file(dir.join(name)).unwrap();
}
let second = fs
.readdir(FUSE_ROOT_ID, dh, first.len() as u64, 65536)
.unwrap();
assert_eq!(second.len(), 0, "no entries left to enumerate");
let third = fs.readdir(FUSE_ROOT_ID, dh, 10, 65536).unwrap();
assert_eq!(
third.len(),
40,
"snapshot still has 40 entries from index 10"
);
for (i, entry) in third.iter().enumerate() {
assert_eq!(entry.name, first[i + 10].name);
}
fs.releasedir(FUSE_ROOT_ID, dh).unwrap();
}
#[test]
fn readdir_with_unknown_fh_still_returns_entries() {
let dir = tmpdir("readdir_unknown_fh");
std::fs::write(dir.join("a.txt"), b"a").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let entries = fs.readdir(FUSE_ROOT_ID, 99999, 0, 4096).unwrap();
assert!(entries.iter().any(|e| e.name == b"a.txt"));
}
#[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 name_safe_table() {
for ok in ["file.txt", "a", ".hidden", "..foo", "foo..", "a.b.c"] {
assert!(
name_safe(OsStr::new(ok)).is_ok(),
"{ok:?} should be allowed"
);
}
for bad in ["", ".", ".."] {
assert_eq!(name_safe(OsStr::new(bad)).unwrap_err(), EINVAL, "{bad:?}");
}
assert_eq!(name_safe(OsStr::new("a/b")).unwrap_err(), EINVAL);
assert_eq!(name_safe(OsStr::new("../x")).unwrap_err(), EINVAL);
assert_eq!(
name_safe(OsStr::from_bytes(b"foo\0bar")).unwrap_err(),
EINVAL
);
}
#[test]
fn create_gates_traversal_on_the_write_path() {
let dir = tmpdir("t-create-dotdot");
let fs = PosixFs::new(&dir).unwrap();
assert_eq!(
fs.create(FUSE_ROOT_ID, OsStr::new(".."), 0o644, 0)
.unwrap_err(),
EINVAL
);
assert_eq!(
fs.create(FUSE_ROOT_ID, OsStr::new("a/b"), 0o644, 0)
.unwrap_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() {
}
#[test]
fn snapshot_round_trip_preserves_inode_and_children_tables() {
let dir = tmpdir("t-snap-roundtrip");
std::fs::create_dir_all(dir.join("dist/scripts")).unwrap();
std::fs::write(dir.join("dist/cli.js"), b"#!/usr/bin/env node\n").unwrap();
std::fs::write(
dir.join("dist/scripts/probeRunnerServer.js"),
b"// probe runner\n",
)
.unwrap();
let fs1 = PosixFs::new(&dir).unwrap();
let dist = fs1.lookup(FUSE_ROOT_ID, OsStr::new("dist")).unwrap();
let _cli = fs1.lookup(dist.nodeid, OsStr::new("cli.js")).unwrap();
let scripts = fs1.lookup(dist.nodeid, OsStr::new("scripts")).unwrap();
let probe = fs1
.lookup(scripts.nodeid, OsStr::new("probeRunnerServer.js"))
.unwrap();
let blob = fs1.snapshot_state();
let fs2 = PosixFs::new(&dir).unwrap();
fs2.restore_state(&blob).expect("restore should succeed");
let root = std::fs::canonicalize(&dir).unwrap();
let st2 = fs2.st.lock().unwrap();
assert_eq!(
st2.inodes.get(&dist.nodeid).map(|i| &i.host_path),
Some(&root.join("dist"))
);
assert_eq!(
st2.inodes.get(&scripts.nodeid).map(|i| &i.host_path),
Some(&root.join("dist").join("scripts"))
);
assert_eq!(
st2.inodes.get(&probe.nodeid).map(|i| &i.host_path),
Some(
&root
.join("dist")
.join("scripts")
.join("probeRunnerServer.js")
)
);
let key = (dist.nodeid, b"scripts".to_vec());
assert_eq!(st2.children.get(&key).copied(), Some(scripts.nodeid));
let key = (scripts.nodeid, b"probeRunnerServer.js".to_vec());
assert_eq!(st2.children.get(&key).copied(), Some(probe.nodeid));
assert!(st2.next_nodeid > probe.nodeid);
}
#[test]
fn restore_rejects_root_mismatch() {
let dir1 = tmpdir("t-snap-root1");
let dir2 = tmpdir("t-snap-root2");
let fs1 = PosixFs::new(&dir1).unwrap();
let blob = fs1.snapshot_state();
let fs2 = PosixFs::new(&dir2).unwrap();
let err = fs2.restore_state(&blob).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let msg = err.to_string();
assert!(msg.contains("root mismatch"), "unexpected error: {msg}");
}
#[test]
fn restore_rejects_malformed_blobs() {
let dir = tmpdir("t-snap-malformed");
let fs = PosixFs::new(&dir).unwrap();
let mut bad = b"XXXX".to_vec();
bad.extend_from_slice(&1u32.to_le_bytes());
let err = fs.restore_state(&bad).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let err = fs.restore_state(b"PFSS").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
let mut bad = b"PFSS".to_vec();
bad.extend_from_slice(&99u32.to_le_bytes());
let err = fs.restore_state(&bad).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("version 99"));
}
#[test]
fn snapshot_v2_round_trip_preserves_handles() {
let dir = tmpdir("t-snap-v2-handles");
std::fs::write(dir.join("a.bin"), b"alpha").unwrap();
std::fs::write(dir.join("b.bin"), b"beta-content-larger").unwrap();
let fs1 = PosixFs::new(&dir).unwrap();
let a = fs1.lookup(FUSE_ROOT_ID, OsStr::new("a.bin")).unwrap();
let b = fs1.lookup(FUSE_ROOT_ID, OsStr::new("b.bin")).unwrap();
let fh_a = fs1.open(a.nodeid, libc::O_RDONLY as u32).unwrap();
let fh_b = fs1.open(b.nodeid, libc::O_RDWR as u32).unwrap();
let blob = fs1.snapshot_state();
let fs2 = PosixFs::new(&dir).unwrap();
fs2.restore_state(&blob).expect("restore should succeed");
let st = fs2.st.lock().unwrap();
let entry_a = st.handles.get(&fh_a).expect("fh_a survived restore");
let entry_b = st.handles.get(&fh_b).expect("fh_b survived restore");
assert!(entry_a.fd.is_none(), "restored handle starts as lazy");
assert!(entry_b.fd.is_none(), "restored handle starts as lazy");
assert_eq!(entry_a.nodeid, a.nodeid);
assert_eq!(entry_b.nodeid, b.nodeid);
assert_eq!(entry_a.flags, libc::O_RDONLY as u32);
assert_eq!(entry_b.flags, libc::O_RDWR as u32);
}
#[test]
fn lazy_reopen_on_first_read() {
let dir = tmpdir("t-snap-v2-lazy-read");
let content = b"PFSS v2 lazy reopen content";
std::fs::write(dir.join("payload"), content).unwrap();
let fs1 = PosixFs::new(&dir).unwrap();
let e = fs1.lookup(FUSE_ROOT_ID, OsStr::new("payload")).unwrap();
let fh = fs1.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let blob = fs1.snapshot_state();
drop(fs1);
let fs2 = PosixFs::new(&dir).unwrap();
fs2.restore_state(&blob).expect("restore should succeed");
{
let st = fs2.st.lock().unwrap();
let entry = st.handles.get(&fh).unwrap();
assert!(entry.fd.is_none(), "lazy before first I/O");
}
let buf = fs2.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(buf, content);
{
let st = fs2.st.lock().unwrap();
let entry = st.handles.get(&fh).unwrap();
assert!(entry.fd.is_some(), "fd installed after first I/O");
}
let buf2 = fs2.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(buf2, content);
}
#[test]
fn lazy_reopen_strips_destructive_flags() {
let dir = tmpdir("t-snap-v2-flag-mask");
let original = b"DO NOT TRUNCATE ME ON RESTORE";
std::fs::write(dir.join("important"), original).unwrap();
let fs1 = PosixFs::new(&dir).unwrap();
let e = fs1.lookup(FUSE_ROOT_ID, OsStr::new("important")).unwrap();
let fh = {
let mut st = fs1.st.lock().unwrap();
let fh = st.next_fh;
st.next_fh += 1;
let nasty_flags =
(libc::O_RDWR | libc::O_TRUNC | libc::O_APPEND | libc::O_CREAT) as u32;
st.handles.insert(
fh,
HandleEntry {
fd: None,
nodeid: e.nodeid,
flags: nasty_flags,
dir_snapshot: None,
},
);
fh
};
let blob = fs1.snapshot_state();
drop(fs1);
let fs2 = PosixFs::new(&dir).unwrap();
fs2.restore_state(&blob).expect("restore should succeed");
let buf = fs2.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(buf, original, "lazy reopen must NOT trigger O_TRUNC");
let md = std::fs::metadata(dir.join("important")).unwrap();
assert_eq!(md.len(), original.len() as u64);
}
#[test]
fn release_works_on_lazy_handle() {
let dir = tmpdir("t-snap-v2-release-lazy");
std::fs::write(dir.join("f"), b"some bytes").unwrap();
let fs1 = PosixFs::new(&dir).unwrap();
let e = fs1.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs1.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let blob = fs1.snapshot_state();
drop(fs1);
let fs2 = PosixFs::new(&dir).unwrap();
fs2.restore_state(&blob).expect("restore should succeed");
fs2.release(e.nodeid, fh).expect("release of lazy fh ok");
let st = fs2.st.lock().unwrap();
assert!(!st.handles.contains_key(&fh));
}
#[test]
fn version_1_blob_is_accepted_with_no_handles() {
let dir = tmpdir("t-snap-v1-compat");
std::fs::write(dir.join("root-file"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let root_path = std::fs::canonicalize(&dir).unwrap();
let root_bytes = root_path.as_os_str().as_bytes();
let mut blob = Vec::new();
blob.extend_from_slice(b"PFSS");
blob.extend_from_slice(&1u32.to_le_bytes()); blob.extend_from_slice(&100u64.to_le_bytes()); blob.extend_from_slice(&1u64.to_le_bytes()); blob.extend_from_slice(&FUSE_ROOT_ID.to_le_bytes()); blob.push(1u8); blob.extend_from_slice(&(root_bytes.len() as u32).to_le_bytes());
blob.extend_from_slice(root_bytes);
blob.extend_from_slice(&0u64.to_le_bytes());
fs.restore_state(&blob).expect("v1 blob accepted");
let st = fs.st.lock().unwrap();
assert!(st.handles.is_empty());
assert!(st.next_nodeid >= 100);
assert_eq!(st.next_fh, 1);
}
#[test]
fn next_fh_avoids_restored_collision() {
let dir = tmpdir("t-snap-v2-next-fh");
std::fs::write(dir.join("first"), b"1").unwrap();
std::fs::write(dir.join("second"), b"2").unwrap();
let fs1 = PosixFs::new(&dir).unwrap();
let a = fs1.lookup(FUSE_ROOT_ID, OsStr::new("first")).unwrap();
for _ in 0..5 {
let _ = fs1.open(a.nodeid, libc::O_RDONLY as u32).unwrap();
}
let max_fh_before = {
let st = fs1.st.lock().unwrap();
st.handles.keys().copied().max().unwrap()
};
let blob = fs1.snapshot_state();
drop(fs1);
let fs2 = PosixFs::new(&dir).unwrap();
fs2.restore_state(&blob).expect("restore should succeed");
{
let st = fs2.st.lock().unwrap();
assert!(
st.next_fh > max_fh_before,
"next_fh ({}) must be greater than max restored fh ({})",
st.next_fh,
max_fh_before
);
}
let b = fs2.lookup(FUSE_ROOT_ID, OsStr::new("second")).unwrap();
let new_fh = fs2.open(b.nodeid, libc::O_RDONLY as u32).unwrap();
assert!(
new_fh > max_fh_before,
"new open fh ({new_fh}) must not collide with restored max ({max_fh_before})"
);
}
}