/* vi:ai:tabstop=8:shiftwidth=4:softtabstop=4:expandtab
 *
 * Author: Gabriel Burca (gburca dash fuse at ebixio dot com)
 * Version: 1.2
 * Latest version: http://ebixio.com/rofs-filtered/old/rofs-filtered.c
 *
 * This FUSE file system allows the user to mount a directory read-only and filter
 * the files shown in the read-only directory based on regular expressions found in
 * the optional /etc/rofs-filtered.rc configuration file.
 *
 * What's the use of such a file system? Say you have a ton of *.flac music
 * files, along with the transcoded *.mp3 files in the same directory
 * structure. You'll want to show only one of the formats to music players that
 * can play both flac and mp3 so that the songs don't show up twice. You might
 * also want to show only mp3 files to players that don't understand the flac
 * format.
 * 
 * Based on:
 * ROFS - The read-only filesystem for FUSE.
 * 
 * On Ubuntu/Debian install: libfuse2, libfuse-dev, fuse-utils
 * Version 2.5 or later of FUSE is required. If needed, it can be obtained from
 * debuntu.org by adding the following line to /etc/apt/sources.list:
 *      deb http://repository.debuntu.org/ dapper multiverse
 *
 * Compile using: gcc -o rofs-filtered -Wall -ansi -W -std=c99 -g -ggdb -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -lfuse rofs-filtered.c
 *
 * Mount by adding the following line to /etc/fstab:
 * /full/path/to/rofs-filtered#/the/read/write/device /the/read/only/mount/point fuse defaults,allow_other 0 0
 *
 * Unmount: fusermount -u /the/read/only/mount/point
 *  OR
 * Unmount: umount /the/read/only/mount/point
 *
 * The user might need to be in the "fuse" UNIX group.
 * 
 *********************************************************************************
 * Copyright (C) 2006-2007  Gabriel Burca (gburca dash fuse at ebixio dot com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *********************************************************************************
 */

// We depend on version 2.5 of FUSE because it provides an "access" callback.
// Some applications would call "access" and figure out a file is writable
// (which was the default behavior of "access" prior to 2.5), then attempt to
// open the file "rw", fail, and bomb out because of the conflicting info.
#define FUSE_USE_VERSION 25

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/xattr.h>
#include <dirent.h>
#include <unistd.h>
#include <regex.h>
#include <syslog.h>
#include <stdarg.h>
#include <fuse.h>


// Some hard-coded values:
static const char *EXEC_NAME = "rofs-filtered";
static const char config_file[] = "/etc/rofs-filtered.rc";
static const int log_facility = LOG_DAEMON;

// Global to store our read-write path
char *rw_path;
regex_t **patterns = NULL;
int pattern_count = 0;


/** Translate an rofs path into it's underlying filesystem path */
static char* translate_path(const char* path)
{

    char *rPath= malloc(sizeof(char)*(strlen(path)+strlen(rw_path)+1));

    if (!rPath) return NULL;

    strcpy(rPath,rw_path);
    if (rPath[strlen(rPath)-1]=='/') {
        rPath[strlen(rPath)-1]='\0';
    }
    strcat(rPath,path);

    return rPath;
}

/** Log a message to syslog */
static void log_msg(const int level, const char *format, ... /*args*/) {
    va_list ap;
    va_start(ap, format);

    vsyslog(log_facility | level, format, ap);
    va_end(ap);
}

/** Report user-friendly regex errors */
static void log_regex_error(int error, regex_t *regex, const char* pattern) {
    size_t msg_len;
    char *err_msg;

    msg_len = regerror(error, regex, NULL, 0);
    err_msg = (char *)malloc(msg_len);

    if (err_msg) {
        regerror(error, regex, err_msg, msg_len);
        log_msg(LOG_ERR, "RegEx error: \"%s\" while parsing pattern: \"%s\"",
                err_msg, pattern);
        //printf("Error: %s %s\n", err_msg, pattern);
        free(err_msg);
    }

    regfree(regex);
}

/** Read the RegEx configuration file */
static int read_config(const char *conf_file) {
#define MAX_LINE 1024
    regex_t *regex, *ignore_pattern;
    char line[MAX_LINE];
    int regcomp_res, pcount = 0;
    char *eol = NULL;

    FILE *fh = fopen(conf_file, "r");
    if (fh == NULL) {
        log_msg(LOG_ERR, "Failed to open config file: %s", conf_file);
        return -1;
    }

    ignore_pattern = (regex_t *)malloc(sizeof(regex_t));
    if (! ignore_pattern) {
        log_msg(LOG_ERR, "Out of memory!");
        return -1;
    }
    regcomp_res = regcomp(ignore_pattern, "^#|^\\s*$",
                    REG_EXTENDED | REG_NOSUB);
    if (regcomp_res) {
        log_msg(LOG_ERR, "Failed compiling config parser regex.");
        return -1;
    }

    while ( fgets(line, MAX_LINE, fh) ) {
        if (! regexec(ignore_pattern, line, 0, NULL, 0)) continue;

        regex = (regex_t *)malloc(sizeof(regex_t));
        if (! regex) {
            log_msg(LOG_ERR, "Out of memory!");
            return -1;
        }

        // Remove the \n EOL
        eol = index(line, '\n');
        if (eol) *eol = '\0';
        regcomp_res = regcomp(regex, line, REG_EXTENDED | REG_NOSUB);
        if ( regcomp_res ) {
            log_regex_error(regcomp_res, regex, line);
        } else {
            // Add regex to the stash 
            pcount++;
            patterns = realloc(patterns, sizeof(regex_t *) * pcount);
            patterns[pcount - 1] = regex;
        }
    }

    pattern_count = pcount;

    regfree(ignore_pattern);
    fclose(fh);
    return 0;
}

