From 80085c9844bb9e4a6c426a8ecae0b88361e2e4f0 Mon Sep 17 00:00:00 2001 From: Antonio SJ Musumeci Date: Sun, 30 Mar 2025 18:06:30 -0500 Subject: [PATCH] Add statx support --- libfuse/include/fuse.h | 10 ++ libfuse/include/fuse_lowlevel.h | 6 ++ libfuse/lib/fuse.cpp | 73 +++++++++++++ libfuse/lib/fuse_lowlevel.cpp | 16 +++ src/fs_inode.cpp | 26 ++++- src/fs_inode.hpp | 11 +- src/fs_statx.hpp | 51 +++++++++ src/fuse_statx.cpp | 186 ++++++++++++++++++++++++++++++++ src/fuse_statx.hpp | 38 +++++++ src/mergerfs.cpp | 3 + src/symlinkify.hpp | 29 +++++ 11 files changed, 446 insertions(+), 3 deletions(-) create mode 100644 src/fs_statx.hpp create mode 100644 src/fuse_statx.cpp create mode 100644 src/fuse_statx.hpp diff --git a/libfuse/include/fuse.h b/libfuse/include/fuse.h index 2d615223..4c73d1fd 100644 --- a/libfuse/include/fuse.h +++ b/libfuse/include/fuse.h @@ -549,6 +549,16 @@ struct fuse_operations int (*removemapping)(); int (*syncfs)(); int (*tmpfile)(const char *, mode_t, fuse_file_info_t *); + int (*statx)(const char *fusepath, + const uint32_t flags, + const uint32_t mask, + struct fuse_statx *st, + fuse_timeouts_t *timeout); + int (*statx_fh)(const uint64_t fh, + const uint32_t flags, + const uint32_t mask, + struct fuse_statx *st, + fuse_timeouts_t *timeout); }; /** Extra context that may be needed by some filesystems diff --git a/libfuse/include/fuse_lowlevel.h b/libfuse/include/fuse_lowlevel.h index 2c0561f2..613bcc8d 100644 --- a/libfuse/include/fuse_lowlevel.h +++ b/libfuse/include/fuse_lowlevel.h @@ -257,6 +257,12 @@ int fuse_reply_attr(fuse_req_t req, const struct stat *attr, const uint64_t timeout); + +int fuse_reply_statx(fuse_req_t req, + int flags, + struct fuse_statx *st, + const uint64_t timeout); + /** * Reply with the contents of a symbolic link * diff --git a/libfuse/lib/fuse.cpp b/libfuse/lib/fuse.cpp index 1c77da97..4a14d535 100644 --- a/libfuse/lib/fuse.cpp +++ b/libfuse/lib/fuse.cpp @@ -1718,6 +1718,78 @@ fuse_lib_getattr(fuse_req_t req, } } +static +void +fuse_lib_statx_path(fuse_req_t req_, + struct fuse_in_header *hdr_, + struct fuse *f_, + fuse_statx_in *inarg_) +{ + int err; + char *fusepath; + struct fuse_statx st{}; + fuse_timeouts_t timeouts{}; + + err = get_path(f_,hdr_->nodeid,&fusepath); + if(err) + { + fuse_reply_err(req_,err); + return; + } + + err = f_->fs->op.statx(fusepath, + inarg_->sx_flags, + inarg_->sx_mask, + &st, + &timeouts); + + free_path(f_,hdr_->nodeid,fusepath); + + if(err) + fuse_reply_err(req_,err); + else + fuse_reply_statx(req_,0,&st,timeouts.attr); +} + +static +void +fuse_lib_statx_fh(fuse_req_t req_, + struct fuse_in_header *hdr_, + struct fuse *f_, + fuse_statx_in *inarg_) +{ + int err; + struct fuse_statx st{}; + fuse_timeouts_t timeouts{}; + + err = f_->fs->op.statx_fh(inarg_->fh, + inarg_->sx_flags, + inarg_->sx_mask, + &st, + &timeouts); + + if(err) + fuse_reply_err(req_,err); + else + fuse_reply_statx(req_,0,&st,timeouts.attr); +} + +static +void +fuse_lib_statx(fuse_req_t req_, + struct fuse_in_header *hdr_) +{ + struct fuse *f = req_fuse_prepare(req_); + fuse_statx_in *inarg; + + inarg = (fuse_statx_in*)fuse_hdr_arg(hdr_); + + if(inarg->getattr_flags & FUSE_GETATTR_FH) + fuse_lib_statx_fh(req_,hdr_,f,inarg); + else + fuse_lib_statx_path(req_,hdr_,f,inarg); +} + static void fuse_lib_setattr(fuse_req_t req, @@ -3643,6 +3715,7 @@ static struct fuse_lowlevel_ops fuse_path_ops = .setupmapping = fuse_lib_setupmapping, .setxattr = fuse_lib_setxattr, .statfs = fuse_lib_statfs, + .statx = fuse_lib_statx, .symlink = fuse_lib_symlink, .syncfs = fuse_lib_syncfs, .tmpfile = fuse_lib_tmpfile, diff --git a/libfuse/lib/fuse_lowlevel.cpp b/libfuse/lib/fuse_lowlevel.cpp index b3d40969..2c475074 100644 --- a/libfuse/lib/fuse_lowlevel.cpp +++ b/libfuse/lib/fuse_lowlevel.cpp @@ -343,6 +343,22 @@ fuse_reply_attr(fuse_req_t req, return send_reply_ok(req,&arg,size); } +int +fuse_reply_statx(fuse_req_t req_, + int flags_, + struct fuse_statx *st_, + uint64_t timeout_) +{ + struct fuse_statx_out outarg{}; + + outarg.flags = flags_; + outarg.attr_valid = timeout_; + outarg.attr_valid_nsec = 0; + outarg.stat = *(struct fuse_statx*)st_; + + return send_reply_ok(req_,&outarg,sizeof(outarg)); +} + int fuse_reply_readlink(fuse_req_t req, const char *linkname) diff --git a/src/fs_inode.cpp b/src/fs_inode.cpp index 107a6cb7..683cf168 100644 --- a/src/fs_inode.cpp +++ b/src/fs_inode.cpp @@ -16,9 +16,10 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include "fs_inode.hpp" + #include "ef.hpp" #include "errno.hpp" -#include "fs_inode.hpp" #include "rapidhash.h" #include @@ -39,7 +40,9 @@ static uint32_t h64_to_h32(uint64_t h_) { - return (h_ - (h_ >> 32)); + h_ ^= (h_ >> 32); + h_ *= 0x9E3779B9; + return h_; } static @@ -227,6 +230,18 @@ namespace fs st_->st_ino); } + void + calc(const char *fusepath_, + const uint64_t fusepath_len_, + struct fuse_statx *st_) + { + st_->ino = calc(fusepath_, + fusepath_len_, + st_->mode, + st_->dev_major ^ st_->dev_minor, + st_->ino); + } + void calc(const char *fusepath_, struct stat *st_) @@ -234,6 +249,13 @@ namespace fs calc(fusepath_,strlen(fusepath_),st_); } + void + calc(const char *fusepath_, + struct fuse_statx *st_) + { + calc(fusepath_,strlen(fusepath_),st_); + } + void calc(const std::string &fusepath_, struct stat *st_) diff --git a/src/fs_inode.hpp b/src/fs_inode.hpp index 11e772c8..fd7f1013 100644 --- a/src/fs_inode.hpp +++ b/src/fs_inode.hpp @@ -18,6 +18,8 @@ #pragma once +#include "fuse_kernel.h" + #include #include @@ -42,13 +44,20 @@ namespace fs mode_t const mode, dev_t const dev, ino_t ino); + void calc(const char *fusepath, const uint64_t fusepath_len, struct stat *st); + void calc(const char *fusepath, + const uint64_t fusepath_len, + struct fuse_statx *st); + void calc(const char *fusepath, struct stat *st); + void calc(const char *fusepath, + struct fuse_statx *st); + void calc(const std::string &fusepath, struct stat *st); - } } diff --git a/src/fs_statx.hpp b/src/fs_statx.hpp new file mode 100644 index 00000000..70c13a2a --- /dev/null +++ b/src/fs_statx.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "fuse_kernel.h" + +#include + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include + +namespace fs +{ + static + inline + int + statx(const int dirfd_, + const char *pathname_, + const int flags_, + const unsigned int mask_, + struct fuse_statx *st_) + { +#ifdef STATX_TYPE + int rv; + + rv = ::statx(dirfd_, + pathname_, + flags_, + mask_, + (struct statx*)st_); + + return ((rv == -1) ? -errno : 0); +#else + return -ENOSYS; +#endif + } + + static + inline + int + statx(const int dirfd_, + const std::string &pathname_, + const int flags_, + const unsigned int mask_, + struct fuse_statx *st_) + { + return fs::statx(dirfd_,pathname_.c_str(),flags_,mask_,st_); + } +} diff --git a/src/fuse_statx.cpp b/src/fuse_statx.cpp new file mode 100644 index 00000000..7d1648c9 --- /dev/null +++ b/src/fuse_statx.cpp @@ -0,0 +1,186 @@ +#include "fuse_statx.hpp" + +#include "config.hpp" +#include "errno.hpp" +#include "fileinfo.hpp" +#include "fs_inode.hpp" +#include "fs_path.hpp" +#include "fs_statx.hpp" +#include "symlinkify.hpp" +#include "ugid.hpp" + +#include "fmt/core.h" + +#include "fuse.h" + +#include + + +static +void +_set_stat_if_leads_to_dir(const std::string &path_, + struct fuse_statx *st_) +{ + int rv; + struct fuse_statx st; + + rv = fs::statx(AT_FDCWD,path_,AT_SYMLINK_FOLLOW,STATX_TYPE,st_); + if(rv < 0) + return; + + if(S_ISDIR(st.mode)) + *st_ = st; + + return; +} + +static +void +_set_stat_if_leads_to_reg(const std::string &path_, + struct fuse_statx *st_) +{ + int rv; + struct fuse_statx st; + + rv = fs::statx(AT_FDCWD,path_,AT_SYMLINK_FOLLOW,STATX_TYPE,st_); + if(rv < 0) + return; + + if(S_ISREG(st.mode)) + *st_ = st; + + return; +} + +static +int +_statx_controlfile(struct fuse_statx *st_) +{ + static const uid_t uid = ::getuid(); + static const gid_t gid = ::getgid(); + static const time_t now = ::time(NULL); + + st_->ino = fs::inode::MAGIC; + st_->mode = (S_IFREG|S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH); + st_->nlink = 1; + st_->uid = uid; + st_->gid = gid; + st_->size = 0; + st_->blocks = 0; + st_->atime.tv_sec = now; + st_->mtime.tv_sec = now; + st_->ctime.tv_sec = now; + + return 0; +} + +static +int +_statx(const Policy::Search &searchFunc_, + const Branches &branches_, + const char *fusepath_, + const uint32_t flags_, + const uint32_t mask_, + struct fuse_statx *st_, + const bool symlinkify_, + const time_t symlinkify_timeout_, + FollowSymlinks followsymlinks_) +{ + int rv; + std::string fullpath; + StrVec basepaths; + + rv = searchFunc_(branches_,fusepath_,&basepaths); + if(rv == -1) + return -errno; + + fullpath = fs::path::make(basepaths[0],fusepath_); + + switch(followsymlinks_) + { + case FollowSymlinks::ENUM::NEVER: + rv = fs::statx(AT_FDCWD,fullpath,flags_|AT_SYMLINK_NOFOLLOW,mask_,st_); + break; + case FollowSymlinks::ENUM::DIRECTORY: + rv = fs::statx(AT_FDCWD,fullpath,flags_|AT_SYMLINK_NOFOLLOW,mask_,st_); + if((rv >= 0) && S_ISLNK(st_->mode)) + ::_set_stat_if_leads_to_dir(fullpath,st_); + break; + case FollowSymlinks::ENUM::REGULAR: + rv = fs::statx(AT_FDCWD,fullpath,flags_|AT_SYMLINK_NOFOLLOW,mask_,st_); + if((rv >= 0) && S_ISLNK(st_->mode)) + ::_set_stat_if_leads_to_reg(fullpath,st_); + break; + case FollowSymlinks::ENUM::ALL: + rv = fs::statx(AT_FDCWD,fullpath,flags_|AT_SYMLINK_FOLLOW,mask_,st_); + if(rv < 0) + rv = fs::statx(AT_FDCWD,fullpath,flags_|AT_SYMLINK_NOFOLLOW,mask_,st_); + break; + } + + if(rv < 0) + return rv; + + if(symlinkify_ && symlinkify::can_be_symlink(*st_,symlinkify_timeout_)) + symlinkify::convert(fullpath,st_); + + fs::inode::calc(fusepath_,st_); + + return 0; +} + +static +int +_statx(const char *fusepath_, + const uint32_t flags_, + const uint32_t mask_, + struct fuse_statx *st_, + fuse_timeouts_t *timeout_) +{ + int rv; + Config::Read cfg; + const fuse_context *fc = fuse_get_context(); + const ugid::Set ugid(fc->uid,fc->gid); + + rv = ::_statx(cfg->func.getattr.policy, + cfg->branches, + fusepath_, + flags_, + mask_, + st_, + cfg->symlinkify, + cfg->symlinkify_timeout, + cfg->follow_symlinks); + + timeout_->entry = ((rv >= 0) ? + cfg->cache_entry : + cfg->cache_negative_entry); + timeout_->attr = cfg->cache_attr; + + return rv; +} + +int +FUSE::statx(const char *fusepath_, + const uint32_t flags_, + const uint32_t mask_, + struct fuse_statx *st_, + fuse_timeouts_t *timeout_) +{ + if(fusepath_ == CONTROLFILE) + return ::_statx_controlfile(st_); + + return ::_statx(fusepath_,flags_|AT_STATX_DONT_SYNC,mask_,st_,timeout_); +} + +int +FUSE::statx_fh(const uint64_t fh_, + const uint32_t flags_, + const uint32_t mask_, + struct fuse_statx *st_, + fuse_timeouts_t *timeout_) +{ + FileInfo *fi = reinterpret_cast(fh_); + + return ::_statx(fi->fusepath.c_str(),flags_,mask_,st_,timeout_); +} diff --git a/src/fuse_statx.hpp b/src/fuse_statx.hpp new file mode 100644 index 00000000..9b0df523 --- /dev/null +++ b/src/fuse_statx.hpp @@ -0,0 +1,38 @@ +/* + ISC License + + Copyright (c) 2025, Antonio SJ Musumeci + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#pragma once + +#include "fuse.h" +#include "fuse_kernel.h" + +#include + +namespace FUSE +{ + int statx(const char *fusepath, + const uint32_t flags, + const uint32_t mask, + struct fuse_statx *st, + fuse_timeouts_t *timeout); + int statx_fh(const uint64_t fh, + const uint32_t flags, + const uint32_t mask, + struct fuse_statx *st, + fuse_timeouts_t *timeout); +} diff --git a/src/mergerfs.cpp b/src/mergerfs.cpp index 6460c134..c7665bf1 100644 --- a/src/mergerfs.cpp +++ b/src/mergerfs.cpp @@ -71,6 +71,7 @@ #include "fuse_setupmapping.hpp" #include "fuse_setxattr.hpp" #include "fuse_statfs.hpp" +#include "fuse_statx.hpp" #include "fuse_symlink.hpp" #include "fuse_syncfs.hpp" #include "fuse_tmpfile.hpp" @@ -139,6 +140,8 @@ namespace l ops_.setupmapping = FUSE::setupmapping; ops_.setxattr = FUSE::setxattr; ops_.statfs = FUSE::statfs; + ops_.statx = FUSE::statx; + ops_.statx_fh = FUSE::statx_fh; ops_.symlink = FUSE::symlink; ops_.syncfs = FUSE::syncfs; ops_.tmpfile = FUSE::tmpfile; diff --git a/src/symlinkify.hpp b/src/symlinkify.hpp index 5f81effa..3a82d4aa 100644 --- a/src/symlinkify.hpp +++ b/src/symlinkify.hpp @@ -18,6 +18,8 @@ #pragma once +#include "fuse_kernel.h" + #include #include @@ -42,6 +44,22 @@ namespace symlinkify ((now - st_.st_ctime) > timeout_)); } + static + inline + bool + can_be_symlink(const struct fuse_statx &st_, + const time_t timeout_) + { + if(S_ISDIR(st_.mode) || + (st_.mode & (S_IWUSR|S_IWGRP|S_IWOTH))) + return false; + + const time_t now = ::time(NULL); + + return (((now - st_.mtime.tv_sec) > timeout_) && + ((now - st_.ctime.tv_sec) > timeout_)); + } + static inline void @@ -52,4 +70,15 @@ namespace symlinkify st_->st_size = target_.size(); st_->st_blocks = 0; } + + static + inline + void + convert(const std::string &target_, + struct fuse_statx *st_) + { + st_->mode = (((st_->mode & ~S_IFMT) | S_IFLNK) | 0777); + st_->size = target_.size(); + st_->blocks = 0; + } }