From e49e2d94ce2d3b6835a29bb5bc59e422dbc58f63 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 7 May 2020 10:27:08 +0200 Subject: [PATCH] import Signed-off-by: Wolfgang Bumiller --- .cargo/config | 5 + .gitignore | 3 + Cargo.toml | 16 + examples/tmpfs/block_file.rs | 241 +++++++++ examples/tmpfs/fs.rs | 419 ++++++++++++++++ examples/tmpfs/macros.rs | 15 + examples/tmpfs/main.rs | 318 ++++++++++++ src/fuse_fd.rs | 66 +++ src/lib.rs | 13 + src/requests.rs | 912 +++++++++++++++++++++++++++++++++++ src/session.rs | 696 ++++++++++++++++++++++++++ src/sys.rs | 365 ++++++++++++++ src/util.rs | 46 ++ 13 files changed, 3115 insertions(+) create mode 100644 .cargo/config create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 examples/tmpfs/block_file.rs create mode 100644 examples/tmpfs/fs.rs create mode 100644 examples/tmpfs/macros.rs create mode 100644 examples/tmpfs/main.rs create mode 100644 src/fuse_fd.rs create mode 100644 src/lib.rs create mode 100644 src/requests.rs create mode 100644 src/session.rs create mode 100644 src/sys.rs create mode 100644 src/util.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..3b5b6e4 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,5 @@ +[source] +[source.debian-packages] +directory = "/usr/share/cargo/registry" +[source.crates-io] +replace-with = "debian-packages" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a569f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +test diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..45db99a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "proxmox-fuse" +version = "0.1.0" +authors = ["Wolfgang Bumiller "] +edition = "2018" +license = "AGPL-3" +description = "Expose fuse requests as async streams." + +exclude = [ "debian" ] + +[dependencies] +anyhow = "1.0" +futures = "0.3" +libc = "0.2" +mio = "0.6.21" +tokio = { version = "0.2", features = ["io-driver", "macros", "signal", "stream"] } diff --git a/examples/tmpfs/block_file.rs b/examples/tmpfs/block_file.rs new file mode 100644 index 0000000..7327983 --- /dev/null +++ b/examples/tmpfs/block_file.rs @@ -0,0 +1,241 @@ +//! Implements "block" based sparse vector. + +use std::io; + +pub struct Extent { + offset: u64, + data: Vec, +} + +enum SearchBias { + Low, + High, +} + +pub struct BlockFile { + extents: Vec, + block_size: usize, + block_mask: u64, + max_extent_size: usize, +} + +impl BlockFile { + /// Panics if `block_size` is not a power of two. + pub fn new(block_size: usize) -> Self { + if !block_size.is_power_of_two() { + panic!("block size must be a power of two"); + } + + let block_mask = ((block_size as u64) << 1) - 1; + let max_extent_size = 0x7FFF_FFFF & !(block_mask as usize); + + Self { + extents: Vec::new(), + block_size, + block_mask, + max_extent_size, + } + } + + pub fn size(&self) -> u64 { + self.extents + .last() + .map(|e| e.offset + e.data.len() as u64) + .unwrap_or(0) + } + + /// A binary search which allows choosing whether the lower or higher key should be returned when + /// there's no exact match. + /// + /// Returns -1 if `offset` is smaller than the first extent (eg when first writing to offset + /// 4096 and then to 0). + /// Returns `self.size()` if `offset` is past the currently written data. + /// Returns an index otherwise. + fn search_extent(&self, offset: u64, bias: SearchBias) -> Result { + let mut a = -1isize; + let mut b = self.extents.len() as isize; + + while (b - a) > 1 { + let i = a + (b - a) / 2; // since `(a + b)/2` might overflow... in theory + let entry_ofs = self.extents[i as usize].offset; + if offset < entry_ofs { + b = i; + } else if offset > entry_ofs { + a = i; + } else { + return Ok(i as usize); + } + } + + Err(match bias { + SearchBias::Low => a, + SearchBias::High => b, + }) + } + + fn get_read_extents(&self, offset: u64) -> &[Extent] { + match self.search_extent(offset, SearchBias::Low) { + Ok(index) => &self.extents[index..], + Err(beg) => { + assert!(beg >= -1); + let beg = beg.max(0) as usize; + assert!((beg as u64) <= self.size()); + &self.extents[beg..] + } + } + } + + pub fn read(&self, mut buf: &mut [u8], mut offset: u64) -> io::Result { + let end = offset + buf.len() as u64; + if end > self.size() { + let remaining = self.size() - offset; + buf = &mut buf[..(remaining as usize)] + } + let return_size = buf.len(); + + for extent in self.get_read_extents(offset) { + let data = if extent.offset <= offset { + // in the first one we may be starting in the middle: + let inside = (offset - extent.offset) as usize; + &extent.data[inside..] + } else { + let empty_len = extent.offset - offset; + if empty_len > (buf.len() as u64) { + break; + } + let (to_clear, remaining) = buf.split_at_mut(empty_len as usize); + unsafe { + std::ptr::write_bytes(to_clear.as_mut_ptr(), 0, to_clear.len()); + } + offset += to_clear.len() as u64; + buf = remaining; + &extent.data[..] + }; + + let data_len = data.len().min(buf.len()); + let (to_write, remaining) = buf.split_at_mut(data_len); + to_write.copy_from_slice(&data[..data_len]); + offset += to_write.len() as u64; + buf = remaining; + } + + // clear the remaining buffer with zeroes: + unsafe { + std::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len()); + } + + Ok(return_size) + } + + pub fn truncate(&mut self, size: u64) { + match self.search_extent(size, SearchBias::Low) { + Ok(index) => self.extents.truncate(index), + Err(-1) => self.extents.truncate(0), + Err(index) => { + let index = index as usize; + self.extents.truncate(index + 1); + let begin = self.extents[index].offset; + self.extents[index].data.truncate((size - begin) as usize); + } + } + } + + pub fn write(&mut self, buf: &[u8], offset: u64) -> io::Result<()> { + let block_mask_usize = self.block_mask as usize; + if (buf.len() + block_mask_usize) & !block_mask_usize > self.max_extent_size { + let mut offset = offset; + for chunk in buf.chunks(self.max_extent_size) { + self.write(chunk, offset)?; + offset += chunk.len() as u64; + } + return Ok(()); + } + + let index = match self.search_extent(offset, SearchBias::Low) { + Err(-1) => { + // This is the very first extent to be written. + let block_ofs = offset & !self.block_mask; + let data_end_ofs = offset + buf.len() as u64; + let extent_size = (data_end_ofs - block_ofs) as usize; + let mut data = Vec::with_capacity(extent_size); + let leading_zeros = (offset - block_ofs) as usize; + unsafe { + data.set_len(extent_size); + let to_zero = &mut data[..leading_zeros]; + std::ptr::write_bytes(to_zero.as_mut_ptr(), 0, to_zero.len()); + } + data[leading_zeros..].copy_from_slice(buf); + self.extents.push(Extent { + offset: block_ofs, + data, + }); + return Ok(()); + } + Ok(index) => index, + Err(index) => { + assert!(index >= 0); + index as usize + } + }; + + // We write in part to the extent at `index` and may want to merge with the extents + // following it. + + let (extent, further_extents) = self.extents[index..].split_first_mut().unwrap(); + + let block_mask_usize = self.block_mask as usize; + let in_ofs = (offset - extent.offset) as usize; + let in_end = in_ofs + buf.len(); + let in_block_end = (in_end + block_mask_usize) & !block_mask_usize; + if in_block_end > self.max_extent_size { + let possible = self.max_extent_size - in_ofs; + self.write(&buf[..possible], offset)?; + return self.write(&buf[possible..], offset + (possible as u64)); + } + + // at this point we know we will not exceed the maximum extent size: + + if extent.data.len() >= in_end { + // we're not resizing the extent, so just wite and leave + extent.data[in_ofs..in_end].copy_from_slice(buf); + return Ok(()); + } + + // we definitely need to resize: + let mut needed_end = in_end; + if !further_extents.is_empty() { + needed_end = (needed_end + block_mask_usize) & !block_mask_usize; + } + + extent.data.reserve(needed_end - extent.data.len()); + unsafe { + extent.data.set_len(needed_end); + let to_zero = &mut extent.data[in_end..]; + std::ptr::write_bytes(to_zero.as_mut_ptr(), 0, to_zero.len()); + } + + extent.data[in_ofs..in_end].copy_from_slice(buf); + + let cur_end = extent.offset + extent.data.len() as u64; + + // data has been written, now handle the trailing extents: + let next = match further_extents.first_mut() { + None => return Ok(()), + Some(next) => next, + }; + + if cur_end <= extent.offset { + return Ok(()); + } + + let over_offset = extent.offset + (in_end as u64); + let over_by = (over_offset - next.offset) as usize; + let to_move_end = (over_by + block_mask_usize) & !block_mask_usize; + let to_move_size = to_move_end - over_by; + let extent_data_len = extent.data.len(); + extent.data[(extent_data_len - to_move_size)..].copy_from_slice(&next.data[..to_move_size]); + next.offset += to_move_end as u64; + next.data = next.data.split_off(to_move_end); + Ok(()) + } +} diff --git a/examples/tmpfs/fs.rs b/examples/tmpfs/fs.rs new file mode 100644 index 0000000..411d7de --- /dev/null +++ b/examples/tmpfs/fs.rs @@ -0,0 +1,419 @@ +//! The tmpfs. + +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::ffi::{OsStr, OsString}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Mutex, RwLock}; +use std::time::Duration; +use std::{io, mem}; + +use anyhow::Error; + +use crate::block_file::BlockFile; + +fn now() -> Duration { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() +} + +fn new_stat(inode: u64, mode: libc::mode_t, uid: libc::uid_t, gid: libc::gid_t) -> libc::stat { + let mut stat: libc::stat = unsafe { mem::zeroed() }; + stat.st_ino = inode; + stat.st_mode = mode; + stat.st_uid = uid; + stat.st_gid = gid; + let now = now(); + stat.st_mtime = now.as_secs() as i64; + stat.st_mtime_nsec = i64::from(now.subsec_nanos()); + stat.st_atime = stat.st_mtime; + stat.st_atime_nsec = stat.st_mtime_nsec; + stat.st_ctime = stat.st_mtime; + stat.st_ctime_nsec = stat.st_mtime_nsec; + stat +} + +fn new_dir_stat(inode: u64) -> libc::stat { + new_stat(inode, 0o755 | libc::S_IFDIR, 0, 0) +} + +pub struct Fs { + entries: RwLock>>>, + free_inodes: Mutex>, +} + +impl Fs { + pub fn new() -> Self { + Self { + entries: RwLock::new(vec![ + None, + Some(Box::new(FsEntry { + inode: proxmox_fuse::ROOT_ID, + parent: proxmox_fuse::ROOT_ID, + lookups: AtomicUsize::new(1), + links: AtomicUsize::new(1), + stat: RwLock::new(new_dir_stat(1)), + content: FsContent::Dir(Dir::new()), + })), + ]), + free_inodes: Mutex::new(Vec::new()), + } + } + + pub fn lookup(&self, inode: u64) -> io::Result { + let inode = usize::try_from(inode).map_err(|_| { + // we could have never created such an inode for the kernel... + io_format_err!("kernel accessed unexpected too-large inode: {}", inode) + })?; + match self.entries.read().unwrap().get(inode) { + Some(Some(entry)) => { + let entry: &FsEntry = entry; + let entry = entry as *const FsEntry; + Ok(InodeLookup::new(self, unsafe { &*entry })) + } + // This inode has been deleted... + Some(None) => io_return!(libc::ENOENT), + // This inode has never been advertised to the kernel: + None => io_bail!("kernel looked up never-advertised inode: {}", inode), + } + } + + pub fn lookup_at(&self, inode: u64, name: &OsStr) -> io::Result { + let node = self.lookup(inode)?; + + if name == OsStr::new(".") { + return Ok(node); + } + + match &node.content { + FsContent::Dir(dir) => { + if name == OsStr::new("..") { + return self.lookup(node.parent); + } + + match dir.files.read().unwrap().get(name) { + Some(inode) => self.lookup(*inode), + None => io_return!(libc::ENOENT), + } + } + _ => io_return!(libc::ENOTDIR), + } + } + + unsafe fn forget_entry(&self, entry: &FsEntry, nlookup: usize) { + if entry.lookups.fetch_sub(nlookup, Ordering::AcqRel) > 1 { + // there were still more lookups present + return; + } + + // lookup count dropped to zero, check the hard links: + if entry.links.load(Ordering::Acquire) != 0 { + return; + } + + let inode = entry.inode; + + entry.on_drop(self); + drop(entry); + + // no lookups, no hard links, delete: + eprintln!("Deleting inode {}", inode); + let mut entries_lock = self.entries.write().unwrap(); + entries_lock[inode as usize] = None; + drop(entries_lock); + self.free_inodes.lock().unwrap().push(inode); + } + + pub fn forget(&self, inode: u64, nlookup: usize) -> io::Result<()> { + let entries_lock = self.entries.read().unwrap(); + match entries_lock.get(inode as usize).as_ref() { + Some(Some(entry)) => { + unsafe { + let entry = entry.as_ref() as *const FsEntry; + drop(entries_lock); + self.forget_entry(&*entry, nlookup); + } + Ok(()) + } + Some(None) => io_return!(libc::ENOENT), + None => io_bail!("tried to forget a never-looked-up inode"), + } + } + + fn do_create( + &self, + parent: u64, + name: OsString, + mode: libc::mode_t, + fs_content: FsContent, + ) -> io::Result { + use std::collections::btree_map::Entry::*; + let parent_dir = self.lookup(parent)?; + + match &parent_dir.content { + FsContent::Dir(content) => { + let mut content = content.files.write().unwrap(); + match content.entry(name) { + Occupied(_) => io_return!(libc::EEXIST), + Vacant(vacancy) => { + let mut stat = new_stat(0, mode, 0, 0); + + // create an inode and put a write-lock the entry list + let inode = self.free_inodes.lock().unwrap().pop(); + let mut entry_lock = self.entries.write().unwrap(); + let inode = match inode { + Some(inode) => inode, + None => { + let inode = entry_lock.len(); + entry_lock.push(None); + inode as u64 + } + }; + + // create the directory + stat.st_ino = inode; + let dir = Box::new(FsEntry { + inode, + parent, + lookups: AtomicUsize::new(0), + links: AtomicUsize::new(1), + stat: RwLock::new(stat), + content: fs_content, + }); + + let ptr = &*dir as *const FsEntry; + + // Insert into the file system: + entry_lock[inode as usize] = Some(dir); + // Hardlink the inode into the directory + vacancy.insert(inode); + + Ok(InodeLookup::new(self, unsafe { &*ptr })) + } + } + } + _ => io_return!(libc::ENOTDIR), + } + } + + pub fn mkdir( + &self, + parent: u64, + name: OsString, + mode: libc::mode_t, + ) -> io::Result { + self.do_create( + parent, + name, + mode | libc::S_IFDIR, + FsContent::Dir(Dir::new()), + ) + } + + pub fn create( + &self, + parent: u64, + name: OsString, + mode: libc::mode_t, + ) -> io::Result { + self.do_create( + parent, + name, + mode | libc::S_IFREG, + FsContent::File(File::new()), + ) + } + + pub fn unlink(&self, parent: u64, name: &OsStr, is_rmdir: bool) -> Result<(), Error> { + let parent_dir = self.lookup(parent)?; + let entry = match &parent_dir.content { + FsContent::Dir(content) => { + let mut content = content.files.write().unwrap(); + + // FIXME: once BTreeMap::remove_entry is stable, use this to avoid cloning `name`. + let inode = match content.remove(name) { + Some(entry) => entry, + None => io_return!(libc::ENOENT), + }; + + let entry = self.lookup(inode)?; + match &entry.content { + FsContent::Dir(_) if !is_rmdir => { + content.insert(name.to_owned(), inode); + io_return!(libc::EISDIR); + } + FsContent::Dir(dir) if !dir.is_empty() => { + content.insert(name.to_owned(), inode); + io_return!(libc::ENOTEMPTY); + } + FsContent::Dir(_) => (), + _ if is_rmdir => { + content.insert(name.to_owned(), inode); + io_return!(libc::ENOTDIR); + } + _ => (), + } + + entry + } + _ => io_return!(libc::ENOTDIR), + }; + + entry.links.fetch_sub(1, Ordering::AcqRel); + + Ok(()) + } + + pub fn write(&self, inode: u64, data: &[u8], offset: u64) -> io::Result<()> { + let node = self.lookup(inode)?; + match &node.content { + FsContent::File(file) => { + let new_size = file.write(data, offset)?; + node.stat.write().unwrap().st_size = new_size as libc::off_t; + Ok(()) + } + _ => io_return!(libc::EBADF), + } + } + + pub fn read(&self, inode: u64, data: &mut [u8], offset: u64) -> io::Result { + let node = self.lookup(inode)?; + match &node.content { + FsContent::File(file) => file.read(data, offset), + _ => io_return!(libc::EBADF), + } + } +} + +pub struct InodeLookup<'a> { + fs: &'a Fs, + entry: Option<&'a FsEntry>, +} + +impl<'a> Drop for InodeLookup<'a> { + fn drop(&mut self) { + if let Some(entry) = self.entry.take() { + unsafe { + self.fs.forget_entry(entry, 1); + } + } + } +} + +impl<'a> InodeLookup<'a> { + fn new(fs: &'a Fs, entry: &'a FsEntry) -> InodeLookup<'a> { + entry.lookups.fetch_add(1, Ordering::AcqRel); + Self { + fs, + entry: Some(entry), + } + } + + pub fn leak(mut self) -> &'a FsEntry { + self.entry.take().unwrap() + } + + pub fn increment_lookup(&self) { + self.entry.unwrap().lookups.fetch_add(1, Ordering::AcqRel); + } +} + +impl<'a> std::ops::Deref for InodeLookup<'a> { + type Target = FsEntry; + + fn deref(&self) -> &Self::Target { + self.entry.clone().unwrap() + } +} + +pub struct FsEntry { + pub inode: u64, + pub parent: u64, + pub lookups: AtomicUsize, + pub links: AtomicUsize, + pub stat: RwLock, + pub content: FsContent, +} + +impl FsEntry { + fn try_add_link(&self) -> io::Result<()> { + loop { + let links = self.links.load(Ordering::Acquire); + if links == 0 { + eprintln!("Tried to increase a hardlink count of 0"); + io_return!(libc::ENOENT); + } + if self + .links + .compare_exchange(links, links + 1, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + return Ok(()); + } + } + } + + fn on_drop(&self, fs: &Fs) { + if let FsContent::Dir(dir) = &self.content { + if let Err(err) = dir.on_drop(fs) { + eprintln!("error cleaning out directory: {}", err); + } + } + } +} + +pub enum FsContent { + Dir(Dir), + File(File), +} + +pub struct Dir { + pub files: RwLock>, +} + +impl Dir { + fn new() -> Self { + Self { + files: RwLock::new(BTreeMap::new()), + } + } + + fn is_empty(&self) -> bool { + self.files.read().unwrap().is_empty() + } + + fn on_drop(&self, fs: &Fs) -> io::Result<()> { + let files = mem::take(&mut *self.files.write().unwrap()); + + for inode in files.values() { + fs.lookup(*inode)?.links.fetch_sub(1, Ordering::AcqRel); + } + + Ok(()) + } +} + +pub struct File { + data: RwLock, +} + +impl File { + fn new() -> Self { + Self { + data: RwLock::new(BlockFile::new(4096)), + } + } + + /// Returns the new absolute file size, so we can update the stat member. + fn write(&self, data: &[u8], offset: u64) -> io::Result { + let mut content = self.data.write().unwrap(); + content.write(data, offset)?; + Ok(content.size()) + } + + /// Returns the amount of bytes actually read. + fn read(&self, data: &mut [u8], offset: u64) -> io::Result { + self.data.read().unwrap().read(data, offset) + } +} diff --git a/examples/tmpfs/macros.rs b/examples/tmpfs/macros.rs new file mode 100644 index 0000000..dff079a --- /dev/null +++ b/examples/tmpfs/macros.rs @@ -0,0 +1,15 @@ +macro_rules! io_format_err { + ($($fmt:tt)*) => { + ::std::io::Error::new(::std::io::ErrorKind::Other, format!($($fmt)*)) + } +} + +macro_rules! io_bail { + ($($fmt:tt)*) => { return Err(io_format_err!($($fmt)*).into()); } +} + +macro_rules! io_return { + ($errno:expr) => { + return Err(::std::io::Error::from_raw_os_error($errno).into()); + }; +} diff --git a/examples/tmpfs/main.rs b/examples/tmpfs/main.rs new file mode 100644 index 0000000..d135ed0 --- /dev/null +++ b/examples/tmpfs/main.rs @@ -0,0 +1,318 @@ +use std::convert::TryFrom; +use std::ffi::OsStr; +use std::path::Path; +use std::{io, mem}; + +use anyhow::{bail, format_err, Error}; +use futures::future::FutureExt; +use futures::select; +use futures::stream::TryStreamExt; +use tokio::signal::unix::{signal, SignalKind}; + +use proxmox_fuse::requests::{self, FuseRequest, SetTime}; +use proxmox_fuse::{EntryParam, Fuse, ReplyBufState, Request}; + +#[macro_use] +pub mod macros; + +pub mod block_file; +pub mod fs; + +use fs::Fs; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let mut args = std::env::args_os().skip(1); + + let path = args.next().ok_or_else(|| format_err!("missing path"))?; + + let mut interrupt = signal(SignalKind::interrupt())?; + let fuse = Fuse::builder("mytmpfs")? + .debug() + .enable_readdir() + .enable_mkdir() + .enable_rmdir() + .enable_create() + .enable_unlink() + .enable_mknod() + .enable_setattr() + .enable_read() + .enable_write() + .build()? + .mount(Path::new(&path))?; + + select! { + res = handle_fuse(fuse).fuse() => res?, + _ = interrupt.recv().fuse() => { + eprintln!("interrupted"); + } + } + + Ok(()) +} + +fn to_entry_param(stat: &libc::stat) -> EntryParam { + EntryParam { + inode: stat.st_ino, + generation: 1, + attr: stat.clone(), + attr_timeout: std::f64::MAX, + entry_timeout: std::f64::MAX, + } +} + +fn handle_io_err( + err: io::Error, + reply: impl FnOnce(io::Error) -> io::Result<()>, +) -> Result<(), Error> { + // `io_bail` is used for error reporting where we return `EIO` + if err.kind() == io::ErrorKind::Other { + eprintln!("An IO error occured: {}", err); + } + reply(err)?; + Ok(()) +} + +fn handle_err(err: Error, reply: impl FnOnce(io::Error) -> io::Result<()>) -> Result<(), Error> { + match err.downcast::() { + Ok(err) => handle_io_err(err, reply), + Err(err) => { + // `bail` (non-`io::Error`) is used for fatal errors which should actually cancel: + eprintln!("internal error: {}", err); + Err(err) + } + } +} + +async fn handle_fuse(mut fuse: Fuse) -> Result<(), Error> { + let fs = Fs::new(); + + while let Some(request) = fuse.try_next().await? { + match request { + Request::Getattr(request) => match fs.lookup(request.inode) { + Ok(node) => request.reply(&node.leak().stat.read().unwrap(), std::f64::MAX)?, + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + }, + Request::Setattr(mut request) => match handle_setattr(&fs, &mut request) { + Ok(node) => request.reply(&node.stat.read().unwrap(), std::f64::MAX)?, + Err(err) => handle_err(err, |err| request.io_fail(err))?, + }, + Request::Lookup(request) => match fs.lookup_at(request.parent, &request.file_name) { + Ok(node) => request.reply(&to_entry_param(&node.leak().stat.read().unwrap()))?, + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + }, + Request::Forget(request) => match fs.forget(request.inode, request.count as usize) { + Ok(()) => request.reply(), + Err(err) => eprintln!("error forgetting inode {}: {}", request.inode, err), + }, + Request::Readdir(mut request) => match handle_readdir(&fs, &mut request) { + Ok(()) => request.reply()?, + Err(err) => handle_err(err, |err| request.io_fail(err))?, + }, + Request::Mkdir(mut request) => { + let reply = fs.mkdir( + request.parent, + mem::take(&mut request.dir_name), + request.mode, + ); + match reply { + Ok(entry) => { + request.reply(&to_entry_param(&entry.leak().stat.read().unwrap()))? + } + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + } + } + Request::Create(mut request) => { + let reply = fs.create( + request.parent, + mem::take(&mut request.file_name), + request.mode, + ); + match reply { + Ok(entry) => { + // CREATE acts as `Lookup` + `Open` + entry.increment_lookup(); + request.reply(&to_entry_param(&entry.leak().stat.read().unwrap()), 0)? + } + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + } + } + Request::Mknod(mut request) => { + let reply = fs.create( + request.parent, + mem::take(&mut request.file_name), + request.mode, + ); + match reply { + Ok(entry) => { + request.reply(&to_entry_param(&entry.leak().stat.read().unwrap()))? + } + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + } + } + Request::Open(request) => match fs.lookup(request.inode) { + Ok(node) => request.reply(&to_entry_param(&node.leak().stat.read().unwrap()), 0)?, + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + }, + Request::Release(request) => match fs.forget(request.inode, 1) { + Ok(()) => request.reply()?, + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + }, + Request::Unlink(request) => { + match fs.unlink(request.parent, &request.file_name, false) { + Ok(()) => request.reply()?, + Err(err) => handle_err(err, |err| request.io_fail(err))?, + } + } + Request::Rmdir(request) => match fs.unlink(request.parent, &request.dir_name, true) { + Ok(()) => request.reply()?, + Err(err) => handle_err(err, |err| request.io_fail(err))?, + }, + Request::Write(request) => { + match fs.write(request.inode, request.data(), request.offset) { + Ok(()) => { + let len = request.data().len(); + request.reply(len)?; + } + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + } + } + Request::Read(request) => { + // For simplicity we just limit reads to 1 MiB for now... + let size = request.size.min(1024 * 1024); + let mut buf = Vec::with_capacity(size); + unsafe { + buf.set_len(size); + } + match fs.read(request.inode, &mut buf, request.offset) { + Ok(got) => { + unsafe { + buf.set_len(got); + } + request.reply(&buf)?; + } + Err(err) => handle_io_err(err, |err| request.io_fail(err))?, + } + } + other => bail!("Got unknown request: {:?}", other), + } + } + Ok(()) +} + +fn handle_readdir(fs: &Fs, request: &mut requests::Readdir) -> Result<(), Error> { + let offset = match isize::try_from(request.offset) { + Ok(offset) => offset, + Err(_) => bail!("bad offset"), + }; + + let dir = fs.lookup(request.inode)?; + + match &dir.content { + fs::FsContent::Dir(content) => { + let files = content.files.read().unwrap(); + let file_count = files.len() as isize; + let mut next = offset; + for (name, &inode) in files.iter().skip(offset as usize) { + next += 1; + let inode = fs.lookup(inode)?; + let stat = inode.stat.read().unwrap(); + match request.add_entry(&name, &stat, next)? { + ReplyBufState::Ok => (), + ReplyBufState::Full => return Ok(()), + } + } + drop(files); + + if next == file_count { + next += 1; + let inode = fs.lookup(dir.parent)?; + let stat = inode.stat.read().unwrap(); + match request.add_entry(OsStr::new(".."), &stat, next)? { + ReplyBufState::Ok => (), + ReplyBufState::Full => return Ok(()), + } + } + + if next == file_count + 1 { + next += 1; + match request.add_entry(OsStr::new("."), &dir.stat.read().unwrap(), next)? { + ReplyBufState::Ok => (), + ReplyBufState::Full => return Ok(()), + } + } + + Ok(()) + } + _ => io_return!(libc::ENOTDIR), + } +} + +fn handle_setattr<'a>( + fs: &'a Fs, + request: &mut requests::Setattr, +) -> Result, Error> { + use std::time::SystemTime; + + let file = fs.lookup(request.inode)?; + + let mut stat = file.stat.write().unwrap(); + let mut now = None; + + if let Some(mode) = request.mode() { + stat.st_mode = (stat.st_mode & libc::S_IFMT) | mode + } + + if let Some(uid) = request.uid() { + stat.st_uid = uid; + } + + if let Some(gid) = request.gid() { + stat.st_gid = gid; + } + + if let Some(size) = request.size() { + stat.st_size = size as libc::off_t; + } + + if let Some(time) = match request.atime() { + Some(SetTime::Now) => { + let new_now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + now = Some(new_now); + Some(new_now) + } + Some(SetTime::Time(time)) => Some(time), + None => None, + } { + stat.st_atime = time.as_secs() as _; + stat.st_atime_nsec = time.subsec_nanos() as _; + } + + if let Some(time) = match request.mtime() { + Some(SetTime::Now) => match now { + Some(now) => Some(now), + None => { + let new_now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + //now = Some(new_now); + Some(new_now) + } + }, + Some(SetTime::Time(time)) => Some(time), + None => None, + } { + stat.st_mtime = time.as_secs() as _; + stat.st_mtime_nsec = time.subsec_nanos() as _; + } + + if let Some(time) = request.ctime() { + stat.st_ctime = time.as_secs() as _; + stat.st_ctime_nsec = time.subsec_nanos() as _; + } + + drop(stat); + Ok(file) +} diff --git a/src/fuse_fd.rs b/src/fuse_fd.rs new file mode 100644 index 0000000..68d9e79 --- /dev/null +++ b/src/fuse_fd.rs @@ -0,0 +1,66 @@ +//! This binds the fuse file descriptor to the tokio reactor. + +use std::io; +use std::os::unix::io::{AsRawFd, RawFd}; + +use mio::event::Evented; +use mio::unix::EventedFd; +use mio::{Poll, PollOpt, Ready, Token}; + +pub struct FuseFd { + fd: RawFd, +} + +impl FuseFd { + pub(crate) fn from_raw(fd: RawFd) -> io::Result { + let this = Self { fd }; + + // make sure it is nonblocking + unsafe { + let rc = libc::fcntl(fd, libc::F_GETFL); + if rc == -1 { + return Err(io::Error::last_os_error()); + } + + let rc = libc::fcntl(fd, libc::F_SETFL, rc | libc::O_NONBLOCK); + if rc == -1 { + return Err(io::Error::last_os_error()); + } + } + + Ok(this) + } +} + +impl AsRawFd for FuseFd { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl Evented for FuseFd { + fn register( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + EventedFd(&self.fd).register(poll, token, interest, opts) + } + + fn reregister( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + EventedFd(&self.fd).reregister(poll, token, interest, opts) + } + + fn deregister(&self, poll: &Poll) -> io::Result<()> { + EventedFd(&self.fd).deregister(poll) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..df509ec --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,13 @@ +pub(crate) mod fuse_fd; +pub mod requests; +pub(crate) mod session; +pub(crate) mod sys; +pub(crate) mod util; + +#[doc(inline)] +pub use sys::{EntryParam, ReplyBufState, ROOT_ID}; + +#[doc(inline)] +pub use requests::Request; + +pub use session::{Fuse, FuseSession, FuseSessionBuilder}; diff --git a/src/requests.rs b/src/requests.rs new file mode 100644 index 0000000..13f8079 --- /dev/null +++ b/src/requests.rs @@ -0,0 +1,912 @@ +//! The bigger part of the public API is about which requests we're handling: +//! +//! Currently all reply functions are regular functions returning an `io::Result`, however, it is +//! possible that in the future these will become `async fns`, depending on whether the fuse file +//! descriptor can actually fill up when it is non-blocking? + +use std::ffi::{CStr, CString, OsStr, OsString}; +use std::io; +use std::os::unix::ffi::OsStrExt; +use std::sync::Arc; +use std::time::Duration; + +use crate::sys::{self, ReplyBufState}; +use crate::util::Stat; + +#[derive(Debug)] +pub struct RequestGuard { + raw: sys::Request, +} + +unsafe impl Send for RequestGuard {} +unsafe impl Sync for RequestGuard {} + +impl RequestGuard { + pub(crate) fn from_raw(raw: sys::Request) -> Self { + Self { raw } + } + + /// Consume the request and to not trigger the automatic `ENOSYS` response. + pub fn into_raw(mut self) -> sys::Request { + std::mem::replace(&mut self.raw, sys::Request::NULL) + } +} + +impl Drop for RequestGuard { + fn drop(&mut self) { + if !self.raw.is_null() { + unsafe { + let _ = sys::fuse_reply_err(self.raw, libc::ENOSYS); + } + } + } +} + +fn reply_err(request: RequestGuard, errno: libc::c_int) -> io::Result<()> { + unsafe { + let rc = sys::fuse_reply_err(request.into_raw(), errno); + if rc == 0 { + Ok(()) + } else { + Err(io::Error::from_raw_os_error(-rc)) + } + } +} + +macro_rules! reply_result { + ($self:ident : $expr:expr) => {{ + let rc = unsafe { $expr }; + if rc == 0 { + let _done = $self.request.into_raw(); + Ok(()) + } else { + Err(io::Error::from_raw_os_error(-rc)) + } + }}; +} + +/// Helper trait to easily provide the fail method for all the request types within the `Request` +/// enum even after they have been moved out of the enum, without requiring the exact type. +pub trait FuseRequest: Sized { + /// Send an error reply. + fn fail(self, errno: libc::c_int) -> io::Result<()>; + + /// Convenience method to use an `io::Error` as a response. + fn io_fail(self, error: io::Error) -> io::Result<()> { + self.fail(error.raw_os_error().unwrap_or(libc::EIO)) + } + + // /// Wrap code so that `std::io::Errors` get sent as a reply and other errors (including errors + // /// sending the reply) will propagate through as errors. + // fn wrap(self, func: F) -> io::Result<()> + // where + // F: FnOnce(Self) -> Result<(), E>, + // E: std::error::Error + 'static, + // { + // match func(self) { + // Ok(()) => Ok(()), + // Err(err) => { + // if let Some(err) = err.downcast_ref::() => { + // self.fail(err.raw_os_error().unwrap_or(libc::EIO)) + // } else { + // Err(err) + // } + // } + // } + // } +} + +#[derive(Debug)] +pub enum Request { + Lookup(Lookup), + Forget(Forget), + Getattr(Getattr), + Setattr(Setattr), + Readdir(Readdir), + ReaddirPlus(ReaddirPlus), + Mkdir(Mkdir), + Create(Create), + Mknod(Mknod), + Open(Open), + Release(Release), + Read(Read), + Unlink(Unlink), + Rmdir(Rmdir), + Write(Write), + Readlink(Readlink), + ListXAttrSize(ListXAttrSize), + ListXAttr(ListXAttr), + GetXAttrSize(GetXAttrSize), + GetXAttr(GetXAttr), + // NOTE: + // Open: + // will need to create `FuseFileInfo.fh`, for which we'll probably add a trait generic + // parameter to the `Fuse` object with a `set` method which we call in the `open` + // *callback* already (as the `struct fuse_file_info` becomes invalid after any callback + // returns), and methods to get/delete the data. This will be either a generic type on + // `Fuse` itself, or we provide an &dyn or Box for this. + // Flush: will need `FuseFileInfo.lock_owner` + // Poll: will need `FuseFileInfo.poll_events` +} + +impl FuseRequest for Request { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + match self { + Request::Forget(r) => { + r.reply(); + Ok(()) + } + Request::Lookup(r) => r.fail(errno), + Request::Getattr(r) => r.fail(errno), + Request::Setattr(r) => r.fail(errno), + Request::Readdir(r) => r.fail(errno), + Request::ReaddirPlus(r) => r.fail(errno), + Request::Mkdir(r) => r.fail(errno), + Request::Create(r) => r.fail(errno), + Request::Mknod(r) => r.fail(errno), + Request::Open(r) => r.fail(errno), + Request::Release(r) => r.fail(errno), + Request::Read(r) => r.fail(errno), + Request::Unlink(r) => r.fail(errno), + Request::Rmdir(r) => r.fail(errno), + Request::Write(r) => r.fail(errno), + Request::Readlink(r) => r.fail(errno), + Request::ListXAttrSize(r) => r.fail(errno), + Request::ListXAttr(r) => r.fail(errno), + Request::GetXAttrSize(r) => r.fail(errno), + Request::GetXAttr(r) => r.fail(errno), + } + } +} + +/// A lookup for an entry in a directory. This should increase the lookup count for the inode, +/// as from then on the kernel will refer to the looked-up entry only via the inode.. +#[derive(Debug)] +pub struct Lookup { + pub(crate) request: RequestGuard, + pub parent: u64, + pub file_name: OsString, +} + +impl FuseRequest for Lookup { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Lookup { + pub fn reply(self, entry: &sys::EntryParam) -> io::Result<()> { + let rc = unsafe { sys::fuse_reply_entry(self.request.raw, Some(entry)) }; + if rc == 0 { + let _done = self.request.into_raw(); + Ok(()) + } else { + Err(io::Error::from_raw_os_error(-rc)) + } + } +} + +/// Forget references (lookup count) for an inode. Once an inode reaches a lookup count of zero, +/// the kernel will not refer to the inode anymore, meaning any cached information to access it may +/// be released. +#[derive(Debug)] +pub struct Forget { + pub(crate) request: RequestGuard, + pub inode: u64, + pub count: u64, +} + +impl FuseRequest for Forget { + /// Forget cannot fail. + fn fail(self, _errno: libc::c_int) -> io::Result<()> { + Ok(()) + } +} + +impl Forget { + pub fn reply(self) { + unsafe { + sys::fuse_reply_none(self.request.into_raw()); + } + } +} + +/// This is the equivalent of a `stat` call. +/// +/// The inode is already known, so no changes to the lookup count occur. +#[derive(Debug)] +pub struct Getattr { + pub(crate) request: RequestGuard, + pub inode: u64, +} + +impl FuseRequest for Getattr { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Getattr { + /// Send a reply for a `Getattr` request. + pub fn reply(self, stat: &libc::stat, timeout: f64) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_attr(self.request.raw, Some(stat), timeout)) + } +} + +/// Get the contents of a directory without changing any lookup counts. (Contrary to +/// `ReaddirPlus`). +#[derive(Debug)] +pub struct Readdir { + pub(crate) request: Option, + pub inode: u64, + pub offset: i64, + + reply_buffer: Option, +} + +impl Drop for Readdir { + fn drop(&mut self) { + if self.reply_buffer.is_some() { + let _ = reply_err(self.request.take().unwrap(), libc::EIO); + } + } +} + +impl FuseRequest for Readdir { + fn fail(mut self, errno: libc::c_int) -> io::Result<()> { + self.reply_buffer = None; + reply_err(self.request.take().unwrap(), errno) + } +} + +impl Readdir { + pub(crate) fn new(request: RequestGuard, inode: u64, size: usize, offset: i64) -> Self { + let raw_request = request.raw; + + Self { + request: Some(request), + inode, + offset, + reply_buffer: Some(sys::ReplyBuf::new(raw_request, size)), + } + } + + /// Add a reply entry. Note that unless you also consume the `Readdir` object with a call to + /// the `reply()` method, this will produce an `EIO` error. + pub fn add_entry( + &mut self, + name: &OsStr, + stat: &libc::stat, + next: isize, + ) -> io::Result { + let name = CString::new(name.as_bytes()).map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "tried to reply with invalid file name", + ) + })?; + + Ok(self + .reply_buffer + .as_mut() + .unwrap() + .add_readdir(&name, stat, next)) + } + + /// Send a successful reply. This also works if no entries have been added via `add_entry`, + /// indicating an empty directory without `.` or `..` present. + pub fn reply(mut self) -> io::Result<()> { + let _disable_guard = self.request.take().unwrap().into_raw(); + self.reply_buffer.take().unwrap().reply() + } +} + +/// Lookup all the contents of a directory. On success, the lookup count of all the returned +/// entries should be increased by 1. +#[derive(Debug)] +pub struct ReaddirPlus { + pub(crate) request: Option, + pub inode: u64, + pub offset: i64, + + reply_buffer: Option, +} + +impl Drop for ReaddirPlus { + fn drop(&mut self) { + if self.reply_buffer.is_some() { + let _ = reply_err(self.request.take().unwrap(), libc::EIO); + } + } +} + +impl FuseRequest for ReaddirPlus { + fn fail(mut self, errno: libc::c_int) -> io::Result<()> { + self.reply_buffer = None; + reply_err(self.request.take().unwrap(), errno) + } +} + +impl ReaddirPlus { + pub(crate) fn new(request: RequestGuard, inode: u64, size: usize, offset: i64) -> Self { + let raw_request = request.raw; + + Self { + request: Some(request), + inode, + offset, + reply_buffer: Some(sys::ReplyBuf::new(raw_request, size)), + } + } + + /// Add a reply entry. Note that unless you also consume the `ReaddirPlus` object with a call + /// to the `reply()` method, this will produce an `EIO` error. + pub fn add_entry( + &mut self, + name: &OsStr, + stat: &libc::stat, + next: isize, + generation: u64, + attr_timeout: f64, + entry_timeout: f64, + ) -> io::Result { + let name = CString::new(name.as_bytes()).map_err(|_| { + io::Error::new( + io::ErrorKind::Other, + "tried to reply with invalid file name", + ) + })?; + + let entry = sys::EntryParam { + inode: stat.st_ino, + generation, + attr: stat.clone(), + attr_timeout, + entry_timeout, + }; + + Ok(self + .reply_buffer + .as_mut() + .unwrap() + .add_readdir_plus(&name, &entry, next)) + } + + /// Send a successful reply. This also works if no entries have been added via `add_entry`, + /// indicating an empty directory without `.` or `..` present. + pub fn reply(mut self) -> io::Result<()> { + let _disable_guard = self.request.take().unwrap().into_raw(); + self.reply_buffer.take().unwrap().reply() + } +} + +/// Create a new directory with a lookup count of 1. +#[derive(Debug)] +pub struct Mkdir { + pub(crate) request: RequestGuard, + pub parent: u64, + pub dir_name: OsString, + pub mode: libc::mode_t, +} + +impl FuseRequest for Mkdir { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Mkdir { + pub fn reply(self, entry: &sys::EntryParam) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_entry(self.request.raw, Some(entry))) + } +} + +/// Create a new file with a lookup count of 1. +#[derive(Debug)] +pub struct Create { + pub(crate) request: RequestGuard, + pub parent: u64, + pub file_name: OsString, + pub mode: libc::mode_t, + pub(crate) file_info: sys::FuseFileInfo, +} + +impl FuseRequest for Create { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Create { + /// The `fh` provided here will be available in later requests for this file handle. + pub fn reply(mut self, entry: &sys::EntryParam, fh: u64) -> io::Result<()> { + self.file_info.fh = fh; + reply_result!(self: sys::fuse_reply_create(self.request.raw, Some(entry), &self.file_info)) + } +} + +/// Create a new node (file or device) with a lookup count of 1. +#[derive(Debug)] +pub struct Mknod { + pub(crate) request: RequestGuard, + pub parent: u64, + pub file_name: OsString, + pub mode: libc::mode_t, + pub dev: libc::dev_t, +} + +impl FuseRequest for Mknod { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Mknod { + /// The lookup count should be bumped by this reply. + pub fn reply(self, entry: &sys::EntryParam) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_entry(self.request.raw, Some(entry))) + } +} + +/// Open a file. This counts as one reference to the file and can be tracked separately or as part +/// of the lookup count. If dealing with opened files requires a kind of state, the `fh` parameter +/// on the `reply` method should point to that state, as it will be included in all requests +/// related to this handle. +#[derive(Debug)] +pub struct Open { + pub(crate) request: RequestGuard, + pub inode: u64, + pub flags: libc::c_int, + pub(crate) file_info: sys::FuseFileInfo, +} + +impl FuseRequest for Open { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Open { + /// The `fh` provided here will be available in later requests for this file handle. + pub fn reply(mut self, entry: &sys::EntryParam, fh: u64) -> io::Result<()> { + self.file_info.fh = fh; + reply_result!(self: sys::fuse_reply_open(self.request.raw, Some(entry), &self.file_info)) + } +} + +/// Release a reference to a file. +#[derive(Debug)] +pub struct Release { + pub(crate) request: RequestGuard, + pub inode: u64, + pub fh: u64, + pub flags: libc::c_int, +} + +impl FuseRequest for Release { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Release { + pub fn reply(self) -> io::Result<()> { + reply_err(self.request, 0) + } +} + +/// Read from a file. +#[derive(Debug)] +pub struct Read { + pub(crate) request: RequestGuard, + pub inode: u64, + pub fh: u64, + pub size: usize, + pub offset: u64, +} + +impl FuseRequest for Read { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Read { + pub fn reply(self, data: &[u8]) -> io::Result<()> { + let ptr = data.as_ptr() as *const libc::c_char; + reply_result!(self: sys::fuse_reply_buf(self.request.raw, ptr, data.len())) + } +} + +pub enum SetTime { + /// The time should be set to the current time. + Now, + + /// Time since the epoch. + Time(Duration), +} + +fn c_duration(secs: libc::time_t, nsecs: i64) -> Duration { + Duration::new(secs as u64, nsecs as u32) +} + +impl SetTime { + /// Truncates nsecs! + fn from_c(secs: libc::time_t, nsecs: i64) -> Self { + SetTime::Time(c_duration(secs, nsecs)) + } +} + +/// Set attributes of a file. +#[derive(Debug)] +pub struct Setattr { + pub(crate) request: RequestGuard, + pub inode: u64, + pub fh: Option, + pub to_set: libc::c_int, + pub(crate) stat: Stat, +} + +impl FuseRequest for Setattr { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Setattr { + /// `Some` if the mode field should be modified. + pub fn mode(&self) -> Option { + if (self.to_set & sys::setattr::MODE) != 0 { + Some(self.stat.st_mode) + } else { + None + } + } + + /// `Some` if the uid field should be modified. + pub fn uid(&self) -> Option { + if (self.to_set & sys::setattr::UID) != 0 { + Some(self.stat.st_uid) + } else { + None + } + } + + /// `Some` if the gid field should be modified. + pub fn gid(&self) -> Option { + if (self.to_set & sys::setattr::GID) != 0 { + Some(self.stat.st_gid) + } else { + None + } + } + + /// `Some` if the size field should be modified. + pub fn size(&self) -> Option { + if (self.to_set & sys::setattr::SIZE) != 0 { + Some(self.stat.st_size as u64) + } else { + None + } + } + + /// `Some` if the atime field should be modified. + pub fn atime(&self) -> Option { + if (self.to_set & sys::setattr::ATIME) != 0 { + Some(SetTime::from_c(self.stat.st_atime, self.stat.st_atime_nsec)) + } else if (self.to_set & sys::setattr::ATIME_NOW) != 0 { + Some(SetTime::Now) + } else { + None + } + } + + /// `Some` if the mtime field should be modified. + pub fn mtime(&self) -> Option { + if (self.to_set & sys::setattr::MTIME) != 0 { + Some(SetTime::from_c(self.stat.st_mtime, self.stat.st_mtime_nsec)) + } else if (self.to_set & sys::setattr::MTIME_NOW) != 0 { + Some(SetTime::Now) + } else { + None + } + } + + /// `Some` if the ctime field should be modified. + pub fn ctime(&self) -> Option { + if (self.to_set & sys::setattr::CTIME) != 0 { + Some(c_duration(self.stat.st_ctime, self.stat.st_ctime_nsec)) + } else { + None + } + } + + /// Send a reply for a `Setattr` request. + pub fn reply(self, stat: &libc::stat, timeout: f64) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_attr(self.request.raw, Some(stat), timeout)) + } +} + +/// Remove a hard-link to a file. Note that the removed file may still have active references which +/// should still be usable. This only unlinks the file from one directory. +#[derive(Debug)] +pub struct Unlink { + pub(crate) request: RequestGuard, + pub parent: u64, + pub file_name: OsString, +} + +impl FuseRequest for Unlink { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Unlink { + /// The `fh` provided here will be available in later requests for this file handle. + pub fn reply(self) -> io::Result<()> { + reply_err(self.request, 0) + } +} + +/// Remove a directory entry. Note that the removed directory may still have active references +/// which should still be usable. Only its hard link into the directory hierarchy is dropped. +#[derive(Debug)] +pub struct Rmdir { + pub(crate) request: RequestGuard, + pub parent: u64, + pub dir_name: OsString, +} + +impl FuseRequest for Rmdir { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Rmdir { + /// The `fh` provided here will be available in later requests for this file handle. + pub fn reply(self) -> io::Result<()> { + reply_err(self.request, 0) + } +} + +/// Write to a file. +#[derive(Debug)] +pub struct Write { + pub(crate) request: RequestGuard, + pub inode: u64, + pub fh: u64, + pub data: *const u8, + pub size: usize, + pub offset: u64, + + /// We keep a reference count on the buffer we pass to `fuse_session_receive_buf` so it will + /// not be cleared until it is used up by requests borrowing data from it, like `Write`. + pub(crate) buffer: Arc, +} + +unsafe impl Send for Write {} +unsafe impl Sync for Write {} + +impl FuseRequest for Write { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Write { + pub fn reply(self, size: usize) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_write(self.request.raw, size)) + } + + #[inline] + pub fn data(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.data, self.size) } + } +} + +/// Read a symbolic link. +#[derive(Debug)] +pub struct Readlink { + pub(crate) request: RequestGuard, + pub inode: u64, +} + +impl FuseRequest for Readlink { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl Readlink { + pub fn reply(self, data: &OsStr) -> io::Result<()> { + let data = CString::new(data.as_bytes()).map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tried to reply with invalid link") + })?; + + self.c_reply(&data) + } + + pub fn c_reply(self, data: &CStr) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_readlink(self.request.raw, data.as_ptr())) + } +} + +/// Get the size of the extended attribute list. +#[derive(Debug)] +pub struct ListXAttrSize { + pub(crate) request: RequestGuard, + pub inode: u64, +} + +impl FuseRequest for ListXAttrSize { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl ListXAttrSize { + pub fn reply(self, size: usize) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_xattr(self.request.raw, size)) + } +} + +/// List extended attributes. +#[derive(Debug)] +pub struct ListXAttr { + pub(crate) request: RequestGuard, + pub inode: u64, + pub size: usize, + response: Vec, +} + +impl FuseRequest for ListXAttr { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl ListXAttr { + pub(crate) fn new(request: RequestGuard, inode: u64, size: usize) -> Self { + Self { + request, + inode, + size, + response: Vec::new(), + } + } + + /// Check whether we can add `len` bytes to the buffer. This must already include the + /// terminating zero. + fn check_add(&self, len: usize) -> bool { + self.response.len() + len <= len + } + + /// Add an extended attribute entry to the response list. + /// + /// This returns `Full` if the entry would overflow the caller's buffer, but will not fail the + /// request. Use `fail_full()` to send the default reply for a too-small buffer, or `reply()` + /// to reply with success. + pub fn add(&mut self, name: &OsStr) -> ReplyBufState { + unsafe { self.add_bytes_without_zero(name.as_bytes()) } + } + + /// Add a raw attribute name the response list. It must not contain any zeroes. + /// + /// See `add` for details. + pub unsafe fn add_bytes_without_zero(&mut self, name: &[u8]) -> ReplyBufState { + if !self.check_add(name.len() + 1) { + return ReplyBufState::Full; + } + + self.response.reserve(name.len() + 1); + self.response.extend(name); + self.response.push(0); + + ReplyBufState::Ok + } + + /// Add a raw attribute name which is already zero terminated to the response list. + /// + /// See `add` for details. + pub unsafe fn add_bytes_with_zero(&mut self, name: &[u8]) -> ReplyBufState { + if !self.check_add(name.len() + 1) { + return ReplyBufState::Full; + } + + self.response.extend(name); + + ReplyBufState::Ok + } + + /// Add a raw attribute name which is already zero terminated to the response list. + /// + /// This is the safe version as it uses a `CStr`. + /// + /// See `add` for details. + pub fn add_c_string(&mut self, name: &CStr) -> ReplyBufState { + unsafe { self.add_bytes_with_zero(name.to_bytes_with_nul()) } + } + + /// Try to replace the current reply buffer with a raw data buffer. If the provided data is too + /// large it will be returned as an error. + pub fn set_raw_reply(&mut self, data: Vec) -> Result<(), Vec> { + if data.len() > self.size { + return Err(data); + } + + self.response = data; + Ok(()) + } + + /// Reply with the standard error for a too-small size. + pub fn fail_full(self) -> io::Result<()> { + self.fail(libc::ERANGE) + } + + /// Reply to the request with the current data buffer. + pub fn reply(self) -> io::Result<()> { + let ptr = self.response.as_ptr() as *const libc::c_char; + reply_result!(self: sys::fuse_reply_buf(self.request.raw, ptr, self.response.len())) + } + + /// Reply to the request with either an error (`ERANGE` if the data doesn't fit) or with the + /// provided raw buffer discarding anything previously added to the reply. + pub fn reply_raw(self, data: &[u8]) -> io::Result<()> { + if data.len() > self.size { + return self.fail_full(); + } + + let ptr = data.as_ptr() as *const libc::c_char; + reply_result!(self: sys::fuse_reply_buf(self.request.raw, ptr, data.len())) + } +} + +/// Get the size of an extended attribute. +#[derive(Debug)] +pub struct GetXAttrSize { + pub(crate) request: RequestGuard, + pub inode: u64, + pub attr_name: OsString, +} + +impl FuseRequest for GetXAttrSize { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl GetXAttrSize { + pub fn reply(self, size: usize) -> io::Result<()> { + reply_result!(self: sys::fuse_reply_xattr(self.request.raw, size)) + } +} + +/// Get an extended attribute. +#[derive(Debug)] +pub struct GetXAttr { + pub(crate) request: RequestGuard, + pub inode: u64, + pub attr_name: OsString, + pub size: usize, +} + +impl FuseRequest for GetXAttr { + fn fail(self, errno: libc::c_int) -> io::Result<()> { + reply_err(self.request, errno) + } +} + +impl GetXAttr { + /// Reply to the request either with an error (`ERANGE` if the buffer doesn't fit), or with the + /// provided data. + pub fn reply(self, data: &[u8]) -> io::Result<()> { + if data.len() > self.size { + return self.fail(libc::ERANGE); + } + + let ptr = data.as_ptr() as *const libc::c_char; + reply_result!(self: sys::fuse_reply_buf(self.request.raw, ptr, data.len())) + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..9076a4b --- /dev/null +++ b/src/session.rs @@ -0,0 +1,696 @@ +use std::cell::RefCell; +use std::collections::VecDeque; +use std::ffi::{CStr, CString, OsStr}; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::pin::Pin; +use std::ptr::{self, NonNull}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::{io, mem}; + +use anyhow::{bail, format_err, Error}; +use futures::ready; +use tokio::io::PollEvented; +use tokio::stream::Stream; + +use crate::fuse_fd::FuseFd; +use crate::requests::{self, Request, RequestGuard}; +use crate::sys; +use crate::util::Stat; + +/// The default set of operations enabled when nothing else is set via the `FuseSessionBuilder` +/// methods. +/// +/// By default the stream can only yield `Gettattr` requests. +pub const DEFAULT_OPERATIONS: sys::Operations = sys::Operations { + lookup: Some(FuseData::lookup), + forget: Some(FuseData::forget), + getattr: Some(FuseData::getattr), + ..sys::Operations::DEFAULT +}; + +struct FuseData { + /// We're assuming that it's possible `fuse_session_process_buf` may trigger multiple + /// callbacks, so we need to enqueue them all, + /// + /// This is a `RefCell` since we're implementing `Stream` and therefore can only be polled by a + /// single thread at a time. The requests get pushed here, and then immediately yielded by the + /// `Stream::poll_next()` method. + pending_requests: RefCell>, + fbuf: Arc, +} + +unsafe impl Send for FuseData {} +unsafe impl Sync for FuseData {} + +impl FuseData { + extern "C" fn lookup(request: sys::Request, parent: u64, file_name: sys::StrPtr) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let file_name = unsafe { CStr::from_ptr(file_name) }; + let file_name = OsStr::from_bytes(file_name.to_bytes()).to_owned(); + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Lookup(requests::Lookup { + request: RequestGuard::from_raw(request), + parent, + file_name, + })); + } + + extern "C" fn forget(request: sys::Request, inode: u64, nlookup: u64) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Forget(requests::Forget { + request: RequestGuard::from_raw(request), + inode, + count: nlookup, + })); + } + + extern "C" fn getattr(request: sys::Request, inode: u64, _file_info: *const sys::FuseFileInfo) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Getattr(requests::Getattr { + request: RequestGuard::from_raw(request), + inode, + })); + } + + extern "C" fn readdir( + request: sys::Request, + inode: u64, + size: libc::size_t, + offset: libc::off_t, + _file_info: *const sys::FuseFileInfo, + ) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Readdir(requests::Readdir::new( + RequestGuard::from_raw(request), + inode, + size, + offset, + ))); + } + + extern "C" fn readdirplus( + request: sys::Request, + inode: u64, + size: libc::size_t, + offset: libc::off_t, + _file_info: *const sys::FuseFileInfo, + ) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::ReaddirPlus(requests::ReaddirPlus::new( + RequestGuard::from_raw(request), + inode, + size, + offset, + ))); + } + + extern "C" fn mkdir( + request: sys::Request, + parent: u64, + dir_name: sys::StrPtr, + mode: libc::mode_t, + ) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let dir_name = unsafe { CStr::from_ptr(dir_name) }; + let dir_name = OsStr::from_bytes(dir_name.to_bytes()).to_owned(); + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Mkdir(requests::Mkdir { + request: RequestGuard::from_raw(request), + parent, + dir_name, + mode, + })); + } + + extern "C" fn create( + request: sys::Request, + parent: u64, + file_name: sys::StrPtr, + mode: libc::mode_t, + file_info: *const sys::FuseFileInfo, + ) { + let (fuse_data, file_info, file_name) = unsafe { + ( + &*(sys::fuse_req_userdata(request) as *mut FuseData), + &*file_info, + CStr::from_ptr(file_name), + ) + }; + let file_name = OsStr::from_bytes(file_name.to_bytes()).to_owned(); + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Create(requests::Create { + request: RequestGuard::from_raw(request), + parent, + file_name, + mode, + file_info: file_info.clone(), + })); + } + + extern "C" fn mknod( + request: sys::Request, + parent: u64, + file_name: sys::StrPtr, + mode: libc::mode_t, + dev: libc::dev_t, + ) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let file_name = unsafe { CStr::from_ptr(file_name) }; + let file_name = OsStr::from_bytes(file_name.to_bytes()).to_owned(); + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Mknod(requests::Mknod { + request: RequestGuard::from_raw(request), + parent, + file_name, + mode, + dev, + })); + } + + extern "C" fn open(request: sys::Request, inode: u64, file_info: *const sys::FuseFileInfo) { + let (fuse_data, file_info) = unsafe { + ( + &*(sys::fuse_req_userdata(request) as *mut FuseData), + &*file_info, + ) + }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Open(requests::Open { + request: RequestGuard::from_raw(request), + inode, + flags: file_info.flags, + file_info: file_info.clone(), + })); + } + + extern "C" fn release(request: sys::Request, inode: u64, file_info: *const sys::FuseFileInfo) { + let (fuse_data, file_info) = unsafe { + ( + &*(sys::fuse_req_userdata(request) as *mut FuseData), + &*file_info, + ) + }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Release(requests::Release { + request: RequestGuard::from_raw(request), + inode, + flags: file_info.flags, + fh: file_info.fh, + })); + } + + extern "C" fn read( + request: sys::Request, + inode: u64, + size: libc::size_t, + offset: libc::off_t, + file_info: *const sys::FuseFileInfo, + ) { + let (fuse_data, file_info) = unsafe { + ( + &*(sys::fuse_req_userdata(request) as *mut FuseData), + &*file_info, + ) + }; + let size = usize::from(size); + let offset = offset as u64; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Read(requests::Read { + request: RequestGuard::from_raw(request), + fh: file_info.fh, + inode, + size, + offset, + })); + } + + extern "C" fn readlink(request: sys::Request, inode: u64) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Readlink(requests::Readlink { + request: RequestGuard::from_raw(request), + inode, + })); + } + + extern "C" fn setattr( + request: sys::Request, + inode: u64, + stat: *const libc::stat, + to_set: libc::c_int, + file_info: *const sys::FuseFileInfo, + ) { + let (fuse_data, stat, file_info) = unsafe { + ( + &*(sys::fuse_req_userdata(request) as *mut FuseData), + &*stat, + if file_info.is_null() { + None + } else { + Some(&*file_info) + }, + ) + }; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Setattr(requests::Setattr { + request: RequestGuard::from_raw(request), + inode, + to_set, + stat: Stat::from(stat.clone()), + fh: file_info.map(|fi| fi.fh), + })); + } + + extern "C" fn unlink(request: sys::Request, parent: u64, file_name: sys::StrPtr) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let file_name = unsafe { CStr::from_ptr(file_name) }; + let file_name = OsStr::from_bytes(file_name.to_bytes()).to_owned(); + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Unlink(requests::Unlink { + request: RequestGuard::from_raw(request), + parent, + file_name, + })); + } + + extern "C" fn rmdir(request: sys::Request, parent: u64, dir_name: sys::StrPtr) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let dir_name = unsafe { CStr::from_ptr(dir_name) }; + let dir_name = OsStr::from_bytes(dir_name.to_bytes()).to_owned(); + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Rmdir(requests::Rmdir { + request: RequestGuard::from_raw(request), + parent, + dir_name, + })); + } + + extern "C" fn write( + request: sys::Request, + inode: u64, + buffer: *const u8, + size: libc::size_t, + offset: libc::off_t, + file_info: *const sys::FuseFileInfo, + ) { + let (fuse_data, file_info) = unsafe { + ( + &*(sys::fuse_req_userdata(request) as *mut FuseData), + &*file_info, + ) + }; + let size = usize::from(size); + let offset = offset as u64; + fuse_data + .pending_requests + .borrow_mut() + .push_back(Request::Write(requests::Write { + request: RequestGuard::from_raw(request), + fh: file_info.fh, + inode, + data: buffer, + size, + offset, + buffer: Arc::clone(&fuse_data.fbuf), + })); + } + + extern "C" fn listxattr(request: sys::Request, inode: u64, size: libc::size_t) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let size = usize::from(size); + fuse_data.pending_requests.borrow_mut().push_back({ + if size == 0 { + Request::ListXAttrSize(requests::ListXAttrSize { + request: RequestGuard::from_raw(request), + inode, + }) + } else { + Request::ListXAttr(requests::ListXAttr::new( + RequestGuard::from_raw(request), + inode, + size, + )) + } + }); + } + + extern "C" fn getxattr( + request: sys::Request, + inode: u64, + attr_name: sys::StrPtr, + size: libc::size_t, + ) { + let fuse_data = unsafe { &*(sys::fuse_req_userdata(request) as *mut FuseData) }; + let attr_name = unsafe { CStr::from_ptr(attr_name) }; + let attr_name = OsStr::from_bytes(attr_name.to_bytes()).to_owned(); + let size = usize::from(size); + fuse_data.pending_requests.borrow_mut().push_back({ + if size == 0 { + Request::GetXAttrSize(requests::GetXAttrSize { + request: RequestGuard::from_raw(request), + inode, + attr_name, + }) + } else { + Request::GetXAttr(requests::GetXAttr { + request: RequestGuard::from_raw(request), + inode, + attr_name, + size, + }) + } + }); + } +} + +pub struct FuseSessionBuilder { + args: Vec, + has_debug: bool, + operations: sys::Operations, +} + +impl FuseSessionBuilder { + pub fn options(self, option: &str) -> Result { + Ok(self.options_c( + CString::new(option).map_err(|err| format_err!("bad option string: {}", err))?, + )) + } + + pub fn options_os(self, option: &OsStr) -> Result { + Ok(self.options_c( + CString::new(option.as_bytes()) + .map_err(|err| format_err!("bad option string: {}", err))?, + )) + } + + pub fn options_c(mut self, option: CString) -> Self { + self.args.reserve(2); + self.args.push(CString::new("-o").unwrap()); + self.args.push(option); + self + } + + pub fn debug(mut self) -> Self { + if !self.has_debug { + self.args.push(CString::new("--debug").unwrap()); + } + self + } + + pub fn build(self) -> Result { + let args: Vec<*const libc::c_char> = self.args.iter().map(|cstr| cstr.as_ptr()).collect(); + + let fuse_data = Box::new(FuseData { + pending_requests: RefCell::new(VecDeque::new()), + fbuf: Arc::new(sys::FuseBuf::new()), + }); + let session = unsafe { + sys::fuse_session_new( + Some(&sys::FuseArgs::from(&args[..])), + Some(&self.operations), + mem::size_of_val(&self.operations), + fuse_data.as_ref() as *const FuseData as sys::ConstPtr, + ) + }; + drop(args); + + if session.is_null() { + bail!("failed to create fuse session"); + } + + Ok(FuseSession { + session, + fuse_data: Some(fuse_data), + mounted: false, + }) + } + + /// Enable `Readdir` requests. + pub fn enable_readdir(mut self) -> Self { + self.operations.readdir = Some(FuseData::readdir); + self + } + + /// Enables all of `ReaddirPlus`, `Lookup` and `Forget` requests. + /// + /// The `Lookup` and `Forget` requests are required for reference counting implied by + /// `ReaddirPlus`. The kernel should send `Forget` requests for references created via + /// `ReaddirPlus`. Not handling them wouldn't make much sense. + pub fn enable_readdirplus(mut self) -> Self { + self.operations.readdirplus = Some(FuseData::readdirplus); + self + } + + /// Enable `Mkdir` requests. + /// + /// Note that the lookup count of newly created directory should be 1. + pub fn enable_mkdir(mut self) -> Self { + self.operations.mkdir = Some(FuseData::mkdir); + self + } + + /// Enable `Create`, `Open` and `Release` requests. + /// + /// Create and open a file. + pub fn enable_create(mut self) -> Self { + self.operations.create = Some(FuseData::create); + self.enable_open() + } + + /// Enable `Mknod`. + /// + /// This may be used by the kernel instead of `Create`. + pub fn enable_mknod(mut self) -> Self { + self.operations.mknod = Some(FuseData::mknod); + self + } + + /// Enable `Open` requests. + /// + /// Open a file. + pub fn enable_open(mut self) -> Self { + self.operations.open = Some(FuseData::open); + self.operations.release = Some(FuseData::release); + self + } + + /// Enable `Setattr` requests. + pub fn enable_setattr(mut self) -> Self { + self.operations.setattr = Some(FuseData::setattr); + self + } + + /// Enable `Unlink` requests. + pub fn enable_unlink(mut self) -> Self { + self.operations.unlink = Some(FuseData::unlink); + self + } + + /// Enable `Rmdir` requests. + pub fn enable_rmdir(mut self) -> Self { + self.operations.rmdir = Some(FuseData::rmdir); + self + } + + /// Enable `Read` requests. + pub fn enable_read(mut self) -> Self { + self.operations.read = Some(FuseData::read); + self + } + + /// Enable `Write` requests. + pub fn enable_write(mut self) -> Self { + self.operations.write = Some(FuseData::write); + self + } + + /// Enable `Readlink` requests. + pub fn enable_readlink(mut self) -> Self { + self.operations.readlink = Some(FuseData::readlink); + self + } + + /// Enable requests to list extended attributes: + /// + /// * `ListXAttrSize` + /// * `ListXAttr` + /// * `GetXAttrSize` + /// * `GetXAttr` + pub fn enable_read_xattr(mut self) -> Self { + self.operations.listxattr = Some(FuseData::listxattr); + self.operations.getxattr = Some(FuseData::getxattr); + self + } +} + +pub struct FuseSession { + session: sys::MutPtr, + fuse_data: Option>, + mounted: bool, +} + +impl Drop for FuseSession { + fn drop(&mut self) { + unsafe { + if self.mounted { + let _ = sys::fuse_session_unmount(self.session); + } + + if !self.session.is_null() { + let _ = sys::fuse_session_destroy(self.session); + } + } + } +} + +impl FuseSession { + pub fn mount(mut self, mountpoint: &Path) -> Result { + let mountpoint = mountpoint.canonicalize()?; + let mountpoint = CString::new(mountpoint.as_os_str().as_bytes()) + .map_err(|err| format_err!("bad path for mount point: {}", err))?; + + let rc = unsafe { sys::fuse_session_mount(self.session, mountpoint.as_ptr()) }; + if rc != 0 { + bail!("mount failed"); + } + self.mounted = true; + + let fd = unsafe { sys::fuse_session_fd(self.session) }; + if fd < 0 { + bail!("failed to get fuse session file descriptor"); + } + + let fuse_fd = PollEvented::new(FuseFd::from_raw(fd)?)?; + + // disable mount guard + self.mounted = false; + Ok(Fuse { + session: SessionPtr(unsafe { + NonNull::new_unchecked(mem::replace(&mut self.session, ptr::null_mut())) + }), + fuse_data: self.fuse_data.take().unwrap(), + fuse_fd, + }) + } +} + +/// Wrap only the session pointer so we can catch auto-trait impl failures for cfuse_data and +/// fuse_fd. +struct SessionPtr(NonNull); + +impl SessionPtr { + #[inline] + fn as_ptr(&self) -> sys::MutPtr { + (self.0).as_ptr() + } +} + +unsafe impl Send for SessionPtr {} +unsafe impl Sync for SessionPtr {} + +/// A mounted fuse file system. +pub struct Fuse { + session: SessionPtr, + fuse_data: Box, + fuse_fd: PollEvented, +} + +// We lose these via the raw session pointer: +impl Unpin for Fuse {} + +impl Drop for Fuse { + fn drop(&mut self) { + unsafe { + let _ = sys::fuse_session_unmount(self.session.as_ptr()); + let _ = sys::fuse_session_destroy(self.session.as_ptr()); + } + } +} + +impl Fuse { + pub fn builder(name: &str) -> Result { + let name = CString::new(name).map_err(|err| format_err!("bad name: {}", err))?; + + Ok(FuseSessionBuilder { + args: vec![name], + has_debug: false, + operations: DEFAULT_OPERATIONS, + }) + } +} + +impl Stream for Fuse { + type Item = io::Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.get_mut(); + loop { + if let Some(request) = this.fuse_data.pending_requests.borrow_mut().pop_front() { + return Poll::Ready(Some(Ok(request))); + } + + ready!(this.fuse_fd.poll_read_ready(cx, mio::Ready::readable()))?; + + let buf: &mut sys::FuseBuf = match Arc::get_mut(&mut this.fuse_data.fbuf) { + Some(buf) => buf, + None => { + this.fuse_data.fbuf = Arc::new(sys::FuseBuf::new()); + // we literally just did Arc::new() + Arc::get_mut(&mut this.fuse_data.fbuf).unwrap() + } + }; + + let rc = unsafe { sys::fuse_session_receive_buf(this.session.as_ptr(), Some(buf)) }; + + if rc == -libc::EAGAIN { + match this.fuse_fd.clear_read_ready(cx, mio::Ready::readable()) { + Ok(()) => continue, + Err(err) => return Poll::Ready(Some(Err(err))), + } + } else if rc < 0 { + return Poll::Ready(Some(Err(io::Error::from_raw_os_error(-rc)))); + } + + unsafe { + sys::fuse_session_process_buf(this.session.as_ptr(), Some(&buf)); + } + // and try again: + } + } +} diff --git a/src/sys.rs b/src/sys.rs new file mode 100644 index 0000000..c5d4485 --- /dev/null +++ b/src/sys.rs @@ -0,0 +1,365 @@ +//! libfuse3 bindings + +use std::ffi::CStr; +use std::io; +use std::marker::PhantomData; + +use libc::{c_char, c_int, c_void, off_t, size_t}; + +/// Node ID of the root i-node. This is fixed according to the FUSE API. +pub const ROOT_ID: u64 = 1; + +/// FFI types for easier readability +pub type RawRequest = *mut c_void; +pub type MutPtr = *mut c_void; +pub type ConstPtr = *const c_void; +pub type StrPtr = *const c_char; +pub type MutStrPtr = *mut c_char; + +/// To help us out with auto-trait implementations: +#[derive(Clone, Copy, Debug)] +#[repr(transparent)] +pub struct Request { + raw: RawRequest, +} + +impl Request { + pub const NULL: Self = Self { + raw: std::ptr::null_mut(), + }; + + #[inline] + pub fn is_null(&self) -> bool { + self.raw.is_null() + } +} + +unsafe impl Send for Request {} +unsafe impl Sync for Request {} + +/// Command line arguments passed to fuse. +#[repr(C)] +#[derive(Debug)] +pub struct FuseArgs<'a> { + argc: c_int, + argv: *const StrPtr, + allocated: c_int, + _phantom: PhantomData<&'a [*const StrPtr]>, +} + +impl<'a> From<&'a [*const c_char]> for FuseArgs<'a> { + fn from(slice: &[*const c_char]) -> Self { + Self { + argc: slice.len() as c_int, + argv: slice.as_ptr(), + allocated: 0, + _phantom: PhantomData, + } + } +} + +#[rustfmt::skip] +#[link(name = "fuse3")] +extern "C" { + pub fn fuse_session_new(args: Option<&FuseArgs>, oprs: Option<&Operations>, size: size_t, op: ConstPtr) -> MutPtr; + pub fn fuse_session_fd(session: ConstPtr) -> c_int; + pub fn fuse_session_mount(session: ConstPtr, mountpoint: StrPtr) -> c_int; + pub fn fuse_session_unmount(session: ConstPtr); + pub fn fuse_session_destroy(session: ConstPtr); + pub fn fuse_reply_attr(req: Request, attr: Option<&libc::stat>, timeout: f64) -> c_int; + pub fn fuse_reply_err(req: Request, errno: c_int) -> c_int; + pub fn fuse_reply_buf(req: Request, buf: *const c_char, size: size_t) -> c_int; + pub fn fuse_reply_entry(req: Request, entry: Option<&EntryParam>) -> c_int; + pub fn fuse_reply_create(req: Request, entry: Option<&EntryParam>, file_info: *const FuseFileInfo) -> c_int; + pub fn fuse_reply_open(req: Request, entry: Option<&EntryParam>, file_info: *const FuseFileInfo) -> c_int; + pub fn fuse_reply_xattr(req: Request, size: size_t) -> c_int; + pub fn fuse_reply_readlink(req: Request, link: StrPtr) -> c_int; + pub fn fuse_reply_none(req: Request); + pub fn fuse_reply_write(req: Request, count: libc::size_t) -> c_int; + pub fn fuse_req_userdata(req: Request) -> MutPtr; + pub fn fuse_add_direntry_plus(req: Request, buf: MutStrPtr, bufsize: size_t, name: StrPtr, stbuf: Option<&EntryParam>, off: c_int) -> size_t; + pub fn fuse_add_direntry(req: Request, buf: MutStrPtr, bufsize: size_t, name: StrPtr, stbuf: Option<&libc::stat>, off: c_int) -> size_t; + pub fn fuse_session_process_buf(session: ConstPtr, buf: Option<&FuseBuf>); + pub fn fuse_session_receive_buf(session: ConstPtr, buf: Option<&mut FuseBuf>) -> c_int; +} + +// Generate a `const Operations::DEFAULT` we can use as `..DEFAULT` when not implementing every +// single call. +macro_rules! default_to_none { + ( + $(#[$attr:meta])* + pub struct $name:ident { $(pub $field:ident : $ty:ty,)* } + ) => ( + $(#[$attr])* + pub struct $name { + $(pub $field : $ty,)* + } + + impl $name { + pub const DEFAULT: Self = Self { + $($field : None,)* + }; + } + ); +} + +#[rustfmt::skip] +default_to_none! { + /// `Operations` defines the callback function table of supported operations. + #[repr(C)] + #[derive(Default)] + pub struct Operations { + // The order in which the functions are listed matters, as the offset in the + // struct defines what function the fuse driver uses. + // It should therefore not be altered! + pub init: Option, + pub destroy: Option, + pub lookup: Option, + pub forget: Option, + pub getattr: Option, + pub setattr: Option, + pub readlink: Option, + pub mknod: Option, + pub mkdir: Option, + pub unlink: Option, + pub rmdir: Option, + pub symlink: Option, + pub rename: Option, + pub link: Option, + pub open: Option, + pub read: Option, + pub write: Option, + pub flush: Option, + pub release: Option, + pub fsync: Option, + pub opendir: Option, + pub readdir: Option, + pub releasedir: Option, + pub fsyncdir: Option, + pub statfs: Option, + pub setxattr: Option, + pub getxattr: Option, + pub listxattr: Option, + pub removexattr: Option, + pub access: Option, + pub create: Option, + pub getlk: Option, + pub setlk: Option, + pub bmap: Option, + pub ioctl: Option, + pub poll: Option, + pub write_buf: Option, + pub retrieve_reply: Option, + pub forget_multi: Option, + pub flock: Option, + pub fallocate: Option, + pub readdirplus: Option, + pub copy_file_range: Option, + } +} + +/// FUSE entry for fuse_reply_entry in lookup callback +#[repr(C)] +pub struct EntryParam { + pub inode: u64, + pub generation: u64, + pub attr: libc::stat, + pub attr_timeout: f64, + pub entry_timeout: f64, +} + +impl EntryParam { + /// A simple entry has a maximum attribute/entry timeout value and always a generatio of 1. + /// This is a convenience method used since we mostly use this for static unchangable archives. + pub fn simple(inode: u64, attr: libc::stat) -> Self { + Self { + inode, + generation: 1, + attr, + attr_timeout: std::f64::MAX, + entry_timeout: std::f64::MAX, + } + } +} + +#[derive(Debug)] +#[repr(C)] +pub struct FuseBuf { + /// Size of data in bytes + size: size_t, + + /// Buffer flags + flags: c_int, + + /// Memory pointer + /// + /// Used unless FUSE_BUF_IS_FD flag is set. + mem: *mut c_void, + + /// File descriptor + /// + /// Used if FUSE_BUF_IS_FD flag is set. + fd: c_int, + + /// File position + /// + /// Used if FUSE_BUF_FD_SEEK flag is set. + pos: off_t, +} + +impl Drop for FuseBuf { + fn drop(&mut self) { + unsafe { + libc::free(self.mem); + } + } +} + +impl FuseBuf { + pub fn new() -> Self { + unsafe { std::mem::zeroed() } + } +} + +#[derive(Clone, Debug)] +#[repr(C)] +pub struct FuseFileInfo { + /// Open flags. Available in open() and release() + pub flags: c_int, + + /// Various bitfields which we will not support for now: + _bits: u64, + + /// File handle. May be filled in by filesystem in open(). + /// Available in all other file operations + pub fh: u64, + + /// Lock owner id. Available in locking operations and flush. + pub lock_owner: u64, + + /// Requested poll events. Available in ->poll. Only set on kernels + /// which support it. If unsupported, this field is set to zero. + pub poll_events: u32, +} + +#[rustfmt::skip] +pub mod setattr { + pub const MODE : libc::c_int = 1 << 0; + pub const UID : libc::c_int = 1 << 1; + pub const GID : libc::c_int = 1 << 2; + pub const SIZE : libc::c_int = 1 << 3; + pub const ATIME : libc::c_int = 1 << 4; + pub const MTIME : libc::c_int = 1 << 5; + pub const ATIME_NOW : libc::c_int = 1 << 7; + pub const MTIME_NOW : libc::c_int = 1 << 8; + pub const CTIME : libc::c_int = 1 << 10; +} + +/// State of ReplyBuf after last add_entry call +#[must_use] +pub enum ReplyBufState { + /// Entry was successfully added to ReplyBuf + Ok, + /// Entry did not fit into ReplyBuf, was not added + Full, +} + +impl ReplyBufState { + #[inline] + pub fn is_full(self) -> bool { + match self { + ReplyBufState::Full => true, + _ => false, + } + } +} + +/// Used to correctly fill and reply the buffer for the readdirplus callback +pub struct ReplyBuf { + /// internal buffer holding the binary data + buffer: Vec, + /// offset up to which the buffer is filled already + filled: usize, + /// fuse request the buffer is used to reply to + request: Request, +} + +impl std::fmt::Debug for ReplyBuf { + fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result { + Ok(()) + } +} + +impl ReplyBuf { + /// Create a new empty `ReplyBuf` of `size` with element counting index at `next`. + pub fn new(request: Request, size: usize) -> Self { + let mut buffer = Vec::with_capacity(size); + unsafe { + buffer.set_len(size); + } + Self { + buffer, + filled: 0, + request, + } + } + + /// Send the reply with what we have buffered so far. + pub fn reply(mut self) -> io::Result<()> { + let rc = unsafe { + let ptr = self.buffer.as_mut_ptr() as *mut c_char; + fuse_reply_buf(self.request, ptr, self.filled) + }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::from_raw_os_error(-rc)) + } + } + + fn after_add(&mut self, entry_size: usize) -> ReplyBufState { + let filled = self.filled + entry_size; + + if filled > self.buffer.len() { + ReplyBufState::Full + } else { + self.filled = filled; + ReplyBufState::Ok + } + } + + pub fn add_readdir_plus( + &mut self, + name: &CStr, + attr: &EntryParam, + next: isize, + ) -> ReplyBufState { + let size = unsafe { + let buffer = &mut self.buffer[self.filled..]; + fuse_add_direntry_plus( + self.request, + buffer.as_mut_ptr() as *mut c_char, + buffer.len(), + name.as_ptr(), + Some(attr), + next as c_int, + ) as usize + }; + self.after_add(size) + } + + pub fn add_readdir(&mut self, name: &CStr, attr: &libc::stat, next: isize) -> ReplyBufState { + let size = unsafe { + let buffer = &mut self.buffer[self.filled..]; + fuse_add_direntry( + self.request, + buffer.as_mut_ptr() as *mut c_char, + buffer.len(), + name.as_ptr(), + Some(attr), + next as c_int, + ) as usize + }; + self.after_add(size) + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..c77c210 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,46 @@ +//! Some helpers. + +use std::fmt; + +/// Helper for `Debug` derives. +#[derive(Clone)] +pub struct Stat { + stat: libc::stat, +} + +impl From for Stat { + fn from(stat: libc::stat) -> Self { + Self { stat } + } +} + +impl std::ops::Deref for Stat { + type Target = libc::stat; + + fn deref(&self) -> &Self::Target { + &self.stat + } +} + +impl std::ops::DerefMut for Stat { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.stat + } +} + +impl fmt::Debug for Stat { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + // don't care much about more fields than these: + fmt.debug_struct("stat") + .field("st_ino", &self.stat.st_ino) + .field("st_mode", &self.stat.st_mode) + .field("st_uid", &self.stat.st_uid) + .field("st_gid", &self.stat.st_gid) + .field("st_rdev", &self.stat.st_rdev) + .field("st_size", &self.stat.st_size) + .field("st_ctime", &self.stat.st_ctime) + .field("st_mtime", &self.stat.st_mtime) + .field("st_atime", &self.stat.st_atime) + .finish() + } +}