Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eeac00b
Filesystem methods now return `Result<_, Errno>`.
rarensu Jul 16, 2025
d6a1d06
Filesystem method `req` argument simplified from borrowed `Request` t…
rarensu Jul 16, 2025
c6e6e4e
Struct-based return types for most Filesystem methods. Created new st…
rarensu Jul 16, 2025
b57b04a
Many small changes for style and optimization.
rarensu Jul 16, 2025
cebd72b
New! Generic enum Container that allows the API to accept
rarensu Jul 16, 2025
52c90f6
Reorganized Dirent(Plus)-related structs from ll -> public.
rarensu Jul 16, 2025
455b5c9
Updated examples to use Filesystem traits that return containers.
rarensu Jul 16, 2025
28d7887
Style changes suggested by Clippy.
rarensu Jul 16, 2025
b82724d
Corrections to MacOS features.
rarensu Jul 21, 2025
3fb1e6e
Reorganized definitions and imports slightly.
rarensu Jul 21, 2025
b2d3977
New feature! Crossbeam_channel-based Notifications handled safely by …
rarensu Jul 21, 2025
08fba79
Completed the passthrough feature. Backing ids are acquired through t…
rarensu Jul 21, 2025
9cc620e
Draft changelog entry announcing new features.
rarensu Jul 21, 2025
5e4b0ff
Revert bad clippy suggestion in hello::tests. Thinking about using By…
rarensu Jul 21, 2025
f0d79de
Check for non-directory in simple example.
rarensu Jul 21, 2025
5e25789
More tactful error handling on background session join.
rarensu Jul 22, 2025
0df9d34
Backticks in Container::to_vec() docstring
rarensu Jul 30, 2025
f643488
Standardized Derive traits for easier unit testing.
rarensu Aug 1, 2025
55bc56c
Feature gates: `"threaded"` and `"no-rc"` (both on by default). Backg…
rarensu Aug 2, 2025
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# FUSE for Rust - Changelog

## release candidate 0.16.0 - 2025-07-21 (pending)
* **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.
* A generic enum `Container` has been implemented to enable flexible ownership models in returned data.
* 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.
* Notifications and their results now pass through crossbeam_channels that are safely handled in a synchronous loop.
* A new `heartbeat()` operation enables a single-threaded execution model to monitor the passage of time.
* Idiomatic implementation of Passthrough and Notify have been completed.
* Unit tests have been added to most examples, enabled by the new callback-free API.
* Feature gates and clippy configuration are more consistently applied.
* Minor fixes for better MacOS support.
* Improved documentation throughout.

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

Expand Down
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde = { version = "1.0.102", features = ["std", "derive"], optional = true }
smallvec = "1.6.1"
zerocopy = { version = "0.8", features = ["derive"] }
nix = { version = "0.29.0", features = ["fs", "user"] }
crossbeam-channel = "0.5"

[dev-dependencies]
env_logger = "0.11.7"
Expand All @@ -35,7 +36,9 @@ nix = { version = "0.29.0", features = ["poll", "fs", "ioctl"] }
pkg-config = { version = "0.3.14", optional = true }

[features]
default = []
default = ["threaded"]
threaded = ["no-rc"]
no-rc = []
libfuse = ["pkg-config"]
serializable = ["serde"]
macfuse-4-compat = []
Expand Down
4 changes: 2 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn main() {
if pkg_config::Config::new()
.atleast_version("2.6.0")
.probe("fuse") // for macFUSE 4.x
.map_err(|e| eprintln!("{}", e))
.map_err(|e| eprintln!("{e}"))
.is_ok()
{
println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse2\"");
Expand All @@ -25,7 +25,7 @@ fn main() {
pkg_config::Config::new()
.atleast_version("2.6.0")
.probe("osxfuse") // for osxfuse 3.x
.map_err(|e| eprintln!("{}", e))
.map_err(|e| eprintln!("{e}"))
.unwrap();
println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse2\"");
}
Expand Down
239 changes: 200 additions & 39 deletions examples/hello.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use clap::{crate_version, Arg, ArgAction, Command};
use fuser::{
FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry,
Request,
FileAttr, Bytes, Dirent, DirentList, Entry, Errno,
Filesystem, FileType, MountOption, RequestMeta,
};
use libc::ENOENT;
use std::ffi::OsStr;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use std::time::{Duration, UNIX_EPOCH};
use std::rc::Rc;

