/* vim: set ts=8 sw=8 sts=8 noet tw=78:
 *
 * tup - A file-based build system
 *
 * Copyright (C) 2012-2024  Mike Shal <marfey@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * 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.
 */

#define _ATFILE_SOURCE
#define _GNU_SOURCE
#ifdef linux
/* For pread()/pwrite() */
#define _XOPEN_SOURCE 500
#endif

#include "compat/utimensat.h"
#include "tup_fuse_fs.h"
#include "tup/config.h"
#include "tup/ccache.h"
#include "tup/debug.h"
#include "tup/server.h"
#include "tup/container.h"
#include "tup/entry.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/resource.h>

static struct thread_root troot = THREAD_ROOT_INITIALIZER;
static int server_mode = 0;
static pid_t ourpgid;
static int max_open_files = 128;

void tup_fuse_fs_init(void)
{
	struct rlimit rlim;
	ourpgid = getpgid(0);
	if(getrlimit(RLIMIT_NOFILE, &rlim) == 0) {
		int x;
		for(x=0; x<10; x++) {
			/* Keep doubling until we hit the real limit, whatever
			 * that is. OSX sets rlim.rlim_max to -1 for some
			 * reason, so we have no idea what the limit is.
			 */
			rlim.rlim_cur *= 2;
			if(setrlimit(RLIMIT_NOFILE, &rlim) != 0)
				break;
		}
		if(getrlimit(RLIMIT_NOFILE, &rlim) == 0) {
			max_open_files = rlim.rlim_cur / 2;
		}
	}
}

int tup_fuse_add_group(int id, struct file_info *finfo)
{
	finfo->tnode.id = id;
	if(thread_tree_insert(&troot, &finfo->tnode) < 0) {
		fprintf(stderr, "tup error: Unable to insert id %i into the fuse tree\n", id);
		return -1;
	}
	return 0;
}

int tup_fuse_rm_group(struct file_info *finfo)
{
	thread_tree_rm(&troot, &finfo->tnode);
	return 0;
}

void tup_fuse_set_parser_mode(int mode)
{
	server_mode = mode;
}

static int is_hidden(const char *path)
{
	if(strstr(path, "/.git") != NULL)
		return 1;
	if(strstr(path, "/.tup") != NULL)
		return 1;
	if(strstr(path, "/.hg") != NULL)
		return 1;
	if(strstr(path, "/.svn") != NULL)
		return 1;
	if(strstr(path, "/.bzr") != NULL)
		return 1;
	if(is_ccache_path(path))
		return 1;
	return 0;
}

static struct file_info *get_finfo(const char *path)
{
	struct thread_tree *tt;
	int jobnum;

	if(!path)
		return NULL;
	if(path[0] != '/')
		return NULL;
	path++;

	if(strncmp(path, TUP_JOB, sizeof(TUP_JOB)-1) != 0) {
		return NULL;
	}
	path += sizeof(TUP_JOB)-1;
	jobnum = strtol(path, NULL, 0);

	tt = thread_tree_search(&troot, jobnum);
	if(tt) {
		struct file_info *finfo;
		finfo = container_of(tt, struct file_info, tnode);
		finfo_lock(finfo);
		return finfo;
	}

	return NULL;
}

static void put_finfo(struct file_info *finfo)
{
	finfo_unlock(finfo);
}

static const char *peel(const char *path)
{
	if(!path)
		return NULL;
	if(path[0] != '/')
		return NULL;

	if(strncmp(path + 1, TUP_JOB, sizeof(TUP_JOB)-1) == 0) {
		char *slash;

		path += sizeof(TUP_JOB); /* +1 and -1 cancel */
		slash = strchr(path, '/');
		if(slash) {
			path = slash;
		} else {
			path = "/";
		}

	}

	return path;
}

static struct mapping *add_mapping_internal(struct file_info *finfo, const char *path)
{
	static int filenum = 0;
	static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
	struct mapping *map = NULL;
	int size;
	int myfile;
	const char *peeled;

	peeled = peel(path);

	if(!is_hidden(peeled)) {
		if(handle_open_file(ACCESS_WRITE, peeled, finfo) < 0) {
			/* TODO: Set failure on internal server? */
			fprintf(stderr, "tup internal error: handle open file failed\n");
			return NULL;
		}
	}

	map = malloc(sizeof *map);
	if(!map) {
		perror("malloc");
		return NULL;
	}
	map->realname = strdup(peeled);
	if(!map->realname) {
		perror("strdup");
		return NULL;
	}
	size = sizeof(int) * 2 + sizeof(TUP_TMP) + 1;
	map->tmpname = malloc(size);
	if(!map->tmpname) {
		perror("malloc");
		return NULL;
	}
	map->tent = NULL; /* This is used when saving dependencies */

	pthread_mutex_lock(&lock);
	myfile = filenum;
	filenum++;
	pthread_mutex_unlock(&lock);

	if(snprintf(map->tmpname, size, TUP_TMP "/%x", myfile) >= size) {
		fprintf(stderr, "tup internal error: mapping tmpname is sized incorrectly.\n");
		return NULL;
	}

	TAILQ_INSERT_TAIL(&finfo->mapping_list, map, list);
	return map;
}

