/*
  FUSE: Filesystem in Userspace
  Copyright (C) 2001-2007  Miklos Szeredi <miklos@szeredi.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 "mount_util.h"

#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>

#ifdef __NetBSD__
#include <perfuse.h>

#define MS_RDONLY MNT_RDONLY
#define MS_NOSUID MNT_NOSUID
#define MS_NODEV MNT_NODEV
#define MS_NOEXEC MNT_NOEXEC
#define MS_SYNCHRONOUS MNT_SYNCHRONOUS
#define MS_NOATIME MNT_NOATIME
#ifndef MNT_DETACH
#define MNT_DETACH 2
#endif

#define umount2(mnt, flags) unmount(mnt,(flags == MNT_DETACH) ? MNT_FORCE : 0)
#endif

#define FUSERMOUNT_PROG "fusermount"
#define FUSERMOUNT3_PROG "fusermount3"
#define FUSE_COMMFD_ENV "_FUSE_COMMFD"

#ifndef MS_DIRSYNC
#define MS_DIRSYNC 128
#endif

enum
  {
    KEY_KERN_FLAG,
    KEY_KERN_OPT,
    KEY_FUSERMOUNT_OPT,
    KEY_SUBTYPE_OPT,
    KEY_MTAB_OPT,
    KEY_RO
  };

struct mount_opts
{
  int allow_other;
  int flags;
  int auto_unmount;
  int blkdev;
  char *fsname;
  char *subtype;
  char *subtype_opt;
  char *mtab_opts;
  char *fusermount_opts;
  char *kernel_opts;
};

#define FUSE_MOUNT_OPT(t,p) { t, offsetof(struct mount_opts,p), 1 }

static const struct fuse_opt fuse_mount_opts[] =
  {
    FUSE_MOUNT_OPT("allow_other",allow_other),
    FUSE_MOUNT_OPT("blkdev",blkdev),
    FUSE_MOUNT_OPT("auto_unmount",auto_unmount),
    FUSE_MOUNT_OPT("fsname=%s",fsname),
    FUSE_MOUNT_OPT("subtype=%s",subtype),
    FUSE_OPT_KEY("allow_other",KEY_KERN_OPT),
    FUSE_OPT_KEY("auto_unmount",KEY_FUSERMOUNT_OPT),
    FUSE_OPT_KEY("blkdev",KEY_FUSERMOUNT_OPT),
    FUSE_OPT_KEY("fsname=",KEY_FUSERMOUNT_OPT),
    FUSE_OPT_KEY("subtype=",KEY_SUBTYPE_OPT),
    FUSE_OPT_KEY("large_read",KEY_KERN_OPT),
    FUSE_OPT_KEY("blksize=",KEY_KERN_OPT),
    FUSE_OPT_KEY("default_permissions",KEY_KERN_OPT),
    FUSE_OPT_KEY("context=",KEY_KERN_OPT),
    FUSE_OPT_KEY("fscontext=",KEY_KERN_OPT),
    FUSE_OPT_KEY("defcontext=",KEY_KERN_OPT),
    FUSE_OPT_KEY("rootcontext=",KEY_KERN_OPT),
    FUSE_OPT_KEY("max_read=",KEY_KERN_OPT),
    FUSE_OPT_KEY("max_read=",FUSE_OPT_KEY_KEEP),
    FUSE_OPT_KEY("user=",KEY_MTAB_OPT),
    FUSE_OPT_KEY("-n",KEY_MTAB_OPT),
    FUSE_OPT_KEY("-r",KEY_RO),
    FUSE_OPT_KEY("ro",KEY_KERN_FLAG),
    FUSE_OPT_KEY("rw",KEY_KERN_FLAG),
    FUSE_OPT_KEY("suid",KEY_KERN_FLAG),
    FUSE_OPT_KEY("nosuid",KEY_KERN_FLAG),
    FUSE_OPT_KEY("dev",KEY_KERN_FLAG),
    FUSE_OPT_KEY("nodev",KEY_KERN_FLAG),
    FUSE_OPT_KEY("exec",KEY_KERN_FLAG),
    FUSE_OPT_KEY("noexec",KEY_KERN_FLAG),
    FUSE_OPT_KEY("async",KEY_KERN_FLAG),
    FUSE_OPT_KEY("sync",KEY_KERN_FLAG),
    FUSE_OPT_KEY("dirsync",KEY_KERN_FLAG),
    FUSE_OPT_KEY("atime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("noatime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("relatime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("norelatime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("lazytime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("nolazytime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("diratime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("nodiratime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("strictatime",KEY_KERN_FLAG),
    FUSE_OPT_KEY("nostrictatime",KEY_KERN_FLAG),
    FUSE_OPT_END
  };

static void
exec_fusermount(const char *argv[])
{
  execv(FUSERMOUNT_DIR "/mergerfs-" FUSERMOUNT_PROG,(char**)argv);
  execvp("mergerfs-" FUSERMOUNT_PROG,(char**)argv);
  execv(FUSERMOUNT_DIR "/" FUSERMOUNT_PROG,(char**)argv);
  execvp(FUSERMOUNT_PROG,(char**)argv);
  execv(FUSERMOUNT_DIR "/" FUSERMOUNT3_PROG,(char**)argv);
  execvp(FUSERMOUNT3_PROG,(char**)argv);
}

struct mount_flags
{
  const char *opt;
  unsigned long flag;
  int on;
};

static const struct mount_flags mount_flags[] =
  {
    { "rw", MS_RDONLY, 0 },
    { "ro", MS_RDONLY, 1 },
    { "suid", MS_NOSUID, 0 },
    { "nosuid", MS_NOSUID, 1 },
    { "dev", MS_NODEV, 0 },
    { "nodev", MS_NODEV, 1 },
    { "exec", MS_NOEXEC, 0 },
    { "noexec", MS_NOEXEC, 1 },
    { "async", MS_SYNCHRONOUS, 0 },
    { "sync", MS_SYNCHRONOUS, 1 },
    { "atime", MS_NOATIME, 0 },
    { "noatime", MS_NOATIME, 1 },
    { "relatime", MS_RELATIME, 1 },
    { "norelatime", MS_RELATIME, 0 },
    { "lazytime", MS_LAZYTIME, 1 },
    { "nolazytime", MS_LAZYTIME, 0 },
    { "diratime", MS_NODIRATIME, 0 },
    { "nodiratime", MS_NODIRATIME, 1 },
    { "strictatime", MS_STRICTATIME, 1 },
    { "nostrictatime", MS_STRICTATIME, 0 },
#ifndef __NetBSD__
    { "dirsync", MS_DIRSYNC, 1 },
#endif
    { NULL, 0, 0 }
  };

static
void
set_mount_flag(const char *s,
               int        *flags)
{
  int i;

  for(i = 0; mount_flags[i].opt != NULL; i++)
    {
      const char *opt = mount_flags[i].opt;
      if(strcmp(opt, s) == 0)
        {
          if(mount_flags[i].on)
            *flags |= mount_flags[i].flag;
          else
            *flags &= ~mount_flags[i].flag;
          return;
        }
    }

  fprintf(stderr, "mergerfs: internal error, can't find mount flag\n");
  abort();
}

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_FLAG:
      set_mount_flag(arg, &mo->flags);
      return 0;

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

    case KEY_FUSERMOUNT_OPT:
      return fuse_opt_add_opt_escaped(&mo->fusermount_opts, arg);

    case KEY_SUBTYPE_OPT:
      return fuse_opt_add_opt(&mo->subtype_opt, arg);

    case KEY_MTAB_OPT:
      return fuse_opt_add_opt(&mo->mtab_opts, arg);

    case FUSE_OPT_KEY_OPT:
      if(strncmp("x-", arg, 2) == 0)
        return fuse_opt_add_opt(&mo->mtab_opts, arg);
      return 1;
    }

  /* passthrough unknown options */
  return 1;
}

