/* mount.cc: mount handling.

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 "winsup.h"
#include "miscfuncs.h"
#include <mntent.h>
#include <ctype.h>
#include <winioctl.h>
#include <cygwin/version.h>
#include "cygerrno.h"
#include "security.h"
#include "path.h"
#include "shared_info.h"
#include "fhandler.h"
#include "dtable.h"
#include "cygheap.h"
#include "cygtls.h"
#include "tls_pbuf.h"
#include <ntdll.h>
#include <wchar.h>
#include <stdio.h>
#include <assert.h>

/* Determine if path prefix matches current cygdrive */
#define iscygdrive(path) \
  (path_prefix_p (mount_table->cygdrive, (path), mount_table->cygdrive_len, false))

#define iscygdrive_device(path) \
  (isalpha (path[mount_table->cygdrive_len]) && \
   (path[mount_table->cygdrive_len + 1] == '/' || \
    !path[mount_table->cygdrive_len + 1]))

#define isproc(path) \
  (path_prefix_p (proc, (path), proc_len, false))

bool NO_COPY mount_info::got_usr_bin;
bool NO_COPY mount_info::got_usr_lib;
int NO_COPY mount_info::root_idx = -1;

/* is_native_path: Return non-zero if PATH starts with \??\[a-zA-Z] or
		   with \\?\[a-zA-Z] or with \\.\[a-zA-Z].

   is_unc_share: Return non-zero if PATH begins with //server/share
		 or with one of the native prefixes //./ or //?/

   This function is only used to test for valid input strings.
   The later normalization drops the native prefixes. */

/* list of invalid chars in server names.  Note that underscore is ok,
   but it cripples interoperability. */
const char _invalid_server_char[] = ",~:!@#$%^&'.(){} ";
#define valid_server_char(_c)	({				\
		const char __c = (_c);				\
		!iscntrl(__c)					\
		&& strchr (_invalid_server_char, (_c)) == NULL;	\
	})

/* list of invalid chars in UNC filenames.  These are a few more than
   for "normal" filenames. */
const char _invalid_share_char[] = "\"/\\[]:|<>+=;,?*";
#define valid_share_char(_c)	({				\
		const char __c = (_c);				\
		!iscntrl(__c)					\
		&& strchr (_invalid_share_char, __c) == NULL;	\
	})

static inline bool
is_native_path (const char *path)
{
  return isdirsep (path[0])
	 && (isdirsep (path[1]) || path[1] == '?')
	 && (path[2] == '?' || path[2] == '.')
	 && isdirsep (path[3])
	 && isalpha (path[4]);
}

static inline bool
is_unc_share (const char *path)
{
  const char *p;
  return (isdirsep (path[0])
	 && isdirsep (path[1])
	 && valid_server_char (path[2])
	 && ((p = strpbrk (path + 3, "\\/")) != NULL)
	 && valid_share_char (p[1]));
}

/* Return true if src_path is a valid, internally supported device name.
   In that case, win32_path gets the corresponding NT device name and
   dev is appropriately filled with device information. */

static bool
win32_device_name (const char *src_path, char *win32_path, device& dev)
{
  dev.parse (src_path);
  if (dev == FH_FS || dev == FH_DEV)
    return false;
  strcpy (win32_path, dev.native ());
  return true;
}

/* Beginning with Samba 3.0.28a, Samba allows to get version information using
   the ExtendedInfo member returned by a FileFsObjectIdInformation request.
   We just store the samba_version information for now.  Older versions than
   3.2 are still guessed at by testing the file system flags. */
#define SAMBA_EXTENDED_INFO_MAGIC 0x536d4261 /* "SmBa" */
#define SAMBA_EXTENDED_INFO_VERSION_STRING_LENGTH 28
#pragma pack(push,4)
struct smb_extended_info {
  DWORD		samba_magic;		/* Always SAMBA_EXTENDED_INFO_MAGIC */
  DWORD		samba_version;		/* Major/Minor/Release/Revision */
  DWORD		samba_subversion;	/* Prerelease/RC/Vendor patch */
  LARGE_INTEGER samba_gitcommitdate;
  char		samba_version_string[SAMBA_EXTENDED_INFO_VERSION_STRING_LENGTH];
};
#pragma pack(pop)

#define MAX_FS_INFO_CNT 32
class fs_info_cache
{
  static muto fsi_lock;
  uint32_t count;
  struct {
    fs_info fsi;
    uint32_t hash;
  } entry[MAX_FS_INFO_CNT];

  uint32_t genhash (PFILE_FS_VOLUME_INFORMATION);

public:
  fs_info_cache () : count (0) { fsi_lock.init ("fsi_lock"); }
  fs_info *search (PFILE_FS_VOLUME_INFORMATION, uint32_t &);
  void add (uint32_t, fs_info *);
};

static fs_info_cache fsi_cache;
muto NO_COPY fs_info_cache::fsi_lock;

uint32_t
fs_info_cache::genhash (PFILE_FS_VOLUME_INFORMATION pffvi)
{
  uint32_t hash = 0;
  const uint16_t *p = (const uint16_t *) pffvi;
  const uint16_t *end = (const uint16_t *)
			((const uint8_t *) p + sizeof *pffvi
			 + pffvi->VolumeLabelLength  - sizeof (WCHAR));
  pffvi->__dummy = 0;	/* This member can have random values! */
  while (p < end)
    hash = *p++ + (hash << 6) + (hash << 16) - hash;
  return hash;
}

fs_info *
fs_info_cache::search (PFILE_FS_VOLUME_INFORMATION pffvi, uint32_t &hash)
{
  hash = genhash (pffvi);
  for (uint32_t i = 0; i < count; ++i)
    if (entry[i].hash == hash)
      return &entry[i].fsi;
  return NULL;
}

void
fs_info_cache::add (uint32_t hashval, fs_info *new_fsi)
{
  fsi_lock.acquire ();
  if (count < MAX_FS_INFO_CNT)
    {
      entry[count].fsi = *new_fsi;
      entry[count].hash = hashval;
      ++count;
    }
  fsi_lock.release ();
}

