/*
 * Copyright (C) Eugene 'Vindex' Stulin, 2023-2024
 *
 * Distributed under
 * the Boost Software License 1.0 or (at your option)
 * the GNU Lesser General Public License 3.0 or later.
 * This file is offered as-is, without any warranty.
 *
 * Function descriptions were copied (and partially modified)
 * from some header files of the FUSE library (licensed by GNU LGPL v2.1):
 *
 * This library was inspired by the dfuse library and uses some ideas from it:
 * https://github.com/dlang-community/dfuse (licensed by BSL-1.0).
 */

/**
 * Main module.
 * Contains the FileSystem class and some useful functions.
 */

module oxfuse.oxfuse;

import oxfuse.cfuse;

import std.algorithm : map;
import std.array : array;
import std.exception : enforce, collectException;
import std.format : format;
import std.string : toStringz, fromStringz;

import core.stdc.errno;
import core.stdc.stdio : printf;
static import core.stdc.stdlib;
import core.stdc.string : strcmp, memset, strerror, strncpy;

import core.thread : thread_attachThis;


/*******************************************************************************
 * Class with basic file operations for inheritance.
 * Operation descriptions are taken from libfuse documentation
 * with small modifications.
 *
 * File operations are marked with `@(null)` attribute. It means that they are
 * not implemented and the final operation functions will be null pointers.
 * When overriding file operations, you should not use these attributes
 * (see examples).
 */
abstract class FileSystem {

    /***************************************************************************
     * Initialize filesystem.
     */
    void initialize(fuse_conn_info* conn, fuse_config* cfg) {}

    /***************************************************************************
     * Clean up filesystem.
     *
     * Called on filesystem exit.
     */
    void destroy() {
        core.stdc.stdlib.exit(0);
    }