/* return value:
 * >= 0	 => fd
 * -1	 => error
 */
static
int
receive_fd(int fd)
{
  struct msghdr msg;
  struct iovec iov;
  char buf[1];
  int rv;
  size_t ccmsg[CMSG_SPACE(sizeof(int)) / sizeof(size_t)];
  struct cmsghdr *cmsg;

  iov.iov_base = buf;
  iov.iov_len = 1;

  memset(&msg, 0, sizeof(msg));
  msg.msg_name = 0;
  msg.msg_namelen = 0;
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  /* old BSD implementations should use msg_accrights instead of
   * msg_control; the interface is different. */
  msg.msg_control = ccmsg;
  msg.msg_controllen = sizeof(ccmsg);

  while(((rv = recvmsg(fd, &msg, 0)) == -1) && errno == EINTR)
    ;
  if(rv == -1)
    {
      perror("recvmsg");
      return -1;
    }
  if(!rv)
    {
      /* EOF */
      return -1;
    }

  cmsg = CMSG_FIRSTHDR(&msg);
  if(!cmsg)
    {
      fprintf(stderr,
              "no control message received or not enough buf space\n");
      return -1;
    }

  if(cmsg->cmsg_type != SCM_RIGHTS)
    {
      fprintf(stderr, "got control message of unknown type %d\n",
              cmsg->cmsg_type);
      return -1;
    }

  return *reinterpret_cast<int *>(CMSG_DATA(cmsg));
}

