/*
 * Soapbox - A way to deny processes to write files outside some directories
 *
 * Copyright (C) 2003 by Dag Wieers <dag@wieers.com>
 * Copyright (C) 2012 by Ralf S. Engelschall <rse@engelschall.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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <utime.h>
#include <fcntl.h>
#include <limits.h>
#include <time.h>
#ifdef __FreeBSD__
#include <libgen.h>
#endif

/*
 *  ==== GLOBAL VARIABLES ====
 */

static int (*sb_real_chmod)   (const char *, mode_t);
static int (*sb_real_chown)   (const char *, uid_t, gid_t);
static int (*sb_real_lchown)  (const char *, uid_t, gid_t);
static int (*sb_real_link)    (const char *, const char *);
static int (*sb_real_mkdir)   (const char *, mode_t);
static int (*sb_real_mkfifo)  (const char *, mode_t);
static int (*sb_real_mknod)   (const char *, mode_t, dev_t);
static int (*sb_real___xmknod)(int, const char *, mode_t, dev_t *);
static int (*sb_real_open)    (const char *, int, ...);
static int (*sb_real_open64)  (const char *, int, ...);
static int (*sb_real_creat)   (const char *, mode_t);
static int (*sb_real_creat64) (const char *, mode_t);
static int (*sb_real_remove)  (const char *);
static int (*sb_real_rename)  (const char *, const char *);
static int (*sb_real_rmdir)   (const char *);
static int (*sb_real_symlink) (const char *, const char *);
static int (*sb_real_unlink)  (const char *);
static int (*sb_real_utime)   (const char *, const struct utimbuf *);
static int (*sb_real_utimes)  (const char *, const struct timeval *);

#define SB_PATHSEP ":,;|"

enum { SB_R_FILE, SB_R_LINK };

enum { SB_A_UNKN, SB_A_WARN, SB_A_ERR, SB_A_HALT };
#define SB_A_DEFAULT SB_A_WARN
#define SB_A_DEFAULT_NAME "warn"

static char *sb_path;
static int   sb_devnull;
static int   sb_action = SB_A_UNKN;
static int   sb_debug = 0;
static FILE *sb_logfp;

/*
 *  ==== UTILITY FUNCTIONS ====
 */

static void sb_die(FILE *out, const char *str, ...) {
    va_list argptr;

    fprintf(out, "soapbox: ");
    va_start(argptr, str);
    vfprintf(out, str, argptr);
    va_end(argptr);
    fprintf(out, "\n");
    exit(-1);
}

static void sb_log(int level, const char *str, ...) {
    va_list argptr;

    if (sb_debug & level || level == 0) {
        if (level != 0)
            fprintf(sb_logfp, "soapbox: debug%i: ", level);
        else
            fprintf(sb_logfp, "soapbox: ");
        va_start(argptr, str);
        vfprintf(sb_logfp, str, argptr);
        va_end(argptr);
        fprintf(sb_logfp, "\n");
    }
}

static void sb_dlcheck(const char *err) {
    if (err != NULL)
        sb_die(sb_logfp, "%s", err);
}

/*
 *  ==== DSO MANAGEMENT ====
 */

