/* Code related to unix files.
   Copyright (C) 1987,1991 Free Software Foundation, Inc.

   This file is part of Q.
   The code is derived from 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 1, 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, 675 Mass Ave, Cambridge, MA 02139, USA. */
#include "config.h"

#include "shelldefs.h"
/*#include "exceptions.h"*/

#include "trap.h"
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <ctype.h>
#include <sys/types.h>
extern char* getenv(const char*);

#if !defined (USG) || defined (HAVE_RESOURCE)
#include <sys/time.h>
#endif /* USG */

#if !defined (_POSIX_VERSION)
#  if defined (HAVE_RESOURCE)
#    include <sys/resource.h>
#  endif
#endif /* _POSIX_VERSION */

#include <sys/file.h>
#include <posixstat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/param.h>

/* DOT_FOUND_IN_SEARCH becomes non-zero when find_user_command ()
   encounters a `.' as the directory pathname while scanning the
   list of possible pathnames; i.e., if `.' comes before the directory
   containing the file of interest. */
int dot_found_in_search = 0;

#define u_mode_bits(x) (((x) & 0000700) >> 6)
#define g_mode_bits(x) (((x) & 0000070) >> 3)
#define o_mode_bits(x) (((x) & 0000007) >> 0)
#define X_BIT(x) (x & 1)

/* Return some flags based on information about this file.
   The EXISTS bit is non-zero if the file is found.
   The EXECABLE bit is non-zero the file is executble.
   Zero is returned if the file is not found. */
int
file_status (name)
     char *name;
{
  struct stat finfo;
  static int user_id = -1;

  /* Determine whether this file exists or not. */
  if (stat (name, &finfo) < 0)
    return (0);

  /* If the file is a directory, then it is not "executable" in the
     sense of the shell. */
  if (S_ISDIR (finfo.st_mode))
    return (FS_EXISTS);

  /* Find out if the file is actually executable.  By definition, the
     only other criteria is that the file has an execute bit set that
     we can use. */
  if (user_id == -1)
    user_id = geteuid ();

  /* Root only requires execute permission for any of owner, group or
     others to be able to exec a file. */
  if (user_id == 0)
    {
      int bits;

      bits = (u_mode_bits (finfo.st_mode) |
	      g_mode_bits (finfo.st_mode) |
	      o_mode_bits (finfo.st_mode));

      if (X_BIT (bits))
	return (FS_EXISTS | FS_EXECABLE);
    }

  /* If we are the owner of the file, the owner execute bit applies. */
  if (user_id == finfo.st_uid && X_BIT (u_mode_bits (finfo.st_mode)))
    return (FS_EXISTS | FS_EXECABLE);

  /* If we are in the owning group, the group permissions apply. */
  if (group_member (finfo.st_gid) && X_BIT (g_mode_bits (finfo.st_mode)))
    return (FS_EXISTS | FS_EXECABLE);

  /* If `others' have execute permission to the file, then so do we,
     since we are also `others'. */
  if (X_BIT (o_mode_bits (finfo.st_mode)))
    return (FS_EXISTS | FS_EXECABLE);
  else
    return (FS_EXISTS);
}

/* Return non-zero if FILE exists and is executable.
   Note that this function is the definition of what an
   executable file is; do not change this unless YOU know
   what an executable file is. */
int
executable_file (file)
     char *file;
{
  if (file_status (file) & FS_EXECABLE)
    return (1);
  else
    return (0);
}

/* Return 1 if STRING is an absolute program name; it is absolute if it
   contains any slashes.  This is used to decide whether or not to look
   up through $PATH. */
int
absolute_program (string)
     char *string;
{
  return ((char *)index (string, '/') != (char *)NULL);
}