void
fuse_kern_unmount(const char *mountpoint,
                  int         fd)
{
  int res;
  int pid;

  if(!mountpoint)
    return;

  if(fd != -1)
    {
      struct pollfd pfd;

      pfd.fd = fd;
      pfd.events = 0;
      res = poll(&pfd, 1, 0);

      /* Need to close file descriptor, otherwise synchronous umount
         would recurse into filesystem, and deadlock.

         Caller expects fuse_kern_unmount to close the fd, so close it
         anyway. */
      close(fd);

      /* If file poll returns POLLERR on the device file descriptor,
         then the filesystem is already unmounted */
      if(res == 1 &&(pfd.revents & POLLERR))
        return;
    }

  if(geteuid() == 0)
    {
      fuse_mnt_umount("fuse", mountpoint, mountpoint, 1);
      return;
    }

  res = umount2(mountpoint,MNT_DETACH);
  if(res == 0)
    return;

  pid = fork();
  if(pid == -1)
    return;

  if(pid == 0)
    {
      const char *argv[] =
        { FUSERMOUNT_PROG, "-u", "-q", "-z", "--", mountpoint, NULL };

      exec_fusermount(argv);
      perror("mergerfs: failed to exec fusermount");
      _exit(1);
    }
  waitpid(pid, NULL, 0);
}

static
int
fuse_mount_fusermount(const char        *mountpoint,
                      struct mount_opts *mo,
                      const char        *opts,
                      int                quiet)
{
  int fds[2];
  int pid;
  int res;
  int rv;

  if(!mountpoint)
    {
      fprintf(stderr, "mergerfs: missing mountpoint parameter\n");
      return -1;
    }

  res = socketpair(PF_UNIX, SOCK_STREAM, 0, fds);
  if(res == -1)
    {
      perror("mergerfs: socketpair() failed");
      return -1;
    }

  pid = fork();
  if(pid == -1)
    {
      perror("mergerfs: fork() failed");
      close(fds[0]);
      close(fds[1]);
      return -1;
    }

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

      if(quiet)
        {
          int fd = open("/dev/null", O_RDONLY);
          if(fd != -1)
            {
              dup2(fd, 1);
              dup2(fd, 2);
            }
        }

      argv[a++] = FUSERMOUNT_PROG;
      if(opts)
        {
          argv[a++] = "-o";
          argv[a++] = opts;
        }
      argv[a++] = "--";
      argv[a++] = mountpoint;
      argv[a++] = NULL;

      close(fds[1]);
      fcntl(fds[0], F_SETFD, 0);
      snprintf(env, sizeof(env), "%i", fds[0]);
      setenv(FUSE_COMMFD_ENV, env, 1);
      exec_fusermount(argv);
      perror("mergerfs: failed to exec fusermount");
      _exit(1);
    }

  close(fds[0]);
  rv = receive_fd(fds[1]);

  if(!mo->auto_unmount)
    {
      /* with auto_unmount option fusermount will not exit until
         this socket is closed */
      close(fds[1]);
      waitpid(pid, NULL, 0); /* bury zombie */
    }

  return rv;
}