/*  DSO initialization  */
void _init(int argc, char *argv[]) {
    char *soapboxaction;
    char *soapboxdbg;
    char *soapboxlog;

    /*  make output unbuffered  */
    setvbuf(stdout, (char *)NULL, _IONBF, 0);
    setvbuf(stderr, (char *)NULL, _IONBF, 0);

    /*  handle and unset logging environment variable  */
    soapboxlog = getenv("SOAPBOXLOG");
    unsetenv("SOAPBOXLOG");
    if (soapboxlog == NULL || *soapboxlog == '\0') {
        sb_logfp = stderr;
    }
    else {
        sb_logfp = fopen(soapboxlog, "a");
        if (sb_logfp == NULL) {
            sb_logfp = stderr;
            sb_die(sb_logfp, "%s: %s (%i)", soapboxlog, strerror(errno), errno);
        }
        setvbuf(sb_logfp, (char *)NULL, _IONBF, 0);
    }

    /*  handle and unset debugging environment variable  */
    soapboxdbg = getenv("SOAPBOXDEBUG");
    unsetenv("SOAPBOXDEBUG");
    if (soapboxdbg != NULL)
        sb_debug = atoi(soapboxdbg);
    sb_log(8, "Variable SOAPBOXDEBUG is set to %i.", sb_debug);

    /*  handle and unset path environment variable  */
    sb_path = getenv("SOAPBOXPATH");
    unsetenv("SOAPBOXPATH");
    if (sb_path == NULL)
        sb_path = "";
    if (sb_path == NULL || *sb_path=='\0')
        sb_log(8, "Variable SOAPBOXPATH is not set. Not allowed to write anywhere.");
    else
        sb_log(8, "Variable SOAPBOXPATH is set to \"%s\".", sb_path);

    /*  handle and unset action environment variable  */
    soapboxaction = getenv("SOAPBOXACTION");
    unsetenv("SOAPBOXACTION");
    if (soapboxaction == NULL || *soapboxaction == '\0') {
        soapboxaction = SB_A_DEFAULT_NAME;
        sb_log(8, "Variable SOAPBOXACTION is not set. Using \"%s\" by default.", SB_A_DEFAULT_NAME);
    }
    else
        sb_log(8, "Variable SOAPBOXACTION is set to \"%s\".", soapboxaction);
    if (strcmp(soapboxaction, "warn") == 0)
        sb_action = SB_A_WARN;
    else if (strcmp(soapboxaction, "err") == 0)
        sb_action = SB_A_ERR;
    else if (strcmp(soapboxaction, "halt") == 0)
        sb_action = SB_A_HALT;
    if (sb_action == SB_A_UNKN) {
        sb_log(8, "Variable SOAPBOXACTION=\"%s\" is unknown. Using \"%s\" by default.", soapboxaction, SB_A_DEFAULT_NAME);
        sb_action = SB_A_DEFAULT;
    }

    /*  resolve real functions from libc  */
    sb_real_chmod    = dlsym(RTLD_NEXT, "chmod");    sb_dlcheck(dlerror());
    sb_real_chown    = dlsym(RTLD_NEXT, "chown");    sb_dlcheck(dlerror());
    sb_real_lchown   = dlsym(RTLD_NEXT, "lchown");   sb_dlcheck(dlerror());
    sb_real_link     = dlsym(RTLD_NEXT, "link");     sb_dlcheck(dlerror());
    sb_real_mkdir    = dlsym(RTLD_NEXT, "mkdir");    sb_dlcheck(dlerror());
    sb_real_mkfifo   = dlsym(RTLD_NEXT, "mkfifo");   sb_dlcheck(dlerror());
    sb_real_mknod    = dlsym(RTLD_NEXT, "mknod");    sb_dlcheck(dlerror());
#ifdef __linux__
    sb_real___xmknod = dlsym(RTLD_NEXT, "__xmknod"); sb_dlcheck(dlerror());
#endif
    sb_real_open     = dlsym(RTLD_NEXT, "open");     sb_dlcheck(dlerror());
#ifdef __linux__
    sb_real_open64   = dlsym(RTLD_NEXT, "open64");   sb_dlcheck(dlerror());
#endif
    sb_real_creat    = dlsym(RTLD_NEXT, "creat");    sb_dlcheck(dlerror());
#ifdef __linux__
    sb_real_creat64  = dlsym(RTLD_NEXT, "creat64");  sb_dlcheck(dlerror());
#endif
    sb_real_remove   = dlsym(RTLD_NEXT, "remove");   sb_dlcheck(dlerror());
    sb_real_rename   = dlsym(RTLD_NEXT, "rename");   sb_dlcheck(dlerror());
    sb_real_rmdir    = dlsym(RTLD_NEXT, "rmdir");    sb_dlcheck(dlerror());
    sb_real_symlink  = dlsym(RTLD_NEXT, "symlink");  sb_dlcheck(dlerror());
    sb_real_unlink   = dlsym(RTLD_NEXT, "unlink");   sb_dlcheck(dlerror());
    sb_real_utime    = dlsym(RTLD_NEXT, "utime");    sb_dlcheck(dlerror());
    sb_real_utimes   = dlsym(RTLD_NEXT, "utimes");   sb_dlcheck(dlerror());

    /*  open /dev/null for reuse  */
    sb_devnull = sb_real_open("/dev/null", O_RDWR);
    if (sb_devnull == -1)
        sb_die(sb_logfp, "/dev/null: %s", strerror(errno));
}

