/* mount.cc

This file is part of Cygwin.

This software is a copyrighted work licensed under the terms of the
Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
details. */

#include <stdio.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <mntent.h>
#include <windows.h>
#include <sys/cygwin.h>
#include <cygwin/version.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <dirent.h>
#include "path.h"

#include <errno.h>

#define NT_MAX_PATH 32768

#define EXEC_FLAGS (MOUNT_EXEC | MOUNT_NOTEXEC | MOUNT_CYGWIN_EXEC)

static void mount_entries (void);
static void show_mounts (void);
static void show_cygdrive_info (void);
static void change_cygdrive_prefix (const char *new_prefix, int flags);
static int mount_already_exists (const char *posix_path, int flags);

// static short create_missing_dirs = FALSE;
static bool force = false;

static const char *progname;

static void
error (const char *path)
{
  fprintf (stderr, "%s: %s: %s\n", progname, path,
	   (errno == EMFILE) ? "Too many mount entries" : strerror (errno));
  exit (1);
}

/* FIXME: do_mount should also print a warning message if the dev arg
   is a non-existent Win32 path. */

static void
do_mount (const char *dev, const char *where, int flags)
{
  struct stat statbuf;
  int statres;

  statres = stat (where, &statbuf);

#if 0
  if (statres == -1)
    {
      /* FIXME: this'll fail if mount dir is missing any parent dirs */
      if (create_missing_dirs == TRUE)
	{
	  if (mkdir (where, 0755) == -1)
	    fprintf (stderr, "Warning: unable to create %s!\n", where);
	  else
	    statres = 0; /* Pretend stat succeeded if we could mkdir. */
	}
    }
#endif

  if (statres == -1)
    {
      if (!force)
	fprintf (stderr, "%s: warning - %s does not exist.\n", progname, where);
    }
  else if (!(statbuf.st_mode & S_IFDIR))
    {
      if (!force)
	fprintf (stderr, "%s: warning: %s is not a directory.\n",
		 progname, where);
    }

  if (!force && !(flags & (EXEC_FLAGS | MOUNT_BIND)) && strlen (dev))
    {
      char devtmp[1 + 2 * strlen (dev)];
      strcpy (devtmp, dev);
      char c = strchr (devtmp, '\0')[-1];
      if (c == '/' || c == '\\')
	strcat (devtmp, ".");
      /* Use a curious property of Windows which allows the use of \.. even
	 on non-directory paths. */
      for (const char *p = dev; (p = strpbrk (p, "/\\")); p++)
	strcat (devtmp, "\\..");
      strcat (devtmp, "\\");
      if (GetDriveType (devtmp) == DRIVE_REMOTE)
	{
	  fprintf (stderr,
      "%s: defaulting to 'notexec' mount option for speed since native path\n"
      "%*creferences a remote share.  Use '-f' option to override.\n",
		   progname, (int) strlen(progname) + 2, ' ');
	  flags |= MOUNT_NOTEXEC;
	}
    }

  if (mount (dev, where, flags))
    error (where);
}

static void
from_fstab (bool user)
{
  char path[PATH_MAX];
  char buf[65536];
  mnt_t *m = mount_table + max_mount_entry;

  strcpy (path, "/etc/fstab");
  if (user)
    {
      strcat (path, ".d/");
      strcat (path, getlogin ());
    }
  FILE *fh = fopen (path, "rt");
  if (!fh)
    return;
  while (fgets (buf, 65536, fh))
    {
      char *c = strrchr (buf, '\n');
      if (c)
      	*c = '\0';
      if (from_fstab_line (m, buf, user))
	++m;
    }
  max_mount_entry = m - mount_table;
  fclose (fh);
}

