1
0
Files
overlaydirs/overlay.cc
2025-05-27 11:36:06 +03:00

843 lines
21 KiB
C++

/* SPDX-License-Identifier: Apache-2.0
* (c) 2025, Konstantin Demin
*/
#include "overlay.hh"
#include <cerrno>
#include <cstdio>
#include <list>
#include <set>
#include <tuple>
extern "C" {
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/limits.h>
}
struct x_dirspec_t {
dev_t dev;
ino_t ino;
int fd;
};
typedef std::vector<x_dirspec_t> dirspec_vec;
typedef std::set<std::tuple<dev_t, ino_t>> dev_ino_set;
struct x_entry_t {
int32_t root_id;
uint32_t type;
xxhash_t name_hash;
dev_t dev;
ino_t ino;
ovl_name_t name;
};
static
size_t procfs_fd2name(int fd, char * buffer, size_t size);
template <typename T>
static inline
bool set_contains(const std::set<T> & set, const T & needle);
static inline
bool filter_out_dots(const char * name);
static inline
bool handle_file_type(int type, bool allow_symlinks = false);
static inline
ovl_entry_kind dirent_to_entry_kind(typeof(dirent::d_type) d_type);
bool arg_to_rootspec(const char * arg, ovl_dirspec_t * rootspec, uint32_t flags)
{
if ((!arg) || (!rootspec)) return false;
if (!(arg[0])) return false;
int fd, fd_real;
fd = open(arg, O_DIRECTORY | O_RDONLY);
// here is relatively small window for race/TOCTOU
fd_real = open(arg, O_PATH | O_NOFOLLOW | O_RDONLY);
if (fd < 0) {
if (fd_real >= 0) (void) close(fd_real);
return false;
}
(void) memset(rootspec, 0, sizeof(ovl_dirspec_t));
do {
struct stat st;
(void) memset(&st, 0, sizeof(st));
if (fstat(fd, &st) != 0) break;
if (!S_ISDIR(st.st_mode)) break;
rootspec->dev = st.st_dev;
rootspec->ino = st.st_ino;
rootspec->fd = fd;
rootspec->root.len = procfs_fd2name((fd_real < 0) ? fd : fd_real, rootspec->root.str, ovl_path_max);
} while (false);
if ((fd_real >= 0) && (fd_real != fd)) {
(void) close(fd_real);
}
// basic sanity checks:
// - path length is non-zero
// - either dev_t or ino_t are non-zero
if ((!rootspec->root.len) || ((!rootspec->dev) && (!rootspec->ino))) {
// not needed anymore
(void) close(fd);
(void) memset(rootspec, 0, sizeof(ovl_dirspec_t));
return false;
}
// try to adjust/shorten path against current working directory
do {
bool relative = !!(flags & ovl_rootspec_flags::relative);
if (!relative) break;
ovl_path_t cwd;
(void) memset(&cwd, 0, sizeof(cwd));
if (!getcwd(cwd.str, ovl_path_max)) break;
cwd.len = strnlen(cwd.str, ovl_path_max);
if ((!cwd.len) || (cwd.len == ovl_path_max)) break;
// cwd is root - nothing to do here
if ((cwd.len == 1) && (cwd.str[0] == '/')) break;
// ensure that rootpath has prefix of cwd + "/"
if (cwd.len >= rootspec->root.len) break;
if (strncmp(rootspec->root.str, cwd.str, cwd.len) != 0) break;
if (rootspec->root.str[cwd.len] != '/') break;
// "cwd" isn't needed anymore so reuse it as temporary container
(void) memset(cwd.str, 0, cwd.len);
size_t prefix_len = cwd.len + 1;
cwd.len = rootspec->root.len - prefix_len;
(void) memcpy(cwd.str, &rootspec->root.str[prefix_len], cwd.len);
// finally replace with adjusted value
(void) memcpy(&rootspec->root, &cwd, sizeof(ovl_path_t));
} while (false);
return true;
}
bool is_empty_dir(int dirfd)
{
// internal method, parameters are already checked
/* if (dirfd < 0) return false; */
// reset handle to beginning
(void) lseek(dirfd, 0, SEEK_SET);
int fd_dir = dup(dirfd);
if (fd_dir < 0) return false;
DIR * p_dir = fdopendir(fd_dir);
if (!p_dir) {
(void) close(fd_dir);
return false;
}
// read current directory entries
bool empty = true;
for (struct dirent * dent = nullptr; (dent = readdir(p_dir)) != nullptr; ) {
if (!filter_out_dots(dent->d_name)) continue;
empty = false;
break;
}
// rewind directory
rewinddir(p_dir);
(void) lseek(fd_dir, 0, SEEK_SET);
// close handles
(void) closedir(p_dir);
// handle was reowned [by libc] via fdopendir() but why not to try to close it [again]
(void) close(fd_dir);
return empty;
}
class overlay_processor {
sort_mode _sort;
unsigned int _depth;
ovl_dirspec_vec _roots;
// we should report problems when they arise but there's common problem:
// name is relative to almost unknown path and we don't know the full path.
// furthermore we won't track path due to performance reasons.
ovl_entry_vec _impl(const dirspec_vec & paths, unsigned int depth) {
ovl_entry_vec rv = ovl_entry_vec();
if (!depth) return rv;
int n_usable_paths = 0;
for (const auto & p : paths) {
if (p.fd < 0) continue;
n_usable_paths++;
}
if (!n_usable_paths) return rv;
bool multiple_layers = n_usable_paths > 1;
auto seen_paths = dev_ino_set();
auto dirs = std::list<x_entry_t>();
auto nondirs = std::list<x_entry_t>();
for (size_t i = 0; i < paths.size(); i++) {
const auto & p = paths[i];
if (p.fd < 0) continue;
if (multiple_layers) {
auto needle = std::make_tuple(p.dev, p.ino);
if (set_contains(seen_paths, needle)) {
continue;
}
(void) seen_paths.emplace(needle);
}
// reset handle to beginning
(void) lseek(p.fd, 0, SEEK_SET);
int fd_dir = dup(p.fd);
if (fd_dir < 0) {
(void) fprintf(stderr, "overlaydirs: dup(%d): %s\n", p.fd, strerror(errno));
continue;
}
DIR * p_dir = fdopendir(fd_dir);
if (!p_dir) {
(void) fprintf(stderr, "overlaydirs: fdopendir(%d): %s\n", fd_dir, strerror(errno));
(void) close(fd_dir);
continue;
}
// read current directory entries
auto ent_all = std::vector<x_entry_t>();
for (struct dirent * dent = nullptr; (dent = readdir(p_dir)) != nullptr; ) {
if (!filter_out_dots(dent->d_name)) continue;
if (!handle_file_type(dent->d_type)) continue;
x_entry_t t;
(void) memset(&t, 0, sizeof(t));
// 1. only basic info is filled - name and type
// 2. directories and symlinks are handled later
// 3. dev_t and ino_t are relevant only for directories
t.name.len = strnlen(dent->d_name, sizeof(dirent::d_name));
if (t.name.len == sizeof(dirent::d_name)) {
// TODO: report warning about too long name
// (see function' comment)
continue;
}
t.type = dent->d_type;
(void) memcpy(t.name.str, dent->d_name, t.name.len);
ent_all.push_back(t);
}
// rewind directory
rewinddir(p_dir);
(void) lseek(fd_dir, 0, SEEK_SET);
// close handles
(void) closedir(p_dir); /* p_dir = nullptr; */
// handle was reowned [by libc] via fdopendir() but why not to try to close it [again]
(void) close(fd_dir); /* fd_dir = -1; */
// process entries
auto hash_skip = std::set<xxhash_t>();
auto ent_dirs = std::list<x_entry_t>();
auto ent_nondirs = std::list<x_entry_t>();
for (auto & x : ent_all) {
bool is_skip = false;
do {
if (x.name.len < 2) break;
/* if (strncmp(&x.name.name[x.name.len - 2], ".-", 2) != 0) break; */
if (x.name.str[x.name.len - 2] != '.') break;
if (x.name.str[x.name.len - 1] != '-') break;
// current entry is "skip" entry
is_skip = true;
} while (false);
if (is_skip) {
// skip file is found but we're not using them
if (!multiple_layers) continue;
// don't use "bare skip" entries (e.g. named exactly ".-")
if (x.name.len == 2) continue;
(void) hash_skip.emplace(xxhash(x.name.str, x.name.len - 2));
continue;
}
if ((x.type == DT_DIR) || (x.type == DT_LNK)) {
struct stat st;
(void) memset(&st, 0, sizeof(st));
if (fstatat(p.fd, x.name.str, &st, 0) != 0) {
// TODO: report warning about unavailable entry
// (see function' comment)
continue;
}
// handle (new) entry type
typeof(x_entry_t::type) new_type = IFTODT(st.st_mode);
if ((x.type == DT_DIR) && (new_type != DT_DIR)) {
// TODO: report warning about inconsistent entry type
// (see function' comment)
// PS: this is probably a bug
continue;
}
// skip dangling symlinks along with unknown file types
if (!handle_file_type(new_type, true)) {
// TODO: report warning about unapplicable entry type
// (see function' comment)
continue;
}
// adjust entry type
x.type = new_type;
if (x.type == DT_DIR) {
if (depth > 1) {
// we should track precise dev_t and ino_t for directories
x.dev = st.st_dev;
x.ino = st.st_ino;
}
else {
// end-of-depth directories are handled as regular files
x.type = DT_REG;
}
}
}
x.root_id = i;
x.name_hash = xxhash(x.name.str, x.name.len);
if (x.type == DT_DIR) {
ent_dirs.push_back(x);
} else {
ent_nondirs.push_back(x);
}
}
// not needed anymore
ent_all.clear();
ent_all.shrink_to_fit();
// remove "skip" entries
if (!hash_skip.empty()) {
nondirs.remove_if(
[&hash_skip](const x_entry_t & x) -> bool {
return set_contains(hash_skip, x.name_hash);
}
);
dirs.remove_if(
[&hash_skip](const x_entry_t & x) -> bool {
return set_contains(hash_skip, x.name_hash);
}
);
// not needed anymore
hash_skip.clear();
}
// "hash_skip" is now empty and doesn't contain exactly "skip" entries
// but is going to be reused further
// override all entries with current "non-dir" entries
if (!ent_nondirs.empty()) {
for (const auto & x : ent_nondirs) {
(void) hash_skip.emplace(x.name_hash);
}
nondirs.remove_if(
[&hash_skip](const x_entry_t & x) -> bool {
return set_contains(hash_skip, x.name_hash);
}
);
dirs.remove_if(
[&hash_skip](const x_entry_t & x) -> bool {
return set_contains(hash_skip, x.name_hash);
}
);
// not needed anymore
hash_skip.clear();
}
// override "non-dir" entries with "dir" entries
if (!ent_dirs.empty()) {
for (const auto & x : ent_dirs) {
(void) hash_skip.emplace(x.name_hash);
}
nondirs.remove_if(
[&hash_skip](const x_entry_t & x) -> bool {
return set_contains(hash_skip, x.name_hash);
}
);
// not needed anymore
hash_skip.clear();
}
// merge current entries with accumulated "all" entries
for (const auto & x : ent_nondirs) {
nondirs.push_back(x);
}
ent_nondirs.clear();
for (const auto & x : ent_dirs) {
dirs.push_back(x);
}
ent_dirs.clear();
}
// sort "dir" entries
if (_sort != sort_mode::none) {
dirs.sort(
[this](const x_entry_t & a, const x_entry_t & b) -> bool {
if (a.name_hash == b.name_hash) return a.root_id < b.root_id;
return custom_sort(a.name.str, b.name.str, _sort) < 0;
}
);
}
// merge entries
auto ent_all = std::list<x_entry_t>();
for (const auto & x : dirs) {
ent_all.push_back(x);
}
for (const auto & x : nondirs) {
ent_all.push_back(x);
}
// not needed anymore
nondirs.clear();
// sort entries
if (_sort != sort_mode::none) {
ent_all.sort(
[this](const x_entry_t & a, const x_entry_t & b) -> bool {
// in case of directory entries
if (a.name_hash == b.name_hash) return a.root_id < b.root_id;
return custom_sort(a.name.str, b.name.str, _sort) < 0;
}
);
}
auto seen_dirs = std::set<xxhash_t>();
for (const auto & x : ent_all) {
if (x.root_id < 0) continue;
ovl_entry_kind etype = dirent_to_entry_kind(x.type);
if (etype == ovl_entry_kind::unknown) continue;
if (etype == ovl_entry_kind::directory) {
// skip if directory with the same name is already handled
if (set_contains(seen_dirs, x.name_hash)) continue;
(void) seen_dirs.emplace(x.name_hash);
}
ovl_entry_t t;
t.type = etype;
t.root_id = x.root_id;
t.name_hash = x.name_hash;
t.name.len = x.name.len;
(void) memcpy(t.name.str, x.name.str, t.name.len);
if (t.type == ovl_entry_kind::directory) {
// root_id is meaningless for directories
t.root_id = 0;
auto child_paths = dirspec_vec();
for (size_t i = 0; i < paths.size(); i++) {
child_paths.push_back(x_dirspec_t{ .dev = 0, .ino = 0, .fd = -1 });
}
for (const auto & x : dirs) {
if (x.name_hash != t.name_hash) continue;
int fd = openat(paths[x.root_id].fd, t.name.str, O_DIRECTORY | O_RDONLY);
if (fd < 0) {
// TODO: report warning about unsuccessful openat()
// (see function' comment)
continue;
}
child_paths[x.root_id].dev = x.dev;
child_paths[x.root_id].ino = x.ino;
child_paths[x.root_id].fd = fd;
}
t.children = _impl(child_paths, depth - 1);
for (const auto & x : child_paths) {
if (x.fd < 0) continue;
(void) close(x.fd);
}
child_paths.clear();
child_paths.shrink_to_fit();
}
rv.push_back(t);
}
// not needed anymore
dirs.clear();
return rv;
}
public:
overlay_processor(const ovl_dirspec_vec & roots, unsigned int depth, sort_mode sort) {
_roots = ovl_dirspec_vec();
for (const auto & r : roots) {
_roots.push_back(r);
}
_sort = sort;
_depth = (depth > ovl_depth_max) ? ovl_depth_max : depth;
}
ovl_result run(void) {
auto rv = ovl_result();
auto paths = dirspec_vec();
for (const auto & r : _roots) {
rv.roots.push_back(r.root);
paths.push_back(x_dirspec_t{ .dev = r.dev, .ino = r.ino, .fd = r.fd });
}
rv.entries = _impl(paths, _depth);
return rv;
}
};
ovl_result process_overlay(ovl_dirspec_vec & roots, sort_mode sort, unsigned int depth)
{
auto rv = ovl_result();
if (!depth) return rv;
if (roots.empty()) return rv;
rv = overlay_processor(roots, depth, sort).run();
// close all open file descriptors
for (auto & r : roots) {
(void) close(r.fd);
r.fd = -1;
}
return rv;
}
class overlay_printer {
print_mode _print;
ovl_result _ovl;
void _impl(const ovl_entry_vec & entries, const ovl_path_t * subpath = nullptr) {
for (const auto & e : entries) {
switch (e.type) {
case ovl_entry_kind::file:
print_path(_print, &(_ovl.roots[e.root_id]), subpath, &(e.name));
break;
case ovl_entry_kind::directory:
// NOT printing directory name
ovl_path_t n_subpath;
(void) memset(&n_subpath, 0, sizeof(n_subpath));
if (subpath) {
n_subpath.len = subpath->len + 1 + e.name.len;
if (n_subpath.len >= ovl_path_max) {
(void) fprintf(stderr, "subpath too long: /%s/%s/\n", subpath->str, e.name.str);
continue;
}
/* (void) snprintf(n_subpath.str, ovl_path_max, "%s/%s", subpath->str, e.name.str); */
size_t off = 0;
(void) memcpy(n_subpath.str, subpath->str, subpath->len);
off = subpath->len;
n_subpath.str[off++] = '/';
(void) memcpy(n_subpath.str + off, e.name.str, e.name.len);
} else {
// ovl_name_max is definitely smaller than ovl_path_max
n_subpath.len = e.name.len;
(void) memcpy(n_subpath.str, e.name.str, e.name.len);
}
_impl(e.children, &n_subpath);
break;
default:
break;
}
}
}
public:
overlay_printer(const ovl_result & ovl, print_mode print) {
_ovl = ovl;
_print = print;
}
void run(void) {
_impl(_ovl.entries);
}
};
void list_overlay(const ovl_result & overlay, print_mode print)
{
overlay_printer(overlay, print).run();
}
class overlay_merger {
ovl_result _ovl;
ovl_dirspec_t _target;
int _impl(const ovl_entry_vec & entries, int dirfd, const ovl_path_t * subpath = nullptr) {
int rv = 0;
int new_fd;
ovl_path_t t;
for (const auto & e : entries) {
switch (e.type) {
case ovl_entry_kind::file:
(void) memset(&t, 0, sizeof(t));
if (subpath) {
t.len = _ovl.roots[e.root_id].len + 1 + subpath->len + 1 + e.name.len;
if (t.len >= ovl_path_max) {
(void) fprintf(stderr, "name too long: %s/%s/%s\n", _ovl.roots[e.root_id].str, subpath->str, e.name.str);
return ENAMETOOLONG;
}
/* (void) snprintf(t.str, ovl_path_max, "%s/%s/%s\n", _ovl.roots[e.root_id].str, subpath->str, e.name.str); */
size_t off = 0;
(void) memcpy(t.str, _ovl.roots[e.root_id].str, _ovl.roots[e.root_id].len);
off = _ovl.roots[e.root_id].len;
t.str[off++] = '/';
(void) memcpy(t.str + off, subpath->str, subpath->len);
off += subpath->len;
t.str[off++] = '/';
(void) memcpy(t.str + off, e.name.str, e.name.len);
} else {
t.len = _ovl.roots[e.root_id].len + 1 + e.name.len;
if (t.len >= ovl_path_max) {
(void) fprintf(stderr, "name too long: %s/%s\n", _ovl.roots[e.root_id].str, e.name.str);
return ENAMETOOLONG;
}
/* (void) snprintf(t.str, ovl_path_max, "%s/%s\n", _ovl.roots[e.root_id].str, e.name.str); */
size_t off = 0;
(void) memcpy(t.str, _ovl.roots[e.root_id].str, _ovl.roots[e.root_id].len);
off = _ovl.roots[e.root_id].len;
t.str[off++] = '/';
(void) memcpy(t.str + off, e.name.str, e.name.len);
}
if (symlinkat(t.str, dirfd, e.name.str) < 0) {
rv = errno;
if (subpath) {
(void) fprintf(stderr, "symlinkat() error: \"%s\" with %s/%s/%s -> %s\n", strerror(rv), _target.root.str, subpath->str, e.name.str, t.str);
} else {
(void) fprintf(stderr, "symlinkat() error: \"%s\" with %s/%s -> %s\n", strerror(rv), _target.root.str, e.name.str, t.str);
}
return rv;
}
break;
case ovl_entry_kind::directory:
(void) memset(&t, 0, sizeof(t));
if (subpath) {
t.len = subpath->len + 1 /* "/" */ + e.name.len;
if (t.len >= ovl_path_max) {
(void) fprintf(stderr, "subpath too long: /%s/%s/\n", subpath->str, e.name.str);
return ENAMETOOLONG;
}
/* (void) snprintf(t.str, ovl_path_max, "%s/%s", subpath->str, e.name.str); */
(void) memcpy(t.str, subpath->str, subpath->len);
t.str[subpath->len] = '/';
(void) memcpy(t.str + subpath->len + 1, e.name.str, e.name.len);
} else {
// ovl_name_max is definitely smaller than ovl_path_max
t.len = e.name.len;
(void) memcpy(t.str, e.name.str, e.name.len);
}
if (mkdirat(dirfd, e.name.str, 0755) < 0) {
rv = errno;
if (subpath) {
(void) fprintf(stderr, "mkdirat() error: \"%s\" with path %s/%s/%s\n", strerror(rv), _target.root.str, subpath->str, e.name.str);
} else {
(void) fprintf(stderr, "mkdirat() error: \"%s\" with path %s/%s\n", strerror(rv), _target.root.str, e.name.str);
}
return rv;
}
new_fd = openat(dirfd, e.name.str, O_DIRECTORY | O_RDONLY);
if (new_fd < 0) {
rv = errno;
if (subpath) {
(void) fprintf(stderr, "openat() error: \"%s\" with path %s/%s/%s\n", strerror(rv), _target.root.str, subpath->str, e.name.str);
} else {
(void) fprintf(stderr, "openat() error: \"%s\" with path %s/%s\n", strerror(rv), _target.root.str, e.name.str);
}
return rv;
}
rv = _impl(e.children, new_fd, &t);
(void) close(new_fd);
if (rv != 0) return rv;
break;
default:
break;
}
}
return rv;
}
public:
overlay_merger(const ovl_result & ovl, const ovl_dirspec_t & target) {
_ovl = ovl;
_target = target;
}
int run(void) {
return _impl(_ovl.entries, _target.fd);
}
};
int merge_overlay(const ovl_result & overlay, const ovl_dirspec_t & target)
{
if (!is_empty_dir(target.fd)) return -1;
return overlay_merger(overlay, target).run();
}
static
size_t procfs_fd2name(int fd, char * buffer, size_t size)
{
// internal method, parameters are already checked
/* if ((fd < 0) || (!buffer) || (!size)) return 0; */
// "/proc/self/fd/" - 14, "%d" - up to 10
char procfs_link[32];
(void) memset(procfs_link, 0, sizeof(procfs_link));
(void) snprintf(procfs_link, sizeof(procfs_link) - 1, "/proc/self/fd/%d", fd);
(void) memset(buffer, 0, size);
ssize_t x = readlink(procfs_link, buffer, size - 1);
x = (x > 0) ? x : 0;
// not really needed
buffer[x] = 0;
// already clamped
return (size_t) x;
}
#ifndef HAVE_CPP_SET_CONTAINS
#ifdef __cplusplus
#if __cplusplus >= 202002L
#define HAVE_CPP_SET_CONTAINS 1
#endif
#endif
#endif
#ifndef HAVE_CPP_SET_CONTAINS
#define HAVE_CPP_SET_CONTAINS 0
#endif
template <typename T>
static inline
bool set_contains(const std::set<T> & set, const T & needle)
{
#if HAVE_CPP_SET_CONTAINS
return set.contains(needle);
#else
return (set.find(needle) != set.cend());
#endif
}
static inline
bool filter_out_dots(const char * name)
{
/*
if (strcmp(name, ".") == 0) return false;
if (strcmp(name, "..") == 0) return false;
return true;
*/
if (name[0] != '.') return true;
switch (name[1]) {
case 0: return false;
case '.': return (name[2] != 0);
}
return true;
}
static inline
bool handle_file_type(int type, bool allow_symlinks)
{
switch (type) {
// common file types
case DT_REG: /* -fallthrough */
case DT_DIR: /* -fallthrough */
// less common types (but also allowed)
case DT_BLK: /* -fallthrough */
case DT_CHR: /* -fallthrough */
case DT_FIFO: /* -fallthrough */
case DT_SOCK:
return true;
// symlinks should be handled separately!
case DT_LNK:
return !allow_symlinks;
default:
return false;
}
}
static inline
ovl_entry_kind dirent_to_entry_kind(typeof(dirent::d_type) d_type) {
switch (d_type) {
case DT_DIR:
return ovl_entry_kind::directory;
case DT_REG: /* -fallthrough */
case DT_BLK: /* -fallthrough */
case DT_CHR: /* -fallthrough */
case DT_FIFO: /* -fallthrough */
case DT_SOCK:
return ovl_entry_kind::file;
// symlinks should be already handled!
}
return ovl_entry_kind::unknown;
}