/*  DSO finish  */
void _fini(void) {
    close(sb_devnull);
    if (sb_logfp != stderr)
        fclose(sb_logfp);
}

/*
 *  ==== HELPER FUNCTIONS ====
 */

static char *sb_dirname(const char *path) {
    char *ptr;
    char safe[PATH_MAX+1];
    safe[0]='\0'; safe[PATH_MAX]='\0';

    if (strrchr(path, '/') == NULL)
        getcwd(safe, PATH_MAX);
    else {
        snprintf(safe, PATH_MAX, "%s", path);
        ptr = strrchr(safe, '/');
        *ptr = '\0';
    }
    return strndup(safe, strlen(safe));
}

/*  sb_rewrite relative path to absolute.  */
static char *sb_rewrite(const char *path, const int flag) {
    char *linkdir, *out;
    struct stat *buf;
    char safe[PATH_MAX+1];

    /*  terminate string for safety  */
    safe[0] = '\0';
    safe[PATH_MAX] = '\0';

    /*  to make sure path is not empty and defined. Return empty string  */
    if (path == NULL || *path == '\0')
        return strndup(safe, 0);

    /*  check if file exists  */
    buf = malloc(sizeof(struct stat));
    if (lstat(path, buf) == 0) {
        /*  case 1: file exists  */
        int type = (buf->st_mode & 0170000);
        int mode = (buf->st_mode & 07777);
        if (sb_debug & 8) {
            switch (type) {
                case S_IFLNK: sb_log(8, "File \"%s\" is a symlink. (%04o)",            path, mode); break;;
                case S_IFREG: sb_log(8, "File \"%s\" is a regular file. (%04o)",       path, mode); break;;
                case S_IFDIR: sb_log(8, "File \"%s\" is a directory. (%04o)",          path, mode); break;;
                case S_IFCHR: sb_log(8, "File \"%s\" is a character device. (%04o)",   path, mode); break;;
                case S_IFBLK: sb_log(8, "File \"%s\" is a block device. (%04o)",       path, mode); break;;
                default:      sb_log(8, "File \"%s\" is an unknown file type. (%04o)", path, mode); break;;
            }
        }

        /*  resolve path  */
        if ((type == S_IFLNK) && (flag == SB_R_LINK)) {
            /*  if it is a symlink and if asked (SB_R_LINK), we should make its
                dirname absolute (not the symlink) and add its basename  */
            linkdir = sb_rewrite(sb_dirname(path), SB_R_LINK);
            snprintf(safe, PATH_MAX, "%s/%s", linkdir, basename(path));
            free(linkdir);
        }
        else
            realpath(path, safe);
    }
    else {
        /*  case 2: file not existing  */
        sb_log(8, "File \"%s\" does not exist.", path);
        realpath(path, safe); /* TODO: realpath(3) resolves not correctly for not existing path?!  */
    }
    free(buf);
    out = strndup(safe, strlen(safe));
    if (strcmp(path, out) != 0)
        sb_log(4, "File \"%s\" is actually \"%s\".", path, out);
    return out;
}

