Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# FUSE for Rust - Changelog

## 0.16.0 - 2025-06-24
* **Major API Refactor**: The `Filesystem` trait methods have been refactored to return `Result` instead of using `Reply` objects for callbacks.
* All `Filesystem` trait methods that previously accepted a `reply: ReplyT` parameter now return a `Result<T, Errno>`, where `T` is a struct containing the success data for that operation.
* The `Request` object passed to `Filesystem` methods has been replaced with `RequestMeta`, a smaller struct containing only the request's metadata (uid, gid, pid, unique id). The full request parsing is now handled internally.
* Additional public structs are introduced to simplify handling of request data, response data, and errors.
* This change unifies the implementation of `Filesystem` methods and brings them more in line with Idiomatic Rust.
* Examples and tests have been updated to match this new API. A few unrelated bugs in examples and tests have been fixed.
* Idiomatic implementation of Passthrough and Notify are TODO items.

## 0.15.1 - 2024-11-27
* Fix crtime related panic that could occur on MacOS. See PR #322 for details.

Expand Down
125 changes: 94 additions & 31 deletions examples/hello.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use clap::{crate_version, Arg, ArgAction, Command};
use fuser::{
FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry,
Request,
Filesystem, MountOption, Attr, DirEntry,
Entry, Errno, RequestMeta, FileType, FileAttr
};
use libc::ENOENT;
use std::ffi::OsStr;
use std::ffi::{OsStr, OsString};
use std::time::{Duration, UNIX_EPOCH};

const TTL: Duration = Duration::from_secs(1); // 1 second
Expand Down Expand Up @@ -50,66 +49,68 @@ const HELLO_TXT_ATTR: FileAttr = FileAttr {
struct HelloFS;

impl Filesystem for HelloFS {
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
if parent == 1 && name.to_str() == Some("hello.txt") {
reply.entry(&TTL, &HELLO_TXT_ATTR, 0);
fn lookup(&mut self, _req: RequestMeta, parent: u64, name: OsString) -> Result<Entry, Errno> {
if parent == 1 && name == OsStr::new("hello.txt") {
Ok(Entry{attr: HELLO_TXT_ATTR, ttl: TTL, generation: 0})
} else {
reply.error(ENOENT);
Err(Errno::ENOENT)
}
}

fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
fn getattr(
&mut self,
_req: RequestMeta,
ino: u64,
_fh: Option<u64>,
) -> Result<Attr, Errno> {
match ino {
1 => reply.attr(&TTL, &HELLO_DIR_ATTR),
2 => reply.attr(&TTL, &HELLO_TXT_ATTR),
_ => reply.error(ENOENT),
1 => Ok(Attr{attr: HELLO_DIR_ATTR, ttl: TTL,}),
2 => Ok(Attr{attr: HELLO_TXT_ATTR, ttl: TTL,}),
_ => Err(Errno::ENOENT),
}
}

fn read(
&mut self,
_req: &Request,
_req: RequestMeta,
ino: u64,
_fh: u64,
offset: i64,
_size: u32,
_flags: i32,
_lock: Option<u64>,
reply: ReplyData,
) {
) -> Result<Vec<u8>, Errno> {
if ino == 2 {
reply.data(&HELLO_TXT_CONTENT.as_bytes()[offset as usize..]);
Ok(HELLO_TXT_CONTENT.as_bytes()[offset as usize..].to_vec())
} else {
reply.error(ENOENT);
Err(Errno::ENOENT)
}
}

fn readdir(
&mut self,
_req: &Request,
_req: RequestMeta,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
_max_bytes: u32,
) -> Result<Vec<DirEntry>, Errno> {
if ino != 1 {
reply.error(ENOENT);
return;
return Err(Errno::ENOENT);
}

let entries = vec![
(1, FileType::Directory, "."),
(1, FileType::Directory, ".."),
(2, FileType::RegularFile, "hello.txt"),
DirEntry { ino: 1, offset: 1, kind: FileType::Directory, name: OsString::from(".") },
DirEntry { ino: 1, offset: 2, kind: FileType::Directory, name: OsString::from("..") },
DirEntry { ino: 2, offset: 3, kind: FileType::RegularFile, name: OsString::from("hello.txt") },
];

for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
// i + 1 means the index of the next entry
if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) {
break;
}
let mut result = Vec::new();
for entry in entries.into_iter().skip(offset as usize) {
// example loop where additional logic could be inserted
result.push(entry);
}
reply.ok();
Ok(result)
}
}

Expand Down Expand Up @@ -147,3 +148,65 @@ fn main() {
}
fuser::mount2(HelloFS, mountpoint, &options).unwrap();
}