static struct mapping *add_mapping(const char *path)
{
	struct file_info *finfo;
	struct mapping *map = NULL;

	finfo = get_finfo(path);
	if(finfo) {
		map = add_mapping_internal(finfo, path);
		put_finfo(finfo);
	}
	return map;
}

static struct mapping *find_mapping(struct file_info *finfo, const char *path)
{
	const char *peeled;
	struct mapping *map;

	peeled = peel(path);
	TAILQ_FOREACH(map, &finfo->mapping_list, list) {
		if(strcmp(peeled, map->realname) == 0) {
			return map;
		}
	}
	return NULL;
}

static int context_check(void)
{
	pid_t pgid;

	/* Only processes spawned by tup should be able to access our
	 * file-system. This is determined by the fact that all sub-processes
	 * should be in the same process group as tup itself. Since the fuse
	 * thread runs in the main tup process, we can check our own pgid by
	 * using getpgid(0). If their pgid doesn't match, we bail since nobody
	 * else is allowed to look at our filesystem. If they could, that would
	 * hose up our dependency analysis.
	 */
	pgid = getpgid(fuse_get_context()->pid);

	/* OSX will fail to return a valid pgid for a zombie process.  However,
	 * for some reason when using 'ar' to create archives, a zombie libtool
	 * process will call 'unlink' on the .fuse_hidden file. If we ignore
	 * that check, then tup will save the .fuse_hidden file as a separate
	 * output because hidden files are ignored.
	 *
	 * Separately, Linux running in a container will have a bogus fuse
	 * context pid, so getpgid() always fails. There doesn't seem to be
	 * much we can do in this case. Fortunately, if lxc is working that
	 * probably means we're using a separate mount namespace anyway, making
	 * this check moot.
	 */
	if(pgid == -1 && errno == ESRCH) {
		return 0;
	}

	if(ourpgid != pgid) {
		if(server_debug_enabled()) {
			fprintf(stderr, "[33mtup fuse warning: Process pid=%i, uid=%i, gid=%i is trying to access the tup server's fuse filesystem.[0m\n",
					fuse_get_context()->pid, fuse_get_context()->uid, fuse_get_context()->gid);
		}
		return -1;
	}
	return 0;
}

static int ignore_file(const char *path)
{
	if(strncmp(path, "/dev", 4) == 0)
		return 1;
	if(strncmp(path, "/sys", 4) == 0)
		return 1;
	if(strncmp(path, "/proc", 5) == 0)
		return 1;
	if(is_ccache_path(path))
		return 1;
	return 0;
}

static void tup_fuse_handle_file(const char *path, const char *stripped, enum access_type at)
{
	struct file_info *finfo;

	if(ignore_file(peel(path)))
		return;

	finfo = get_finfo(path);
	if(finfo) {
		if(handle_open_file(at, peel(path), finfo) < 0) {
			/* TODO: Set failure on internal server? */
			fprintf(stderr, "tup internal error: handle open file failed\n");
		}
		if(stripped) {
			if(handle_open_file(at, stripped, finfo) < 0) {
				/* TODO: Set failure on internal server? */
				fprintf(stderr, "tup internal error: handle open file failed\n");
			}
		}
		put_finfo(finfo);
	}
}

static const char *get_virtual_var(const char *peeled)
{
	const char *stripped;

	if(strncmp(peeled, get_tup_top(), get_tup_top_len()) == 0) {
		peeled += get_tup_top_len();
		if(peeled[0] != '/')
			return NULL;
		peeled++;
		stripped = peeled;
	} else {
		return NULL;
	}
	if(stripped) {
		const char *var = strstr(stripped, TUP_VAR_VIRTUAL_DIR);
		if(var) {
			var += TUP_VAR_VIRTUAL_DIR_LEN;
			return var;
		}
	}
	return NULL;
}

/* tup_fs_* originally from fuse-2.8.5/example/fusexmp.c */
#ifdef FUSE3
static int tup_fs_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
#else
static int tup_fs_getattr(const char *path, struct stat *stbuf)
#endif
{
	int res;
	const char *peeled;
	struct mapping *map;
	struct tmpdir *tmpdir;
	struct file_info *finfo;
	const char *var;
	const char *stripped = NULL;
	int rc;

#ifdef FUSE3
	(void) fi;
#endif
	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);

	/* If we have a temporary directory of the name we're trying to do
	 * getattr(), just pretend it has the same permissions as the top
	 * tup directory. This isn't necessarily accurate, but should work for
	 * most cases.
	 */
	finfo = get_finfo(path);
	if(finfo) {
		if(strcmp(peeled, ".tup/mnt") == 0 || strstr(peeled, "/.tup/mnt") != NULL) {
			/* t6056 - don't allow sub-processes to mess with our
			 * data.
			 */
			put_finfo(finfo);
			return -EPERM;
		}
		rc = 0;
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peeled) == 0) {
				if(fstat(tup_top_fd(), stbuf) < 0)
					rc = -errno;
				put_finfo(finfo);
				return rc;
			}
		}
		map = find_mapping(finfo, path);
		if(map)
			peeled = map->tmpname;
		put_finfo(finfo);
	}

	/* First we get a getattr("@tup@"), then we get a
	 * getattr("@tup@/CONFIG_FOO"). So first time we return success so fuse
	 * will assume the directory is there, then the second time we keep
	 * track of the variable and return failure because we're not actually
	 * going to open anything.
	 */
	var = get_virtual_var(peeled);
	if(var) {
		if(var[0] == 0) {
			stbuf->st_mode = S_IFDIR | 0755;
			stbuf->st_nlink = 2;
			return 0;
		} else {
			/* skip '/' */
			var++;

			if(finfo) {
				finfo_lock(finfo);
				if(handle_open_file(ACCESS_VAR, var, finfo) < 0) {
					fprintf(stderr, "tup error: Unable to save dependency on @-%s\n", var);
					return 1;
				}
				finfo_unlock(finfo);
			}
			/* Always return error, since we can't actually open
			 * an @-variable.
			 */
			return -1;
		}
	}

	res = fstatat(tup_top_fd(), peeled, stbuf, AT_SYMLINK_NOFOLLOW);
	if (res == -1) {
		rc = -errno;
	} else {
		rc = 0;
	}
	tup_fuse_handle_file(path, stripped, ACCESS_READ);

	return rc;
}