bool
fs_info::update (PUNICODE_STRING upath, HANDLE in_vol)
{
  NTSTATUS status = STATUS_OBJECT_NAME_NOT_FOUND;
  HANDLE vol;
  OBJECT_ATTRIBUTES attr;
  IO_STATUS_BLOCK io;
  bool no_media = false;
  FILE_FS_DEVICE_INFORMATION ffdi;
  FILE_FS_OBJECTID_INFORMATION ffoi;
  struct {
    FILE_FS_ATTRIBUTE_INFORMATION ffai;
    WCHAR buf[NAME_MAX + 1];
  } ffai_buf;
  struct {
    FILE_FS_VOLUME_INFORMATION ffvi;
    WCHAR buf[NAME_MAX + 1];
  } ffvi_buf;
  UNICODE_STRING dir;
  UNICODE_STRING fsname;

  clear ();
  /* Always caseinsensitive.  We really just need access to the drive. */
  InitializeObjectAttributes (&attr, upath, OBJ_CASE_INSENSITIVE, NULL, NULL);
  if (in_vol)
    vol = in_vol;
  else
    {
      ULONG access = READ_CONTROL;
      /* Note: Don't use the FILE_OPEN_REPARSE_POINT flag here.  The reason
	 is that symlink_info::check relies on being able to open a handle
	 to the target of a volume mount point. */
      status = NtOpenFile (&vol, access, &attr, &io, FILE_SHARE_VALID_FLAGS,
			   FILE_OPEN_FOR_BACKUP_INTENT);
      /* At least one filesystem (HGFS, VMware shared folders) doesn't like
	 to be opened with access set to just READ_CONTROL. */
      if (status == STATUS_INVALID_PARAMETER)
	{
	  access |= FILE_READ_DATA;
	  status = NtOpenFile (&vol, access, &attr, &io, FILE_SHARE_VALID_FLAGS,
			       FILE_OPEN_FOR_BACKUP_INTENT);
	}
      while (!NT_SUCCESS (status)
	     && (attr.ObjectName->Length > 7 * sizeof (WCHAR)
		 || status == STATUS_NO_MEDIA_IN_DEVICE))
	{
	  RtlSplitUnicodePath (attr.ObjectName, &dir, NULL);
	  attr.ObjectName = &dir;
	  if (status == STATUS_NO_MEDIA_IN_DEVICE)
	    {
	      no_media = true;
	      dir.Length = 6 * sizeof (WCHAR);
	    }
	  else if (dir.Length > 7 * sizeof (WCHAR))
	    dir.Length -= sizeof (WCHAR);
	  status = NtOpenFile (&vol, access, &attr, &io, FILE_SHARE_VALID_FLAGS,
			       FILE_OPEN_FOR_BACKUP_INTENT);
	}
      if (!NT_SUCCESS (status))
	{
	  debug_printf ("Cannot access path %S, status %y",
			attr.ObjectName, status);
	  return false;
	}
    }
  sernum = 0;
  status = NtQueryVolumeInformationFile (vol, &io, &ffvi_buf.ffvi,
					 sizeof ffvi_buf,
					 FileFsVolumeInformation);
  uint32_t hash = 0;
  if (NT_SUCCESS (status))
    {
      /* If the FS doesn't return a valid serial number (PrlSF is a candidate),
	 create reproducible serial number from path.  We need this to create
	 a unique per-drive/share hash. */
      if (ffvi_buf.ffvi.VolumeSerialNumber == 0)
	{
	  UNICODE_STRING path_prefix;
	  WCHAR *p;

	  if (upath->Buffer[5] == L':' && upath->Buffer[6] == L'\\')
	    p = upath->Buffer + 6;
	  else
	    {
	      /* We're expecting an UNC path.  Move p to the backslash after
	         "\??\UNC\server\share" or the trailing NUL. */
	      p = upath->Buffer + 7;  /* Skip "\??\UNC" */
	      int bs_cnt = 0;

	      while (*++p)
		if (*p == L'\\')
		    if (++bs_cnt > 1)
		      break;
	    }
	  RtlInitCountedUnicodeString (&path_prefix, upath->Buffer,
				       (p - upath->Buffer) * sizeof (WCHAR));
	  ffvi_buf.ffvi.VolumeSerialNumber = hash_path_name ((ino_t) 0,
							     &path_prefix);
	}
      fs_info *fsi = fsi_cache.search (&ffvi_buf.ffvi, hash);
      if (fsi)
	{
	  *this = *fsi;
	  if (!in_vol)
	    NtClose (vol);
	  return true;
	}
      sernum = ffvi_buf.ffvi.VolumeSerialNumber;
    }
  status = NtQueryVolumeInformationFile (vol, &io, &ffdi, sizeof ffdi,
					 FileFsDeviceInformation);
  if (!NT_SUCCESS (status))
    ffdi.DeviceType = ffdi.Characteristics = 0;

  if ((ffdi.Characteristics & FILE_REMOTE_DEVICE)
      || (!ffdi.DeviceType
	  && RtlEqualUnicodePathPrefix (attr.ObjectName, &ro_u_uncp, TRUE)))
    is_remote_drive (true);

  if (!no_media)
    status = NtQueryVolumeInformationFile (vol, &io, &ffai_buf.ffai,
					   sizeof ffai_buf,
					   FileFsAttributeInformation);
  if (no_media || !NT_SUCCESS (status))
    {
      debug_printf ("Cannot get volume attributes (%S), %y",
		    attr.ObjectName, status);
      if (!in_vol)
	NtClose (vol);
      return false;
    }
  flags (ffai_buf.ffai.FileSystemAttributes);
  name_len (ffai_buf.ffai.MaximumComponentNameLength);
  RtlInitCountedUnicodeString (&fsname, ffai_buf.ffai.FileSystemName,
			       ffai_buf.ffai.FileSystemNameLength);
  if (is_remote_drive ())
    {
/* Should be reevaluated for each new OS.  Right now this mask is valid up
   to Windows 11.  The important point here is to test only flags indicating
   capabilities and to ignore flags indicating a specific state of this
   volume.  At present these flags to ignore are FILE_VOLUME_IS_COMPRESSED,
   FILE_READ_ONLY_VOLUME, FILE_SEQUENTIAL_WRITE_ONCE and FILE_DAX_VOLUME. */
#define GETVOLINFO_VALID_MASK (FILE_CASE_SENSITIVE_SEARCH		\
			       | FILE_CASE_PRESERVED_NAMES		\
			       | FILE_UNICODE_ON_DISK			\
			       | FILE_PERSISTENT_ACLS			\
			       | FILE_FILE_COMPRESSION			\
			       | FILE_VOLUME_QUOTAS			\
			       | FILE_SUPPORTS_SPARSE_FILES		\
			       | FILE_SUPPORTS_REPARSE_POINTS		\
			       | FILE_SUPPORTS_REMOTE_STORAGE		\
			       | FILE_RETURNS_CLEANUP_RESULT_INFO	\
			       | FILE_SUPPORTS_POSIX_UNLINK_RENAME	\
			       | FILE_SUPPORTS_OBJECT_IDS		\
			       | FILE_SUPPORTS_ENCRYPTION		\
			       | FILE_NAMED_STREAMS			\
			       | FILE_SUPPORTS_TRANSACTIONS		\
			       | FILE_SUPPORTS_HARD_LINKS		\
			       | FILE_SUPPORTS_EXTENDED_ATTRIBUTES	\
			       | FILE_SUPPORTS_OPEN_BY_FILE_ID		\
			       | FILE_SUPPORTS_USN_JOURNAL		\
			       | FILE_SUPPORTS_INTEGRITY_STREAMS	\
			       | FILE_SUPPORTS_BLOCK_REFCOUNTING	\
			       | FILE_SUPPORTS_SPARSE_VDL		\
			       | FILE_SUPPORTS_GHOSTING)
/* This is the pre-Win7 mask used to recognize 3rd-party drivers.  We'll never
   learn in time when those drivers start to support the new (har har) Win7 FS
   flags.  */
#define GETVOLINFO_NON_WIN_MASK (FILE_CASE_SENSITIVE_SEARCH		\
			       | FILE_CASE_PRESERVED_NAMES		\
			       | FILE_UNICODE_ON_DISK			\
			       | FILE_PERSISTENT_ACLS			\
			       | FILE_FILE_COMPRESSION			\
			       | FILE_VOLUME_QUOTAS			\
			       | FILE_SUPPORTS_SPARSE_FILES		\
			       | FILE_SUPPORTS_REPARSE_POINTS		\
			       | FILE_SUPPORTS_REMOTE_STORAGE		\
			       | FILE_SUPPORTS_OBJECT_IDS		\
			       | FILE_SUPPORTS_ENCRYPTION		\
			       | FILE_NAMED_STREAMS			\
			       | FILE_SUPPORTS_TRANSACTIONS)

#define TEST_GVI(f,m) (((f) & GETVOLINFO_VALID_MASK) == (m))
#define TEST_GVI_NON_WIN(f,m) (((f) & GETVOLINFO_NON_WIN_MASK) == (m))

/* FIXME: This flag twist is getting awkward.  There should really be some
   other method.  Maybe we need mount flags to allow the user to fix file
   system problems without having to wait for a Cygwin fix. */

/* Volume quotas are potentially supported since Samba 3.0, object ids and
   the unicode on disk flag since Samba 3.2. */
#define SAMBA_IGNORE (FILE_VOLUME_QUOTAS \
		      | FILE_SUPPORTS_OBJECT_IDS \
		      | FILE_UNICODE_ON_DISK)
#define FS_IS_SAMBA TEST_GVI_NON_WIN(flags () & ~SAMBA_IGNORE, \
			     FILE_CASE_SENSITIVE_SEARCH \
			     | FILE_CASE_PRESERVED_NAMES \
			     | FILE_PERSISTENT_ACLS)
/* Netapp DataOnTap. */
#define NETAPP_IGNORE (FILE_SUPPORTS_SPARSE_FILES \
		       | FILE_SUPPORTS_REPARSE_POINTS \
		       | FILE_PERSISTENT_ACLS)
#define FS_IS_NETAPP_DATAONTAP TEST_GVI_NON_WIN(flags () & ~NETAPP_IGNORE, \
			     FILE_CASE_SENSITIVE_SEARCH \
			     | FILE_CASE_PRESERVED_NAMES \
			     | FILE_UNICODE_ON_DISK \
			     | FILE_NAMED_STREAMS)
/* These are the minimal flags supported by NTFS since Windows 7, when
   checking a remote NTFS filesystem.  Every filesystem not supporting these
   flags is not a native NTFS.
   We subsume them under the filesystem type "cifs". */
#define MINIMAL_WIN_NTFS_FLAGS (FILE_CASE_SENSITIVE_SEARCH \
				| FILE_CASE_PRESERVED_NAMES \
				| FILE_UNICODE_ON_DISK \
				| FILE_PERSISTENT_ACLS \
				| FILE_FILE_COMPRESSION \
				| FILE_VOLUME_QUOTAS \
				| FILE_SUPPORTS_SPARSE_FILES \
				| FILE_SUPPORTS_REPARSE_POINTS \
				| FILE_SUPPORTS_OBJECT_IDS \
				| FILE_SUPPORTS_ENCRYPTION \
				| FILE_NAMED_STREAMS \
				| FILE_SUPPORTS_HARD_LINKS \
				| FILE_SUPPORTS_EXTENDED_ATTRIBUTES)
#define FS_IS_WINDOWS_NTFS TEST_GVI(flags () & MINIMAL_WIN_NTFS_FLAGS, \
				    MINIMAL_WIN_NTFS_FLAGS)
/* These are the exact flags of a real Windows FAT/FAT32 filesystem.
   Newer FAT32/exFAT support FILE_SUPPORTS_ENCRYPTION as well.
   Anything else is a filesystem faking to be FAT. */