    /***************************************************************************
     * Get file attributes.
     *
     * Similar to stat(). The 'st_dev' and 'st_blksize' fields are ignored.
     * The 'st_ino' field is ignored except if the 'use_ino'
     * mount option is given. In that case it is passed to userspace,
     * but libfuse and the kernel will still assign a different
     * inode for internal use (called the "nodeid").
     *
     * `fi` will always be null if the file is not currently open, but
     * may also be null if the file is open.
     */
    @(null)
    stat_t getattr(string path, ffi_t* fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Create a directory.
     *
     * Note that the mode argument may not have the type specification
     * bits set, i.e. S_ISDIR(mode) can be false.
     * To obtain the correct directory type bits use mode|S_IFDIR
     */
    @(null)
    void mkdir(string path, mode_t mode) {}

    /***************************************************************************
     * Create a file node.
     *
     * This is called for creation of all non-directory, non-symlink nodes.
     * If the filesystem defines a create() method,
     * then for regular files that will be called instead.
     */
    @(null)
    void mknod(string path, mode_t mode, dev_t dev) {}

    /***************************************************************************
     * Create and open a file.
     *
     * If the file does not exist, first create it with the specified
     * mode, and then open it. If the file exists, open() is called.
     *
     * If this method is not implemented or under Linux kernel
     * versions earlier than 2.6.15, the mknod() and open() methods
     * will be called instead.
     */
    @(null)
    void create(string path, mode_t mode, ffi_t* fi) {}

    /***************************************************************************
     * Create a symbolic link.
     */
    @(null)
    void symlink(string src, string dst) {}

    /***************************************************************************
     * Read the target of a symbolic link.
     *
     * The buffer should be filled with a null terminated string.
     * The buffer size argument includes the space for the terminating
     * null character. If the linkname is too long to fit in the buffer,
     * it should be truncated.
     */
    @(null)
    string readlink(string path) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Create a hard link to a file.
     */
    @(null)
    void link(string src, string dst) {}

    /***************************************************************************
     * Check file access permissions.
     *
     * This will be called for the access() system call.
     * If the 'default_permissions' mount option is given,
     * this method is not called.
     *
     * This method is not called under Linux kernel versions 2.4.x.
     */
    @(null)
    bool access(string path, int mode) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Rename a file.
     *
     * *flags* may be `RENAME_EXCHANGE` or `RENAME_NOREPLACE`.
     * If `RENAME_NOREPLACE` is specified, the filesystem must not
     * overwrite *dst* if it exists and throw an error instead.
     * If `RENAME_EXCHANGE` is specified, the filesystem must atomically
     * exchange the two files, i.e. both must exist and neither may be deleted.
     */
    @(null)
    void rename(string src, string dst, uint flags) {}

    /***************************************************************************
     * Remove a directory.
     */
    @(null)
    void rmdir(string path) {}

    /***************************************************************************
     * Remove a file (hard link).
     */
    @(null)
    void unlink(string path) {}

    /***************************************************************************
     * Open a file.
     *
     * Open flags are available in fi.flags. The following rules apply.
     *
     *  - If O_CREAT is used and the file doesn't exist, create() is called.
     *
     *  - Access modes (O_RDONLY, O_WRONLY, O_RDWR, O_EXEC, O_SEARCH)
     *    should be used by the filesystem to check if the operation is
     *    permitted.  If the `-o default_permissions` mount option is
     *    given, this check is already done by the kernel before calling
     *    open() and may thus be omitted by the filesystem.
     *
     * Filesystem may store an arbitrary file handle (pointer,
     * index, etc.) in fi.fh, and use this in other all other file
     * operations (read, write, flush, release, fsync).
     *
     * Filesystem may also implement stateless file I/O and not store
     * anything in fi.fh.
     *
     * There are also some flags (direct_io, keep_cache) which the
     * filesystem may set in fi, to change the way the file is opened.
     * See fuse_file_info (ffi_t) structure for more details.
     *
     * If this request is answered with an error code of ENOTSUP
     * and FUSE_CAP_NO_OPEN_SUPPORT is set in
     * `fuse_conn_info.capable`, this is treated as success and
     * future calls to open will also succeed without being send
     * to the filesystem process.
     */
    @(null)
    void open(string path, ffi_t* fi) {}

    /***************************************************************************
     * Write data to an open file.
     *
     * Write should return exactly the number of bytes requested
     * except on error. An exception to this is when the 'direct_io'
     * mount option is specified (see read operation).
     *
     * Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
     * expected to reset the setuid and setgid bits.
     */
    @(null)
    int write(string path, const ubyte* data, size_t size, off_t off, ffi_t* fi)
    do {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Read data from an open file.
     *
     * Read should return exactly the number of bytes requested except
     * on EOF or error, otherwise the rest of the data will be
     * substituted with zeroes. An exception to this is when the
     * 'direct_io' mount option is specified, in which case the return
     * value of the read system call will reflect the return value of
     * this operation.
     */
    @(null)
    int read(string path, ubyte* buff, size_t, off_t off, ffi_t* fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Find next data or hole after the specified offset.
     */
    @(null)
    off_t lseek(string path, off_t off, int whence, ffi_t* fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Change the size of a file.
     *
     * `fi` will always be null if the file is not currenlty open, but
     * may also be null if the file is open.
     *
     * Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
     * expected to reset the setuid and setgid bits.
     */
    @(null)
    void truncate(string path, off_t off, ffi_t* fi) {}

    /***************************************************************************
     * Possibly flush cached data.
     *
     * This is not equivalent to fsync(). It's not a request to sync dirty data.
     *
     * Flush is called on each close() of a file descriptor, as opposed to
     * release which is called on the close of the last file descriptor for
     * a file.  Under Linux, errors returned by flush() will be passed to
     * userspace as errors from close(), so flush() is a good place to write
     * back any cached dirty data. However, many applications ignore errors
     * on close(), and on non-Linux systems, close() may succeed even if flush()
     * returns an error. For these reasons, filesystems should not assume
     * that errors returned by flush will ever be noticed or even
     * delivered.
     *
     * Note:
     *   The flush() method may be called more than once for each
     *   open().  This happens if more than one file descriptor refers to an
     *   open file handle, e.g. due to dup(), dup2() or fork() calls.  It is
     *   not possible to determine if a flush is final, so each flush should
     *   be treated equally.  Multiple write-flush sequences are relatively
     *   rare, so this shouldn't be a problem.
     *
     * Filesystems shouldn't assume that flush will be called at any
     * particular point.  It may be called more times than expected, or not
     * at all.
     *
     * See_Also:
     *   `http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html`
     */
    @(null)
    void flush(string path, ffi_t* fi) {}

    /***************************************************************************
     * Release an open file.
     *
     * Release is called when there are no more references to an open file:
     * all file descriptors are closed and all memory mappings are unmapped.
     *
     * For every open() call there will be exactly one release() call with the
     * same flags and file handle. It is possible to have a fiel opened more
     * than once, in which case only the last release will mean, that no more
     * reads/writes will happen on the file.
     */
    @(null)
    void release(string path, ffi_t* fi) {}

    /***************************************************************************
     * Open directory.
     *
     * Unless the 'default_permissions' mount option is given,
     * this method should check if opendir is permitted for this
     * directory. Optionally opendir may also return an arbitrary
     * filehandle in the ffi_t structure (fi.fh), which will be
     * passed to readdir, releasedir and fsyncdir.
     */
    @(null)
    void opendir(string path, ffi_t* fi) {}

    /***************************************************************************
     * Read directory.
     */
    @(null)
    string[] readdir(string path, ffi_t* fi) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Release directory. Corrensponds to 'closedir' system call.
     */
    @(null)
    void releasedir(string path, ffi_t* fi) {}

    /***************************************************************************
     * Change the permission bits of a file.
     *
     * `fi` will always be null if the file is not currenlty open, but
     * may also be null if the file is open.
     */
    @(null)
    void chmod(string path, mode_t mode, ffi_t* fi) {}

    /***************************************************************************
     * Change the owner and group of a file.
     *
     * `fi` will always be null if the file is not currenlty open, but
     * may also be null if the file is open.
     *
     * Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
     * expected to reset the setuid and setgid bits.
     */
    @(null)
    void chown(string path, uid_t uid, gid_t gid, ffi_t* fi) {}

    /***************************************************************************
     * Synchronize file contents.
     *
     * If the datasync parameter is non-zero, then only the user data
     * should be flushed, not the meta data.
     */
    @(null)
    void fsync(string path, int datasync, ffi_t* fi) {}

    /***************************************************************************
     * Synchronize directory contents.
     *
     * If the datasync parameter is non-zero, then only the user data
     * should be flushed, not the meta data.
     */
    @(null)
    void fsyncdir(string path, int sync, ffi_t* fi) {}

    /***************************************************************************
     * Change the access and modification times of a file with
     * nanosecond resolution.
     *
     * This supersedes the old utime() interface.  New applications
     * should use this.
     *
     * `fi` will always be null if the file is not currenlty open, but
     * may also be null if the file is open.
     *
     * See the utimensat(2) man page for details.
     */
    @(null)
    void utimens(string path, ref const(timespec)[2] tv, ffi_t* fi) {}

    /***************************************************************************
     * Perform BSD file locking operation.
     *
     * The op argument will be either LOCK_SH, LOCK_EX or LOCK_UN.
     *
     * Nonblocking requests will be indicated by ORing LOCK_NB to
     * the above operations.
     *
     * For more information see the flock(2) manual page.
     *
     * Additionally fi.owner will be set to a value unique to
     * this open file.  This same value will be supplied to
     * release() when the file is released.
     *
     * Note: if this method is not implemented, the kernel will still
     * allow file locking to work locally.  Hence it is only
     * interesting for network filesystems and similar.
     */
    @(null)
    void flock(string path, ffi_t* fi, int op) {}

    /***************************************************************************
     * Allocates space for an open file.
     *
     * This function ensures that required space is allocated for specified
     * file. If this function succeeds then any subsequent write
     * request to specified range is guaranteed not to fail because of lack
     * of space on the file system media.
     */
    @(null)
    void fallocate(string path, int mode, off_t off, off_t len, ffi_t* fi) {}

    /***************************************************************************
     * Get file system statistics.
     */
    @(null)
    statvfs_t statfs(string path) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Ioctl.
     *
     * flags will have FUSE_IOCTL_COMPAT set for 32bit ioctls in
     * 64bit environment. The size and direction of data is
     * determined by _IOC_*() decoding of cmd. For _IOC_NONE,
     * data will be null, for _IOC_WRITE data is out area, for
     * _IOC_READ in area and if both are set in/out area.
     * In all non-null cases, the area is of _IOC_SIZE(cmd) bytes.
     *
     * If flags has FUSE_IOCTL_DIR then the ffi_t refers to a
     * directory file handle.
     *
     * Note: the unsigned long request submitted by the application
     * is truncated to 32 bits.
     */
    @(null)
    void ioctl(string p, uint cmd, void* arg, ffi_t* fi, uint flags, void* data)
    do {}

    /***************************************************************************
     * Set extended attributes.
     */
    @(null)
    void setxattr(string path, string k, string v, int flags) {}

    /***************************************************************************
     * Get extended attributes.
     */
    @(null)
    string getxattr(string p, string k) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * List extended attributes.
     */
    @(null)
    string[] listxattr(string path) {
        throw new FuseException(ENOTSUP);
    }


    /***************************************************************************
     * Remove extended attributes.
     */
    @(null)
    void removexattr(string path, string key) {}

    /***************************************************************************
     * Perform POSIX file locking operation.
     *
     * The cmd argument will be either F_GETLK, F_SETLK or F_SETLKW.
     *
     * For the meaning of fields in 'struct flock' see the man page
     * for fcntl(2).  The l_whence field will always be set to
     * SEEK_SET.
     *
     * For checking lock ownership, the 'fi.owner' argument must be used.
     *
     * For F_GETLK operation, the library will first check currently
     * held locks, and if a conflicting lock is found it will return
     * information without calling this method.
     * This ensures, that for local locks the l_pid field is correctly
     * filled in. The results may not be accurate in case of race conditions
     * and in the presence of hard links, but it's unlikely that an
     * application would rely on accurate GETLK results in these
     * cases. If a conflicting lock is not found, this method will be
     * called, and the filesystem may fill out l_pid by a meaningful
     * value, or it may leave this field zero.
     *
     * For F_SETLK and F_SETLKW the l_pid field will be set to the pid
     * of the process performing the locking operation.
     *
     * Note: if this method is not implemented, the kernel will still
     * allow file locking to work locally.  Hence it is only
     * interesting for network filesystems and similar.
     */
    @(null)
    void lock(string path, ffi_t* fi, int cmd, flock_t* flock) {}

    /***************************************************************************
     * Map block index within file to block index within device.
     *
     * Note: This makes sense only for block device backed filesystems
     * mounted with the 'blkdev' option.
     */
    @(null)
    void bmap(string path, size_t blocksize, ulong* idx) {}

    /***************************************************************************
     * Poll for IO readiness events.
     *
     * Note: If ph is non-null, the client should notify
     * when IO readiness events occur by calling
     * fuse_notify_poll() with the specified ph.
     *
     * Regardless of the number of times poll with a non-null ph
     * is received, single notification is enough to clear all.
     * Notifying more times incurs overhead but doesn't harm
     * correctness.
     *
     * The callee is responsible for destroying ph with
     * $(LLW_REF fuse_pollhandle_destroy) when no longer in use.
     */
    @(null)
    void poll(string path, ffi_t* fi, fuse_pollhandle* ph, uint* reventsp) {}

    /***************************************************************************
     * Copy a range of data from one file to another.
     *
     * Performs an optimized copy between two file descriptors without the
     * additional cost of transferring data through the FUSE kernel module
     * to user space (glibc) and then back into the FUSE filesystem again.
     *
     * In case this method is not implemented, applications are expected to
     * fall back to a regular file copy.
     */
    @(null)
    ssize_t copy_file_range(
        string path_in,
        ffi_t* fi_in,
        off_t offset_in,
        string path_out,
        ffi_t* fi_out,
        off_t offset_out,
        size_t size,
        int flags
    ) {
        throw new FuseException(ENOTSUP);
    }

    /***************************************************************************
     * Called for FuseException containg errno.
     * It can be used for additional logging.
     */
    void handleFuseException(FuseException e) nothrow {}

    /***************************************************************************
     * Called for unknown exception cases (if errno is 0)
     * during the execution of any file operation.
     * The best thing to do is print the error.
     */
    void handleException(Exception e) nothrow {}
}


/*******************************************************************************
 * Mounts user file system to the designated location.
 * Template parameter `FS` is a file system class (real, not abstract).
 * Params:
 *     fsName   = File system name.
 *     fs       = Object of file system. For the file system must first be
 *                assigned a mount point with setMountPoint().
 *     fuseArgs = Options for FUSE (without FS name and without mount point),
 *                like -d, -f, -o autounmount, -o uid=UID, etc.
 *                The complete list is available by calling showFuseHelp().
 */
int mount(FS)(string fsName, FS fs, string mountPoint, string[] fuseArgs = null)
if (is(FS : FileSystem)) {
    string[] args = [fsName, mountPoint] ~ fuseArgs;
    auto argv = map!(a => a.toStringz)(args).array;
    int argc = cast(int)(argv.length);
    auto operations = getFileOperations!FS();
    return fuse_main(argc, cast(char**)argv.ptr, &operations, &fs);
}


/*******************************************************************************
 * Prints help for FUSE.
 */
void showFuseHelp() {
    string[] args = ["", ".", "--help"];
    auto argv = map!(a => a.toStringz)(args).array;
    int argc = cast(int)(argv.length);
    int ret = fuse_main(argc, cast(char**)argv.ptr, null, null);
    assert(ret == 0);
}


/*******************************************************************************
 * Exception to pass errno.
 * This exception is the main way to convey error information
 * in any file operation.
 */
class FuseException : Exception {
    int err;

    this(
        int err,
        string message = "",
        string file = __FILE__,
        size_t line = __LINE__
    ) {
        super(message, file, line);
        this.err = err;
        errno = this.err;
    }
}


/**
 * Throws FuseException if value is equal to true.
 * Params:
 *     value = Data for comparison with true.
 *     code = errno value for FuseException
 *     message = Optional additional information.
 * Returns:
 *     Received value.
 */
pragma(inline, true)
T fe(T)(T value, int code, string message = "") {
    if (!value) {
        throw new FuseException(code, message);
    }
    return value;
}


/// Throws FuseException with the specified code and message.
pragma(inline, true)
void fe(int code, string message = "") {
    throw new FuseException(code, message);
}


private:


void attach() nothrow {
    // collectException(thread_attachThis());
    import core.memory;
    GC.disable();
}


immutable errFmt = "\nException: %s:%s '%s'. %s";

extern(C) {
    void* call_init(fuse_conn_info* conn, fuse_config* cfg)
    nothrow {
        attach();
        auto o = cast(FileSystem*) fuse_get_context().private_data;
        try {
            o.initialize(conn, cfg);
        } catch (FuseException e) {
            string errnoMsg = (e.err != 0) ? getInfoAboutError(e.err) : "?";
            string msg;
            try {
                msg = format!errFmt(e.file, e.line, errnoMsg, e.msg);
            } catch (Exception) {}
            assert(false, msg);
        } catch (Exception e) {
            string msg;
            try {
                msg = format!"\nError: %s:%s > '%s'"(e.file, e.line, e.msg);
            } catch (Exception) {}
            assert(false, msg);
        }
        return o;
    }

    int call_getattr(const char* path, stat_t* st, ffi_t* fi)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                string strPath = path.fromStringz.idup;
                stat_t statInfo = o.getattr(strPath, fi);
                *st = statInfo;
                return 0;
            }
        )();
    }

    int call_mkdir(const char* path, mode_t mode)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.mkdir(path.fromStringz.idup, mode);
                return 0;
            }
        )();
    }

    int call_mknod(const char* path, mode_t mode, dev_t dev)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.mknod(path.fromStringz.idup, mode, dev);
                return 0;
            }
        )();
    }

    int call_create(const char* path, mode_t mode, ffi_t* fi)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.create(path.fromStringz.idup, mode, fi);
                return 0;
            }
        )();
    }

    int call_symlink(const char* src, const char* dst)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.symlink(src.fromStringz.idup, dst.fromStringz.idup);
                return 0;
            }
        )();
    }

    int call_readlink(const char* path, char* buff, size_t size)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                if (size == 0) {
                    return 0;
                }
                import core.stdc.string : strncpy;
                string destination = o.readlink(path.fromStringz.idup);
                strncpy(buff, destination.toStringz, size);
                return 0;
            }
        )();
    }

    int call_link(const char* src, const char* dst)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.link(src.fromStringz.idup, dst.fromStringz.idup);
                return 0;
            }
        )();
    }

    int call_access(const char* path, int mode)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                if (o.access(path.fromStringz.idup, mode)) {
                    return 0;
                }
                return -errno;
            }
        )();
    }

    int call_rename(const char* src, const char* dst, uint flags)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.rename(src.fromStringz.idup, dst.fromStringz.idup, flags);
                return 0;
            }
        )();
    }

    int call_rmdir(const char* path)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.rmdir(path.fromStringz.idup);
                return 0;
            }
        )();
    }

    int call_unlink(const char* path)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.unlink(path.fromStringz.idup);
                return 0;
            }
        )();
    }

    int call_open(const char* path, ffi_t* fi)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.open(path.fromStringz.idup, fi);
                return 0;
            }
        )();
    }

    int call_write(
        const char* path, const ubyte* data, size_t size, off_t off, ffi_t* fi
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                return o.write(path.fromStringz.idup, data, size, off, fi);
            }
        )();
    }

    int call_read(
        const char* path, ubyte* buff, size_t size, off_t off, ffi_t* fi
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                return o.read(path.fromStringz.idup, buff, size, off, fi);
            }
        )();
    }

    off_t call_lseek(const char* path, off_t off, int whence, ffi_t* fi)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                return o.lseek(path.fromStringz.idup, off, whence, fi);
            }
        )();
    }

    int call_truncate(const char* path, off_t off, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.truncate(path.fromStringz.idup, off, fi);
                return 0;
            }
        )();
    }

    int call_flush(const char* path, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.flush(path.fromStringz.idup, fi);
                return 0;
            }
        )();
    }

    int call_release(const char* path, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.release(path.fromStringz.idup, fi);
                return 0;
            }
        )();
    }

    int call_opendir(const char* path, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.opendir(path.fromStringz.idup, fi);
                return 0;
            }
        )();
    }

    int call_readdir(
        const char* path,
        void* buf,
        fuse_fill_dir_t filler,
        off_t offset,
        ffi_t* fi,
        fuse_readdir_flags flags
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                string[] files = o.readdir(path.fromStringz.idup, fi);
                foreach(file; files) {
                    filler(buf, cast(char*)file.toStringz, null, 0, 0);
                }
                return 0;
            }
        )();
    }

    int call_releasedir(const char* path, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.releasedir(path.fromStringz.idup, fi);
                return 0;
            }
        )();
    }

    int call_chmod(const char* path, mode_t mode, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.chmod(path.fromStringz.idup, mode, fi);
                return 0;
            }
        )();
    }

    int call_chown(const char* path, uid_t uid, gid_t gid, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.chown(path.fromStringz.idup, uid, gid, fi);
                return 0;
            }
        )();
    }

    int call_fsync(const char* path, int sync, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.fsync(path.fromStringz.idup, sync, fi);
                return 0;
            }
        )();
    }

    int call_fsyncdir(const char* path, int sync, ffi_t* fi) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.fsyncdir(path.fromStringz.idup, sync, fi);
                return 0;
            }
        )();
    }

    int call_utimens(const char* path, ref const(timespec)[2] tv, ffi_t* fi)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.utimens(path.fromStringz.idup, tv, fi);
                return 0;
            }
        )();
    }

    int call_flock(const char* path, ffi_t* fi, int op) nothrow {
        return callOperation!(
            (FileSystem o) {
                o.flock(path.fromStringz.idup, fi, op);
                return 0;
            }
        )();
    }

    int call_fallocate(
        const char* path, int mode, off_t off, off_t len, ffi_t* fi
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.fallocate(path.fromStringz.idup, mode, off, len, fi);
                return 0;
            }
        )();
    }

    int call_statfs(const char* path, statvfs_t* statvfs)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                *statvfs = o.statfs(path.fromStringz.idup);
                return 0;
            }
        )();
    }

    int call_ioctl(
        const char* path, uint cmd, void* arg, ffi_t* fi, uint flags, void* data
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.ioctl(path.fromStringz.idup, cmd, arg, fi, flags, data);
                return 0;
            }
        )();
    }

    int call_setxattr(
        const char* path, const char* k, const char* v, size_t vsize, int flags
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                string key = k.fromStringz.idup;
                string value = v[0 .. vsize].idup;
                o.setxattr(path.fromStringz.idup, key, value, flags);
                return 0;
            }
        )();
    }

    int call_getxattr(const char* p, const char* k, char* buff, size_t size)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                string path = p.fromStringz.idup;
                string key = k.fromStringz.idup;
                string value = o.getxattr(path, key);
                fe(value.length + 1 <= size, ERANGE);  // +1 for final '\0'
                memset(buff, '\0', size);
                strncpy(buff, value.ptr, size-1);
                return 0;
            }
        )();
    }

    int call_listxattr(const char* path, char* list, size_t size)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                string p = path.fromStringz.idup;
                string[] attrs = o.listxattr(p);
                size_t pos = 0;
                foreach(attr; attrs) {
                    char[] chars = attr.dup ~ '\0';
                    foreach(ch; chars) {
                        if (size == pos) {
                            return 0;
                        }
                        list[pos] = ch;
                        pos++;
                    }
                }
                return 0;
            }
        )();
    }

    int call_removexattr(const char* path, const char* key)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                string p = path.fromStringz.idup;
                string k = key.fromStringz.idup;
                o.removexattr(p, k);
                return 0;
            }
        )();
    }

    int call_lock(const char* path, ffi_t* fi, int cmd, flock_t* flock)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.lock(path.fromStringz.idup, fi, cmd, flock);
                return 0;
            }
        )();
    }

    int call_bmap(const char* path, size_t blocksize, ulong* idx)
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.bmap(path.fromStringz.idup, blocksize, idx);
                return 0;
            }
        )();
    }

    int call_poll(
        const char* path, ffi_t* fi, fuse_pollhandle* ph, uint* reventsp
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                o.poll(path.fromStringz.idup, fi, ph, reventsp);
                return 0;
            }
        )();
    }

    ssize_t call_copy_file_range(
        const char* path_in,
        ffi_t* fi_in,
        off_t offset_in,
        const char* path_out,
        ffi_t* fi_out,
        off_t offset_out,
        size_t size,
        int flags
    )
    nothrow {
        return callOperation!(
            (FileSystem o) {
                return o.copy_file_range(
                    path_in.fromStringz.idup,
                    fi_in,
                    offset_in,
                    path_out.fromStringz.idup,
                    fi_out,
                    offset_out,
                    size,
                    flags
                );
            }
        )();
    }

    void call_destroy(void* private_data)
    nothrow {
        attach();
        auto o = cast(FileSystem*)fuse_get_context().private_data;
        try {
            o.destroy();
        } catch (Exception e) {
            string fmt = "Error while terminating file system: %s\n";
            printf(fmt.toStringz, e.msg.toStringz);
        }
    }

}


