]> git.proxmox.com Git - proxmox-fuse.git/commitdiff
import
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Thu, 7 May 2020 08:27:08 +0000 (10:27 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 20 May 2020 08:04:23 +0000 (10:04 +0200)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
13 files changed:
.cargo/config [new file with mode: 0644]
.gitignore [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
examples/tmpfs/block_file.rs [new file with mode: 0644]
examples/tmpfs/fs.rs [new file with mode: 0644]
examples/tmpfs/macros.rs [new file with mode: 0644]
examples/tmpfs/main.rs [new file with mode: 0644]
src/fuse_fd.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/requests.rs [new file with mode: 0644]
src/session.rs [new file with mode: 0644]
src/sys.rs [new file with mode: 0644]
src/util.rs [new file with mode: 0644]

diff --git a/.cargo/config b/.cargo/config
new file mode 100644 (file)
index 0000000..3b5b6e4
--- /dev/null
@@ -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 (file)
index 0000000..6a569f5
--- /dev/null
@@ -0,0 +1,3 @@
+/target
+Cargo.lock
+test
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..45db99a
--- /dev/null
@@ -0,0 +1,16 @@
+[package]
+name = "proxmox-fuse"
+version = "0.1.0"
+authors = ["Wolfgang Bumiller <w.bumiller@proxmox.com>"]
+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 (file)
index 0000000..7327983
--- /dev/null
@@ -0,0 +1,241 @@
+//! Implements "block" based sparse vector.
+
+use std::io;
+
+pub struct Extent {
+    offset: u64,
+    data: Vec<u8>,
+}
+
+enum SearchBias {
+    Low,
+    High,
+}
+
+pub struct BlockFile {
+    extents: Vec<Extent>,
+    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<usize, isize> {
+        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<usize> {
+        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 (file)
index 0000000..411d7de
--- /dev/null
@@ -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<Vec<Option<Box<FsEntry>>>>,
+    free_inodes: Mutex<Vec<u64>>,
+}
+
+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<InodeLookup> {
+        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<InodeLookup> {
+        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<InodeLookup> {
+        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<InodeLookup> {
+        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<InodeLookup> {
+        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<usize> {
+        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<libc::stat>,
+    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<BTreeMap<OsString, u64>>,
+}
+
+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<BlockFile>,
+}
+
+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<u64> {
+        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<usize> {
+        self.data.read().unwrap().read(data, offset)
+    }
+}
diff --git a/examples/tmpfs/macros.rs b/examples/tmpfs/macros.rs
new file mode 100644 (file)
index 0000000..dff079a
--- /dev/null
@@ -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 (file)
index 0000000..d135ed0
--- /dev/null
@@ -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::<io::Error>() {
+        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<fs::InodeLookup<'a>, 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 (file)
index 0000000..68d9e79
--- /dev/null
@@ -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<Self> {
+        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 (file)
index 0000000..df509ec
--- /dev/null
@@ -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 (file)
index 0000000..13f8079
--- /dev/null
@@ -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<F, E>(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::<io::Error>() => {
+    //                 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<dyn> 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<RequestGuard>,
+    pub inode: u64,
+    pub offset: i64,
+
+    reply_buffer: Option<sys::ReplyBuf>,
+}
+
+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<ReplyBufState> {
+        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<RequestGuard>,
+    pub inode: u64,
+    pub offset: i64,
+
+    reply_buffer: Option<sys::ReplyBuf>,
+}
+
+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<ReplyBufState> {
+        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<u64>,
+    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<libc::mode_t> {
+        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<libc::uid_t> {
+        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<libc::gid_t> {
+        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<u64> {
+        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<SetTime> {
+        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<SetTime> {
+        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<Duration> {
+        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<sys::FuseBuf>,
+}
+
+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<u8>,
+}
+
+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<u8>) -> Result<(), Vec<u8>> {
+        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 (file)
index 0000000..9076a4b
--- /dev/null
@@ -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<VecDeque<Request>>,
+    fbuf: Arc<sys::FuseBuf>,
+}
+
+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<CString>,
+    has_debug: bool,
+    operations: sys::Operations,
+}
+
+impl FuseSessionBuilder {
+    pub fn options(self, option: &str) -> Result<Self, Error> {
+        Ok(self.options_c(
+            CString::new(option).map_err(|err| format_err!("bad option string: {}", err))?,
+        ))
+    }
+
+    pub fn options_os(self, option: &OsStr) -> Result<Self, Error> {
+        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<FuseSession, Error> {
+        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<Box<FuseData>>,
+    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<Fuse, Error> {
+        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<libc::c_void>);
+
+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<FuseData>,
+    fuse_fd: PollEvented<FuseFd>,
+}
+
+// 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<FuseSessionBuilder, Error> {
+        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<Request>;
+
+    fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
+        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 (file)
index 0000000..c5d4485
--- /dev/null
@@ -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<extern fn(userdata: MutPtr)>,
+        pub destroy:         Option<extern fn(userdata: MutPtr)>,
+        pub lookup:          Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
+        pub forget:          Option<extern fn(req: Request, inode: u64, nlookup: u64)>,
+        pub getattr:         Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo)>,
+        pub setattr:         Option<extern fn(req: Request, inode: u64, attr: *const libc::stat, to_set: c_int, file_info: *const FuseFileInfo)>,
+        pub readlink:        Option<extern fn(req: Request, inode: u64)>,
+        pub mknod:           Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: libc::mode_t, rdev: libc::dev_t)>,
+        pub mkdir:           Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: libc::mode_t)>,
+        pub unlink:          Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
+        pub rmdir:           Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
+        pub symlink:         Option<extern fn(req: Request, link: StrPtr, parent: u64, name: StrPtr)>,
+        pub rename:          Option<extern fn(req: Request, parent: u64, name: StrPtr, newparent: u64, newname: StrPtr, flags: c_int)>,
+        pub link:            Option<extern fn(req: Request, inode: u64, newparent: u64, newname: StrPtr)>,
+        pub open:            Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo)>,
+        pub read:            Option<extern fn(req: Request, inode: u64, size: size_t, offset: libc::off_t, file_info: *const FuseFileInfo)>,
+        pub write:           Option<extern fn(req: Request, inode: u64, buffer: *const u8, size: size_t, offset: libc::off_t, file_info: *const FuseFileInfo)>,
+        pub flush:           Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo)>,
+        pub release:         Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo)>,
+        pub fsync:           Option<extern fn(req: Request, inode: u64, datasync: c_int, file_info: *const FuseFileInfo)>,
+        pub opendir:         Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo)>,
+        pub readdir:         Option<extern fn(req: Request, inode: u64, size: size_t, offset: off_t, file_info: *const FuseFileInfo)>,
+        pub releasedir:      Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo)>,
+        pub fsyncdir:        Option<extern fn(req: Request, inode: u64, datasync: c_int, file_info: *const FuseFileInfo)>,
+        pub statfs:          Option<extern fn(req: Request, inode: u64)>,
+        pub setxattr:        Option<extern fn(req: Request, inode: u64, name: StrPtr, value: StrPtr, size: size_t, flags: c_int)>,
+        pub getxattr:        Option<extern fn(req: Request, inode: u64, name: StrPtr, size: size_t)>,
+        pub listxattr:       Option<extern fn(req: Request, inode: u64, size: size_t)>,
+        pub removexattr:     Option<extern fn(req: Request, inode: u64, name: StrPtr)>,
+        pub access:          Option<extern fn(req: Request, inode: u64, mask: i32)>,
+        pub create:          Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: libc::mode_t, file_info: *const FuseFileInfo)>,
+        pub getlk:           Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo, lock: MutPtr)>,
+        pub setlk:           Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo, lock: MutPtr, sleep: c_int)>,
+        pub bmap:            Option<extern fn(req: Request, inode: u64, blocksize: size_t, idx: u64)>,
+        pub ioctl:           Option<extern fn(req: Request, inode: u64, cmd: c_int, arg: MutPtr, file_info: *const FuseFileInfo, flags: c_int, in_buf: ConstPtr, in_bufsz: size_t, out_bufsz: size_t)>,
+        pub poll:            Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo, pollhandle: MutPtr)>,
+        pub write_buf:       Option<extern fn(req: Request, inode: u64, bufv: MutPtr, offset: libc::off_t, file_info: *const FuseFileInfo)>,
+        pub retrieve_reply:  Option<extern fn(req: Request, cookie: ConstPtr, inode: u64, offset: libc::off_t, bufv: MutPtr)>,
+        pub forget_multi:    Option<extern fn(req: Request, count: size_t, forgets: MutPtr)>,
+        pub flock:           Option<extern fn(req: Request, inode: u64, file_info: *const FuseFileInfo, op: c_int)>,
+        pub fallocate:       Option<extern fn(req: Request, inode: u64, mode: c_int, offset: libc::off_t, length: libc::off_t, file_info: *const FuseFileInfo)>,
+        pub readdirplus:     Option<extern fn(req: Request, inode: u64, size: size_t, offset: off_t, file_info: *const FuseFileInfo)>,
+        pub copy_file_range: Option<extern fn(req: Request, ino_in: u64, off_in: libc::off_t, fi_in: *const FuseFileInfo, ino_out: u64, off_out: libc::off_t, fi_out: *const FuseFileInfo, len: size_t, flags: c_int)>,
+    }
+}
+
+/// 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<u8>,
+    /// 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 (file)
index 0000000..c77c210
--- /dev/null
@@ -0,0 +1,46 @@
+//! Some helpers.
+
+use std::fmt;
+
+/// Helper for `Debug` derives.
+#[derive(Clone)]
+pub struct Stat {
+    stat: libc::stat,
+}
+
+impl From<libc::stat> 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()
+    }
+}