#define WIN_FAT_FLAGS (FILE_CASE_PRESERVED_NAMES | FILE_UNICODE_ON_DISK)
#define FAT_IGNORE (FILE_SUPPORTS_ENCRYPTION)
#define FS_IS_WINDOWS_FAT  TEST_GVI(flags () & ~FAT_IGNORE, WIN_FAT_FLAGS)

      if ((flags () & FILE_SUPPORTS_OBJECT_IDS)
	  && NT_SUCCESS (NtQueryVolumeInformationFile (vol, &io, &ffoi,
						   sizeof ffoi,
						   FileFsObjectIdInformation)))
	{
	  smb_extended_info *extended_info = (smb_extended_info *)
					     &ffoi.ExtendedInfo;
	  if (extended_info->samba_magic == SAMBA_EXTENDED_INFO_MAGIC)
	    {
	      is_samba (true);
	      samba_version (extended_info->samba_version);
	    }
	}
      /* First check the remote filesystems claiming to be NTFS. */
      if (!got_fs ()
	  && is_ntfs (RtlEqualUnicodeString (&fsname, &ro_u_ntfs, FALSE))
	  /* Test for older Samba releases not supporting extended info. */
	  && !is_samba (FS_IS_SAMBA)
	  /* Netapp inode info is unusable, can't handle trailing dots and
	     spaces, has a bug in "move and delete" semantics. */
	  && !is_netapp (FS_IS_NETAPP_DATAONTAP))
	/* Any other remote FS faking to be NTFS. */
	is_cifs (!FS_IS_WINDOWS_NTFS);
      /* Then check remote filesystems claiming to be FAT.  Except for real
	 FAT and Netapp, all of them are subsumed under the "CIFS" filesystem
	 type for now. */
      if (!got_fs ()
	  && is_fat (RtlEqualUnicodePathPrefix (&fsname, &ro_u_fat, TRUE))
	  && !is_netapp (FS_IS_NETAPP_DATAONTAP))
	is_cifs (!FS_IS_WINDOWS_FAT);
      /* Then check remote filesystems honest about their name. */
      if (!got_fs ()
	  /* Microsoft exFAT */
	  && !is_exfat (RtlEqualUnicodeString (&fsname, &ro_u_exfat, FALSE))
	  /* Microsoft NFS needs distinct access methods for metadata. */
	  && !is_nfs (RtlEqualUnicodeString (&fsname, &ro_u_nfs, FALSE))
	  /* MVFS == Rational ClearCase remote filesystem.  Has a couple of
	     drawbacks, like not supporting DOS attributes other than R/O
	     and stuff like that. */
	  && !is_mvfs (RtlEqualUnicodePathPrefix (&fsname, &ro_u_mvfs, FALSE))
	  /* NWFS == Novell Netware FS.  Broken info class, see below. */
	  /* NcFsd == Novell Netware FS via own driver. */
	  && !is_nwfs (RtlEqualUnicodeString (&fsname, &ro_u_nwfs, FALSE))
	  && !is_ncfsd (RtlEqualUnicodeString (&fsname, &ro_u_ncfsd, FALSE))
	  /* UNIXFS == TotalNet Advanced Server (TAS).  Doesn't support
	     FileIdBothDirectoryInformation.  See below. */
	  && !is_unixfs (RtlEqualUnicodeString (&fsname, &ro_u_unixfs, FALSE))
	  /* AFSRDRFsd == Andrew File System.  Doesn't support DOS attributes.
	     Only native symlinks are supported. */
	  && !is_afs (RtlEqualUnicodeString (&fsname, &ro_u_afs, FALSE)))
	{
	  /* PrlSF == Parallels Desktop File System.  Has a bug in
	     FileNetworkOpenInformation, see below. */
	  is_prlfs (RtlEqualUnicodeString (&fsname, &ro_u_prlfs, FALSE));
	}
      if (got_fs ())
	{
	  /* UNIXFS is known to choke on FileIdBothDirectoryInformation.
	     Some other CIFS servers have problems with this call as well.
	     Know example: EMC NS-702.  We just don't use that info class on
	     any remote CIFS.  */
	  has_buggy_fileid_dirinfo (is_cifs () || is_unixfs ());
	  /* NWFS is known to have a broken FileBasicInformation info
	     class.  It can't be used to fetch information, only to set
	     information.  Therefore, for NWFS we have to fallback to the
	     FileNetworkOpenInformation info class.  Unfortunately we can't
	     use FileNetworkOpenInformation all the time since that fails on
	     other filesystems like NFS.
	     UNUSED, but keep in for information purposes. */
	  has_buggy_basic_info (is_nwfs ());
	  /* Netapp and NWFS/NcFsd are too dumb to allow non-DOS filenames
	     containing trailing dots and spaces when accessed from Windows
	     clients.  We subsume CIFS into this class of filesystems right
	     away since at least some of them are not capable either. */
	  has_dos_filenames_only (is_netapp () || is_nwfs ()
				  || is_ncfsd () || is_cifs ());
	  /* Netapp and NWFS don't grok re-opening a file by handle.  They
	     only support this if the filename is non-null and the handle is
	     the handle to a directory. NcFsd IR10 is supposed to be ok. */
	  has_buggy_reopen (is_netapp () || is_nwfs ());
	}
    }
  if (!got_fs ()
      && !is_ntfs (RtlEqualUnicodeString (&fsname, &ro_u_ntfs, FALSE))
      && !is_fat (RtlEqualUnicodePathPrefix (&fsname, &ro_u_fat, TRUE))
      && !is_exfat (RtlEqualUnicodeString (&fsname, &ro_u_exfat, FALSE))
      && !is_refs (RtlEqualUnicodeString (&fsname, &ro_u_refs, FALSE))
      && !is_csc_cache (RtlEqualUnicodeString (&fsname, &ro_u_csc, FALSE))
      && is_cdrom (ffdi.DeviceType == FILE_DEVICE_CD_ROM))
    is_udf (RtlEqualUnicodeString (&fsname, &ro_u_udf, FALSE));
  if (!got_fs ())
    {
      /* The filesystem name is only used in fillout_mntent and only if
	 the filesystem isn't one of the well-known filesystems anyway. */
      sys_wcstombs (fsn, sizeof fsn, ffai_buf.ffai.FileSystemName,
		    ffai_buf.ffai.FileSystemNameLength / sizeof (WCHAR));
      strlwr (fsn);
    }
  has_acls (flags () & FS_PERSISTENT_ACLS);
  /* Netapp inode numbers are fly-by-night. */
  hasgood_inode ((has_acls () && !is_netapp ()) || is_nfs ());
  /* Case sensitivity is supported if FILE_CASE_SENSITIVE_SEARCH is set,
     except on Samba which handles Windows clients case insensitive.

     NFS doesn't set the FILE_CASE_SENSITIVE_SEARCH flag but is case
     sensitive. */
  caseinsensitive ((!(flags () & FILE_CASE_SENSITIVE_SEARCH) || is_samba ())
		   && !is_nfs ());

  if (!in_vol)
    NtClose (vol);
  fsi_cache.add (hash, this);
  return true;
}

inline void
mount_info::create_root_entry (const PWCHAR root)
{
 /* Create a default root dir derived from the location of the Cygwin DLL.
    The entry is immutable, unless the "override" option is given in /etc/fstab. */
  char native_root[PATH_MAX];
  sys_wcstombs (native_root, PATH_MAX, root);
  assert (*native_root != '\0');
  if (add_item (native_root, "/",
		MOUNT_SYSTEM | MOUNT_IMMUTABLE | MOUNT_AUTOMATIC)
      < 0)
    api_fatal ("add_item (\"%s\", \"/\", ...) failed, errno %d", native_root, errno);
  /* Create a default cygdrive entry.  Note that this is a user entry.
     This allows to override it with mount, unless the sysadmin created
     a cygdrive entry in /etc/fstab. */
  cygdrive_flags = MOUNT_NOPOSIX | MOUNT_CYGDRIVE;
  strcpy (cygdrive, CYGWIN_INFO_CYGDRIVE_DEFAULT_PREFIX "/");
  cygdrive_len = strlen (cygdrive);
}

/* init: Initialize the mount table.  */

void
mount_info::init (bool user_init)
{
  PWCHAR pathend;
  WCHAR path[PATH_MAX];

  pathend = wcpcpy (path, cygheap->installation_root.Buffer);
  if (!user_init)
    create_root_entry (path);

  pathend = wcpcpy (pathend, L"\\etc\\fstab");
  from_fstab (user_init, path, pathend);

  if (!user_init && (!got_usr_bin || !got_usr_lib))
    {
      char native[PATH_MAX];
      if (root_idx < 0)
	api_fatal ("root_idx %d, user_shared magic %y, nmounts %d", root_idx, user_shared->version, nmounts);
      char *p = stpcpy (native, mount[root_idx].native_path);
      if (!got_usr_bin)
      {
	stpcpy (p, "\\bin");
	add_item (native, "/usr/bin", MOUNT_SYSTEM | MOUNT_AUTOMATIC);
      }
      if (!got_usr_lib)
      {
	stpcpy (p, "\\lib");
	add_item (native, "/usr/lib", MOUNT_SYSTEM | MOUNT_AUTOMATIC);
      }
    }
}

int
mount_item::build_win32 (char *dst, const char *src, unsigned *outflags, unsigned chroot_pathlen)
{
  int n, err = 0;
  const char *real_native_path;
  int real_posix_pathlen;
  *outflags = flags;
  if (!cygheap->root.exists () || posix_pathlen != 1 || posix_path[0] != '/')
    {
      n = native_pathlen;
      real_native_path = native_path;
      real_posix_pathlen = chroot_pathlen ?: posix_pathlen;
    }
  else
    {
      n = cygheap->root.native_length ();
      real_native_path = cygheap->root.native_path ();
      real_posix_pathlen = posix_pathlen;
    }
  memcpy (dst, real_native_path, n + 1);
  const char *p = src + real_posix_pathlen;
  if (*p == '/')
    /* nothing */;
  else if ((isdrive (dst) && !dst[2]) || *p)
    dst[n++] = '\\';
  if ((n + strlen (p)) >= NT_MAX_PATH)
    err = ENAMETOOLONG;
  else
    backslashify (p, dst + n, 0);
  return err;
}