static void
do_mount_from_fstab (const char *where)
{
  force = true;
  /* Read fstab entries. */
  from_fstab (false);
  from_fstab (true);
  /* Loop through fstab entries and see if it matches `where'.  If `where'
     is NULL, all entries match. */
  bool exists = false;
  for (mnt_t *m = mount_table; m - mount_table < max_mount_entry; ++m)
    if (!where || !strcmp (where, m->posix))
      {
	if (m->flags & MOUNT_CYGDRIVE)
	  {
	    /* Get the cygdrive info */
	    char user[MAX_PATH];
	    char system[MAX_PATH];
	    char user_flags[MAX_PATH];
	    char system_flags[MAX_PATH];

	    exists = true;
	    cygwin_internal (CW_GET_CYGDRIVE_INFO, user, system, user_flags,
			     system_flags);
	    if ((*user && strcmp (user, m->posix) != 0)
		|| (*system && strcmp (system, m->posix) != 0))
	      if (mount (NULL, m->posix, m->flags))
		error (m->posix);
	  }
	else
	  {
	    exists = true;
	    /* Compare with existing mount table.  If the entry doesn't exist,
	       mount it. */
	    FILE *mt = setmntent ("/-not-used-", "r");
	    struct mntent *p;

	    while ((p = getmntent (mt)) != NULL)
	      if (!strcmp (m->posix, p->mnt_dir))
		break;
	    if (!p)
	      do_mount (m->native, m->posix, m->flags);
	    endmntent (mt);
	    if (where)
	      break;
	  }
      }
  if (!exists && where)
    fprintf (stderr,
	     "%s: can't find %s in /etc/fstab or in /etc/fstab.d/$USER\n",
	     progname, where);
}

static struct option longopts[] =
{
  {"all", no_argument, NULL, 'a'},
  {"change-cygdrive-prefix", no_argument, NULL, 'c'},
  {"force", no_argument, NULL, 'f'},
  {"help", no_argument, NULL, 'h' },
  {"mount-entries", no_argument, NULL, 'm'},
  {"options", required_argument, NULL, 'o'},
  {"show-cygdrive-prefix", no_argument, NULL, 'p'},
  {"version", no_argument, NULL, 'V'},
  {NULL, 0, NULL, 0}
};

static char opts[] = "acfhmpVo:";

static void __attribute__ ((__noreturn__))
usage (FILE *where = stderr)
{
  char *options;

  fprintf (where, "Usage: %1$s [OPTION] [<win32path> <posixpath>]\n\
       %1$s -a\n\
       %1$s <posixpath>\n\
\n\
Display information about mounted filesystems, or mount a filesystem\n\
\n\
  -a, --all                     mount all filesystems mentioned in fstab\n\
  -c, --change-cygdrive-prefix  change the cygdrive path prefix to <posixpath>\n\
  -f, --force                   force mount, don't warn about missing mount\n\
				point directories\n\
  -h, --help                    output usage information and exit\n\
  -m, --mount-entries           write fstab entries to replicate mount points\n\
				and cygdrive prefixes\n\
  -o, --options X[,X...]	specify mount options\n\
  -p, --show-cygdrive-prefix    show user and/or system cygdrive path prefix\n\
  -V, --version                 output version information and exit\n\n",
  progname);
  if (!cygwin_internal (CW_LST_MNT_OPTS, &options))
    fprintf (where, "Valid options are: %s\n\n", options);
  exit (where == stderr ? 1 : 0);
}

static void
print_version ()
{
  printf ("mount (cygwin) %d.%d.%d\n"
	  "Mount filesystem utility\n"
	  "Copyright (C) 1996 - %s Cygwin Authors\n"
	  "This is free software; see the source for copying conditions.  There is NO\n"
	  "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n",
	  CYGWIN_VERSION_DLL_MAJOR / 1000,
	  CYGWIN_VERSION_DLL_MAJOR % 1000,
	  CYGWIN_VERSION_DLL_MINOR,
	  strrchr (__DATE__, ' ') + 1);
}

static char *
concat3 (char *a, const char *b, const char *c)
{
  size_t totlen = strlen (a) + strlen (b) + strlen (c) + 1;
  a = (char *) realloc (a, totlen);
  return strcat (strcat (a, b), c);
}

