#include "fuse.hpp"
#include "handler.hpp"
#include <fnmatch.h>
#include <dirent.h>
#include <vector>
#define FUSE_USE_VERSION 31
#include <fuse.h> // libfuse-dev; -lfuse
#define abs2rel(path) while (*path == '/') path++ // TODO: more sanitization needed?
typedef struct {
bool is_fd;
union {
int fd;
struct {
char* buf;
off_t len;
};
};
} read_ctx_t;
static struct {
const config_t* config;
int src_fd;
struct stat src_stat;
} ctx = {};
static bool filter_match(const char* path, bool is_fn=false) {
if (!*ctx.config->filter) {
return true;
}
if (!is_fn) {
const char* p = strrchr(path, '/');
if (p) {
path = p+1;
}
}
return fnmatch(ctx.config->filter, path, 0) == 0;
}
// call external handler to get list of injected files
static int readdir_ext(const char* path, char*& buf, std::vector<const char*>& list) {
int len = handler(ctx.config->list_handler, path, ctx.src_fd, -1, buf);
if (len < 0) {
return len;
}
char* p = buf;
while (*p) {
char* nl = strchr(p, '\n'); // ok as 0-terminated
if (nl == p) { // empty line?
p++;
} else if (nl) {
*nl = '\0';
if (!strchr(p, '/')) {
if (filter_match(p, true)) {
for (std::vector<const char*>::iterator it = list.begin(); it != list.end(); it++) {
if (strcmp(p, *it) == 0) {
LOG("'%s' returned '%s' for '%s' again, skipping", ctx.config->list_handler, *it, path);
list.erase(it);
break;
}
}
list.push_back(p);
}
}
p = nl+1;
} else { // *p but no newline
free(buf);
LOG("cannot parse '%s' result for '%s'", ctx.config->list_handler, path);
return -EBADMSG;
}
}
LOG_DEBUG("got %zu new entries for '%s'", list.size(), path);
return 0;
}
// call external handler to get list of injected files, returns whether a file is found
static bool readdir_ext(const char* path) {
if (!filter_match(path)) {
return false;
}
// TODO: some better solution
char dirname[PATH_MAX];
if (strlen(path) >= sizeof(dirname)) {
LOG_ERR(E2BIG, "'%s'", path);
return false;
}
strcpy(dirname, path);
char* p = strrchr(dirname, '/');
if (p) { // not in root
*p = '\0';
}
// ask list handler for parent dir
char* ext_buf = NULL;
std::vector<const char*> ext_list;
int rv = readdir_ext(p? dirname: "", ext_buf, ext_list);
if (rv < 0) {
return false;
}
// search through results
bool found = false;
for (std::vector<const char*>::iterator it = ext_list.begin(); it != ext_list.end(); it++) {
if (strcmp(*it, p? p+1: path) == 0) {
found = true;
break;
}
}
free(ext_buf);
ext_list.clear();
LOG_DEBUG("'%s': %sfound", path, found? "": "not ");
return found;
}
// fuse readdir callback
static int readdir_cb(const char* path, void* buf, fuse_fill_dir_t filler, off_t off, struct fuse_file_info* fi) {
LOG_DEBUG("readdir '%s'", path);
// get dir handle
abs2rel(path);
int fd = *path? openat(ctx.src_fd, path, O_RDONLY|O_DIRECTORY): dup(ctx.src_fd);
if (fd == -1) {
int e = errno;
LOG_ERRNO("cannot open '%s' for readdir", path);
return -e;
}
DIR* dp = fdopendir(fd);
if (dp == NULL) {
int e = errno;
LOG_ERRNO("cannot open '%s' for readdir ptr", path);
(void)close(fd);
return -e;
}
rewinddir(dp); // in case it was dup'ed above
int rv = 0;
// get all injected files for this dir
char* ext_buf = NULL;
std::vector<const char*> ext_list;
if ((rv = readdir_ext(path, ext_buf, ext_list)) < 0) {
(void)closedir(dp);
return rv;
}
// readdir
struct dirent entry;
struct dirent* result;
while (true) {
rv = readdir_r(dp, &entry, &result);
if (rv != 0) break;
if (!result) break;
// we iterate over it all again as we don't trust the external tool to ensure uniqueness. otherwise, we could simply pass the filler.
for (std::vector<const char*>::iterator it = ext_list.begin(); it != ext_list.end(); it++) {
if (strcmp(entry.d_name, *it) == 0) {
LOG("'%s' already exists in '%s', skipping", *it, path);
ext_list.erase(it);
break;
}
}
LOG_DEBUG("* '%s'", entry.d_name);
if (filler(buf, entry.d_name, NULL, 0) != 0) { // TODO: allow seeking TODO: provide stat?
rv = E2BIG;
break;
}
}
// bail out upon err
(void)closedir(dp);
if (rv != 0) {
LOG_ERR(rv, "readdir '%s'", path);
if (ext_buf) {
free(ext_buf);
}
return -rv;
}
// add injected files last
for (std::vector<const char*>::iterator it = ext_list.begin(); it != ext_list.end(); it++) {
LOG_DEBUG("+ '%s'", *it);
if (filler(buf, *it, NULL, 0) != 0) {
free(ext_buf);
LOG_ERR(E2BIG, "readdir+ '%s'", path);
return -E2BIG;
}
}
if (ext_buf) {
free(ext_buf);
}
return 0;
}
// provide dummy attrs for (yet) unexisting files
static int getattr_ext(const char* path, struct stat* stbuf) {
// ask list handler for parent dir
if (!readdir_ext(path)) {
return -ENOENT;
}
time_t now = time(NULL);
stbuf->st_mode = S_IFREG | (ctx.src_stat.st_mode & (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)); // take root's r/w permission mask
stbuf->st_nlink = 1;
stbuf->st_uid = geteuid(); // but our own id
stbuf->st_gid = getegid();
stbuf->st_size = 0;
stbuf->st_atime = now;
stbuf->st_mtime = now;
stbuf->st_ctime = now;
return 0;
}
// fuse stat callback
static int getattr_cb(const char* path, struct stat* stbuf) {
LOG_DEBUG("getattr '%s'", path);
abs2rel(path);
if ((*path? fstatat(ctx.src_fd, path, stbuf, 0): fstat(ctx.src_fd, stbuf)) == -1) {
if (errno == ENOENT) {
return *path? getattr_ext(path, stbuf): -ENOENT;
}
int e = errno;
LOG_ERRNO("getattr '%s'", path);
return -e;
}
return 0;
}
// call external handler for file contents streamed to disk (persistent)
static int open_ext(const char* path) {
if (!readdir_ext(path)) {
return -ENOENT;
}
int fd = openat(ctx.src_fd, path, O_CREAT|O_EXCL|O_RDWR, ctx.src_stat.st_mode & (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)); // TODO: tempfile & atomic move instead?
if (fd == -1) {
int e = errno; // exists? -> race
LOG_ERRNO("cannot create '%s'", path);
return -e;
}
char* dummy;
int rv = handler(ctx.config->file_handler, path, ctx.src_fd, fd, dummy);
if (rv < 0) {
(void)unlinkat(ctx.src_fd, path, 0);
(void)close(fd);
return rv;
}
return fd;
}
// call external handler for file contents kept in memory (non-persistent)
static int open_ext(const char* path, char*& buf, off_t& len) {
if (!readdir_ext(path)) {
return -ENOENT;
}
len = handler(ctx.config->file_handler, path, ctx.src_fd, -1, buf);
return (len < 0)? len: 0;
}
// fuse open callback
static int open_cb(const char *path, struct fuse_file_info* fi) {
LOG_DEBUG("open '%s'", path);
if ((fi->flags & O_WRONLY) || (fi->flags & O_RDWR)) {
return -EROFS; // not sure if needed
}
fi->direct_io = 1;
fi->keep_cache = 0;
// get fd by local file or newly created one
abs2rel(path);
if (!*path) return -EISDIR;
int fd = openat(ctx.src_fd, path, O_RDONLY); // O_NOFOLLOW?
if (fd == -1) {
if (errno != ENOENT) {
return -errno;
}
if (ctx.config->persistent) { // create file now from external tool
fd = open_ext(path);
if (fd < 0) { // valid fd otherwise
return fd;
}
}
}
// pass along fd or buf with file contents
read_ctx_t* data = (read_ctx_t*)malloc(sizeof(read_ctx_t));
if (fd != -1) {
data->is_fd = true;
data->fd = fd;
} else {
data->is_fd = false;
int rv = open_ext(path, data->buf, data->len);
if (rv < 0) {
free(data);
return rv;
}
}
fi->fh = (intptr_t)data;
return 0;
}
// fuse close callback
static int release_cb(const char* path, struct fuse_file_info* fi) {
LOG_DEBUG("release '%s'", path);
if (!fi->fh) {
return -EINVAL;
}
read_ctx_t* data = (read_ctx_t*)fi->fh;
if (data->is_fd) {
(void)close(data->fd);
} else {
free(data->buf);
}
free(data);
fi->fh = 0;
return 0;
}
// fuse read callback
static int read_cb(const char* path, char* buf, size_t len, off_t off, struct fuse_file_info* fi) {
LOG_DEBUG("read '%s': %ld:%zu", path, (long)off, len);
// get fd or buf from open
if (!fi->fh) {
return -EINVAL;
}
const read_ctx_t* data = (read_ctx_t*)fi->fh;
int rv = 0;
if (data->is_fd) {
while (rv < (int)len) {
ssize_t r = pread(data->fd, buf+rv, len-rv, off+rv);
if (r == 0) {
break; // eof
} else if (r < 0) {
rv = -errno;
break;
} else {
rv += r;
}
}
} else {
if (off >= data->len) {
return 0;
} else if (off + (off_t)len > data->len) {
len = data->len - off;
}
memcpy(buf, data->buf, len);
rv = len;
}
if (rv < 0) {
LOG_ERR(-rv, "cannot read from '%s'", path);
}
return rv;
}
// actual main
bool fifuma_fuse_loop(const config_t& config) {
LOG_DEBUG("source: '%s'", config.src);
LOG_DEBUG("mountpoint: '%s'", config.dst);
LOG_DEBUG("persistent: %d", config.persistent? 1: 0);
LOG_DEBUG("filter: '%s'", *config.filter? config.filter: "*");
LOG_DEBUG("dir-handler: '%s'", config.list_handler);
LOG_DEBUG("file-handler: '%s'", config.file_handler);
// setup global context
ctx.config = &config;
ctx.src_fd = open(config.src, O_RDONLY|O_DIRECTORY);
if (ctx.src_fd == -1) { // get fd before mounting
LOG_ERRNO("open(%s,O_DIRECTORY)", config.src);
return 1;
}
if (fstat(ctx.src_fd, &ctx.src_stat) == -1) {
LOG_ERRNO("fstat(%s)", ctx.config->src);
return -EBADF;
}
// setup fuse args
bool overlay = (strcmp(config.src, config.dst) == 0);
const char* argv[] = {
"fifuma",
"-s", // single-threaded
"-f", // foreground
"-o",
overlay? "nonempty,ro,auto_unmount,default_permissions": "ro,auto_unmount,default_permissions",
config.dst, // mountpoint
NULL
};
int argc = (sizeof(argv) / sizeof(*argv)) - 1;
// setup fuse hooks
struct fuse_operations ops = {};
ops.getattr = getattr_cb;
ops.open = open_cb;
ops.read = read_cb;
ops.readdir = readdir_cb;
ops.release = release_cb;
LOG_DEBUG("starting up...");
return fuse_main(argc, (char**)argv, &ops, &ctx) == 0;
}