/* This does the dirty work for find_path_file () and find_user_command ().
   NAME is the name of the file to search for.
   PATH_LIST is a colon separated list of directories to search.
   FLAGS contains bit fields which control the files which are eligible.
   Some values are:
      FS_EXEC_ONLY:		The file must be an executable to be found.
      FS_EXEC_PREFERRED:	If we can't find an executable, then the
				the first file matching NAME will do.
      FS_EXISTS:		The first file found will do.
*/
char *
find_user_command_in_path (name, path_list, flags)
     char *name;
     char *path_list;
     int flags;
{
  extern char *extract_colon_unit ();
  char *full_path, *path, *file_to_lose_on;
  int status, path_index, name_len;
  struct stat finfo;

  name_len = strlen (name);

  /* The file name which we would try to execute, except that it isn't
     possible to execute it.  This is the first file that matches the
     name that we are looking for while we are searching $PATH for a
     suitable one to execute.  If we cannot find a suitable executable
     file, then we use this one. */
  file_to_lose_on = (char *)NULL;

  /* We haven't started looking, so we certainly haven't seen
     a `.' as the directory path yet. */
  dot_found_in_search = 0;

  if (absolute_program (name))
    {
      full_path = (char *)xmalloc (1 + name_len);
      strcpy (full_path, name);

      status = file_status (full_path);

      if (!(status & FS_EXISTS))
	return (0);

      if ((flags & FS_EXEC_ONLY) && (status & FS_EXECABLE))
	return (full_path);
      else
	{
	  free (full_path);
	  return ((char *)NULL);
	}
    }

  /* Find out the location of the current working directory. */
  stat (".", &finfo);

  path_index = 0;
  while (path_list && path_list[path_index])
    {
      path = extract_colon_unit (path_list, &path_index);

      if (!path || !*path)
	{
	  if (path)
	    free (path);

	  path = savestring ("."); /* By definition. */
	}

      if (*path == '~')
	{
	  char *tilde_expand ();
	  char *t = tilde_expand (path);
	  free (path);
	  path = t;
	}

#if 0
      /* Remember the location of "." in the path, in all its forms
	 (as long as they begin with a `.', e.g. `./.') */
      if ((*path == '.') &&
	  same_file (".", path, &finfo, (struct stat *)NULL))
	dot_found_in_search = 1;
#endif

      full_path = (char *)xmalloc (2 + strlen (path) + name_len);
      sprintf (full_path, "%s/%s", path, name);
      free (path);

      status = file_status (full_path);

      if (!(status & FS_EXISTS))
	goto next_file;

      /* The file exists.  If the caller simply wants the first file,
	 here it is. */
      if (flags & FS_EXISTS)
	return (full_path);

       /* If the file is executable, then it satisfies the cases of
	  EXEC_ONLY and EXEC_PREFERRED.  Return this file unconditionally. */
      if (status & FS_EXECABLE)
	{
	  if (file_to_lose_on)
	    free (file_to_lose_on);

	  return (full_path);
	}

      /* The file is not executable, but it does exist.  If we prefer
	 an executable, then remember this one if it is the first one
	 we have found. */
      if (flags & FS_EXEC_PREFERRED)
	{
	  if (!file_to_lose_on)
	    file_to_lose_on = savestring (full_path);
	}

    next_file:
      free (full_path);
    }

  /* We didn't find exactly what the user was looking for.  Return
     the contents of FILE_TO_LOSE_ON which is NULL when the search
     required an executable, or non-NULL if a file was found and the
     search would accept a non-executable as a last resort. */
  return (file_to_lose_on);
}

int
check_executable_file(char *name)
{
    char *path_list = getenv("PATH");
    char *path;
    if (!path_list) return 0;
    path = find_user_command_in_path (name, path_list, FS_EXEC_ONLY);
    if (!path) return 0;
    free(path);
    return 1;
}

#if defined (HAVE_MULTIPLE_GROUPS)
/* The number of groups that this user is a member of. */
static int ngroups = 0;
static gid_t *group_array = (gid_t *)NULL;
static int default_group_array_size = 0;
#endif /* HAVE_MULTIPLE_GROUPS */

/* Return non-zero if GID is one that we have in our groups list. */
int
group_member (gid)
     gid_t gid;
{
#if !defined (HAVE_MULTIPLE_GROUPS)
  return ((gid == getgid ()) || (gid == getegid ()));
#else
  register int i;

  /* getgroups () returns the number of elements that it was able to
     place into the array.  We simply continue to call getgroups ()
     until the number of elements placed into the array is smaller than
     the physical size of the array. */

  while (ngroups == default_group_array_size)
    {
      default_group_array_size += 64;

      group_array = (gid_t *)
	xrealloc (group_array,
		  default_group_array_size * sizeof (gid_t));

      ngroups = getgroups (default_group_array_size, group_array);
    }

  /* In case of error, the user loses. */
  if (ngroups < 0)
    return (0);

  /* Search through the list looking for GID. */
  for (i = 0; i < ngroups; i++)
    if (gid == group_array[i])
      return (1);

  return (0);
#endif /* HAVE_MULTIPLE_GROUPS */
}

/* Given a string containing units of information separated by colons,
   return the next one pointed to by INDEX, or NULL if there are no more.
   Advance INDEX to the character after the colon. */
char *
extract_colon_unit (string, index)
     char *string;
     int *index;
{
  int i, start;

  i = *index;

  if (!string || (i >= strlen (string)))
    return ((char *)NULL);

  /* Each call to this routine leaves the index pointing at a colon if
     there is more to the path.  If I is > 0, then increment past the
     `:'.  (If I is 0, then the path has a leading colon.  If this is
     not done, the second call to this routine will always return NULL,
     which will be translated to  `.', even if `.' is not in the path.
     Trailing colons are handled OK by the `else' part of the if
     statement; it returns an empty string for the last component of a
     path with a trailing colon, and the routines that call this will
     translate that to `.'. */

  if (i && string[i] == ':')
    i++;

  start = i;

  while (string[i] && string[i] != ':') i++;

  *index = i;

  if (i == start)
    {
      if (!string[i])
	return ((char *)NULL);

      (*index)++;

      return (savestring (""));
    }
  else
    {
      char *value;

      value = (char *)xmalloc (1 + (i - start));
      strncpy (value, &string[start], (i - start));
      value [i - start] = '\0';

      return (value);
    }
}