int
main (int argc, char **argv)
{
  int i;
  int flags = 0;
  char *options = strdup ("");
  enum do_what
  {
    nada,
    saw_change_cygdrive_prefix,
    saw_show_cygdrive_prefix,
    saw_mount_commands,
    saw_mount_all,
  } do_what = nada;

  progname = program_invocation_short_name;

  if (argc == 1)
    {
      show_mounts ();
      exit (0);
    }

  while ((i = getopt_long (argc, argv, opts, longopts, NULL)) != EOF)
    switch (i)
      {
      case 'a':
	if (do_what == nada)
	  do_what = saw_mount_all;
	else
	  usage ();
	break;
      case 'c':
	if (do_what == nada)
	  do_what = saw_change_cygdrive_prefix;
	else
	  usage ();
	break;
      case 'f':
	force = true;
	break;
      case 'h':
	usage (stdout);
	break;
      case 'm':
	if (do_what == nada)
	  do_what = saw_mount_commands;
	else
	  usage ();
	break;
      case 'o':
	if (do_what == saw_mount_all)
	  usage ();
	else if (*options)
	  options = concat3 (options, ",", optarg);
	else
	  options = strdup (optarg);
	break;
      case 'p':
	if (do_what == nada)
	  do_what = saw_show_cygdrive_prefix;
	else
	  usage ();
	break;
      case 'V':
	print_version ();
	return 0;
	break;
      default:
	fprintf (stderr, "Try `%s --help' for more information.\n", progname);
	return 1;
      }

  if (cygwin_internal (CW_CVT_MNT_OPTS, &options, &flags))
    {
      fprintf (stderr, "%s: invalid option - '%s'\n", progname, options);
      exit (1);
    }

  if (flags & MOUNT_NOTEXEC && flags & (MOUNT_EXEC | MOUNT_CYGWIN_EXEC))
    {
      fprintf (stderr, "%s: invalid combination of executable options\n",
	       progname);
      exit (1);
    }

  cygwin_internal (CW_SET_DOS_FILE_WARNING, false);

  argc--;
  switch (do_what)
    {
    case saw_change_cygdrive_prefix:
      if (optind != argc)
	usage ();
      change_cygdrive_prefix (argv[optind], flags);
      break;
    case saw_show_cygdrive_prefix:
      if (optind <= argc)
	usage ();
      show_cygdrive_info ();
      break;
    case saw_mount_commands:
      if (optind <= argc)
	usage ();
      mount_entries ();
      break;
    case saw_mount_all:
      if (optind <= argc)
	usage ();
      do_mount_from_fstab (NULL);
      break;
    default:
      if (optind == argc)
	do_mount_from_fstab (argv[optind]);
      else if (optind != (argc - 1))
	{
	  fprintf (stderr, "%s: too many arguments\n", progname);
	  usage ();
	}
      else if (force || !mount_already_exists (argv[optind + 1], flags))
	do_mount (argv[optind], argv[optind + 1], flags);
      else
	{
	  errno = EBUSY;
	  error (argv[optind + 1]);
	}
    }

  /* NOTREACHED */
  return 0;
}

static char *
convert_spaces (char *tgt, const char *src)
{
  char *tp, *spacep;
  const char *sp;

  tp = tgt;
  for (sp = src; (spacep = strchr (sp, ' ')); sp = spacep + 1)
    {
      tp = stpncpy (tp, sp, spacep - sp);
      tp = stpcpy (tp, "\\040");
    }
  stpcpy (tp, sp);
  return tgt;
}