const TTL: Duration = Duration::from_secs(1); // 1 second

Expand Down Expand Up @@ -47,69 +48,148 @@ const HELLO_TXT_ATTR: FileAttr = FileAttr {
blksize: 512,
};

struct HelloFS;
// An example of reusable Borrowed data.
// This entry derives its lifetime from string literal,
// which is 'static.
const DOT_ENTRY: Dirent<'static> = Dirent {
ino: 1,
offset: 1,
kind: FileType::Directory,
name: Bytes::Ref(b"."),
};

/// Example Filesystem data
struct HelloFS<'a> {
hello_entry: Rc<Dirent<'a>>,
}

impl HelloFS<'_> {
fn new() -> Self {
HelloFS{
// An example of reusable Shared data.
// Entry #3 is allocated here once.
// It is persistent until replaced.
hello_entry: Rc::new(
Dirent {
ino: 2,
offset: 3,
kind: FileType::RegularFile,
name: OsString::from("hello.txt").into(),
}
)
}
}
}

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);
impl Filesystem for HelloFS<'static> {
// Must specify HelloFS lifetime ('static) here
// to enable its methods to return borrowed data
fn lookup(&mut self, _req: RequestMeta, parent: u64, name: &Path) -> Result<Entry, Errno> {
if parent == 1 && name == OsStr::new("hello.txt") {
Ok(Entry{
ino: 2,
generation: None,
file_ttl: TTL,
attr: HELLO_TXT_ATTR,
attr_ttl: TTL,
})
} 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<(FileAttr, Duration), Errno> {
match ino {
1 => reply.attr(&TTL, &HELLO_DIR_ATTR),
2 => reply.attr(&TTL, &HELLO_TXT_ATTR),
_ => reply.error(ENOENT),
1 => Ok((HELLO_DIR_ATTR, TTL)),
2 => Ok((HELLO_TXT_ATTR, TTL)),
_ => Err(Errno::ENOENT),
}
}

fn read(
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn read<'a>(
&mut self,
_req: &Request,
_req: RequestMeta,
ino: u64,
_fh: u64,
offset: i64,
_size: u32,
_flags: i32,
_lock: Option<u64>,
reply: ReplyData,
) {
) -> Result<Bytes<'a>, Errno> {
if ino == 2 {
reply.data(&HELLO_TXT_CONTENT.as_bytes()[offset as usize..]);
// HELLO_TXT_CONTENT is &'static str, so its bytes are &'static [u8]
let bytes = HELLO_TXT_CONTENT.as_bytes();
let slice_len = bytes.len();
let offset = offset as usize;
if offset >= slice_len {
Ok(Bytes::Ref(&[]))
} else {
// Returning as Borrowed to avoid a copy.
Ok(Bytes::Ref(&bytes[offset..]))
}
} else {
reply.error(ENOENT);
Err(Errno::ENOENT)
}
}

fn readdir(
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn readdir<'dir, 'name>(
&mut self,
_req: &Request,
_req: RequestMeta,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
_max_bytes: u32,
) -> Result<DirentList<'dir, 'name>, 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"),
];
// This example builds the list of 3 directory entries from scratch
// on each call to readdir().
let mut entries= Vec::new();

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;
}
}
reply.ok();
// Entry 1: example of borrowed data.
// - name: "."
// - entry is constructed in the global scope.
// - lifetime is 'static.
// - a reference is passed along.
entries.push(DOT_ENTRY.clone());