#[cfg(test)]
mod test {
use fuser::{Filesystem, RequestMeta, Errno, FileType};
use std::ffi::OsString;

fn dummy_meta() -> RequestMeta {
RequestMeta { unique: 0, uid: 1000, gid: 1000, pid: 2000 }
}

#[test]
fn test_lookup_hello_txt() {
let mut hellofs = super::HelloFS {};
let req = dummy_meta();
let result = hellofs.lookup(req, 1, OsString::from("hello.txt"));
assert!(result.is_ok(), "Lookup for hello.txt should succeed");
if let Ok(entry) = result {
assert_eq!(entry.attr.ino, 2, "Lookup should return inode 2 for hello.txt");
assert_eq!(entry.attr.kind, FileType::RegularFile, "hello.txt should be a regular file");
assert_eq!(entry.attr.perm, 0o644, "hello.txt should have permissions 0o644");
}
}

#[test]
fn test_read_hello_txt() {
let mut hellofs = super::HelloFS {};
let req = dummy_meta();
let result = hellofs.read(req, 2, 0, 0, 13, 0, None);
assert!(result.is_ok(), "Read for hello.txt should succeed");
if let Ok(content) = result {
assert_eq!(String::from_utf8_lossy(&content), "Hello World!\n", "Content of hello.txt should be 'Hello World!\\n'");
}
}

#[test]
fn test_readdir_root() {
let mut hellofs = super::HelloFS {};
let req = dummy_meta();
let result = hellofs.readdir(req, 1, 0, 0, 4096);
assert!(result.is_ok(), "Readdir on root should succeed");
if let Ok(entries) = result {
assert_eq!(entries.len(), 3, "Root directory should contain exactly 3 entries");
assert_eq!(entries[0].name, OsString::from("."), "First entry should be '.'");
assert_eq!(entries[0].ino, 1, "Inode for '.' should be 1");
assert_eq!(entries[1].name, OsString::from(".."), "Second entry should be '..'");
assert_eq!(entries[1].ino, 1, "Inode for '..' should be 1");
assert_eq!(entries[2].name, OsString::from("hello.txt"), "Third entry should be 'hello.txt'");
assert_eq!(entries[2].ino, 2, "Inode for 'hello.txt' should be 2");
}
}

#[test]
fn test_create_fails_readonly() {
let mut hellofs = super::HelloFS {};
let req = dummy_meta();
let result = hellofs.create(req, 1, OsString::from("newfile.txt"), 0o644, 0, 0);
assert!(result.is_err(), "Create should fail for read-only filesystem");
if let Err(e) = result {
assert_eq!(e, Errno::ENOSYS, "Create should return ENOSYS for unsupported operation");
}
}
}
86 changes: 45 additions & 41 deletions examples/ioctl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

use clap::{crate_version, Arg, ArgAction, Command};
use fuser::{
FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry,
Request,
FileAttr, FileType, Filesystem, MountOption, RequestMeta, Entry, Attr, Ioctl, Errno, DirEntry,
};
use libc::{EINVAL, ENOENT};
use log::debug;
use std::ffi::OsStr;
use std::ffi::{OsStr, OsString};
use std::time::{Duration, UNIX_EPOCH};

const TTL: Duration = Duration::from_secs(1); // 1 second
Expand Down Expand Up @@ -70,82 +68,82 @@ impl FiocFS {
}