void assignOperationPointer(FS, alias opname)(ref fuse_operations operations) {
    enum fnUDA = __traits(getAttributes, mixin("FS." ~ opname));
    static if (fnUDA.length != 1) {
        mixin("operations." ~ opname) = mixin("&call_" ~ opname);
    } else static if (fnUDA[0] == null) {
        mixin("operations." ~ opname) = null;
    }
}


static immutable opNames = [  // 38 functions
    "getattr", "mkdir", "mknod", "create", "symlink", "readlink", "link",
    "access", "rename", "rmdir", "unlink",
    "open", "write", "read", "lseek", "truncate", "flush", "release",
    "opendir", "readdir", "releasedir",
    "chmod", "chown", "fsync", "fsyncdir", "utimens", "flock", "fallocate",
    "statfs",
    "ioctl",
    "setxattr", "getxattr", "listxattr", "removexattr",
    "lock", "bmap", "poll", "copy_file_range"
];


fuse_operations getFileOperations(FS)() if (is(FS : FileSystem)) {
    fuse_operations operations;
    operations.init = &call_init;
    operations.destroy = &call_destroy;
    static foreach(name; opNames) {
        assignOperationPointer!(FS, name)(operations);
    }
    operations.write_buf = null;  // not used in oxfuse
    operations.read_buf = null;  // not used in oxfuse
    return operations;
}


auto callOperation(alias fn)() nothrow {
    attach();
    auto o = cast(FileSystem*)fuse_get_context().private_data;
    try {
        return fn(*o);
    } catch (FuseException fuseExc) {
        o.handleFuseException(fuseExc);
        return -fuseExc.err;
    } catch (Exception e) {
        if (errno != 0) {
            return -errno;
        }
        o.handleException(e);
        return -EIO;
    }
}


/*******************************************************************************
 * Returns string containing text view of an errno value.
 * (Copied from amalthea.libcore, see:
 * $(EXT_LINK https://gitlab.com/os-18/amalthea)
 */
string getInfoAboutError(int err) nothrow {
    import core.stdc.string : strerror;
    import std.string : fromStringz;
    return strerror(err).fromStringz.idup;
}