/* conv_to_win32_path: Ensure src_path is a pure Win32 path and store
   the result in win32_path.

   If win32_path != NULL, the relative path, if possible to keep, is
   stored in win32_path.  If the relative path isn't possible to keep,
   the full path is stored.

   If full_win32_path != NULL, the full path is stored there.

   The result is zero for success, or an errno value.

   {,full_}win32_path must have sufficient space (i.e. NT_MAX_PATH bytes).  */

int
mount_info::conv_to_win32_path (const char *src_path, char *dst, device& dev,
				unsigned *flags)
{
  bool chroot_ok = !cygheap->root.exists ();

  dev = FH_FS;

  *flags = 0;
  debug_printf ("conv_to_win32_path (%s)", src_path);

  int i, rc;
  mount_item *mi = NULL;	/* initialized to avoid compiler warning */

  /* The path is already normalized, without ../../ stuff, we need to have this
     so that we can move from one mounted directory to another with relative
     stuff.

     eg mounting c:/foo /foo
     d:/bar /bar

     cd /bar
     ls ../foo

     should look in c:/foo, not d:/foo.

     converting normalizex UNIX path to a DOS-style path, looking up the
     appropriate drive in the mount table.  */

  /* See if this is a cygwin "device" */
  if (win32_device_name (src_path, dst, dev))
    {
      *flags = 0;
      rc = 0;
      goto out_no_chroot_check;
    }

  /* If the path is on a network drive or a //./ resp. //?/ path prefix,
     bypass the mount table.  If it's // or //MACHINE, use the netdrive
     device. */
  if (src_path[1] == '/')
    {
      if (!strchr (src_path + 2, '/'))
	{
	  dev = *netdrive_dev;
	  *flags = 0;
	}
      else
	{
	  /* For UNC paths, use the cygdrive prefix flags as default setting.
	     This is more natural since UNC paths, just like cygdrive paths,
	     are rather (warning, poetic description ahead) windows into the
	     native Win32 world.  This also gives the user an elegant way to
	     change the settings for those paths in a central place. */
	  *flags = cygdrive_flags;
	}
      backslashify (src_path, dst, 0);
      /* Go through chroot check */
      goto out;
    }
  if (isproc (src_path))
    {
      dev = *proc_dev;
      dev = fhandler_proc::get_proc_fhandler (src_path);
      if (dev == FH_NADA)
	return ENOENT;
      *flags = 0;
      if (isprocsys_dev (dev))
	{
	  if (src_path[procsys_len])
	    backslashify (src_path + procsys_len, dst, 0);
	  else	/* Avoid empty NT path. */
	    stpcpy (dst, "\\");
	  *flags = cygdrive_flags;
	}
      else
	strcpy (dst, src_path);
      goto out;
    }
  /* Check if the cygdrive prefix was specified.  If so, just strip
     off the prefix and transform it into an MS-DOS path. */
  else if (iscygdrive (src_path))
    {
      int n = mount_table->cygdrive_len - 1;
      int unit;

      if (!src_path[n])
	{
	  dst[0] = '\0';
	  if (mount_table->cygdrive_len > 1)
	    dev = *cygdrive_dev;
	}
      else if (cygdrive_win32_path (src_path, dst, unit))
	{
	  *flags = cygdrive_flags;
	  goto out;
	}
      else if (mount_table->cygdrive_len > 1)
	return ENOENT;
    }

  int chroot_pathlen;
  chroot_pathlen = 0;
  /* Check the mount table for prefix matches. */
  for (i = 0; i < nmounts; i++)
    {
      const char *path;
      int len;

      mi = mount + posix_sorted[i];
      if (!cygheap->root.exists ()
	  || (mi->posix_pathlen == 1 && mi->posix_path[0] == '/'))
	{
	  path = mi->posix_path;
	  len = mi->posix_pathlen;
	}
      else if (cygheap->root.posix_ok (mi->posix_path))
	{
	  path = cygheap->root.unchroot (mi->posix_path);
	  chroot_pathlen = len = strlen (path);
	}
      else
	{
	  chroot_pathlen = 0;
	  continue;
	}

      if (path_prefix_p (path, src_path, len, mi->flags & MOUNT_NOPOSIX))
	break;
    }

  if (i < nmounts)
    {
      int err = mi->build_win32 (dst, src_path, flags, chroot_pathlen);
      if (err)
	return err;
      chroot_ok = true;
    }
  else
    {
      int offset = 0;
      if (src_path[1] != '/' && src_path[1] != ':')
	offset = cygheap->cwd.get_drive (dst);
      backslashify (src_path, dst + offset, 0);
    }
 out:
  if (chroot_ok || cygheap->root.ischroot_native (dst))
    rc = 0;
  else
    {
      debug_printf ("attempt to access outside of chroot '%s - %s'",
		    cygheap->root.posix_path (), cygheap->root.native_path ());
      rc = ENOENT;
    }

 out_no_chroot_check:
  debug_printf ("src_path %s, dst %s, flags %y, rc %d", src_path, dst, *flags, rc);
  return rc;
}

size_t
mount_info::get_mounts_here (const char *parent_dir, size_t parent_dir_len,
			     PUNICODE_STRING mount_points,
			     PUNICODE_STRING cygd)
{
  size_t n_mounts = 0;

  for (int i = 0; i < nmounts; i++)
    {
      mount_item *mi = mount + posix_sorted[i];
      char *last_slash = strrchr (mi->posix_path, '/');
      if (!last_slash)
	continue;
      if (last_slash == mi->posix_path)
	{
	  if (parent_dir_len == 1 && mi->posix_pathlen > 1)
	    sys_mbstouni_alloc (&mount_points[n_mounts++], HEAP_BUF,
			        last_slash + 1);
	}
      else if (parent_dir_len == (size_t) (last_slash - mi->posix_path)
	       && strncasematch (parent_dir, mi->posix_path, parent_dir_len))
	sys_mbstouni_alloc (&mount_points[n_mounts++], HEAP_BUF,
			    last_slash + 1);
    }
  sys_mbstouni_alloc (cygd, HEAP_BUF, cygdrive + 1);
  if (cygd->Length)
    cygd->Length -= 2;	// Strip trailing slash
  return n_mounts;
}

void
mount_info::free_mounts_here (PUNICODE_STRING mount_points, int n_mounts,
			      PUNICODE_STRING cygd)
{
  for (int i = 0; i < n_mounts; ++i)
    cfree (mount_points[i].Buffer);
  cfree (cygd->Buffer);
}

/* cygdrive_posix_path: Build POSIX path used as the
   mount point for cygdrives created when there is no other way to
   obtain a POSIX path from a Win32 one.

   Recognized flag values:
   - 0x001:                      Add trailing slash.
   - 0x200 == CCP_PROC_CYGDRIVE: Return /proc/cygdrive rather than actual
                                 cygdrive prefix. */

void
mount_info::cygdrive_posix_path (const char *src, char *dst, int flags)
{
  int len;

  if (flags & CCP_PROC_CYGDRIVE)
    {
      len = sizeof ("/proc/cygdrive/") - 1;
      memcpy (dst, "/proc/cygdrive/", len + 1);
    }
  else
    {
      len = cygdrive_len;
      memcpy (dst, cygdrive, len + 1);
    }

  /* Now finish the path off with the drive letter to be used.
     The cygdrive prefix always ends with a trailing slash so
     the drive letter is added after the path. */
  dst[len++] = cyg_tolower (src[0]);
  if (!src[2] || (isdirsep (src[2]) && !src[3]))
    dst[len++] = '\000';
  else
    {
      int n;
      dst[len++] = '/';
      if (isdirsep (src[2]))
	n = 3;
      else
	n = 2;
      strcpy (dst + len, src + n);
    }
  slashify (dst, dst, !!(flags & 0x1));
}

int
mount_info::cygdrive_win32_path (const char *src, char *dst, int& unit)
{
  int res;
  const char *p = src + cygdrive_len;
  if (!isalpha (*p) || (!isdirsep (p[1]) && p[1]))
    {
      unit = -1; /* FIXME: should be zero, maybe? */
      dst[0] = '\0';
      res = 0;
    }
  else
    {
      /* drive letter must always be uppercase for casesensitive native NT. */
      dst[0] = cyg_toupper (*p);
      dst[1] = ':';
      strcpy (dst + 2, p + 1);
      backslashify (dst, dst, !dst[2]);
      unit = dst[0];
      res = 1;
    }
  debug_printf ("src '%s', dst '%s'", src, dst);
  return res;
}

/* conv_to_posix_path: Ensure src_path is a POSIX path.

   The result is zero for success, or an errno value.
   posix_path must have sufficient space (i.e. NT_MAX_PATH bytes).
   If keep_rel_p is non-zero, relative paths stay that way.  */

/* TODO: Change conv_to_posix_path to work with native paths. */

/* src_path is a wide Win32 path. */
int
mount_info::conv_to_posix_path (PWCHAR src_path, char *posix_path,
				int ccp_flags)
{
  bool changed = false;
  if (!wcsncmp (src_path, L"\\\\?\\", 4))
    {
      src_path += 4;
      if (src_path[1] != L':') /* native UNC path */
	{
	  *(src_path += 2) = L'\\';
	  changed = true;
	}
    }
  tmp_pathbuf tp;
  char *buf = tp.c_get ();
  sys_wcstombs (buf, NT_MAX_PATH, src_path);
  int ret = conv_to_posix_path (buf, posix_path, ccp_flags);
  if (changed)
    src_path[0] = L'C';
  return ret;
}

