From 10ab3bda79d5e66672e44d58085836aecaf20876 Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:32:07 +0200 Subject: [PATCH 1/6] Bump fuse-mt to 0.7.0, update fuser dependency to 0.14.0 and add hello example --- Cargo.toml | 9 +++++---- {example => examples/passthrough}/Cargo.toml | 4 ++-- {example => examples/passthrough}/README.md | 0 {example => examples/passthrough}/src/libc_extras.rs | 0 {example => examples/passthrough}/src/libc_wrappers.rs | 0 {example => examples/passthrough}/src/main.rs | 0 {example => examples/passthrough}/src/passthrough.rs | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) rename {example => examples/passthrough}/Cargo.toml (76%) rename {example => examples/passthrough}/README.md (100%) rename {example => examples/passthrough}/src/libc_extras.rs (100%) rename {example => examples/passthrough}/src/libc_wrappers.rs (100%) rename {example => examples/passthrough}/src/main.rs (100%) rename {example => examples/passthrough}/src/passthrough.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index a955cd4..966a876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fuse_mt" -version = "0.6.1" +version = "0.7.0" authors = ["William R. Fraser "] repository = "https://github.com/wfraser/fuse-mt" description = "A higher-level FUSE filesystem library with multi-threading and inode->path translation." @@ -8,13 +8,14 @@ categories = ["filesystem"] keywords = ["fuse", "filesystem"] license = "MIT/Apache-2.0" readme = "README.md" -edition = "2018" +edition = "2021" + [dependencies] -fuser = "0.13" +fuser = "0.14" libc = "0.2" log = "0.4" threadpool = "1.8" [workspace] -members = [".", "example"] +members = [".", "examples/hello", "examples/passthrough"] diff --git a/example/Cargo.toml b/examples/passthrough/Cargo.toml similarity index 76% rename from example/Cargo.toml rename to examples/passthrough/Cargo.toml index fe86e2a..639ed1b 100644 --- a/example/Cargo.toml +++ b/examples/passthrough/Cargo.toml @@ -3,9 +3,9 @@ name = "passthrufs" version = "0.1.0" authors = ["William R. Fraser "] edition = "2018" -workspace = ".." +workspace = "../.." [dependencies] libc = "0.2" log = "0.4" -fuse_mt = { path = ".." } +fuse_mt = { path = "../.." } diff --git a/example/README.md b/examples/passthrough/README.md similarity index 100% rename from example/README.md rename to examples/passthrough/README.md diff --git a/example/src/libc_extras.rs b/examples/passthrough/src/libc_extras.rs similarity index 100% rename from example/src/libc_extras.rs rename to examples/passthrough/src/libc_extras.rs diff --git a/example/src/libc_wrappers.rs b/examples/passthrough/src/libc_wrappers.rs similarity index 100% rename from example/src/libc_wrappers.rs rename to examples/passthrough/src/libc_wrappers.rs diff --git a/example/src/main.rs b/examples/passthrough/src/main.rs similarity index 100% rename from example/src/main.rs rename to examples/passthrough/src/main.rs diff --git a/example/src/passthrough.rs b/examples/passthrough/src/passthrough.rs similarity index 99% rename from example/src/passthrough.rs rename to examples/passthrough/src/passthrough.rs index a253605..21bc17d 100644 --- a/example/src/passthrough.rs +++ b/examples/passthrough/src/passthrough.rs @@ -122,7 +122,7 @@ impl PassthroughFS { const TTL: Duration = Duration::from_secs(1); -impl FilesystemMT for PassthroughFS { +impl FilesystemMT<'_> for PassthroughFS { fn init(&self, _req: RequestInfo) -> ResultEmpty { debug!("init"); Ok(()) From 698784bbec6577ee6387c16e23ba978c726fb954 Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:32:32 +0200 Subject: [PATCH 2/6] Bump add hello example --- examples/hello/Cargo.toml | 11 +++ examples/hello/README.md | 9 ++ examples/hello/src/main.rs | 196 +++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 examples/hello/Cargo.toml create mode 100644 examples/hello/README.md create mode 100644 examples/hello/src/main.rs diff --git a/examples/hello/Cargo.toml b/examples/hello/Cargo.toml new file mode 100644 index 0000000..60ff793 --- /dev/null +++ b/examples/hello/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "hello" +version = "0.1.0" +authors = ["not-jan"] +edition = "2021" +workspace = "../.." + +[dependencies] +libc = "0.2" +log = "0.4" +fuse_mt = { path = "../.." } diff --git a/examples/hello/README.md b/examples/hello/README.md new file mode 100644 index 0000000..d8de5fb --- /dev/null +++ b/examples/hello/README.md @@ -0,0 +1,9 @@ +A simple test filesystem showcasing the unmanaged capabilities of fuse-mt. + +It implements a filesystem that serves a single file: `hello.txt`. + +Just enough fuse operations are implemented to interact with the file system using `ls`, `cat` and the likes. + +To use it and test fuse_mt, run: + + cargo run \ No newline at end of file diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs new file mode 100644 index 0000000..58cf0ce --- /dev/null +++ b/examples/hello/src/main.rs @@ -0,0 +1,196 @@ +use std::env; +use std::ffi::{OsStr, OsString}; +use std::time::{Duration, SystemTime}; + +use fuse_mt::{CallbackResult, DirectoryEntry, FileAttr, FilesystemMT, FileType, Inode, RawFileAttr, RawFilesystemMT, RequestInfo, ResultEmpty, ResultEntry, ResultInode, ResultOpen, ResultReaddir, ResultSlice, ResultStatfs, Statfs}; + +#[derive(Debug)] +struct HelloFS { + ttl: Duration, + bootup: SystemTime +} + +impl HelloFS { + const ROOT_INODE: Inode = 1; + const HELLO_INODE: Inode = 2; + const HELLO_NAME: &'static str = "hello.txt"; + const HELLO_CONTENT: &'static [u8] = b"Hello World, this is fuse-mt!\x0a"; + + fn root_attr(&self, uid: u32, gid: u32) -> FileAttr { + FileAttr { + size: 0, + blocks: 0, + atime: self.bootup, + mtime: self.bootup, + ctime: self.bootup, + crtime: self.bootup, + kind: FileType::Directory, + perm: 0o755, + nlink: 2, + uid, + gid, + rdev: 0, + flags: 0, + } + } + + fn hello_attr(&self, uid: u32, gid: u32) -> FileAttr { + FileAttr { + size: Self::HELLO_CONTENT.len() as u64, + blocks: 1, + atime: self.bootup, + mtime: self.bootup, + ctime: self.bootup, + crtime: self.bootup, + kind: FileType::RegularFile, + perm: 0o755, + nlink: 1, + uid, + gid, + rdev: 0, + flags: 0, + } + } +} + +impl FilesystemMT<'_, Inode, RawFileAttr> for HelloFS { + fn getattr(&self, req: RequestInfo, path: Inode, _fh: Option) -> ResultEntry { + match path { + Self::ROOT_INODE => Ok((self.ttl, self.root_attr(req.uid, req.gid).as_raw(Self::ROOT_INODE, 0))), + Self::HELLO_INODE => Ok((self.ttl, self.hello_attr(req.uid, req.gid).as_raw(Self::HELLO_INODE, 0))), + _ => Err(libc::ENOENT) + } + } + + fn open(&self, _req: RequestInfo, path: Inode, _flags: u32) -> ResultOpen { + match path { + Self::HELLO_INODE => Ok((2, 0)), + _ => Err(libc::ENOENT) + } + } + + fn read(&self, _req: RequestInfo, path: Inode, _fh: u64, offset: u64, size: u32, callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult) -> CallbackResult { + let result = if path == Self::HELLO_INODE { + let start = offset as usize; + let end = start + size as usize; + + Ok(&Self::HELLO_CONTENT[start..end]) + } else { + Err(libc::ENOENT) + }; + + callback(result) + } + + fn opendir(&self, _req: RequestInfo, path: Inode, _flags: u32) -> ResultOpen { + match path { + Self::ROOT_INODE => Ok((1, 0)), + _ => Err(libc::ENOTDIR) + } + } + + fn readdir(&self, _req: RequestInfo, path: Inode, _fh: u64) -> ResultReaddir { + if path != 1 { + return Err(libc::ENOENT); + } + + Ok(vec![ + DirectoryEntry { + name: ".".into(), + kind: FileType::Directory, + }, + DirectoryEntry { + name: "..".into(), + kind: FileType::Directory, + }, + DirectoryEntry { + name: "hello.txt".into(), + kind: FileType::Directory, + } + ]) + } + + fn releasedir(&self, _req: RequestInfo, _path: Inode, _fh: u64, _flags: u32) -> ResultEmpty { + Ok(()) + } + + fn statfs(&self, _req: RequestInfo, path: Inode) -> ResultStatfs { + if path != Self::ROOT_INODE { + return Err(libc::ENOENT); + } + + Ok(Statfs { + blocks: 1, + bfree: 0, + bavail: 0, + files: 2, + ffree: 0, + bsize: 512, + namelen: 512, + frsize: 512, + }) + } +} + +impl RawFilesystemMT for HelloFS { + fn lookup(&self, req: RequestInfo, parent: Inode, name: &OsStr) -> ResultEntry { + if parent != Self::ROOT_INODE { + return Err(libc::ENOENT); + } + + if let Some(Self::HELLO_NAME) = name.to_str() { + let attr: RawFileAttr = self.hello_attr(req.uid, req.gid).as_raw(Self::HELLO_INODE, 0); + + Ok((self.ttl, attr)) + } else { + Err(libc::ENOENT) + } + } + + fn forget(&self, _req: RequestInfo, _path: Inode, _nlookup: u64) { + } + + fn parent(&self, _req: RequestInfo, path: Inode) -> ResultInode { + match path { + Self::ROOT_INODE | Self::HELLO_INODE => Ok(Self::ROOT_INODE), + _ => Err(libc::ENOENT) + } + } +} + +struct ConsoleLogger; + +impl log::Log for ConsoleLogger { + fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool { + true + } + + fn log(&self, record: &log::Record<'_>) { + println!("{}: {}: {}", record.target(), record.level(), record.args()); + } + + fn flush(&self) {} +} + +static LOGGER: ConsoleLogger = ConsoleLogger; + +fn main() { + log::set_logger(&LOGGER).unwrap(); + log::set_max_level(log::LevelFilter::Debug); + + let args: Vec = env::args_os().collect(); + + if args.len() != 2 { + println!("usage: {} ", &env::args().next().unwrap()); + std::process::exit(-1); + } + + let filesystem = HelloFS { + ttl: Duration::from_secs(1), + bootup: SystemTime::now() + }; + + let fuse_args = [OsStr::new("-o"), OsStr::new("fsname=hellofs"), OsStr::new("-o"), OsStr::new("ro")]; + + fuse_mt::mount(fuse_mt::RawFuseMT::new(filesystem, 1), &args[1], &fuse_args[..]).unwrap(); +} From 46f1a483131927ef36153090d0c3b56143c8b3f1 Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:33:07 +0200 Subject: [PATCH 3/6] Add ability to use an unmanaged version of fuse-mt --- src/fusemt.rs | 734 +++++++++++++++++++++++++++++++++++++++++++++++++- src/types.rs | 217 ++++++++++++--- 2 files changed, 912 insertions(+), 39 deletions(-) diff --git a/src/fusemt.rs b/src/fusemt.rs index 3a08d97..fbcfaaa 100644 --- a/src/fusemt.rs +++ b/src/fusemt.rs @@ -64,6 +64,35 @@ impl TimeOrNowExt for TimeOrNow { } } +impl RawFuseMT { + pub fn new(target_fs: T, num_threads: usize) -> RawFuseMT { + RawFuseMT { + target: Arc::new(target_fs), + threads: None, + num_threads, + } + } + + fn threadpool_run(&mut self, f: F) { + if self.num_threads == 0 { + f() + } else { + if self.threads.is_none() { + debug!("initializing threadpool with {} threads", self.num_threads); + self.threads = Some(ThreadPool::new(self.num_threads)); + } + self.threads.as_ref().unwrap().execute(f); + } + } +} + +#[derive(Debug)] +pub struct RawFuseMT { + target: Arc, + threads: Option, + num_threads: usize, +} + #[derive(Debug)] pub struct FuseMT { target: Arc, @@ -73,7 +102,7 @@ pub struct FuseMT { directory_cache: DirectoryCache, } -impl FuseMT { +impl FilesystemMT<'a, &'a Path> + Sync + Send + 'static> FuseMT { pub fn new(target_fs: T, num_threads: usize) -> FuseMT { FuseMT { target: Arc::new(target_fs), @@ -108,7 +137,708 @@ macro_rules! get_path { } } -impl fuser::Filesystem for FuseMT { +impl fuser::Filesystem for RawFuseMT { + fn init( + &mut self, + req: &fuser::Request<'_>, + _config: &mut fuser::KernelConfig, // TODO + ) -> Result<(), libc::c_int> { + debug!("init"); + self.target.init(req.info()) + } + + fn destroy(&mut self) { + debug!("destroy"); + self.target.destroy(); + } + + fn lookup( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + reply: fuser::ReplyEntry, + ) { + + debug!("lookup: {:?}, {:?}", parent, name); + + match self.target.lookup(req.info(), parent, name) { + Ok((ttl, attr)) => { + reply.entry(&ttl, &fuse_fileattr(attr.into(), attr.inode), attr.generation); + }, + Err(e) => reply.error(e), + } + } + + fn forget( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + nlookup: u64, + ) { + debug!("forget: {:?}, {:?}", ino, nlookup); + self.target.forget(_req.info(), ino, nlookup); + } + + fn getattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + reply: fuser::ReplyAttr, + ) { + debug!("getattr: {:?}", ino); + match self.target.getattr(req.info(), ino, None) { + Ok((ttl, attr)) => { + reply.attr(&ttl, &fuse_fileattr(attr.into(), ino)) + }, + Err(e) => reply.error(e), + } + } + fn setattr( + &mut self, + req: &fuser::Request<'_>, // passed to all + ino: u64, // translated to path; passed to all + mode: Option, // chmod + uid: Option, // chown + gid: Option, // chown + size: Option, // truncate + atime: Option, // utimens + mtime: Option, // utimens + _ctime: Option, // ? TODO + fh: Option, // passed to all + crtime: Option, // utimens_osx (OS X only) + chgtime: Option, // utimens_osx (OS X only) + bkuptime: Option, // utimens_osx (OS X only) + flags: Option, // utimens_osx (OS X only) + reply: fuser::ReplyAttr, + ) { + + debug!("setattr: {:?}", ino); + + debug!("\tino:\t{:?}", ino); + debug!("\tmode:\t{:?}", mode); + debug!("\tuid:\t{:?}", uid); + debug!("\tgid:\t{:?}", gid); + debug!("\tsize:\t{:?}", size); + debug!("\tatime:\t{:?}", atime); + debug!("\tmtime:\t{:?}", mtime); + debug!("\tfh:\t{:?}", fh); + + // TODO: figure out what C FUSE does when only some of these are implemented. + + if let Some(mode) = mode { + if let Err(e) = self.target.chmod(req.info(), ino, fh, mode) { + reply.error(e); + return; + } + } + + if uid.is_some() || gid.is_some() { + if let Err(e) = self.target.chown(req.info(), ino, fh, uid, gid) { + reply.error(e); + return; + } + } + + if let Some(size) = size { + if let Err(e) = self.target.truncate(req.info(), ino, fh, size) { + reply.error(e); + return; + } + } + + if atime.is_some() || mtime.is_some() { + let atime = atime.map(TimeOrNowExt::time); + let mtime = mtime.map(TimeOrNowExt::time); + if let Err(e) = self.target.utimens(req.info(), ino, fh, atime, mtime) { + reply.error(e); + return; + } + } + + if crtime.is_some() || chgtime.is_some() || bkuptime.is_some() || flags.is_some() { + if let Err(e) = self.target.utimens_macos(req.info(), ino, fh, crtime, chgtime, bkuptime, flags) { + reply.error(e); + return + } + } + + match self.target.getattr(req.info(), ino, fh) { + Ok((ttl, attr)) => reply.attr(&ttl, &fuse_fileattr(attr.into(), ino)), + Err(e) => reply.error(e), + } + } + + + fn readlink( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + reply: fuser::ReplyData, + ) { + debug!("readlink: {:?}", ino); + match self.target.readlink(req.info(), ino) { + Ok(data) => reply.data(&data), + Err(e) => reply.error(e), + } + } + + fn mknod( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + _umask: u32, // TODO + rdev: u32, + reply: fuser::ReplyEntry, + ) { + debug!("mknod: {:?}/{:?}", parent, name); + match self.target.mknod(req.info(), parent, name, mode, rdev) { + Ok((ttl, attr)) => { + reply.entry(&ttl, &fuse_fileattr(attr.into(), attr.inode), attr.generation) + }, + Err(e) => reply.error(e), + } + } + + fn mkdir( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + _umask: u32, // TODO + reply: fuser::ReplyEntry, + ) { + debug!("mkdir: {:?}/{:?}", parent, name); + match self.target.mkdir(req.info(), parent, name, mode) { + Ok((ttl, attr)) => { + reply.entry(&ttl, &fuse_fileattr(attr.into(), attr.inode), attr.generation) + }, + Err(e) => reply.error(e), + } + } + + fn unlink( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + reply: fuser::ReplyEmpty, + ) { + debug!("unlink: {:?}/{:?}", parent, name); + match self.target.unlink(req.info(), parent, name) { + Ok(()) => { + reply.ok() + }, + Err(e) => reply.error(e), + } + } + + fn rmdir( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + reply: fuser::ReplyEmpty, + ) { + debug!("rmdir: {:?}/{:?}", parent, name); + match self.target.rmdir(req.info(), parent, name) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn symlink( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + link: &Path, + reply: fuser::ReplyEntry, + ) { + debug!("symlink: {:?}/{:?} -> {:?}", parent, name, link); + match self.target.symlink(req.info(), parent, name, link) { + Ok((ttl, attr)) => { + reply.entry(&ttl, &fuse_fileattr(attr.into(), attr.inode), attr.generation) + }, + Err(e) => reply.error(e), + } + } + + + fn rename( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + newparent: u64, + newname: &OsStr, + _flags: u32, // TODO + reply: fuser::ReplyEmpty, + ) { + + debug!("rename: {:?}/{:?} -> {:?}/{:?}", parent, name, newparent, newname); + match self.target.rename(req.info(), parent, name, newparent, newname) { + Ok(()) => { + reply.ok() + }, + Err(e) => reply.error(e), + } + } + + fn link( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + newparent: u64, + newname: &OsStr, + reply: fuser::ReplyEntry, + ) { + debug!("link: {:?} -> {:?}/{:?}", ino, newparent, newname); + match self.target.link(req.info(), ino, newparent, newname) { + Ok((ttl, attr)) => { + reply.entry(&ttl, &fuse_fileattr(attr.into(), attr.inode), attr.generation); + }, + Err(e) => reply.error(e), + } + } + + fn open( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + flags: i32, + reply: fuser::ReplyOpen, + ) { + debug!("open: {:?}", ino); + match self.target.open(req.info(), ino, flags as u32) { // TODO: change flags to i32 + Ok((fh, flags)) => reply.opened(fh, flags), + Err(e) => reply.error(e), + } + } + + + + fn read( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + _flags: i32, // TODO + _lock_owner: Option, // TODO + reply: fuser::ReplyData, + ) { + + debug!("read: {:?} {:#x} @ {:#x}", ino, size, offset); + if offset < 0 { + error!("read called with a negative offset"); + reply.error(libc::EINVAL); + return; + } + let target = self.target.clone(); + let req_info = req.info(); + self.threadpool_run(move || { + target.read(req_info, ino, fh, offset as u64, size, |result| { + match result { + Ok(data) => reply.data(data), + Err(e) => reply.error(e), + } + CallbackResult { + _private: std::marker::PhantomData {}, + } + }); + }); + } + + fn write( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + data: &[u8], + _write_flags: u32, // TODO + flags: i32, + _lock_owner: Option, // TODO + reply: fuser::ReplyWrite, + ) { + + debug!("write: {:?} {:#x} @ {:#x}", ino, data.len(), offset); + if offset < 0 { + error!("write called with a negative offset"); + reply.error(libc::EINVAL); + return; + } + let target = self.target.clone(); + let req_info = req.info(); + + // The data needs to be copied here before dispatching to the threadpool because it's a + // slice of a single buffer that `fuser` re-uses for the entire session. + let data_buf = Vec::from(data); + + self.threadpool_run(move|| { + match target.write(req_info, ino, fh, offset as u64, data_buf, flags as u32) { + Ok(written) => reply.written(written), + Err(e) => reply.error(e), + } + }); + } + + + + fn flush( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + lock_owner: u64, + reply: fuser::ReplyEmpty, + ) { + debug!("flush: {:?}", ino); + let target = self.target.clone(); + let req_info = req.info(); + self.threadpool_run(move|| { + match target.flush(req_info, ino, fh, lock_owner) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + }); + } + + fn release( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + flags: i32, + lock_owner: Option, + flush: bool, + reply: fuser::ReplyEmpty, + ) { + debug!("release: {:?}", ino); + match self.target.release( + req.info(), ino, fh, flags as u32, lock_owner.unwrap_or(0) /* TODO */, flush) + { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn fsync( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + datasync: bool, + reply: fuser::ReplyEmpty, + ) { + debug!("fsync: {:?}", ino); + let target = self.target.clone(); + let req_info = req.info(); + self.threadpool_run(move|| { + match target.fsync(req_info, ino, fh, datasync) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + }); + } + + fn opendir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + flags: i32, + reply: fuser::ReplyOpen, + ) { + debug!("opendir: {:?}", ino); + match self.target.opendir(req.info(), ino, flags as u32) { + Ok((fh, flags)) => { + reply.opened(fh, flags); + }, + Err(e) => reply.error(e), + } + } + + fn readdir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + mut reply: fuser::ReplyDirectory, + ) { + + debug!("readdir: {:?} @ {}", ino, offset); + + if offset < 0 { + error!("readdir called with a negative offset"); + reply.error(libc::EINVAL); + return; + } + + // TODO: We're relying on the implementation caching here + let entries = match self.target.readdir(req.info(), ino, fh) { + Ok(entries) => { + entries + }, + Err(e) => { + reply.error(e); + return; + } + }; + + let parent_inode = match self.target.parent(req.info(), ino) { + Ok(inode) => inode, + Err(errno) => { + error!("readdir: unable to get inode for parent of {:?}", ino); + reply.error(errno); + return; + } + }; + + debug!("directory has {} entries", entries.len()); + + for (index, entry) in entries.iter().skip(offset as usize).enumerate() { + let entry_inode = if entry.name == Path::new(".") { + ino + } else if entry.name == Path::new("..") { + parent_inode + } else { + // Don't bother looking in the inode table for the entry; FUSE doesn't pre- + // populate its inode cache with this value, so subsequent access to these + // files is going to involve it issuing a LOOKUP operation anyway. + !1 + }; + + debug!("readdir: adding entry #{}, {:?}", offset + index as i64, entry.name); + + let buffer_full: bool = reply.add( + entry_inode, + offset + index as i64 + 1, + entry.kind, + entry.name.as_os_str()); + + if buffer_full { + debug!("readdir: reply buffer is full"); + break; + } + } + + reply.ok(); + } + + fn releasedir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + flags: i32, + reply: fuser::ReplyEmpty, + ) { + + debug!("releasedir: {:?}", ino); + + match self.target.releasedir(req.info(), ino, fh, flags as u32) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn fsyncdir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + datasync: bool, + reply: fuser::ReplyEmpty, + ) { + + debug!("fsyncdir: {:?} (datasync: {:?})", ino, datasync); + match self.target.fsyncdir(req.info(), ino, fh, datasync) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn statfs( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + reply: fuser::ReplyStatfs, + ) { + debug!("statfs: {:?}", ino); + match self.target.statfs(req.info(), ino) { + Ok(statfs) => reply.statfs( + statfs.blocks, + statfs.bfree, + statfs.bavail, + statfs.files, + statfs.ffree, + statfs.bsize, + statfs.namelen, + statfs.frsize), + Err(e) => reply.error(e), + } + } + + fn setxattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + name: &OsStr, + value: &[u8], + flags: i32, + position: u32, + reply: fuser::ReplyEmpty, + ) { + debug!("setxattr: {:?} {:?} ({} bytes, flags={:#x}, pos={:#x}", + ino, name, value.len(), flags, position); + match self.target.setxattr(req.info(), ino, name, value, flags as u32, position) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn getxattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + name: &OsStr, + size: u32, + reply: fuser::ReplyXattr, + ) { + debug!("getxattr: {:?} {:?}", ino, name); + match self.target.getxattr(req.info(), ino, name, size) { + Ok(Xattr::Size(size)) => { + debug!("getxattr: sending size {}", size); + reply.size(size) + }, + Ok(Xattr::Data(vec)) => { + debug!("getxattr: sending {} bytes", vec.len()); + reply.data(&vec) + }, + Err(e) => { + debug!("getxattr: error {}", e); + reply.error(e) + }, + } + } + + fn listxattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + size: u32, + reply: fuser::ReplyXattr, + ) { + debug!("listxattr: {:?}", ino); + match self.target.listxattr(req.info(), ino, size) { + Ok(Xattr::Size(size)) => { + debug!("listxattr: sending size {}", size); + reply.size(size) + }, + Ok(Xattr::Data(vec)) => { + debug!("listxattr: sending {} bytes", vec.len()); + reply.data(&vec) + } + Err(e) => reply.error(e), + } + } + + fn removexattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + name: &OsStr, + reply: fuser::ReplyEmpty, + ) { + debug!("removexattr: {:?}, {:?}", ino, name); + match self.target.removexattr(req.info(), ino, name) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn access( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + mask: i32, + reply: fuser::ReplyEmpty, + ) { + debug!("access: {:?}, mask={:#o}", ino, mask); + match self.target.access(req.info(), ino, mask as u32) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + fn create( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + _umask: u32, // TODO + flags: i32, + reply: fuser::ReplyCreate, + ) { + debug!("create: {:?}/{:?} (mode={:#o}, flags={:#x})", parent, name, mode, flags); + match self.target.create(req.info(), parent, name, mode, flags as u32) { + Ok(create) => { + let attr = fuse_fileattr(create.attr.into(), create.attr.inode); + reply.created(&create.ttl, &attr, create.attr.generation, create.fh, create.flags); + }, + Err(e) => reply.error(e), + } + } + + + // getlk + + // setlk + + // bmap + + #[cfg(target_os = "macos")] + fn setvolname( + &mut self, + req: &fuser::Request<'_>, + name: &OsStr, + reply: fuser::ReplyEmpty, + ) { + debug!("setvolname: {:?}", name); + match self.target.setvolname(req.info(), name) { + Ok(()) => reply.ok(), + Err(e) => reply.error(e), + } + } + + // exchange (macOS only, undocumented) + + #[cfg(target_os = "macos")] + fn getxtimes( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + reply: fuser::ReplyXTimes, + ) { + debug!("getxtimes: {:?}", ino); + match self.target.getxtimes(req.info(), ino) { + Ok(xtimes) => { + reply.xtimes(xtimes.bkuptime, xtimes.crtime); + } + Err(e) => reply.error(e), + } + } +} + + +impl FilesystemMT<'a, &'a Path> + Sync + Send + 'static> fuser::Filesystem for FuseMT { fn init( &mut self, req: &fuser::Request<'_>, diff --git a/src/types.rs b/src/types.rs index f93174a..2a7c271 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,8 +4,10 @@ // use std::ffi::{OsStr, OsString}; +use std::ops::{Deref, DerefMut}; use std::path::Path; use std::time::{Duration, SystemTime}; +pub use crate::inode_table::Inode; /// Info about a request. #[derive(Clone, Copy, Debug)] @@ -81,12 +83,55 @@ pub struct FileAttr { pub flags: u32, } +/// File attributes with inode and generation +/// This implements DerefMut to not break the API +#[derive(Clone, Copy, Debug)] +pub struct RawFileAttr { + /// inode + pub inode: libc::ino_t, + pub generation: u64, + pub attr: FileAttr +} + +impl Deref for RawFileAttr { + type Target = FileAttr; + + fn deref(&self) -> &Self::Target { + &self.attr + } +} + +// TODO: Is this a good choice for the API? +impl DerefMut for RawFileAttr { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.attr + } +} + +impl From for FileAttr { + fn from(value: RawFileAttr) -> Self { + value.attr + } +} + +impl FileAttr { + /// Convert this `FileAttr` instance to an instance of `RawFileAttr` + /// by adding an inode and its generation + pub fn as_raw(self, inode: Inode, generation: u64) -> RawFileAttr { + RawFileAttr { + inode, + generation, + attr: self + } + } +} + /// The return value for `create`: contains info on the newly-created file, as well as a handle to /// the opened file. #[derive(Clone, Debug)] -pub struct CreatedEntry { +pub struct CreatedEntry where Attr: Copy + Clone { pub ttl: Duration, - pub attr: FileAttr, + pub attr: Attr, pub fh: u64, pub flags: u32, } @@ -106,16 +151,19 @@ pub struct XTimes { pub crtime: SystemTime, } + + pub type ResultEmpty = Result<(), libc::c_int>; -pub type ResultEntry = Result<(Duration, FileAttr), libc::c_int>; +pub type ResultEntry = Result<(Duration, Attr), libc::c_int>; pub type ResultOpen = Result<(u64, u32), libc::c_int>; pub type ResultReaddir = Result, libc::c_int>; pub type ResultData = Result, libc::c_int>; pub type ResultSlice<'a> = Result<&'a [u8], libc::c_int>; pub type ResultWrite = Result; pub type ResultStatfs = Result; -pub type ResultCreate = Result; +pub type ResultCreate = Result, libc::c_int>; pub type ResultXattr = Result; +pub type ResultInode = Result; #[cfg(target_os = "macos")] pub type ResultXTimes = Result; @@ -131,7 +179,7 @@ pub struct CallbackResult { } /// This trait must be implemented to implement a filesystem with FuseMT. -pub trait FilesystemMT { +pub trait FilesystemMT<'a, T = &'a Path, Attr = FileAttr> where Attr: Copy + Clone { /// Called on mount, before any other function. fn init(&self, _req: RequestInfo) -> ResultEmpty { Ok(()) @@ -145,7 +193,7 @@ pub trait FilesystemMT { /// Get the attributes of a filesystem entry. /// /// * `fh`: a file handle if this is called on an open file. - fn getattr(&self, _req: RequestInfo, _path: &Path, _fh: Option) -> ResultEntry { + fn getattr(&self, _req: RequestInfo, _path: T, _fh: Option) -> ResultEntry { Err(libc::ENOSYS) } @@ -156,7 +204,7 @@ pub trait FilesystemMT { /// /// * `fh`: a file handle if this is called on an open file. /// * `mode`: the mode to change the file to. - fn chmod(&self, _req: RequestInfo, _path: &Path, _fh: Option, _mode: u32) -> ResultEmpty { + fn chmod(&self, _req: RequestInfo, _path: T, _fh: Option, _mode: u32) -> ResultEmpty { Err(libc::ENOSYS) } @@ -165,7 +213,7 @@ pub trait FilesystemMT { /// * `fh`: a file handle if this is called on an open file. /// * `uid`: user ID to change the file's owner to. If `None`, leave the UID unchanged. /// * `gid`: group ID to change the file's group to. If `None`, leave the GID unchanged. - fn chown(&self, _req: RequestInfo, _path: &Path, _fh: Option, _uid: Option, _gid: Option) -> ResultEmpty { + fn chown(&self, _req: RequestInfo, _path: T, _fh: Option, _uid: Option, _gid: Option) -> ResultEmpty { Err(libc::ENOSYS) } @@ -173,7 +221,7 @@ pub trait FilesystemMT { /// /// * `fh`: a file handle if this is called on an open file. /// * `size`: size in bytes to set as the file's length. - fn truncate(&self, _req: RequestInfo, _path: &Path, _fh: Option, _size: u64) -> ResultEmpty { + fn truncate(&self, _req: RequestInfo, _path: T, _fh: Option, _size: u64) -> ResultEmpty { Err(libc::ENOSYS) } @@ -182,20 +230,20 @@ pub trait FilesystemMT { /// * `fh`: a file handle if this is called on an open file. /// * `atime`: the time of last access. /// * `mtime`: the time of last modification. - fn utimens(&self, _req: RequestInfo, _path: &Path, _fh: Option, _atime: Option, _mtime: Option) -> ResultEmpty { + fn utimens(&self, _req: RequestInfo, _path: T, _fh: Option, _atime: Option, _mtime: Option) -> ResultEmpty { Err(libc::ENOSYS) } /// Set timestamps of a filesystem entry (with extra options only used on MacOS). #[allow(clippy::too_many_arguments)] - fn utimens_macos(&self, _req: RequestInfo, _path: &Path, _fh: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option) -> ResultEmpty { + fn utimens_macos(&self, _req: RequestInfo, _path: T, _fh: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option) -> ResultEmpty { Err(libc::ENOSYS) } // END OF SETATTR FUNCTIONS /// Read a symbolic link. - fn readlink(&self, _req: RequestInfo, _path: &Path) -> ResultData { + fn readlink(&self, _req: RequestInfo, _path: T) -> ResultData { Err(libc::ENOSYS) } @@ -205,7 +253,7 @@ pub trait FilesystemMT { /// * `name`: name of the entry. /// * `mode`: mode for the new entry. /// * `rdev`: if mode has the bits `S_IFCHR` or `S_IFBLK` set, this is the major and minor numbers for the device file. Otherwise it should be ignored. - fn mknod(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _mode: u32, _rdev: u32) -> ResultEntry { + fn mknod(&self, _req: RequestInfo, _parent: T, _name: &OsStr, _mode: u32, _rdev: u32) -> ResultEntry { Err(libc::ENOSYS) } @@ -214,7 +262,7 @@ pub trait FilesystemMT { /// * `parent`: path to the directory to make the directory under. /// * `name`: name of the directory. /// * `mode`: permissions for the new directory. - fn mkdir(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _mode: u32) -> ResultEntry { + fn mkdir(&self, _req: RequestInfo, _parent: T, _name: &OsStr, _mode: u32) -> ResultEntry { Err(libc::ENOSYS) } @@ -222,7 +270,7 @@ pub trait FilesystemMT { /// /// * `parent`: path to the directory containing the file to delete. /// * `name`: name of the file to delete. - fn unlink(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr) -> ResultEmpty { + fn unlink(&self, _req: RequestInfo, _parent: T, _name: &OsStr) -> ResultEmpty { Err(libc::ENOSYS) } @@ -230,7 +278,7 @@ pub trait FilesystemMT { /// /// * `parent`: path to the directory containing the directory to delete. /// * `name`: name of the directory to delete. - fn rmdir(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr) -> ResultEmpty { + fn rmdir(&self, _req: RequestInfo, _parent: T, _name: &OsStr) -> ResultEmpty { Err(libc::ENOSYS) } @@ -239,7 +287,7 @@ pub trait FilesystemMT { /// * `parent`: path to the directory to make the link in. /// * `name`: name of the symbolic link. /// * `target`: path (may be relative or absolute) to the target of the link. - fn symlink(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _target: &Path) -> ResultEntry { + fn symlink(&self, _req: RequestInfo, _parent: T, _name: &OsStr, _target: &Path) -> ResultEntry { Err(libc::ENOSYS) } @@ -249,7 +297,7 @@ pub trait FilesystemMT { /// * `name`: name of the existing entry. /// * `newparent`: path to the directory it should be renamed into (may be the same as `parent`). /// * `newname`: name of the new entry. - fn rename(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _newparent: &Path, _newname: &OsStr) -> ResultEmpty { + fn rename(&self, _req: RequestInfo, _parent: T, _name: &OsStr, _newparent: T, _newname: &OsStr) -> ResultEmpty { Err(libc::ENOSYS) } @@ -258,7 +306,7 @@ pub trait FilesystemMT { /// * `path`: path to an existing file. /// * `newparent`: path to the directory for the new link. /// * `newname`: name for the new link. - fn link(&self, _req: RequestInfo, _path: &Path, _newparent: &Path, _newname: &OsStr) -> ResultEntry { + fn link(&self, _req: RequestInfo, _path: T, _newparent: T, _newname: &OsStr) -> ResultEntry { Err(libc::ENOSYS) } @@ -270,7 +318,7 @@ pub trait FilesystemMT { /// Return a tuple of (file handle, flags). The file handle will be passed to any subsequent /// calls that operate on the file, and can be any value you choose, though it should allow /// your filesystem to identify the file opened even without any path info. - fn open(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen { + fn open(&self, _req: RequestInfo, _path: T, _flags: u32) -> ResultOpen { Err(libc::ENOSYS) } @@ -288,7 +336,7 @@ pub trait FilesystemMT { /// the result data as a slice, or an error code. /// /// Return the return value from the `callback` function. - fn read(&self, _req: RequestInfo, _path: &Path, _fh: u64, _offset: u64, _size: u32, callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult) -> CallbackResult { + fn read(&self, _req: RequestInfo, _path: T, _fh: u64, _offset: u64, _size: u32, callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult) -> CallbackResult { callback(Err(libc::ENOSYS)) } @@ -301,7 +349,7 @@ pub trait FilesystemMT { /// * `flags`: /// /// Return the number of bytes written. - fn write(&self, _req: RequestInfo, _path: &Path, _fh: u64, _offset: u64, _data: Vec, _flags: u32) -> ResultWrite { + fn write(&self, _req: RequestInfo, _path: T, _fh: u64, _offset: u64, _data: Vec, _flags: u32) -> ResultWrite { Err(libc::ENOSYS) } @@ -316,7 +364,7 @@ pub trait FilesystemMT { /// * `fh`: file handle returned from the `open` call. /// * `lock_owner`: if the filesystem supports locking (`setlk`, `getlk`), remove all locks /// belonging to this lock owner. - fn flush(&self, _req: RequestInfo, _path: &Path, _fh: u64, _lock_owner: u64) -> ResultEmpty { + fn flush(&self, _req: RequestInfo, _path: T, _fh: u64, _lock_owner: u64) -> ResultEmpty { Err(libc::ENOSYS) } @@ -331,7 +379,7 @@ pub trait FilesystemMT { /// * `lock_owner`: if the filesystem supports locking (`setlk`, `getlk`), remove all locks /// belonging to this lock owner. /// * `flush`: whether pending data must be flushed or not. - fn release(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32, _lock_owner: u64, _flush: bool) -> ResultEmpty { + fn release(&self, _req: RequestInfo, _path: T, _fh: u64, _flags: u32, _lock_owner: u64, _flush: bool) -> ResultEmpty { Err(libc::ENOSYS) } @@ -342,7 +390,7 @@ pub trait FilesystemMT { /// * `path`: path to the file. /// * `fh`: file handle returned from the `open` call. /// * `datasync`: if `false`, also write metadata, otherwise just write file data. - fn fsync(&self, _req: RequestInfo, _path: &Path, _fh: u64, _datasync: bool) -> ResultEmpty { + fn fsync(&self, _req: RequestInfo, _path: T, _fh: u64, _datasync: bool) -> ResultEmpty { Err(libc::ENOSYS) } @@ -356,7 +404,7 @@ pub trait FilesystemMT { /// Return a tuple of (file handle, flags). The file handle will be passed to any subsequent /// calls that operate on the directory, and can be any value you choose, though it should /// allow your filesystem to identify the directory opened even without any path info. - fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen { + fn opendir(&self, _req: RequestInfo, _path: T, _flags: u32) -> ResultOpen { Err(libc::ENOSYS) } @@ -366,7 +414,7 @@ pub trait FilesystemMT { /// * `fh`: file handle returned from the `opendir` call. /// /// Return all the entries of the directory. - fn readdir(&self, _req: RequestInfo, _path: &Path, _fh: u64) -> ResultReaddir { + fn readdir(&self, _req: RequestInfo, _path: T, _fh: u64) -> ResultReaddir { Err(libc::ENOSYS) } @@ -377,14 +425,14 @@ pub trait FilesystemMT { /// * `path`: path to the directory. /// * `fh`: file handle returned from the `opendir` call. /// * `flags`: the file access flags passed to the `opendir` call. - fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty { + fn releasedir(&self, _req: RequestInfo, _path: T, _fh: u64, _flags: u32) -> ResultEmpty { Err(libc::ENOSYS) } /// Write out any pending changes to a directory. /// /// Analogous to the `fsync` call. - fn fsyncdir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _datasync: bool) -> ResultEmpty { + fn fsyncdir(&self, _req: RequestInfo, _path: T, _fh: u64, _datasync: bool) -> ResultEmpty { Err(libc::ENOSYS) } @@ -393,7 +441,7 @@ pub trait FilesystemMT { /// * `path`: path to some folder in the filesystem. /// /// See the `Statfs` struct for more details. - fn statfs(&self, _req: RequestInfo, _path: &Path) -> ResultStatfs { + fn statfs(&self, _req: RequestInfo, _path: T) -> ResultStatfs { Err(libc::ENOSYS) } @@ -404,7 +452,7 @@ pub trait FilesystemMT { /// * `value`: the data to set the value to. /// * `flags`: can be either `XATTR_CREATE` or `XATTR_REPLACE`. /// * `position`: offset into the attribute value to write data. - fn setxattr(&self, _req: RequestInfo, _path: &Path, _name: &OsStr, _value: &[u8], _flags: u32, _position: u32) -> ResultEmpty { + fn setxattr(&self, _req: RequestInfo, _path: T, _name: &OsStr, _value: &[u8], _flags: u32, _position: u32) -> ResultEmpty { Err(libc::ENOSYS) } @@ -416,7 +464,7 @@ pub trait FilesystemMT { /// /// If `size` is 0, return `Xattr::Size(n)` where `n` is the size of the attribute data. /// Otherwise, return `Xattr::Data(data)` with the requested data. - fn getxattr(&self, _req: RequestInfo, _path: &Path, _name: &OsStr, _size: u32) -> ResultXattr { + fn getxattr(&self, _req: RequestInfo, _path: T, _name: &OsStr, _size: u32) -> ResultXattr { Err(libc::ENOSYS) } @@ -429,7 +477,7 @@ pub trait FilesystemMT { /// attribute names. /// Otherwise, return `Xattr::Data(data)` where `data` is all the null-terminated attribute /// names. - fn listxattr(&self, _req: RequestInfo, _path: &Path, _size: u32) -> ResultXattr { + fn listxattr(&self, _req: RequestInfo, _path: T, _size: u32) -> ResultXattr { Err(libc::ENOSYS) } @@ -437,7 +485,7 @@ pub trait FilesystemMT { /// /// * `path`: path to the file. /// * `name`: name of the attribute to remove. - fn removexattr(&self, _req: RequestInfo, _path: &Path, _name: &OsStr) -> ResultEmpty { + fn removexattr(&self, _req: RequestInfo, _path: T, _name: &OsStr) -> ResultEmpty { Err(libc::ENOSYS) } @@ -448,7 +496,7 @@ pub trait FilesystemMT { /// /// Return `Ok(())` if all requested permissions are allowed, otherwise return `Err(EACCES)` /// or other error code as appropriate (e.g. `ENOENT` if the file doesn't exist). - fn access(&self, _req: RequestInfo, _path: &Path, _mask: u32) -> ResultEmpty { + fn access(&self, _req: RequestInfo, _path: T, _mask: u32) -> ResultEmpty { Err(libc::ENOSYS) } @@ -461,7 +509,7 @@ pub trait FilesystemMT { /// /// Return a `CreatedEntry` (which contains the new file's attributes as well as a file handle /// -- see documentation on `open` for more info on that). - fn create(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _mode: u32, _flags: u32) -> ResultCreate { + fn create(&self, _req: RequestInfo, _parent: T, _name: &OsStr, _mode: u32, _flags: u32) -> ResultCreate { Err(libc::ENOSYS) } @@ -487,7 +535,102 @@ pub trait FilesystemMT { /// /// Return an `XTimes` struct with the times, or other error code as appropriate. #[cfg(target_os = "macos")] - fn getxtimes(&self, _req: RequestInfo, _path: &Path) -> ResultXTimes { + fn getxtimes(&self, _req: RequestInfo, _path: T) -> ResultXTimes { Err(libc::ENOSYS) } } + +/// Extension trait for `FilesystemMT` to allow for unmanaged file systems where fuse-mt +/// does not handle inode allocation and management. +/// +/// This trait provides methods for filesystem operations that are not automatically +/// managed by fuse-mt, such as inode lookup, forgetting inodes, and retrieving parent +/// inodes. Implementing this trait allows for more control over the filesystem's behavior +/// in scenarios where manual inode management is necessary. +pub trait RawFilesystemMT: for <'a> FilesystemMT<'a, Inode, RawFileAttr> { + /// Performs a lookup operation for a given file name within a parent inode. + /// + /// This method is used to find a file or directory by its name within a parent directory. + /// It returns the attributes of the found file or directory, or an error if the lookup fails. + /// + /// # Arguments + /// + /// * `_req` - The request information. + /// * `_parent` - The parent inode. + /// * `_name` - The name of the file or directory to look up. + /// + /// # Returns + /// + /// * `ResultEntry` - The result of the lookup operation, containing the + /// attributes of the found file or directory, or an error. + /// + /// # Examples + /// + /// ```ignore + /// // Assuming `fs` is an instance of a struct implementing `RawFilesystemMT` + /// let req = RequestInfo::new(); // Example request info + /// let parent_inode = Inode::new(1); // Example parent inode + /// let name = OsStr::new("example.txt"); // Example file name + /// + /// let result = fs.lookup(req, parent_inode, name); + /// match result { + /// Ok(entry) => println!("Found file with attributes: {:?}", entry.attrs), + /// Err(e) => println!("Lookup failed: {:?}", e), + /// } + /// ``` + fn lookup(&self, _req: RequestInfo, _parent: Inode, _name: &OsStr) -> ResultEntry; + + /// Forgets a previously looked-up inode. + /// + /// This method is used to inform the filesystem that a previously looked-up inode is no + /// longer needed. This can be used to free up resources associated with the inode. + /// + /// # Arguments + /// + /// * `_req` - The request information. + /// * `_path` - The inode to forget. + /// * `_nlookup` - The number of lookups to forget. + /// + /// # Examples + /// + /// ```ignore + /// // Assuming `fs` is an instance of a struct implementing `RawFilesystemMT` + /// let req = RequestInfo::new(); // Example request info + /// let inode_to_forget = Inode::new(1); // Example inode to forget + /// let nlookup = 1; // Number of lookups to forget + /// + /// fs.forget(req, inode_to_forget, nlookup); + /// ``` + fn forget(&self, _req: RequestInfo, _path: Inode, _nlookup: u64); + + /// Retrieves the parent inode of a given inode. + /// + /// This method is used to find the parent directory of a given inode. It returns the + /// parent inode, or an error if the operation fails. + /// + /// This is only ever really used for readdir() at the moment. + /// + /// # Arguments + /// + /// * `_req` - The request information. + /// * `_path` - The inode for which to find the parent. + /// + /// # Returns + /// + /// * `ResultInode` - The result of the operation, containing the parent inode, or an error. + /// + /// # Examples + /// + /// ```ignore + /// // Assuming `fs` is an instance of a struct implementing `RawFilesystemMT` + /// let req = RequestInfo::new(); // Example request info + /// let inode = Inode::new(1); // Example inode + /// + /// let result = fs.parent(req, inode); + /// match result { + /// Ok(parent_inode) => println!("Parent inode: {:?}", parent_inode), + /// Err(e) => println!("Failed to find parent: {:?}", e), + /// } + /// ``` + fn parent(&self, _req: RequestInfo, _path: Inode) -> ResultInode; +} \ No newline at end of file From 04e9947e20632cc3d4cc6deb9fe72e326ed99766 Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:33:26 +0200 Subject: [PATCH 4/6] Update .gitignore to ignore RustRover configuration --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a9d37c5..e51cf05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +.idea/ \ No newline at end of file From 977f2edc0a2bd34f56ddfa20693ff8b01cd44bfd Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:34:17 +0200 Subject: [PATCH 5/6] Update README to reflect new unmanaged capabilities --- README.md | 5 +++-- examples/passthrough/src/passthrough.rs | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1954be5..77c4763 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This code is a wrapper on top of the Rust FUSE crate with the following additions: * Dispatch system calls on multiple threads, so that e.g. I/O doesn't block directory listing. -* Translate inodes into paths, to simplify filesystem implementation. +* Optionally translate inodes into paths, to simplify filesystem implementation. The `fuser` crate provides a minimal, low-level access to the FUSE kernel API, whereas this crate is more high-level, like the FUSE C API. @@ -16,7 +16,8 @@ It includes a sample filesystem that uses the crate to pass all system calls thr This is a work-in-progress. Bug reports, pull requests, and other feedback are welcome! Some random notes on the implementation: -* The trait that filesystems will implement is called `FilesystemMT`, and instead of the FUSE crate's convention of having methods return void and including a "reply" parameter, the methods return their values. This feels more idiomatic to me. They also take `&Path` arguments instead of inode numbers. +* The trait that filesystems will implement is called `FilesystemMT<'_>`, and instead of the FUSE crate's convention of having methods return void and including a "reply" parameter, the methods return their values. This feels more idiomatic to me. They also take `&Path` arguments instead of inode numbers. +* If you do not wish fuse-mt to translate inode numbers into `&Path` but still retain the comfort of fuse-mt you may implement `FilesystemMT<'_, Inode, RawFileAttr>` and `RawFilesystemMT` instead. All methods will then receive `u64` as their input. * Currently, only the following calls are dispatched to other threads: * read * write diff --git a/examples/passthrough/src/passthrough.rs b/examples/passthrough/src/passthrough.rs index 21bc17d..d5645a4 100644 --- a/examples/passthrough/src/passthrough.rs +++ b/examples/passthrough/src/passthrough.rs @@ -250,7 +250,7 @@ impl FilesystemMT<'_> for PassthroughFS { error!("seek({:?}, {}): {}", path, offset, e); return callback(Err(e.raw_os_error().unwrap())); } - match file.read(unsafe { mem::transmute(data.spare_capacity_mut()) }) { + match file.read(unsafe { mem::transmute::<&mut [std::mem::MaybeUninit], &mut [u8]>(data.spare_capacity_mut()) }) { Ok(n) => { unsafe { data.set_len(n) }; }, Err(e) => { error!("read {:?}, {:#x} @ {:#x}: {}", path, size, offset, e); @@ -331,8 +331,8 @@ impl FilesystemMT<'_> for PassthroughFS { } fn chown(&self, _req: RequestInfo, path: &Path, fh: Option, uid: Option, gid: Option) -> ResultEmpty { - let uid = uid.unwrap_or(::std::u32::MAX); // docs say "-1", but uid_t is unsigned - let gid = gid.unwrap_or(::std::u32::MAX); // ditto for gid_t + let uid = uid.unwrap_or(u32::MAX); // docs say "-1", but uid_t is unsigned + let gid = gid.unwrap_or(u32::MAX); // ditto for gid_t debug!("chown: {:?} to {}:{}", path, uid, gid); let result = if let Some(fd) = fh { @@ -624,7 +624,7 @@ impl FilesystemMT<'_> for PassthroughFS { if size > 0 { let mut data = Vec::::with_capacity(size as usize); let nread = libc_wrappers::llistxattr( - real, unsafe { mem::transmute(data.spare_capacity_mut()) })?; + real, unsafe { mem::transmute::<&mut [std::mem::MaybeUninit], &mut [u8]>(data.spare_capacity_mut()) })?; unsafe { data.set_len(nread) }; Ok(Xattr::Data(data)) } else { @@ -641,7 +641,7 @@ impl FilesystemMT<'_> for PassthroughFS { if size > 0 { let mut data = Vec::::with_capacity(size as usize); let nread = libc_wrappers::lgetxattr( - real, name.to_owned(), unsafe { mem::transmute(data.spare_capacity_mut()) })?; + real, name.to_owned(), unsafe { mem::transmute::<&mut [std::mem::MaybeUninit], &mut [u8]>(data.spare_capacity_mut()) })?; unsafe { data.set_len(nread) }; Ok(Xattr::Data(data)) } else { From 48ca952e3aa06a04cec93ad26ce6116f323efd21 Mon Sep 17 00:00:00 2001 From: not-jan <61017633+not-jan@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:34:38 +0200 Subject: [PATCH 6/6] Update CI to also test the hello example --- .github/workflows/cargo-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cargo-test.yml b/.github/workflows/cargo-test.yml index 9fd674b..56879be 100644 --- a/.github/workflows/cargo-test.yml +++ b/.github/workflows/cargo-test.yml @@ -43,11 +43,11 @@ jobs: with: command: test - - name: Run clippy on example + - name: Run clippy on examples uses: actions-rs/cargo@v1 with: command: clippy - args: --manifest-path example/Cargo.toml --all-targets -- ${{matrix.deny_warnings}} + args: --package hello --package passthrufs --all-targets -- ${{matrix.deny_warnings}} - name: Run smoke test run: ./smoke_test.sh