static void
mount_entries (void)
{
  FILE *m = setmntent ("/-not-used-", "r");
  struct mntent *p;
  const char *format_mnt = "%s %s %s %s 0 0\n";
  const char *format_cyg = "none %s cygdrive %s 0 0\n";

  // write fstab entries for normal mount points
  while ((p = getmntent (m)) != NULL)
    // Only list non-cygdrives and non-automounts
    if (!strstr (p->mnt_opts, ",noumount") && !strstr (p->mnt_opts, ",auto"))
      {
	char fsname[NT_MAX_PATH], dirname[NT_MAX_PATH];
	/* Drop the "bind" option since it can't be reverted. */
	char *c = strstr (p->mnt_opts, ",bind");
	if (c)
	  memmove (c, c + 5, strlen (c + 5) + 1);
	printf (format_mnt, convert_spaces (fsname, p->mnt_fsname),
			    convert_spaces (dirname, p->mnt_dir),
			    p->mnt_type, p->mnt_opts);
      }
  endmntent (m);

  // write fstab entry for cygdrive prefix
  m = setmntent ("/-not-used-", "r");
  while ((p = getmntent (m)) != NULL)
    {
      char *noumount;
      if ((noumount = strstr (p->mnt_opts, ",noumount")))
	{
	  char dirname[NT_MAX_PATH];
	  char opts[strlen (p->mnt_opts) + 1];

	  convert_spaces (dirname, p->mnt_dir);
	  // remove trailing slash
	  char *ls = strrchr (dirname, '/');
	  if (ls)
	    {
	      // last slash == leading slash?  cygdrive prefix == "/"
	      if (ls == dirname)
		++ls;
	      *ls = '\0';
	    }
	  *stpncpy (opts, p->mnt_opts, noumount - p->mnt_opts) = '\0';
	  printf (format_cyg, dirname, opts);
	  break;
	}
    }
  endmntent (m);

  exit(0);
}

static void
show_mounts (void)
{
  FILE *m = setmntent ("/-not-used-", "r");
  struct mntent *p;
  const char *format = "%s on %s type %s (%s)\n";

  // printf (format, "Device", "Directory", "Type", "Flags");
  while ((p = getmntent (m)) != NULL)
    printf (format, p->mnt_fsname, p->mnt_dir, p->mnt_type, p->mnt_opts);
  endmntent (m);
}

/* Return 1 if mountpoint from the same registry area is already in
   mount table.  Otherwise return 0. */
static int
mount_already_exists (const char *posix_path, int flags)
{
  int found_matching = 0;

  FILE *m = setmntent ("/-not-used-", "r");
  struct mntent *p;

  while ((p = getmntent (m)) != NULL)
    {
      /* if the paths match, and they're both the same type of mount. */
      if (strcmp (p->mnt_dir, posix_path) == 0)
	{
	  if (p->mnt_type[0] == 'u')
	    {
	      if (!(flags & MOUNT_SYSTEM)) /* both current_user */
		found_matching = 1;
	      else
		fprintf (stderr,
			 "%s: warning: system mount point of '%s' "
			 "will always be masked by user mount.\n",
			 progname, posix_path);
	      break;
	    }
	  else if (p->mnt_type[0] == 's')
	    {
	      if (flags & MOUNT_SYSTEM) /* both system */
		found_matching = 1;
	      else
		fprintf (stderr,
			 "%s: warning: user mount point of '%s' "
			 "masks system mount.\n", progname, posix_path);
	      break;
	    }
	  else
	    {
	      fprintf (stderr, "%s: warning: couldn't determine mount type.\n",
		       progname);
	      break;
	    }
	}
    }
  endmntent (m);

  return found_matching;
}

/* change_cygdrive_prefix: Change the cygdrive prefix */
static void
change_cygdrive_prefix (const char *new_prefix, int flags)
{
  flags |= MOUNT_CYGDRIVE;

  if (mount (NULL, new_prefix, flags))
    error (new_prefix);

  exit (0);
}

/* show_cygdrive_info: Show the user and/or cygdrive info, i.e., prefix and
   flags.*/
static void
show_cygdrive_info ()
{
  /* Get the cygdrive info */
  char user[MAX_PATH];
  char system[MAX_PATH];
  char user_flags[MAX_PATH];
  char system_flags[MAX_PATH];
  cygwin_internal (CW_GET_CYGDRIVE_INFO, user, system, user_flags,
		   system_flags);

  /* Display the user and system cygdrive path prefix, if necessary
     (ie, not empty) */
  const char *format = "%-18s  %-11s  %s\n";
  printf (format, "Prefix", "Type", "Flags");
  if (strlen (user) > 0)
    printf (format, user, "user", user_flags);
  if (strlen (system) > 0)
    printf (format, system, "nouser", system_flags);

  exit (0);
}