int
mount_info::conv_to_posix_path (const char *src_path, char *posix_path,
				int ccp_flags)
{
  int src_path_len = strlen (src_path);
  int relative = !isabspath (src_path);
  int append_slash;

  if (src_path_len <= 1)
    append_slash = 0;
  else
    {
      const char *lastchar = src_path + src_path_len - 1;
      append_slash = isdirsep (*lastchar)
		     && ((ccp_flags & __CCP_APP_SLASH) || lastchar[-1] != ':');
    }

  debug_printf ("conv_to_posix_path (%s, 0x%x, %s)", src_path, ccp_flags,
		append_slash ? "add-slash" : "no-add-slash");

  if (src_path_len >= NT_MAX_PATH)
    {
      debug_printf ("ENAMETOOLONG");
      return ENAMETOOLONG;
    }

  /* FIXME: For now, if the path is relative and it's supposed to stay
     that way, skip mount table processing. */

  if ((ccp_flags & CCP_RELATIVE) && relative)
    {
      slashify (src_path, posix_path, 0);
      debug_printf ("%s = conv_to_posix_path (%s)", posix_path, src_path);
      return 0;
    }

  tmp_pathbuf tp;
  char *pathbuf = tp.c_get ();
  char *tail;
  int rc = normalize_win32_path (src_path, pathbuf, tail);
  if (rc != 0)
    {
      debug_printf ("%d = conv_to_posix_path(%s)", rc, src_path);
      return rc;
    }

  int pathbuflen = tail - pathbuf;
  for (int i = 0; i < nmounts; ++i)
    {
      mount_item &mi = mount[native_sorted[i]];
      if (!path_prefix_p (mi.native_path, pathbuf, mi.native_pathlen,
			  mi.flags & MOUNT_NOPOSIX))
	continue;

      if (cygheap->root.exists () && !cygheap->root.posix_ok (mi.posix_path))
	continue;

      /* SRC_PATH is in the mount table. */
      int nextchar;
      const char *p = pathbuf + mi.native_pathlen;

      if (!*p || !p[1])
	nextchar = 0;
      else if (isdirsep (*p))
	nextchar = -1;
      else
	nextchar = 1;

      int addslash = nextchar > 0 ? 1 : 0;
      if ((mi.posix_pathlen + (pathbuflen - mi.native_pathlen) + addslash) >= NT_MAX_PATH)
	return ENAMETOOLONG;
      strcpy (posix_path, mi.posix_path);
      if (addslash || (!nextchar && append_slash))
	strcat (posix_path, "/");
      if (nextchar)
	slashify (p,
		  posix_path + addslash + (mi.posix_pathlen == 1
		  ? 0 : mi.posix_pathlen),
		  append_slash);

      if (cygheap->root.exists ())
	{
	  const char *p = cygheap->root.unchroot (posix_path);
	  memmove (posix_path, p, strlen (p) + 1);
	}
      goto out;
    }

  if (!cygheap->root.exists ())
    /* nothing */;
  else if (!cygheap->root.ischroot_native (pathbuf))
    return ENOENT;
  else
    {
      const char *p = pathbuf + cygheap->root.native_length ();
      if (*p)
	slashify (p, posix_path, append_slash);
      else
	{
	  posix_path[0] = '/';
	  posix_path[1] = '\0';
	}
      goto out;
    }

  /* Not in the database.  This should [theoretically] only happen if either
     the path begins with //, or / isn't mounted, or the path has a drive
     letter not covered by the mount table.  If it's a relative path then the
     caller must want an absolute path (otherwise we would have returned
     above).  So we always return an absolute path at this point. */
  if (isdrive (pathbuf))
    cygdrive_posix_path (pathbuf, posix_path, append_slash | ccp_flags);
  else
    {
      /* The use of src_path and not pathbuf here is intentional.
	 We couldn't translate the path, so just ensure no \'s are present. */
      slashify (src_path, posix_path, append_slash);
    }

out:
  debug_printf ("%s = conv_to_posix_path (%s)", posix_path, src_path);
  return 0;
}

inline char *
skip_ws (char *in)
{
  while (*in == ' ' || *in == '\t')
    ++in;
  return in;
}

inline char *
find_ws (char *in)
{
  while (*in && *in != ' ' && *in != '\t')
    ++in;
  return in;
}

inline char *
conv_fstab_spaces (char *field)
{
  char *sp = field;
  while ((sp = strstr (sp, "\\040")) != NULL)
    {
      *sp++ = ' ';
      memmove (sp, sp + 3, strlen (sp + 3) + 1);
    }
  return field;
}

struct opt
{
  const char *name;
  unsigned val;
  bool clear;
} oopts[] =
{
  {"acl", MOUNT_NOACL, 1},
  {"auto", 0, 0},
  {"binary", MOUNT_TEXT, 1},
  {"bind", MOUNT_BIND, 0},
  {"cygexec", MOUNT_CYGWIN_EXEC, 0},
  {"dos", MOUNT_DOS, 0},
  {"exec", MOUNT_EXEC, 0},
  {"ihash", MOUNT_IHASH, 0},
  {"noacl", MOUNT_NOACL, 0},
  {"nosuid", 0, 0},
  {"notexec", MOUNT_NOTEXEC, 0},
  {"nouser", MOUNT_SYSTEM, 0},
  {"override", MOUNT_OVERRIDE, 0},
  {"posix=0", MOUNT_NOPOSIX, 0},
  {"posix=1", MOUNT_NOPOSIX, 1},
  {"sparse", MOUNT_SPARSE, 0},
  {"text", MOUNT_TEXT, 0},
  {"user", MOUNT_SYSTEM, 1}
};

static int
compare_flags (const void *a, const void *b)
{
  const opt *oa = (const opt *) a;
  const opt *ob = (const opt *) b;

  return strcmp (oa->name, ob->name);
}

extern "C" bool
fstab_read_flags (char **options, unsigned &flags, bool external)
{
  opt key;

  while (**options)
    {
      char *p = strchr (*options, ',');
      if (p)
	*p++ = '\0';
      else
	p = strchr (*options, '\0');

      key.name = *options;
      opt *o = (opt *) bsearch (&key, oopts,
				sizeof oopts / sizeof (opt),
				sizeof (opt), compare_flags);
      if (!o)
	{
	  if (!external)
	    system_printf ("invalid fstab option - '%s'", *options);
	  return false;
	}
      if (o->clear)
	flags &= ~o->val;
      else
	flags |= o->val;
      *options = p;
    }
  return true;
}

extern "C" char *
fstab_list_flags ()
{
  size_t len = 0;
  opt *o;

  for (o = oopts; o < (oopts + (sizeof (oopts) / sizeof (oopts[0]))); o++)
    len += strlen (o->name) + 1;
  char *buf = (char *) malloc (len);
  if (buf)
    {
      char *bp = buf;
      for (o = oopts; o < (oopts + (sizeof (oopts) / sizeof (oopts[0]))); o++)
	{
	  bp = stpcpy (bp, o->name);
	  *bp++ = ',';
	}
      *--bp = '\0';
    }
  return buf;
}

bool
mount_info::from_fstab_line (char *line, bool user)
{
  char *native_path, *posix_path, *fs_type;

  /* First field: Native path. */
  char *c = skip_ws (line);
  if (!*c || *c == '#')
    return true;
  char *cend = find_ws (c);
  *cend = '\0';
  native_path = conv_fstab_spaces (c);
  /* Always convert drive letter to uppercase for case sensitivity. */
  if (isdrive (native_path))
    native_path[0] = cyg_toupper (native_path[0]);
  /* Second field: POSIX path. */
  c = skip_ws (cend + 1);
  if (!*c)
    return true;
  cend = find_ws (c);
  *cend = '\0';
  posix_path = conv_fstab_spaces (c);
  /* Third field: FS type. */
  c = skip_ws (cend + 1);
  if (!*c)
    return true;
  cend = find_ws (c);
  *cend = '\0';
  fs_type = c;
  /* Forth field: Flags. */
  c = skip_ws (cend + 1);
  if (!*c)
    return true;
  cend = find_ws (c);
  *cend = '\0';
  unsigned mount_flags = MOUNT_SYSTEM;
  if (!strcmp (fs_type, "cygdrive"))
    mount_flags |= MOUNT_NOPOSIX;
  if (!strcmp (fs_type, "usertemp"))
    mount_flags |= MOUNT_IMMUTABLE;
  if (!fstab_read_flags (&c, mount_flags, false))
    return true;
  if (mount_flags & MOUNT_BIND)
    {
      /* Prepend root path to bound path. */
      char *bound_path = native_path;
      device dev;
      unsigned flags = 0;
      native_path = (char *) alloca (PATH_MAX);
      int error = conv_to_win32_path (bound_path, native_path, dev, &flags);
      if (error || strlen (native_path) >= MAX_PATH)
	return true;
      if ((mount_flags & ~MOUNT_SYSTEM) == MOUNT_BIND)
	mount_flags = (MOUNT_BIND | flags)
		      & ~(MOUNT_IMMUTABLE | MOUNT_AUTOMATIC);
    }
  if (user)
    mount_flags &= ~MOUNT_SYSTEM;
  if (!strcmp (fs_type, "cygdrive"))
    {
      cygdrive_flags = mount_flags | MOUNT_CYGDRIVE;
      slashify (posix_path, cygdrive, 1);
      cygdrive_len = strlen (cygdrive);
    }
  else if (!strcmp (fs_type, "usertemp"))
    {
      WCHAR tmp[PATH_MAX + 1];

      if (GetTempPathW (PATH_MAX, tmp))
	{
	  tmp_pathbuf tp;
	  char *mb_tmp = tp.c_get ();
	  sys_wcstombs (mb_tmp, PATH_MAX, tmp);

	  mount_flags |= MOUNT_USER_TEMP;
	  int res = mount_table->add_item (mb_tmp, posix_path, mount_flags);
	  if (res && get_errno () == EMFILE)
	    return false;
	}
    }
  else
    {
      int res = mount_table->add_item (native_path, posix_path, mount_flags);
      if (res && get_errno () == EMFILE)
	return false;
    }
  return true;
}

