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

/* Copyright (C) 2006 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 2, 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; see the file COPYING.  If not, write to the Free Software
   Foundation, 59 Temple Place, Suite 330, Boston, MA 02111 USA. */

#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 __P((const char *));
static int sh_stataccess __P((char *, int));
#if HAVE_DECL_SETREGID
static int sh_euidaccess __P((char *, int));
#endif

static int
path_is_devfd (path)
     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 (path, finfo)
     const char *path;
     struct stat *finfo;
{
  if (*path == '\0')
    {
      errno = ENOENT;
      return (-1);
    }
  if (path[0] == '/' && path[1] == 'd' && strncmp (path, "/dev/fd/", 8) == 0)
    {
#if !defined (HAVE_DEV_FD)
      intmax_t fd;
      int r;

      if (legal_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. */
      char pbuf[32];
      strcpy (pbuf, DEV_FD_PREFIX);
      strcat (pbuf, 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 (path, mode)
     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 (path, mode)
     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 (path, mode)
     char *path;
     int mode;
{
  if (path_is_devfd (path))
    return (sh_stataccess (path, mode));

#if defined (HAVE_EACCESS)		/* FreeBSD */
  return (eaccess (path, mode));
#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)
    return (access (path, mode));  

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