2020-06-22 13:03:48 -04:00
|
|
|
/*
|
|
|
|
|
ISC License
|
|
|
|
|
|
2026-02-23 13:22:21 -06:00
|
|
|
Copyright (c) 2026, Antonio SJ Musumeci <trapexit@spawn.link>
|
2020-06-22 13:03:48 -04:00
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-03-30 18:06:30 -05:00
|
|
|
#include "fs_inode.hpp"
|
|
|
|
|
|
2026-02-28 12:53:29 -06:00
|
|
|
#include "rapidhash/rapidhash.h"
|
2020-06-22 13:03:48 -04:00
|
|
|
|
2026-02-27 15:51:04 -06:00
|
|
|
#include <atomic>
|
2020-06-22 13:03:48 -04:00
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
typedef u64 (*inodefunc_t)(const std::string_view,
|
|
|
|
|
const std::string_view,
|
2026-03-01 13:58:06 -06:00
|
|
|
const mode_t,
|
|
|
|
|
const ino_t);
|
2025-04-06 01:03:26 -05:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static u64 _hybrid_hash(const std::string_view,
|
|
|
|
|
const std::string_view,
|
2026-03-01 13:58:06 -06:00
|
|
|
const mode_t,
|
|
|
|
|
const ino_t);
|
2020-06-22 13:03:48 -04:00
|
|
|
|
|
|
|
|
|
2026-02-27 15:51:04 -06:00
|
|
|
static std::atomic<inodefunc_t> g_func{::_hybrid_hash};
|
2020-06-22 13:03:48 -04:00
|
|
|
|
2020-08-03 17:18:22 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
const void*
|
|
|
|
|
_data(const std::string_view s_)
|
|
|
|
|
{
|
|
|
|
|
return (s_.empty() ? "" : s_.data());
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-03 17:18:22 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u32
|
|
|
|
|
_h64_to_h32(u64 h_)
|
2020-08-03 17:18:22 -04:00
|
|
|
{
|
2025-03-30 18:06:30 -05:00
|
|
|
h_ ^= (h_ >> 32);
|
|
|
|
|
h_ *= 0x9E3779B9;
|
|
|
|
|
return h_;
|
2020-08-03 17:18:22 -04:00
|
|
|
}
|
|
|
|
|
|
2020-06-22 13:03:48 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_hash(const std::string_view s_)
|
2020-06-22 13:03:48 -04:00
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
return rapidhash(::_data(s_),
|
|
|
|
|
s_.size());
|
2020-06-22 13:03:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_branch_seed(const std::string_view branch_path_)
|
2020-06-22 13:03:48 -04:00
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
return ::_hash(branch_path_);
|
|
|
|
|
}
|
2025-04-06 01:03:26 -05:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_devino_hash_from_seed(const u64 branch_seed_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
return rapidhash_withSeed(&ino_,
|
|
|
|
|
sizeof(ino_),
|
|
|
|
|
branch_seed_);
|
2020-06-22 13:03:48 -04:00
|
|
|
}
|
|
|
|
|
|
2020-08-03 17:18:22 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_path_hash_value(const std::string_view fusepath_)
|
2020-08-03 17:18:22 -04:00
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
return ::_hash(fusepath_);
|
|
|
|
|
}
|
2020-08-03 17:18:22 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_path_hash32_value(const std::string_view fusepath_)
|
|
|
|
|
{
|
|
|
|
|
return ::_h64_to_h32(::_path_hash_value(fusepath_));
|
|
|
|
|
}
|
2020-08-03 17:18:22 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_devino_hash_value(const std::string_view branch_path_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
return ::_devino_hash_from_seed(::_branch_seed(branch_path_),
|
|
|
|
|
ino_);
|
2020-08-03 17:18:22 -04:00
|
|
|
}
|
|
|
|
|
|
2020-06-22 13:03:48 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_devino_hash32_value(const std::string_view branch_path_,
|
|
|
|
|
const ino_t ino_)
|
2020-06-22 13:03:48 -04:00
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
return ::_h64_to_h32(::_devino_hash_value(branch_path_,ino_));
|
|
|
|
|
}
|
2020-06-22 13:03:48 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_passthrough(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
return ino_;
|
|
|
|
|
}
|
2020-06-22 13:03:48 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_path_hash(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
return ::_path_hash_value(fusepath_);
|
2020-06-22 13:03:48 -04:00
|
|
|
}
|
|
|
|
|
|
2020-08-03 17:18:22 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_path_hash32(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
2020-08-03 17:18:22 -04:00
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
return ::_path_hash32_value(fusepath_);
|
|
|
|
|
}
|
2020-08-03 17:18:22 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_devino_hash(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
return ::_devino_hash_value(branch_path_,ino_);
|
|
|
|
|
}
|
2020-08-03 17:18:22 -04:00
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_devino_hash32(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
return ::_devino_hash32_value(branch_path_,ino_);
|
2020-08-03 17:18:22 -04:00
|
|
|
}
|
|
|
|
|
|
2020-06-22 13:03:48 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_hybrid_hash(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
2020-06-22 13:03:48 -04:00
|
|
|
{
|
|
|
|
|
return (S_ISDIR(mode_) ?
|
2025-07-25 17:53:48 -05:00
|
|
|
::_path_hash(branch_path_,fusepath_,mode_,ino_) :
|
|
|
|
|
::_devino_hash(branch_path_,fusepath_,mode_,ino_));
|
2020-06-22 13:03:48 -04:00
|
|
|
}
|
|
|
|
|
|
2020-08-03 17:18:22 -04:00
|
|
|
static
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2026-04-10 22:04:57 -04:00
|
|
|
_hybrid_hash32(const std::string_view branch_path_,
|
|
|
|
|
const std::string_view fusepath_,
|
2025-07-25 17:53:48 -05:00
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
2020-08-03 17:18:22 -04:00
|
|
|
{
|
|
|
|
|
return (S_ISDIR(mode_) ?
|
2025-07-25 17:53:48 -05:00
|
|
|
::_path_hash32(branch_path_,fusepath_,mode_,ino_) :
|
|
|
|
|
::_devino_hash32(branch_path_,fusepath_,mode_,ino_));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
static
|
|
|
|
|
fs::inode::Algo
|
|
|
|
|
_algo_from_func(const inodefunc_t func_)
|
|
|
|
|
{
|
|
|
|
|
if(func_ == ::_passthrough)
|
|
|
|
|
return fs::inode::Algo::PASSTHROUGH;
|
|
|
|
|
if(func_ == ::_path_hash)
|
|
|
|
|
return fs::inode::Algo::PATH_HASH;
|
|
|
|
|
if(func_ == ::_path_hash32)
|
|
|
|
|
return fs::inode::Algo::PATH_HASH32;
|
|
|
|
|
if(func_ == ::_devino_hash)
|
|
|
|
|
return fs::inode::Algo::DEVINO_HASH;
|
|
|
|
|
if(func_ == ::_devino_hash32)
|
|
|
|
|
return fs::inode::Algo::DEVINO_HASH32;
|
|
|
|
|
if(func_ == ::_hybrid_hash)
|
|
|
|
|
return fs::inode::Algo::HYBRID_HASH;
|
|
|
|
|
if(func_ == ::_hybrid_hash32)
|
|
|
|
|
return fs::inode::Algo::HYBRID_HASH32;
|
|
|
|
|
|
|
|
|
|
return fs::inode::Algo::HYBRID_HASH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static
|
|
|
|
|
u64
|
|
|
|
|
_calc_with_algo(const fs::inode::Algo algo_,
|
|
|
|
|
const u64 branch_seed_,
|
|
|
|
|
const std::string_view fusepath_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
switch(algo_)
|
|
|
|
|
{
|
|
|
|
|
case fs::inode::Algo::PASSTHROUGH:
|
|
|
|
|
return ino_;
|
|
|
|
|
case fs::inode::Algo::PATH_HASH:
|
|
|
|
|
return ::_path_hash_value(fusepath_);
|
|
|
|
|
case fs::inode::Algo::PATH_HASH32:
|
|
|
|
|
return ::_path_hash32_value(fusepath_);
|
|
|
|
|
case fs::inode::Algo::DEVINO_HASH:
|
|
|
|
|
return ::_devino_hash_from_seed(branch_seed_,ino_);
|
|
|
|
|
case fs::inode::Algo::DEVINO_HASH32:
|
|
|
|
|
return ::_h64_to_h32(::_devino_hash_from_seed(branch_seed_,ino_));
|
|
|
|
|
case fs::inode::Algo::HYBRID_HASH:
|
|
|
|
|
return (S_ISDIR(mode_) ?
|
|
|
|
|
::_path_hash_value(fusepath_) :
|
|
|
|
|
::_devino_hash_from_seed(branch_seed_,ino_));
|
|
|
|
|
case fs::inode::Algo::HYBRID_HASH32:
|
|
|
|
|
return (S_ISDIR(mode_) ?
|
|
|
|
|
::_path_hash32_value(fusepath_) :
|
|
|
|
|
::_h64_to_h32(::_devino_hash_from_seed(branch_seed_,ino_)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ino_;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 17:53:48 -05:00
|
|
|
int
|
2025-09-17 23:51:16 -04:00
|
|
|
fs::inode::set_algo(const std::string &algo_)
|
2025-07-25 17:53:48 -05:00
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
inodefunc_t func = nullptr;
|
|
|
|
|
|
2025-07-25 17:53:48 -05:00
|
|
|
if(algo_ == "passthrough")
|
2026-04-10 22:04:57 -04:00
|
|
|
func = ::_passthrough;
|
|
|
|
|
else if(algo_ == "path-hash")
|
|
|
|
|
func = ::_path_hash;
|
|
|
|
|
else if(algo_ == "path-hash32")
|
|
|
|
|
func = ::_path_hash32;
|
|
|
|
|
else if(algo_ == "devino-hash")
|
|
|
|
|
func = ::_devino_hash;
|
|
|
|
|
else if(algo_ == "devino-hash32")
|
|
|
|
|
func = ::_devino_hash32;
|
|
|
|
|
else if(algo_ == "hybrid-hash")
|
|
|
|
|
func = ::_hybrid_hash;
|
|
|
|
|
else if(algo_ == "hybrid-hash32")
|
|
|
|
|
func = ::_hybrid_hash32;
|
2025-07-25 17:53:48 -05:00
|
|
|
else
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
2026-04-10 22:04:57 -04:00
|
|
|
g_func.store(func);
|
|
|
|
|
|
2025-07-25 17:53:48 -05:00
|
|
|
return 0;
|
2020-08-03 17:18:22 -04:00
|
|
|
}
|
2020-06-22 13:03:48 -04:00
|
|
|
|
2025-07-25 17:53:48 -05:00
|
|
|
std::string
|
|
|
|
|
fs::inode::get_algo(void)
|
|
|
|
|
{
|
2026-04-10 22:04:57 -04:00
|
|
|
switch(::_algo_from_func(g_func.load()))
|
|
|
|
|
{
|
|
|
|
|
case fs::inode::Algo::PASSTHROUGH:
|
|
|
|
|
return "passthrough";
|
|
|
|
|
case fs::inode::Algo::PATH_HASH:
|
|
|
|
|
return "path-hash";
|
|
|
|
|
case fs::inode::Algo::PATH_HASH32:
|
|
|
|
|
return "path-hash32";
|
|
|
|
|
case fs::inode::Algo::DEVINO_HASH:
|
|
|
|
|
return "devino-hash";
|
|
|
|
|
case fs::inode::Algo::DEVINO_HASH32:
|
|
|
|
|
return "devino-hash32";
|
|
|
|
|
case fs::inode::Algo::HYBRID_HASH:
|
|
|
|
|
return "hybrid-hash";
|
|
|
|
|
case fs::inode::Algo::HYBRID_HASH32:
|
|
|
|
|
return "hybrid-hash32";
|
|
|
|
|
}
|
2025-07-25 17:53:48 -05:00
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
2026-05-17 13:40:53 -04:00
|
|
|
fs::inode::ReaddirCalc::ReaddirCalc(const std::string &branch_path_,
|
|
|
|
|
const fs::relpath &dirpath_)
|
2026-04-10 22:04:57 -04:00
|
|
|
: _algo(::_algo_from_func(g_func.load())),
|
Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
2026-05-17 13:40:53 -04:00
|
|
|
_branch_seed(::_branch_seed(branch_path_)),
|
|
|
|
|
_fusepath(fs::relpath::concat(dirpath_,"__mergerfs__")),
|
2026-04-10 22:04:57 -04:00
|
|
|
_filename_offset(_fusepath.size() - std::string_view("__mergerfs__").size())
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u64
|
|
|
|
|
fs::inode::ReaddirCalc::calc(const char *name_,
|
|
|
|
|
const std::size_t namelen_,
|
|
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
|
|
|
|
switch(_algo)
|
|
|
|
|
{
|
|
|
|
|
case Algo::PASSTHROUGH:
|
|
|
|
|
return ino_;
|
|
|
|
|
case Algo::DEVINO_HASH:
|
|
|
|
|
return ::_devino_hash_from_seed(_branch_seed,ino_);
|
|
|
|
|
case Algo::DEVINO_HASH32:
|
|
|
|
|
return ::_h64_to_h32(::_devino_hash_from_seed(_branch_seed,ino_));
|
|
|
|
|
case Algo::HYBRID_HASH:
|
|
|
|
|
if(!S_ISDIR(mode_))
|
|
|
|
|
return ::_devino_hash_from_seed(_branch_seed,ino_);
|
|
|
|
|
break;
|
|
|
|
|
case Algo::HYBRID_HASH32:
|
|
|
|
|
if(!S_ISDIR(mode_))
|
|
|
|
|
return ::_h64_to_h32(::_devino_hash_from_seed(_branch_seed,ino_));
|
|
|
|
|
break;
|
|
|
|
|
case Algo::PATH_HASH:
|
|
|
|
|
case Algo::PATH_HASH32:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_fusepath.resize(_filename_offset);
|
|
|
|
|
_fusepath.append(name_,namelen_);
|
|
|
|
|
|
|
|
|
|
return ::_calc_with_algo(_algo,
|
|
|
|
|
_branch_seed,
|
|
|
|
|
_fusepath,
|
|
|
|
|
mode_,
|
|
|
|
|
ino_);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 13:58:06 -06:00
|
|
|
u64
|
2025-09-17 23:51:16 -04:00
|
|
|
fs::inode::calc(const std::string &branch_path_,
|
Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
2026-05-17 13:40:53 -04:00
|
|
|
const fs::relpath &fusepath_,
|
2026-04-10 22:04:57 -04:00
|
|
|
const mode_t mode_,
|
|
|
|
|
const ino_t ino_)
|
|
|
|
|
{
|
Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
2026-05-17 13:40:53 -04:00
|
|
|
return g_func.load()(branch_path_,
|
2026-04-10 22:04:57 -04:00
|
|
|
fusepath_.native(),
|
|
|
|
|
mode_,
|
|
|
|
|
ino_);
|
2025-07-25 17:53:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
2025-09-17 23:51:16 -04:00
|
|
|
fs::inode::calc(const std::string &branch_path_,
|
Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
2026-05-17 13:40:53 -04:00
|
|
|
const fs::relpath &fusepath_,
|
2026-04-10 22:04:57 -04:00
|
|
|
struct stat *st_)
|
|
|
|
|
{
|
|
|
|
|
st_->st_ino = calc(branch_path_,
|
|
|
|
|
fusepath_,
|
|
|
|
|
st_->st_mode,
|
|
|
|
|
st_->st_ino);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 17:53:48 -05:00
|
|
|
void
|
Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
2026-05-17 13:40:53 -04:00
|
|
|
fs::inode::calc(const std::string &branch_path_,
|
|
|
|
|
const fs::relpath &fusepath_,
|
2026-04-10 22:04:57 -04:00
|
|
|
struct fuse_statx *st_)
|
|
|
|
|
{
|
|
|
|
|
st_->ino = calc(branch_path_,
|
|
|
|
|
fusepath_,
|
|
|
|
|
st_->mode,
|
|
|
|
|
st_->ino);
|
|
|
|
|
}
|