static int tup_fs_access(const char *path, int mask)
{
	int res;
	const char *peeled;
	struct mapping *map;
	struct file_info *finfo;
	struct tmpdir *tmpdir;
	const char *var;

	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);

	finfo = get_finfo(path);
	if(finfo) {
		int entry_found = 0;
		int rc = 0;

		map = find_mapping(finfo, path);
		if(map)
			peeled = map->tmpname;

		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peeled) == 0) {
				/* For a temporary directory, just use the same
				 * access permissions as the top-level directory.
				 * This could be finer grained to use the actual
				 * permissions assigned in mkdir for a temp
				 * directory.
				 */
				if(faccessat(tup_top_fd(), ".", mask, 0) < 0)
					rc = -errno;
				entry_found = 1;
				break;
			}
		}
		put_finfo(finfo);
		if(entry_found)
			return rc;
	}

	/* OSX will call access() on the virtual directory before calling
	 * getattr() on the variable name, so we check for that here. The
	 * var[0] == 0 check means it is just the @tup@ directory itself, and
	 * not a variable name.
	 */
	var = get_virtual_var(peeled);
	if(var && var[0] == 0) {
		return 0;
	}

	/* This is preceded by a getattr - no need to handle a read event */
	res = faccessat(tup_top_fd(), peeled, mask, 0);
	if (res == -1)
		return -errno;

	return 0;
}

static int tup_fs_readlink(const char *path, char *buf, size_t size)
{
	int res;
	const char *peeled;
	struct file_info *finfo;
	struct mapping *map;
	const char *stripped = NULL;

	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);

	finfo = get_finfo(path);
	if(finfo) {
		map = find_mapping(finfo, path);
		if(map)
			peeled = map->tmpname;
		put_finfo(finfo);
	}

	/* /proc/self gets special treatment, since we want the pid of the
	 * process doing the readlink(). If we let the kernel handle it then we
	 * get the pid of this fuse process, which is obviously incorrect.
	 */
	if(strcmp(peeled, "/proc/self") == 0) {
		res = snprintf(buf, size - 1, "%i", fuse_get_context()->pid);
		if(res >= (signed)size - 1) {
			/* According to readlink(2), if the buffer is too small then the result
			 * is truncated.
			 */
			res = size - 1;
		}
	} else {
		res = readlinkat(tup_top_fd(), peeled, buf, size - 1);
		if(res == -1)
			return -errno;
	}
	tup_fuse_handle_file(path, stripped, ACCESS_READ);

	buf[res] = '\0';
	return 0;
}

static void add_dir_entries(DIR *dp, void *buf, fuse_fill_dir_t filler,
			    int ignore_dot_tup)
{
	struct dirent *de;
	while((de = readdir(dp)) != NULL) {
		struct stat st;
		memset(&st, 0, sizeof(st));
		st.st_ino = de->d_ino;
		st.st_mode = de->d_type << 12;

		if(!ignore_dot_tup || strcmp(de->d_name, ".tup") != 0)
			if(mfiller(buf, de->d_name, &st, 0))
				break;
	}
}

static int fill_actual_directory(const char *peeled, void *buf,
				 fuse_fill_dir_t filler, int ignore_dot_tup)
{
	DIR *dp;
	int fd;

	fd = openat(tup_top_fd(), peeled, O_RDONLY);
	if(fd >= 0) {
		dp = fdopendir(fd);
		if(dp == NULL)
			return -errno;

		add_dir_entries(dp, buf, filler, ignore_dot_tup);
		closedir(dp);
	}
	return 0;
}

static int readdir_parser(const char *path, void *buf, fuse_fill_dir_t filler)
{
	if(strncmp(path, get_tup_top(), get_tup_top_len()) == 0) {
		if(tup_fuse_server_get_dir_entries(path + get_tup_top_len(),
						   buf, filler) < 0)
			return -EPERM;
	} else {
		/* t4052 */
		return fill_actual_directory(path, buf, filler, 0);
	}
	return 0;
}

