#define _GNU_SOURCE
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <string.h>
#include "contain.h"

static char *root;

static void bindnode(char *src, char *dst) {
  int fd;

  if ((fd = open(dst, O_WRONLY | O_CREAT, 0600)) >= 0)
    close(fd);
  if (mount(src, dst, NULL, MS_BIND, NULL) < 0)
    error(1, 0, "Failed to bind '%s' into '%s'", src, dst);
}

void cleanup(void) {
  if (root) {
    umount2(root, MNT_DETACH);
    rmdir(root);
  }
}

static char *binditem(char *b, char **s, char **d) {
  char *orig = b;

  while (b && *b && strchr(",;", *b))
    b++;
  if (b == NULL || *b == '\0')
    return NULL;
  *s = b;
  while (*b && *b != ':')
    b++;
  if (*b != ':')
    error(1, 0, "Invalid bind format '%s'", orig);
  *b++ = '\0';
  *d = b;
  while (*b && !strchr(",;:", *b))
    b++;
  if (*b == ':')
    error(1, 0, "Invalid bind format '%s'", orig);
  if (*b)
    *b++ = '\0';
  return b;
}

void createroot(char *src, int console, char *helper, char *bind) {
  mode_t mask;
  pid_t child;
  char *bindsrc = NULL, *binddst = NULL;

  root = tmpdir();
  atexit(cleanup);

  if (mount(src, root, NULL, MS_BIND | MS_REC, NULL) < 0)
    error(1, 0, "Failed to bind new root filesystem");
  else if (chdir(root) < 0)
    error(1, 0, "Failed to enter new root filesystem");

  mask = umask(0);
  mkdir("dev" , 0755);
  if (mount("tmpfs", "dev", "tmpfs", 0, "mode=0755") < 0)
    error(1, 0, "Failed to mount /dev tmpfs in new root filesystem");

  mkdir("dev/pts", 0755);
  if (mount("devpts", "dev/pts", "devpts", 0, "newinstance,ptmxmode=666") < 0)
    error(1, 0, "Failed to mount /dev/pts in new root filesystem");

  mkdir("dev/tmp", 0755);
  umask(mask);

  if (console >= 0)
    bindnode(ptsname(console), "dev/console");
  bindnode("/dev/full", "dev/full");
  bindnode("/dev/null", "dev/null");
  bindnode("/dev/random", "dev/random");
  bindnode("/dev/tty", "dev/tty");
  bindnode("/dev/urandom", "dev/urandom");
  bindnode("/dev/zero", "dev/zero");
  symlink("pts/ptmx", "dev/ptmx");

  mkdir("run/shm" , 0777);
  if (mount("tmpfs", "run/shm", "tmpfs", 0, "mode=0777") < 0)
    error(1, 0, "Failed to mount /run/shm tmpfs in new root filesystem");
  symlink("/run/shm", "dev/shm");


  while ((bind = binditem(bind, &bindsrc, &binddst)))
    bindnode(bindsrc, binddst);

  if (helper)
    switch (child = fork()) {
      case -1:
        error(1, errno, "fork");
      case 0:
        execlp(SHELL, SHELL, "-c", helper, NULL);
        error(1, errno, "exec %s", helper);
      default:
        waitforexit(child);
    }
}

void enterroot(void) {
  if (syscall(__NR_pivot_root, ".", "dev/tmp") < 0)
    error(1, 0, "Failed to pivot into new root filesystem");

  if (chdir("/dev/tmp") >= 0) {
    while (*root == '/')
      root++;
    rmdir(root);
  }

  root = NULL;

  if (chdir("/") < 0 || umount2("/dev/tmp", MNT_DETACH) < 0)
    error(1, 0, "Failed to detach old root filesystem");
  else
    rmdir("/dev/tmp");
}

void mountproc(void) {
  mode_t mask;

  mask = umask(0);
  mkdir("proc" , 0755);
  umask(mask);

  if (mount("proc", "proc", "proc", 0, NULL) < 0)
    error(1, 0, "Failed to mount /proc in new root filesystem");
}

void mountsys(void) {
  mode_t mask;

  mask = umask(0);
  mkdir("sys" , 0755);
  umask(mask);

  if (mount("sysfs", "sys", "sysfs", 0, NULL) < 0)
    error(1, 0, "Failed to mount /sys in new root filesystem");
}