/** If the file name matches one of the RegEx patterns, hide it. */
static int should_hide(const char *name) {
    int res;
    for (int i = 0; i < pattern_count; i++) {
        res = regexec(patterns[i], name, 0, NULL, 0);
        if (res == 0) {
            // We have a match.
            return 1;
        }
    }
    return 0;
}


/******************************
 *   
 * Callbacks for FUSE
 * 
 ******************************/

static int callback_getattr(const char *path, struct stat *st_data) {
    if (should_hide(path)) return -ENOENT;

    int res;
    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }
    res = lstat(trpath, st_data);
    free(trpath);
    if(res == -1) {
        return -errno;
    }
    // Remove write permissions = chmod a-w
    st_data->st_mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
    return 0;
}

static int callback_readlink(const char *path, char *buf, size_t size) {
    if (should_hide(path)) return -ENOENT;

    int res;
    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }
    res = readlink(trpath, buf, size - 1);
    free(trpath);
    if(res == -1) {
        return -errno;
    }
    buf[res] = '\0';
    return 0;
}

static int callback_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                            off_t offset, struct fuse_file_info *fi)
{
    if (should_hide(path)) return -ENOENT;

    DIR *dp;
    struct dirent *de;

    (void) offset;
    (void) fi;

    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }

    dp = opendir(trpath);
    free(trpath);
    if(dp == NULL) {
        return -errno;
    }

    while((de = readdir(dp)) != NULL) {
        if (should_hide(de->d_name)) {
            // hide some files and directories
        } else {
            struct stat st;
            memset(&st, 0, sizeof(st));
            st.st_ino = de->d_ino;
            st.st_mode = de->d_type << 12;
            if (filler(buf, de->d_name, &st, 0))
                break;
        }
    }

    closedir(dp);
    return 0;
}

static int callback_mknod(const char *path, mode_t mode, dev_t rdev) {
    (void)path;
    (void)mode;
    (void)rdev;
    return -EPERM;
}

static int callback_mkdir(const char *path, mode_t mode) {
    (void)path;
    (void)mode;
    return -EPERM;
}

static int callback_unlink(const char *path) {
    (void)path;
    return -EPERM;
}

static int callback_rmdir(const char *path) {
    (void)path;
    return -EPERM;
}

static int callback_symlink(const char *from, const char *to)
{
    (void)from;
    (void)to;
    return -EPERM;
}

static int callback_rename(const char *from, const char *to) {
    if (should_hide(from)) return -ENOENT;

    (void)from;
    (void)to;
    return -EPERM;
}

static int callback_link(const char *from, const char *to) {
    if (should_hide(from)) return -ENOENT;

    (void)from;
    (void)to;
    return -EPERM;
}

static int callback_chmod(const char *path, mode_t mode) {
    if (should_hide(path)) return -ENOENT;

    (void)path;
    (void)mode;
    return -EPERM;
}

static int callback_chown(const char *path, uid_t uid, gid_t gid) {
    if (should_hide(path)) return -ENOENT;

    (void)path;
    (void)uid;
    (void)gid;
    return -EPERM;
}

static int callback_truncate(const char *path, off_t size) {
    if (should_hide(path)) return -ENOENT;

    (void)path;
    (void)size;
    return -EPERM;
}

static int callback_utime(const char *path, struct utimbuf *buf)
{
    (void)path;
    (void)buf;
    return -EPERM;
}

/** This function should just check if the operation is permitted for the given
 * flags. FUSE will provide it's own file descriptor to the calling
 * application.
 */
static int callback_open(const char *path, struct fuse_file_info *finfo) {
    if (should_hide(path)) return -ENOENT;

    int res;

    /* We allow opens, unless they're tring to write, sneaky
     * people.
     */
    int flags = finfo->flags;

    if ((flags & O_WRONLY) || (flags & O_RDWR) || (flags & O_CREAT) || (flags & O_EXCL) || (flags & O_TRUNC)) {
        return -EPERM;
    }

    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }

    res = open(trpath, flags);

    free(trpath);
    if(res == -1) {
        return -errno;
    }
    close(res);
    return 0;
}

static int callback_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *finfo) {
    if (should_hide(path)) return -ENOENT;

    int fd;
    int res;
    (void)finfo;

    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }

    errno = 0;
    fd = open(trpath, O_RDONLY);
    free(trpath);
    if (fd == -1) return -errno;

    errno = 0;
    res = pread(fd, buf, size, offset);

    if (res == -1) {
        res = -errno;
    }
    close(fd);
    return res;
}