#ifdef FUSE3
static int tup_fs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
			  off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags)
#else
static int tup_fs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
			  off_t offset, struct fuse_file_info *fi)
#endif
{
	const char *peeled;
	struct file_info *finfo;
	int is_tmpdir = 0;

	(void) offset;
	(void) fi;
#ifdef FUSE3
	(void) flags;
#endif

	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);
	finfo = get_finfo(path);
	if(finfo) {
		struct tmpdir *tmpdir;
		struct mapping *map;
		struct stat st;

		/* In the parser, we have to look at the tup database, not
		 * the filesystem.
		 */
		if(server_mode == SERVER_PARSER_MODE) {
			int rc;
			rc = readdir_parser(peeled, buf, filler);
			if(rc < 0) {
				finfo->server_fail = 1;
			}
			put_finfo(finfo);
			return rc;
		}

		/* If we are doing readdir() on a temporary directory, make
		 * sure we don't try to save the dependency or do a real
		 * opendir(), since that won't work.
		 */
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peeled) == 0) {
				is_tmpdir = 1;
				break;
			}
		}

		/* Check any mappings to see if there are extra files that
		 * we need to add to the list in addition to whatever we
		 * get from the real opendir/readdir (if applicable).
		 */
		TAILQ_FOREACH(map, &finfo->mapping_list, list) {
			const char *realname;

			/* Get the 'real' realname of the file. Eg: sub/bar.txt
			 * becomes "bar.txt" if we are doing readdir("sub").
			 */
			if(peeled[0] == '.') {
				/* TODO: ?? */
				realname = map->realname;
			} else {
				int len;
				len = strlen(peeled);
				if(strncmp(peeled, map->realname, len) != 0)
					continue;
				if(map->realname[len] != '/')
					continue;
				realname = &map->realname[len+1];
			}
			/* Make sure we don't include "sub/dir/bar.txt" if
			 * we are just doing readdir("sub").
			 */
			if(strchr(realname, '/') != NULL)
				continue;

			if(fstatat(tup_top_fd(), map->tmpname, &st, AT_SYMLINK_NOFOLLOW) < 0) {
				perror("lstat");
				fprintf(stderr, "tup error: Unable to stat temporary file '%s'\n", map->tmpname);
				put_finfo(finfo);
				return -1;
			}
			if(mfiller(buf, realname, &st, 0))
				break;
		}

		/* Check any tmpdir subdirs, and add them to the list */
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			int peeled_len;
			int tmpdir_len;

			/* if this tmpdir is a subdir of the readdir() dir */
			peeled_len = strlen(peeled);
			tmpdir_len = strlen(tmpdir->dirname);
			if (tmpdir_len > peeled_len
			    && strncmp(peeled, tmpdir->dirname, peeled_len) == 0
			    && tmpdir->dirname[peeled_len] == '/') {
				const char *realname;

				if(fstat(tup_top_fd(), &st) < 0) {
					int rc = -errno;
					fprintf(stderr, "tup error: Unable to stat .tup directory\n");
					put_finfo(finfo);
					return rc;
				}

				realname = &tmpdir->dirname[peeled_len+1];

				/* Make sure we don't include "sub/dir/bar" if
				* we are just doing readdir("sub").
				*/
				if(strchr(realname, '/') != NULL)
					continue;

				if(mfiller(buf, realname, &st, 0))
					break;
			}
		}

		put_finfo(finfo);
	}

	if(is_tmpdir)
		return 0;

	tup_fuse_handle_file(path, NULL, ACCESS_READ);

	/* If finfo is NULL, we're outside of tup, so we don't need to ignore
	 * any files called '.tup' in that case.
	 */
	return fill_actual_directory(peeled, buf, filler, finfo != NULL);
}

static int mknod_internal(const char *path, mode_t mode, int flags, int close_fd)
{
	int rc;
	struct mapping *map;

	if(context_check() < 0)
		return -EPERM;

	/* t5119 - Make sure we could write to the actual underlying directory,
	 * since that can affect program behavior. For example, inkscape uses
	 * fontconfig, which tries to write .uuid files in /usr/. If we report
	 * that the write succeeds, we'll have issues with unspecified outputs.
	 */
	char *dir;
	char *dirtmp = strdup(path);
	if(!dirtmp) {
		return -ENOMEM;
	}
	dir = dirname(dirtmp);

	/* Use tup_fs_access in case it's a directory we mkdir'd during program
	 * execution.
	 */
	rc = tup_fs_access(dir, W_OK);
	free(dirtmp);
	if(rc < 0) {
		return -errno;
	}

	/* On Linux this could just be 'mknod(path, mode, rdev)' but this
	   is more portable */
	if (S_ISREG(mode)) {
		map = add_mapping(path);
		if(!map) {
			return -ENOMEM;
		} else {
			/* TODO: Error check */
			tup_fuse_handle_file(path, NULL, ACCESS_WRITE);

			rc = openat(tup_top_fd(), map->tmpname, flags, mode);
			if(rc < 0)
				return -errno;
			if(close_fd) {
				if(close(rc) < 0)
					return -errno;
				rc = 0;
			}
		}
	} else if S_ISFIFO(mode) {
		map = add_mapping(path);
		if(!map) {
			return -ENOMEM;
		} else {
			rc = mkfifo(map->tmpname, mode);
			if(rc < 0)
				return -errno;
		}
	} else if S_ISSOCK(mode) {
		map = add_mapping(path);
		if(!map) {
			return -ENOMEM;
		} else {
			rc = mknod(map->tmpname, mode, 0);
			if(rc < 0)
				return -errno;
		}
	} else {
		/* Other things (eg: actual device nodes) are not
		 * permitted.
		 */
		fprintf(stderr, "tup error: mknod() with mode 0x%x is not permitted.\n", mode);
		return -EPERM;
	}

	return rc;
}

