mirror of
https://github.com/trapexit/mergerfs.git
synced 2026-10-20 08:18:40 +00:00
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.
556 lines
15 KiB
C++
556 lines
15 KiB
C++
/*
|
|
ISC License
|
|
|
|
Copyright (c) 2026, Antonio SJ Musumeci <trapexit@spawn.link>
|
|
|
|
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.
|
|
*/
|
|
|
|
#include "fs_path.hpp"
|
|
|
|
#include <cassert>
|
|
#include <cstring>
|
|
|
|
|
|
fs::relpath::relpath() noexcept
|
|
: _start(REL_OFFSET),
|
|
_size(0),
|
|
_rel_start(REL_OFFSET),
|
|
_rel_size(0)
|
|
{
|
|
_buf[REL_OFFSET] = '\0';
|
|
}
|
|
|
|
fs::relpath::relpath(const char *s_)
|
|
: fs::relpath()
|
|
{
|
|
if(s_ != nullptr)
|
|
assign(std::string_view(s_));
|
|
}
|
|
|
|
fs::relpath::relpath(std::string_view s_)
|
|
: fs::relpath()
|
|
{
|
|
assign(s_);
|
|
}
|
|
|
|
fs::relpath::relpath(const std::string &s_)
|
|
: fs::relpath()
|
|
{
|
|
assign(std::string_view(s_));
|
|
}
|
|
|
|
fs::relpath::relpath(const fs::relpath &o_)
|
|
: _start(o_._start),
|
|
_size(o_._size),
|
|
_rel_start(o_._rel_start),
|
|
_rel_size(o_._rel_size)
|
|
{
|
|
std::memcpy(_buf + _start,o_._buf + _start,_size + 1);
|
|
}
|
|
|
|
fs::relpath::relpath(fs::relpath &&o_) noexcept
|
|
: _start(o_._start),
|
|
_size(o_._size),
|
|
_rel_start(o_._rel_start),
|
|
_rel_size(o_._rel_size)
|
|
{
|
|
std::memcpy(_buf + _start,o_._buf + _start,_size + 1);
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::operator=(const fs::relpath &o_)
|
|
{
|
|
if(this == &o_)
|
|
return *this;
|
|
_start = o_._start;
|
|
_size = o_._size;
|
|
_rel_start = o_._rel_start;
|
|
_rel_size = o_._rel_size;
|
|
std::memcpy(_buf + _start,o_._buf + _start,_size + 1);
|
|
return *this;
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::operator=(fs::relpath &&o_) noexcept
|
|
{
|
|
if(this == &o_)
|
|
return *this;
|
|
_start = o_._start;
|
|
_size = o_._size;
|
|
_rel_start = o_._rel_start;
|
|
_rel_size = o_._rel_size;
|
|
std::memcpy(_buf + _start,o_._buf + _start,_size + 1);
|
|
return *this;
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::operator=(const char *s_)
|
|
{
|
|
return assign(s_ ? std::string_view(s_) : std::string_view());
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::operator=(std::string_view s_)
|
|
{
|
|
return assign(s_);
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::operator=(const std::string &s_)
|
|
{
|
|
return assign(std::string_view(s_));
|
|
}
|
|
|
|
void
|
|
fs::relpath::_init(std::string_view s_)
|
|
{
|
|
// No transformation: store the input verbatim. The caller is
|
|
// responsible for canonical rel form (no leading '/'). Mergerfs's
|
|
// pipeline guarantees this:
|
|
// - libfuse's try_get_path() ends with consolidate_to_rel(strip=
|
|
// true), which drops the leading '/' produced by prepend().
|
|
// - branch.path / fusepath via the free operator/ flows through
|
|
// concat() which does no stripping; the rel side comes in
|
|
// canonical and stays canonical.
|
|
//
|
|
// Storing inputs verbatim means an fs::relpath can also hold a
|
|
// fully-built absolute fullpath (e.g. the result of `branch.path /
|
|
// fusepath_`) without losing the leading '/'. Such an object is
|
|
// not a candidate for set_prefix() rebuilding -- doing so would
|
|
// produce double slashes -- but that combination is explicitly
|
|
// outside the documented contract.
|
|
|
|
const std::size_t n = s_.size();
|
|
if(n > 0)
|
|
std::memmove(_buf + REL_OFFSET,s_.data(),n);
|
|
_start = REL_OFFSET;
|
|
_size = static_cast<std::uint16_t>(n);
|
|
_rel_start = REL_OFFSET;
|
|
_rel_size = static_cast<std::uint16_t>(n);
|
|
_buf[REL_OFFSET+n] = '\0';
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::assign(std::string_view s_)
|
|
{
|
|
_init(s_);
|
|
return *this;
|
|
}
|
|
|
|
void
|
|
fs::relpath::clear() noexcept
|
|
{
|
|
// Reset to the canonical default-constructed state: anchored at
|
|
// REL_OFFSET, empty. This makes prepend() safe immediately after
|
|
// clear() so the path can be reused.
|
|
_size = 0;
|
|
_rel_start = REL_OFFSET;
|
|
_rel_size = 0;
|
|
_start = REL_OFFSET;
|
|
_buf[_start] = '\0';
|
|
}
|
|
|
|
void
|
|
fs::relpath::set_prefix(std::string_view p_)
|
|
{
|
|
// Contract: paths are in canonical mergerfs form.
|
|
// - prefix has no trailing '/'
|
|
// - rel has no leading '/'
|
|
// No sanitization happens here; if you violate the contract you
|
|
// get double slashes. Mergerfs's fusepaths come from libfuse with
|
|
// the leading '/' already stripped (consolidate_to_rel(strip=true)
|
|
// in try_get_path) and branch paths are realpath-canonicalized at
|
|
// config load, so the contract holds.
|
|
//
|
|
// Fast path: the prefix (plus '/' separator) fits in front of the
|
|
// current rel anchor _rel_start. Rel data is not touched, only
|
|
// [_start, _rel_start) is rewritten.
|
|
//
|
|
// Slow path: prefix is larger than the current prefix area. The
|
|
// rel data is shifted rightward, _rel_start advances to make room,
|
|
// then the prefix is written linearly. Under Linux PATH_MAX this
|
|
// branch is unreachable; it engages only under contract violation
|
|
// or future-larger PATH_MAX environments.
|
|
|
|
const std::size_t rel_size = _rel_size; // cached
|
|
const std::size_t p_size = p_.size();
|
|
|
|
if(p_size == 0)
|
|
{
|
|
// Drop the prefix. Rel stays at its current anchor.
|
|
_start = _rel_start;
|
|
_size = static_cast<std::uint16_t>(rel_size);
|
|
_buf[_start + _size] = '\0';
|
|
return;
|
|
}
|
|
|
|
const std::size_t sep = (rel_size > 0) ? 1 : 0;
|
|
const std::size_t needed = p_size + sep; // bytes of prefix-area required
|
|
|
|
// Contract guard: prefix + sep + rel + NUL must fit in BUF_SIZE.
|
|
// Violations should be caught at the source rather than silently
|
|
// corrupting the buffer via uint16 overflow.
|
|
assert(needed + rel_size < BUF_SIZE);
|
|
|
|
if(needed > _rel_start)
|
|
{
|
|
// Slow path: shift rel rightward so the prefix area can grow.
|
|
const std::size_t new_rel_start = needed;
|
|
if(rel_size > 0)
|
|
std::memmove(_buf + new_rel_start,_buf + _rel_start,rel_size);
|
|
_rel_start = static_cast<std::uint16_t>(new_rel_start);
|
|
}
|
|
|
|
const std::size_t new_start = _rel_start - needed;
|
|
std::memmove(_buf + new_start,p_.data(),p_size);
|
|
if(sep)
|
|
_buf[new_start + p_size] = '/';
|
|
_start = static_cast<std::uint16_t>(new_start);
|
|
_size = static_cast<std::uint16_t>(needed + rel_size);
|
|
_buf[_start + _size] = '\0';
|
|
// _rel_size unchanged: set_prefix only rewrites the prefix area
|
|
// (and, in the slow path, relocates the rel without changing its
|
|
// length).
|
|
}
|
|
|
|
bool
|
|
fs::relpath::_aliases(std::string_view s_) const noexcept
|
|
{
|
|
// Returns true when the view's data pointer lies inside _buf.
|
|
// Used to detect self-aliasing in assign_concat so we can stash the
|
|
// source before clobbering it.
|
|
std::uintptr_t p = reinterpret_cast<std::uintptr_t>(s_.data());
|
|
std::uintptr_t b = reinterpret_cast<std::uintptr_t>(_buf);
|
|
return (p >= b) && (p < b + BUF_SIZE);
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::assign_concat(std::string_view base_,
|
|
std::string_view rel_)
|
|
{
|
|
// The common case is that neither input aliases _buf (e.g.
|
|
// `branch.path / fusepath`, where branch.path is std::string and
|
|
// fusepath is a stack-owned fs::relpath belonging to a different
|
|
// object). Skip the BUF_SIZE stack reservation in that case and only
|
|
// allocate the temp when at least one input does alias.
|
|
//
|
|
// When aliasing is detected, stash the aliasing input(s) to a
|
|
// stack-local temp so the subsequent writes don't corrupt our own
|
|
// source data (e.g. `p /= "x"` where base_ is p.native()). The temp
|
|
// is BUF_SIZE so it can fit any well-formed base+rel.
|
|
const bool base_aliases = _aliases(base_);
|
|
const bool rel_aliases = _aliases(rel_);
|
|
if(base_aliases || rel_aliases)
|
|
{
|
|
alignas(8) char tmp[BUF_SIZE];
|
|
std::size_t tmp_pos = 0;
|
|
if(base_aliases)
|
|
{
|
|
std::memcpy(tmp + tmp_pos,base_.data(),base_.size());
|
|
base_ = std::string_view(tmp + tmp_pos,base_.size());
|
|
tmp_pos += base_.size();
|
|
}
|
|
if(rel_aliases)
|
|
{
|
|
std::memcpy(tmp + tmp_pos,rel_.data(),rel_.size());
|
|
rel_ = std::string_view(tmp + tmp_pos,rel_.size());
|
|
}
|
|
// base_ and rel_ now point into `tmp`. The rel write below and
|
|
// set_prefix both complete before this scope ends, so tmp's
|
|
// lifetime covers all reads.
|
|
_init(rel_);
|
|
set_prefix(base_);
|
|
return *this;
|
|
}
|
|
|
|
// Common path: no aliasing -- write rel at REL_OFFSET via _init,
|
|
// then prepend base via set_prefix.
|
|
_init(rel_);
|
|
set_prefix(base_);
|
|
return *this;
|
|
}
|
|
|
|
fs::relpath
|
|
fs::relpath::concat(std::string_view base_,
|
|
std::string_view rel_)
|
|
{
|
|
fs::relpath p;
|
|
p.assign_concat(base_,rel_);
|
|
return p;
|
|
}
|
|
|
|
fs::relpath
|
|
fs::relpath::operator/(std::string_view rhs_) const
|
|
{
|
|
return fs::relpath::concat(native(),rhs_);
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::operator/=(std::string_view rhs_)
|
|
{
|
|
return assign_concat(native(),rhs_);
|
|
}
|
|
|
|
std::string_view
|
|
fs::relpath::filename() const noexcept
|
|
{
|
|
if(_size == 0)
|
|
return {};
|
|
std::string_view sv = native();
|
|
auto pos = sv.rfind('/');
|
|
if(pos == std::string_view::npos)
|
|
return sv;
|
|
return sv.substr(pos + 1);
|
|
}
|
|
|
|
std::string_view
|
|
fs::relpath::parent_path() const noexcept
|
|
{
|
|
if(_size <= 1)
|
|
return {}; // "" or "/" -> ""
|
|
std::string_view sv = native();
|
|
auto pos = sv.rfind('/');
|
|
if(pos == std::string_view::npos)
|
|
return {};
|
|
if(pos == 0)
|
|
return sv.substr(0,1); // "/foo" -> "/"
|
|
return sv.substr(0,pos);
|
|
}
|
|
|
|
std::string_view
|
|
fs::relpath::extension() const noexcept
|
|
{
|
|
std::string_view fn = filename();
|
|
if(fn.empty() || fn == "." || fn == "..")
|
|
return {};
|
|
auto pos = fn.rfind('.');
|
|
if(pos == std::string_view::npos || pos == 0)
|
|
return {};
|
|
return fn.substr(pos);
|
|
}
|
|
|
|
std::string_view
|
|
fs::relpath::stem() const noexcept
|
|
{
|
|
std::string_view fn = filename();
|
|
if(fn == "." || fn == "..")
|
|
return fn;
|
|
std::string_view ext = extension();
|
|
return fn.substr(0,fn.size() - ext.size());
|
|
}
|
|
|
|
fs::relpath&
|
|
fs::relpath::remove_filename()
|
|
{
|
|
if(_size == 0)
|
|
return *this;
|
|
std::string_view sv = native();
|
|
auto pos = sv.rfind('/');
|
|
if(pos == std::string_view::npos)
|
|
{
|
|
clear();
|
|
return *this;
|
|
}
|
|
// Keep the trailing slash so the result is the directory form.
|
|
// Use parent_path() if you want the dirname without the trailing '/'.
|
|
const std::size_t new_size = pos + 1;
|
|
const std::size_t prefix_len = _rel_start - _start;
|
|
_size = static_cast<std::uint16_t>(new_size);
|
|
// Truncating into the prefix area drops the rel portion entirely;
|
|
// clamp to 0 so the uint16 can't underflow.
|
|
_rel_size = (new_size > prefix_len)
|
|
? static_cast<std::uint16_t>(new_size - prefix_len)
|
|
: 0;
|
|
_buf[_start + _size] = '\0';
|
|
return *this;
|
|
}
|
|
|
|
void
|
|
fs::relpath::resize(std::size_t n_)
|
|
{
|
|
if(n_ > _size)
|
|
// Zero-fill new bytes to match std::string::resize semantics.
|
|
std::memset(_buf + _start + _size,'\0',n_ - _size);
|
|
const std::size_t prefix_len = _rel_start - _start;
|
|
_size = static_cast<std::uint16_t>(n_);
|
|
// Shrinking below the prefix area drops the rel portion entirely;
|
|
// clamp to 0 so the uint16 can't underflow.
|
|
_rel_size = (n_ > prefix_len)
|
|
? static_cast<std::uint16_t>(n_ - prefix_len)
|
|
: 0;
|
|
_buf[_start + _size] = '\0';
|
|
}
|
|
|
|
void
|
|
fs::relpath::swap(fs::relpath &o_) noexcept
|
|
{
|
|
// Self-swap is a no-op; bail before touching memcpy with identical
|
|
// src/dst pointers (UB per the C standard) and avoid the round-trip
|
|
// through the stack temp.
|
|
if(this == &o_)
|
|
return;
|
|
|
|
// Copy this's used portion to a stack temp, slot in o's data, then
|
|
// restore the saved data into o. Touches only the actually-used
|
|
// bytes plus the metadata quadruplet.
|
|
alignas(8) char tmp[BUF_SIZE];
|
|
const std::uint16_t my_start = _start;
|
|
const std::uint16_t my_size = _size;
|
|
const std::uint16_t my_rel_start = _rel_start;
|
|
const std::uint16_t my_rel_size = _rel_size;
|
|
|
|
std::memcpy(tmp + my_start,_buf + my_start,my_size + 1);
|
|
|
|
_start = o_._start;
|
|
_size = o_._size;
|
|
_rel_start = o_._rel_start;
|
|
_rel_size = o_._rel_size;
|
|
std::memcpy(_buf + _start,o_._buf + _start,_size + 1);
|
|
|
|
o_._start = my_start;
|
|
o_._size = my_size;
|
|
o_._rel_start = my_rel_start;
|
|
o_._rel_size = my_rel_size;
|
|
std::memcpy(o_._buf + my_start,tmp + my_start,my_size + 1);
|
|
}
|
|
|
|
std::string_view
|
|
fs::relpath::prefix() const noexcept
|
|
{
|
|
const std::size_t plen = _rel_start - _start;
|
|
return {_buf + _start,plen};
|
|
}
|
|
|
|
std::string_view
|
|
fs::relpath::rel() const noexcept
|
|
{
|
|
return {_buf + _rel_start,_rel_size};
|
|
}
|
|
|
|
void
|
|
fs::relpath::append(const char *s_,
|
|
std::size_t n_)
|
|
{
|
|
if(n_ == 0)
|
|
return;
|
|
std::memcpy(_buf + _start + _size,s_,n_);
|
|
_size = static_cast<std::uint16_t>(_size + n_);
|
|
_rel_size = static_cast<std::uint16_t>(_rel_size + n_);
|
|
_buf[_start + _size] = '\0';
|
|
}
|
|
|
|
void
|
|
fs::relpath::append(std::string_view s_)
|
|
{
|
|
append(s_.data(),s_.size());
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// Extracts the next path component from sv (which has leading '/'
|
|
// already consumed), advancing sv past it. Returns empty when no
|
|
// more components.
|
|
inline
|
|
std::string_view
|
|
_next_component(std::string_view &sv_)
|
|
{
|
|
while(!sv_.empty() && sv_.front() == '/')
|
|
sv_.remove_prefix(1);
|
|
if(sv_.empty())
|
|
return {};
|
|
auto pos = sv_.find('/');
|
|
std::string_view c =
|
|
(pos == std::string_view::npos) ? sv_ : sv_.substr(0,pos);
|
|
sv_.remove_prefix(c.size());
|
|
return c;
|
|
}
|
|
}
|
|
|
|
fs::relpath
|
|
fs::relpath::lexically_relative(std::string_view base_) const
|
|
{
|
|
std::string_view a = native();
|
|
std::string_view b = base_;
|
|
|
|
// Both sides must have the same shape. fs::relpath always starts in
|
|
// canonical (no-leading-slash) form, but legacy callers and the
|
|
// libfuse symlink-rewrite path may pass leading-slash literals
|
|
// through native(); accept either as long as both match. Shape
|
|
// mismatch returns an empty result; a successful relativization
|
|
// produces "." (for equal paths) so empty is unambiguous failure.
|
|
const bool a_abs = (!a.empty() && a.front() == '/');
|
|
const bool b_abs = (!b.empty() && b.front() == '/');
|
|
if(a_abs != b_abs)
|
|
return fs::relpath();
|
|
if(a_abs)
|
|
{
|
|
a.remove_prefix(1);
|
|
b.remove_prefix(1);
|
|
}
|
|
|
|
for(;;)
|
|
{
|
|
auto sa = a;
|
|
auto sb = b;
|
|
auto ca = _next_component(sa);
|
|
auto cb = _next_component(sb);
|
|
if(ca.empty() && cb.empty())
|
|
break;
|
|
if(ca != cb)
|
|
break;
|
|
a = sa;
|
|
b = sb;
|
|
}
|
|
|
|
std::size_t up = 0;
|
|
{
|
|
std::string_view tmp = b;
|
|
for(;;)
|
|
{
|
|
auto c = _next_component(tmp);
|
|
if(c.empty())
|
|
break;
|
|
if(c == "..")
|
|
// ".." in base means the relativization can't resolve
|
|
// without filesystem access; return empty.
|
|
return fs::relpath();
|
|
if(c != ".")
|
|
++up;
|
|
}
|
|
}
|
|
|
|
fs::relpath result;
|
|
for(std::size_t i = 0; i < up; ++i)
|
|
{
|
|
if(i > 0)
|
|
result.append("/",1);
|
|
result.append("..",2);
|
|
}
|
|
|
|
for(;;)
|
|
{
|
|
auto c = _next_component(a);
|
|
if(c.empty())
|
|
break;
|
|
if(!result.empty())
|
|
result.append("/",1);
|
|
result.append(c);
|
|
}
|
|
|
|
if(result.empty())
|
|
result.assign(".");
|
|
|
|
return result;
|
|
}
|