/* eaccess.c - eaccess replacement for the shell, plus other access functions. */

/* Copyright (C) 2006-2020,2022-2024 Free Software Foundation, Inc.

   This file is part of GNU Bash, the Bourne Again SHell.

   Bash is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   Bash is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with Bash.  If not, see <http://www.gnu.org/licenses/>.
*/

#if defined (HAVE_CONFIG_H)
#  include <config.h>
#endif

#include <stdio.h>

#include "bashtypes.h"

#if defined (HAVE_UNISTD_H)
#  include <unistd.h>
#endif

#include "bashansi.h"

#include <errno.h>
#if !defined (errno)
extern int errno;
#endif /* !errno */

#if !defined (_POSIX_VERSION) && defined (HAVE_SYS_FILE_H)
#  include <sys/file.h>
#endif /* !_POSIX_VERSION */
#include "posixstat.h"
#include "filecntl.h"

#include "shell.h"

#if !defined (R_OK)
#define R_OK 4
#define W_OK 2
#define X_OK 1
#define F_OK 0
#endif /* R_OK */

static int path_is_devfd (const char *);
static int sh_stataccess (const char *, int);
#if HAVE_DECL_SETREGID
static int sh_euidaccess (const char *, int);
#endif

static int
path_is_devfd (const char *path)
{
  if (path[0] == '/' && path[1] == 'd' && strncmp (path, "/dev/fd/", 8) == 0)
    return 1;
  else if (STREQN (path, "/dev/std", 8))
    {
      if (STREQ (path+8, "in") || STREQ (path+8, "out") || STREQ (path+8, "err"))
	return 1;
      else
	return 0;
    }
  else
    return 0;
}

/* A wrapper for stat () which disallows pathnames that are empty strings
   and handles /dev/fd emulation on systems that don't have it. */
int
sh_stat (const char *path, struct stat *finfo)
{
  static char *pbuf = 0;

  if (*path == '\0')
    {
      errno = ENOENT;
      return (-1);
    }
  if (path[0] == '/' && path[1] == 'd' && strncmp (path, "/dev/fd/", 8) == 0)
    {
      /* If stating /dev/fd/n doesn't produce the same results as fstat of
	 FD N, then define DEV_FD_STAT_BROKEN */
#if !defined (HAVE_DEV_FD) || defined (DEV_FD_STAT_BROKEN)
      intmax_t fd;
      int r;

      if (valid_number (path + 8, &fd) && fd == (int)fd)
        {
          r = fstat ((int)fd, finfo);
          if (r == 0 || errno != EBADF)
            return (r);
        }
      errno = ENOENT;
      return (-1);
#else
  /* If HAVE_DEV_FD is defined, DEV_FD_PREFIX is defined also, and has a
     trailing slash.  Make sure /dev/fd/xx really uses DEV_FD_PREFIX/xx.
     On most systems, with the notable exception of linux, this is
     effectively a no-op. */
      pbuf = xrealloc (pbuf, sizeof (DEV_FD_PREFIX) + strlen (path + 8));
      strcpy (pbuf, DEV_FD_PREFIX);
      strcpy (pbuf + sizeof (DEV_FD_PREFIX) - 1, path + 8);
      return (stat (pbuf, finfo));
#endif /* !HAVE_DEV_FD */
    }
#if !defined (HAVE_DEV_STDIN)
  else if (STREQN (path, "/dev/std", 8))
    {
      if (STREQ (path+8, "in"))
	return (fstat (0, finfo));
      else if (STREQ (path+8, "out"))
	return (fstat (1, finfo));
      else if (STREQ (path+8, "err"))
	return (fstat (2, finfo));
      else
	return (stat (path, finfo));
    }
#endif /* !HAVE_DEV_STDIN */
  return (stat (path, finfo));
}

/* Do the same thing access(2) does, but use the effective uid and gid,
   and don't make the mistake of telling root that any file is
   executable.  This version uses stat(2). */
static int
sh_stataccess (const char *path, int mode)
{
  struct stat st;

  if (sh_stat (path, &st) < 0)
    return (-1);

  if (current_user.euid == 0)
    {
      /* Root can read or write any file. */
      if ((mode & X_OK) == 0)
	return (0);

      /* Root can execute any file that has any one of the execute
	 bits set. */
      if (st.st_mode & S_IXUGO)
	return (0);
    }

  if (st.st_uid == current_user.euid)	/* owner */
    mode <<= 6;
  else if (group_member (st.st_gid))
    mode <<= 3;

  if (st.st_mode & mode)
    return (0);

  errno = EACCES;
  return (-1);
}

#if HAVE_DECL_SETREGID
/* Version to call when uid != euid or gid != egid.  We temporarily swap
   the effective and real uid and gid as appropriate. */
static int
sh_euidaccess (const char *path, int mode)
{
  int r, e;

  if (current_user.uid != current_user.euid)
    setreuid (current_user.euid, current_user.uid);
  if (current_user.gid != current_user.egid)
    setregid (current_user.egid, current_user.gid);

  r = access (path, mode);
  e = errno;

  if (current_user.uid != current_user.euid)
    setreuid (current_user.uid, current_user.euid);
  if (current_user.gid != current_user.egid)
    setregid (current_user.gid, current_user.egid);

  errno = e;
  return r;  
}
#endif

int
sh_eaccess (const char *path, int mode)
{
  int ret;

  if (path_is_devfd (path))
    return (sh_stataccess (path, mode));

#if (defined (HAVE_FACCESSAT) && defined (AT_EACCESS)) || defined (HAVE_EACCESS)
#  if defined (HAVE_FACCESSAT) && defined (AT_EACCESS)
  ret = faccessat (AT_FDCWD, path, mode, AT_EACCESS);
#  else		/* HAVE_EACCESS */	/* FreeBSD */
  ret = eaccess (path, mode);	/* XXX -- not always correct for X_OK */
#  endif	/* HAVE_EACCESS */
#  if defined (__FreeBSD__) || defined (SOLARIS) || defined (_AIX)
  if (ret == 0 && current_user.euid == 0 && mode == X_OK)
    return (sh_stataccess (path, mode));
#  endif	/* __FreeBSD__ || SOLARIS || _AIX */
  return ret;
#elif defined (EFF_ONLY_OK)		/* SVR4(?), SVR4.2 */
  return access (path, mode|EFF_ONLY_OK);
#else
  if (mode == F_OK)
    return (sh_stataccess (path, mode));
    
#  if HAVE_DECL_SETREGID
  if (current_user.uid != current_user.euid || current_user.gid != current_user.egid)
    return (sh_euidaccess (path, mode));
#  endif

  if (current_user.uid == current_user.euid && current_user.gid == current_user.egid)
    {
      ret = access (path, mode);
#if defined (__FreeBSD__) || defined (SOLARIS)
      if (ret == 0 && current_user.euid == 0 && mode == X_OK)
	return (sh_stataccess (path, mode));
#endif
      return ret;
    }

  return (sh_stataccess (path, mode));
#endif
}