bool
mount_info::from_fstab (bool user, WCHAR fstab[], PWCHAR fstab_end)
{
  UNICODE_STRING upath;
  OBJECT_ATTRIBUTES attr;
  NT_readline rl;
  tmp_pathbuf tp;
  char *buf = tp.c_get ();

  if (user)
    {
      PWCHAR username;
      sys_mbstowcs (username = wcpcpy (fstab_end, L".d\\"),
		    NT_MAX_PATH - (fstab_end - fstab),
		    cygheap->user.name ());
      /* Make sure special chars in the username are converted according to
	 the rules. */
      transform_chars (username, username + wcslen (username) - 1);
    }
  RtlInitUnicodeString (&upath, fstab);
  InitializeObjectAttributes (&attr, &upath, OBJ_CASE_INSENSITIVE, NULL, NULL);
  debug_printf ("Try to read mounts from %W", fstab);
  if (rl.init (&attr, buf, NT_MAX_PATH))
    while ((buf = rl.gets ()))
      if (!from_fstab_line (buf, user))
	break;
  return true;
}

/* write_cygdrive_info: Store default prefix and flags
   to use when creating cygdrives to the special user shared mem
   location used to store cygdrive information. */

int
mount_info::write_cygdrive_info (const char *cygdrive_prefix, unsigned flags)
{
  /* Verify cygdrive prefix starts with a forward slash and if there's
     another character, it's not a slash. */
  if ((cygdrive_prefix == NULL) || (*cygdrive_prefix == 0) ||
      (!isslash (cygdrive_prefix[0])) ||
      ((cygdrive_prefix[1] != '\0') && (isslash (cygdrive_prefix[1]))))
    {
      set_errno (EINVAL);
      return -1;
    }
  /* Don't allow overriding of a system cygdrive prefix. */
  if (cygdrive_flags & MOUNT_SYSTEM)
    {
      set_errno (EPERM);
      return -1;
    }

  slashify (cygdrive_prefix, cygdrive, 1);
  cygdrive_flags = flags & ~MOUNT_SYSTEM;
  cygdrive_len = strlen (cygdrive);

  return 0;
}

int
mount_info::get_cygdrive_info (char *user, char *system, char *user_flags,
			       char *system_flags)
{
  if (user)
    *user = '\0';
  if (system)
    *system = '\0';
  if (user_flags)
    *user_flags = '\0';
  if (system_flags)
    *system_flags = '\0';

  char *path = (cygdrive_flags & MOUNT_SYSTEM) ? system : user;
  char *flags = (cygdrive_flags & MOUNT_SYSTEM) ? system_flags : user_flags;

  if (path)
    {
      strcpy (path, cygdrive);
      /* Strip trailing slash for backward compatibility. */
      if (cygdrive_len > 2)
	path[cygdrive_len - 1] = '\0';
    }
  if (flags)
    strcpy (flags, (cygdrive_flags & MOUNT_TEXT) ? "textmode" : "binmode");
  return 0;
}

static mount_item *mounts_for_sort;

/* sort_by_posix_name: qsort callback to sort the mount entries.  Sort
   user mounts ahead of system mounts to the same POSIX path. */
/* FIXME: should the user be able to choose whether to prefer user or
   system mounts??? */
static int
sort_by_posix_name (const void *a, const void *b)
{
  mount_item *ap = mounts_for_sort + (*((int*) a));
  mount_item *bp = mounts_for_sort + (*((int*) b));

  /* Base weighting on longest posix path first so that the most
     obvious path will be chosen. */
  size_t alen = strlen (ap->posix_path);
  size_t blen = strlen (bp->posix_path);

  int res = blen - alen;

  if (res)
    return res;		/* Path lengths differed */

  /* The two paths were the same length, so just determine normal
     lexical sorted order. */
  res = strcmp (ap->posix_path, bp->posix_path);

  if (res == 0)
   {
     /* need to select between user and system mount to same POSIX path */
     if (!(bp->flags & MOUNT_SYSTEM))	/* user mount */
      return 1;
     else
      return -1;
   }

  return res;
}

/* sort_by_native_name: qsort callback to sort the mount entries.  Sort
   user mounts ahead of system mounts to the same POSIX path. */
/* FIXME: should the user be able to choose whether to prefer user or
   system mounts??? */
static int
sort_by_native_name (const void *a, const void *b)
{
  mount_item *ap = mounts_for_sort + (*((int*) a));
  mount_item *bp = mounts_for_sort + (*((int*) b));

  /* Base weighting on longest win32 path first so that the most
     obvious path will be chosen. */
  size_t alen = strlen (ap->native_path);
  size_t blen = strlen (bp->native_path);

  int res = blen - alen;

  if (res)
    return res;		/* Path lengths differed */

  /* The two paths were the same length, so just determine normal
     lexical sorted order. */
  res = strcmp (ap->native_path, bp->native_path);

  if (res == 0)
   {
     /* need to select between user and system mount to same POSIX path */
     if (!(bp->flags & MOUNT_SYSTEM))	/* user mount */
      return 1;
     else
      return -1;
   }

  return res;
}

void
mount_info::sort ()
{
  for (int i = 0; i < nmounts; i++)
    native_sorted[i] = posix_sorted[i] = i;
  /* Sort them into reverse length order, otherwise we won't
     be able to look for /foo in /.  */
  mounts_for_sort = mount;	/* ouch. */
  qsort (posix_sorted, nmounts, sizeof (posix_sorted[0]), sort_by_posix_name);
  qsort (native_sorted, nmounts, sizeof (native_sorted[0]), sort_by_native_name);
}

/* Add an entry to the mount table.
   Returns 0 on success, -1 on failure and errno is set.

   This is where all argument validation is done.  It may not make sense to
   do this when called internally, but it's cleaner to keep it all here.  */

int
mount_info::add_item (const char *native, const char *posix,
		      unsigned mountflags)
{
  tmp_pathbuf tp;
  char *nativetmp = tp.c_get ();
  /* FIXME: The POSIX path is stored as value name right now, which is
     restricted to 256 bytes. */
  char posixtmp[CYG_MAX_PATH];
  char *nativetail, *posixtail, error[] = "error";
  int nativeerr, posixerr;

  /* Something's wrong if either path is NULL or empty, or if it's
     not a UNC or absolute path. */

  if (native == NULL || !isabspath (native) ||
      !(is_native_path (native) || is_unc_share (native) || isdrive (native)))
    nativeerr = EINVAL;
  else
    nativeerr = normalize_win32_path (native, nativetmp, nativetail);

  if (posix == NULL || !isabspath (posix) ||
      is_unc_share (posix) || isdrive (posix))
    posixerr = EINVAL;
  else
    posixerr = normalize_posix_path (posix, posixtmp, posixtail);

  debug_printf ("%s[%s], %s[%s], %y",
		native, nativeerr ? error : nativetmp,
		posix, posixerr ? error : posixtmp, mountflags);

  if (nativeerr || posixerr)
    {
      set_errno (nativeerr ?: posixerr);
      return -1;
    }

  /* Make sure both paths do not end in /. */
  if (nativetail > nativetmp + 1 && nativetail[-1] == '\\')
    nativetail[-1] = '\0';
  if (posixtail > posixtmp + 1 && posixtail[-1] == '/')
    posixtail[-1] = '\0';

  /* Write over an existing mount item with the same POSIX path if
     it exists and is from the same registry area. */
  int i;
  for (i = 0; i < nmounts; i++)
    {
      if (!strcmp (mount[i].posix_path, posixtmp))
	{
	  /* Don't allow overriding of a system mount with a user mount. */
	  if ((mount[i].flags & MOUNT_SYSTEM) && !(mountflags & MOUNT_SYSTEM))
	    {
	      set_errno (EPERM);
	      return -1;
	    }
	  if ((mount[i].flags & MOUNT_SYSTEM) != (mountflags & MOUNT_SYSTEM))
	    continue;
	  else if (!(mount[i].flags & MOUNT_IMMUTABLE))
	    break;
	  else if (mountflags & MOUNT_OVERRIDE)
	    {
	      mountflags |= MOUNT_IMMUTABLE;
	      break;
	    }
	  else
	    {
	      set_errno (EPERM);
	      return -1;
	    }
	}
    }

  if (i == nmounts && nmounts == MAX_MOUNTS)
    {
      set_errno (EMFILE);
      return -1;
    }

  if (i == nmounts)
    nmounts++;

  if (strcmp (posixtmp, "/usr/bin") == 0)
    got_usr_bin = true;

  if (strcmp (posixtmp, "/usr/lib") == 0)
    got_usr_lib = true;

  if (posixtmp[0] == '/' && posixtmp[1] == '\0' && !(mountflags & MOUNT_CYGDRIVE))
    root_idx = i;

  mount[i].init (nativetmp, posixtmp, mountflags);
  sort ();

  return 0;
}