static int
fuse_mount_sys(const char *mnt, struct mount_opts *mo, const char *mnt_opts)
{
  int fd;
  int res;
  char tmp[128];
  const char *devname = "/dev/fuse";
  std::string source;
  std::string type;
  struct stat stbuf;

  if(!mnt)
    {
      fprintf(stderr, "mergerfs: missing mountpoint parameter\n");
      return -1;
    }

  res = stat(mnt, &stbuf);
  if(res == -1)
    {
      switch(errno)
        {
        case ENOTCONN:
          fprintf(stderr,
                  "mergerfs: mountpoint %s appears to be a broken FUSE mount; "
                  "unmounting it\n",
                  mnt);
          res = umount(mnt);
          if(res == 0)
            fprintf(stderr, "mergerfs: umount succeeded");
          break;
        default:
          fprintf(stderr,
                  "mergerfs: failed to access mountpoint %s: %s; "
                  "attempting to mount anyway\n",
                  mnt,
                  strerror(errno));
          break;
        }

      memset(&stbuf, 0, sizeof(stbuf));
      res = stat(mnt,&stbuf);
      if(res == -1)
        stbuf.st_mode = S_IFDIR;
    }

  if(mo->auto_unmount)
    {
      /* Tell the caller to fallback to fusermount because
         auto-unmount does not work otherwise. */
      return -2;
    }

  fd = open(devname, O_RDWR);
  if(fd == -1)
    {
      if(errno == ENODEV || errno == ENOENT)
        fprintf(stderr,
                "mergerfs: device not found, try 'modprobe fuse' first\n");
      else
        fprintf(stderr, "mergerfs: failed to open %s: %s\n", devname,
                strerror(errno));
      return -1;
    }

  snprintf(tmp,
           sizeof(tmp),
           "fd=%i,rootmode=%o,user_id=%u,group_id=%u",
           fd,
           (stbuf.st_mode & S_IFMT),
           getuid(),
           getgid());

  res = fuse_opt_add_opt(&mo->kernel_opts, tmp);
  if(res == -1)
    goto out_close;

  type = mo->blkdev ? "fuseblk" : "fuse";
  if(mo->subtype)
    type += "." + std::string(mo->subtype);

  source = mo->fsname ? mo->fsname :(mo->subtype ? mo->subtype : devname);

  res = mount(source.c_str(), mnt, type.c_str(), mo->flags,
              mo->kernel_opts);
  if(res == -1 && errno == ENODEV && mo->subtype)
    {
      /* Probably missing subtype support */
      type = mo->blkdev ? "fuseblk" : "fuse";
      if(mo->fsname)
        {
          if(!mo->blkdev)
            source = std::string(mo->subtype) + "#" + mo->fsname;
        }
      else
        {
          source = type;
        }
      res = mount(source.c_str(), mnt, type.c_str(), mo->flags,
                  mo->kernel_opts);
    }
  if(res == -1)
    {
      /*
       * Maybe kernel doesn't support unprivileged mounts, in this
       * case try falling back to fusermount
       */
      if(errno == EPERM)
        {
          res = -2;
        }
      else
        {
          int errno_save = errno;
          if(mo->blkdev && errno == ENODEV && !fuse_mnt_check_fuseblk())
            {
              fprintf(stderr,"mergerfs: 'fuseblk' support missing\n");
            }
          else
            {
              fprintf(stderr,
                      "mergerfs: mount failed: %s\n",
                      strerror(errno_save));
            }
        }

      goto out_close;
    }

#ifndef __NetBSD__
#ifndef IGNORE_MTAB
  if(geteuid() == 0)
    {
      char *newmnt = fuse_mnt_resolve_path("fuse", mnt);
      res = -1;
      if(!newmnt)
        goto out_umount;

      res = fuse_mnt_add_mount("fuse", source.c_str(), newmnt, type.c_str(),
                               mnt_opts);
      free(newmnt);
      if(res == -1)
        goto out_umount;
    }
#endif /* IGNORE_MTAB */
#endif /* __NetBSD__ */

  return fd;

 out_umount:
  umount2(mnt,MNT_DETACH); /* lazy umount */
 out_close:
  close(fd);
  return res;
}

static int
get_mnt_flag_opts(char **mnt_optsp, int flags)
{
  int i;

  if(!(flags & MS_RDONLY) && fuse_opt_add_opt(mnt_optsp, "rw") == -1)
    return -1;

  for(i = 0; mount_flags[i].opt != NULL; i++)
    {
      if(mount_flags[i].on &&(flags & mount_flags[i].flag)
         && fuse_opt_add_opt(mnt_optsp, mount_flags[i].opt) == -1)
        return -1;
    }
  return 0;
}

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

  memset(&mo, 0, sizeof(mo));
  mo.flags = MS_NOSUID | MS_NODEV;

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

  res = -1;
  if(get_mnt_flag_opts(&mnt_opts, mo.flags) == -1)
    goto out;
  if(mo.kernel_opts && fuse_opt_add_opt(&mnt_opts, mo.kernel_opts) == -1)
    goto out;
  if(mo.mtab_opts && fuse_opt_add_opt(&mnt_opts, mo.mtab_opts) == -1)
    goto out;

  res = fuse_mount_sys(mountpoint, &mo, mnt_opts);
  if(res == -2)
    {
      if(mo.fusermount_opts
         && fuse_opt_add_opt(&mnt_opts, mo.fusermount_opts) == -1)
        goto out;

      if(mo.subtype)
        {
          char *tmp_opts = NULL;

          res = -1;
          if((fuse_opt_add_opt(&tmp_opts, mnt_opts) == -1) ||
             (fuse_opt_add_opt(&tmp_opts, mo.subtype_opt) == -1))
            {
              free(tmp_opts);
              goto out;
            }

          res = fuse_mount_fusermount(mountpoint, &mo, tmp_opts, 1);
          free(tmp_opts);
          if(res == -1)
            res = fuse_mount_fusermount(mountpoint, &mo, mnt_opts, 0);
        }
      else
        {
          res = fuse_mount_fusermount(mountpoint, &mo, mnt_opts, 0);
        }
    }
 out:
  free(mnt_opts);
  free(mo.fsname);
  free(mo.subtype);
  free(mo.fusermount_opts);
  free(mo.subtype_opt);
  free(mo.kernel_opts);
  free(mo.mtab_opts);
  return res;
}