static int callback_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *finfo) {
    if (should_hide(path)) return -ENOENT;

    (void)path;
    (void)buf;
    (void)size;
    (void)offset;
    (void)finfo;
    return -EPERM;
}

static int callback_statfs(const char *path, struct statvfs *st_buf) {
    if (should_hide(path)) return -ENOENT;

    int res;
    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }

    res = statvfs(trpath, st_buf);
    free(trpath);
    if (res == -1) {
        return -errno;
    }
    return 0;
}

static int callback_release(const char *path, struct fuse_file_info *finfo) {
    (void) path;
    (void) finfo;
    return 0;
}

static int callback_fsync(const char *path, int crap, struct fuse_file_info *finfo) {
    (void) path;
    (void) crap;
    (void) finfo;
    return 0;
}

static int callback_access(const char *path, int mode) {
    if (should_hide(path)) return -ENOENT;

    if (mode & W_OK) return -1; // We are ReadOnly

    int res;
    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }

    errno = 0;
    res = access(trpath, mode);
    free(trpath);
    if (res == -1 && errno != 0) {
        return -errno;
    }
    return res;
}

/**
 * Set the value of an extended attribute
 */
static int callback_setxattr(const char *path, const char *name, const char *value, size_t size, int flags) {
    if (should_hide(path)) return -ENOENT;

    (void)path;
    (void)name;
    (void)value;
    (void)size;
    (void)flags;
    return -EPERM;
}

/**
 * Get the value of an extended attribute.
 */
static int callback_getxattr(const char *path, const char *name, char *value, size_t size) {
    if (should_hide(path)) return -ENOENT;

    int res;

    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }
    res = lgetxattr(trpath, name, value, size);
    free(trpath);
    if(res == -1) {
        return -errno;
    }
    return res;
}

/**
 * List the supported extended attributes.
 */
static int callback_listxattr(const char *path, char *list, size_t size) {
    if (should_hide(path)) return -ENOENT;

    int res;

    char *trpath=translate_path(path);

    if (!trpath) {
        errno = ENOMEM;
        return -errno;
    }

    res = llistxattr(trpath, list, size);
    free(trpath);
    if(res == -1) {
        return -errno;
    }
    return res;

}

/**
 * Remove an extended attribute.
 */
static int callback_removexattr(const char *path, const char *name) {
    if (should_hide(path)) return -ENOENT;

    (void)path;
    (void)name;
    return -EPERM;

}

struct fuse_operations callback_oper = {
    .getattr        = callback_getattr,
    .readlink       = callback_readlink,
    .readdir        = callback_readdir,
    .mknod      = callback_mknod,
    .mkdir      = callback_mkdir,
    .symlink    = callback_symlink,
    .unlink     = callback_unlink,
    .rmdir      = callback_rmdir,
    .rename     = callback_rename,
    .link       = callback_link,
    .chmod      = callback_chmod,
    .chown      = callback_chown,
    .truncate   = callback_truncate,
    .utime      = callback_utime,
    .open           = callback_open,
    .read           = callback_read,
    .write      = callback_write,
    .statfs         = callback_statfs,
    .release    = callback_release,
    .fsync      = callback_fsync,
    .access     = callback_access,

    /* Extended attributes support for userland interaction */
    .setxattr   = callback_setxattr,
    .getxattr       = callback_getxattr,
    .listxattr      = callback_listxattr,
    .removexattr= callback_removexattr
};


int main(int argc, char *argv[])
{
    openlog(EXEC_NAME, LOG_PID, log_facility);
    log_msg(LOG_INFO, "Starting up...");
    for (int i = 0; i < argc; i++) log_msg(LOG_DEBUG, "    arg %i = %s", i, argv[i]);

    if (argc < 3) {
        fprintf(stderr, "Usage: rofs-filtered <RW-Path> <Filtered-Path> [FUSE options]\n");
        log_msg(LOG_ERR, "Not enough arguments. argc = %i", argc);
        exit(1);
    }

    if (access(argv[1], F_OK)) {
        log_msg(LOG_ERR, "The following directory does not exist: %s", argv[1]);
        exit(2);
    }
    if (access(argv[2], F_OK)) {
        log_msg(LOG_ERR, "The following directory does not exist: %s", argv[2]);
        exit(3);
    }

    // We save away the first argument (the RW-Path)
    int len = strlen(argv[1]) + 1;
    rw_path = malloc(len);
    if (rw_path) {
        strncpy(rw_path, argv[1], len);
    } else {
        exit(4);
    }

    // Shift all arguments up by one (overwriting the first argument) because
    // fuse_main doesn't accept the mount source, only the mountpoint.
    for (int i = 2; i < argc; i++) {
        argv[i - 1] = argv[i];
    }
    argc--;

    if (read_config(config_file)) {
        log_msg(LOG_ERR, "Error parsing config file: %s", config_file);
    }

    // Hand off control to FUSE
    fuse_main(argc, argv, &callback_oper);
    return 0;
}