/* Delete a mount table entry where path is either a Win32 or POSIX
   path. Since the mount table is really just a table of aliases,
   deleting / is ok (although running without a slash mount is
   strongly discouraged because some programs may run erratically
   without one).  If MOUNT_SYSTEM is set in flags, remove from system
   registry, otherwise remove the user registry mount.
*/

int
mount_info::del_item (const char *path, unsigned flags)
{
  tmp_pathbuf tp;
  char *pathtmp = tp.c_get ();
  int posix_path_p = false;

  /* Something's wrong if path is NULL or empty. */
  if (path == NULL || *path == 0 || !isabspath (path))
    {
      set_errno (EINVAL);
      return -1;
    }

  if (is_unc_share (path) || strpbrk (path, ":\\"))
    backslashify (path, pathtmp, 0);
  else
    {
      slashify (path, pathtmp, 0);
      posix_path_p = true;
    }
  nofinalslash (pathtmp, pathtmp);

  for (int i = 0; i < nmounts; i++)
    {
      int ent = native_sorted[i]; /* in the same order as getmntent() */
      if (((posix_path_p)
	   ? !strcmp (mount[ent].posix_path, pathtmp)
	   : strcasematch (mount[ent].native_path, pathtmp)))
	{
	  /* Don't allow removal of a system mount. */
	  if (mount[ent].flags & MOUNT_SYSTEM)
	    {
	      set_errno (EPERM);
	      return -1;
	    }
	  nmounts--; /* One less mount table entry */
	  /* Fill in the hole if not at the end of the table */
	  if (ent < nmounts)
	    memmove (mount + ent, mount + ent + 1,
		     sizeof (mount[ent]) * (nmounts - ent));
	  sort (); /* Resort the table */
	  return 0;
	}
    }
  set_errno (EINVAL);
  return -1;
}

/************************* mount_item class ****************************/

/* Don't add new fs types without adding them to fs_info_type in mount.h!
   Don't reorder without reordering fs_info_type in mount.h!*/
fs_names_t fs_names[] = {
    { "none", false },
    { "vfat", true },
    { "exfat", true },
    { "ntfs", true },
    { "refs", true },
    { "smbfs", false },
    { "nfs", false },
    { "netapp", false },
    { "iso9660", true },
    { "udf", true },
    { "csc-cache", false },
    { "unixfs", false },
    { "mvfs", false },
    { "cifs", false },
    { "nwfs", false },
    { "ncfsd", false },
    { "afs", false },
    { "prlfs", false },
    { NULL, false }
};

static mntent *
fillout_mntent (const char *native_path, const char *posix_path, unsigned flags)
{
  struct mntent& ret=_my_tls.locals.mntbuf;
  bool append_bs = false;

  /* Remove drivenum from list if we see a x: style path */
  if (strlen (native_path) == 2 && native_path[1] == ':')
    {
      int drivenum = cyg_tolower (native_path[0]) - 'a';
      if (drivenum >= 0 && drivenum <= 31)
	_my_tls.locals.available_drives &= ~(1 << drivenum);
      append_bs = true;
    }

  /* Pass back pointers to mount_table strings reserved for use by
     getmntent rather than pointers to strings in the internal mount
     table because the mount table might change, causing weird effects
     from the getmntent user's point of view. */

  ret.mnt_fsname = _my_tls.locals.mnt_fsname;
  strcpy (_my_tls.locals.mnt_dir, posix_path);
  ret.mnt_dir = _my_tls.locals.mnt_dir;

  /* Try to give a filesystem type that matches what a Linux application might
     expect. Naturally, this is a moving target, but we can make some
     reasonable guesses for popular types. */

  fs_info mntinfo;
  tmp_pathbuf tp;
  UNICODE_STRING unat;
  tp.u_get (&unat);
  get_nt_native_path (native_path, unat, false);
  if (append_bs)
    RtlAppendUnicodeToString (&unat, L"\\");
  mntinfo.update (&unat, NULL);

  if (mntinfo.what_fs () > none && mntinfo.what_fs () < max_fs_type)
    strcpy (_my_tls.locals.mnt_type, fs_names[mntinfo.what_fs ()].name);
  else
    strcpy (_my_tls.locals.mnt_type, mntinfo.fsname ());

  ret.mnt_type = _my_tls.locals.mnt_type;

  slashify (native_path, _my_tls.locals.mnt_fsname, false);

  /* mnt_opts is a string that details mount params such as
     binary or textmode, or exec.  We don't print
     `silent' here; it's a magic internal thing. */

  if (flags & MOUNT_TEXT)
    strcpy (_my_tls.locals.mnt_opts, (char *) "text");
  else
    strcpy (_my_tls.locals.mnt_opts, (char *) "binary");

  if (flags & MOUNT_CYGWIN_EXEC)
    strcat (_my_tls.locals.mnt_opts, (char *) ",cygexec");
  else if (flags & MOUNT_EXEC)
    strcat (_my_tls.locals.mnt_opts, (char *) ",exec");
  else if (flags & MOUNT_NOTEXEC)
    strcat (_my_tls.locals.mnt_opts, (char *) ",notexec");

  if (flags & MOUNT_NOACL)
    strcat (_my_tls.locals.mnt_opts, (char *) ",noacl");

  if (flags & MOUNT_DOS)
    strcat (_my_tls.locals.mnt_opts, (char *) ",dos");

  if (flags & MOUNT_IHASH)
    strcat (_my_tls.locals.mnt_opts, (char *) ",ihash");

  if (flags & MOUNT_NOPOSIX)
    strcat (_my_tls.locals.mnt_opts, (char *) ",posix=0");

  if (flags & MOUNT_SPARSE)
    strcat (_my_tls.locals.mnt_opts, (char *) ",sparse");

  if (!(flags & MOUNT_SYSTEM))		/* user mount */
    strcat (_my_tls.locals.mnt_opts, (char *) ",user");

  if (flags & MOUNT_CYGDRIVE)		/* cygdrive */
    strcat (_my_tls.locals.mnt_opts, (char *) ",noumount");

  if (flags & (MOUNT_AUTOMATIC | MOUNT_CYGDRIVE))
    strcat (_my_tls.locals.mnt_opts, (char *) ",auto");

  if (flags & (MOUNT_BIND))
    strcat (_my_tls.locals.mnt_opts, (char *) ",bind");

  if (flags & (MOUNT_USER_TEMP))
    strcat (_my_tls.locals.mnt_opts, (char *) ",usertemp");

  ret.mnt_opts = _my_tls.locals.mnt_opts;

  ret.mnt_freq = 1;
  ret.mnt_passno = 1;
  return &ret;
}

struct mntent *
mount_item::getmntent ()
{
  return fillout_mntent (native_path, posix_path, flags);
}

static struct mntent *
cygdrive_getmntent ()
{
  char native_path[4];
  char posix_path[CYG_MAX_PATH];
  DWORD mask = 1, drive = 'a';
  struct mntent *ret = NULL;

  while (_my_tls.locals.available_drives)
    {
      for (/* nothing */; drive <= 'z'; mask <<= 1, drive++)
	if (_my_tls.locals.available_drives & mask)
	  break;

      __small_sprintf (native_path, "%c:\\", cyg_toupper (drive));
      if (GetFileAttributes (native_path) == INVALID_FILE_ATTRIBUTES)
	{
	  _my_tls.locals.available_drives &= ~mask;
	  continue;
	}
      native_path[2] = '\0';
      __small_sprintf (posix_path, "%s%c", mount_table->cygdrive, drive);
      ret = fillout_mntent (native_path, posix_path, mount_table->cygdrive_flags);
      break;
    }

  return ret;
}

struct mntent *
mount_info::getmntent (int x)
{
  if (x < 0 || x >= nmounts)
    return cygdrive_getmntent ();

  return mount[native_sorted[x]].getmntent ();
}

/* Fill in the fields of a mount table entry.  */

void
mount_item::init (const char *native, const char *posix, unsigned mountflags)
{
  strcpy ((char *) native_path, native);
  strcpy ((char *) posix_path, posix);

  native_pathlen = strlen (native_path);
  posix_pathlen = strlen (posix_path);

  flags = mountflags;
}

/********************** Mount System Calls **************************/

/* Mount table system calls.
   Note that these are exported to the application.  */

/* mount: Add a mount to the mount table in memory and to the registry
   that will cause paths under win32_path to be translated to paths
   under posix_path. */