// Entry 2: example of single-use Owned data.
// - name: ".."
// - entry is constructed during each call to readdir().
let dotdot_entry = Dirent {
ino: 1, // Parent of root is itself for simplicity.
// Note: this can cause some weird behavior for an observer.
offset: 2,
kind: FileType::Directory,
// ownership of the string is moved into the DirEntry
name: OsString::from("..").into()
};
// Ownership of the entry is passed along
entries.push(dotdot_entry);

// Entry 3: an example of shared data.
// - name: "hello.txt"
// - entry is constructed in HelloFS::new()
// - Ownership of a smart reference is passed along.
entries.push(self.hello_entry.as_ref().clone());

// Slice the collected entries based on the requested offset.
let entries: Vec<Dirent> = entries.into_iter().skip(offset as usize).collect();
// ( Only references and smart pointers are being reorganized at this time;
// the underlying data should just stay where it is.)

// Entries may be returned as borrowed, owned, or shared.
// From<...> and Into<...> methods can be used to help construct the return type.
Ok(entries.into())
}
}

Expand Down Expand Up @@ -145,5 +225,86 @@ fn main() {
if matches.get_flag("allow-root") {
options.push(MountOption::AllowRoot);
}
fuser::mount2(HelloFS, mountpoint, &options).unwrap();
let hellofs = HelloFS::new();
fuser::mount2(hellofs, mountpoint, &options).unwrap();
}

#[cfg(test)]
mod test {
use fuser::{Filesystem, RequestMeta, Errno, FileType};
use std::ffi::OsStr;
use std::path::PathBuf;
use std::os::unix::ffi::OsStrExt;

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

#[test]
fn test_lookup_hello_txt() {
let mut hellofs = super::HelloFS::new();
let req = dummy_meta();
let result = hellofs.lookup(req, 1, &PathBuf::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::new();
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.as_ref()), "Hello World!\n", "Content of hello.txt should be 'Hello World!\\n'");
}
}

#[test]
fn test_readdir_root() {
let mut hellofs = super::HelloFS::new();
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_list) = result {
let entries_slice = entries_list.as_ref();
assert_eq!(entries_slice.len(), 3, "Root directory should contain exactly 3 entries");

// Check entry 0: "."
let entry0_data = &entries_slice[0];
assert_eq!(entry0_data.name.as_ref(), OsStr::new(".").as_bytes(), "First entry should be '.'");
assert_eq!(entry0_data.ino, 1, "Inode for '.' should be 1");
assert_eq!(entry0_data.offset, 1, "Offset for '.' should be 1");
assert_eq!(entry0_data.kind, FileType::Directory, "'.' should be a directory");

// Check entry 1: ".."
let entry1_data = &entries_slice[1];
assert_eq!(entry1_data.name.as_ref(), OsStr::new("..").as_bytes(), "Second entry should be '..'");
assert_eq!(entry1_data.ino, 1, "Inode for '..' should be 1");
assert_eq!(entry1_data.offset, 2, "Offset for '..' should be 2");
assert_eq!(entry1_data.kind, FileType::Directory, "'..' should be a directory");

// Check entry 2: "hello.txt"
let entry2_data = &entries_slice[2];
assert_eq!(entry2_data.name.as_ref(), OsStr::new("hello.txt").as_bytes(), "Third entry should be 'hello.txt'");
assert_eq!(entry2_data.ino, 2, "Inode for 'hello.txt' should be 2");
assert_eq!(entry2_data.offset, 3, "Offset for 'hello.txt' should be 3");
assert_eq!(entry2_data.kind, FileType::RegularFile, "'hello.txt' should be a regular file");
}
}

#[test]
fn test_create_fails_readonly() {
let mut hellofs = super::HelloFS::new();
let req = dummy_meta();
let result = hellofs.create(req, 1, &PathBuf::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");
}
}
}
Loading
Loading