impl Filesystem for FiocFS {
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
if parent == 1 && name.to_str() == Some("fioc") {
reply.entry(&TTL, &self.fioc_file_attr, 0);
fn lookup(&mut self, _req: RequestMeta, parent: u64, name: OsString) -> Result<Entry, Errno> {
if parent == 1 && name == OsStr::new("fioc") {
Ok(Entry {
attr: self.fioc_file_attr,
ttl: TTL,
generation: 0,
})
} else {
reply.error(ENOENT);
Err(Errno::ENOENT)
}
}

fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
fn getattr(&mut self, _req: RequestMeta, ino: u64, _fh: Option<u64>) -> Result<Attr, Errno> {
match ino {
1 => reply.attr(&TTL, &self.root_attr),
2 => reply.attr(&TTL, &self.fioc_file_attr),
_ => reply.error(ENOENT),
1 => Ok(Attr { attr: self.root_attr, ttl: TTL,}),
2 => Ok(Attr { attr: self.fioc_file_attr, ttl: TTL,}),
_ => Err(Errno::ENOENT),
}
}

fn read(
&mut self,
_req: &Request,
_req: RequestMeta,
ino: u64,
_fh: u64,
offset: i64,
_size: u32,
_flags: i32,
_lock: Option<u64>,
reply: ReplyData,
) {
) -> Result<Vec<u8>, Errno> {
if ino == 2 {
reply.data(&self.content[offset as usize..])
Ok(self.content[offset as usize..].to_vec())
} else {
reply.error(ENOENT);
Err(Errno::ENOENT)
}
}

fn readdir(
&mut self,
_req: &Request,
_req: RequestMeta,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
_max_bytes: u32,
) -> Result<Vec<DirEntry>, Errno> {
if ino != 1 {
reply.error(ENOENT);
return;
return Err(Errno::ENOENT);
}

let entries = vec![
(1, FileType::Directory, "."),
(1, FileType::Directory, ".."),
(2, FileType::RegularFile, "fioc"),
DirEntry { ino: 1, offset: 1, kind: FileType::Directory, name: OsString::from(".") },
DirEntry { ino: 1, offset: 2, kind: FileType::Directory, name: OsString::from("..") },
DirEntry { ino: 2, offset: 3, kind: FileType::RegularFile, name: OsString::from("fioc") },
];

for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
// i + 1 means the index of the next entry
if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) {
break;
}
let mut result = Vec::new();
for entry in entries.into_iter().skip(offset as usize) {
// example loop where additional logic could be inserted
result.push(entry);
}
reply.ok();
Ok(result)
}

#[cfg(feature = "abi-7-11")]
fn ioctl(
&mut self,
_req: &Request<'_>,
_req: RequestMeta,
ino: u64,
_fh: u64,
_flags: u32,
cmd: u32,
in_data: &[u8],
in_data: Vec<u8>,
_out_size: u32,
reply: fuser::ReplyIoctl,
) {
) -> Result<Ioctl, Errno> {
if ino != 2 {
reply.error(EINVAL);
return;
return Err(Errno::EINVAL);
}

const FIOC_GET_SIZE: u64 = nix::request_code_read!('E', 0, std::mem::size_of::<usize>());
Expand All @@ -154,16 +152,22 @@ impl Filesystem for FiocFS {
match cmd.into() {
FIOC_GET_SIZE => {
let size_bytes = self.content.len().to_ne_bytes();
reply.ioctl(0, &size_bytes);
Ok(Ioctl {
result: 0,
data: size_bytes.to_vec(),
})
}
FIOC_SET_SIZE => {
let new_size = usize::from_ne_bytes(in_data.try_into().unwrap());
let new_size = usize::from_ne_bytes(in_data.as_slice().try_into().unwrap());
self.content = vec![0_u8; new_size];
reply.ioctl(0, &[]);
Ok(Ioctl {
result: 0,
data: vec![],
})
}
_ => {
debug!("unknown ioctl: {}", cmd);
reply.error(EINVAL);
Err(Errno::EINVAL)
}
}
}
Expand Down
Loading
Loading