/*  verify if program has access to a path.  */
static int sb_has_access(char *path) {
    int found = 0;
    char *pathlist;
    char *curpath;

    pathlist = strndup(sb_path, strlen(sb_path));
    curpath = strtok(pathlist, SB_PATHSEP);
    while (curpath != NULL && !found) {
        if (curpath != '\0' && strstr(path, curpath) == path) {
            found = 1;
            sb_log(4, "Allow access to \"%s\" (in \"%s\").", path, curpath);
        }
        curpath = strtok(NULL, SB_PATHSEP);
    }
    free(pathlist);
    return found;
}

static void sb_str_cmode(char *str, const char *cmode) {
    char *temp;

    temp = strndup(str, strlen(str));
    if (strlen(str) != 0)
        snprintf(str, PATH_MAX, "%s|%s", temp, cmode);
    else
        snprintf(str, PATH_MAX, "%s%s", temp, cmode);
    free(temp);
}

static char *sb_str_flags(const int flags) {
    char str[PATH_MAX+1];

    /*  terminate string for safety  */
    str[0] = '\0';
    str[PATH_MAX] = '\0';

    if (flags & O_WRONLY)    sb_str_cmode(str, "O_WRONLY");
    else if (flags & O_RDWR) sb_str_cmode(str, "O_RDWR");
    else                     sb_str_cmode(str, "O_RDONLY");

    if (flags & O_EXCL)      sb_str_cmode(str, "O_EXCL");
    if (flags & O_TRUNC)     sb_str_cmode(str, "O_TRUNC");
    if (flags & O_APPEND)    sb_str_cmode(str, "O_APPEND");
    if (flags & O_NONBLOCK)  sb_str_cmode(str, "O_NONBLOCK");
    if (flags & O_CREAT)     sb_str_cmode(str, "O_CREAT");
    if (flags & O_NOCTTY)    sb_str_cmode(str, "O_NOCTTY");
    if (flags & O_SYNC)      sb_str_cmode(str, "O_SYNC");
    if (flags & O_NOFOLLOW)  sb_str_cmode(str, "O_NOFOLLOW");
    if (flags & O_DIRECT)    sb_str_cmode(str, "O_DIRECT");
    if (flags & O_ASYNC)     sb_str_cmode(str, "O_ASYNC");
#ifdef O_LARGEFILE
    if (flags & O_LARGEFILE) sb_str_cmode(str, "O_LARGEFILE");
#endif
    if (flags & O_DIRECTORY) sb_str_cmode(str, "O_DIRECTORY");

    return strndup(str, strlen(str));
}

