use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::sync::{Arc, Mutex};
use super::backend::{Errno, FsBackend, MemoryFs};
use super::dax::{parse_remove_payload, DaxSession};
use super::protocol::{
align_up, AttrOut, CreateIn, DirentHeader, EntryOut, ForgetIn, FsyncIn, GetattrIn, InHeader,
InitIn, InitOut, Kstatfs, MkdirIn, Opcode, OpenIn, OpenOut, OutHeader, ReadIn, ReleaseIn,
SetupMappingIn, StatfsOut, WriteIn, WriteOut, FUSE_KERNEL_MINOR_VERSION, FUSE_KERNEL_VERSION,
FUSE_MAP_ALIGNMENT, FUSE_SUBMOUNTS,
};
#[derive(Clone, Copy, Debug)]
struct Negotiated {
major: u32,
minor: u32,
flags: u32,
max_write: u32,
map_alignment_log2: u16,
}
impl Default for Negotiated {
fn default() -> Self {
Self {
major: 0,
minor: 0,
flags: 0,
max_write: 1024 * 1024,
map_alignment_log2: 14, }
}
}
pub struct FuseServer {
neg: Mutex<Negotiated>,
backend: Arc<dyn FsBackend>,
dax: Mutex<Option<Arc<DaxSession>>>,
}
impl Default for FuseServer {
fn default() -> Self {
Self::new(Arc::new(MemoryFs::new()))
}
}
pub struct Reply {
pub bytes: Vec<u8>,
}
impl Reply {
pub fn header_only(unique: u64, error: i32) -> Self {
let out = OutHeader {
len: core::mem::size_of::<OutHeader>() as u32,
error,
unique,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const OutHeader as *const u8,
core::mem::size_of::<OutHeader>(),
)
}
.to_vec();
Reply { bytes }
}
pub fn with_payload(unique: u64, payload: &[u8]) -> Self {
let total = core::mem::size_of::<OutHeader>() + payload.len();
let mut bytes = Vec::with_capacity(total);
let out = OutHeader {
len: total as u32,
error: 0,
unique,
};
unsafe {
bytes.extend_from_slice(std::slice::from_raw_parts(
&out as *const OutHeader as *const u8,
core::mem::size_of::<OutHeader>(),
));
}
bytes.extend_from_slice(payload);
Reply { bytes }
}
}
fn or_err(unique: u64, r: Result<Reply, Errno>) -> Reply {
match r {
Ok(r) => r,
Err(e) => Reply::header_only(unique, e),
}
}
fn read_struct<T: Copy + Default>(buf: &[u8]) -> Result<T, Errno> {
if buf.len() < core::mem::size_of::<T>() {
return Err(super::backend::EINVAL);
}
Ok(unsafe { core::ptr::read_unaligned(buf.as_ptr() as *const T) })
}
fn emit_dirent(out: &mut Vec<u8>, ino: u64, off: u64, typ: u32, name: &[u8]) -> usize {
let hdr_size = core::mem::size_of::<DirentHeader>();
let entry_size = align_up(hdr_size + name.len(), 8);
let start = out.len();
let hdr = DirentHeader {
ino,
off,
namelen: name.len() as u32,
typ,
};
unsafe {
out.extend_from_slice(std::slice::from_raw_parts(
&hdr as *const DirentHeader as *const u8,
hdr_size,
));
}
out.extend_from_slice(name);
out.resize(start + entry_size, 0);
entry_size
}
impl FuseServer {
pub fn new(backend: Arc<dyn FsBackend>) -> Self {
Self {
neg: Mutex::new(Negotiated::default()),
backend,
dax: Mutex::new(None),
}
}
pub fn set_backend(&mut self, backend: Arc<dyn FsBackend>) {
self.backend = backend;
}
pub fn set_dax(&self, session: Arc<DaxSession>) {
*self.dax.lock().unwrap() = Some(session);
}
pub fn negotiated_max_write(&self) -> u32 {
self.neg.lock().unwrap().max_write
}
pub fn negotiated_major(&self) -> u32 {
self.neg.lock().unwrap().major
}
pub fn negotiated_minor(&self) -> u32 {
self.neg.lock().unwrap().minor
}
pub fn negotiated_flags(&self) -> u32 {
self.neg.lock().unwrap().flags
}
pub fn dispatch(&self, hdr: &InHeader, payload: &[u8]) -> Reply {
let op = Opcode::from_u32(hdr.opcode);
trace(|| format!(
"req op={op:?} unique={} nodeid={} payload_len={}",
hdr.unique, hdr.nodeid, payload.len()
));
match op {
Some(Opcode::Init) => self.handle_init(hdr, payload),
Some(Opcode::Destroy) => self.handle_destroy(hdr),
Some(Opcode::Lookup) => or_err(hdr.unique, self.handle_lookup(hdr, payload)),
Some(Opcode::Forget) => self.handle_forget(hdr, payload),
Some(Opcode::Getattr) => or_err(hdr.unique, self.handle_getattr(hdr, payload)),
Some(Opcode::Open) => or_err(hdr.unique, self.handle_open(hdr, payload)),
Some(Opcode::Read) => or_err(hdr.unique, self.handle_read(hdr, payload)),
Some(Opcode::Release) => or_err(hdr.unique, self.handle_release(hdr, payload)),
Some(Opcode::Write) => or_err(hdr.unique, self.handle_write(hdr, payload)),
Some(Opcode::Fsync) => or_err(hdr.unique, self.handle_fsync(hdr, payload)),
Some(Opcode::Create) => or_err(hdr.unique, self.handle_create(hdr, payload)),
Some(Opcode::Mkdir) => or_err(hdr.unique, self.handle_mkdir(hdr, payload)),
Some(Opcode::Unlink) => or_err(hdr.unique, self.handle_unlink(hdr, payload)),
Some(Opcode::Rmdir) => or_err(hdr.unique, self.handle_rmdir(hdr, payload)),
Some(Opcode::Opendir) => or_err(hdr.unique, self.handle_opendir(hdr, payload)),
Some(Opcode::Readdir) => or_err(hdr.unique, self.handle_readdir(hdr, payload)),
Some(Opcode::Releasedir) => or_err(hdr.unique, self.handle_releasedir(hdr, payload)),
Some(Opcode::Statfs) => or_err(hdr.unique, self.handle_statfs(hdr)),
Some(Opcode::SetupMapping) => or_err(hdr.unique, self.handle_setup_mapping(hdr, payload)),
Some(Opcode::RemoveMapping) => or_err(hdr.unique, self.handle_remove_mapping(hdr, payload)),
_ => Reply::header_only(hdr.unique, super::backend::ENOSYS),
}
}
fn handle_init(&self, hdr: &InHeader, payload: &[u8]) -> Reply {
let mut req = InitIn::default();
let n = payload.len().min(core::mem::size_of::<InitIn>());
if n >= 8 {
unsafe {
core::ptr::copy_nonoverlapping(
payload.as_ptr(),
&mut req as *mut InitIn as *mut u8,
n,
);
}
}
let major = req.major.min(FUSE_KERNEL_VERSION);
let minor = if major < FUSE_KERNEL_VERSION {
req.minor
} else {
req.minor.min(FUSE_KERNEL_MINOR_VERSION)
};
let supported = FUSE_MAP_ALIGNMENT | FUSE_SUBMOUNTS;
let agreed = (req.flags & supported) | FUSE_MAP_ALIGNMENT;
let mut neg = self.neg.lock().unwrap();
neg.major = major;
neg.minor = minor;
neg.flags = agreed;
let out = InitOut {
major,
minor,
max_readahead: req.max_readahead,
flags: agreed,
max_background: 16,
congestion_threshold: 12,
max_write: neg.max_write,
time_gran: 1,
max_pages: 256,
map_alignment: neg.map_alignment_log2,
flags2: 0,
max_stack_depth: 0,
unused: [0; 6],
};
eprintln!(
"[fuse] INIT: guest v{}.{} flags={:#x} → agreed v{}.{} flags={:#x} (map_align={})",
req.major, req.minor, req.flags, major, minor, agreed, neg.map_alignment_log2,
);
let payload = unsafe {
std::slice::from_raw_parts(
&out as *const InitOut as *const u8,
core::mem::size_of::<InitOut>(),
)
};
Reply::with_payload(hdr.unique, payload)
}
fn handle_destroy(&self, hdr: &InHeader) -> Reply {
eprintln!("[fuse] DESTROY");
let mut neg = self.neg.lock().unwrap();
*neg = Negotiated::default();
Reply::header_only(hdr.unique, 0)
}
fn handle_lookup(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let name = strip_nul(payload).ok_or(super::backend::EINVAL)?;
let entry = self.backend.lookup(hdr.nodeid, OsStr::from_bytes(name))?;
let out = EntryOut {
nodeid: entry.nodeid,
generation: entry.generation,
entry_valid: entry.entry_valid,
attr_valid: entry.attr_valid,
entry_valid_nsec: 0,
attr_valid_nsec: 0,
attr: entry.attr,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const EntryOut as *const u8,
core::mem::size_of::<EntryOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
fn handle_forget(&self, hdr: &InHeader, payload: &[u8]) -> Reply {
if let Ok(req) = read_struct::<ForgetIn>(payload) {
self.backend.forget(hdr.nodeid, req.nlookup);
}
let _ = hdr;
Reply::header_only(0, 0)
}
fn handle_getattr(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<GetattrIn>(payload).unwrap_or_default();
let fh = if req.flags & 1 != 0 { Some(req.fh) } else { None };
let attr = self.backend.getattr(hdr.nodeid, fh)?;
let out = AttrOut {
attr_valid: 1,
attr_valid_nsec: 0,
_pad: 0,
attr,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const AttrOut as *const u8,
core::mem::size_of::<AttrOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
fn handle_open(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<OpenIn>(payload)?;
let fh = self.backend.open(hdr.nodeid, req.flags)?;
trace(|| format!(
"OPEN: nodeid={} flags={:#x} → fh={fh}",
hdr.nodeid, req.flags
));
let out = OpenOut {
fh,
open_flags: 0,
_pad: 0,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const OpenOut as *const u8,
core::mem::size_of::<OpenOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
fn handle_read(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<ReadIn>(payload)?;
let data = self.backend.read(hdr.nodeid, req.fh, req.offset, req.size)?;
Ok(Reply::with_payload(hdr.unique, &data))
}
fn handle_release(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<ReleaseIn>(payload)?;
self.backend.release(hdr.nodeid, req.fh)?;
Ok(Reply::header_only(hdr.unique, 0))
}
fn handle_write(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<WriteIn>(payload)?;
let hdr_size = core::mem::size_of::<WriteIn>();
if payload.len() < hdr_size + req.size as usize {
return Err(super::backend::EINVAL);
}
let data = &payload[hdr_size..hdr_size + req.size as usize];
let written = self.backend.write(hdr.nodeid, req.fh, req.offset, data)?;
let out = WriteOut {
size: written,
_pad: 0,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const WriteOut as *const u8,
core::mem::size_of::<WriteOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
fn handle_fsync(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<FsyncIn>(payload)?;
let datasync = req.fsync_flags & 1 != 0;
self.backend.fsync(hdr.nodeid, req.fh, datasync)?;
Ok(Reply::header_only(hdr.unique, 0))
}
fn handle_create(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let cin = read_struct::<CreateIn>(payload)?;
let name = strip_nul(&payload[core::mem::size_of::<CreateIn>()..]).ok_or(super::backend::EINVAL)?;
let (entry, fh) = self
.backend
.create(hdr.nodeid, OsStr::from_bytes(name), cin.mode, cin.flags)?;
let entry_out = EntryOut {
nodeid: entry.nodeid,
generation: entry.generation,
entry_valid: entry.entry_valid,
attr_valid: entry.attr_valid,
entry_valid_nsec: 0,
attr_valid_nsec: 0,
attr: entry.attr,
};
let open_out = OpenOut { fh, open_flags: 0, _pad: 0 };
let mut buf = Vec::with_capacity(core::mem::size_of::<EntryOut>() + core::mem::size_of::<OpenOut>());
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
&entry_out as *const EntryOut as *const u8,
core::mem::size_of::<EntryOut>(),
)
});
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
&open_out as *const OpenOut as *const u8,
core::mem::size_of::<OpenOut>(),
)
});
Ok(Reply::with_payload(hdr.unique, &buf))
}
fn handle_mkdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<MkdirIn>(payload)?;
let name = strip_nul(&payload[core::mem::size_of::<MkdirIn>()..]).ok_or(super::backend::EINVAL)?;
let entry = self.backend.mkdir(hdr.nodeid, OsStr::from_bytes(name), req.mode)?;
let out = EntryOut {
nodeid: entry.nodeid,
generation: entry.generation,
entry_valid: entry.entry_valid,
attr_valid: entry.attr_valid,
entry_valid_nsec: 0,
attr_valid_nsec: 0,
attr: entry.attr,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const EntryOut as *const u8,
core::mem::size_of::<EntryOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
fn handle_unlink(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let name = strip_nul(payload).ok_or(super::backend::EINVAL)?;
self.backend.unlink(hdr.nodeid, OsStr::from_bytes(name))?;
Ok(Reply::header_only(hdr.unique, 0))
}
fn handle_rmdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let name = strip_nul(payload).ok_or(super::backend::EINVAL)?;
self.backend.rmdir(hdr.nodeid, OsStr::from_bytes(name))?;
Ok(Reply::header_only(hdr.unique, 0))
}
fn handle_opendir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<OpenIn>(payload)?;
let fh = self.backend.opendir(hdr.nodeid, req.flags)?;
let out = OpenOut {
fh,
open_flags: 0,
_pad: 0,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const OpenOut as *const u8,
core::mem::size_of::<OpenOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
fn handle_readdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<ReadIn>(payload)?;
let entries = self
.backend
.readdir(hdr.nodeid, req.fh, req.offset, req.size)?;
let mut out: Vec<u8> = Vec::with_capacity(req.size as usize);
let cap = req.size as usize;
for (i, e) in entries.iter().enumerate() {
let needed = align_up(core::mem::size_of::<DirentHeader>() + e.name.len(), 8);
if out.len() + needed > cap {
break;
}
let off = req.offset + i as u64 + 1;
emit_dirent(&mut out, e.ino, off, e.typ, &e.name);
}
Ok(Reply::with_payload(hdr.unique, &out))
}
fn handle_releasedir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<ReleaseIn>(payload)?;
self.backend.releasedir(hdr.nodeid, req.fh)?;
Ok(Reply::header_only(hdr.unique, 0))
}
fn handle_setup_mapping(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let req = read_struct::<SetupMappingIn>(payload)?;
let dax = self
.dax
.lock()
.unwrap()
.clone()
.ok_or(super::backend::ENOSYS)?;
dax.setup(hdr.nodeid, &req)?;
Ok(Reply::header_only(hdr.unique, 0))
}
fn handle_remove_mapping(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
let entries = parse_remove_payload(payload)?;
let dax = self
.dax
.lock()
.unwrap()
.clone()
.ok_or(super::backend::ENOSYS)?;
let mut first_err: Option<Errno> = None;
for e in &entries {
if let Err(err) = dax.remove(hdr.nodeid, e) {
first_err.get_or_insert(err);
}
}
match first_err {
None => Ok(Reply::header_only(hdr.unique, 0)),
Some(e) => Err(e),
}
}
fn handle_statfs(&self, hdr: &InHeader) -> Result<Reply, Errno> {
let s = self.backend.statfs(hdr.nodeid)?;
let out = StatfsOut {
st: Kstatfs {
blocks: s.blocks,
bfree: s.bfree,
bavail: s.bavail,
files: s.files,
ffree: s.ffree,
bsize: s.bsize,
namelen: s.namelen,
frsize: s.frsize,
_pad: 0,
spare: [0; 6],
},
};
let bytes = unsafe {
std::slice::from_raw_parts(
&out as *const StatfsOut as *const u8,
core::mem::size_of::<StatfsOut>(),
)
};
Ok(Reply::with_payload(hdr.unique, bytes))
}
}
fn trace<F: FnOnce() -> String>(make_msg: F) {
use std::io::Write;
let Some(target) = std::env::var_os("SUPERMACHINE_FUSE_TRACE") else { return };
let s = make_msg();
let target = target.to_string_lossy().into_owned();
if target == "1" || target == "stderr" {
eprintln!("[fuse] {s}");
return;
}
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&target) {
let _ = writeln!(f, "[fuse] {s}");
}
}
fn strip_nul(buf: &[u8]) -> Option<&[u8]> {
if buf.is_empty() {
return None;
}
let end = if buf[buf.len() - 1] == 0 {
buf.len() - 1
} else {
buf.len()
};
if end == 0 {
None
} else {
Some(&buf[..end])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fuse::backend::MemoryFs;
use crate::fuse::protocol::{FUSE_KERNEL_MINOR_VERSION, FUSE_ROOT_ID, S_IFDIR, S_IFMT, S_IFREG};
fn make_server() -> (FuseServer, Arc<MemoryFs>) {
let fs = Arc::new(MemoryFs::new());
let srv = FuseServer::new(fs.clone());
(srv, fs)
}
fn make_in_header(op: Opcode, unique: u64, nodeid: u64, payload_len: usize) -> InHeader {
InHeader {
len: (core::mem::size_of::<InHeader>() + payload_len) as u32,
opcode: op as u32,
unique,
nodeid,
uid: 1000,
gid: 1000,
pid: 42,
padding: 0,
}
}
fn extract_out_header(bytes: &[u8]) -> OutHeader {
unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const OutHeader) }
}
#[test]
fn init_then_lookup_root_child() {
let (srv, fs) = make_server();
let f = fs.add_file(FUSE_ROOT_ID, b"foo.txt", b"hi".to_vec());
let init_in = InitIn {
major: FUSE_KERNEL_VERSION,
minor: FUSE_KERNEL_MINOR_VERSION,
max_readahead: 0,
flags: FUSE_MAP_ALIGNMENT,
flags2: 0,
unused: [0; 11],
};
let payload = unsafe {
std::slice::from_raw_parts(
&init_in as *const InitIn as *const u8,
core::mem::size_of::<InitIn>(),
)
};
let hdr = make_in_header(Opcode::Init, 1, 0, payload.len());
let init_reply = srv.dispatch(&hdr, payload);
assert_eq!(extract_out_header(&init_reply.bytes).error, 0);
let name = b"foo.txt\0";
let hdr = make_in_header(Opcode::Lookup, 2, FUSE_ROOT_ID, name.len());
let reply = srv.dispatch(&hdr, name);
let oh = extract_out_header(&reply.bytes);
assert_eq!(oh.error, 0, "LOOKUP returned errno");
let entry_out: EntryOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const EntryOut,
)
};
assert_eq!(entry_out.nodeid, f);
assert_eq!(entry_out.attr.size, 2);
assert_eq!(entry_out.attr.mode & S_IFMT, S_IFREG);
}
#[test]
fn lookup_miss_returns_enoent() {
let (srv, _) = make_server();
let name = b"nope\0";
let hdr = make_in_header(Opcode::Lookup, 7, FUSE_ROOT_ID, name.len());
let reply = srv.dispatch(&hdr, name);
assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::ENOENT);
}
#[test]
fn open_then_read_returns_file_bytes() {
let (srv, fs) = make_server();
let payload = (0u8..=255).collect::<Vec<_>>();
let f = fs.add_file(FUSE_ROOT_ID, b"data.bin", payload.clone());
let open_in = OpenIn {
flags: 0,
open_flags: 0,
};
let hdr = make_in_header(
Opcode::Open,
3,
f,
core::mem::size_of::<OpenIn>(),
);
let raw = unsafe {
std::slice::from_raw_parts(
&open_in as *const OpenIn as *const u8,
core::mem::size_of::<OpenIn>(),
)
};
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
let open_out: OpenOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
)
};
let fh = open_out.fh;
let read_in = ReadIn {
fh,
offset: 50,
size: 100,
read_flags: 0,
lock_owner: 0,
flags: 0,
_pad: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&read_in as *const ReadIn as *const u8,
core::mem::size_of::<ReadIn>(),
)
};
let hdr = make_in_header(Opcode::Read, 4, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
let oh = extract_out_header(&reply.bytes);
assert_eq!(oh.error, 0);
let data = &reply.bytes[core::mem::size_of::<OutHeader>()..];
assert_eq!(data.len(), 100);
assert_eq!(data, &payload[50..150]);
let rel = ReleaseIn {
fh,
flags: 0,
release_flags: 0,
lock_owner: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&rel as *const ReleaseIn as *const u8,
core::mem::size_of::<ReleaseIn>(),
)
};
let hdr = make_in_header(Opcode::Release, 5, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
}
#[test]
fn opendir_readdir_releasedir_round_trip() {
let (srv, fs) = make_server();
fs.add_file(FUSE_ROOT_ID, b"a", b"x".to_vec());
fs.add_file(FUSE_ROOT_ID, b"bb", b"y".to_vec());
fs.add_dir(FUSE_ROOT_ID, b"ccc");
let open_in = OpenIn {
flags: 0,
open_flags: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&open_in as *const OpenIn as *const u8,
core::mem::size_of::<OpenIn>(),
)
};
let hdr = make_in_header(Opcode::Opendir, 8, FUSE_ROOT_ID, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
let open_out: OpenOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
)
};
let fh = open_out.fh;
let read_in = ReadIn {
fh,
offset: 0,
size: 4096,
read_flags: 0,
lock_owner: 0,
flags: 0,
_pad: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&read_in as *const ReadIn as *const u8,
core::mem::size_of::<ReadIn>(),
)
};
let hdr = make_in_header(Opcode::Readdir, 9, FUSE_ROOT_ID, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
let mut cur = &reply.bytes[core::mem::size_of::<OutHeader>()..];
let mut names: Vec<(Vec<u8>, u32)> = Vec::new();
while cur.len() >= core::mem::size_of::<DirentHeader>() {
let dh: DirentHeader = unsafe { core::ptr::read_unaligned(cur.as_ptr() as *const DirentHeader) };
let nm_start = core::mem::size_of::<DirentHeader>();
let nm_end = nm_start + dh.namelen as usize;
let name = cur[nm_start..nm_end].to_vec();
names.push((name, dh.typ));
let entry_size = align_up(nm_end, 8);
cur = &cur[entry_size..];
}
assert_eq!(names.len(), 3);
let by_name: std::collections::HashMap<&[u8], u32> =
names.iter().map(|(n, t)| (n.as_slice(), *t)).collect();
assert_eq!(by_name[&b"a"[..]], crate::fuse::protocol::DT_REG);
assert_eq!(by_name[&b"bb"[..]], crate::fuse::protocol::DT_REG);
assert_eq!(by_name[&b"ccc"[..]], crate::fuse::protocol::DT_DIR);
let rel = ReleaseIn {
fh,
flags: 0,
release_flags: 0,
lock_owner: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&rel as *const ReleaseIn as *const u8,
core::mem::size_of::<ReleaseIn>(),
)
};
let hdr = make_in_header(Opcode::Releasedir, 10, FUSE_ROOT_ID, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
}
#[test]
fn getattr_returns_attr_of_root() {
let (srv, _) = make_server();
let getattr_in = GetattrIn::default();
let raw = unsafe {
std::slice::from_raw_parts(
&getattr_in as *const GetattrIn as *const u8,
core::mem::size_of::<GetattrIn>(),
)
};
let hdr = make_in_header(Opcode::Getattr, 11, FUSE_ROOT_ID, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
let attr_out: AttrOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const AttrOut,
)
};
assert_eq!(attr_out.attr.ino, FUSE_ROOT_ID);
assert_eq!(attr_out.attr.mode & S_IFMT, S_IFDIR);
}
#[test]
fn statfs_returns_filesystem_stats() {
let (srv, _) = make_server();
let hdr = make_in_header(Opcode::Statfs, 12, FUSE_ROOT_ID, 0);
let reply = srv.dispatch(&hdr, &[]);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
let st: StatfsOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const StatfsOut,
)
};
assert_eq!(st.st.bsize, 4096);
}
#[test]
fn forget_decrements_lookup_count() {
let (srv, fs) = make_server();
let f = fs.add_file(FUSE_ROOT_ID, b"x", b"data".to_vec());
let name = b"x\0";
let hdr = make_in_header(Opcode::Lookup, 1, FUSE_ROOT_ID, name.len());
let _ = srv.dispatch(&hdr, name);
let forget = ForgetIn { nlookup: 1 };
let raw = unsafe {
std::slice::from_raw_parts(
&forget as *const ForgetIn as *const u8,
core::mem::size_of::<ForgetIn>(),
)
};
let hdr = make_in_header(Opcode::Forget, 2, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).unique, 0);
}
#[test]
fn unsupported_opcode_replies_enosys() {
let (srv, _) = make_server();
let hdr = make_in_header(Opcode::Symlink, 99, FUSE_ROOT_ID, 0);
let reply = srv.dispatch(&hdr, &[]);
assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::ENOSYS);
}
#[test]
fn setup_mapping_without_dax_returns_enosys() {
let (srv, _) = make_server();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: super::super::protocol::FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&req as *const SetupMappingIn as *const u8,
core::mem::size_of::<SetupMappingIn>(),
)
};
let hdr = make_in_header(Opcode::SetupMapping, 13, FUSE_ROOT_ID, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::ENOSYS);
}
#[test]
fn write_then_read_round_trip() {
let (srv, fs) = make_server();
let f = fs.add_file(FUSE_ROOT_ID, b"out.bin", vec![0u8; 16]);
let open_in = OpenIn { flags: 0, open_flags: 0 };
let raw = unsafe {
std::slice::from_raw_parts(
&open_in as *const OpenIn as *const u8,
core::mem::size_of::<OpenIn>(),
)
};
let hdr = make_in_header(Opcode::Open, 100, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
let open_out: OpenOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
)
};
let fh = open_out.fh;
let payload_bytes: [u8; 8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22];
let write_in = WriteIn {
fh,
offset: 4,
size: payload_bytes.len() as u32,
write_flags: 0,
lock_owner: 0,
flags: 0,
_pad: 0,
};
let mut buf = Vec::new();
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
&write_in as *const WriteIn as *const u8,
core::mem::size_of::<WriteIn>(),
)
});
buf.extend_from_slice(&payload_bytes);
let hdr = make_in_header(Opcode::Write, 101, f, buf.len());
let reply = srv.dispatch(&hdr, &buf);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
let write_out: WriteOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const WriteOut,
)
};
assert_eq!(write_out.size, 8);
let read_in = ReadIn {
fh,
offset: 0,
size: 16,
read_flags: 0,
lock_owner: 0,
flags: 0,
_pad: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&read_in as *const ReadIn as *const u8,
core::mem::size_of::<ReadIn>(),
)
};
let hdr = make_in_header(Opcode::Read, 102, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
let data = &reply.bytes[core::mem::size_of::<OutHeader>()..];
assert_eq!(&data[..4], &[0, 0, 0, 0]);
assert_eq!(&data[4..12], &payload_bytes[..]);
}
#[test]
fn fsync_round_trip() {
let (srv, fs) = make_server();
let f = fs.add_file(FUSE_ROOT_ID, b"f", vec![0u8; 4]);
let open_in = OpenIn { flags: 0, open_flags: 0 };
let raw = unsafe {
std::slice::from_raw_parts(
&open_in as *const OpenIn as *const u8,
core::mem::size_of::<OpenIn>(),
)
};
let hdr = make_in_header(Opcode::Open, 200, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
let open_out: OpenOut = unsafe {
core::ptr::read_unaligned(
reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
)
};
let fsync_in = FsyncIn { fh: open_out.fh, fsync_flags: 0, _pad: 0 };
let raw = unsafe {
std::slice::from_raw_parts(
&fsync_in as *const FsyncIn as *const u8,
core::mem::size_of::<FsyncIn>(),
)
};
let hdr = make_in_header(Opcode::Fsync, 201, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0);
}
#[test]
fn setup_then_remove_mapping_via_dax_session() {
use crate::fuse::dax::{DaxSession, MockCall, MockHvfMapper};
use crate::fuse::protocol::{
FUSE_SETUPMAPPING_FLAG_READ, FUSE_SETUPMAPPING_FLAG_WRITE,
};
use crate::fuse::backend::{Entry, DirEntry as BDirEntry, StatFs};
use std::ffi::OsStr;
use std::sync::Mutex;
struct DaxStubBackend {
inner: MemoryFs,
next_va: Mutex<usize>,
}
impl FsBackend for DaxStubBackend {
fn lookup(&self, p: u64, n: &OsStr) -> Result<Entry, super::super::backend::Errno> { self.inner.lookup(p, n) }
fn forget(&self, n: u64, nl: u64) { self.inner.forget(n, nl) }
fn getattr(&self, n: u64, f: Option<u64>) -> Result<crate::fuse::protocol::Attr, super::super::backend::Errno> { self.inner.getattr(n, f) }
fn open(&self, n: u64, f: u32) -> Result<u64, super::super::backend::Errno> { self.inner.open(n, f) }
fn read(&self, n: u64, h: u64, o: u64, s: u32) -> Result<Vec<u8>, super::super::backend::Errno> { self.inner.read(n, h, o, s) }
fn release(&self, n: u64, h: u64) -> Result<(), super::super::backend::Errno> { self.inner.release(n, h) }
fn opendir(&self, n: u64, f: u32) -> Result<u64, super::super::backend::Errno> { self.inner.opendir(n, f) }
fn readdir(&self, n: u64, h: u64, o: u64, s: u32) -> Result<Vec<BDirEntry>, super::super::backend::Errno> { self.inner.readdir(n, h, o, s) }
fn releasedir(&self, n: u64, h: u64) -> Result<(), super::super::backend::Errno> { self.inner.releasedir(n, h) }
fn statfs(&self, n: u64) -> Result<StatFs, super::super::backend::Errno> { self.inner.statfs(n) }
fn dax_map(&self, _n: u64, _f: u64, _fo: u64, len: u64, _p: u32) -> Result<*mut u8, super::super::backend::Errno> {
let mut g = self.next_va.lock().unwrap();
let va = *g;
*g += len as usize;
Ok(va as *mut u8)
}
fn dax_unmap(&self, _n: u64, _va: *mut u8, _len: u64) -> Result<(), super::super::backend::Errno> { Ok(()) }
}
let backend = Arc::new(DaxStubBackend {
inner: MemoryFs::new(),
next_va: Mutex::new(0x40_0000_0000usize),
});
let f = backend.inner.add_file(FUSE_ROOT_ID, b"a", vec![0u8; 0x4000]);
let mapper = Arc::new(MockHvfMapper::new());
let session = Arc::new(DaxSession::new(
0x80_0000_0000,
0x4_0000_0000,
backend.clone(),
mapper.clone(),
));
let srv = FuseServer::new(backend);
srv.set_dax(session);
let setup = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ | FUSE_SETUPMAPPING_FLAG_WRITE,
moffset: 0x2_0000,
};
let raw = unsafe {
std::slice::from_raw_parts(
&setup as *const SetupMappingIn as *const u8,
core::mem::size_of::<SetupMappingIn>(),
)
};
let hdr = make_in_header(Opcode::SetupMapping, 20, f, raw.len());
let reply = srv.dispatch(&hdr, raw);
assert_eq!(extract_out_header(&reply.bytes).error, 0, "SETUPMAPPING ok");
let calls = mapper.calls();
assert_eq!(calls.len(), 1);
match &calls[0] {
MockCall::Map { gpa, len, .. } => {
assert_eq!(*gpa, 0x80_0000_0000 + 0x2_0000);
assert_eq!(*len, 0x4000);
}
_ => panic!(),
}
use crate::fuse::protocol::{RemoveMappingIn, RemoveMappingOne};
let rm_hdr = RemoveMappingIn { count: 1, _pad: 0 };
let rm_one = RemoveMappingOne {
moffset: 0x2_0000,
len: 0x4000,
};
let mut buf = Vec::new();
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
&rm_hdr as *const RemoveMappingIn as *const u8,
core::mem::size_of::<RemoveMappingIn>(),
)
});
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
&rm_one as *const RemoveMappingOne as *const u8,
core::mem::size_of::<RemoveMappingOne>(),
)
});
let hdr = make_in_header(Opcode::RemoveMapping, 21, f, buf.len());
let reply = srv.dispatch(&hdr, &buf);
assert_eq!(extract_out_header(&reply.bytes).error, 0, "REMOVEMAPPING ok");
let calls = mapper.calls();
assert_eq!(calls.len(), 2);
match &calls[1] {
MockCall::Unmap { gpa, len } => {
assert_eq!(*gpa, 0x80_0000_0000 + 0x2_0000);
assert_eq!(*len, 0x4000);
}
_ => panic!(),
}
}
}