/*
  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.
*/

#pragma once

#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_RO,
  KEY_HELP,
  KEY_VERSION,
  KEY_KERN
};

struct mount_opts {
  int allow_other;
  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 },
  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("large_read",		KEY_KERN),
  FUSE_OPT_END
};

static void mount_help(void)
{
  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 = static_cast<struct mount_opts*>(data);

  switch (key) {
  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;
}

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;
  size_t 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)
{
  int res = -1;
  struct mount_opts mo;

  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.ishelp)
    return 0;

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