extern "C" int
mount (const char *win32_path, const char *posix_path, unsigned flags)
{
  /* FIXME: Should we disallow setting MOUNT_SYSTEM in flags since it
     isn't really supported except from fstab? */
  int res = -1;

  __try
    {
      if (!*posix_path)
	set_errno (EINVAL);
      else if (strpbrk (posix_path, "\\:"))
	set_errno (EINVAL);
      else if (flags & MOUNT_CYGDRIVE) /* normal mount */
	{
	  /* When flags include MOUNT_CYGDRIVE, take this to mean that
	    we actually want to change the cygdrive prefix and flags
	    without actually mounting anything. */
	  res = mount_table->write_cygdrive_info (posix_path, flags);
	  win32_path = NULL;
	}
      else if (!*win32_path)
	set_errno (EINVAL);
      else
	{
	  char *w32_path = (char *) win32_path;
	  if (flags & MOUNT_BIND)
	    {
	      /* Prepend root path to bound path. */
	      tmp_pathbuf tp;
	      device dev;

	      unsigned conv_flags = 0;
	      const char *bound_path = w32_path;

	      w32_path = tp.c_get ();
	      int error = mount_table->conv_to_win32_path (bound_path, w32_path,
							   dev, &conv_flags);
	      if (error || strlen (w32_path) >= MAX_PATH)
		return true;
	      if ((flags & ~MOUNT_SYSTEM) == MOUNT_BIND)
		flags = (MOUNT_BIND | conv_flags)
			& ~(MOUNT_IMMUTABLE | MOUNT_AUTOMATIC);
	    }
	  /* Make sure all mounts are user mounts, even those added via
	     mount -a. */
	  flags &= ~MOUNT_SYSTEM;
	  res = mount_table->add_item (w32_path, posix_path, flags);
	}

      syscall_printf ("%R = mount(%s, %s, %y)",
		      res, win32_path, posix_path, flags);
    }
  __except (EFAULT) {}
  __endtry
  return res;
}

/* umount: The standard umount call only has a path parameter.  Since
   it is not possible for this call to specify whether to remove the
   mount from the user or global mount registry table, assume the user
   table. */

extern "C" int
umount (const char *path)
{
  __try
    {
      if (!*path)
	{
	  set_errno (EINVAL);
	  __leave;
	}
      return cygwin_umount (path, 0);
    }
  __except (EFAULT) {}
  __endtry
  return -1;
}

/* cygwin_umount: This is like umount but takes an additional flags
   parameter that specifies whether to umount from the user or system-wide
   registry area. */

extern "C" int
cygwin_umount (const char *path, unsigned flags)
{
  int res = -1;

  if (!(flags & MOUNT_CYGDRIVE))
    res = mount_table->del_item (path, flags & ~MOUNT_SYSTEM);

  syscall_printf ("%R = cygwin_umount(%s, %d)", res,  path, flags);
  return res;
}

#define is_dev(d,s)	wcsncmp((d),(s),sizeof(s) - 1)

disk_type
get_disk_type (LPCWSTR dos)
{
  WCHAR dev[MAX_PATH], *d = dev;
  if (!QueryDosDeviceW (dos, dev, MAX_PATH))
    return DT_NODISK;
  if (is_dev (dev, L"\\Device\\"))
    {
      d += 8;
      switch (*d)
	{
	case L'C':
	  if (is_dev (d, L"CdRom"))
	    return DT_CDROM;
	  break;
	case L'F':
	  if (is_dev (d, L"Floppy"))
	    return DT_FLOPPY;
	  break;
	case L'H':
	  if (is_dev (d, L"Harddisk"))
	    return DT_HARDDISK;
	  break;
	case L'L':
	  if (is_dev (d, L"LanmanRedirector\\"))
	    return DT_SHARE_SMB;
	  break;
	case L'M':
	  if (is_dev (d, L"MRxNfs\\"))
	    return DT_SHARE_NFS;
	  break;
	}
    }
  return DT_NODISK;
}

extern "C" FILE *
setmntent (const char *filep, const char *)
{
  _my_tls.locals.iteration = 0;
  _my_tls.locals.available_drives = GetLogicalDrives ();
  /* Filter floppy drives on A: and B: */
  if ((_my_tls.locals.available_drives & 1)
      && get_disk_type (L"A:") == DT_FLOPPY)
    _my_tls.locals.available_drives &= ~1;
  if ((_my_tls.locals.available_drives & 2)
      && get_disk_type (L"B:") == DT_FLOPPY)
    _my_tls.locals.available_drives &= ~2;
  return (FILE *) filep;
}

extern "C" struct mntent *
getmntent (FILE *)
{
  return mount_table->getmntent (_my_tls.locals.iteration++);
}

extern "C" struct mntent *
getmntent_r (FILE *, struct mntent *mntbuf, char *buf, int buflen)
{
  struct mntent *mnt = mount_table->getmntent (_my_tls.locals.iteration++);
  int fsname_len, dir_len, type_len, tmplen = buflen;

  if (!mnt)
    return NULL;

  fsname_len = strlen (mnt->mnt_fsname) + 1;
  dir_len = strlen (mnt->mnt_dir) + 1;
  type_len = strlen (mnt->mnt_type) + 1;

  snprintf (buf, buflen, "%s%c%s%c%s%c%s", mnt->mnt_fsname, '\0',
	    mnt->mnt_dir, '\0', mnt->mnt_type, '\0', mnt->mnt_opts);

  mntbuf->mnt_fsname = buf;
  tmplen -= fsname_len;
  mntbuf->mnt_dir = tmplen > 0 ? buf + fsname_len : (char *)"";
  tmplen -= dir_len;
  mntbuf->mnt_type = tmplen > 0 ? buf + fsname_len + dir_len : (char *)"";
  tmplen -= type_len;
  mntbuf->mnt_opts = tmplen > 0 ? buf + fsname_len + dir_len + type_len : (char *)"";
  mntbuf->mnt_freq = mnt->mnt_freq;
  mntbuf->mnt_passno = mnt->mnt_passno;
  return mntbuf;
}

extern "C" int
endmntent (FILE *)
{
  return 1;
}

dos_drive_mappings::dos_drive_mappings ()
: mappings(0)
{
  tmp_pathbuf tp;
  wchar_t vol[64]; /* Long enough for Volume GUID string */
  wchar_t *devpath = tp.w_get ();
  wchar_t *mounts = tp.w_get ();

  /* Iterate over all volumes, fetch the first path from the list of
     DOS paths the volume is mounted to, or use the GUID volume path
     otherwise. */
  HANDLE sh = FindFirstVolumeW (vol, 64);
  if (sh == INVALID_HANDLE_VALUE)
    debug_printf ("FindFirstVolumeW, %E");
  else {
    do
      {
	/* Skip drives which are not mounted. */
	DWORD len;
	if (!GetVolumePathNamesForVolumeNameW (vol, mounts, NT_MAX_PATH, &len)
	    || mounts[0] == L'\0')
	  continue;
	*wcsrchr (vol, L'\\') = L'\0';
	if (QueryDosDeviceW (vol + 4, devpath, NT_MAX_PATH))
	  {
	    /* The DOS drive mapping can be another symbolic link.  If so,
	       the mapping won't work since the section name is the name
	       after resolving all symlinks.  Resolve symlinks here, too. */
	    for (int syml_cnt = 0; syml_cnt < SYMLOOP_MAX; ++syml_cnt)
	      {
		UNICODE_STRING upath;
		OBJECT_ATTRIBUTES attr;
		NTSTATUS status;
		HANDLE h;

		RtlInitUnicodeString (&upath, devpath);
		InitializeObjectAttributes (&attr, &upath,
					    OBJ_CASE_INSENSITIVE, NULL, NULL);
		status = NtOpenSymbolicLinkObject (&h, SYMBOLIC_LINK_QUERY,
						   &attr);
		if (!NT_SUCCESS (status))
		  break;
		RtlInitEmptyUnicodeString (&upath, devpath, (NT_MAX_PATH - 1)
							    * sizeof (WCHAR));
		status = NtQuerySymbolicLinkObject (h, &upath, NULL);
		NtClose (h);
		if (!NT_SUCCESS (status))
		  break;
		devpath[upath.Length / sizeof (WCHAR)] = L'\0';
	      }
	    mapping *m = new mapping ();
	    if (m)
	      {
		m->dospath = wcsdup (mounts);
		m->ntdevpath = wcsdup (devpath);
		if (!m->dospath || !m->ntdevpath)
		  {
		    free (m->dospath);
		    free (m->ntdevpath);
		    delete m;
		    continue;
		  }
		m->doslen = wcslen (m->dospath);
		m->dospath[--m->doslen] = L'\0'; /* Drop trailing backslash */
		m->ntlen = wcslen (m->ntdevpath);
		m->next = mappings;
		mappings = m;
	      }
	  }
	else
	  debug_printf ("Unable to determine the native mapping for %ls "
			"(error %u)", vol, GetLastError ());
      }
    while (FindNextVolumeW (sh, vol, 64));
    FindVolumeClose (sh);
  }
}

wchar_t *
dos_drive_mappings::fixup_if_match (wchar_t *path)
{
  /* Check for network drive first. */
  if (!wcsncmp (path, L"\\Device\\Mup\\", 12))
    {
      path += 10;
      path[0] = L'\\';
      return path;
    }
  /* Then test local drives. */
  for (mapping *m = mappings; m; m = m->next)
    if (!wcsncmp (m->ntdevpath, path, m->ntlen))
      {
	wchar_t *tmppath;

	if (m->ntlen > m->doslen)
	  wcsncpy (path += m->ntlen - m->doslen, m->dospath, m->doslen);
	else if ((tmppath = wcsdup (path + m->ntlen)) != NULL)
	  {
	    wcpcpy (wcpcpy (path, m->dospath), tmppath);
	    free (tmppath);
	  }
	break;
      }
  return path;
}

dos_drive_mappings::~dos_drive_mappings ()
{
  mapping *n = 0;
  for (mapping *m = mappings; m; m = n)
    {
      n = m->next;
      free (m->dospath);
      free (m->ntdevpath);
      delete m;
    }
}
