├── .gitignore ├── .rustfmt.toml ├── src ├── raw │ ├── connection │ │ ├── mod.rs │ │ ├── async_io.rs │ │ └── tokio.rs │ ├── flags.rs │ ├── request.rs │ ├── mod.rs │ ├── reply.rs │ └── filesystem.rs ├── path │ ├── inode_generator.rs │ ├── mod.rs │ ├── session.rs │ ├── reply.rs │ └── path_filesystem.rs ├── errno.rs ├── helper.rs ├── lib.rs ├── notify.rs └── mount_options.rs ├── README.md ├── examples ├── Cargo.toml └── src │ ├── helloworld │ └── main.rs │ ├── poll │ └── main.rs │ └── memfs │ └── main.rs ├── LICENSE ├── Cargo.toml └── .cirrus.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .idea -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | group_imports = "StdExternalCrate" 3 | -------------------------------------------------------------------------------- /src/raw/connection/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | #[cfg(all(not(feature = "tokio-runtime"), feature = "async-io-runtime"))] 4 | pub use async_io::FuseConnection; 5 | #[cfg(all(not(feature = "async-io-runtime"), feature = "tokio-runtime"))] 6 | pub use tokio::FuseConnection; 7 | 8 | #[cfg(feature = "async-io-runtime")] 9 | mod async_io; 10 | #[cfg(feature = "tokio-runtime")] 11 | mod tokio; 12 | 13 | pub(crate) type CompleteIoResult = (T, io::Result); 14 | -------------------------------------------------------------------------------- /src/raw/flags.rs: -------------------------------------------------------------------------------- 1 | //! request flags. 2 | 3 | pub use crate::raw::abi::FUSE_IOCTL_32BIT; 4 | pub use crate::raw::abi::FUSE_IOCTL_COMPAT; 5 | pub use crate::raw::abi::FUSE_IOCTL_DIR; 6 | pub use crate::raw::abi::FUSE_IOCTL_MAX_IOV; 7 | pub use crate::raw::abi::FUSE_IOCTL_RETRY; 8 | pub use crate::raw::abi::FUSE_IOCTL_UNRESTRICTED; 9 | pub use crate::raw::abi::FUSE_POLL_SCHEDULE_NOTIFY; 10 | pub use crate::raw::abi::FUSE_READ_LOCKOWNER; 11 | pub use crate::raw::abi::FUSE_WRITE_CACHE; 12 | pub use crate::raw::abi::FUSE_WRITE_LOCKOWNER; 13 | -------------------------------------------------------------------------------- /src/path/inode_generator.rs: -------------------------------------------------------------------------------- 1 | use slab::Slab; 2 | 3 | use crate::Inode; 4 | 5 | #[derive(Debug)] 6 | pub struct InodeGenerator { 7 | slab: Slab<()>, 8 | } 9 | 10 | impl InodeGenerator { 11 | pub fn new() -> Self { 12 | let mut slab = Slab::new(); 13 | // drop 0 key 14 | slab.insert(()); 15 | 16 | Self { slab } 17 | } 18 | 19 | pub fn allocate_inode(&mut self) -> Inode { 20 | self.slab.insert(()) as _ 21 | } 22 | 23 | pub fn release_inode(&mut self, inode: Inode) { 24 | if self.slab.contains(inode as _) { 25 | self.slab.remove(inode as _); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/raw/request.rs: -------------------------------------------------------------------------------- 1 | use crate::raw::abi::fuse_in_header; 2 | 3 | #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] 4 | /// Request data 5 | pub struct Request { 6 | /// the unique identifier of this request. 7 | pub unique: u64, 8 | /// the uid of this request. 9 | pub uid: u32, 10 | /// the gid of this request. 11 | pub gid: u32, 12 | /// the pid of this request. 13 | pub pid: u32, 14 | } 15 | 16 | impl From<&fuse_in_header> for Request { 17 | fn from(header: &fuse_in_header) -> Self { 18 | Self { 19 | unique: header.unique, 20 | uid: header.uid, 21 | gid: header.gid, 22 | pid: header.pid, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/path/mod.rs: -------------------------------------------------------------------------------- 1 | //! path based 2 | //! 3 | //! it is recommend to use path based [`PathFilesystem`] first, [`PathFilesystem`] is more simple 4 | //! than inode based [`Filesystem`][crate::raw::Filesystem]. However if you want to control the 5 | //! inode or do the path<->inode map on yourself, use [`Filesystem`][crate::raw::Filesystem]. 6 | 7 | pub use path_filesystem::PathFilesystem; 8 | pub use session::Session; 9 | 10 | pub use crate::raw::Request; 11 | 12 | mod inode_generator; 13 | mod inode_path_bridge; 14 | mod path_filesystem; 15 | pub mod reply; 16 | mod session; 17 | 18 | pub mod prelude { 19 | pub use super::reply::FileAttr; 20 | pub use super::reply::*; 21 | pub use super::PathFilesystem; 22 | pub use super::Request; 23 | pub use super::Session; 24 | pub use crate::notify::Notify; 25 | pub use crate::FileType; 26 | pub use crate::SetAttr; 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuse3 2 | an async version fuse library for rust 3 | 4 | [![Cargo](https://img.shields.io/crates/v/fuse3.svg)]( 5 | https://crates.io/crates/fuse3) 6 | [![Documentation](https://docs.rs/fuse3/badge.svg)]( 7 | https://docs.rs/fuse3) 8 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)]( 9 | https://github.com/Sherlock-Holo/fuse3) 10 | 11 | ## feature 12 | 13 | - support unprivileged mode by using `fusermount3` 14 | - support `readdirplus` to improve read dir performance 15 | - support posix file lock 16 | - support handles the `O_TRUNC` open flag 17 | - support async direct IO 18 | - support enable `no_open` and `no_open_dir` option 19 | 20 | ## still not support 21 | 22 | - `ioctl` implement 23 | - fuseblk mode 24 | 25 | ## unstable 26 | 27 | - `poll` 28 | - `notify_reply` 29 | 30 | ## Supported Rust Versions 31 | 32 | The minimum supported version is 1.75. 33 | 34 | ## License 35 | 36 | MIT 37 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.1.0" 4 | authors = ["Sherlock Holo "] 5 | edition = "2021" 6 | publish = false 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [[bin]] 11 | name = "helloworld" 12 | path = "src/helloworld/main.rs" 13 | 14 | [[bin]] 15 | name = "memfs" 16 | path = "src/memfs/main.rs" 17 | 18 | [[bin]] 19 | name = "poll" 20 | path = "src/poll/main.rs" 21 | 22 | [[bin]] 23 | name = "path_memfs" 24 | path = "src/path_memfs/main.rs" 25 | 26 | [dependencies] 27 | fuse3 = { path = "../", features = ["tokio-runtime", "unprivileged"] } 28 | libc = "0.2.158" 29 | tokio = { version = "1.36", features = ["macros", "rt", "time", "signal"] } 30 | futures-util = "0.3.30" 31 | mio = { version = "0.8.11", features = ["os-poll"] } 32 | tempfile = "3.10" 33 | bytes = "1.5" 34 | tracing = "0.1.40" 35 | tracing-subscriber = "0.3" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sherlock Holo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/raw/mod.rs: -------------------------------------------------------------------------------- 1 | //! inode based 2 | //! 3 | //! it is not recommend to use this inode based [`Filesystem`] first, you need to handle inode 4 | //! allocate, recycle and sometimes map to the path, [`PathFilesystem`][crate::path::PathFilesystem] 5 | //! helps you do those jobs so you can pay more attention to your filesystem design. However if you 6 | //! want to control the inode or do the path<->inode map on yourself, [`Filesystem`] is the only one 7 | //! choose. 8 | 9 | use bytes::Bytes; 10 | pub use filesystem::Filesystem; 11 | use futures_util::future::Either; 12 | pub use request::Request; 13 | #[cfg(any(feature = "async-io-runtime", feature = "tokio-runtime"))] 14 | pub use session::{MountHandle, Session}; 15 | 16 | pub(crate) type FuseData = Either, (Vec, Bytes)>; 17 | 18 | pub(crate) mod abi; 19 | mod connection; 20 | mod filesystem; 21 | pub mod flags; 22 | pub mod reply; 23 | mod request; 24 | pub(crate) mod session; 25 | 26 | pub mod prelude { 27 | pub use super::reply::FileAttr; 28 | pub use super::reply::*; 29 | pub use super::Filesystem; 30 | pub use super::Request; 31 | pub use super::Session; 32 | pub use crate::notify::Notify; 33 | pub use crate::FileType; 34 | pub use crate::SetAttr; 35 | } 36 | -------------------------------------------------------------------------------- /src/path/session.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::Path; 3 | 4 | use crate::path::inode_path_bridge::InodePathBridge; 5 | use crate::path::path_filesystem::PathFilesystem; 6 | use crate::raw; 7 | use crate::MountOptions; 8 | 9 | #[cfg(any(feature = "async-io-runtime", feature = "tokio-runtime"))] 10 | #[derive(Debug)] 11 | /// fuse filesystem session, path based. 12 | pub struct Session { 13 | mount_options: MountOptions, 14 | } 15 | 16 | #[cfg(any(feature = "async-io-runtime", feature = "tokio-runtime"))] 17 | impl Session { 18 | /// new a fuse filesystem session. 19 | pub fn new(mount_options: MountOptions) -> Self { 20 | Self { mount_options } 21 | } 22 | 23 | #[cfg(feature = "unprivileged")] 24 | /// mount the filesystem without root permission. 25 | pub async fn mount_with_unprivileged( 26 | self, 27 | fs: FS, 28 | mount_path: P, 29 | ) -> io::Result 30 | where 31 | P: AsRef, 32 | FS: PathFilesystem + Send + Sync + 'static, 33 | { 34 | let bridge = InodePathBridge::new(fs); 35 | 36 | raw::Session::new(self.mount_options) 37 | .mount_with_unprivileged(bridge, mount_path) 38 | .await 39 | } 40 | 41 | /// mount the filesystem with root permission. 42 | pub async fn mount(self, fs: FS, mount_path: P) -> io::Result 43 | where 44 | P: AsRef, 45 | FS: PathFilesystem + Send + Sync + 'static, 46 | { 47 | let bridge = InodePathBridge::new(fs); 48 | 49 | raw::Session::new(self.mount_options) 50 | .mount(bridge, mount_path) 51 | .await 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/errno.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{self, Display, Formatter}; 3 | use std::io::Error as IoError; 4 | use std::os::raw::c_int; 5 | 6 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 7 | /// linux errno wrap. 8 | pub struct Errno(c_int); 9 | 10 | impl From for c_int { 11 | fn from(errno: Errno) -> Self { 12 | -errno.0 13 | } 14 | } 15 | 16 | impl From for Errno { 17 | fn from(errno: c_int) -> Self { 18 | Self(errno) 19 | } 20 | } 21 | 22 | /// When raw os error is undefined, will return Errno(libc::EIO) 23 | impl From for Errno { 24 | fn from(err: IoError) -> Self { 25 | if let Some(errno) = err.raw_os_error() { 26 | Self(errno) 27 | } else { 28 | Self(libc::EIO) 29 | } 30 | } 31 | } 32 | 33 | impl From for IoError { 34 | fn from(errno: Errno) -> Self { 35 | IoError::from_raw_os_error(errno.0) 36 | } 37 | } 38 | 39 | impl Display for Errno { 40 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 41 | write!(f, "errno is {}", self.0) 42 | } 43 | } 44 | 45 | impl Errno { 46 | pub fn new_not_exist() -> Self { 47 | Self(libc::ENOENT) 48 | } 49 | 50 | pub fn new_exist() -> Self { 51 | Self(libc::EEXIST) 52 | } 53 | 54 | pub fn new_is_dir() -> Self { 55 | Self(libc::EISDIR) 56 | } 57 | 58 | pub fn new_is_not_dir() -> Self { 59 | Self(libc::ENOTDIR) 60 | } 61 | 62 | pub fn is_not_exist(&self) -> bool { 63 | self.0 == libc::ENOENT 64 | } 65 | 66 | pub fn is_exist(&self) -> bool { 67 | self.0 == libc::EEXIST 68 | } 69 | 70 | pub fn is_dir(&self) -> bool { 71 | self.0 == libc::EISDIR 72 | } 73 | 74 | pub fn is_not_dir(&self) -> bool { 75 | self.0 == libc::ENOTDIR 76 | } 77 | } 78 | 79 | impl Error for Errno {} 80 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuse3" 3 | version = "0.8.1" 4 | authors = ["Sherlock Holo "] 5 | edition = "2021" 6 | readme = "README.md" 7 | keywords = ["fuse", "filesystem", "system", "bindings"] 8 | categories = ["api-bindings", "filesystem"] 9 | license = "MIT" 10 | repository = "https://github.com/Sherlock-Holo/fuse3" 11 | description = "FUSE user-space library async version implementation." 12 | rust-version = "1.77" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [workspace] 17 | members = [".", "examples"] 18 | 19 | [features] 20 | tokio-runtime = ["dep:tokio"] 21 | async-io-runtime = ["dep:async-fs", "dep:async-global-executor", "dep:async-lock", "dep:async-io", "dep:async-process", "futures-util/io"] 22 | file-lock = [] 23 | unprivileged = ["nix/socket", "dep:which"] 24 | 25 | [dependencies] 26 | async-fs = { version = "2.1.1", optional = true } 27 | async-global-executor = { version = "2.4.1", optional = true } 28 | async-lock = { version = "3.3.0", optional = true } 29 | async-notify = "0.3" 30 | async-io = { version = "2.3.1", optional = true } 31 | async-process = { version = "2.1.0", optional = true } 32 | bincode = "1.3.3" 33 | bytes = "1.5" 34 | futures-channel = { version = "0.3.30", features = ["sink"] } 35 | futures-util = { version = "0.3.30", features = ["sink"] } 36 | libc = "0.2.158" 37 | nix = { version = "0.29.0", default-features = false, features = ["fs", "mount", "user"] } 38 | serde = { version = "1.0.196", features = ["derive"] } 39 | slab = "0.4.9" 40 | tracing = "0.1.40" 41 | trait-make = "0.1" 42 | which = { version = "6", optional = true } 43 | 44 | [dependencies.tokio] 45 | version = "1.36" 46 | features = ["fs", "rt", "sync", "net", "macros", "process", "time"] 47 | optional = true 48 | 49 | [package.metadata.docs.rs] 50 | rustdoc-args = ["--cfg", "docsrs"] 51 | features = ["file-lock", "unprivileged", "tokio-runtime"] 52 | targets = [ 53 | "i686-unknown-freebsd", 54 | "i686-unknown-linux-gnu", 55 | "x86_64-unknown-freebsd", 56 | "x86_64-unknown-linux-gnu", 57 | ] 58 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | build: &BUILD 2 | cargo_cache: 3 | folder: $HOME/.cargo/registry 4 | fingerprint_script: cat Cargo.lock || echo "" 5 | build_script: 6 | - . $HOME/.cargo/env || true 7 | - rustup component add clippy 8 | - cargo check --all-targets --features=tokio-runtime,file-lock,unprivileged 9 | - cargo check --all-targets --features=async-io-runtime,file-lock,unprivileged 10 | # - RUSTDOCFLAGS="--cfg docsrs" cargo doc --features=file-lock,unprivileged,tokio-runtime # disable until doc_cfg and doc_auto_cfg are stable 11 | - cargo doc --features=file-lock,unprivileged,tokio-runtime 12 | - cargo clippy --all-targets --features=tokio-runtime,file-lock,unprivileged 13 | - cargo clippy --all-targets --features=async-io-runtime,file-lock,unprivileged 14 | before_cache_script: rm -rf $HOME/.cargo/registry/index 15 | 16 | 17 | task: 18 | name: FreeBSD 19 | freebsd_instance: 20 | image: freebsd-14-3-release-amd64-ufs 21 | setup_script: 22 | - fetch https://sh.rustup.rs -o rustup.sh 23 | - sh rustup.sh -y --profile=minimal 24 | - . $HOME/.cargo/env 25 | << : *BUILD 26 | 27 | task: 28 | name: MacOS 29 | macos_instance: 30 | image: ghcr.io/cirruslabs/macos-sonoma-base:latest 31 | setup_script: 32 | - curl https://sh.rustup.rs -o rustup.sh 33 | - sh rustup.sh -y --profile=minimal 34 | - . $HOME/.cargo/env 35 | << : *BUILD 36 | 37 | task: 38 | name: Linux 39 | container: 40 | image: rust:latest 41 | << : *BUILD 42 | 43 | minver_task: 44 | depends_on: 45 | - FreeBSD 46 | - Linux 47 | - MacOS 48 | freebsd_instance: 49 | image: freebsd-14-3-release-amd64-ufs 50 | setup_script: 51 | - fetch https://sh.rustup.rs -o rustup.sh 52 | - sh rustup.sh -y --default-toolchain nightly --profile=minimal 53 | - . $HOME/.cargo/env 54 | test_script: 55 | - . $HOME/.cargo/env || true 56 | - cargo update -Zdirect-minimal-versions 57 | - cargo check --all-targets --features=tokio-runtime,file-lock,unprivileged 58 | - cargo check --all-targets --features=async-io-runtime,file-lock,unprivileged 59 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use bincode::{DefaultOptions, Options}; 4 | use nix::sys::stat::mode_t; 5 | 6 | use crate::FileType; 7 | 8 | pub trait Apply: Sized { 9 | fn apply(mut self, f: F) -> Self 10 | where 11 | F: FnOnce(&mut Self), 12 | { 13 | f(&mut self); 14 | self 15 | } 16 | } 17 | 18 | impl Apply for T {} 19 | 20 | #[inline] 21 | pub fn get_first_null_position(data: impl AsRef<[u8]>) -> Option { 22 | data.as_ref().iter().position(|char| *char == 0) 23 | } 24 | 25 | // Some platforms like Linux x86_64 have mode_t = u32, and lint warns of a trivial_numeric_casts. 26 | // But others like macOS x86_64 have mode_t = u16, requiring a typecast. So, just silence lint. 27 | #[cfg(target_os = "linux")] 28 | #[allow(trivial_numeric_casts)] 29 | /// returns the mode for a given file kind and permission 30 | pub const fn mode_from_kind_and_perm(kind: FileType, perm: u16) -> u32 { 31 | kind.const_into_mode_t() | perm as mode_t 32 | } 33 | 34 | // Some platforms like Linux x86_64 have mode_t = u32, and lint warns of a trivial_numeric_casts. 35 | // But others like macOS x86_64 have mode_t = u16, requiring a typecast. So, just silence lint. 36 | #[cfg(all( 37 | not(target_os = "linux"), 38 | any(target_os = "freebsd", target_os = "macos") 39 | ))] 40 | #[allow(trivial_numeric_casts)] 41 | /// returns the mode for a given file kind and permission 42 | pub const fn mode_from_kind_and_perm(kind: FileType, perm: u16) -> u32 { 43 | (kind.const_into_mode_t() | perm as mode_t) as u32 44 | } 45 | 46 | /// returns the permission for a given file kind and mode 47 | #[allow(clippy::unnecessary_cast)] // Not unnecessary on all platforms. 48 | pub const fn perm_from_mode_and_kind(kind: FileType, mode: mode_t) -> u16 { 49 | (mode ^ kind.const_into_mode_t()) as u16 50 | } 51 | 52 | #[inline] 53 | pub const fn get_padding_size(dir_entry_size: usize) -> usize { 54 | // 64bit align 55 | let entry_size = (dir_entry_size + mem::size_of::() - 1) & !(mem::size_of::() - 1); 56 | 57 | entry_size - dir_entry_size 58 | } 59 | 60 | pub fn get_bincode_config() -> impl Options { 61 | DefaultOptions::new() 62 | .with_little_endian() 63 | .allow_trailing_bytes() 64 | .with_fixint_encoding() 65 | } 66 | -------------------------------------------------------------------------------- /src/path/reply.rs: -------------------------------------------------------------------------------- 1 | //! reply structures. 2 | use std::ffi::OsString; 3 | use std::num::NonZeroU32; 4 | use std::time::{Duration, SystemTime}; 5 | 6 | use futures_util::stream::Stream; 7 | 8 | #[cfg(feature = "file-lock")] 9 | pub use crate::raw::reply::ReplyLock; 10 | pub use crate::raw::reply::{ 11 | ReplyBmap, ReplyCopyFileRange, ReplyData, ReplyLSeek, ReplyOpen, ReplyPoll, ReplyStatFs, 12 | ReplyWrite, ReplyXAttr, 13 | }; 14 | use crate::{FileType, Inode, Result}; 15 | 16 | /// file attributes 17 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 18 | pub struct FileAttr { 19 | /// Size in bytes 20 | pub size: u64, 21 | /// Size in blocks 22 | pub blocks: u64, 23 | /// Time of last access 24 | pub atime: SystemTime, 25 | /// Time of last modification 26 | pub mtime: SystemTime, 27 | /// Time of last change 28 | pub ctime: SystemTime, 29 | #[cfg(target_os = "macos")] 30 | /// Time of creation (macOS only) 31 | pub crtime: SystemTime, 32 | /// Kind of file (directory, file, pipe, etc) 33 | pub kind: FileType, 34 | /// Permissions 35 | pub perm: u16, 36 | /// Number of hard links 37 | pub nlink: u32, 38 | /// User id 39 | pub uid: u32, 40 | /// Group id 41 | pub gid: u32, 42 | /// Rdev 43 | pub rdev: u32, 44 | #[cfg(target_os = "macos")] 45 | /// Flags (macOS only, see chflags(2)) 46 | pub flags: u32, 47 | pub blksize: u32, 48 | } 49 | 50 | impl From<(Inode, FileAttr)> for crate::raw::reply::FileAttr { 51 | fn from((inode, attr): (u64, FileAttr)) -> Self { 52 | crate::raw::reply::FileAttr { 53 | ino: inode, 54 | size: attr.size, 55 | blocks: attr.blocks, 56 | atime: attr.atime.into(), 57 | mtime: attr.mtime.into(), 58 | ctime: attr.ctime.into(), 59 | #[cfg(target_os = "macos")] 60 | crtime: attr.crtime.into(), 61 | kind: attr.kind, 62 | perm: attr.perm, 63 | nlink: attr.nlink, 64 | uid: attr.uid, 65 | gid: attr.gid, 66 | rdev: attr.rdev, 67 | #[cfg(target_os = "macos")] 68 | flags: attr.flags, 69 | blksize: attr.blksize, 70 | } 71 | } 72 | } 73 | 74 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 75 | /// init reply 76 | pub struct ReplyInit { 77 | /// the max write size 78 | pub max_write: NonZeroU32, 79 | } 80 | 81 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 82 | /// entry reply. 83 | pub struct ReplyEntry { 84 | /// the attribute TTL. 85 | pub ttl: Duration, 86 | /// the attribute. 87 | pub attr: FileAttr, 88 | } 89 | 90 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 91 | /// reply attr. 92 | pub struct ReplyAttr { 93 | /// the attribute TTL. 94 | pub ttl: Duration, 95 | /// the attribute. 96 | pub attr: FileAttr, 97 | } 98 | 99 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 100 | /// crate reply. 101 | pub struct ReplyCreated { 102 | /// the attribute TTL. 103 | pub ttl: Duration, 104 | /// the attribute of file. 105 | pub attr: FileAttr, 106 | /// the generation of file. 107 | pub generation: u64, 108 | /// the file handle. 109 | pub fh: u64, 110 | /// the flags. 111 | pub flags: u32, 112 | } 113 | 114 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 115 | /// directory entry. 116 | pub struct DirectoryEntry { 117 | /// entry kind. 118 | pub kind: FileType, 119 | /// entry name. 120 | pub name: OsString, 121 | /// Directory offset of the _next_ entry 122 | pub offset: i64, 123 | } 124 | 125 | /// readdir reply. 126 | pub struct ReplyDirectory>> { 127 | pub entries: S, 128 | } 129 | 130 | /*#[derive(Debug)] 131 | pub struct ReplyIoctl { 132 | pub result: i32, 133 | pub flags: u32, 134 | pub in_iovs: u32, 135 | pub out_iovs: u32, 136 | }*/ 137 | 138 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 139 | /// directory entry with attribute 140 | pub struct DirectoryEntryPlus { 141 | /// the entry kind. 142 | pub kind: FileType, 143 | /// the entry name. 144 | pub name: OsString, 145 | /// Directory offset of the _next_ entry 146 | pub offset: i64, 147 | /// the entry attribute. 148 | pub attr: FileAttr, 149 | /// the entry TTL. 150 | pub entry_ttl: Duration, 151 | /// the attribute TTL. 152 | pub attr_ttl: Duration, 153 | } 154 | 155 | /// the readdirplus reply. 156 | pub struct ReplyDirectoryPlus>> { 157 | pub entries: S, 158 | } 159 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! FUSE user-space library async version implementation. 2 | //! 3 | //! This is an improved rewrite of the FUSE user-space library to fully take advantage of Rust's 4 | //! architecture. 5 | //! 6 | //! This library doesn't depend on `libfuse`, unless enable `unprivileged` feature, this feature 7 | //! will support mount the filesystem without root permission by using `fusermount3` binary. 8 | //! 9 | //! # Features: 10 | //! 11 | //! - `file-lock`: enable POSIX file lock feature. 12 | //! - `async-io-runtime`: use [async_io](https://docs.rs/async-io) and 13 | //! [async-global-executor](https://docs.rs/async-global-executor) to drive async io and task. 14 | //! - `tokio-runtime`: use [tokio](https://docs.rs/tokio) runtime to drive async io and task. 15 | //! - `unprivileged`: allow mount filesystem without root permission by using `fusermount3`. 16 | //! 17 | //! # Notes: 18 | //! 19 | //! You must enable `async-io-runtime` or `tokio-runtime` feature. 20 | 21 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 22 | 23 | #[cfg(any( 24 | all(target_os = "linux", feature = "unprivileged"), 25 | target_os = "macos" 26 | ))] 27 | use std::io::{self, ErrorKind}; 28 | #[cfg(target_os = "macos")] 29 | use std::path::Path; 30 | #[cfg(any( 31 | all(target_os = "linux", feature = "unprivileged"), 32 | target_os = "macos" 33 | ))] 34 | use std::path::PathBuf; 35 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 36 | 37 | pub use errno::Errno; 38 | pub use helper::{mode_from_kind_and_perm, perm_from_mode_and_kind}; 39 | pub use mount_options::MountOptions; 40 | use nix::sys::stat::mode_t; 41 | use raw::abi::{ 42 | fuse_setattr_in, FATTR_ATIME, FATTR_ATIME_NOW, FATTR_CTIME, FATTR_GID, FATTR_LOCKOWNER, 43 | FATTR_MODE, FATTR_MTIME, FATTR_MTIME_NOW, FATTR_SIZE, FATTR_UID, 44 | }; 45 | #[cfg(target_os = "macos")] 46 | use raw::abi::{FATTR_BKUPTIME, FATTR_CHGTIME, FATTR_CRTIME, FATTR_FLAGS}; 47 | 48 | mod errno; 49 | mod helper; 50 | mod mount_options; 51 | pub mod notify; 52 | pub mod path; 53 | pub mod raw; 54 | 55 | /// Filesystem Inode. 56 | pub type Inode = u64; 57 | 58 | /// pre-defined Result, the Err type is [`Errno`]. 59 | pub type Result = std::result::Result; 60 | 61 | /// File types 62 | #[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] 63 | pub enum FileType { 64 | /// Named pipe (S_IFIFO) 65 | NamedPipe, 66 | /// Character device (S_IFCHR) 67 | CharDevice, 68 | /// Block device (S_IFBLK) 69 | BlockDevice, 70 | /// Directory (S_IFDIR) 71 | Directory, 72 | /// Regular file (S_IFREG) 73 | RegularFile, 74 | /// Symbolic link (S_IFLNK) 75 | Symlink, 76 | /// Unix domain socket (S_IFSOCK) 77 | Socket, 78 | } 79 | 80 | impl FileType { 81 | /// convert [`FileType`] into [`mode_t`] 82 | pub const fn const_into_mode_t(self) -> mode_t { 83 | match self { 84 | FileType::NamedPipe => libc::S_IFIFO, 85 | FileType::CharDevice => libc::S_IFCHR, 86 | FileType::BlockDevice => libc::S_IFBLK, 87 | FileType::Directory => libc::S_IFDIR, 88 | FileType::RegularFile => libc::S_IFREG, 89 | FileType::Symlink => libc::S_IFLNK, 90 | FileType::Socket => libc::S_IFSOCK, 91 | } 92 | } 93 | } 94 | 95 | impl From for mode_t { 96 | fn from(kind: FileType) -> Self { 97 | kind.const_into_mode_t() 98 | } 99 | } 100 | 101 | /// the setattr argument. 102 | #[derive(Debug, Clone, Default, Eq, PartialEq)] 103 | pub struct SetAttr { 104 | /// set file or directory mode. 105 | pub mode: Option, 106 | /// set file or directory uid. 107 | pub uid: Option, 108 | /// set file or directory gid. 109 | pub gid: Option, 110 | /// set file or directory size. 111 | pub size: Option, 112 | /// the lock_owner argument. 113 | pub lock_owner: Option, 114 | /// set file or directory atime. 115 | pub atime: Option, 116 | /// set file or directory mtime. 117 | pub mtime: Option, 118 | /// set file or directory ctime. 119 | pub ctime: Option, 120 | #[cfg(target_os = "macos")] 121 | pub crtime: Option, 122 | #[cfg(target_os = "macos")] 123 | pub chgtime: Option, 124 | #[cfg(target_os = "macos")] 125 | pub bkuptime: Option, 126 | #[cfg(target_os = "macos")] 127 | pub flags: Option, 128 | } 129 | 130 | /// Helper for constructing Timestamps from fuse_setattr_in, which sign-casts 131 | /// the seconds. 132 | macro_rules! fsai2ts { 133 | ( $secs: expr, $nsecs: expr) => { 134 | Some(Timestamp::new($secs as i64, $nsecs)) 135 | }; 136 | } 137 | 138 | impl From<&fuse_setattr_in> for SetAttr { 139 | fn from(setattr_in: &fuse_setattr_in) -> Self { 140 | let mut set_attr = Self::default(); 141 | 142 | if setattr_in.valid & FATTR_MODE > 0 { 143 | set_attr.mode = Some(setattr_in.mode as mode_t); 144 | } 145 | 146 | if setattr_in.valid & FATTR_UID > 0 { 147 | set_attr.uid = Some(setattr_in.uid); 148 | } 149 | 150 | if setattr_in.valid & FATTR_GID > 0 { 151 | set_attr.gid = Some(setattr_in.gid); 152 | } 153 | 154 | if setattr_in.valid & FATTR_SIZE > 0 { 155 | set_attr.size = Some(setattr_in.size); 156 | } 157 | 158 | if setattr_in.valid & FATTR_ATIME > 0 { 159 | set_attr.atime = fsai2ts!(setattr_in.atime, setattr_in.atimensec); 160 | } 161 | 162 | if setattr_in.valid & FATTR_ATIME_NOW > 0 { 163 | set_attr.atime = Some(SystemTime::now().into()); 164 | } 165 | 166 | if setattr_in.valid & FATTR_MTIME > 0 { 167 | set_attr.mtime = fsai2ts!(setattr_in.mtime, setattr_in.mtimensec); 168 | } 169 | 170 | if setattr_in.valid & FATTR_MTIME_NOW > 0 { 171 | set_attr.mtime = Some(SystemTime::now().into()); 172 | } 173 | 174 | if setattr_in.valid & FATTR_LOCKOWNER > 0 { 175 | set_attr.lock_owner = Some(setattr_in.lock_owner); 176 | } 177 | 178 | if setattr_in.valid & FATTR_CTIME > 0 { 179 | set_attr.ctime = fsai2ts!(setattr_in.ctime, setattr_in.ctimensec); 180 | } 181 | 182 | #[cfg(target_os = "macos")] 183 | if setattr_in.valid & FATTR_CRTIME > 0 { 184 | set_attr.ctime = fsai2ts!(setattr_in.crtime, setattr_in.crtimensec); 185 | } 186 | 187 | #[cfg(target_os = "macos")] 188 | if setattr_in.valid & FATTR_CHGTIME > 0 { 189 | set_attr.ctime = fsai2ts!(setattr_in.chgtime, setattr_in.chgtimensec); 190 | } 191 | 192 | #[cfg(target_os = "macos")] 193 | if setattr_in.valid & FATTR_BKUPTIME > 0 { 194 | set_attr.ctime = fsai2ts!(setattr_in.bkuptime, setattr_in.bkuptimensec); 195 | } 196 | 197 | #[cfg(target_os = "macos")] 198 | if setattr_in.valid & FATTR_FLAGS > 0 { 199 | set_attr.flags = Some(setattr_in.flags); 200 | } 201 | 202 | set_attr 203 | } 204 | } 205 | 206 | /// A file's timestamp, according to FUSE. 207 | /// 208 | /// Nearly the same as a `libc::timespec`, except for the width of the nsec 209 | /// field. 210 | // Could implement From for Duration, and/or libc::timespec, if desired 211 | #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] 212 | pub struct Timestamp { 213 | pub sec: i64, 214 | pub nsec: u32, 215 | } 216 | 217 | impl Timestamp { 218 | /// Create a new timestamp from its component parts. 219 | /// 220 | /// `nsec` should be less than 1_000_000_000. 221 | pub fn new(sec: i64, nsec: u32) -> Self { 222 | Timestamp { sec, nsec } 223 | } 224 | } 225 | 226 | impl From for Timestamp { 227 | fn from(t: SystemTime) -> Self { 228 | let d = t 229 | .duration_since(UNIX_EPOCH) 230 | .unwrap_or_else(|_| Duration::from_secs(0)); 231 | Timestamp { 232 | sec: d.as_secs().try_into().unwrap_or(i64::MAX), 233 | nsec: d.subsec_nanos(), 234 | } 235 | } 236 | } 237 | 238 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 239 | fn find_fusermount3() -> io::Result { 240 | which::which("fusermount3").map_err(|err| { 241 | io::Error::new( 242 | ErrorKind::Other, 243 | format!("find fusermount3 binary failed {err:?}"), 244 | ) 245 | }) 246 | } 247 | 248 | #[cfg(target_os = "macos")] 249 | fn find_macfuse_mount() -> io::Result { 250 | if Path::new("/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse").exists() { 251 | Ok(PathBuf::from( 252 | "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse", 253 | )) 254 | } else { 255 | Err(io::Error::new( 256 | ErrorKind::NotFound, 257 | "macfuse mount binary not found, Please install macfuse first.", 258 | )) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/notify.rs: -------------------------------------------------------------------------------- 1 | //! notify kernel. 2 | 3 | use std::ffi::OsString; 4 | use std::os::unix::ffi::OsStrExt; 5 | 6 | use bincode::Options; 7 | use bytes::{Buf, Bytes}; 8 | use futures_channel::mpsc::UnboundedSender; 9 | use futures_util::future::Either; 10 | use futures_util::sink::SinkExt; 11 | 12 | use crate::helper::get_bincode_config; 13 | use crate::raw::abi::{ 14 | fuse_notify_code, fuse_notify_delete_out, fuse_notify_inval_entry_out, 15 | fuse_notify_inval_inode_out, fuse_notify_poll_wakeup_out, fuse_notify_retrieve_out, 16 | fuse_notify_store_out, fuse_out_header, FUSE_NOTIFY_DELETE_OUT_SIZE, 17 | FUSE_NOTIFY_INVAL_ENTRY_OUT_SIZE, FUSE_NOTIFY_INVAL_INODE_OUT_SIZE, 18 | FUSE_NOTIFY_POLL_WAKEUP_OUT_SIZE, FUSE_NOTIFY_RETRIEVE_OUT_SIZE, FUSE_NOTIFY_STORE_OUT_SIZE, 19 | FUSE_OUT_HEADER_SIZE, 20 | }; 21 | use crate::raw::FuseData; 22 | 23 | #[derive(Debug, Clone)] 24 | /// notify kernel there are something need to handle. 25 | pub struct Notify { 26 | sender: UnboundedSender, 27 | } 28 | 29 | impl Notify { 30 | pub(crate) fn new(sender: UnboundedSender) -> Self { 31 | Self { sender } 32 | } 33 | 34 | /// notify kernel there are something need to handle. If notify failed, the `kind` will be 35 | /// return in `Err`. 36 | async fn notify(&mut self, kind: NotifyKind) -> Result<(), NotifyKind> { 37 | let data = match &kind { 38 | NotifyKind::Wakeup { kh } => { 39 | let out_header = fuse_out_header { 40 | len: (FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_POLL_WAKEUP_OUT_SIZE) as u32, 41 | error: fuse_notify_code::FUSE_POLL as i32, 42 | unique: 0, 43 | }; 44 | 45 | let wakeup_out = fuse_notify_poll_wakeup_out { kh: *kh }; 46 | 47 | let mut data = 48 | Vec::with_capacity(FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_POLL_WAKEUP_OUT_SIZE); 49 | 50 | get_bincode_config() 51 | .serialize_into(&mut data, &out_header) 52 | .expect("vec size is not enough"); 53 | get_bincode_config() 54 | .serialize_into(&mut data, &wakeup_out) 55 | .expect("vec size is not enough"); 56 | 57 | Either::Left(data) 58 | } 59 | 60 | NotifyKind::InvalidInode { inode, offset, len } => { 61 | let out_header = fuse_out_header { 62 | len: (FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_INVAL_INODE_OUT_SIZE) as u32, 63 | error: fuse_notify_code::FUSE_NOTIFY_INVAL_INODE as i32, 64 | unique: 0, 65 | }; 66 | 67 | let invalid_inode_out = fuse_notify_inval_inode_out { 68 | ino: *inode, 69 | off: *offset, 70 | len: *len, 71 | }; 72 | 73 | let mut data = 74 | Vec::with_capacity(FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_INVAL_INODE_OUT_SIZE); 75 | 76 | get_bincode_config() 77 | .serialize_into(&mut data, &out_header) 78 | .expect("vec size is not enough"); 79 | get_bincode_config() 80 | .serialize_into(&mut data, &invalid_inode_out) 81 | .expect("vec size is not enough"); 82 | 83 | Either::Left(data) 84 | } 85 | 86 | NotifyKind::InvalidEntry { parent, name } => { 87 | let out_header = fuse_out_header { 88 | len: (FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_INVAL_ENTRY_OUT_SIZE) as u32, 89 | error: fuse_notify_code::FUSE_NOTIFY_INVAL_ENTRY as i32, 90 | unique: 0, 91 | }; 92 | 93 | let invalid_entry_out = fuse_notify_inval_entry_out { 94 | parent: *parent, 95 | namelen: name.len() as _, 96 | _padding: 0, 97 | }; 98 | 99 | let mut data = 100 | Vec::with_capacity(FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_INVAL_ENTRY_OUT_SIZE); 101 | 102 | get_bincode_config() 103 | .serialize_into(&mut data, &out_header) 104 | .expect("vec size is not enough"); 105 | get_bincode_config() 106 | .serialize_into(&mut data, &invalid_entry_out) 107 | .expect("vec size is not enough"); 108 | 109 | // TODO should I add null at the end? 110 | 111 | Either::Right((data, Bytes::copy_from_slice(name.as_bytes()))) 112 | } 113 | 114 | NotifyKind::Delete { 115 | parent, 116 | child, 117 | name, 118 | } => { 119 | let out_header = fuse_out_header { 120 | len: (FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_DELETE_OUT_SIZE) as u32, 121 | error: fuse_notify_code::FUSE_NOTIFY_DELETE as i32, 122 | unique: 0, 123 | }; 124 | 125 | let delete_out = fuse_notify_delete_out { 126 | parent: *parent, 127 | child: *child, 128 | namelen: name.len() as _, 129 | _padding: 0, 130 | }; 131 | 132 | let mut data = 133 | Vec::with_capacity(FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_DELETE_OUT_SIZE); 134 | 135 | get_bincode_config() 136 | .serialize_into(&mut data, &out_header) 137 | .expect("vec size is not enough"); 138 | get_bincode_config() 139 | .serialize_into(&mut data, &delete_out) 140 | .expect("vec size is not enough"); 141 | 142 | // TODO should I add null at the end? 143 | 144 | Either::Right((data, Bytes::copy_from_slice(name.as_bytes()))) 145 | } 146 | 147 | NotifyKind::Store { 148 | inode, 149 | offset, 150 | data, 151 | } => { 152 | let out_header = fuse_out_header { 153 | len: (FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_STORE_OUT_SIZE) as u32, 154 | error: fuse_notify_code::FUSE_NOTIFY_STORE as i32, 155 | unique: 0, 156 | }; 157 | 158 | let store_out = fuse_notify_store_out { 159 | nodeid: *inode, 160 | offset: *offset, 161 | size: data.len() as _, 162 | _padding: 0, 163 | }; 164 | 165 | let mut data_buf = 166 | Vec::with_capacity(FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_STORE_OUT_SIZE); 167 | 168 | get_bincode_config() 169 | .serialize_into(&mut data_buf, &out_header) 170 | .expect("vec size is not enough"); 171 | get_bincode_config() 172 | .serialize_into(&mut data_buf, &store_out) 173 | .expect("vec size is not enough"); 174 | 175 | Either::Right((data_buf, data.clone())) 176 | } 177 | 178 | NotifyKind::Retrieve { 179 | notify_unique, 180 | inode, 181 | offset, 182 | size, 183 | } => { 184 | let out_header = fuse_out_header { 185 | len: (FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_RETRIEVE_OUT_SIZE) as u32, 186 | error: fuse_notify_code::FUSE_NOTIFY_RETRIEVE as i32, 187 | unique: 0, 188 | }; 189 | 190 | let retrieve_out = fuse_notify_retrieve_out { 191 | notify_unique: *notify_unique, 192 | nodeid: *inode, 193 | offset: *offset, 194 | size: *size, 195 | _padding: 0, 196 | }; 197 | 198 | let mut data = 199 | Vec::with_capacity(FUSE_OUT_HEADER_SIZE + FUSE_NOTIFY_RETRIEVE_OUT_SIZE); 200 | 201 | get_bincode_config() 202 | .serialize_into(&mut data, &out_header) 203 | .expect("vec size is not enough"); 204 | get_bincode_config() 205 | .serialize_into(&mut data, &retrieve_out) 206 | .expect("vec size is not enough"); 207 | 208 | Either::Left(data) 209 | } 210 | }; 211 | 212 | self.sender.send(data).await.or(Err(kind)) 213 | } 214 | 215 | /// try to notify kernel the IO is ready, kernel can wakeup the waiting program. 216 | pub async fn wakeup(mut self, kh: u64) { 217 | let _ = self.notify(NotifyKind::Wakeup { kh }).await; 218 | } 219 | 220 | /// try to notify the cache invalidation about an inode. 221 | pub async fn invalid_inode(mut self, inode: u64, offset: i64, len: i64) { 222 | let _ = self 223 | .notify(NotifyKind::InvalidInode { inode, offset, len }) 224 | .await; 225 | } 226 | 227 | /// try to notify the invalidation about a directory entry. 228 | pub async fn invalid_entry(mut self, parent: u64, name: OsString) { 229 | let _ = self.notify(NotifyKind::InvalidEntry { parent, name }).await; 230 | } 231 | 232 | /// try to notify a directory entry has been deleted. 233 | pub async fn delete(mut self, parent: u64, child: u64, name: OsString) { 234 | let _ = self 235 | .notify(NotifyKind::Delete { 236 | parent, 237 | child, 238 | name, 239 | }) 240 | .await; 241 | } 242 | 243 | /// try to push the data in an inode for updating the kernel cache. 244 | pub async fn store(mut self, inode: u64, offset: u64, mut data: impl Buf) { 245 | let _ = self 246 | .notify(NotifyKind::Store { 247 | inode, 248 | offset, 249 | data: data.copy_to_bytes(data.remaining()), 250 | }) 251 | .await; 252 | } 253 | 254 | /// try to retrieve data in an inode from the kernel cache. 255 | pub async fn retrieve(mut self, notify_unique: u64, inode: u64, offset: u64, size: u32) { 256 | let _ = self 257 | .notify(NotifyKind::Retrieve { 258 | notify_unique, 259 | inode, 260 | offset, 261 | size, 262 | }) 263 | .await; 264 | } 265 | } 266 | 267 | #[derive(Debug)] 268 | /// the kind of notify. 269 | enum NotifyKind { 270 | /// notify the IO is ready. 271 | Wakeup { kh: u64 }, 272 | 273 | // TODO need check is right or not 274 | /// notify the cache invalidation about an inode. 275 | InvalidInode { inode: u64, offset: i64, len: i64 }, 276 | 277 | /// notify the invalidation about a directory entry. 278 | InvalidEntry { parent: u64, name: OsString }, 279 | 280 | /// notify a directory entry has been deleted. 281 | Delete { 282 | parent: u64, 283 | child: u64, 284 | name: OsString, 285 | }, 286 | 287 | /// push the data in an inode for updating the kernel cache. 288 | Store { 289 | inode: u64, 290 | offset: u64, 291 | data: Bytes, 292 | }, 293 | 294 | /// retrieve data in an inode from the kernel cache. 295 | Retrieve { 296 | notify_unique: u64, 297 | inode: u64, 298 | offset: u64, 299 | size: u32, 300 | }, 301 | } 302 | -------------------------------------------------------------------------------- /examples/src/helloworld/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::num::NonZeroU32; 4 | use std::time::{Duration, SystemTime}; 5 | 6 | use bytes::Bytes; 7 | use fuse3::raw::prelude::*; 8 | use fuse3::{MountOptions, Result}; 9 | use futures_util::stream; 10 | use futures_util::stream::Stream; 11 | use tracing::Level; 12 | 13 | const CONTENT: &str = "hello world\n"; 14 | 15 | const PARENT_INODE: u64 = 1; 16 | const FILE_INODE: u64 = 2; 17 | const FILE_NAME: &str = "hello-world.txt"; 18 | const PARENT_MODE: u16 = 0o755; 19 | const FILE_MODE: u16 = 0o644; 20 | const TTL: Duration = Duration::from_secs(1); 21 | const STATFS: ReplyStatFs = ReplyStatFs { 22 | blocks: 1, 23 | bfree: 0, 24 | bavail: 0, 25 | files: 1, 26 | ffree: 0, 27 | bsize: 4096, 28 | namelen: u32::MAX, 29 | frsize: 0, 30 | }; 31 | 32 | struct HelloWorld; 33 | 34 | impl Filesystem for HelloWorld { 35 | async fn init(&self, _req: Request) -> Result { 36 | Ok(ReplyInit { 37 | max_write: NonZeroU32::new(16 * 1024).unwrap(), 38 | }) 39 | } 40 | 41 | async fn destroy(&self, _req: Request) {} 42 | 43 | async fn lookup(&self, _req: Request, parent: u64, name: &OsStr) -> Result { 44 | if parent != PARENT_INODE { 45 | return Err(libc::ENOENT.into()); 46 | } 47 | 48 | if name != OsStr::new(FILE_NAME) { 49 | return Err(libc::ENOENT.into()); 50 | } 51 | 52 | Ok(ReplyEntry { 53 | ttl: TTL, 54 | attr: FileAttr { 55 | ino: FILE_INODE, 56 | size: CONTENT.len() as u64, 57 | blocks: 0, 58 | atime: SystemTime::now().into(), 59 | mtime: SystemTime::now().into(), 60 | ctime: SystemTime::now().into(), 61 | #[cfg(target_os = "macos")] 62 | crtime: SystemTime::now().into(), 63 | kind: FileType::RegularFile, 64 | perm: FILE_MODE, 65 | nlink: 0, 66 | uid: 0, 67 | gid: 0, 68 | rdev: 0, 69 | #[cfg(target_os = "macos")] 70 | flags: 0, 71 | blksize: 0, 72 | }, 73 | generation: 0, 74 | }) 75 | } 76 | 77 | async fn getattr( 78 | &self, 79 | _req: Request, 80 | inode: u64, 81 | _fh: Option, 82 | _flags: u32, 83 | ) -> Result { 84 | if inode == PARENT_INODE { 85 | Ok(ReplyAttr { 86 | ttl: TTL, 87 | attr: FileAttr { 88 | ino: PARENT_INODE, 89 | size: 0, 90 | blocks: 0, 91 | atime: SystemTime::now().into(), 92 | mtime: SystemTime::now().into(), 93 | ctime: SystemTime::now().into(), 94 | #[cfg(target_os = "macos")] 95 | crtime: SystemTime::now().into(), 96 | kind: FileType::Directory, 97 | perm: PARENT_MODE, 98 | nlink: 0, 99 | uid: 0, 100 | gid: 0, 101 | rdev: 0, 102 | #[cfg(target_os = "macos")] 103 | flags: 0, 104 | blksize: 0, 105 | }, 106 | }) 107 | } else if inode == FILE_INODE { 108 | Ok(ReplyAttr { 109 | ttl: TTL, 110 | attr: FileAttr { 111 | ino: FILE_INODE, 112 | size: CONTENT.len() as _, 113 | blocks: 0, 114 | atime: SystemTime::now().into(), 115 | mtime: SystemTime::now().into(), 116 | ctime: SystemTime::now().into(), 117 | #[cfg(target_os = "macos")] 118 | crtime: SystemTime::now().into(), 119 | kind: FileType::RegularFile, 120 | perm: FILE_MODE, 121 | nlink: 0, 122 | uid: 0, 123 | gid: 0, 124 | rdev: 0, 125 | #[cfg(target_os = "macos")] 126 | flags: 0, 127 | blksize: 0, 128 | }, 129 | }) 130 | } else { 131 | Err(libc::ENOENT.into()) 132 | } 133 | } 134 | 135 | async fn open(&self, _req: Request, inode: u64, flags: u32) -> Result { 136 | if inode != PARENT_INODE && inode != FILE_INODE { 137 | return Err(libc::ENOENT.into()); 138 | } 139 | 140 | Ok(ReplyOpen { fh: 0, flags }) 141 | } 142 | 143 | async fn read( 144 | &self, 145 | _req: Request, 146 | inode: u64, 147 | _fh: u64, 148 | offset: u64, 149 | size: u32, 150 | ) -> Result { 151 | if inode != FILE_INODE { 152 | return Err(libc::ENOENT.into()); 153 | } 154 | 155 | if offset as usize >= CONTENT.len() { 156 | Ok(ReplyData { data: Bytes::new() }) 157 | } else { 158 | let mut data = &CONTENT.as_bytes()[offset as usize..]; 159 | 160 | if data.len() > size as usize { 161 | data = &data[..size as usize]; 162 | } 163 | 164 | Ok(ReplyData { 165 | data: Bytes::copy_from_slice(data), 166 | }) 167 | } 168 | } 169 | 170 | async fn readdir( 171 | &self, 172 | _req: Request, 173 | inode: u64, 174 | _fh: u64, 175 | offset: i64, 176 | ) -> Result> + Send + '_>> { 177 | if inode == FILE_INODE { 178 | return Err(libc::ENOTDIR.into()); 179 | } 180 | 181 | if inode != PARENT_INODE { 182 | return Err(libc::ENOENT.into()); 183 | } 184 | 185 | let entries = vec![ 186 | Ok(DirectoryEntry { 187 | inode: PARENT_INODE, 188 | kind: FileType::Directory, 189 | name: OsString::from("."), 190 | offset: 1, 191 | }), 192 | Ok(DirectoryEntry { 193 | inode: PARENT_INODE, 194 | kind: FileType::Directory, 195 | name: OsString::from(".."), 196 | offset: 2, 197 | }), 198 | Ok(DirectoryEntry { 199 | inode: FILE_INODE, 200 | kind: FileType::RegularFile, 201 | name: OsString::from(FILE_NAME), 202 | offset: 3, 203 | }), 204 | ]; 205 | 206 | Ok(ReplyDirectory { 207 | entries: stream::iter(entries.into_iter().skip(offset as usize)), 208 | }) 209 | } 210 | 211 | async fn access(&self, _req: Request, inode: u64, _mask: u32) -> Result<()> { 212 | if inode != PARENT_INODE && inode != FILE_INODE { 213 | return Err(libc::ENOENT.into()); 214 | } 215 | 216 | Ok(()) 217 | } 218 | 219 | async fn readdirplus( 220 | &self, 221 | _req: Request, 222 | parent: u64, 223 | _fh: u64, 224 | offset: u64, 225 | _lock_owner: u64, 226 | ) -> Result> + Send + '_>> 227 | { 228 | if parent == FILE_INODE { 229 | return Err(libc::ENOTDIR.into()); 230 | } 231 | 232 | if parent != PARENT_INODE { 233 | return Err(libc::ENOENT.into()); 234 | } 235 | 236 | let entries = vec![ 237 | Ok(DirectoryEntryPlus { 238 | inode: PARENT_INODE, 239 | generation: 0, 240 | kind: FileType::Directory, 241 | name: OsString::from("."), 242 | offset: 1, 243 | attr: FileAttr { 244 | ino: PARENT_INODE, 245 | size: 0, 246 | blocks: 0, 247 | atime: SystemTime::now().into(), 248 | mtime: SystemTime::now().into(), 249 | ctime: SystemTime::now().into(), 250 | #[cfg(target_os = "macos")] 251 | crtime: SystemTime::now().into(), 252 | kind: FileType::Directory, 253 | perm: PARENT_MODE, 254 | nlink: 0, 255 | uid: 0, 256 | gid: 0, 257 | rdev: 0, 258 | #[cfg(target_os = "macos")] 259 | flags: 0, 260 | blksize: 0, 261 | }, 262 | entry_ttl: TTL, 263 | attr_ttl: TTL, 264 | }), 265 | Ok(DirectoryEntryPlus { 266 | inode: PARENT_INODE, 267 | generation: 0, 268 | kind: FileType::Directory, 269 | name: OsString::from(".."), 270 | offset: 2, 271 | attr: FileAttr { 272 | ino: PARENT_INODE, 273 | size: 0, 274 | blocks: 0, 275 | atime: SystemTime::now().into(), 276 | mtime: SystemTime::now().into(), 277 | ctime: SystemTime::now().into(), 278 | #[cfg(target_os = "macos")] 279 | crtime: SystemTime::now().into(), 280 | kind: FileType::Directory, 281 | perm: PARENT_MODE, 282 | nlink: 0, 283 | uid: 0, 284 | gid: 0, 285 | rdev: 0, 286 | #[cfg(target_os = "macos")] 287 | flags: 0, 288 | blksize: 0, 289 | }, 290 | entry_ttl: TTL, 291 | attr_ttl: TTL, 292 | }), 293 | Ok(DirectoryEntryPlus { 294 | inode: FILE_INODE, 295 | generation: 0, 296 | kind: FileType::Directory, 297 | name: OsString::from(FILE_NAME), 298 | offset: 3, 299 | attr: FileAttr { 300 | ino: FILE_INODE, 301 | size: CONTENT.len() as _, 302 | blocks: 0, 303 | atime: SystemTime::now().into(), 304 | mtime: SystemTime::now().into(), 305 | ctime: SystemTime::now().into(), 306 | #[cfg(target_os = "macos")] 307 | crtime: SystemTime::now().into(), 308 | kind: FileType::RegularFile, 309 | perm: FILE_MODE, 310 | nlink: 0, 311 | uid: 0, 312 | gid: 0, 313 | rdev: 0, 314 | #[cfg(target_os = "macos")] 315 | flags: 0, 316 | blksize: 0, 317 | }, 318 | entry_ttl: TTL, 319 | attr_ttl: TTL, 320 | }), 321 | ]; 322 | 323 | Ok(ReplyDirectoryPlus { 324 | entries: stream::iter(entries.into_iter().skip(offset as usize)), 325 | }) 326 | } 327 | 328 | async fn statfs(&self, _req: Request, _inode: u64) -> Result { 329 | Ok(STATFS) 330 | } 331 | } 332 | 333 | #[tokio::main(flavor = "current_thread")] 334 | async fn main() { 335 | log_init(); 336 | 337 | let args = env::args_os().skip(1).take(1).collect::>(); 338 | 339 | let mount_path = args.first(); 340 | 341 | let uid = unsafe { libc::getuid() }; 342 | let gid = unsafe { libc::getgid() }; 343 | 344 | let mut mount_options = MountOptions::default(); 345 | mount_options.uid(uid).gid(gid).read_only(true); 346 | 347 | let mount_path = mount_path.expect("no mount point specified"); 348 | Session::new(mount_options) 349 | .mount_with_unprivileged(HelloWorld {}, mount_path) 350 | .await 351 | .unwrap() 352 | .await 353 | .unwrap() 354 | } 355 | 356 | fn log_init() { 357 | let subscriber = tracing_subscriber::fmt() 358 | .with_max_level(Level::DEBUG) 359 | .finish(); 360 | tracing::subscriber::set_global_default(subscriber).unwrap(); 361 | } 362 | -------------------------------------------------------------------------------- /src/mount_options.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | #[cfg(any(target_os = "linux", target_os = "macos"))] 3 | use std::os::unix::io::RawFd; 4 | 5 | #[cfg(target_os = "freebsd")] 6 | use nix::mount::Nmount; 7 | #[cfg(any(target_os = "macos", target_os = "linux"))] 8 | use nix::unistd; 9 | 10 | /// mount options. 11 | #[derive(Debug, Clone, Default, Eq, PartialEq)] 12 | pub struct MountOptions { 13 | // Options implemented within fuse3 14 | pub(crate) nonempty: bool, 15 | 16 | // mount options 17 | pub(crate) allow_other: bool, 18 | pub(crate) allow_root: bool, 19 | pub(crate) custom_options: Option, 20 | #[cfg(target_os = "linux")] 21 | pub(crate) dirsync: bool, 22 | pub(crate) default_permissions: bool, 23 | pub(crate) fs_name: Option, 24 | pub(crate) gid: Option, 25 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 26 | pub(crate) intr: bool, 27 | #[cfg(target_os = "linux")] 28 | pub(crate) nodiratime: bool, 29 | pub(crate) noatime: bool, 30 | #[cfg(target_os = "linux")] 31 | pub(crate) nodev: bool, 32 | pub(crate) noexec: bool, 33 | pub(crate) nosuid: bool, 34 | pub(crate) read_only: bool, 35 | #[cfg(target_os = "freebsd")] 36 | pub(crate) suiddir: bool, 37 | pub(crate) sync: bool, 38 | pub(crate) uid: Option, 39 | 40 | // Optional FUSE features 41 | pub(crate) dont_mask: bool, 42 | pub(crate) no_open_support: bool, 43 | pub(crate) no_open_dir_support: bool, 44 | pub(crate) handle_killpriv: bool, 45 | pub(crate) write_back: bool, 46 | pub(crate) force_readdir_plus: bool, 47 | 48 | // Other FUSE mount options 49 | // default 40000 50 | pub(crate) rootmode: Option, 51 | } 52 | 53 | impl MountOptions { 54 | /// set fuse filesystem mount `user_id`, default is current uid. 55 | pub fn uid(&mut self, uid: u32) -> &mut Self { 56 | self.uid.replace(uid); 57 | 58 | self 59 | } 60 | 61 | /// set fuse filesystem mount `group_id`, default is current gid. 62 | pub fn gid(&mut self, gid: u32) -> &mut Self { 63 | self.gid.replace(gid); 64 | 65 | self 66 | } 67 | 68 | /// set fuse filesystem name, default is **fuse**. 69 | pub fn fs_name(&mut self, name: impl Into) -> &mut Self { 70 | self.fs_name.replace(name.into()); 71 | 72 | self 73 | } 74 | 75 | /// set fuse filesystem `rootmode`, default is 40000. 76 | #[cfg(target_os = "linux")] 77 | pub fn rootmode(&mut self, rootmode: u32) -> &mut Self { 78 | self.rootmode.replace(rootmode); 79 | 80 | self 81 | } 82 | 83 | /// set fuse filesystem `allow_root` mount option, default is disable. 84 | pub fn allow_root(&mut self, allow_root: bool) -> &mut Self { 85 | self.allow_root = allow_root; 86 | 87 | self 88 | } 89 | 90 | /// set fuse filesystem `allow_other` mount option, default is disable. 91 | pub fn allow_other(&mut self, allow_other: bool) -> &mut Self { 92 | self.allow_other = allow_other; 93 | 94 | self 95 | } 96 | 97 | /// set fuse filesystem `ro` mount option, default is disable. 98 | pub fn read_only(&mut self, read_only: bool) -> &mut Self { 99 | self.read_only = read_only; 100 | 101 | self 102 | } 103 | 104 | /// allow fuse filesystem mount on a non-empty directory, default is not allowed. 105 | pub fn nonempty(&mut self, nonempty: bool) -> &mut Self { 106 | self.nonempty = nonempty; 107 | 108 | self 109 | } 110 | 111 | /// set fuse filesystem `default_permissions` mount option, default is disable. 112 | /// 113 | /// When `default_permissions` is set, the [`raw::access`] and [`path::access`] is useless. 114 | /// 115 | /// [`raw::access`]: crate::raw::Filesystem::access 116 | /// [`path::access`]: crate::path::PathFilesystem::access 117 | pub fn default_permissions(&mut self, default_permissions: bool) -> &mut Self { 118 | self.default_permissions = default_permissions; 119 | 120 | self 121 | } 122 | 123 | /// don't apply umask to file mode on create operations, default is disable. 124 | pub fn dont_mask(&mut self, dont_mask: bool) -> &mut Self { 125 | self.dont_mask = dont_mask; 126 | 127 | self 128 | } 129 | 130 | /// make kernel support zero-message opens, default is disable 131 | pub fn no_open_support(&mut self, no_open_support: bool) -> &mut Self { 132 | self.no_open_support = no_open_support; 133 | 134 | self 135 | } 136 | 137 | /// make kernel support zero-message opendir, default is disable 138 | pub fn no_open_dir_support(&mut self, no_open_dir_support: bool) -> &mut Self { 139 | self.no_open_dir_support = no_open_dir_support; 140 | 141 | self 142 | } 143 | 144 | /// fs handle killing `suid`/`sgid`/`cap` on `write`/`chown`/`trunc`, default is disable. 145 | pub fn handle_killpriv(&mut self, handle_killpriv: bool) -> &mut Self { 146 | self.handle_killpriv = handle_killpriv; 147 | 148 | self 149 | } 150 | 151 | /// try to set the `FUSE_WRITEBACK_CACHE` enable write back cache for buffered writes, default 152 | /// is disable. 153 | /// 154 | /// # Notes: 155 | /// 156 | /// if enable this feature, when write flags has `FUSE_WRITE_CACHE`, file handle is guessed. 157 | pub fn write_back(&mut self, write_back: bool) -> &mut Self { 158 | self.write_back = write_back; 159 | 160 | self 161 | } 162 | 163 | /// force filesystem use readdirplus only, when kernel use readdir will return `ENOSYS`, 164 | /// default is disable. 165 | /// 166 | /// # Notes: 167 | /// this may don't work with some old Linux Kernel. 168 | pub fn force_readdir_plus(&mut self, force_readdir_plus: bool) -> &mut Self { 169 | self.force_readdir_plus = force_readdir_plus; 170 | 171 | self 172 | } 173 | 174 | /// set custom options for fuse filesystem, the custom options will be used in mount 175 | pub fn custom_options(&mut self, custom_options: impl Into) -> &mut Self { 176 | self.custom_options = Some(custom_options.into()); 177 | 178 | self 179 | } 180 | 181 | #[cfg(target_os = "freebsd")] 182 | pub(crate) fn build(&self) -> Nmount<'_> { 183 | let mut nmount = Nmount::new(); 184 | nmount 185 | .str_opt(c"fstype", c"fusefs") 186 | .str_opt(c"from", c"/dev/fuse"); 187 | if self.allow_other { 188 | nmount.null_opt(c"allow_other"); 189 | } 190 | if self.allow_root { 191 | nmount.null_opt(c"allow_root"); 192 | } 193 | if self.default_permissions { 194 | nmount.null_opt(c"default_permissions"); 195 | } 196 | if let Some(fs_name) = &self.fs_name { 197 | nmount.str_opt_owned(c"subtype=", fs_name.as_str()); 198 | } 199 | if self.intr { 200 | nmount.null_opt(c"intr"); 201 | } 202 | if let Some(custom_options) = self.custom_options.as_ref() { 203 | nmount.null_opt_owned(custom_options.as_os_str()); 204 | } 205 | // TODO: additional options: push_symlinks_in, max_read=, timeout= 206 | nmount 207 | } 208 | 209 | #[cfg(target_os = "linux")] 210 | pub(crate) fn build(&self, fd: RawFd) -> OsString { 211 | let mut opts = vec![ 212 | format!("fd={fd}"), 213 | format!( 214 | "user_id={}", 215 | self.uid.unwrap_or_else(|| unistd::getuid().as_raw()) 216 | ), 217 | format!( 218 | "group_id={}", 219 | self.gid.unwrap_or_else(|| unistd::getgid().as_raw()) 220 | ), 221 | format!("rootmode={}", self.rootmode.unwrap_or(40000)), 222 | ]; 223 | 224 | if self.allow_root { 225 | opts.push("allow_root".to_string()); 226 | } 227 | 228 | if self.allow_other { 229 | opts.push("allow_other".to_string()); 230 | } 231 | 232 | if self.default_permissions { 233 | opts.push("default_permissions".to_string()); 234 | } 235 | 236 | let mut options = OsString::from(opts.join(",")); 237 | 238 | if let Some(custom_options) = &self.custom_options { 239 | options.push(","); 240 | options.push(custom_options); 241 | } 242 | 243 | options 244 | } 245 | 246 | #[cfg(target_os = "macos")] 247 | pub(crate) fn build(&self) -> OsString { 248 | let mut opts = vec![String::from("-o fsname=ofs")]; 249 | 250 | if self.allow_root { 251 | opts.push("-o allow_root".to_string()); 252 | } 253 | 254 | if self.allow_other { 255 | opts.push("-o allow_other".to_string()); 256 | } 257 | 258 | let mut options = OsString::from(opts.join(" ")); 259 | 260 | if let Some(custom_options) = &self.custom_options { 261 | options.push(" "); 262 | options.push(custom_options); 263 | } 264 | 265 | options 266 | } 267 | 268 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 269 | pub(crate) fn build_with_unprivileged(&self) -> OsString { 270 | let mut opts = vec![ 271 | format!( 272 | "user_id={}", 273 | self.uid.unwrap_or_else(|| unistd::getuid().as_raw()) 274 | ), 275 | format!( 276 | "group_id={}", 277 | self.gid.unwrap_or_else(|| unistd::getgid().as_raw()) 278 | ), 279 | format!("rootmode={}", self.rootmode.unwrap_or(40000)), 280 | format!( 281 | "fsname={}", 282 | self.fs_name.as_ref().unwrap_or(&"fuse".to_string()) 283 | ), 284 | ]; 285 | 286 | if self.allow_root { 287 | opts.push("allow_root".to_string()); 288 | } 289 | 290 | if self.allow_other { 291 | opts.push("allow_other".to_string()); 292 | } 293 | 294 | if self.read_only { 295 | opts.push("ro".to_string()); 296 | } 297 | 298 | if self.default_permissions { 299 | opts.push("default_permissions".to_string()); 300 | } 301 | 302 | let mut options = OsString::from(opts.join(",")); 303 | 304 | if let Some(custom_options) = &self.custom_options { 305 | options.push(","); 306 | options.push(custom_options); 307 | } 308 | 309 | options 310 | } 311 | 312 | #[cfg(target_os = "freebsd")] 313 | pub(crate) fn flags(&self) -> nix::mount::MntFlags { 314 | use nix::mount::MntFlags; 315 | 316 | let mut flags = MntFlags::empty(); 317 | if self.noatime { 318 | flags.insert(MntFlags::MNT_NOATIME); 319 | } 320 | if self.noexec { 321 | flags.insert(MntFlags::MNT_NOEXEC); 322 | } 323 | if self.nosuid { 324 | flags.insert(MntFlags::MNT_NOSUID); 325 | } 326 | if self.read_only { 327 | flags.insert(MntFlags::MNT_RDONLY); 328 | } 329 | if self.suiddir { 330 | flags.insert(MntFlags::MNT_SUIDDIR); 331 | } 332 | if self.sync { 333 | flags.insert(MntFlags::MNT_SYNCHRONOUS); 334 | } 335 | flags 336 | } 337 | 338 | #[cfg(target_os = "macos")] 339 | pub(crate) fn flags(&self) -> nix::mount::MntFlags { 340 | use nix::mount::MntFlags; 341 | 342 | let mut flags = MntFlags::empty(); 343 | if self.noatime { 344 | flags.insert(MntFlags::MNT_NOATIME); 345 | } 346 | if self.noexec { 347 | flags.insert(MntFlags::MNT_NOEXEC); 348 | } 349 | if self.nosuid { 350 | flags.insert(MntFlags::MNT_NOSUID); 351 | } 352 | if self.read_only { 353 | flags.insert(MntFlags::MNT_RDONLY); 354 | } 355 | 356 | if self.sync { 357 | flags.insert(MntFlags::MNT_SYNCHRONOUS); 358 | } 359 | flags 360 | } 361 | 362 | #[cfg(target_os = "linux")] 363 | pub(crate) fn flags(&self) -> nix::mount::MsFlags { 364 | use nix::mount::MsFlags; 365 | 366 | let mut flags = MsFlags::empty(); 367 | if self.dirsync { 368 | flags.insert(MsFlags::MS_DIRSYNC); 369 | } 370 | if self.noatime { 371 | flags.insert(MsFlags::MS_NOATIME); 372 | } 373 | if self.nodev { 374 | flags.insert(MsFlags::MS_NODEV); 375 | } 376 | if self.nodiratime { 377 | flags.insert(MsFlags::MS_NODIRATIME); 378 | } 379 | if self.noexec { 380 | flags.insert(MsFlags::MS_NOEXEC); 381 | } 382 | if self.nosuid { 383 | flags.insert(MsFlags::MS_NOSUID); 384 | } 385 | if self.read_only { 386 | flags.insert(MsFlags::MS_RDONLY); 387 | } 388 | if self.sync { 389 | flags.insert(MsFlags::MS_SYNCHRONOUS); 390 | } 391 | flags 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/raw/reply.rs: -------------------------------------------------------------------------------- 1 | //! reply structures. 2 | use std::ffi::OsString; 3 | use std::num::NonZeroU32; 4 | use std::time::Duration; 5 | 6 | use bytes::Bytes; 7 | use futures_util::stream::Stream; 8 | 9 | use crate::helper::mode_from_kind_and_perm; 10 | use crate::raw::abi::{ 11 | fuse_attr, fuse_attr_out, fuse_bmap_out, fuse_entry_out, fuse_kstatfs, fuse_lseek_out, 12 | fuse_open_out, fuse_poll_out, fuse_statfs_out, fuse_write_out, 13 | }; 14 | #[cfg(feature = "file-lock")] 15 | use crate::raw::abi::{fuse_file_lock, fuse_lk_out}; 16 | use crate::{FileType, Result, Timestamp}; 17 | 18 | /// file attributes 19 | #[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] 20 | pub struct FileAttr { 21 | /// Inode number 22 | pub ino: u64, 23 | /// Size in bytes 24 | pub size: u64, 25 | /// Size in blocks 26 | pub blocks: u64, 27 | /// Time of last access 28 | pub atime: Timestamp, 29 | /// Time of last modification 30 | pub mtime: Timestamp, 31 | /// Time of last change 32 | pub ctime: Timestamp, 33 | #[cfg(target_os = "macos")] 34 | /// Time of creation (macOS only) 35 | pub crtime: Timestamp, 36 | /// Kind of file (directory, file, pipe, etc) 37 | pub kind: FileType, 38 | /// Permissions 39 | pub perm: u16, 40 | /// Number of hard links 41 | pub nlink: u32, 42 | /// User id 43 | pub uid: u32, 44 | /// Group id 45 | pub gid: u32, 46 | /// Rdev 47 | pub rdev: u32, 48 | #[cfg(target_os = "macos")] 49 | /// Flags (macOS only, see chflags(2)) 50 | pub flags: u32, 51 | pub blksize: u32, 52 | } 53 | 54 | impl From for fuse_attr { 55 | fn from(attr: FileAttr) -> Self { 56 | fuse_attr { 57 | ino: attr.ino, 58 | size: attr.size, 59 | blocks: attr.blocks, 60 | // NB: fuse_kernel.h defines the seconds fields as "uint64_t", but 61 | // they actually get cast to time_t (e.g. int64_t) inside the 62 | // kernel. 63 | atime: attr.atime.sec as u64, 64 | mtime: attr.mtime.sec as u64, 65 | ctime: attr.ctime.sec as u64, 66 | #[cfg(target_os = "macos")] 67 | crtime: attr.crtime.sec as u64, 68 | atimensec: attr.atime.nsec, 69 | mtimensec: attr.mtime.nsec, 70 | ctimensec: attr.ctime.nsec, 71 | #[cfg(target_os = "macos")] 72 | crtimensec: attr.crtime.nsec, 73 | mode: mode_from_kind_and_perm(attr.kind, attr.perm), 74 | nlink: attr.nlink, 75 | uid: attr.uid, 76 | gid: attr.gid, 77 | rdev: attr.rdev, 78 | blksize: attr.blksize, 79 | #[cfg(target_os = "macos")] 80 | flags: attr.flags, 81 | _padding: 0, 82 | } 83 | } 84 | } 85 | 86 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 87 | /// init reply 88 | pub struct ReplyInit { 89 | /// the max write size 90 | pub max_write: NonZeroU32, 91 | } 92 | 93 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 94 | /// entry reply. 95 | pub struct ReplyEntry { 96 | /// the attribute TTL. 97 | pub ttl: Duration, 98 | /// the attribute. 99 | pub attr: FileAttr, 100 | /// the generation. 101 | pub generation: u64, 102 | } 103 | 104 | impl From for fuse_entry_out { 105 | fn from(entry: ReplyEntry) -> Self { 106 | let attr = entry.attr; 107 | 108 | fuse_entry_out { 109 | nodeid: attr.ino, 110 | generation: entry.generation, 111 | entry_valid: entry.ttl.as_secs(), 112 | attr_valid: entry.ttl.as_secs(), 113 | entry_valid_nsec: entry.ttl.subsec_nanos(), 114 | attr_valid_nsec: entry.ttl.subsec_nanos(), 115 | attr: attr.into(), 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 121 | /// reply attr. 122 | pub struct ReplyAttr { 123 | /// the attribute TTL. 124 | pub ttl: Duration, 125 | /// the attribute. 126 | pub attr: FileAttr, 127 | } 128 | 129 | impl From for fuse_attr_out { 130 | fn from(attr: ReplyAttr) -> Self { 131 | fuse_attr_out { 132 | attr_valid: attr.ttl.as_secs(), 133 | attr_valid_nsec: attr.ttl.subsec_nanos(), 134 | dummy: 0, 135 | attr: attr.attr.into(), 136 | } 137 | } 138 | } 139 | 140 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 141 | /// data reply. 142 | pub struct ReplyData { 143 | /// the data. 144 | pub data: Bytes, 145 | } 146 | 147 | impl From for ReplyData { 148 | fn from(data: Bytes) -> Self { 149 | Self { data } 150 | } 151 | } 152 | 153 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 154 | /// open reply. 155 | pub struct ReplyOpen { 156 | /// the file handle id. 157 | /// 158 | /// # Notes: 159 | /// 160 | /// if set fh 0, means use stateless IO. 161 | pub fh: u64, 162 | /// the flags. 163 | pub flags: u32, 164 | } 165 | 166 | impl From for fuse_open_out { 167 | fn from(opened: ReplyOpen) -> Self { 168 | fuse_open_out { 169 | fh: opened.fh, 170 | open_flags: opened.flags, 171 | _padding: 0, 172 | } 173 | } 174 | } 175 | 176 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 177 | /// write reply. 178 | pub struct ReplyWrite { 179 | /// the data written. 180 | pub written: u32, 181 | } 182 | 183 | impl From for fuse_write_out { 184 | fn from(written: ReplyWrite) -> Self { 185 | fuse_write_out { 186 | size: written.written, 187 | _padding: 0, 188 | } 189 | } 190 | } 191 | 192 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 193 | /// statfs reply. 194 | pub struct ReplyStatFs { 195 | /// the number of blocks in the filesystem. 196 | pub blocks: u64, 197 | /// the number of free blocks. 198 | pub bfree: u64, 199 | /// the number of free blocks for non-priviledge users. 200 | pub bavail: u64, 201 | /// the number of inodes. 202 | pub files: u64, 203 | /// the number of free inodes. 204 | pub ffree: u64, 205 | /// the block size. 206 | pub bsize: u32, 207 | /// the maximum length of file name. 208 | pub namelen: u32, 209 | /// the fragment size. 210 | pub frsize: u32, 211 | } 212 | 213 | impl From for fuse_statfs_out { 214 | fn from(stat_fs: ReplyStatFs) -> Self { 215 | fuse_statfs_out { 216 | st: fuse_kstatfs { 217 | blocks: stat_fs.blocks, 218 | bfree: stat_fs.bfree, 219 | bavail: stat_fs.bavail, 220 | files: stat_fs.files, 221 | ffree: stat_fs.ffree, 222 | bsize: stat_fs.bsize, 223 | namelen: stat_fs.namelen, 224 | frsize: stat_fs.frsize, 225 | _padding: 0, 226 | spare: [0; 6], 227 | }, 228 | } 229 | } 230 | } 231 | 232 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 233 | /// xattr reply. 234 | pub enum ReplyXAttr { 235 | Size(u32), 236 | Data(Bytes), 237 | } 238 | 239 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 240 | /// directory entry. 241 | pub struct DirectoryEntry { 242 | /// entry inode. 243 | pub inode: u64, 244 | /// entry kind. 245 | pub kind: FileType, 246 | /// entry name. 247 | pub name: OsString, 248 | /// Directory offset of the _next_ entry 249 | pub offset: i64, 250 | } 251 | 252 | /// readdir reply. 253 | pub struct ReplyDirectory>> { 254 | pub entries: S, 255 | } 256 | 257 | impl> + std::fmt::Debug> std::fmt::Debug 258 | for ReplyDirectory 259 | { 260 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 261 | f.debug_struct("ReplyDirectory") 262 | .field("entries", &self.entries) 263 | .finish() 264 | } 265 | } 266 | 267 | #[cfg(feature = "file-lock")] 268 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 269 | /// file lock reply. 270 | pub struct ReplyLock { 271 | /// starting offset for lock. 272 | pub start: u64, 273 | /// end offset for lock. 274 | pub end: u64, 275 | /// type of lock, such as: [`F_RDLCK`], [`F_WRLCK`] and [`F_UNLCK`] 276 | /// 277 | /// [`F_RDLCK`]: libc::F_RDLCK 278 | /// [`F_WRLCK`]: libc::F_WRLCK 279 | /// [`F_UNLCK`]: libc::F_UNLCK 280 | pub r#type: u32, 281 | /// PID of process blocking our lock 282 | pub pid: u32, 283 | } 284 | 285 | #[cfg(feature = "file-lock")] 286 | impl From for fuse_lk_out { 287 | fn from(lock: ReplyLock) -> Self { 288 | fuse_lk_out { 289 | lk: fuse_file_lock { 290 | start: lock.start, 291 | end: lock.end, 292 | r#type: lock.r#type, 293 | pid: lock.pid, 294 | }, 295 | } 296 | } 297 | } 298 | 299 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 300 | /// crate reply. 301 | pub struct ReplyCreated { 302 | /// the attribute TTL. 303 | pub ttl: Duration, 304 | /// the attribute of file. 305 | pub attr: FileAttr, 306 | /// the generation of file. 307 | pub generation: u64, 308 | /// the file handle. 309 | pub fh: u64, 310 | /// the flags. 311 | pub flags: u32, 312 | } 313 | 314 | impl From for (fuse_entry_out, fuse_open_out) { 315 | fn from(created: ReplyCreated) -> Self { 316 | let attr = created.attr; 317 | 318 | let entry_out = fuse_entry_out { 319 | nodeid: attr.ino, 320 | generation: created.generation, 321 | entry_valid: created.ttl.as_secs(), 322 | attr_valid: created.ttl.as_secs(), 323 | entry_valid_nsec: created.ttl.subsec_micros(), 324 | attr_valid_nsec: created.ttl.subsec_micros(), 325 | attr: attr.into(), 326 | }; 327 | 328 | let open_out = fuse_open_out { 329 | fh: created.fh, 330 | open_flags: created.flags, 331 | _padding: 0, 332 | }; 333 | 334 | (entry_out, open_out) 335 | } 336 | } 337 | 338 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 339 | // TODO need more detail 340 | /// bmap reply. 341 | pub struct ReplyBmap { 342 | pub block: u64, 343 | } 344 | 345 | impl From for fuse_bmap_out { 346 | fn from(bmap: ReplyBmap) -> Self { 347 | fuse_bmap_out { block: bmap.block } 348 | } 349 | } 350 | 351 | /*#[derive(Debug)] 352 | pub struct ReplyIoctl { 353 | pub result: i32, 354 | pub flags: u32, 355 | pub in_iovs: u32, 356 | pub out_iovs: u32, 357 | }*/ 358 | 359 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 360 | // TODO need more detail 361 | /// poll reply 362 | pub struct ReplyPoll { 363 | pub revents: u32, 364 | } 365 | 366 | impl From for fuse_poll_out { 367 | fn from(poll: ReplyPoll) -> Self { 368 | fuse_poll_out { 369 | revents: poll.revents, 370 | _padding: 0, 371 | } 372 | } 373 | } 374 | 375 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 376 | /// directory entry with attribute 377 | pub struct DirectoryEntryPlus { 378 | /// the entry inode. 379 | pub inode: u64, 380 | /// the entry generation. 381 | pub generation: u64, 382 | /// the entry kind. 383 | pub kind: FileType, 384 | /// the entry name. 385 | pub name: OsString, 386 | /// Directory offset of the _next_ entry 387 | pub offset: i64, 388 | /// the entry attribute. 389 | pub attr: FileAttr, 390 | /// the entry TTL. 391 | pub entry_ttl: Duration, 392 | /// the attribute TTL. 393 | pub attr_ttl: Duration, 394 | } 395 | 396 | /// the readdirplus reply. 397 | pub struct ReplyDirectoryPlus>> { 398 | pub entries: S, 399 | } 400 | 401 | impl> + std::fmt::Debug> std::fmt::Debug 402 | for ReplyDirectoryPlus 403 | { 404 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 405 | f.debug_struct("ReplyDirectoryPlus") 406 | .field("entries", &self.entries) 407 | .finish() 408 | } 409 | } 410 | 411 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 412 | /// the lseek reply. 413 | pub struct ReplyLSeek { 414 | /// lseek offset. 415 | pub offset: u64, 416 | } 417 | 418 | impl From for fuse_lseek_out { 419 | fn from(seek: ReplyLSeek) -> Self { 420 | fuse_lseek_out { 421 | offset: seek.offset, 422 | } 423 | } 424 | } 425 | 426 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] 427 | /// copy_file_range reply. 428 | pub struct ReplyCopyFileRange { 429 | /// data copied size. 430 | pub copied: u64, 431 | } 432 | 433 | impl From for fuse_write_out { 434 | fn from(copied: ReplyCopyFileRange) -> Self { 435 | fuse_write_out { 436 | size: copied.copied as u32, 437 | _padding: 0, 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /examples/src/poll/main.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::num::NonZeroU32; 3 | use std::os::unix::io::AsRawFd; 4 | use std::path::PathBuf; 5 | use std::sync::atomic::{AtomicBool, Ordering}; 6 | use std::sync::Arc; 7 | use std::time::{Duration, SystemTime}; 8 | 9 | use bytes::Bytes; 10 | use fuse3::raw::prelude::*; 11 | use fuse3::{MountOptions, Result}; 12 | use futures_util::stream; 13 | use futures_util::stream::Stream; 14 | use mio::unix::SourceFd; 15 | use mio::{Events, Interest, Token}; 16 | use tokio::time; 17 | use tracing::{debug, info, Level}; 18 | 19 | const CONTENT: &str = "hello world\n"; 20 | 21 | const PARENT_INODE: u64 = 1; 22 | const FILE_INODE: u64 = 2; 23 | const FILE_NAME: &str = "hello-world.txt"; 24 | const PARENT_MODE: u16 = 0o755; 25 | const FILE_MODE: u16 = 0o644; 26 | const TTL: Duration = Duration::from_secs(1); 27 | 28 | #[derive(Debug, Default)] 29 | struct Poll { 30 | ready: Arc, 31 | } 32 | 33 | impl Filesystem for Poll { 34 | async fn init(&self, _req: Request) -> Result { 35 | Ok(ReplyInit { 36 | max_write: NonZeroU32::new(16 * 1024).unwrap(), 37 | }) 38 | } 39 | 40 | async fn destroy(&self, _req: Request) {} 41 | 42 | async fn lookup(&self, _req: Request, parent: u64, name: &OsStr) -> Result { 43 | if parent != PARENT_INODE { 44 | return Err(libc::ENOENT.into()); 45 | } 46 | 47 | if name != OsStr::new(FILE_NAME) { 48 | return Err(libc::ENOENT.into()); 49 | } 50 | 51 | Ok(ReplyEntry { 52 | ttl: TTL, 53 | attr: FileAttr { 54 | ino: FILE_INODE, 55 | size: CONTENT.len() as u64, 56 | blocks: 0, 57 | atime: SystemTime::now().into(), 58 | mtime: SystemTime::now().into(), 59 | ctime: SystemTime::now().into(), 60 | #[cfg(target_os = "macos")] 61 | crtime: SystemTime::now().into(), 62 | kind: FileType::RegularFile, 63 | perm: FILE_MODE, 64 | nlink: 0, 65 | uid: 0, 66 | gid: 0, 67 | rdev: 0, 68 | #[cfg(target_os = "macos")] 69 | flags: 0, 70 | blksize: 0, 71 | }, 72 | generation: 0, 73 | }) 74 | } 75 | 76 | async fn getattr( 77 | &self, 78 | _req: Request, 79 | inode: u64, 80 | _fh: Option, 81 | _flags: u32, 82 | ) -> Result { 83 | if inode == PARENT_INODE { 84 | Ok(ReplyAttr { 85 | ttl: TTL, 86 | attr: FileAttr { 87 | ino: PARENT_INODE, 88 | size: 0, 89 | blocks: 0, 90 | atime: SystemTime::now().into(), 91 | mtime: SystemTime::now().into(), 92 | ctime: SystemTime::now().into(), 93 | #[cfg(target_os = "macos")] 94 | crtime: SystemTime::now().into(), 95 | kind: FileType::Directory, 96 | perm: PARENT_MODE, 97 | nlink: 0, 98 | uid: 0, 99 | gid: 0, 100 | rdev: 0, 101 | #[cfg(target_os = "macos")] 102 | flags: 0, 103 | blksize: 0, 104 | }, 105 | }) 106 | } else if inode == FILE_INODE { 107 | Ok(ReplyAttr { 108 | ttl: TTL, 109 | attr: FileAttr { 110 | ino: FILE_INODE, 111 | size: CONTENT.len() as _, 112 | blocks: 0, 113 | atime: SystemTime::now().into(), 114 | mtime: SystemTime::now().into(), 115 | ctime: SystemTime::now().into(), 116 | #[cfg(target_os = "macos")] 117 | crtime: SystemTime::now().into(), 118 | kind: FileType::RegularFile, 119 | perm: FILE_MODE, 120 | nlink: 0, 121 | uid: 0, 122 | gid: 0, 123 | rdev: 0, 124 | #[cfg(target_os = "macos")] 125 | flags: 0, 126 | blksize: 0, 127 | }, 128 | }) 129 | } else { 130 | Err(libc::ENOENT.into()) 131 | } 132 | } 133 | 134 | async fn open(&self, _req: Request, inode: u64, flags: u32) -> Result { 135 | if inode != PARENT_INODE && inode != FILE_INODE { 136 | return Err(libc::ENOENT.into()); 137 | } 138 | 139 | Ok(ReplyOpen { fh: 1, flags }) 140 | } 141 | 142 | async fn read( 143 | &self, 144 | _req: Request, 145 | inode: u64, 146 | _fh: u64, 147 | offset: u64, 148 | size: u32, 149 | ) -> Result { 150 | if inode != FILE_INODE { 151 | return Err(libc::ENOENT.into()); 152 | } 153 | 154 | if offset as usize >= CONTENT.len() { 155 | Ok(ReplyData { data: Bytes::new() }) 156 | } else { 157 | let mut data = &CONTENT.as_bytes()[offset as usize..]; 158 | 159 | if data.len() > size as usize { 160 | data = &data[..size as usize]; 161 | } 162 | 163 | Ok(ReplyData { 164 | data: Bytes::copy_from_slice(data), 165 | }) 166 | } 167 | } 168 | 169 | async fn readdir( 170 | &self, 171 | _req: Request, 172 | inode: u64, 173 | _fh: u64, 174 | offset: i64, 175 | ) -> Result> + Send + '_>> { 176 | if inode == FILE_INODE { 177 | return Err(libc::ENOTDIR.into()); 178 | } 179 | 180 | if inode != PARENT_INODE { 181 | return Err(libc::ENOENT.into()); 182 | } 183 | 184 | let entries = vec![ 185 | Ok(DirectoryEntry { 186 | inode: PARENT_INODE, 187 | kind: FileType::Directory, 188 | name: OsString::from("."), 189 | offset: 1, 190 | }), 191 | Ok(DirectoryEntry { 192 | inode: PARENT_INODE, 193 | kind: FileType::Directory, 194 | name: OsString::from(".."), 195 | offset: 2, 196 | }), 197 | Ok(DirectoryEntry { 198 | inode: FILE_INODE, 199 | kind: FileType::RegularFile, 200 | name: OsString::from(FILE_NAME), 201 | offset: 3, 202 | }), 203 | ]; 204 | 205 | Ok(ReplyDirectory { 206 | entries: stream::iter(entries.into_iter().skip(offset as usize)), 207 | }) 208 | } 209 | 210 | async fn access(&self, _req: Request, inode: u64, _mask: u32) -> Result<()> { 211 | if inode != PARENT_INODE && inode != FILE_INODE { 212 | return Err(libc::ENOENT.into()); 213 | } 214 | 215 | Ok(()) 216 | } 217 | 218 | async fn readdirplus( 219 | &self, 220 | _req: Request, 221 | parent: u64, 222 | _fh: u64, 223 | offset: u64, 224 | _lock_owner: u64, 225 | ) -> Result> + Send + '_>> 226 | { 227 | if parent == FILE_INODE { 228 | return Err(libc::ENOTDIR.into()); 229 | } 230 | 231 | if parent != PARENT_INODE { 232 | return Err(libc::ENOENT.into()); 233 | } 234 | 235 | let entries = vec![ 236 | Ok(DirectoryEntryPlus { 237 | inode: PARENT_INODE, 238 | generation: 0, 239 | kind: FileType::Directory, 240 | name: OsString::from("."), 241 | offset: 1, 242 | attr: FileAttr { 243 | ino: PARENT_INODE, 244 | size: 0, 245 | blocks: 0, 246 | atime: SystemTime::now().into(), 247 | mtime: SystemTime::now().into(), 248 | ctime: SystemTime::now().into(), 249 | #[cfg(target_os = "macos")] 250 | crtime: SystemTime::now().into(), 251 | kind: FileType::Directory, 252 | perm: PARENT_MODE, 253 | nlink: 0, 254 | uid: 0, 255 | gid: 0, 256 | rdev: 0, 257 | #[cfg(target_os = "macos")] 258 | flags: 0, 259 | blksize: 0, 260 | }, 261 | entry_ttl: TTL, 262 | attr_ttl: TTL, 263 | }), 264 | Ok(DirectoryEntryPlus { 265 | inode: PARENT_INODE, 266 | generation: 0, 267 | kind: FileType::Directory, 268 | name: OsString::from(".."), 269 | offset: 2, 270 | attr: FileAttr { 271 | ino: PARENT_INODE, 272 | size: 0, 273 | blocks: 0, 274 | atime: SystemTime::now().into(), 275 | mtime: SystemTime::now().into(), 276 | ctime: SystemTime::now().into(), 277 | #[cfg(target_os = "macos")] 278 | crtime: SystemTime::now().into(), 279 | kind: FileType::Directory, 280 | perm: PARENT_MODE, 281 | nlink: 0, 282 | uid: 0, 283 | gid: 0, 284 | rdev: 0, 285 | #[cfg(target_os = "macos")] 286 | flags: 0, 287 | blksize: 0, 288 | }, 289 | entry_ttl: TTL, 290 | attr_ttl: TTL, 291 | }), 292 | Ok(DirectoryEntryPlus { 293 | inode: FILE_INODE, 294 | generation: 0, 295 | kind: FileType::Directory, 296 | name: OsString::from(FILE_NAME), 297 | offset: 3, 298 | attr: FileAttr { 299 | ino: FILE_INODE, 300 | size: CONTENT.len() as _, 301 | blocks: 0, 302 | atime: SystemTime::now().into(), 303 | mtime: SystemTime::now().into(), 304 | ctime: SystemTime::now().into(), 305 | #[cfg(target_os = "macos")] 306 | crtime: SystemTime::now().into(), 307 | kind: FileType::RegularFile, 308 | perm: FILE_MODE, 309 | nlink: 0, 310 | uid: 0, 311 | gid: 0, 312 | rdev: 0, 313 | #[cfg(target_os = "macos")] 314 | flags: 0, 315 | blksize: 0, 316 | }, 317 | entry_ttl: TTL, 318 | attr_ttl: TTL, 319 | }), 320 | ]; 321 | 322 | Ok(ReplyDirectoryPlus { 323 | entries: stream::iter(entries.into_iter().skip(offset as usize)), 324 | }) 325 | } 326 | 327 | async fn poll( 328 | &self, 329 | _req: Request, 330 | inode: u64, 331 | _fh: u64, 332 | kh: Option, 333 | flags: u32, 334 | events: u32, 335 | notify: &Notify, 336 | ) -> Result { 337 | if inode != PARENT_INODE && inode != FILE_INODE { 338 | return Err(libc::ENOENT.into()); 339 | } 340 | 341 | debug!("poll flags {} events {}", flags, events); 342 | 343 | if let Some(kh) = kh { 344 | let ready = self.ready.clone(); 345 | 346 | if ready.load(Ordering::SeqCst) { 347 | return Ok(ReplyPoll { revents: events }); 348 | } 349 | 350 | let notify = notify.clone(); 351 | 352 | tokio::spawn(async move { 353 | debug!("start notify"); 354 | 355 | time::sleep(Duration::from_secs(2)).await; 356 | 357 | ready.store(true, Ordering::SeqCst); 358 | 359 | notify.wakeup(kh).await; 360 | 361 | debug!("notify done"); 362 | }); 363 | } 364 | 365 | Ok(ReplyPoll { revents: 0 }) 366 | } 367 | } 368 | 369 | #[tokio::main(flavor = "current_thread")] 370 | async fn main() { 371 | log_init(); 372 | 373 | let uid = unsafe { libc::getuid() }; 374 | let gid = unsafe { libc::getgid() }; 375 | 376 | let mut mount_options = MountOptions::default(); 377 | mount_options.uid(uid).gid(gid).read_only(true); 378 | 379 | let temp_dir = tempfile::tempdir().unwrap(); 380 | 381 | let mount_path = temp_dir.path(); 382 | 383 | let poll = Poll::default(); 384 | 385 | let session = Session::new(mount_options); 386 | 387 | { 388 | let mount_path = mount_path.as_os_str().to_os_string(); 389 | 390 | std::thread::spawn(move || { 391 | std::thread::sleep(Duration::from_secs(2)); 392 | 393 | poll_file(&mount_path); 394 | }); 395 | } 396 | 397 | session 398 | .mount_with_unprivileged(poll, mount_path) 399 | .await 400 | .unwrap() 401 | .await 402 | .unwrap(); 403 | } 404 | 405 | fn log_init() { 406 | let subscriber = tracing_subscriber::fmt() 407 | .with_max_level(Level::DEBUG) 408 | .finish(); 409 | tracing::subscriber::set_global_default(subscriber).unwrap(); 410 | } 411 | 412 | fn poll_file(mount_path: &OsStr) { 413 | let mut poll = mio::Poll::new().unwrap(); 414 | 415 | let mut path = PathBuf::from(mount_path.to_os_string()); 416 | path.push(FILE_NAME); 417 | 418 | let file = std::fs::File::open(&path).unwrap(); 419 | 420 | let fd = file.as_raw_fd(); 421 | let mut fd = SourceFd(&fd); 422 | 423 | const TOKEN: Token = Token(1); 424 | 425 | poll.registry() 426 | .register(&mut fd, TOKEN, Interest::READABLE) 427 | .unwrap(); 428 | 429 | let mut events = Events::with_capacity(1024); 430 | 431 | poll.poll(&mut events, None).unwrap(); 432 | 433 | for event in events.iter() { 434 | info!("{:?}", event); 435 | } 436 | 437 | poll.registry().deregister(&mut fd).unwrap(); 438 | } 439 | -------------------------------------------------------------------------------- /src/raw/connection/async_io.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use std::env; 3 | #[cfg(any(target_os = "linux", target_os = "macos"))] 4 | use std::fs::File; 5 | use std::fs::OpenOptions; 6 | use std::io; 7 | #[cfg(any(target_os = "linux", target_os = "macos"))] 8 | use std::io::Write; 9 | use std::io::{IoSlice, IoSliceMut}; 10 | use std::ops::{Deref, DerefMut}; 11 | use std::os::fd::AsFd; 12 | use std::os::fd::BorrowedFd; 13 | #[cfg(any( 14 | all(target_os = "linux", feature = "unprivileged"), 15 | target_os = "freebsd", 16 | target_os = "macos" 17 | ))] 18 | use std::os::fd::OwnedFd; 19 | #[cfg(target_os = "macos")] 20 | use std::os::fd::{AsRawFd, FromRawFd}; 21 | #[cfg(any( 22 | all(target_os = "linux", feature = "unprivileged"), 23 | target_os = "macos" 24 | ))] 25 | use std::os::unix::io::RawFd; 26 | use std::pin::pin; 27 | use std::sync::Arc; 28 | #[cfg(any( 29 | all(target_os = "linux", feature = "unprivileged"), 30 | target_os = "macos" 31 | ))] 32 | use std::{ffi::OsString, path::Path}; 33 | 34 | #[cfg(any( 35 | all(target_os = "linux", feature = "unprivileged"), 36 | target_os = "freebsd" 37 | ))] 38 | use async_io::Async; 39 | use async_lock::Mutex; 40 | use async_notify::Notify; 41 | #[cfg(any( 42 | all(target_os = "linux", feature = "unprivileged"), 43 | target_os = "macos" 44 | ))] 45 | use async_process::Command; 46 | #[cfg(target_os = "macos")] 47 | use futures_util::{join, try_join}; 48 | use futures_util::{select, FutureExt}; 49 | #[cfg(any( 50 | all(target_os = "linux", feature = "unprivileged"), 51 | target_os = "macos" 52 | ))] 53 | use nix::sys::socket::{self, AddressFamily, ControlMessageOwned, MsgFlags, SockFlag, SockType}; 54 | #[cfg(any( 55 | all(target_os = "linux", feature = "unprivileged"), 56 | target_os = "freebsd", 57 | target_os = "macos" 58 | ))] 59 | use nix::sys::uio; 60 | #[cfg(any( 61 | all(target_os = "linux", feature = "unprivileged"), 62 | target_os = "macos" 63 | ))] 64 | use tracing::debug; 65 | 66 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 67 | use crate::find_fusermount3; 68 | use crate::raw::connection::CompleteIoResult; 69 | #[cfg(any( 70 | all(target_os = "linux", feature = "unprivileged"), 71 | target_os = "macos" 72 | ))] 73 | use crate::MountOptions; 74 | #[derive(Debug)] 75 | pub struct FuseConnection { 76 | unmount_notify: Arc, 77 | mode: ConnectionMode, 78 | } 79 | 80 | impl FuseConnection { 81 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 82 | pub fn new(unmount_notify: Arc) -> io::Result { 83 | #[cfg(target_os = "freebsd")] 84 | { 85 | let connection = NonBlockFuseConnection::new()?; 86 | 87 | Ok(Self { 88 | unmount_notify, 89 | mode: ConnectionMode::NonBlock(connection), 90 | }) 91 | } 92 | 93 | #[cfg(target_os = "linux")] 94 | { 95 | let connection = BlockFuseConnection::new()?; 96 | 97 | Ok(Self { 98 | unmount_notify, 99 | mode: ConnectionMode::Block(connection), 100 | }) 101 | } 102 | } 103 | 104 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 105 | pub async fn new_with_unprivileged( 106 | mount_options: MountOptions, 107 | mount_path: impl AsRef, 108 | unmount_notify: Arc, 109 | ) -> io::Result { 110 | let connection = 111 | NonBlockFuseConnection::new_with_unprivileged(mount_options, mount_path).await?; 112 | 113 | Ok(Self { 114 | unmount_notify, 115 | mode: ConnectionMode::NonBlock(connection), 116 | }) 117 | } 118 | 119 | #[cfg(target_os = "macos")] 120 | pub async fn new_with_unprivileged( 121 | mount_options: MountOptions, 122 | mount_path: impl AsRef, 123 | unmount_notify: Arc, 124 | ) -> io::Result { 125 | let connection = 126 | BlockFuseConnection::new_with_unprivileged(mount_options, mount_path).await?; 127 | 128 | Ok(Self { 129 | unmount_notify, 130 | mode: ConnectionMode::Block(connection), 131 | }) 132 | } 133 | 134 | pub async fn read_vectored + Send + 'static>( 135 | &self, 136 | header_buf: Vec, 137 | data_buf: T, 138 | ) -> Option, T), usize>> { 139 | let mut unmount_fut = pin!(self.unmount_notify.notified().fuse()); 140 | let mut read_fut = pin!(self.inner_read_vectored(header_buf, data_buf).fuse()); 141 | 142 | select! { 143 | _ = unmount_fut => None, 144 | res = read_fut => Some(res) 145 | } 146 | } 147 | 148 | async fn inner_read_vectored + Send + 'static>( 149 | &self, 150 | header_buf: Vec, 151 | data_buf: T, 152 | ) -> CompleteIoResult<(Vec, T), usize> { 153 | match &self.mode { 154 | #[cfg(any(target_os = "linux", target_os = "macos"))] 155 | ConnectionMode::Block(connection) => { 156 | connection.read_vectored(header_buf, data_buf).await 157 | } 158 | #[cfg(any( 159 | all(target_os = "linux", feature = "unprivileged"), 160 | target_os = "freebsd" 161 | ))] 162 | ConnectionMode::NonBlock(connection) => { 163 | connection.read_vectored(header_buf, data_buf).await 164 | } 165 | } 166 | } 167 | 168 | pub async fn write_vectored + Send, U: Deref + Send>( 169 | &self, 170 | data: T, 171 | body_extend_data: Option, 172 | ) -> CompleteIoResult<(T, Option), usize> { 173 | match &self.mode { 174 | #[cfg(any(target_os = "linux", target_os = "macos"))] 175 | ConnectionMode::Block(connection) => { 176 | connection.write_vectored(data, body_extend_data).await 177 | } 178 | #[cfg(any( 179 | all(target_os = "linux", feature = "unprivileged"), 180 | target_os = "freebsd" 181 | ))] 182 | ConnectionMode::NonBlock(connection) => { 183 | connection.write_vectored(data, body_extend_data).await 184 | } 185 | } 186 | } 187 | } 188 | 189 | #[derive(Debug)] 190 | enum ConnectionMode { 191 | #[cfg(any(target_os = "linux", target_os = "macos"))] 192 | Block(BlockFuseConnection), 193 | #[cfg(any( 194 | all(target_os = "linux", feature = "unprivileged"), 195 | target_os = "freebsd" 196 | ))] 197 | NonBlock(NonBlockFuseConnection), 198 | } 199 | 200 | #[cfg(any(target_os = "linux", target_os = "macos"))] 201 | #[derive(Debug)] 202 | struct BlockFuseConnection { 203 | file: File, 204 | read: Mutex<()>, 205 | write: Mutex<()>, 206 | } 207 | 208 | #[cfg(any(target_os = "linux", target_os = "macos"))] 209 | impl BlockFuseConnection { 210 | #[cfg(target_os = "linux")] 211 | pub fn new() -> io::Result { 212 | const DEV_FUSE: &str = "/dev/fuse"; 213 | 214 | let file = OpenOptions::new().write(true).read(true).open(DEV_FUSE)?; 215 | 216 | Ok(Self { 217 | file, 218 | read: Mutex::new(()), 219 | write: Mutex::new(()), 220 | }) 221 | } 222 | 223 | #[cfg(target_os = "macos")] 224 | async fn new_with_unprivileged( 225 | mount_options: MountOptions, 226 | mount_path: impl AsRef, 227 | ) -> io::Result { 228 | use std::{thread, time::Duration}; 229 | 230 | use crate::find_macfuse_mount; 231 | 232 | let (sock0, sock1) = match socket::socketpair( 233 | AddressFamily::Unix, 234 | SockType::Stream, 235 | None, 236 | SockFlag::empty(), 237 | ) { 238 | Err(err) => return Err(err.into()), 239 | 240 | Ok((sock0, sock1)) => (sock0, sock1), 241 | }; 242 | 243 | let binary_path = find_macfuse_mount()?; 244 | 245 | const ENV: &str = "_FUSE_COMMFD"; 246 | 247 | let options = mount_options.build(); 248 | 249 | debug!("mount options {:?}", options); 250 | 251 | let exec_path = match env::current_exe() { 252 | Ok(path) => path, 253 | Err(err) => return Err(err), 254 | }; 255 | 256 | let mount_path = mount_path.as_ref().as_os_str().to_os_string(); 257 | async_global_executor::spawn(async move { 258 | debug!("mount_thread start"); 259 | let fd0 = sock0.as_raw_fd(); 260 | let mut binding = Command::new(binary_path); 261 | let mut child = binding 262 | .env(ENV, fd0.to_string()) 263 | .env("_FUSE_CALL_BY_LIB", "1") 264 | .env("_FUSE_COMMVERS", "2") 265 | .env("_FUSE_DAEMON_PATH", exec_path) 266 | .args(vec![options, mount_path]) 267 | .spawn()?; 268 | if !child.status().await?.success() { 269 | return Err(io::Error::new( 270 | io::ErrorKind::Other, 271 | "fusermount run failed", 272 | )); 273 | } 274 | Ok(()) 275 | }); 276 | 277 | let fd1 = sock1.as_raw_fd(); 278 | let fd = async_global_executor::spawn_blocking(move || { 279 | debug!("wait_thread start"); 280 | // wait for macfuse mount command start 281 | // it seems that socket::recvmsg will not block to wait for the message 282 | // so we need to sleep for a while 283 | thread::sleep(Duration::from_secs(1)); 284 | // let mut buf = vec![0; 10000]; // buf should large enough 285 | let mut buf = vec![]; // it seems 0 len still works well 286 | 287 | let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]); 288 | 289 | let mut bufs = [IoSliceMut::new(&mut buf)]; 290 | 291 | let msg = match socket::recvmsg::<()>( 292 | fd1, 293 | &mut bufs[..], 294 | Some(&mut cmsg_buf), 295 | MsgFlags::empty(), 296 | ) { 297 | Err(err) => return Err(err.into()), 298 | 299 | Ok(msg) => msg, 300 | }; 301 | 302 | let mut cmsgs = match msg.cmsgs() { 303 | Err(err) => return Err(err.into()), 304 | Ok(cmsgs) => cmsgs, 305 | }; 306 | 307 | let fd = if let Some(ControlMessageOwned::ScmRights(fds)) = cmsgs.next() { 308 | if fds.is_empty() { 309 | return Err(io::Error::new(io::ErrorKind::Other, "no fuse fd")); 310 | } 311 | 312 | fds[0] 313 | } else { 314 | return Err(io::Error::new(io::ErrorKind::Other, "get fuse fd failed")); 315 | }; 316 | 317 | Ok(fd) 318 | }) 319 | .await 320 | .unwrap(); 321 | 322 | // Safety: fd is valid 323 | let file = unsafe { File::from_raw_fd(fd) }; 324 | 325 | Ok(Self { 326 | file, 327 | read: Mutex::new(()), 328 | write: Mutex::new(()), 329 | }) 330 | } 331 | 332 | async fn read_vectored + Send + 'static>( 333 | &self, 334 | mut header_buf: Vec, 335 | mut data_buf: T, 336 | ) -> CompleteIoResult<(Vec, T), usize> { 337 | use std::io::Read; 338 | use std::mem::ManuallyDrop; 339 | use std::os::fd::{AsRawFd, FromRawFd}; 340 | 341 | let _guard = self.read.lock().await; 342 | let fd = self.file.as_raw_fd(); 343 | 344 | let ((header_buf, data_buf), res) = async_global_executor::spawn_blocking(move || { 345 | // Safety: when we call read, the fd is still valid, when fd is closed and file is 346 | // dropped, the read operation will return error 347 | let file = unsafe { File::from_raw_fd(fd) }; 348 | // avoid close the file 349 | let mut file = ManuallyDrop::new(file); 350 | 351 | let res = file.read_vectored(&mut [ 352 | IoSliceMut::new(&mut header_buf), 353 | IoSliceMut::new(&mut data_buf), 354 | ]); 355 | 356 | ((header_buf, data_buf), res) 357 | }) 358 | .await; 359 | 360 | ((header_buf, data_buf), res) 361 | } 362 | 363 | async fn write_vectored + Send, U: Deref + Send>( 364 | &self, 365 | data: T, 366 | body_extend_data: Option, 367 | ) -> CompleteIoResult<(T, Option), usize> { 368 | let _guard = self.write.lock().await; 369 | 370 | let res = { 371 | let body_extend_data = body_extend_data.as_deref(); 372 | 373 | match body_extend_data { 374 | None => (&self.file).write_vectored(&[IoSlice::new(data.deref())]), 375 | 376 | Some(body_extend_data) => (&self.file) 377 | .write_vectored(&[IoSlice::new(data.deref()), IoSlice::new(body_extend_data)]), 378 | } 379 | }; 380 | 381 | match res { 382 | Err(err) => ((data, body_extend_data), Err(err)), 383 | Ok(n) => ((data, body_extend_data), Ok(n)), 384 | } 385 | } 386 | } 387 | 388 | #[cfg(any( 389 | all(target_os = "linux", feature = "unprivileged"), 390 | target_os = "freebsd" 391 | ))] 392 | #[derive(Debug)] 393 | struct NonBlockFuseConnection { 394 | fd: Async, 395 | read: Mutex<()>, 396 | write: Mutex<()>, 397 | } 398 | 399 | #[cfg(any( 400 | all(target_os = "linux", feature = "unprivileged"), 401 | target_os = "freebsd" 402 | ))] 403 | impl NonBlockFuseConnection { 404 | #[cfg(target_os = "freebsd")] 405 | fn new() -> io::Result { 406 | const DEV_FUSE: &str = "/dev/fuse"; 407 | 408 | let file = OpenOptions::new().write(true).read(true).open(DEV_FUSE)?; 409 | 410 | Ok(Self { 411 | fd: Async::new(file.into())?, 412 | read: Mutex::new(()), 413 | write: Mutex::new(()), 414 | }) 415 | } 416 | 417 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 418 | async fn new_with_unprivileged( 419 | mount_options: MountOptions, 420 | mount_path: impl AsRef, 421 | ) -> io::Result { 422 | use std::os::fd::{AsRawFd, FromRawFd}; 423 | 424 | let (sock0, sock1) = match socket::socketpair( 425 | AddressFamily::Unix, 426 | SockType::SeqPacket, 427 | None, 428 | SockFlag::empty(), 429 | ) { 430 | Err(err) => return Err(err.into()), 431 | 432 | Ok((sock0, sock1)) => (sock0, sock1), 433 | }; 434 | 435 | let binary_path = find_fusermount3()?; 436 | 437 | const ENV: &str = "_FUSE_COMMFD"; 438 | 439 | let options = mount_options.build_with_unprivileged(); 440 | 441 | debug!("mount options {:?}", options); 442 | 443 | let mount_path = mount_path.as_ref().as_os_str().to_os_string(); 444 | 445 | let fd0 = sock0.as_raw_fd(); 446 | let mut child = Command::new(binary_path) 447 | .env(ENV, fd0.to_string()) 448 | .args(vec![OsString::from("-o"), options, mount_path]) 449 | .spawn()?; 450 | 451 | if !child.status().await?.success() { 452 | return Err(io::Error::new( 453 | io::ErrorKind::Other, 454 | "fusermount run failed", 455 | )); 456 | } 457 | 458 | let fd1 = sock1.as_raw_fd(); 459 | let fd = async_global_executor::spawn_blocking(move || { 460 | // let mut buf = vec![0; 10000]; // buf should large enough 461 | let mut buf = vec![]; // it seems 0 len still works well 462 | 463 | let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]); 464 | 465 | let mut bufs = [IoSliceMut::new(&mut buf)]; 466 | 467 | let msg = match socket::recvmsg::<()>( 468 | fd1, 469 | &mut bufs[..], 470 | Some(&mut cmsg_buf), 471 | MsgFlags::empty(), 472 | ) { 473 | Err(err) => return Err(err.into()), 474 | 475 | Ok(msg) => msg, 476 | }; 477 | 478 | let fd = if let Some(ControlMessageOwned::ScmRights(fds)) = msg.cmsgs()?.next() { 479 | if fds.is_empty() { 480 | return Err(io::Error::new(io::ErrorKind::Other, "no fuse fd")); 481 | } 482 | 483 | fds[0] 484 | } else { 485 | return Err(io::Error::new(io::ErrorKind::Other, "get fuse fd failed")); 486 | }; 487 | 488 | Ok(fd) 489 | }) 490 | .await?; 491 | 492 | // Safety: fd is valid 493 | let fd = unsafe { OwnedFd::from_raw_fd(fd) }; 494 | 495 | Ok(Self { 496 | fd: Async::new(fd)?, 497 | read: Mutex::new(()), 498 | write: Mutex::new(()), 499 | }) 500 | } 501 | 502 | async fn read_vectored + Send + 'static>( 503 | &self, 504 | mut header_buf: Vec, 505 | mut data_buf: T, 506 | ) -> CompleteIoResult<(Vec, T), usize> { 507 | let _guard = self.read.lock().await; 508 | 509 | let res = self 510 | .fd 511 | .read_with(|fd| { 512 | uio::readv( 513 | fd, 514 | &mut [ 515 | IoSliceMut::new(&mut header_buf), 516 | IoSliceMut::new(&mut data_buf), 517 | ], 518 | ) 519 | .map_err(Into::into) 520 | }) 521 | .await; 522 | 523 | ((header_buf, data_buf), res) 524 | } 525 | 526 | async fn write_vectored + Send, U: Deref + Send>( 527 | &self, 528 | data: T, 529 | body_extend_data: Option, 530 | ) -> CompleteIoResult<(T, Option), usize> { 531 | let _guard = self.write.lock().await; 532 | 533 | let res = { 534 | let body_extend_data = body_extend_data.as_deref(); 535 | 536 | match body_extend_data { 537 | None => uio::writev(&self.fd, &[IoSlice::new(data.deref())]), 538 | 539 | Some(body_extend_data) => uio::writev( 540 | &self.fd, 541 | &[IoSlice::new(data.deref()), IoSlice::new(body_extend_data)], 542 | ), 543 | } 544 | }; 545 | 546 | match res { 547 | Err(err) => ((data, body_extend_data), Err(err.into())), 548 | Ok(n) => ((data, body_extend_data), Ok(n)), 549 | } 550 | } 551 | } 552 | 553 | impl AsFd for FuseConnection { 554 | fn as_fd(&self) -> BorrowedFd<'_> { 555 | match &self.mode { 556 | #[cfg(any(target_os = "linux", target_os = "macos"))] 557 | ConnectionMode::Block(connection) => { 558 | // Safety: we own the File 559 | connection.file.as_fd() 560 | } 561 | 562 | #[cfg(any( 563 | all(target_os = "linux", feature = "unprivileged"), 564 | target_os = "freebsd" 565 | ))] 566 | ConnectionMode::NonBlock(connection) => connection.fd.as_fd(), 567 | } 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /src/raw/filesystem.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | use bytes::Bytes; 4 | use futures_util::stream::{Empty, Stream}; 5 | 6 | use crate::notify::Notify; 7 | use crate::raw::reply::*; 8 | use crate::raw::request::Request; 9 | use crate::{Inode, Result, SetAttr}; 10 | 11 | #[allow(unused_variables)] 12 | #[trait_make::make(Send)] 13 | /// Inode based filesystem trait. 14 | pub trait Filesystem { 15 | /// initialize filesystem. Called before any other filesystem method. 16 | async fn init(&self, req: Request) -> Result; 17 | 18 | /// clean up filesystem. Called on filesystem exit which is fuseblk, in normal fuse filesystem, 19 | /// kernel may call forget for root. There is some discuss for this 20 | /// , 21 | /// 22 | async fn destroy(&self, req: Request); 23 | 24 | /// look up a directory entry by name and get its attributes. 25 | async fn lookup(&self, req: Request, parent: Inode, name: &OsStr) -> Result { 26 | Err(libc::ENOSYS.into()) 27 | } 28 | 29 | /// forget an inode. The nlookup parameter indicates the number of lookups previously 30 | /// performed on this inode. If the filesystem implements inode lifetimes, it is recommended 31 | /// that inodes acquire a single reference on each lookup, and lose nlookup references on each 32 | /// forget. The filesystem may ignore forget calls, if the inodes don't need to have a limited 33 | /// lifetime. On unmount it is not guaranteed, that all referenced inodes will receive a forget 34 | /// message. When filesystem is normal(not fuseblk) and unmounting, kernel may send forget 35 | /// request for root and this library will stop session after call forget. There is some 36 | /// discussion for this , 37 | /// 38 | async fn forget(&self, req: Request, inode: Inode, nlookup: u64) {} 39 | 40 | /// get file attributes. If `fh` is None, means `fh` is not set. 41 | async fn getattr( 42 | &self, 43 | req: Request, 44 | inode: Inode, 45 | fh: Option, 46 | flags: u32, 47 | ) -> Result { 48 | Err(libc::ENOSYS.into()) 49 | } 50 | 51 | /// set file attributes. If `fh` is None, means `fh` is not set. 52 | async fn setattr( 53 | &self, 54 | req: Request, 55 | inode: Inode, 56 | fh: Option, 57 | set_attr: SetAttr, 58 | ) -> Result { 59 | Err(libc::ENOSYS.into()) 60 | } 61 | 62 | /// read symbolic link. 63 | async fn readlink(&self, req: Request, inode: Inode) -> Result { 64 | Err(libc::ENOSYS.into()) 65 | } 66 | 67 | /// create a symbolic link. 68 | async fn symlink( 69 | &self, 70 | req: Request, 71 | parent: Inode, 72 | name: &OsStr, 73 | link: &OsStr, 74 | ) -> Result { 75 | Err(libc::ENOSYS.into()) 76 | } 77 | 78 | /// create file node. Create a regular file, character device, block device, fifo or socket 79 | /// node. When creating file, most cases user only need to implement 80 | /// [`create`][Filesystem::create]. 81 | async fn mknod( 82 | &self, 83 | req: Request, 84 | parent: Inode, 85 | name: &OsStr, 86 | mode: u32, 87 | rdev: u32, 88 | ) -> Result { 89 | Err(libc::ENOSYS.into()) 90 | } 91 | 92 | /// create a directory. 93 | async fn mkdir( 94 | &self, 95 | req: Request, 96 | parent: Inode, 97 | name: &OsStr, 98 | mode: u32, 99 | umask: u32, 100 | ) -> Result { 101 | Err(libc::ENOSYS.into()) 102 | } 103 | 104 | /// remove a file. 105 | async fn unlink(&self, req: Request, parent: Inode, name: &OsStr) -> Result<()> { 106 | Err(libc::ENOSYS.into()) 107 | } 108 | 109 | /// remove a directory. 110 | async fn rmdir(&self, req: Request, parent: Inode, name: &OsStr) -> Result<()> { 111 | Err(libc::ENOSYS.into()) 112 | } 113 | 114 | /// rename a file or directory. 115 | async fn rename( 116 | &self, 117 | req: Request, 118 | parent: Inode, 119 | name: &OsStr, 120 | new_parent: Inode, 121 | new_name: &OsStr, 122 | ) -> Result<()> { 123 | Err(libc::ENOSYS.into()) 124 | } 125 | 126 | /// create a hard link. 127 | async fn link( 128 | &self, 129 | req: Request, 130 | inode: Inode, 131 | new_parent: Inode, 132 | new_name: &OsStr, 133 | ) -> Result { 134 | Err(libc::ENOSYS.into()) 135 | } 136 | 137 | /// open a file. Open flags (with the exception of `O_CREAT`, `O_EXCL` and `O_NOCTTY`) are 138 | /// available in flags. Filesystem may store an arbitrary file handle (pointer, index, etc) in 139 | /// fh, and use this in other all other file operations (read, write, flush, release, fsync). 140 | /// Filesystem may also implement stateless file I/O and not store anything in fh. There are 141 | /// also some flags (`direct_io`, `keep_cache`) which the filesystem may set, to change the way 142 | /// the file is opened. A filesystem need not implement this method if it 143 | /// sets [`MountOptions::no_open_support`][crate::MountOptions::no_open_support] and if the 144 | /// kernel supports `FUSE_NO_OPEN_SUPPORT`. 145 | /// 146 | /// # Notes: 147 | /// 148 | /// See `fuse_file_info` structure in 149 | /// [fuse_common.h](https://libfuse.github.io/doxygen/include_2fuse__common_8h_source.html) for 150 | /// more details. 151 | async fn open(&self, req: Request, inode: Inode, flags: u32) -> Result { 152 | Err(libc::ENOSYS.into()) 153 | } 154 | 155 | /// read data. Read should send exactly the number of bytes requested except on EOF or error, 156 | /// otherwise the rest of the data will be substituted with zeroes. An exception to this is 157 | /// when the file has been opened in `direct_io` mode, in which case the return value of the 158 | /// read system call will reflect the return value of this operation. `fh` will contain the 159 | /// value set by the open method, or will be undefined if the open method didn't set any value. 160 | async fn read( 161 | &self, 162 | req: Request, 163 | inode: Inode, 164 | fh: u64, 165 | offset: u64, 166 | size: u32, 167 | ) -> Result { 168 | Err(libc::ENOSYS.into()) 169 | } 170 | 171 | /// write data. Write should return exactly the number of bytes requested except on error. An 172 | /// exception to this is when the file has been opened in `direct_io` mode, in which case the 173 | /// return value of the write system call will reflect the return value of this operation. `fh` 174 | /// will contain the value set by the open method, or will be undefined if the open method 175 | /// didn't set any value. When `write_flags` contains 176 | /// [`FUSE_WRITE_CACHE`](crate::raw::flags::FUSE_WRITE_CACHE), means the write operation is a 177 | /// delay write. 178 | #[allow(clippy::too_many_arguments)] 179 | async fn write( 180 | &self, 181 | req: Request, 182 | inode: Inode, 183 | fh: u64, 184 | offset: u64, 185 | data: &[u8], 186 | write_flags: u32, 187 | flags: u32, 188 | ) -> Result { 189 | Err(libc::ENOSYS.into()) 190 | } 191 | 192 | /// get filesystem statistics. 193 | async fn statfs(&self, req: Request, inode: Inode) -> Result { 194 | Err(libc::ENOSYS.into()) 195 | } 196 | 197 | /// release an open file. Release is called when there are no more references to an open file: 198 | /// all file descriptors are closed and all memory mappings are unmapped. For every open call 199 | /// there will be exactly one release call. The filesystem may reply with an error, but error 200 | /// values are not returned to `close()` or `munmap()` which triggered the release. `fh` will 201 | /// contain the value set by the open method, or will be undefined if the open method didn't 202 | /// set any value. `flags` will contain the same flags as for open. `flush` means flush the 203 | /// data or not when closing file. 204 | async fn release( 205 | &self, 206 | req: Request, 207 | inode: Inode, 208 | fh: u64, 209 | flags: u32, 210 | lock_owner: u64, 211 | flush: bool, 212 | ) -> Result<()> { 213 | Err(libc::ENOSYS.into()) 214 | } 215 | 216 | /// synchronize file contents. If the `datasync` is true, then only the user data should be 217 | /// flushed, not the metadata. 218 | async fn fsync(&self, req: Request, inode: Inode, fh: u64, datasync: bool) -> Result<()> { 219 | Ok(()) 220 | } 221 | 222 | /// set an extended attribute. 223 | async fn setxattr( 224 | &self, 225 | req: Request, 226 | inode: Inode, 227 | name: &OsStr, 228 | value: &[u8], 229 | flags: u32, 230 | position: u32, 231 | ) -> Result<()> { 232 | Err(libc::ENOSYS.into()) 233 | } 234 | 235 | /// Get an extended attribute. If `size` is too small, return `Err`. 236 | /// Otherwise, use [`ReplyXAttr::Data`] to send the attribute data, or 237 | /// return an error. 238 | async fn getxattr( 239 | &self, 240 | req: Request, 241 | inode: Inode, 242 | name: &OsStr, 243 | size: u32, 244 | ) -> Result { 245 | Err(libc::ENOSYS.into()) 246 | } 247 | 248 | /// List extended attribute names. 249 | /// 250 | /// If `size` is too small, return `Err`. Otherwise, use 251 | /// [`ReplyXAttr::Data`] to send the attribute list, or return an error. 252 | async fn listxattr(&self, req: Request, inode: Inode, size: u32) -> Result { 253 | Err(libc::ENOSYS.into()) 254 | } 255 | 256 | /// remove an extended attribute. 257 | async fn removexattr(&self, req: Request, inode: Inode, name: &OsStr) -> Result<()> { 258 | Err(libc::ENOSYS.into()) 259 | } 260 | 261 | /// flush method. This is called on each `close()` of the opened file. Since file descriptors 262 | /// can be duplicated (`dup`, `dup2`, `fork`), for one open call there may be many flush calls. 263 | /// Filesystems shouldn't assume that flush will always be called after some writes, or that if 264 | /// will be called at all. `fh` will contain the value set by the open method, or will be 265 | /// undefined if the open method didn't set any value. 266 | /// 267 | /// # Notes: 268 | /// 269 | /// the name of the method is misleading, since (unlike fsync) the filesystem is not forced to 270 | /// flush pending writes. One reason to flush data, is if the filesystem wants to return write 271 | /// errors. If the filesystem supports file locking operations ([`setlk`][Filesystem::setlk], 272 | /// [`getlk`][Filesystem::getlk]) it should remove all locks belonging to `lock_owner`. 273 | async fn flush(&self, req: Request, inode: Inode, fh: u64, lock_owner: u64) -> Result<()> { 274 | Err(libc::ENOSYS.into()) 275 | } 276 | 277 | /// open a directory. Filesystem may store an arbitrary file handle (pointer, index, etc) in 278 | /// `fh`, and use this in other all other directory stream operations 279 | /// ([`readdir`][Filesystem::readdir], [`releasedir`][Filesystem::releasedir], 280 | /// [`fsyncdir`][Filesystem::fsyncdir]). Filesystem may also implement stateless directory 281 | /// I/O and not store anything in `fh`. A file system need not implement this method if it 282 | /// sets [`MountOptions::no_open_dir_support`][crate::MountOptions::no_open_dir_support] and 283 | /// if the kernel supports `FUSE_NO_OPENDIR_SUPPORT`. 284 | async fn opendir(&self, req: Request, inode: Inode, flags: u32) -> Result { 285 | Err(libc::ENOSYS.into()) 286 | } 287 | 288 | /// read directory. `offset` is used to track the offset of the directory entries. `fh` will 289 | /// contain the value set by the [`opendir`][Filesystem::opendir] method, or will be 290 | /// undefined if the [`opendir`][Filesystem::opendir] method didn't set any value. 291 | async fn readdir<'a>( 292 | &'a self, 293 | req: Request, 294 | parent: Inode, 295 | fh: u64, 296 | offset: i64, 297 | ) -> Result> + Send + 'a>> { 298 | Err::>, _>(libc::ENOSYS.into()) 299 | } 300 | 301 | /// release an open directory. For every [`opendir`][Filesystem::opendir] call there will 302 | /// be exactly one `releasedir` call. `fh` will contain the value set by the 303 | /// [`opendir`][Filesystem::opendir] method, or will be undefined if the 304 | /// [`opendir`][Filesystem::opendir] method didn't set any value. 305 | async fn releasedir(&self, req: Request, inode: Inode, fh: u64, flags: u32) -> Result<()> { 306 | Ok(()) 307 | } 308 | 309 | /// synchronize directory contents. If the `datasync` is true, then only the directory contents 310 | /// should be flushed, not the metadata. `fh` will contain the value set by the 311 | /// [`opendir`][Filesystem::opendir] method, or will be undefined if the 312 | /// [`opendir`][Filesystem::opendir] method didn't set any value. 313 | async fn fsyncdir(&self, req: Request, inode: Inode, fh: u64, datasync: bool) -> Result<()> { 314 | Err(libc::ENOSYS.into()) 315 | } 316 | 317 | #[cfg(feature = "file-lock")] 318 | /// test for a POSIX file lock. 319 | /// 320 | /// # Notes: 321 | /// 322 | /// this is supported on enable **`file-lock`** feature. 323 | #[allow(clippy::too_many_arguments)] 324 | async fn getlk( 325 | &self, 326 | req: Request, 327 | inode: Inode, 328 | fh: u64, 329 | lock_owner: u64, 330 | start: u64, 331 | end: u64, 332 | r#type: u32, 333 | pid: u32, 334 | ) -> Result; 335 | 336 | #[cfg(feature = "file-lock")] 337 | /// acquire, modify or release a POSIX file lock. 338 | /// 339 | /// # Notes: 340 | /// 341 | /// this is supported on enable **`file-lock`** feature. 342 | #[allow(clippy::too_many_arguments)] 343 | async fn setlk( 344 | &self, 345 | req: Request, 346 | inode: Inode, 347 | fh: u64, 348 | lock_owner: u64, 349 | start: u64, 350 | end: u64, 351 | r#type: u32, 352 | pid: u32, 353 | block: bool, 354 | ) -> Result<()>; 355 | 356 | /// check file access permissions. This will be called for the `access()` system call. If the 357 | /// `default_permissions` mount option is given, this method is not be called. This method is 358 | /// not called under Linux kernel versions 2.4.x. 359 | async fn access(&self, req: Request, inode: Inode, mask: u32) -> Result<()> { 360 | Err(libc::ENOSYS.into()) 361 | } 362 | 363 | /// create and open a file. If the file does not exist, first create it with the specified 364 | /// mode, and then open it. Open flags (with the exception of `O_NOCTTY`) are available in 365 | /// flags. Filesystem may store an arbitrary file handle (pointer, index, etc) in `fh`, and use 366 | /// this in other all other file operations ([`read`][Filesystem::read], 367 | /// [`write`][Filesystem::write], [`flush`][Filesystem::flush], 368 | /// [`release`][Filesystem::release], [`fsync`][Filesystem::fsync]). There are also some flags 369 | /// (`direct_io`, `keep_cache`) which the filesystem may set, to change the way the file is 370 | /// opened. If this method is not implemented or under Linux kernel versions earlier than 371 | /// 2.6.15, the [`mknod`][Filesystem::mknod] and [`open`][Filesystem::open] methods will be 372 | /// called instead. 373 | /// 374 | /// # Notes: 375 | /// 376 | /// See `fuse_file_info` structure in 377 | /// [fuse_common.h](https://libfuse.github.io/doxygen/include_2fuse__common_8h_source.html) for 378 | /// more details. 379 | async fn create( 380 | &self, 381 | req: Request, 382 | parent: Inode, 383 | name: &OsStr, 384 | mode: u32, 385 | flags: u32, 386 | ) -> Result { 387 | Err(libc::ENOSYS.into()) 388 | } 389 | 390 | /// handle interrupt. When a operation is interrupted, an interrupt request will send to fuse 391 | /// server with the unique id of the operation. 392 | async fn interrupt(&self, req: Request, unique: u64) -> Result<()> { 393 | Err(libc::ENOSYS.into()) 394 | } 395 | 396 | /// map block index within file to block index within device. 397 | /// 398 | /// # Notes: 399 | /// 400 | /// This may not works because currently this crate doesn't support fuseblk mode yet. 401 | async fn bmap( 402 | &self, 403 | req: Request, 404 | inode: Inode, 405 | blocksize: u32, 406 | idx: u64, 407 | ) -> Result { 408 | Err(libc::ENOSYS.into()) 409 | } 410 | 411 | /*async fn ioctl( 412 | &self, 413 | req: Request, 414 | inode: Inode, 415 | fh: u64, 416 | flags: u32, 417 | cmd: u32, 418 | arg: u64, 419 | in_size: u32, 420 | out_size: u32, 421 | ) -> Result { 422 | Err(libc::ENOSYS.into()) 423 | }*/ 424 | 425 | /// poll for IO readiness events. 426 | #[allow(clippy::too_many_arguments)] 427 | async fn poll( 428 | &self, 429 | req: Request, 430 | inode: Inode, 431 | fh: u64, 432 | kh: Option, 433 | flags: u32, 434 | events: u32, 435 | notify: &Notify, 436 | ) -> Result { 437 | Err(libc::ENOSYS.into()) 438 | } 439 | 440 | /// receive notify reply from kernel. 441 | async fn notify_reply( 442 | &self, 443 | req: Request, 444 | inode: Inode, 445 | offset: u64, 446 | data: Bytes, 447 | ) -> Result<()> { 448 | Err(libc::ENOSYS.into()) 449 | } 450 | 451 | /// forget more than one inode. This is a batch version [`forget`][Filesystem::forget] 452 | async fn batch_forget(&self, req: Request, inodes: &[Inode]) {} 453 | 454 | /// allocate space for an open file. This function ensures that required space is allocated for 455 | /// specified file. 456 | /// 457 | /// # Notes: 458 | /// 459 | /// more information about `fallocate`, please see **`man 2 fallocate`** 460 | async fn fallocate( 461 | &self, 462 | req: Request, 463 | inode: Inode, 464 | fh: u64, 465 | offset: u64, 466 | length: u64, 467 | mode: u32, 468 | ) -> Result<()> { 469 | Err(libc::ENOSYS.into()) 470 | } 471 | 472 | /// read directory entries, but with their attribute, like [`readdir`][Filesystem::readdir] 473 | /// + [`lookup`][Filesystem::lookup] at the same time. 474 | async fn readdirplus<'a>( 475 | &'a self, 476 | req: Request, 477 | parent: Inode, 478 | fh: u64, 479 | offset: u64, 480 | lock_owner: u64, 481 | ) -> Result> + Send + 'a>> 482 | { 483 | Err::>, _>(libc::ENOSYS.into()) 484 | } 485 | 486 | /// rename a file or directory with flags. 487 | async fn rename2( 488 | &self, 489 | req: Request, 490 | parent: Inode, 491 | name: &OsStr, 492 | new_parent: Inode, 493 | new_name: &OsStr, 494 | flags: u32, 495 | ) -> Result<()> { 496 | Err(libc::ENOSYS.into()) 497 | } 498 | 499 | /// find next data or hole after the specified offset. 500 | async fn lseek( 501 | &self, 502 | req: Request, 503 | inode: Inode, 504 | fh: u64, 505 | offset: u64, 506 | whence: u32, 507 | ) -> Result { 508 | Err(libc::ENOSYS.into()) 509 | } 510 | 511 | /// copy a range of data from one file to another. This can improve performance because it 512 | /// reduce data copy: in normal, data will copy from FUSE server to kernel, then to user-space, 513 | /// then to kernel, finally send back to FUSE server. By implement this method, data will only 514 | /// copy in FUSE server internal. 515 | #[allow(clippy::too_many_arguments)] 516 | async fn copy_file_range( 517 | &self, 518 | req: Request, 519 | inode: Inode, 520 | fh_in: u64, 521 | off_in: u64, 522 | inode_out: Inode, 523 | fh_out: u64, 524 | off_out: u64, 525 | length: u64, 526 | flags: u64, 527 | ) -> Result { 528 | Err(libc::ENOSYS.into()) 529 | } 530 | 531 | // TODO setupmapping and removemapping 532 | } 533 | -------------------------------------------------------------------------------- /src/raw/connection/tokio.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use std::env; 3 | #[cfg(any(target_os = "linux", target_os = "macos"))] 4 | use std::fs::File; 5 | use std::fs::OpenOptions; 6 | use std::io; 7 | #[cfg(any( 8 | all(target_os = "linux", feature = "unprivileged"), 9 | target_os = "freebsd", 10 | target_os = "macos", 11 | ))] 12 | use std::io::ErrorKind; 13 | #[cfg(any(target_os = "linux", target_os = "macos"))] 14 | use std::io::Write; 15 | use std::io::{IoSlice, IoSliceMut}; 16 | use std::ops::{Deref, DerefMut}; 17 | #[cfg(any( 18 | all(target_os = "linux", feature = "unprivileged"), 19 | target_os = "freebsd", 20 | target_os = "macos", 21 | ))] 22 | use std::os::fd::OwnedFd; 23 | use std::os::fd::{AsFd, BorrowedFd}; 24 | #[cfg(any(target_os = "freebsd", target_os = "macos"))] 25 | use std::os::unix::fs::OpenOptionsExt; 26 | #[cfg(any( 27 | all(target_os = "linux", feature = "unprivileged"), 28 | target_os = "macos" 29 | ))] 30 | use std::os::unix::io::RawFd; 31 | #[cfg(target_os = "macos")] 32 | use std::os::unix::io::{AsRawFd, FromRawFd}; 33 | use std::pin::pin; 34 | use std::sync::Arc; 35 | #[cfg(any( 36 | all(target_os = "linux", feature = "unprivileged"), 37 | target_os = "macos" 38 | ))] 39 | use std::{ffi::OsString, path::Path}; 40 | 41 | use async_notify::Notify; 42 | use futures_util::lock::Mutex; 43 | use futures_util::{select, FutureExt}; 44 | #[cfg(any( 45 | all(target_os = "linux", feature = "unprivileged"), 46 | target_os = "freebsd", 47 | target_os = "macos", 48 | ))] 49 | use nix::sys::uio; 50 | #[cfg(any( 51 | all(target_os = "linux", feature = "unprivileged"), 52 | target_os = "macos" 53 | ))] 54 | use nix::{ 55 | fcntl::{FcntlArg, OFlag}, 56 | sys::socket::{self, AddressFamily, ControlMessageOwned, MsgFlags, SockFlag, SockType}, 57 | }; 58 | #[cfg(any( 59 | all(target_os = "linux", feature = "unprivileged"), 60 | target_os = "freebsd", 61 | target_os = "macos", 62 | ))] 63 | use tokio::io::unix::AsyncFd; 64 | #[cfg(any( 65 | all(target_os = "linux", feature = "unprivileged"), 66 | target_os = "freebsd", 67 | ))] 68 | use tokio::io::Interest; 69 | #[cfg(any( 70 | all(target_os = "linux", feature = "unprivileged"), 71 | target_os = "macos" 72 | ))] 73 | use tokio::process::Command; 74 | #[cfg(any(target_os = "linux", target_os = "macos"))] 75 | use tokio::task; 76 | #[cfg(any( 77 | all(target_os = "linux", feature = "unprivileged"), 78 | target_os = "macos" 79 | ))] 80 | use tracing::debug; 81 | #[cfg(any(target_os = "freebsd", target_os = "macos"))] 82 | use tracing::warn; 83 | 84 | use super::CompleteIoResult; 85 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 86 | use crate::find_fusermount3; 87 | #[cfg(any( 88 | all(target_os = "linux", feature = "unprivileged"), 89 | target_os = "macos" 90 | ))] 91 | use crate::MountOptions; 92 | 93 | #[derive(Debug)] 94 | pub struct FuseConnection { 95 | unmount_notify: Arc, 96 | mode: ConnectionMode, 97 | } 98 | 99 | impl FuseConnection { 100 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 101 | pub fn new(unmount_notify: Arc) -> io::Result { 102 | #[cfg(target_os = "freebsd")] 103 | { 104 | let connection = NonBlockFuseConnection::new()?; 105 | 106 | Ok(Self { 107 | unmount_notify, 108 | mode: ConnectionMode::NonBlock(connection), 109 | }) 110 | } 111 | 112 | #[cfg(target_os = "linux")] 113 | { 114 | let connection = BlockFuseConnection::new()?; 115 | 116 | Ok(Self { 117 | unmount_notify, 118 | mode: ConnectionMode::Block(connection), 119 | }) 120 | } 121 | } 122 | 123 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 124 | pub async fn new_with_unprivileged( 125 | mount_options: MountOptions, 126 | mount_path: impl AsRef, 127 | unmount_notify: Arc, 128 | ) -> io::Result { 129 | let connection = 130 | NonBlockFuseConnection::new_with_unprivileged(mount_options, mount_path).await?; 131 | 132 | Ok(Self { 133 | unmount_notify, 134 | mode: ConnectionMode::NonBlock(connection), 135 | }) 136 | } 137 | 138 | #[cfg(target_os = "macos")] 139 | pub async fn new_with_unprivileged( 140 | mount_options: MountOptions, 141 | mount_path: impl AsRef, 142 | unmount_notify: Arc, 143 | ) -> io::Result { 144 | let connection = 145 | BlockFuseConnection::new_with_unprivileged(mount_options, mount_path).await?; 146 | 147 | Ok(Self { 148 | unmount_notify, 149 | mode: ConnectionMode::Block(connection), 150 | }) 151 | } 152 | 153 | pub async fn read_vectored + Send + 'static>( 154 | &self, 155 | header_buf: Vec, 156 | data_buf: T, 157 | ) -> Option, T), usize>> { 158 | let mut unmount_fut = pin!(self.unmount_notify.notified().fuse()); 159 | let mut read_fut = pin!(self.inner_read_vectored(header_buf, data_buf).fuse()); 160 | 161 | select! { 162 | _ = unmount_fut => None, 163 | res = read_fut => Some(res) 164 | } 165 | } 166 | 167 | async fn inner_read_vectored + Send + 'static>( 168 | &self, 169 | header_buf: Vec, 170 | data_buf: T, 171 | ) -> CompleteIoResult<(Vec, T), usize> { 172 | match &self.mode { 173 | #[cfg(any(target_os = "linux", target_os = "macos"))] 174 | ConnectionMode::Block(connection) => { 175 | connection.read_vectored(header_buf, data_buf).await 176 | } 177 | #[cfg(any( 178 | all(target_os = "linux", feature = "unprivileged"), 179 | target_os = "freebsd", 180 | ))] 181 | ConnectionMode::NonBlock(connection) => { 182 | connection.read_vectored(header_buf, data_buf).await 183 | } 184 | } 185 | } 186 | 187 | pub async fn write_vectored + Send, U: Deref + Send>( 188 | &self, 189 | data: T, 190 | body_extend_data: Option, 191 | ) -> CompleteIoResult<(T, Option), usize> { 192 | match &self.mode { 193 | #[cfg(any(target_os = "linux", target_os = "macos"))] 194 | ConnectionMode::Block(connection) => { 195 | connection.write_vectored(data, body_extend_data).await 196 | } 197 | #[cfg(any( 198 | all(target_os = "linux", feature = "unprivileged"), 199 | target_os = "freebsd", 200 | ))] 201 | ConnectionMode::NonBlock(connection) => { 202 | connection.write_vectored(data, body_extend_data).await 203 | } 204 | } 205 | } 206 | } 207 | 208 | #[derive(Debug)] 209 | enum ConnectionMode { 210 | #[cfg(any(target_os = "linux", target_os = "macos"))] 211 | Block(BlockFuseConnection), 212 | #[cfg(any( 213 | all(target_os = "linux", feature = "unprivileged"), 214 | target_os = "freebsd", 215 | ))] 216 | NonBlock(NonBlockFuseConnection), 217 | } 218 | 219 | #[cfg(any(target_os = "linux", target_os = "macos"))] 220 | #[derive(Debug)] 221 | struct BlockFuseConnection { 222 | file: File, 223 | read: Mutex<()>, 224 | write: Mutex<()>, 225 | } 226 | 227 | #[cfg(any(target_os = "linux", target_os = "macos"))] 228 | impl BlockFuseConnection { 229 | #[cfg(target_os = "linux")] 230 | pub fn new() -> io::Result { 231 | const DEV_FUSE: &str = "/dev/fuse"; 232 | 233 | let file = OpenOptions::new().write(true).read(true).open(DEV_FUSE)?; 234 | 235 | Ok(Self { 236 | file, 237 | read: Mutex::new(()), 238 | write: Mutex::new(()), 239 | }) 240 | } 241 | 242 | #[cfg(target_os = "macos")] 243 | async fn new_with_unprivileged( 244 | mount_options: MountOptions, 245 | mount_path: impl AsRef, 246 | ) -> io::Result { 247 | use std::{thread, time::Duration}; 248 | 249 | use tokio::time::sleep; 250 | 251 | use crate::find_macfuse_mount; 252 | 253 | let (sock0, sock1) = match socket::socketpair( 254 | AddressFamily::Unix, 255 | SockType::Stream, 256 | None, 257 | SockFlag::empty(), 258 | ) { 259 | Err(err) => return Err(err.into()), 260 | 261 | Ok((sock0, sock1)) => (sock0, sock1), 262 | }; 263 | 264 | let binary_path = find_macfuse_mount()?; 265 | 266 | const ENV: &str = "_FUSE_COMMFD"; 267 | 268 | let options = mount_options.build(); 269 | 270 | debug!("mount options {:?}", options); 271 | 272 | let exec_path = match env::current_exe() { 273 | Ok(path) => path, 274 | Err(err) => return Err(err), 275 | }; 276 | 277 | let mount_path = mount_path.as_ref().as_os_str().to_os_string(); 278 | // macfuse_mound will block until fuse init done, so we can not join it in the current function 279 | tokio::spawn(async move { 280 | debug!("mount_thread start"); 281 | let fd0 = sock0.as_raw_fd(); 282 | let mut binding = Command::new(binary_path); 283 | let child = binding 284 | .env(ENV, fd0.to_string()) 285 | .env("_FUSE_CALL_BY_LIB", "1") 286 | .env("_FUSE_COMMVERS", "2") 287 | .env("_FUSE_DAEMON_PATH", exec_path) 288 | .args(vec![options, mount_path]); 289 | let status = child.spawn()?.wait().await?; 290 | 291 | if status.success() { 292 | Ok(()) 293 | } else { 294 | Err(io::Error::new( 295 | io::ErrorKind::Other, 296 | "fusermount run failed", 297 | )) 298 | } 299 | }); 300 | 301 | let fd1 = sock1.as_raw_fd(); 302 | // wait for macfuse mount 303 | let fd = task::spawn_blocking(move || { 304 | debug!("wait_thread start"); 305 | // wait for macfuse mount command start 306 | // it seems that socket::recvmsg will not block to wait for the message 307 | // so we need to sleep for a while 308 | thread::sleep(Duration::from_secs(1)); 309 | // let mut buf = vec![0; 10000]; // buf should large enough 310 | let mut buf = vec![]; // it seems 0 len still works well 311 | 312 | let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]); 313 | 314 | let mut bufs = [IoSliceMut::new(&mut buf)]; 315 | 316 | let msg = match socket::recvmsg::<()>( 317 | fd1, 318 | &mut bufs[..], 319 | Some(&mut cmsg_buf), 320 | MsgFlags::empty(), 321 | ) { 322 | Err(err) => return Err(err.into()), 323 | 324 | Ok(msg) => msg, 325 | }; 326 | 327 | let mut cmsgs = match msg.cmsgs() { 328 | Err(err) => return Err(err.into()), 329 | Ok(cmsgs) => cmsgs, 330 | }; 331 | let fd = if let Some(ControlMessageOwned::ScmRights(fds)) = cmsgs.next() { 332 | if fds.is_empty() { 333 | return Err(io::Error::new(ErrorKind::Other, "no fuse fd")); 334 | } 335 | 336 | fds[0] 337 | } else { 338 | return Err(io::Error::new(ErrorKind::Other, "get fuse fd failed")); 339 | }; 340 | 341 | Ok(fd) 342 | }) 343 | .await 344 | .unwrap()?; 345 | 346 | let file = unsafe { File::from_raw_fd(fd) }; 347 | Ok(Self { 348 | file, 349 | read: Mutex::new(()), 350 | write: Mutex::new(()), 351 | }) 352 | } 353 | 354 | async fn read_vectored + Send + 'static>( 355 | &self, 356 | mut header_buf: Vec, 357 | mut data_buf: T, 358 | ) -> CompleteIoResult<(Vec, T), usize> { 359 | use std::io::Read; 360 | use std::mem::ManuallyDrop; 361 | use std::os::fd::{AsRawFd, FromRawFd}; 362 | 363 | let _guard = self.read.lock().await; 364 | let fd = self.file.as_raw_fd(); 365 | 366 | let ((header_buf, data_buf), res) = task::spawn_blocking(move || { 367 | // Safety: when we call read, the fd is still valid, when fd is closed and file is 368 | // dropped, the read operation will return error 369 | let file = unsafe { File::from_raw_fd(fd) }; 370 | // avoid close the file 371 | let mut file = ManuallyDrop::new(file); 372 | 373 | let res = file.read_vectored(&mut [ 374 | IoSliceMut::new(&mut header_buf), 375 | IoSliceMut::new(&mut data_buf), 376 | ]); 377 | 378 | ((header_buf, data_buf), res) 379 | }) 380 | .await 381 | .unwrap(); 382 | 383 | ((header_buf, data_buf), res) 384 | } 385 | 386 | async fn write_vectored + Send, U: Deref + Send>( 387 | &self, 388 | data: T, 389 | body_extend_data: Option, 390 | ) -> CompleteIoResult<(T, Option), usize> { 391 | let _guard = self.write.lock().await; 392 | 393 | let res = { 394 | let body_extend_data = body_extend_data.as_deref(); 395 | 396 | match body_extend_data { 397 | None => (&self.file).write_vectored(&[IoSlice::new(data.deref())]), 398 | 399 | Some(body_extend_data) => (&self.file) 400 | .write_vectored(&[IoSlice::new(data.deref()), IoSlice::new(body_extend_data)]), 401 | } 402 | }; 403 | 404 | match res { 405 | Err(err) => ((data, body_extend_data), Err(err)), 406 | Ok(n) => ((data, body_extend_data), Ok(n)), 407 | } 408 | } 409 | } 410 | 411 | #[cfg(any( 412 | all(target_os = "linux", feature = "unprivileged"), 413 | target_os = "freebsd", 414 | ))] 415 | #[derive(Debug)] 416 | struct NonBlockFuseConnection { 417 | fd: AsyncFd, 418 | read: Mutex<()>, 419 | write: Mutex<()>, 420 | } 421 | 422 | #[cfg(any( 423 | all(target_os = "linux", feature = "unprivileged"), 424 | target_os = "freebsd", 425 | ))] 426 | impl NonBlockFuseConnection { 427 | #[cfg(any(target_os = "freebsd", target_os = "macos"))] 428 | fn new() -> io::Result { 429 | #[cfg(target_os = "freebsd")] 430 | const DEV_FUSE: &str = "/dev/fuse"; 431 | 432 | match OpenOptions::new() 433 | .write(true) 434 | .read(true) 435 | .custom_flags(libc::O_NONBLOCK) 436 | .open(DEV_FUSE) 437 | { 438 | Err(e) => { 439 | if e.kind() == ErrorKind::NotFound { 440 | warn!("Cannot open {}. Is the module loaded?", DEV_FUSE); 441 | } 442 | warn!("Cannot open {}. err: {:?}", DEV_FUSE, e); 443 | Err(e) 444 | } 445 | Ok(file) => Ok(Self { 446 | fd: AsyncFd::new(file.into())?, 447 | read: Mutex::new(()), 448 | write: Mutex::new(()), 449 | }), 450 | } 451 | } 452 | 453 | #[cfg(all(target_os = "linux", feature = "unprivileged"))] 454 | async fn new_with_unprivileged( 455 | mount_options: MountOptions, 456 | mount_path: impl AsRef, 457 | ) -> io::Result { 458 | use std::os::fd::{AsRawFd, FromRawFd}; 459 | 460 | let (sock0, sock1) = match socket::socketpair( 461 | AddressFamily::Unix, 462 | SockType::SeqPacket, 463 | None, 464 | SockFlag::empty(), 465 | ) { 466 | Err(err) => return Err(err.into()), 467 | 468 | Ok((sock0, sock1)) => (sock0, sock1), 469 | }; 470 | 471 | let binary_path = find_fusermount3()?; 472 | 473 | const ENV: &str = "_FUSE_COMMFD"; 474 | 475 | let options = mount_options.build_with_unprivileged(); 476 | 477 | debug!("mount options {:?}", options); 478 | 479 | let mount_path = mount_path.as_ref().as_os_str().to_os_string(); 480 | 481 | let fd0 = sock0.as_raw_fd(); 482 | let mut child = Command::new(binary_path) 483 | .env(ENV, fd0.to_string()) 484 | .args(vec![OsString::from("-o"), options, mount_path]) 485 | .spawn()?; 486 | 487 | if !child.wait().await?.success() { 488 | return Err(io::Error::new( 489 | io::ErrorKind::Other, 490 | "fusermount run failed", 491 | )); 492 | } 493 | 494 | let fd1 = sock1.as_raw_fd(); 495 | let fd = task::spawn_blocking(move || { 496 | // let mut buf = vec![0; 10000]; // buf should large enough 497 | let mut buf = vec![]; // it seems 0 len still works well 498 | 499 | let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]); 500 | 501 | let mut bufs = [IoSliceMut::new(&mut buf)]; 502 | 503 | let msg = match socket::recvmsg::<()>( 504 | fd1, 505 | &mut bufs[..], 506 | Some(&mut cmsg_buf), 507 | MsgFlags::empty(), 508 | ) { 509 | Err(err) => return Err(err.into()), 510 | 511 | Ok(msg) => msg, 512 | }; 513 | 514 | let fd = if let Some(ControlMessageOwned::ScmRights(fds)) = msg.cmsgs()?.next() { 515 | if fds.is_empty() { 516 | return Err(io::Error::new(ErrorKind::Other, "no fuse fd")); 517 | } 518 | 519 | fds[0] 520 | } else { 521 | return Err(io::Error::new(ErrorKind::Other, "get fuse fd failed")); 522 | }; 523 | 524 | Ok(fd) 525 | }) 526 | .await 527 | .unwrap()?; 528 | 529 | Self::set_fd_non_blocking(fd)?; 530 | 531 | // Safety: fd is valid 532 | let fd = unsafe { OwnedFd::from_raw_fd(fd) }; 533 | 534 | Ok(Self { 535 | fd: AsyncFd::new(fd)?, 536 | read: Mutex::new(()), 537 | write: Mutex::new(()), 538 | }) 539 | } 540 | 541 | #[cfg(any( 542 | all(target_os = "linux", feature = "unprivileged"), 543 | target_os = "macos" 544 | ))] 545 | fn set_fd_non_blocking(fd: RawFd) -> io::Result<()> { 546 | let flags = nix::fcntl::fcntl(fd, FcntlArg::F_GETFL).map_err(io::Error::from)?; 547 | debug!( 548 | "set fd {:?} to non-blocking", 549 | OFlag::from_bits_truncate(flags) 550 | ); 551 | let flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK; 552 | 553 | debug!("set fd {:?} to non-blocking", flags); 554 | nix::fcntl::fcntl(fd, FcntlArg::F_SETFL(flags)).map_err(io::Error::from)?; 555 | 556 | Ok(()) 557 | } 558 | 559 | async fn read_vectored + Send>( 560 | &self, 561 | mut header_buf: Vec, 562 | mut data_buf: T, 563 | ) -> CompleteIoResult<(Vec, T), usize> { 564 | let _guard = self.read.lock().await; 565 | 566 | loop { 567 | let mut read_guard = match self.fd.ready(Interest::READABLE | Interest::ERROR).await { 568 | Err(err) => return ((header_buf, data_buf), Err(err)), 569 | Ok(read_guard) => read_guard, 570 | }; 571 | 572 | if let Ok(result) = read_guard.try_io(|fd| { 573 | uio::readv( 574 | fd, 575 | &mut [ 576 | IoSliceMut::new(&mut header_buf), 577 | IoSliceMut::new(&mut data_buf), 578 | ], 579 | ) 580 | .map_err(io::Error::from) 581 | }) { 582 | return ((header_buf, data_buf), result); 583 | } else { 584 | continue; 585 | } 586 | } 587 | } 588 | 589 | async fn write_vectored + Send, U: Deref + Send>( 590 | &self, 591 | data: T, 592 | body_extend_data: Option, 593 | ) -> CompleteIoResult<(T, Option), usize> { 594 | let _guard = self.write.lock().await; 595 | 596 | let res = { 597 | let body_extend_data = body_extend_data.as_deref(); 598 | 599 | match body_extend_data { 600 | None => uio::writev(&self.fd, &[IoSlice::new(data.deref())]), 601 | 602 | Some(body_extend_data) => uio::writev( 603 | &self.fd, 604 | &[IoSlice::new(data.deref()), IoSlice::new(body_extend_data)], 605 | ), 606 | } 607 | }; 608 | 609 | match res { 610 | Err(err) => ((data, body_extend_data), Err(err.into())), 611 | Ok(n) => ((data, body_extend_data), Ok(n)), 612 | } 613 | } 614 | } 615 | 616 | impl AsFd for FuseConnection { 617 | fn as_fd(&self) -> BorrowedFd<'_> { 618 | match &self.mode { 619 | #[cfg(any(target_os = "linux", target_os = "macos"))] 620 | ConnectionMode::Block(connection) => connection.file.as_fd(), 621 | 622 | #[cfg(any( 623 | all(target_os = "linux", feature = "unprivileged"), 624 | target_os = "freebsd", 625 | ))] 626 | ConnectionMode::NonBlock(connection) => connection.fd.as_fd(), 627 | } 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /src/path/path_filesystem.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | use bytes::Bytes; 4 | use futures_util::stream::{Empty, Stream}; 5 | 6 | use super::reply::*; 7 | use super::Request; 8 | use crate::notify::Notify; 9 | use crate::{Result, SetAttr}; 10 | 11 | #[allow(unused_variables)] 12 | #[trait_make::make(Send)] 13 | /// Path based filesystem trait. 14 | pub trait PathFilesystem { 15 | /// initialize filesystem. Called before any other filesystem method. 16 | async fn init(&self, req: Request) -> Result; 17 | 18 | /// clean up filesystem. Called on filesystem exit which is fuseblk, in normal fuse filesystem, 19 | /// kernel may call forget for root. There is some discuss for this 20 | /// , 21 | /// 22 | async fn destroy(&self, req: Request); 23 | 24 | /// look up a directory entry by name and get its attributes. 25 | async fn lookup(&self, req: Request, parent: &OsStr, name: &OsStr) -> Result { 26 | Err(libc::ENOSYS.into()) 27 | } 28 | 29 | /// forget an path. The nlookup parameter indicates the number of lookups previously 30 | /// performed on this path. If the filesystem implements path lifetimes, it is recommended 31 | /// that paths acquire a single reference on each lookup, and lose nlookup references on each 32 | /// forget. The filesystem may ignore forget calls, if the paths don't need to have a limited 33 | /// lifetime. On unmount it is not guaranteed, that all referenced paths will receive a forget 34 | /// message. When filesystem is normal(not fuseblk) and unmounting, kernel may send forget 35 | /// request for root and this library will stop session after call forget. There is some 36 | /// discussion for this , 37 | /// 38 | /// 39 | async fn forget(&self, req: Request, parent: &OsStr, nlookup: u64) {} 40 | 41 | /// get file attributes. If `fh` is None, means `fh` is not set. If `path` is None, means the 42 | /// path may be deleted. 43 | async fn getattr( 44 | &self, 45 | req: Request, 46 | path: Option<&OsStr>, 47 | fh: Option, 48 | flags: u32, 49 | ) -> Result { 50 | Err(libc::ENOSYS.into()) 51 | } 52 | 53 | /// set file attributes. If `fh` is None, means `fh` is not set. If `path` is None, means the 54 | /// path may be deleted. 55 | async fn setattr( 56 | &self, 57 | req: Request, 58 | path: Option<&OsStr>, 59 | fh: Option, 60 | set_attr: SetAttr, 61 | ) -> Result { 62 | Err(libc::ENOSYS.into()) 63 | } 64 | 65 | /// read symbolic link. 66 | async fn readlink(&self, req: Request, path: &OsStr) -> Result { 67 | Err(libc::ENOSYS.into()) 68 | } 69 | 70 | /// create a symbolic link. 71 | async fn symlink( 72 | &self, 73 | req: Request, 74 | parent: &OsStr, 75 | name: &OsStr, 76 | link_path: &OsStr, 77 | ) -> Result { 78 | Err(libc::ENOSYS.into()) 79 | } 80 | 81 | /// create file node. Create a regular file, character device, block device, fifo or socket 82 | /// node. When creating file, most cases user only need to implement 83 | /// [`create`][PathFilesystem::create]. 84 | async fn mknod( 85 | &self, 86 | req: Request, 87 | parent: &OsStr, 88 | name: &OsStr, 89 | mode: u32, 90 | rdev: u32, 91 | ) -> Result { 92 | Err(libc::ENOSYS.into()) 93 | } 94 | 95 | /// create a directory. 96 | async fn mkdir( 97 | &self, 98 | req: Request, 99 | parent: &OsStr, 100 | name: &OsStr, 101 | mode: u32, 102 | umask: u32, 103 | ) -> Result { 104 | Err(libc::ENOSYS.into()) 105 | } 106 | 107 | /// remove a file. 108 | async fn unlink(&self, req: Request, parent: &OsStr, name: &OsStr) -> Result<()> { 109 | Err(libc::ENOSYS.into()) 110 | } 111 | 112 | /// remove a directory. 113 | async fn rmdir(&self, req: Request, parent: &OsStr, name: &OsStr) -> Result<()> { 114 | Err(libc::ENOSYS.into()) 115 | } 116 | 117 | /// rename a file or directory. 118 | async fn rename( 119 | &self, 120 | req: Request, 121 | origin_parent: &OsStr, 122 | origin_name: &OsStr, 123 | parent: &OsStr, 124 | name: &OsStr, 125 | ) -> Result<()> { 126 | Err(libc::ENOSYS.into()) 127 | } 128 | 129 | /// create a hard link. 130 | async fn link( 131 | &self, 132 | req: Request, 133 | path: &OsStr, 134 | new_parent: &OsStr, 135 | new_name: &OsStr, 136 | ) -> Result { 137 | Err(libc::ENOSYS.into()) 138 | } 139 | 140 | /// open a file. Open flags (with the exception of `O_CREAT`, `O_EXCL` and `O_NOCTTY`) are 141 | /// available in flags. Filesystem may store an arbitrary file handle (pointer, index, etc) in 142 | /// fh, and use this in other all other file operations (read, write, flush, release, fsync). 143 | /// Filesystem may also implement stateless file I/O and not store anything in fh. There are 144 | /// also some flags (`direct_io`, `keep_cache`) which the filesystem may set, to change the way 145 | /// the file is opened. A file system need not implement this method if it 146 | /// sets [`MountOptions::no_open_support`][crate::MountOptions::no_open_support] and if the 147 | /// kernel supports `FUSE_NO_OPEN_SUPPORT`. 148 | /// 149 | /// # Notes: 150 | /// 151 | /// See `fuse_file_info` structure in 152 | /// [fuse_common.h](https://libfuse.github.io/doxygen/include_2fuse__common_8h_source.html) for 153 | /// more details. 154 | async fn open(&self, req: Request, path: &OsStr, flags: u32) -> Result { 155 | Err(libc::ENOSYS.into()) 156 | } 157 | 158 | /// read data. Read should send exactly the number of bytes requested except on EOF or error, 159 | /// otherwise the rest of the data will be substituted with zeroes. An exception to this is 160 | /// when the file has been opened in `direct_io` mode, in which case the return value of the 161 | /// read system call will reflect the return value of this operation. `fh` will contain the 162 | /// value set by the open method, or will be undefined if the open method didn't set any value. 163 | /// when `path` is None, it means the path may be deleted. 164 | async fn read( 165 | &self, 166 | req: Request, 167 | path: Option<&OsStr>, 168 | fh: u64, 169 | offset: u64, 170 | size: u32, 171 | ) -> Result { 172 | Err(libc::ENOSYS.into()) 173 | } 174 | 175 | /// write data. Write should return exactly the number of bytes requested except on error. An 176 | /// exception to this is when the file has been opened in `direct_io` mode, in which case the 177 | /// return value of the write system call will reflect the return value of this operation. `fh` 178 | /// will contain the value set by the open method, or will be undefined if the open method 179 | /// didn't set any value. When `path` is None, it means the path may be deleted. When 180 | /// `write_flags` contains [`FUSE_WRITE_CACHE`](crate::raw::flags::FUSE_WRITE_CACHE), means the 181 | /// write operation is a delay write. 182 | #[allow(clippy::too_many_arguments)] 183 | async fn write( 184 | &self, 185 | req: Request, 186 | path: Option<&OsStr>, 187 | fh: u64, 188 | offset: u64, 189 | data: &[u8], 190 | write_flags: u32, 191 | flags: u32, 192 | ) -> Result { 193 | Err(libc::ENOSYS.into()) 194 | } 195 | 196 | /// get filesystem statistics. 197 | async fn statfs(&self, req: Request, path: &OsStr) -> Result { 198 | Err(libc::ENOSYS.into()) 199 | } 200 | 201 | /// release an open file. Release is called when there are no more references to an open file: 202 | /// all file descriptors are closed and all memory mappings are unmapped. For every open call 203 | /// there will be exactly one release call. The filesystem may reply with an error, but error 204 | /// values are not returned to `close()` or `munmap()` which triggered the release. `fh` will 205 | /// contain the value set by the open method, or will be undefined if the open method didn't 206 | /// set any value. `flags` will contain the same flags as for open. `flush` means flush the 207 | /// data or not when closing file. when `path` is None, it means the path may be deleted. 208 | async fn release( 209 | &self, 210 | req: Request, 211 | path: Option<&OsStr>, 212 | fh: u64, 213 | flags: u32, 214 | lock_owner: u64, 215 | flush: bool, 216 | ) -> Result<()> { 217 | Err(libc::ENOSYS.into()) 218 | } 219 | 220 | /// synchronize file contents. If the `datasync` is true, then only the user data should be 221 | /// flushed, not the metadata. when `path` is None, it means the path may be deleted. 222 | async fn fsync( 223 | &self, 224 | req: Request, 225 | path: Option<&OsStr>, 226 | fh: u64, 227 | datasync: bool, 228 | ) -> Result<()> { 229 | Ok(()) 230 | } 231 | 232 | /// set an extended attribute. 233 | async fn setxattr( 234 | &self, 235 | req: Request, 236 | path: &OsStr, 237 | name: &OsStr, 238 | value: &[u8], 239 | flags: u32, 240 | position: u32, 241 | ) -> Result<()> { 242 | Err(libc::ENOSYS.into()) 243 | } 244 | 245 | /// get an extended attribute. If size is too small, use [`ReplyXAttr::Size`] to return correct 246 | /// size. If size is enough, use [`ReplyXAttr::Data`] to send it, or return error. 247 | async fn getxattr( 248 | &self, 249 | req: Request, 250 | path: &OsStr, 251 | name: &OsStr, 252 | size: u32, 253 | ) -> Result { 254 | Err(libc::ENOSYS.into()) 255 | } 256 | 257 | /// list extended attribute names. If size is too small, use [`ReplyXAttr::Size`] to return 258 | /// correct size. If size is enough, use [`ReplyXAttr::Data`] to send it, or return error. 259 | async fn listxattr(&self, req: Request, path: &OsStr, size: u32) -> Result { 260 | Err(libc::ENOSYS.into()) 261 | } 262 | 263 | /// remove an extended attribute. 264 | async fn removexattr(&self, req: Request, path: &OsStr, name: &OsStr) -> Result<()> { 265 | Err(libc::ENOSYS.into()) 266 | } 267 | 268 | /// flush method. This is called on each `close()` of the opened file. Since file descriptors 269 | /// can be duplicated (`dup`, `dup2`, `fork`), for one open call there may be many flush calls. 270 | /// Filesystems shouldn't assume that flush will always be called after some writes, or that if 271 | /// will be called at all. `fh` will contain the value set by the open method, or will be 272 | /// undefined if the open method didn't set any value. when `path` is None, it means the path 273 | /// may be deleted. 274 | /// 275 | /// # Notes: 276 | /// 277 | /// the name of the method is misleading, since (unlike fsync) the filesystem is not forced to 278 | /// flush pending writes. One reason to flush data, is if the filesystem wants to return write 279 | /// errors. If the filesystem supports file locking operations ( 280 | /// [`setlk`][PathFilesystem::setlk], [`getlk`][PathFilesystem::getlk]) it should remove all 281 | /// locks belonging to `lock_owner`. 282 | async fn flush( 283 | &self, 284 | req: Request, 285 | path: Option<&OsStr>, 286 | fh: u64, 287 | lock_owner: u64, 288 | ) -> Result<()> { 289 | Err(libc::ENOSYS.into()) 290 | } 291 | 292 | /// open a directory. Filesystem may store an arbitrary file handle (pointer, index, etc) in 293 | /// `fh`, and use this in other all other directory stream operations 294 | /// ([`readdir`][PathFilesystem::readdir], [`releasedir`][PathFilesystem::releasedir], 295 | /// [`fsyncdir`][PathFilesystem::fsyncdir]). Filesystem may also implement stateless directory 296 | /// I/O and not store anything in `fh`. A file system need not implement this method if it 297 | /// sets [`MountOptions::no_open_dir_support`][crate::MountOptions::no_open_dir_support] and if 298 | /// the kernel supports `FUSE_NO_OPENDIR_SUPPORT`. 299 | async fn opendir(&self, req: Request, path: &OsStr, flags: u32) -> Result { 300 | Err(libc::ENOSYS.into()) 301 | } 302 | 303 | /// read directory. `offset` is used to track the offset of the directory entries. `fh` will 304 | /// contain the value set by the [`opendir`][PathFilesystem::opendir] method, or will be 305 | /// undefined if the [`opendir`][PathFilesystem::opendir] method didn't set any value. 306 | async fn readdir<'a>( 307 | &'a self, 308 | req: Request, 309 | path: &'a OsStr, 310 | fh: u64, 311 | offset: i64, 312 | ) -> Result> + Send + 'a>> { 313 | Err::>, _>(libc::ENOSYS.into()) 314 | } 315 | 316 | /// release an open directory. For every [`opendir`][PathFilesystem::opendir] call there will 317 | /// be exactly one `releasedir` call. `fh` will contain the value set by the 318 | /// [`opendir`][PathFilesystem::opendir] method, or will be undefined if the 319 | /// [`opendir`][PathFilesystem::opendir] method didn't set any value. 320 | async fn releasedir(&self, req: Request, path: &OsStr, fh: u64, flags: u32) -> Result<()> { 321 | Ok(()) 322 | } 323 | 324 | /// synchronize directory contents. If the `datasync` is true, then only the directory contents 325 | /// should be flushed, not the metadata. `fh` will contain the value set by the 326 | /// [`opendir`][PathFilesystem::opendir] method, or will be undefined if the 327 | /// [`opendir`][PathFilesystem::opendir] method didn't set any value. 328 | async fn fsyncdir(&self, req: Request, path: &OsStr, fh: u64, datasync: bool) -> Result<()> { 329 | Err(libc::ENOSYS.into()) 330 | } 331 | 332 | #[cfg(feature = "file-lock")] 333 | /// test for a POSIX file lock. 334 | /// 335 | /// # Notes: 336 | /// 337 | /// this is supported on enable **`file-lock`** feature. 338 | #[allow(clippy::too_many_arguments)] 339 | async fn getlk( 340 | &self, 341 | req: Request, 342 | path: Option<&OsStr>, 343 | fh: u64, 344 | lock_owner: u64, 345 | start: u64, 346 | end: u64, 347 | r#type: u32, 348 | pid: u32, 349 | ) -> Result; 350 | 351 | #[cfg(feature = "file-lock")] 352 | /// acquire, modify or release a POSIX file lock. 353 | /// 354 | /// # Notes: 355 | /// 356 | /// this is supported on enable **`file-lock`** feature. 357 | #[allow(clippy::too_many_arguments)] 358 | async fn setlk( 359 | &self, 360 | req: Request, 361 | path: Option<&OsStr>, 362 | fh: u64, 363 | lock_owner: u64, 364 | start: u64, 365 | end: u64, 366 | r#type: u32, 367 | pid: u32, 368 | block: bool, 369 | ) -> Result<()>; 370 | 371 | /// check file access permissions. This will be called for the `access()` system call. If the 372 | /// `default_permissions` mount option is given, this method is not be called. This method is 373 | /// not called under Linux kernel versions 2.4.x. 374 | async fn access(&self, req: Request, path: &OsStr, mask: u32) -> Result<()> { 375 | Err(libc::ENOSYS.into()) 376 | } 377 | 378 | /// create and open a file. If the file does not exist, first create it with the specified 379 | /// mode, and then open it. Open flags (with the exception of `O_NOCTTY`) are available in 380 | /// flags. Filesystem may store an arbitrary file handle (pointer, index, etc) in `fh`, and use 381 | /// this in other all other file operations ([`read`][PathFilesystem::read], 382 | /// [`write`][PathFilesystem::write], [`flush`][PathFilesystem::flush], 383 | /// [`release`][PathFilesystem::release], [`fsync`][PathFilesystem::fsync]). There are also 384 | /// some flags (`direct_io`, `keep_cache`) which the filesystem may set, to change the way the 385 | /// file is opened. If this method is not implemented or under Linux kernel versions earlier 386 | /// than 2.6.15, the [`mknod`][PathFilesystem::mknod] and [`open`][PathFilesystem::open] 387 | /// methods will be called instead. 388 | /// 389 | /// # Notes: 390 | /// 391 | /// See `fuse_file_info` structure in 392 | /// [fuse_common.h](https://libfuse.github.io/doxygen/include_2fuse__common_8h_source.html) for 393 | /// more details. 394 | async fn create( 395 | &self, 396 | req: Request, 397 | parent: &OsStr, 398 | name: &OsStr, 399 | mode: u32, 400 | flags: u32, 401 | ) -> Result { 402 | Err(libc::ENOSYS.into()) 403 | } 404 | 405 | /// handle interrupt. When a operation is interrupted, an interrupt request will send to fuse 406 | /// server with the unique id of the operation. 407 | async fn interrupt(&self, req: Request, unique: u64) -> Result<()> { 408 | Err(libc::ENOSYS.into()) 409 | } 410 | 411 | /// map block index within file to block index within device. 412 | /// 413 | /// # Notes: 414 | /// 415 | /// This may not works because currently this crate doesn't support fuseblk mode yet. 416 | async fn bmap( 417 | &self, 418 | req: Request, 419 | path: &OsStr, 420 | block_size: u32, 421 | idx: u64, 422 | ) -> Result { 423 | Err(libc::ENOSYS.into()) 424 | } 425 | 426 | /*async fn ioctl( 427 | &self, 428 | req: Request, 429 | inode: u64, 430 | fh: u64, 431 | flags: u32, 432 | cmd: u32, 433 | arg: u64, 434 | in_size: u32, 435 | out_size: u32, 436 | ) -> Result { 437 | Err(libc::ENOSYS.into()) 438 | }*/ 439 | 440 | /// poll for IO readiness events. 441 | #[allow(clippy::too_many_arguments)] 442 | async fn poll( 443 | &self, 444 | req: Request, 445 | path: Option<&OsStr>, 446 | fh: u64, 447 | kn: Option, 448 | flags: u32, 449 | envents: u32, 450 | notify: &Notify, 451 | ) -> Result { 452 | Err(libc::ENOSYS.into()) 453 | } 454 | 455 | /// receive notify reply from kernel. 456 | async fn notify_reply( 457 | &self, 458 | req: Request, 459 | path: &OsStr, 460 | offset: u64, 461 | data: Bytes, 462 | ) -> Result<()> { 463 | Err(libc::ENOSYS.into()) 464 | } 465 | 466 | /// forget more than one path. This is a batch version [`forget`][PathFilesystem::forget] 467 | async fn batch_forget(&self, req: Request, paths: &[&OsStr]) {} 468 | 469 | /// allocate space for an open file. This function ensures that required space is allocated for 470 | /// specified file. 471 | /// 472 | /// # Notes: 473 | /// 474 | /// more information about `fallocate`, please see **`man 2 fallocate`** 475 | async fn fallocate( 476 | &self, 477 | req: Request, 478 | path: Option<&OsStr>, 479 | fh: u64, 480 | offset: u64, 481 | length: u64, 482 | mode: u32, 483 | ) -> Result<()> { 484 | Err(libc::ENOSYS.into()) 485 | } 486 | 487 | /// read directory entries, but with their attribute, like [`readdir`][PathFilesystem::readdir] 488 | /// + [`lookup`][PathFilesystem::lookup] at the same time. 489 | async fn readdirplus<'a>( 490 | &'a self, 491 | req: Request, 492 | parent: &'a OsStr, 493 | fh: u64, 494 | offset: u64, 495 | lock_owner: u64, 496 | ) -> Result> + Send + 'a>> 497 | { 498 | Err::>, _>(libc::ENOSYS.into()) 499 | } 500 | 501 | /// rename a file or directory with flags. 502 | async fn rename2( 503 | &self, 504 | req: Request, 505 | origin_parent: &OsStr, 506 | origin_name: &OsStr, 507 | parent: &OsStr, 508 | name: &OsStr, 509 | flags: u32, 510 | ) -> Result<()> { 511 | Err(libc::ENOSYS.into()) 512 | } 513 | 514 | /// find next data or hole after the specified offset. 515 | async fn lseek( 516 | &self, 517 | req: Request, 518 | path: Option<&OsStr>, 519 | fh: u64, 520 | offset: u64, 521 | whence: u32, 522 | ) -> Result { 523 | Err(libc::ENOSYS.into()) 524 | } 525 | 526 | /// copy a range of data from one file to another. This can improve performance because it 527 | /// reduce data copy: in normal, data will copy from FUSE server to kernel, then to user-space, 528 | /// then to kernel, finally send back to FUSE server. By implement this method, data will only 529 | /// copy in FUSE server internal. when `from_path` or `to_path` is None, it means the path may 530 | /// be deleted. 531 | #[allow(clippy::too_many_arguments)] 532 | async fn copy_file_range( 533 | &self, 534 | req: Request, 535 | from_path: Option<&OsStr>, 536 | fh_in: u64, 537 | offset_in: u64, 538 | to_path: Option<&OsStr>, 539 | fh_out: u64, 540 | offset_out: u64, 541 | length: u64, 542 | flags: u64, 543 | ) -> Result { 544 | Err(libc::ENOSYS.into()) 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /examples/src/memfs/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::env; 3 | use std::ffi::{OsStr, OsString}; 4 | use std::io::{self, Cursor, Read}; 5 | use std::num::NonZeroU32; 6 | use std::sync::atomic::{AtomicU64, Ordering}; 7 | use std::sync::Arc; 8 | use std::time::{Duration, SystemTime}; 9 | 10 | use bytes::{Buf, BytesMut}; 11 | use fuse3::raw::prelude::*; 12 | use fuse3::{Errno, Inode, MountOptions, Result}; 13 | use futures_util::stream; 14 | use futures_util::stream::Stream; 15 | use futures_util::StreamExt; 16 | use libc::mode_t; 17 | use tokio::signal; 18 | use tokio::sync::RwLock; 19 | use tracing::metadata::LevelFilter; 20 | use tracing::{debug, info, subscriber}; 21 | use tracing_subscriber::layer::SubscriberExt; 22 | use tracing_subscriber::{fmt, Registry}; 23 | 24 | const TTL: Duration = Duration::from_secs(1); 25 | 26 | #[derive(Debug, Clone)] 27 | enum Entry { 28 | Dir(Arc>), 29 | File(Arc>), 30 | } 31 | 32 | impl Entry { 33 | async fn attr(&self) -> FileAttr { 34 | const BLOCK_SIZE: f64 = 4096f64; 35 | 36 | match self { 37 | Entry::Dir(dir) => { 38 | let nlink = Arc::strong_count(dir) - 1; 39 | let dir = dir.read().await; 40 | 41 | FileAttr { 42 | ino: dir.inode, 43 | size: 4096, 44 | blocks: 1, 45 | atime: SystemTime::UNIX_EPOCH.into(), 46 | mtime: SystemTime::UNIX_EPOCH.into(), 47 | ctime: SystemTime::UNIX_EPOCH.into(), 48 | #[cfg(target_os = "macos")] 49 | crtime: SystemTime::UNIX_EPOCH.into(), 50 | kind: FileType::Directory, 51 | perm: fuse3::perm_from_mode_and_kind(FileType::Directory, dir.mode), 52 | nlink: nlink as _, 53 | uid: 0, 54 | gid: 0, 55 | rdev: 0, 56 | #[cfg(target_os = "macos")] 57 | flags: 0, 58 | blksize: BLOCK_SIZE as _, 59 | } 60 | } 61 | 62 | Entry::File(file) => { 63 | let nlink = Arc::strong_count(file) - 1; 64 | let file = file.read().await; 65 | 66 | FileAttr { 67 | ino: file.inode, 68 | size: file.content.len() as _, 69 | blocks: (file.content.len() as f64 / BLOCK_SIZE).ceil() as _, 70 | atime: SystemTime::UNIX_EPOCH.into(), 71 | mtime: SystemTime::UNIX_EPOCH.into(), 72 | ctime: SystemTime::UNIX_EPOCH.into(), 73 | #[cfg(target_os = "macos")] 74 | crtime: SystemTime::UNIX_EPOCH.into(), 75 | kind: FileType::RegularFile, 76 | perm: fuse3::perm_from_mode_and_kind(FileType::RegularFile, file.mode), 77 | nlink: nlink as _, 78 | uid: 0, 79 | gid: 0, 80 | rdev: 0, 81 | #[cfg(target_os = "macos")] 82 | flags: 0, 83 | blksize: BLOCK_SIZE as _, 84 | } 85 | } 86 | } 87 | } 88 | 89 | async fn set_attr(&self, set_attr: SetAttr) -> FileAttr { 90 | match self { 91 | Entry::Dir(dir) => { 92 | let mut dir = dir.write().await; 93 | 94 | if let Some(mode) = set_attr.mode { 95 | dir.mode = mode; 96 | } 97 | } 98 | 99 | Entry::File(file) => { 100 | let mut file = file.write().await; 101 | 102 | if let Some(size) = set_attr.size { 103 | file.content.truncate(size as _); 104 | } 105 | 106 | if let Some(mode) = set_attr.mode { 107 | file.mode = mode; 108 | } 109 | } 110 | } 111 | 112 | self.attr().await 113 | } 114 | 115 | fn is_dir(&self) -> bool { 116 | matches!(self, Entry::Dir(_)) 117 | } 118 | 119 | async fn inode(&self) -> u64 { 120 | match self { 121 | Entry::Dir(dir) => { 122 | let dir = dir.read().await; 123 | 124 | dir.inode 125 | } 126 | 127 | Entry::File(file) => { 128 | let file = file.read().await; 129 | 130 | file.inode 131 | } 132 | } 133 | } 134 | 135 | fn kind(&self) -> FileType { 136 | if self.is_dir() { 137 | FileType::Directory 138 | } else { 139 | FileType::RegularFile 140 | } 141 | } 142 | 143 | async fn name(&self) -> OsString { 144 | match self { 145 | Entry::Dir(dir) => dir.read().await.name.clone(), 146 | Entry::File(file) => file.read().await.name.clone(), 147 | } 148 | } 149 | } 150 | 151 | #[derive(Debug)] 152 | struct Dir { 153 | inode: u64, 154 | parent: u64, 155 | name: OsString, 156 | children: BTreeMap, 157 | mode: mode_t, 158 | } 159 | 160 | #[derive(Debug)] 161 | struct File { 162 | inode: u64, 163 | parent: u64, 164 | name: OsString, 165 | content: Vec, 166 | mode: mode_t, 167 | } 168 | 169 | #[derive(Debug)] 170 | struct InnerFs { 171 | inode_map: BTreeMap, 172 | inode_gen: AtomicU64, 173 | } 174 | 175 | #[derive(Debug)] 176 | struct Fs(RwLock); 177 | 178 | impl Default for Fs { 179 | fn default() -> Self { 180 | let root = Entry::Dir(Arc::new(RwLock::new(Dir { 181 | inode: 1, 182 | parent: 1, 183 | name: OsString::from("/"), 184 | children: BTreeMap::new(), 185 | mode: 0o755, 186 | }))); 187 | 188 | let mut inode_map = BTreeMap::new(); 189 | 190 | inode_map.insert(1, root); 191 | 192 | Self(RwLock::new(InnerFs { 193 | inode_map, 194 | inode_gen: AtomicU64::new(2), 195 | })) 196 | } 197 | } 198 | 199 | impl Filesystem for Fs { 200 | async fn init(&self, _req: Request) -> Result { 201 | Ok(ReplyInit { 202 | max_write: NonZeroU32::new(16 * 1024).unwrap(), 203 | }) 204 | } 205 | 206 | async fn destroy(&self, _req: Request) { 207 | info!("destroy done") 208 | } 209 | 210 | async fn lookup(&self, _req: Request, parent: u64, name: &OsStr) -> Result { 211 | let inner = self.0.read().await; 212 | 213 | let entry = inner 214 | .inode_map 215 | .get(&parent) 216 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 217 | 218 | if let Entry::Dir(dir) = entry { 219 | let dir = dir.read().await; 220 | 221 | let attr = dir 222 | .children 223 | .get(name) 224 | .ok_or_else(|| Errno::from(libc::ENOENT))? 225 | .attr() 226 | .await; 227 | 228 | Ok(ReplyEntry { 229 | ttl: TTL, 230 | attr, 231 | generation: 0, 232 | }) 233 | } else { 234 | Err(libc::ENOTDIR.into()) 235 | } 236 | } 237 | 238 | async fn forget(&self, _req: Request, _inode: u64, _nlookup: u64) {} 239 | 240 | async fn getattr( 241 | &self, 242 | _req: Request, 243 | inode: u64, 244 | _fh: Option, 245 | _flags: u32, 246 | ) -> Result { 247 | Ok(ReplyAttr { 248 | ttl: TTL, 249 | attr: self 250 | .0 251 | .read() 252 | .await 253 | .inode_map 254 | .get(&inode) 255 | .ok_or_else(|| Errno::from(libc::ENOENT))? 256 | .attr() 257 | .await, 258 | }) 259 | } 260 | 261 | async fn setattr( 262 | &self, 263 | _req: Request, 264 | inode: u64, 265 | _fh: Option, 266 | set_attr: SetAttr, 267 | ) -> Result { 268 | Ok(ReplyAttr { 269 | ttl: TTL, 270 | attr: self 271 | .0 272 | .read() 273 | .await 274 | .inode_map 275 | .get(&inode) 276 | .ok_or_else(|| Errno::from(libc::ENOENT))? 277 | .set_attr(set_attr) 278 | .await, 279 | }) 280 | } 281 | 282 | async fn mkdir( 283 | &self, 284 | _req: Request, 285 | parent: u64, 286 | name: &OsStr, 287 | mode: u32, 288 | _umask: u32, 289 | ) -> Result { 290 | let mut inner = self.0.write().await; 291 | 292 | let entry = inner 293 | .inode_map 294 | .get(&parent) 295 | .ok_or_else(Errno::new_not_exist)?; 296 | 297 | if let Entry::Dir(dir) = entry { 298 | let mut dir = dir.write().await; 299 | 300 | if dir.children.get(name).is_some() { 301 | return Err(libc::EEXIST.into()); 302 | } 303 | 304 | let new_inode = inner.inode_gen.fetch_add(1, Ordering::Relaxed); 305 | 306 | let entry = Entry::Dir(Arc::new(RwLock::new(Dir { 307 | inode: new_inode, 308 | parent, 309 | name: name.to_owned(), 310 | children: BTreeMap::new(), 311 | mode: mode as mode_t, 312 | }))); 313 | 314 | let attr = entry.attr().await; 315 | 316 | dir.children.insert(name.to_os_string(), entry.clone()); 317 | 318 | drop(dir); // fix inner can't borrow as mut next line 319 | 320 | inner.inode_map.insert(new_inode, entry); 321 | 322 | Ok(ReplyEntry { 323 | ttl: TTL, 324 | attr, 325 | generation: 0, 326 | }) 327 | } else { 328 | Err(libc::ENOTDIR.into()) 329 | } 330 | } 331 | 332 | async fn unlink(&self, _req: Request, parent: u64, name: &OsStr) -> Result<()> { 333 | let mut inner = self.0.write().await; 334 | 335 | let entry = inner 336 | .inode_map 337 | .get(&parent) 338 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 339 | 340 | if let Entry::Dir(dir) = entry { 341 | let mut dir = dir.write().await; 342 | 343 | if dir 344 | .children 345 | .get(name) 346 | .ok_or_else(|| Errno::from(libc::ENOENT))? 347 | .is_dir() 348 | { 349 | return Err(libc::EISDIR.into()); 350 | } 351 | 352 | let child_entry = dir.children.remove(name).unwrap(); 353 | 354 | drop(dir); // fix inner can't borrow as mut next line 355 | 356 | if match &child_entry { 357 | Entry::Dir(_) => unreachable!(), 358 | Entry::File(file) => Arc::strong_count(file) == 1, 359 | } { 360 | inner.inode_map.remove(&child_entry.inode().await); 361 | } 362 | 363 | Ok(()) 364 | } else { 365 | Err(libc::ENOTDIR.into()) 366 | } 367 | } 368 | 369 | async fn rmdir(&self, _req: Request, parent: u64, name: &OsStr) -> Result<()> { 370 | let mut inner = self.0.write().await; 371 | 372 | let entry = inner 373 | .inode_map 374 | .get(&parent) 375 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 376 | 377 | if let Entry::Dir(dir) = entry { 378 | let mut dir = dir.write().await; 379 | 380 | if let Entry::Dir(child_dir) = 381 | dir.children.get(name).ok_or_else(Errno::new_not_exist)? 382 | { 383 | if !child_dir.read().await.children.is_empty() { 384 | return Err(Errno::from(libc::ENOTEMPTY)); 385 | } 386 | } else { 387 | return Err(Errno::new_is_not_dir()); 388 | } 389 | 390 | let child_entry = dir.children.remove(name).unwrap(); 391 | 392 | drop(dir); // fix inner can't borrow as mut next line 393 | 394 | if match &child_entry { 395 | Entry::Dir(dir) => Arc::strong_count(dir) == 1, 396 | Entry::File(_) => unreachable!(), 397 | } { 398 | inner.inode_map.remove(&child_entry.inode().await); 399 | } 400 | 401 | Ok(()) 402 | } else { 403 | Err(libc::ENOTDIR.into()) 404 | } 405 | } 406 | 407 | async fn rename( 408 | &self, 409 | _req: Request, 410 | parent: u64, 411 | name: &OsStr, 412 | new_parent: u64, 413 | new_name: &OsStr, 414 | ) -> Result<()> { 415 | let inner = self.0.read().await; 416 | 417 | let parent_entry = inner 418 | .inode_map 419 | .get(&parent) 420 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 421 | 422 | if let Entry::Dir(parent_dir) = parent_entry { 423 | let mut parent_dir = parent_dir.write().await; 424 | 425 | if parent == new_parent { 426 | let entry = parent_dir 427 | .children 428 | .remove(name) 429 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 430 | parent_dir.children.insert(new_name.to_os_string(), entry); 431 | 432 | return Ok(()); 433 | } 434 | 435 | let new_parent_entry = inner 436 | .inode_map 437 | .get(&new_parent) 438 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 439 | 440 | if let Entry::Dir(new_parent_dir) = new_parent_entry { 441 | let mut new_parent_dir = new_parent_dir.write().await; 442 | 443 | let entry = parent_dir 444 | .children 445 | .remove(name) 446 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 447 | new_parent_dir 448 | .children 449 | .insert(new_name.to_os_string(), entry); 450 | 451 | return Ok(()); 452 | } 453 | } 454 | 455 | Err(libc::ENOTDIR.into()) 456 | } 457 | 458 | async fn link( 459 | &self, 460 | _req: Request, 461 | inode: Inode, 462 | new_parent: Inode, 463 | new_name: &OsStr, 464 | ) -> Result { 465 | let inner = self.0.write().await; 466 | 467 | let entry = inner 468 | .inode_map 469 | .get(&inode) 470 | .ok_or_else(Errno::new_not_exist)?; 471 | 472 | let entry_name = entry.name().await; 473 | debug!(?entry_name, "get entry"); 474 | 475 | let new_parent_entry = inner 476 | .inode_map 477 | .get(&new_parent) 478 | .ok_or_else(Errno::new_not_exist)?; 479 | let new_parent_entry_name = new_parent_entry.name().await; 480 | debug!(?new_parent_entry_name, "get new parent entry"); 481 | 482 | match new_parent_entry { 483 | Entry::File(_) => { 484 | return Err(Errno::new_is_not_dir()); 485 | } 486 | 487 | Entry::Dir(dir) => { 488 | let mut dir = dir.write().await; 489 | if dir.children.contains_key(new_name) { 490 | return Err(Errno::new_exist()); 491 | } 492 | 493 | dir.children.insert(new_name.to_os_string(), entry.clone()); 494 | } 495 | } 496 | 497 | Ok(ReplyEntry { 498 | ttl: TTL, 499 | attr: entry.attr().await, 500 | generation: 0, 501 | }) 502 | } 503 | 504 | async fn open(&self, _req: Request, inode: u64, _flags: u32) -> Result { 505 | let inner = self.0.read().await; 506 | 507 | let entry = inner 508 | .inode_map 509 | .get(&inode) 510 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 511 | 512 | if matches!(entry, Entry::File(_)) { 513 | Ok(ReplyOpen { fh: 0, flags: 0 }) 514 | } else { 515 | Err(libc::EISDIR.into()) 516 | } 517 | } 518 | 519 | async fn read( 520 | &self, 521 | _req: Request, 522 | inode: u64, 523 | _fh: u64, 524 | offset: u64, 525 | size: u32, 526 | ) -> Result { 527 | let inner = self.0.read().await; 528 | 529 | let entry = inner 530 | .inode_map 531 | .get(&inode) 532 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 533 | 534 | if let Entry::File(file) = entry { 535 | let file = file.read().await; 536 | 537 | let mut cursor = Cursor::new(&file.content); 538 | cursor.set_position(offset); 539 | 540 | let size = cursor.remaining().min(size as _); 541 | 542 | let mut data = BytesMut::with_capacity(size); 543 | // safety 544 | unsafe { 545 | data.set_len(size); 546 | } 547 | 548 | cursor.read_exact(&mut data).unwrap(); 549 | 550 | Ok(ReplyData { data: data.into() }) 551 | } else { 552 | Err(libc::EISDIR.into()) 553 | } 554 | } 555 | 556 | async fn write( 557 | &self, 558 | _req: Request, 559 | inode: u64, 560 | _fh: u64, 561 | offset: u64, 562 | mut data: &[u8], 563 | _write_flags: u32, 564 | _flags: u32, 565 | ) -> Result { 566 | let inner = self.0.read().await; 567 | 568 | let entry = inner 569 | .inode_map 570 | .get(&inode) 571 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 572 | 573 | if let Entry::File(file) = entry { 574 | let mut file = file.write().await; 575 | 576 | if file.content.len() > offset as _ { 577 | let mut content = &mut file.content[offset as _..]; 578 | 579 | if content.len() > data.len() { 580 | io::copy(&mut data, &mut content).unwrap(); 581 | 582 | return Ok(ReplyWrite { 583 | written: data.len() as _, 584 | }); 585 | } 586 | 587 | let n = io::copy(&mut (&data[..content.len()]), &mut content).unwrap(); 588 | 589 | file.content.extend_from_slice(&data[n as _..]); 590 | 591 | Ok(ReplyWrite { 592 | written: data.len() as _, 593 | }) 594 | } else { 595 | file.content.resize(offset as _, 0); 596 | 597 | file.content.extend_from_slice(data); 598 | 599 | Ok(ReplyWrite { 600 | written: data.len() as _, 601 | }) 602 | } 603 | } else { 604 | Err(libc::EISDIR.into()) 605 | } 606 | } 607 | 608 | async fn release( 609 | &self, 610 | _req: Request, 611 | _inode: u64, 612 | _fh: u64, 613 | _flags: u32, 614 | _lock_owner: u64, 615 | _flush: bool, 616 | ) -> Result<()> { 617 | Ok(()) 618 | } 619 | 620 | async fn fsync(&self, _req: Request, _inode: u64, _fh: u64, _datasync: bool) -> Result<()> { 621 | Ok(()) 622 | } 623 | 624 | async fn flush(&self, _req: Request, _inode: u64, _fh: u64, _lock_owner: u64) -> Result<()> { 625 | Ok(()) 626 | } 627 | 628 | async fn access(&self, _req: Request, _inode: u64, _mask: u32) -> Result<()> { 629 | Ok(()) 630 | } 631 | 632 | async fn create( 633 | &self, 634 | _req: Request, 635 | parent: u64, 636 | name: &OsStr, 637 | mode: u32, 638 | flags: u32, 639 | ) -> Result { 640 | let mut inner = self.0.write().await; 641 | 642 | let entry = inner 643 | .inode_map 644 | .get(&parent) 645 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 646 | 647 | if let Entry::Dir(dir) = entry { 648 | let mut dir = dir.write().await; 649 | 650 | if dir.children.get(name).is_some() { 651 | return Err(libc::EEXIST.into()); 652 | } 653 | 654 | let new_inode = inner.inode_gen.fetch_add(1, Ordering::Relaxed); 655 | 656 | let entry = Entry::File(Arc::new(RwLock::new(File { 657 | inode: new_inode, 658 | parent, 659 | name: name.to_os_string(), 660 | content: vec![], 661 | mode: mode as mode_t, 662 | }))); 663 | 664 | let attr = entry.attr().await; 665 | 666 | dir.children.insert(name.to_os_string(), entry.clone()); 667 | 668 | drop(dir); 669 | 670 | inner.inode_map.insert(new_inode, entry); 671 | 672 | Ok(ReplyCreated { 673 | ttl: TTL, 674 | attr, 675 | generation: 0, 676 | fh: 0, 677 | flags, 678 | }) 679 | } else { 680 | Err(libc::ENOTDIR.into()) 681 | } 682 | } 683 | 684 | async fn interrupt(&self, _req: Request, _unique: u64) -> Result<()> { 685 | Ok(()) 686 | } 687 | 688 | async fn fallocate( 689 | &self, 690 | _req: Request, 691 | inode: u64, 692 | _fh: u64, 693 | offset: u64, 694 | length: u64, 695 | _mode: u32, 696 | ) -> Result<()> { 697 | let inner = self.0.read().await; 698 | 699 | let entry = inner 700 | .inode_map 701 | .get(&inode) 702 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 703 | 704 | if let Entry::File(file) = entry { 705 | let mut file = file.write().await; 706 | 707 | let new_size = (offset + length) as usize; 708 | 709 | let size = file.content.len(); 710 | 711 | if new_size > size { 712 | file.content.reserve(new_size - size); 713 | } else { 714 | file.content.truncate(new_size); 715 | } 716 | 717 | Ok(()) 718 | } else { 719 | Err(libc::EISDIR.into()) 720 | } 721 | } 722 | 723 | async fn readdirplus( 724 | &self, 725 | _req: Request, 726 | parent: u64, 727 | _fh: u64, 728 | offset: u64, 729 | _lock_owner: u64, 730 | ) -> Result> + Send + '_>> 731 | { 732 | let inner = self.0.read().await; 733 | 734 | let entry = inner 735 | .inode_map 736 | .get(&parent) 737 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 738 | 739 | if let Entry::Dir(dir) = entry { 740 | let attr = entry.attr().await; 741 | 742 | let dir = dir.read().await; 743 | 744 | let parent_attr = if dir.parent == dir.inode { 745 | attr 746 | } else { 747 | inner 748 | .inode_map 749 | .get(&dir.parent) 750 | .expect("dir parent not exist") 751 | .attr() 752 | .await 753 | }; 754 | 755 | let pre_children = stream::iter( 756 | vec![ 757 | (dir.inode, FileType::Directory, OsString::from("."), attr, 1), 758 | ( 759 | dir.parent, 760 | FileType::Directory, 761 | OsString::from(".."), 762 | parent_attr, 763 | 2, 764 | ), 765 | ] 766 | .into_iter(), 767 | ); 768 | 769 | let children = pre_children 770 | .chain(stream::iter(dir.children.iter()).enumerate().filter_map( 771 | |(i, (name, entry))| async move { 772 | let inode = entry.inode().await; 773 | let attr = entry.attr().await; 774 | 775 | Some((inode, entry.kind(), name.to_os_string(), attr, i as i64 + 3)) 776 | }, 777 | )) 778 | .map(|(inode, kind, name, attr, offset)| DirectoryEntryPlus { 779 | inode, 780 | generation: 0, 781 | kind, 782 | name, 783 | offset, 784 | attr, 785 | entry_ttl: TTL, 786 | attr_ttl: TTL, 787 | }) 788 | .skip(offset as _) 789 | .map(Ok) 790 | .collect::>() 791 | .await; 792 | 793 | Ok(ReplyDirectoryPlus { 794 | entries: stream::iter(children), 795 | }) 796 | } else { 797 | Err(libc::ENOTDIR.into()) 798 | } 799 | } 800 | 801 | async fn rename2( 802 | &self, 803 | req: Request, 804 | parent: u64, 805 | name: &OsStr, 806 | new_parent: u64, 807 | new_name: &OsStr, 808 | _flags: u32, 809 | ) -> Result<()> { 810 | self.rename(req, parent, name, new_parent, new_name).await 811 | } 812 | 813 | async fn lseek( 814 | &self, 815 | _req: Request, 816 | inode: u64, 817 | _fh: u64, 818 | offset: u64, 819 | whence: u32, 820 | ) -> Result { 821 | let inner = self.0.read().await; 822 | 823 | let entry = inner 824 | .inode_map 825 | .get(&inode) 826 | .ok_or_else(|| Errno::from(libc::ENOENT))?; 827 | 828 | let whence = whence as i32; 829 | 830 | if let Entry::File(file) = entry { 831 | let offset = if whence == libc::SEEK_CUR || whence == libc::SEEK_SET { 832 | offset 833 | } else if whence == libc::SEEK_END { 834 | let content_size = file.read().await.content.len(); 835 | 836 | if content_size >= offset as _ { 837 | content_size as u64 - offset 838 | } else { 839 | 0 840 | } 841 | } else { 842 | return Err(libc::EINVAL.into()); 843 | }; 844 | 845 | Ok(ReplyLSeek { offset }) 846 | } else { 847 | Err(libc::EISDIR.into()) 848 | } 849 | } 850 | 851 | async fn copy_file_range( 852 | &self, 853 | req: Request, 854 | inode: u64, 855 | fh_in: u64, 856 | off_in: u64, 857 | inode_out: u64, 858 | fh_out: u64, 859 | off_out: u64, 860 | length: u64, 861 | flags: u64, 862 | ) -> Result { 863 | let data = self.read(req, inode, fh_in, off_in, length as _).await?; 864 | 865 | let data = data.data.as_ref(); 866 | 867 | let ReplyWrite { written } = self 868 | .write(req, inode_out, fh_out, off_out, data, 0, flags as _) 869 | .await?; 870 | 871 | Ok(ReplyCopyFileRange { 872 | copied: u64::from(written), 873 | }) 874 | } 875 | } 876 | 877 | fn log_init() { 878 | let layer = fmt::layer() 879 | .pretty() 880 | .with_target(true) 881 | .with_writer(io::stderr); 882 | 883 | let layered = Registry::default().with(layer).with(LevelFilter::DEBUG); 884 | 885 | subscriber::set_global_default(layered).unwrap(); 886 | } 887 | 888 | #[tokio::main(flavor = "current_thread")] 889 | async fn main() { 890 | log_init(); 891 | 892 | let args = env::args_os().skip(1).take(1).collect::>(); 893 | 894 | let mount_path = args.first(); 895 | 896 | let uid = unsafe { libc::getuid() }; 897 | let gid = unsafe { libc::getgid() }; 898 | 899 | let not_unprivileged = env::var("NOT_UNPRIVILEGED").ok().as_deref() == Some("1"); 900 | 901 | let mut mount_options = MountOptions::default(); 902 | // .allow_other(true) 903 | mount_options 904 | .fs_name("memfs") 905 | .force_readdir_plus(true) 906 | .uid(uid) 907 | .gid(gid); 908 | 909 | let mount_path = mount_path.expect("no mount point specified"); 910 | 911 | let mut mount_handle = if !not_unprivileged { 912 | Session::new(mount_options) 913 | .mount_with_unprivileged(Fs::default(), mount_path) 914 | .await 915 | .unwrap() 916 | } else { 917 | Session::new(mount_options) 918 | .mount(Fs::default(), mount_path) 919 | .await 920 | .unwrap() 921 | }; 922 | 923 | let handle = &mut mount_handle; 924 | 925 | tokio::select! { 926 | res = handle => res.unwrap(), 927 | _ = signal::ctrl_c() => { 928 | mount_handle.unmount().await.unwrap() 929 | } 930 | } 931 | } 932 | --------------------------------------------------------------------------------