Repositories / jai.git
fs.h
Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git
// -*-C++-*-
#pragma once
#include "defer.h"
#include "err.h"
#include <algorithm>
#include <expected>
#include <filesystem>
#include <ranges>
#include <set>
#include <functional>
#include <string>
#include <string_view>
#include <dirent.h>
#include <fcntl.h>
#include <sys/acl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
// Self-closing file descriptor
using Fd = RaiiHelper<::close, int, -1>;
using std::filesystem::path;
inline path
cat(path left, const path &right)
{
return left += right;
}
inline size_t
components(const path &p)
{
return std::ranges::distance(p.begin(), p.end());
}
inline bool
contains(const path &dir, const path &subpath)
{
return std::ranges::mismatch(dir, subpath).in1 == dir.end();
}
// True if target matches pattern (with * expanded)
bool glob(std::string_view pattern, std::string_view target);
// Compare paths component by component so subtrees are contiguous
struct PathLess {
static bool operator()(const path &a, const path &b)
{
return std::ranges::lexicographical_compare(a, b);
}
};
using PathSet = std::set<path, PathLess>;
using PathMultiset = std::multiset<path, PathLess>;
struct BoolValidator {
template<typename T> bool operator()(const T &v) const { return bool(v); }
};
// Return a range for a subtree rooted at root. root itself will be
// returned only if it does not contain a trailing slash.
inline auto
subtree(const is_one_of<PathSet, PathMultiset> auto &s, const path &root)
{
if (root.relative_path().empty())
return std::ranges::subrange(s.begin(), s.end());
path end = root;
if (end.filename().empty())
end = end.parent_path();
// First possible pathname not under root is the (illegal) pathname
// in which root's final component has a '\0' byte appended.
end += '\0';
return std::ranges::subrange(s.lower_bound(root), s.lower_bound(end));
}
// Return a subtree in reverse order (suitable for unmounting).
inline auto
subtree_rev(const is_one_of<PathSet, PathMultiset> auto &s, const path &root)
{
return subtree(s, root) | std::views::reverse;
}
std::string fdpath(int fd, const path &file);
std::string fdpath(int fd, bool must = false);
PathMultiset mountpoints(const path &mountinfo = "/proc/self/mountinfo");
// source (if non-NULL) is the source printed in /proc/self/mountinfo
Fd xfsopen(const char *fsname, const char *source = nullptr);
// Calls fsconfig(FSCONFIG_CMD_CREATE) and fsmount.
Fd make_mount(int conffd, int attr = MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV);
Fd clone_tree(int dfd, const path &file = {}, bool recursive = false);
void xmnt_move(int fromfd, const path &frompath, int tofd, const path &topath,
int flags);
inline void
xmnt_move(int fromfd, int tofd, const path &topath = {}, int flags = 0)
{
xmnt_move(fromfd, path{}, tofd, topath, flags);
}
void xmnt_setattr(int fd, const path &file, const mount_attr &a,
unsigned int flags = AT_RECURSIVE);
inline void
xmnt_setattr(int fd, const mount_attr &a, unsigned int flags = AT_RECURSIVE)
{
xmnt_setattr(fd, {}, a, flags);
}
inline void
xmnt_propagate(int fd, std::uint64_t propagation, bool recursive = true)
{
mount_attr a{.propagation = propagation};
xmnt_setattr(fd, a, recursive ? AT_RECURSIVE : 0);
}
inline void
xmnt_propagate(int fd, path file, std::uint64_t propagation,
bool recursive = true)
{
mount_attr a{.propagation = propagation};
xmnt_setattr(fd, file, a, recursive ? AT_RECURSIVE : 0);
}
template<std::convertible_to<const char *>... Opt>
requires (sizeof...(Opt) % 2 == 0)
Fd
make_tmpfs(const char *source, Opt... opt)
{
Fd conf = xfsopen("tmpfs", source);
if constexpr (sizeof...(Opt)) {
auto options = std::to_array<const char *>({opt...});
for (auto i = 0uz; i < options.size() - 1; i += 2)
if (fsconfig(*conf, FSCONFIG_SET_STRING, options[i], options[i + 1], 0))
syserr(R"(fsconfig(tmpfs, "{}", "{}"))", options[i], options[i + 1]);
}
return make_mount(*conf);
}
bool recursive_umount(const path &tree, bool detach = true);
enum class FollowLinks {
kNoFollow = 0,
kFollow = 1,
};
using enum FollowLinks;
// Conservatively fails if file is not a regular file or cannot be
// statted for any reason.
bool is_fd_at_path(int targetfd, int dfd, const path &file,
FollowLinks follow = kNoFollow,
struct stat *sbout = nullptr);
bool is_dir_empty(int dirfd);
Fd ensure_dir(int dfd, const path &p, mode_t perm, FollowLinks follow,
bool okay_if_other_owner = false,
std::function<void(int)> createcb = [](int) {});
void make_whiteout(int dfd, const path &p);
bool is_mountpoint(int dfd, const path &file = {},
FollowLinks follow = kNoFollow);
// Open an exclusive lockfile to guard one-time setup. Might fail, in
// which case re-check the need for setup and try again.
Fd open_lockfile(int dfd, const path &file);
// If validate(get()) is true, returns the result of get() in the
// expected value. Otherwise, acquires the lock and returns an error
// value containing a Defer object that releases the lock.
//
// Be careful not to destroy the return value. This would be bad:
//
// if (auto r = lock_or_validate(...); r)
// return std::move(*r);
// // now you no longer have the lock
//
// Instead you want:
//
// auto r = lock_or_validate(...);
// if (r)
// return std::move(*r);
// // now you continue to hold the lock until r is destroyed
template<typename Get,
typename Validate = BoolValidator>
std::expected<std::decay_t<std::invoke_result_t<Get>>, Defer>
lock_or_validate(int dfd, path lockfile, Get get, Validate validate = {})
requires requires {
{ validate(get()) } -> std::convertible_to<bool>;
}
{
std::expected<std::decay_t<std::invoke_result_t<Get>>, Defer> ret =
std::unexpected{Defer{}};
Fd lock;
for (;;) {
if (validate(ret.emplace(get())))
return ret;
if (lock) {
ret = std::unexpected{Defer{[lock = std::move(lock), dfd, lockfile] {
unlinkat(dfd, lockfile.c_str(), 0);
}}};
return ret;
}
lock = open_lockfile(dfd, lockfile);
}
}
std::string open_flags_to_string(int flags);
inline Fd
xopenat(int dfd, const path &file, int flags, mode_t mode = 0755)
{
if (int fd = openat(dfd, file.c_str(), flags, mode); fd >= 0)
return fd;
syserr(R"(openat("{}", {}))",
dfd >= 0 ? (fdpath(dfd) / file).string() : file.string(),
open_flags_to_string(flags));
}
inline Fd
xdup(int fd, int minfd = 3)
{
auto ret = fcntl(fd, F_DUPFD_CLOEXEC, minfd);
if (ret == -1)
syserr("{}: F_DUPFD_CLOEXEC", fdpath(fd));
return ret;
}
inline std::expected<RaiiHelper<closedir>, std::system_error>
try_opendir(int dfd, path file = {}, FollowLinks follow = kNoFollow)
{
if (file.empty())
// re-open in case dfd is O_PATH and to avoid messing with the
// offset of dfd if we read the directory multiple times
file = ".";
Fd fd =
openat(dfd, file.c_str(),
O_RDONLY | O_DIRECTORY | (follow == kNoFollow ? O_NOFOLLOW : 0));
if (!fd)
return std::unexpected{
std::system_error(errno, std::system_category(), fdpath(dfd, file))};
if (auto d = fdopendir(*fd)) {
fd.release();
return d;
}
return std::unexpected{
std::system_error(errno, std::system_category(),
std::format("{}: fdopendir", fdpath(*fd)))};
}
inline RaiiHelper<closedir>
xopendir(int dfd, path file = {}, FollowLinks follow = kNoFollow)
{
if (auto r = try_opendir(dfd, file, follow))
return std::move(*r);
else
throw r.error();
}
// dirent::d_name is an array, so won't convert properly to types that
// treat a char array differently from a const char *.
inline const char *
d_name(const struct dirent *de)
{
return de->d_name;
}
inline std::array<Fd, 2>
xpipe()
{
int fds[2];
if (pipe2(fds, O_CLOEXEC))
syserr("pipe2");
return {fds[0], fds[1]};
}
inline struct stat
xfstat(int fd, path file = {}, FollowLinks follow = kFollow)
{
struct stat sb;
if (file.empty()) {
if (fstat(fd, &sb))
syserr(R"(fstat("{}"))", fdpath(fd));
}
else if (fstatat(fd, file.c_str(), &sb,
follow == kFollow ? 0 : AT_SYMLINK_NOFOLLOW))
syserr(R"({}stat("{}"))", follow == kFollow ? "" : "l", fdpath(fd, file));
return sb;
}
std::string read_fd(int fd);
// This tries to read a file. It will return an error if the file
// cannot be opened (e.g., because it does not exist), but could still
// throw if reading the actual file returns an error or allocating the
// buffer exhausts memory.
std::expected<std::string, std::system_error> try_read_file(int dfd,
path file = {});
inline std::string
read_file(int dfd, path file = {})
{
if (auto res = try_read_file(dfd, file))
return std::move(*res);
else
throw res.error();
}
inline void
create_warn(int fd)
{
warn("created {}", fdpath(fd));
}
Fd ensure_file(
int dfd, path file, std::string_view contents, int mode = 0600,
std::function<void(int)> createcb = [](int) {});
using ACL = RaiiHelper<acl_free, acl_t>;
enum AclType {
kAclAccess = ACL_TYPE_ACCESS, // Set ACL on inode
kAclDefault = ACL_TYPE_DEFAULT, // Set ACL for files created in directory
};
void set_fd_acl(int fd, const char *acltext, AclType which = kAclAccess);