static int tup_fs_mknod(const char *path, mode_t mode, dev_t rdev)
{
	if(rdev) {}
	return mknod_internal(path, mode, O_CREAT | O_EXCL | O_WRONLY, 1);
}

static int tup_fs_mkdir(const char *path, mode_t mode)
{
	struct tmpdir *tmpdir;
	struct file_info *finfo;
	const char *peeled;

	if(mode) {}

	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);
	if(ignore_file(peeled)) {
		int rc;

		/* Things like ccache need to just call mkdir rather than use
		 * our temporary directories.
		 */
		rc = mkdir(peeled, mode);
		if(rc < 0)
			return -errno;
		return 0;
	}

	finfo = get_finfo(path);
	if(finfo) {
		int rc = -1;
		tmpdir = malloc(sizeof *tmpdir);
		if(!tmpdir) {
			perror("malloc");
			rc = -ENOMEM;
		} else {
			tmpdir->dirname = strdup(peel(path));
			if(!tmpdir->dirname) {
				perror("strdup");
				rc = -ENOMEM;
			}
			if(tmpdir && tmpdir->dirname) {
				TAILQ_INSERT_TAIL(&finfo->tmpdir_list, tmpdir, list);
				rc = 0;
			}
		}
		put_finfo(finfo);
		return rc;
	}
	return -EPERM;
}

static int tup_fs_unlink(const char *path)
{
	struct mapping *map;
	struct file_info *finfo;

	if(context_check() < 0)
		return -EPERM;

	finfo = get_finfo(path);
	if(finfo) {
		map = find_mapping(finfo, path);
		if(map) {
			unlinkat(tup_top_fd(), map->tmpname, 0);
			del_map(&finfo->mapping_list, map);
			put_finfo(finfo);
			tup_fuse_handle_file(path, NULL, ACCESS_UNLINK);
			return 0;
		}
		put_finfo(finfo);
	}
	if(strstr(path, ".fuse_hidden") != NULL) {
		/* Similar to the rename check for .fuse_hidden, this shows up
		 * in Arch sometimes.
		 */
		const char *peeled = peel(path);
		int res;

		res = unlink(peeled);
		if(res < 0)
			return -errno;
		return 0;
	}
	fprintf(stderr, "tup error: Unable to unlink files not created during this job: %s\n", peel(path));
	return -EPERM;
}

static int tup_fs_rmdir(const char *path)
{
	struct tmpdir *tmpdir;
	const char *peeled;
	struct file_info *finfo;
	struct mapping *map;

	if(context_check() < 0)
		return -EPERM;

	finfo = get_finfo(path);
	if(finfo) {
		peeled = peel(path);
		size_t len = strlen(peeled);

		// Ensure that there are no subdirectories
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strncmp(tmpdir->dirname, peeled, len) == 0 && tmpdir->dirname[len] == '/') {
				put_finfo(finfo);
				return -ENOTEMPTY;
			}
		}
		// Ensure that there are no files in the directory
		TAILQ_FOREACH(map, &finfo->mapping_list, list) {
			if (strncmp(map->realname, peeled, len) == 0 && map->realname[len] == '/') {
				put_finfo(finfo);
				return -ENOTEMPTY;
			}
		}

		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peeled) == 0) {
				TAILQ_REMOVE(&finfo->tmpdir_list, tmpdir, list);
				free(tmpdir->dirname);
				free(tmpdir);
				put_finfo(finfo);
				return 0;
			}
		}
		put_finfo(finfo);
	}
	fprintf(stderr, "tup error: Unable to rmdir a directory not created during this job: %s\n", peel(path));
	return -EPERM;
}

static int tup_fs_symlink(const char *from, const char *to)
{
	int res;
	struct mapping *tomap;

	if(context_check() < 0)
		return -EPERM;

	tomap = add_mapping(to);
	if(!tomap) {
		return -ENOMEM;
	}

	res = symlinkat(from, tup_top_fd(), tomap->tmpname);
	if (res == -1)
		return -errno;

	return 0;
}