static char *str_timet(time_t time) {
    struct tm *t;
    char str[PATH_MAX+1];

    t = localtime(&time);
    str[0] = '\0';
    str[PATH_MAX] = '\0';
    sprintf(str, "%04i/%02i/%02i-%02i:%02i:%02i",
        t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
    free(t);
    return strndup(str, strlen(str));
}

static char *str_utimbuf(const struct utimbuf *buf) {
    char str[PATH_MAX+1];

    str[0] = '\0';
    str[PATH_MAX] = '\0';
    if (buf == NULL)
        sprintf(str, "NULL");
    else
        sprintf(str, "[%s, %s]", str_timet(buf->actime), str_timet(buf->modtime));
    return strndup(str, strlen(str));
}

/*
 *  ==== OVERLOADED LIBC FUNCTIONS ====
 */

int chmod(const char *path, mode_t mode) {
    char *rpath;

    sb_log(2, "Start chmod(\"%s\", %04o).", path, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do chmod(\"%s\", %04o).", path, mode);
        return sb_real_chmod(path, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to chmod(\"%s\", %04o).", rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int chown(const char *path, uid_t owner, gid_t group) {
    char *rpath;

    sb_log(2, "Start chown(\"%s\", %i, %i).", path, owner, group);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do chown(\"%s\", %i, %i).", path, owner, group);
        return sb_real_chown(path, owner, group);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to chown(\"%s\", %i, %i).", rpath, owner, group);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int lchown(const char *path, uid_t owner, gid_t group) {
    char *rpath;

    sb_log(2, "Start lchown(\"%s\", %i, %i).", path, owner, group);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do lchown(\"%s\", %i, %i).", path, owner, group);
        return sb_real_lchown(path, owner, group);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to lchown(\"%s\", %i, %i).", rpath, owner, group);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int link(const char *oldpath, const char *newpath) {
    char *oldrpath, *newrpath;

    sb_log(2, "Start link(\"%s\", \"%s\").", oldpath, newpath);
    oldrpath = sb_rewrite(oldpath, SB_R_FILE);
    newrpath = sb_rewrite(newpath, SB_R_FILE);
    if (sb_has_access(oldrpath) && sb_has_access(newrpath)) {
        sb_log(4, "Do link(\"%s\", \"%s\").", oldpath, newpath);
        return sb_real_link(oldpath, newpath);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to link(\"%s\", \"%s\").", oldrpath, newrpath);
    free(oldrpath);
    free(newrpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int mkdir(const char *path, mode_t mode) {
    char *rpath;
    sb_log(2, "Start mkdir(\"%s\", %04o).", path, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do mkdir(\"%s\", %04o).", path, mode);
        return sb_real_mkdir(path, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to mkdir(\"%s\", %04o).", rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int mkfifo(const char *path, mode_t mode) {
    char *rpath;

    sb_log(2, "Start mkfifo(\"%s\", %04o).", path, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do mkfifo(\"%s\", %04o).", path, mode);
        return sb_real_mkfifo(path, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to mkfifo(\"%s\", %04o).", rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int mknod(const char *path, mode_t mode, dev_t dev) {
    char *rpath;

    sb_log(2, "Start mknod(\"%s\", %04o).", path, mode);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do mknod(\"%s\", %04o).", path, mode);
        return sb_real_mknod(path, mode, dev);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to mknod(\"%s\", %04o).", rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int __xmknod(int ver, const char *path, mode_t mode, dev_t *dev) {
    char *rpath;

    sb_log(2, "Start __xmknod(%i, \"%s\", %04o).", ver, path, mode);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do __xmknod(%i, \"%s\", %04o).", ver, path, mode);
        return sb_real___xmknod(ver, path, mode, dev);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to __xmknod(%i, \"%s\", %04o).", ver, rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int open(const char *path, int flags, ...) {
    char *rpath;
    char *strflags;
    va_list argptr;
    mode_t mode;
    int found;

    strflags = sb_str_flags(flags);
    if (flags & O_CREAT) {
        va_start(argptr, flags);
        mode = (mode_t)va_arg(argptr, int);
        va_end(argptr);
    }
    else
        mode = 0;
    sb_log(2, "Start open(\"%s\", %s, %04o).", path, strflags, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if ((found = sb_has_access(rpath)) || !(flags & (O_WRONLY|O_RDWR))) {
        /*  disable O_CREAT when O_RDONLY  */
        if (!found) {
            flags &= ~O_CREAT;
            strflags = sb_str_flags(flags);
        }
        sb_log(4, "Do open(\"%s\", %s, %04o).", path, strflags, mode);
        return sb_real_open(path, flags, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to open(\"%s\", %s, %04o).", rpath, strflags, mode);
    free(rpath);
    free(strflags);
    if (sb_action == SB_A_WARN)
        return sb_devnull;
    errno = EACCES;
    return -1;
}

int open64(const char *path, int flags, ...) {
    char *rpath;
    char *strflags;
    va_list argptr;
    mode_t mode;
    int found;

    strflags=sb_str_flags(flags);
    if (flags & O_CREAT) {
        va_start(argptr, flags);
        mode = (mode_t)va_arg(argptr, int);
        va_end(argptr);
    }
    else
        mode = 0;
    sb_log(2, "Start open64(\"%s\", %s, %04o).", path, strflags, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if ((found = sb_has_access(rpath)) || !(flags & (O_WRONLY|O_RDWR))) {
        /*  disable O_CREAT when O_RDONLY  */
        if (!found) {
            flags &= ~O_CREAT;
            strflags = sb_str_flags(flags);
        }
        sb_log(4, "Do open64(\"%s\", %s, %04o).", path, strflags, mode);
        return sb_real_open64(path, flags, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to open64(\"%s\", %s, %04o).", rpath, strflags, mode);
    free(rpath);
    free(strflags);
    if (sb_action == SB_A_WARN)
        return sb_devnull;
    errno = EACCES;
    return -1;
}

int creat(const char *path, mode_t mode) {
    char *rpath;

    sb_log(2, "Start creat(\"%s\", %04o).", path, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do creat(\"%s\", %04o).", path, mode);
        return sb_real_creat(path, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to creat(\"%s\", %04o).", rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return sb_devnull;
    errno = EACCES;
    return -1;
}

int creat64(const char *path, mode_t mode) {
    char *rpath;

    sb_log(2, "Start creat64(\"%s\", %04o).", path, mode);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do creat64(\"%s\", %04o).", path, mode);
        return sb_real_creat64(path, mode);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to creat64(\"%s\", %04o).", rpath, mode);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return sb_devnull;
    errno = EACCES;
    return -1;
}

int remove(const char *path) {
    char *rpath;

    sb_log(2, "Start remove(\"%s\").", path);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do remove(\"%s\").", path);
        return sb_real_remove(path);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to remove(\"%s\").", rpath);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int rename(const char *oldpath, const char *newpath) {
    char *oldrpath, *newrpath;

    sb_log(2, "Start rename(\"%s\", \"%s\").", oldpath, newpath);
    oldrpath = sb_rewrite(oldpath, SB_R_LINK);
    newrpath = sb_rewrite(newpath, SB_R_FILE);
    if (sb_has_access(oldrpath) && sb_has_access(newrpath)) {
        sb_log(4, "Do rename(\"%s\", \"%s\").", oldpath, newpath);
        return sb_real_rename(oldpath, newpath);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to rename(\"%s\", \"%s\").", oldrpath, newrpath);
    free(oldrpath);
    free(newrpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int rmdir(const char *path) {
    char *rpath;

    sb_log(2, "Start rmdir(\"%s\").", path);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do rmdir(\"%s\").", path);
        return sb_real_rmdir(path);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to rmdir(\"%s\").", rpath);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int symlink(const char *oldpath, const char *newpath) {
    char *newrpath;

    sb_log(2, "Start symlink(\"%s\", \"%s\").", oldpath, newpath);
    newrpath = sb_rewrite(newpath, SB_R_LINK);
    if (sb_has_access(newrpath)) {
        sb_log(4, "Do symlink(\"%s\", \"%s\").", oldpath, newpath);
        return sb_real_symlink(oldpath, newpath);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to symlink(\"%s\", \"%s\").", oldpath, newrpath);
    free(newrpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int unlink(const char *path) {
    char *rpath;

    sb_log(2, "Start unlink(\"%s\").", path);
    rpath = sb_rewrite(path, SB_R_LINK);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do unlink(\"%s\").", path);
        return sb_real_unlink(path);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to unlink(\"%s\").", rpath);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int utime(const char *path, const struct utimbuf *buf) {
    char *rpath;

    sb_log(2, "Start utime(\"%s\", NULL).", path);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do utime(\"%s\", NULL).", path);
        return sb_real_utime(path, buf);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to utime(\"%s\", %s).", rpath, str_utimbuf(buf));
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

int utimes(const char *path, const struct timeval *tvp) {
    char *rpath;

    sb_log(2, "Start utimes(\"%s\", NULL).", path);
    rpath = sb_rewrite(path, SB_R_FILE);
    if (sb_has_access(rpath)) {
        sb_log(4, "Do utimes(\"%s\", NULL).", path);
        return sb_real_utimes(path, tvp);
    }
    if (sb_action == SB_A_HALT)
        exit(0);
    sb_log(1, "Attempt to utimes(\"%s\", NULL).", rpath);
    free(rpath);
    if (sb_action == SB_A_WARN)
        return 0;
    errno = EACCES;
    return -1;
}

