/*
  FUSE: Filesystem in Userspace
  Copyright (C) 2005-2008 Csaba Henk <csaba.henk@creo.hu>

  This program can be distributed under the terms of the GNU LGPLv2.
  See the file COPYING.LIB.
*/

#include "fuse_i.h"
#include "fuse_misc.h"
#include "fuse_opt.h"

#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stddef.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <paths.h>
#include <limits.h>

#define FUSERMOUNT_PROG		"mount_fusefs"
#define FUSE_DEV_TRUNK		"/dev/fuse"

enum {
	KEY_ALLOW_ROOT,
	KEY_RO,
	KEY_HELP,
	KEY_VERSION,
	KEY_KERN
};

struct mount_opts {
	int allow_other;
	int allow_root;
	int ishelp;
	char *kernel_opts;
};

#define FUSE_DUAL_OPT_KEY(templ, key) 				\
	FUSE_OPT_KEY(templ, key), FUSE_OPT_KEY("no" templ, key)

static const struct fuse_opt fuse_mount_opts[] = {
	{ "allow_other", offsetof(struct mount_opts, allow_other), 1 },
	{ "allow_root", offsetof(struct mount_opts, allow_root), 1 },
	FUSE_OPT_KEY("allow_root",		KEY_ALLOW_ROOT),
	FUSE_OPT_KEY("-r",			KEY_RO),
	FUSE_OPT_KEY("-h",			KEY_HELP),
	FUSE_OPT_KEY("--help",			KEY_HELP),
	FUSE_OPT_KEY("-V",			KEY_VERSION),
	FUSE_OPT_KEY("--version",		KEY_VERSION),
	/* standard FreeBSD mount options */
	FUSE_DUAL_OPT_KEY("dev",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("async",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("atime",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("dev",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("exec",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("suid",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("symfollow",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("rdonly",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("sync",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("union",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("userquota",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("groupquota",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("clusterr",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("clusterw",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("suiddir",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("snapshot",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("multilabel",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("acls",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("force",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("update",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("ro",			KEY_KERN),
	FUSE_DUAL_OPT_KEY("rw",			KEY_KERN),
	FUSE_DUAL_OPT_KEY("auto",		KEY_KERN),
	/* options supported under both Linux and FBSD */
	FUSE_DUAL_OPT_KEY("allow_other",	KEY_KERN),
	FUSE_DUAL_OPT_KEY("default_permissions",KEY_KERN),
	FUSE_OPT_KEY("max_read=",		KEY_KERN),
	FUSE_OPT_KEY("subtype=",		KEY_KERN),
	/* FBSD FUSE specific mount options */
	FUSE_DUAL_OPT_KEY("private",		KEY_KERN),
	FUSE_DUAL_OPT_KEY("neglect_shares",	KEY_KERN),
	FUSE_DUAL_OPT_KEY("push_symlinks_in",	KEY_KERN),
	FUSE_OPT_KEY("nosync_unmount",		KEY_KERN),
	/* stock FBSD mountopt parsing routine lets anything be negated... */
	/*
	 * Linux specific mount options, but let just the mount util
	 * handle them
	 */
	FUSE_OPT_KEY("fsname=",			KEY_KERN),
	FUSE_OPT_KEY("nonempty",		KEY_KERN),
	FUSE_OPT_KEY("large_read",		KEY_KERN),
	FUSE_OPT_END
};

static void mount_help(void)
{
	fprintf(stderr,
		"    -o allow_root          allow access to root\n"
		);
	system(FUSERMOUNT_PROG " --help");
	fputc('\n', stderr);
}

static void mount_version(void)
{
	system(FUSERMOUNT_PROG " --version");
}

static int fuse_mount_opt_proc(void *data, const char *arg, int key,
			       struct fuse_args *outargs)
{
	struct mount_opts *mo = data;

	switch (key) {
	case KEY_ALLOW_ROOT:
		if (fuse_opt_add_opt(&mo->kernel_opts, "allow_other") == -1 ||
		    fuse_opt_add_arg(outargs, "-oallow_root") == -1)
			return -1;
		return 0;

	case KEY_RO:
		arg = "ro";
		/* fall through */

	case KEY_KERN:
		return fuse_opt_add_opt(&mo->kernel_opts, arg);

	case KEY_HELP:
		mount_help();
		mo->ishelp = 1;
		break;

	case KEY_VERSION:
		mount_version();
		mo->ishelp = 1;
		break;
	}
	return 1;
}

void fuse_unmount_compat22(const char *mountpoint)
{
	char dev[128];
	char *ssc, *umount_cmd;
	FILE *sf;
	int rv;
	char seekscript[] =
		/* error message is annoying in help output */
		"exec 2>/dev/null; "
		"/usr/bin/fstat " FUSE_DEV_TRUNK "* | "
		"/usr/bin/awk 'BEGIN{ getline; if (! ($3 == \"PID\" && $10 == \"NAME\")) exit 1; }; "
		"              { if ($3 == %d) print $10; }' | "
		"/usr/bin/sort | "
		"/usr/bin/uniq | "
		"/usr/bin/awk '{ i += 1; if (i > 1){ exit 1; }; printf; }; END{ if (i == 0) exit 1; }'";

	(void) mountpoint;

	/*
	 * If we don't know the fd, we have to resort to the scripted
	 * solution -- iterating over the fd-s is unpractical, as we
	 * don't know how many of open files we have. (This could be
	 * looked up in procfs -- however, that's optional on FBSD; or
	 * read out from the kmem -- however, that's bound to
	 * privileges (in fact, that's what happens when we call the
	 * setgid kmem fstat(1) utility).
	 */
	if (asprintf(&ssc, seekscript, getpid()) == -1)
		return;

	errno = 0;
	sf = popen(ssc, "r");
	free(ssc);
	if (! sf)
		return;

	fgets(dev, sizeof(dev), sf);
	rv = pclose(sf);
	if (rv)
		return;

	if (asprintf(&umount_cmd, "/sbin/umount %s", dev) == -1)
		return;
	system(umount_cmd);
	free(umount_cmd);
}

static void do_unmount(char *dev, int fd)
{
	char device_path[SPECNAMELEN + 12];
	const char *argv[4];
	const char umount_cmd[] = "/sbin/umount";
	pid_t pid;

	snprintf(device_path, SPECNAMELEN + 12, _PATH_DEV "%s", dev);

	argv[0] = umount_cmd;
	argv[1] = "-f";
	argv[2] = device_path;
	argv[3] = NULL;

	pid = fork();

	if (pid == -1)
		return;

	if (pid == 0) {
		close(fd);
		execvp(umount_cmd, (char **)argv);
		exit(1);
	}

	waitpid(pid, NULL, 0);
}

void fuse_kern_unmount(const char *mountpoint, int fd)
{
	char *ep, dev[128];
	struct stat sbuf;

	(void)mountpoint;

	if (fstat(fd, &sbuf) == -1)
		goto out;

	devname_r(sbuf.st_rdev, S_IFCHR, dev, 128);

	if (strncmp(dev, "fuse", 4))
		goto out;

	strtol(dev + 4, &ep, 10);
	if (*ep != '\0')
		goto out;

	do_unmount(dev, fd);

out:
	close(fd);
}

/* Check if kernel is doing init in background */
static int init_backgrounded(void)
{
	unsigned ibg, len;

	len = sizeof(ibg);

	if (sysctlbyname("vfs.fuse.init_backgrounded", &ibg, &len, NULL, 0))
		return 0;

	return ibg;
}


static int fuse_mount_core(const char *mountpoint, const char *opts)
{
	const char *mountprog = FUSERMOUNT_PROG;
	int fd;
	char *fdnam, *dev;
	pid_t pid, cpid;
	int status;

	fdnam = getenv("FUSE_DEV_FD");

	if (fdnam) {
		char *ep;

		fd = strtol(fdnam, &ep, 10);

		if (*ep != '\0') {
			fprintf(stderr, "invalid value given in FUSE_DEV_FD\n");
			return -1;
		}

		if (fd < 0)
			return -1;

		goto mount;
	}

	dev = getenv("FUSE_DEV_NAME");

	if (! dev)
		dev = (char *)FUSE_DEV_TRUNK;

	if ((fd = open(dev, O_RDWR)) < 0) {
		perror("fuse: failed to open fuse device");
		return -1;
	}

mount:
	if (getenv("FUSE_NO_MOUNT") || ! mountpoint)
		goto out;

	pid = fork();
	cpid = pid;

	if (pid == -1) {
		perror("fuse: fork() failed");
		close(fd);
		return -1;
	}

	if (pid == 0) {
		if (! init_backgrounded()) {
			/*
			 * If init is not backgrounded, we have to
			 * call the mount util backgrounded, to avoid
			 * deadlock.
			 */

			pid = fork();

			if (pid == -1) {
				perror("fuse: fork() failed");
				close(fd);
				exit(1);
			}
		}

		if (pid == 0) {
			const char *argv[32];
			int a = 0;

			if (! fdnam && asprintf(&fdnam, "%d", fd) == -1) {
				perror("fuse: failed to assemble mount arguments");
				exit(1);
			}

			argv[a++] = mountprog;
			if (opts) {
				argv[a++] = "-o";
				argv[a++] = opts;
			}
			argv[a++] = fdnam;
			argv[a++] = mountpoint;
			argv[a++] = NULL;
			execvp(mountprog, (char **) argv);
			perror("fuse: failed to exec mount program");
			exit(1);
		}

		exit(0);
	}

	if (waitpid(cpid, &status, 0) == -1 || WEXITSTATUS(status) != 0) {
		perror("fuse: failed to mount file system");
		close(fd);
		return -1;
	}

out:
	return fd;
}

int fuse_kern_mount(const char *mountpoint, struct fuse_args *args)
{
	struct mount_opts mo;
	int res = -1;

	memset(&mo, 0, sizeof(mo));
	/* mount util should not try to spawn the daemon */
	setenv("MOUNT_FUSEFS_SAFE", "1", 1);
	/* to notify the mount util it's called from lib */
	setenv("MOUNT_FUSEFS_CALL_BY_LIB", "1", 1);

	if (args &&
	    fuse_opt_parse(args, &mo, fuse_mount_opts, fuse_mount_opt_proc) == -1)
		return -1;

	if (mo.allow_other && mo.allow_root) {
		fprintf(stderr, "fuse: 'allow_other' and 'allow_root' options are mutually exclusive\n");
		goto out;
	}
	if (mo.ishelp)
		return 0;

	res = fuse_mount_core(mountpoint, mo.kernel_opts);
out:
	free(mo.kernel_opts);
	return res;
}

FUSE_SYMVER(".symver fuse_unmount_compat22,fuse_unmount@FUSE_2.2");