#ifdef FUSE3
static int tup_fs_rename(const char *from, const char *to, unsigned int flags)
#else
static int tup_fs_rename(const char *from, const char *to)
#endif
{
	struct file_info *finfo;
	const char *peelfrom;
	const char *peelto;
	struct mapping *map;

#ifdef FUSE3
	(void) flags;
#endif
	if(context_check() < 0)
		return -EPERM;

	peelfrom = peel(from);
	peelto = peel(to);

	finfo = get_finfo(to);
	if(finfo) {
		struct tmpdir *tmpdir;
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peelfrom) == 0) {
				free(tmpdir->dirname);
				tmpdir->dirname = strdup(peelto);
				if(!tmpdir->dirname) {
					perror("strdup");
					put_finfo(finfo);
					return -ENOMEM;
				}
				put_finfo(finfo);
				return 0;
			}
		}

		/* If we are re-naming to a previously created file, then
		 * delete the old mapping. (eg: 'ar' will create an empty
		 * library, so we have one mapping, then create a new temp file
		 * and rename it over to 'ar', so we have a new mapping for the
		 * temp node. We need to delete the first empty one since that
		 * file is overwritten).
		 */
		map = find_mapping(finfo, to);
		if(map) {
			unlink(map->tmpname);
			del_map(&finfo->mapping_list, map);
		}

		map = find_mapping(finfo, from);
		if(!map) {
			put_finfo(finfo);
			return -ENOENT;
		}

		if(strstr(peelto, ".fuse_hidden") != NULL) {
			/* If we're renaming to a .fuse_hidden file, treat it
			 * as if the source was unlinked. This happens
			 * sometimes in an Arch VM where a deleted file shows
			 * up in FUSE as a rename to a .fuse_hidden file,
			 * especially in t4017 for some reason.
			 */
			unlinkat(tup_top_fd(), map->tmpname, 0);
			del_map(&finfo->mapping_list, map);
			put_finfo(finfo);
			tup_fuse_handle_file(from, NULL, ACCESS_UNLINK);
		} else {
			free(map->realname);
			map->realname = strdup(peelto);
			if(!map->realname) {
				perror("strdup");
				put_finfo(finfo);
				return -ENOMEM;
			}

			handle_rename(peelfrom, peelto, finfo);
			put_finfo(finfo);
		}
	}

	return 0;
}

static int tup_fs_link(const char *from, const char *to)
{
	if(from || to) {}

	fprintf(stderr, "tup error: hard links are not supported.\n");
	return -EPERM;
}

#ifdef FUSE3
static int tup_fs_chmod(const char *path, mode_t mode, struct fuse_file_info *fi)
#else
static int tup_fs_chmod(const char *path, mode_t mode)
#endif
{
	struct mapping *map;
	struct file_info *finfo;
	const char *peeled;
	struct tmpdir *tmpdir;

#ifdef FUSE3
	(void) fi;
#endif
	if(context_check() < 0)
		return -EPERM;

	finfo = get_finfo(path);
	if(finfo) {
		int rc = 0;
		map = find_mapping(finfo, path);
		if(map) {
			if(fchmodat(tup_top_fd(), map->tmpname, mode, 0) < 0)
				rc = -errno;
			put_finfo(finfo);
			return rc;
		}
		peeled = peel(path);
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peeled) == 0) {
				put_finfo(finfo);
				return 0;
			}
		}
		put_finfo(finfo);
	}
	fprintf(stderr, "tup error: Unable to chmod() files not created by this job: %s\n", peel(path));
	return -EPERM;
}

#ifdef FUSE3
static int tup_fs_chown(const char *path, uid_t uid, gid_t gid, struct fuse_file_info *fi)
#else
static int tup_fs_chown(const char *path, uid_t uid, gid_t gid)
#endif
{
	struct mapping *map;
	struct file_info *finfo;
	const char *peeled;
	struct tmpdir *tmpdir;

#ifdef FUSE3
	(void) fi;
#endif
	if(context_check() < 0)
		return -EPERM;

	finfo = get_finfo(path);
	if(finfo) {
		int rc = 0;
		map = find_mapping(finfo, path);
		if(map) {
			if(fchownat(tup_top_fd(), map->tmpname, uid, gid, AT_SYMLINK_NOFOLLOW) < 0)
				rc = -errno;
			put_finfo(finfo);
			return rc;
		}
		peeled = peel(path);
		TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
			if(strcmp(tmpdir->dirname, peeled) == 0) {
				put_finfo(finfo);
				return 0;
			}
		}
		put_finfo(finfo);
	}
	fprintf(stderr, "tup error: Unable to chown() files not created by this job: %s\n", peel(path));
	return -EPERM;
}

#ifdef FUSE3
static int tup_fs_truncate(const char *path, off_t size, struct fuse_file_info *fi)
#else
static int tup_fs_truncate(const char *path, off_t size)
#endif
{
	struct mapping *map;
	struct file_info *finfo;
	const char *peeled;
	struct tup_entry *match = NULL;

#ifdef FUSE3
	(void) fi;
#endif
	if(context_check() < 0)
		return -EPERM;

	/* TODO: error check? */
	tup_fuse_handle_file(path, NULL, ACCESS_WRITE);
	peeled = peel(path);
	finfo = get_finfo(path);
	if(finfo) {
		map = find_mapping(finfo, path);
		if(map) {
			int fd;
			int rc = 0;
			fd = openat(tup_top_fd(), map->tmpname, O_WRONLY);
			if(!fd) {
				put_finfo(finfo);
				return -errno;
			}
			if(ftruncate(fd, size) < 0)
				rc = -errno;
			if(close(fd) < 0)
				rc = -errno;
			put_finfo(finfo);
			return rc;
		} else {
			if(exclusion_match(stderr, &finfo->exclusion_root, peeled, &match) < 0) {
				put_finfo(finfo);
				return -ENOSYS;
			}
		}
		put_finfo(finfo);
	}
	if(match || is_hidden(peeled)) {
		if(truncate(peeled, size) < 0)
			return -errno;
		return 0;
	}
	fprintf(stderr, "tup error: Unable to truncate() files not created by this job: %s\n", peel(path));
	return -EPERM;
}

#ifdef FUSE3
static int tup_fs_utimens(const char *path, const struct timespec ts[2], struct fuse_file_info *fi)
#else
static int tup_fs_utimens(const char *path, const struct timespec ts[2])
#endif
{
	int res;
	const char *peeled;
	struct mapping *map;
	struct file_info *finfo;
	struct tup_entry *match = NULL;

#ifdef FUSE3
	(void) fi;
#endif
	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);
	finfo = get_finfo(path);
	if(finfo) {
		map = find_mapping(finfo, path);
		if(map) {
			int rc = 0;
			peeled = map->tmpname;

			res = utimensat(tup_top_fd(), peeled, ts, AT_SYMLINK_NOFOLLOW);
			if (res == -1)
				rc = -errno;
			put_finfo(finfo);
			return rc;
		} else {
			struct tmpdir *tmpdir;
			if(exclusion_match(stderr, &finfo->exclusion_root, peeled, &match) < 0) {
				put_finfo(finfo);
				return -ENOSYS;
			}
			/* Ignore a touch on a temporary directory */
			TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
				if(strcmp(tmpdir->dirname, peeled) == 0) {
					put_finfo(finfo);
					return 0;
				}
			}
		}
		put_finfo(finfo);
	}
	if(match || is_hidden(peeled)) {
		if(utimensat(tup_top_fd(), peeled, ts, AT_SYMLINK_NOFOLLOW) < 0)
			return -errno;
		return 0;
	}
	fprintf(stderr, "tup error: Unable to utimens() files not created by this job: %s\n", peeled);
	return -EPERM;
}

static int tup_fs_create(const char *path, mode_t mode, struct fuse_file_info *fi)
{
	int rc;
	struct file_info *finfo;
	rc = mknod_internal(path, mode, fi->flags, 0);
	if(rc < 0)
		return rc;
	finfo = get_finfo(path);
	if(finfo) {
		if(finfo->open_count >= max_open_files) {
			close(rc);
			fi->fh = 0;
		} else {
			fi->fh = rc;
		}
		finfo->open_count++;
		put_finfo(finfo);
	}
	return 0;
}

static int tup_fs_open(const char *path, struct fuse_file_info *fi)
{
	int res = 0;
	int fd;
	enum access_type at = ACCESS_READ;
	const char *peeled;
	const char *openfile;
	struct mapping *map;
	struct file_info *finfo;
	const char *stripped = NULL;

	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);
	openfile = peeled;

	finfo = get_finfo(path);
	if(finfo) {
		if((fi->flags & O_RDWR) || (fi->flags & O_WRONLY))
			at = ACCESS_WRITE;
		map = find_mapping(finfo, path);
		if(map) {
			openfile = map->tmpname;
		} else {
#ifdef FUSE3
			struct tup_entry *match = NULL;
			if(exclusion_match(stderr, &finfo->exclusion_root, path, &match) < 0) {
				put_finfo(finfo);
				return -ENOSYS;
			}
			if(at == ACCESS_WRITE && !is_hidden(path) && !match) {
				map = add_mapping_internal(finfo, path);
				openfile = map->tmpname;
			}
#endif
		}

		fd = openat(tup_top_fd(), openfile, fi->flags);
		if(fd < 0) {
			res = -errno;
		} else {
			if(finfo->open_count >= max_open_files) {
				close(fd);
				fi->fh = 0;
			} else {
				fi->fh = fd;
			}
			finfo->open_count++;
		}

		put_finfo(finfo);
		tup_fuse_handle_file(path, stripped, at);
	} else {
		res = -EPERM;
	}
	return res;
}

static int tup_fs_read(const char *path, char *buf, size_t size, off_t offset,
		       struct fuse_file_info *fi)
{
	int res;
	int fd;

	if(fi->fh == 0) {
		struct file_info *finfo;
		const char *openfile;

		openfile = peel(path);
		finfo = get_finfo(path);
		if(finfo) {
			struct mapping *map;
			map = find_mapping(finfo, path);
			if(map) {
				openfile = map->tmpname;
			}
			put_finfo(finfo);
		}

		fd = openat(tup_top_fd(), openfile, O_RDONLY);
		if(fd < 0)
			return -errno;
	} else {
		fd = fi->fh;
	}

	res = pread(fd, buf, size, offset);
	if (res == -1)
		res = -errno;

	if(fi->fh == 0) {
		close(fd);
	}

	return res;
}

static int tup_fs_write(const char *path, const char *buf, size_t size,
			off_t offset, struct fuse_file_info *fi)
{
	int res;
	int fd = -1;

	if(fi->fh == 0) {
		struct file_info *finfo;
		finfo = get_finfo(path);
		if(finfo) {
			struct mapping *map;
			map = find_mapping(finfo, path);
			if(map) {
				fd = openat(tup_top_fd(), map->tmpname, O_WRONLY);
				if(fd < 0) {
					put_finfo(finfo);
					return -errno;
				}
			}
			put_finfo(finfo);
		}
		if(fd < 0)
			return -EPERM;
	} else {
		fd = fi->fh;
	}

	res = pwrite(fd, buf, size, offset);
	if (res == -1)
		res = -errno;

	if(fi->fh == 0) {
		close(fd);
	}

	return res;
}

static int tup_fs_statfs(const char *path, struct statvfs *stbuf)
{
	int fd;
	int rc = 0;
	const char *peeled;
	struct mapping *map;
	struct file_info *finfo;
	struct tmpdir *tmpdir;

	if(context_check() < 0)
		return -EPERM;

	peeled = peel(path);

	finfo = get_finfo(path);
	if(finfo) {
		map = find_mapping(finfo, path);
		if(map) {
			peeled = map->tmpname;
		} else {
			TAILQ_FOREACH(tmpdir, &finfo->tmpdir_list, list) {
				if(strcmp(tmpdir->dirname, peeled) == 0) {
					if(fstatvfs(tup_top_fd(), stbuf) < 0)
						rc = -errno;
					put_finfo(finfo);
					return rc;
				}
			}
		}
		put_finfo(finfo);
	}

	fd = openat(tup_top_fd(), peeled, O_RDONLY);
	if(fd < 0)
		return -errno;

	if(fstatvfs(fd, stbuf) < 0)
		rc = -errno;
	if(close(fd) < 0)
		rc = -errno;
	return rc;
}

static int tup_fs_flush(const char *path, struct fuse_file_info *fi)
{
	/* We don't actually do anything here, but without flush() sometimes
	 * the sub-process will finish before our fuse fs finishes writing out
	 * all data and calling release(). Eg, if we have a command that does
	 * 'cp bigfile.txt newbigfile.txt', where bigfile.txt contains a lot of
	 * data, then the waitpid() in master_fork returns before release() is
	 * called. We then end up stating the output file and getting a bad
	 * timestamp since data is still being written out. (The file is
	 * written correctly, but our mtime that we store in the db is already
	 * out of date).
	 */
	if(path) {}
	if(fi) {}
	return 0;
}

static int tup_fs_release(const char *path, struct fuse_file_info *fi)
{
	struct file_info *finfo;
	if(fi->fh != 0) {
		if(close(fi->fh) < 0)
			return -errno;
	}

	finfo = get_finfo(path);
	if(finfo) {
		finfo->open_count--;
		pthread_cond_signal(&finfo->cond);
		put_finfo(finfo);
	}
	return 0;
}

static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t init_cond = PTHREAD_COND_INITIALIZER;
static int fuse_inited = 0;

#ifdef FUSE3
static void *tup_fs_init(struct fuse_conn_info *conn, struct fuse_config *cfg)
#else
static void *tup_fs_init(struct fuse_conn_info *conn)
#endif
{
	(void) conn;
#ifdef FUSE3
	(void) cfg;
#endif
	pthread_mutex_lock(&init_lock);
	fuse_inited = 1;
	pthread_cond_signal(&init_cond);
	pthread_mutex_unlock(&init_lock);
	return NULL;
}

int tup_fs_inited(void)
{
	struct timespec ts;

	ts.tv_sec = time(NULL) + 5;
	ts.tv_nsec = 0;
	pthread_mutex_lock(&init_lock);
	while(!fuse_inited) {
		int rc;
		rc = pthread_cond_timedwait(&init_cond, &init_lock, &ts);
		if(rc != 0) {
			pthread_mutex_unlock(&init_lock);
			if(rc == ETIMEDOUT) {
				fprintf(stderr, "tup error: Timed out waiting for the FUSE file-system to be ready.\n");
				return -1;
			}
			perror("pthread_cond_timedwait");
			return -1;
		}
	}
	pthread_mutex_unlock(&init_lock);
	return 0;
}

struct fuse_operations tup_fs_oper = {
	.getattr = tup_fs_getattr,
	.flush = tup_fs_flush,
	.access = tup_fs_access,
	.readlink = tup_fs_readlink,
	.readdir = tup_fs_readdir,
	.mknod = tup_fs_mknod,
	.mkdir = tup_fs_mkdir,
	.symlink = tup_fs_symlink,
	.unlink = tup_fs_unlink,
	.rmdir = tup_fs_rmdir,
	.rename = tup_fs_rename,
	.link = tup_fs_link,
	.chmod = tup_fs_chmod,
	.chown = tup_fs_chown,
	.truncate = tup_fs_truncate,
	.utimens = tup_fs_utimens,
	.create = tup_fs_create,
	.open = tup_fs_open,
	.read = tup_fs_read,
	.write = tup_fs_write,
	.statfs = tup_fs_statfs,
	.release = tup_fs_release,
	.init = tup_fs_init,
};
