/* evalfile.c - read and evaluate commands from a file or file descriptor */

/* Copyright (C) 1996-2017,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/>.
*/

#include <config.h>

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

#include "../bashtypes.h"
#include "posixstat.h"
#include "filecntl.h"

#include <stdio.h>
#include <signal.h>
#include <errno.h>

#include "../bashansi.h"
#include "../bashintl.h"

#include "../shell.h"
#include "../parser.h"
#include "../jobs.h"
#include "../builtins.h"
#include "../flags.h"
#include "../input.h"
#include "../execute_cmd.h"
#include "../trap.h"

#include <y.tab.h>

#if defined (HISTORY)
#  include "../bashhist.h"
#endif

#include <typemax.h>

#include "common.h"

#if !defined (errno)
extern int errno;
#endif

/* Flags for evalfile_internal() */
#define FEVAL_ENOENTOK		0x001
#define FEVAL_BUILTIN		0x002
#define FEVAL_UNWINDPROT	0x004
#define FEVAL_NONINT		0x008
#define FEVAL_LONGJMP		0x010
#define FEVAL_HISTORY		0x020
#define FEVAL_CHECKBINARY	0x040
#define FEVAL_REGFILE		0x080
#define FEVAL_NOPUSHARGS	0x100
#define FEVAL_RETRY		0x200

/* How many `levels' of sourced files we have. */
int sourcelevel = 0;

static int
evalfile_internal (const char *filename, int flags)
{
  volatile int old_interactive;
  procenv_t old_return_catch;
  int return_val, fd, result, pflags, nnull;
  size_t i;
  ssize_t nr;			/* return value from read(2) */
  char *string;
  struct stat finfo;
  size_t file_size;
  sh_vmsg_func_t *errfunc;
#if defined (ARRAY_VARS)
  SHELL_VAR *funcname_v, *bash_source_v, *bash_lineno_v;
  ARRAY *funcname_a, *bash_source_a, *bash_lineno_a;
  struct func_array_state *fa;
#  if defined (DEBUGGER)
  SHELL_VAR *bash_argv_v, *bash_argc_v;
  ARRAY *bash_argv_a, *bash_argc_a;
#  endif
  char *t, tt[2];
#endif

  USE_VAR(pflags);

  errno = 0;
  do
    {
      fd = open (filename, O_RDONLY);
      result = errno;
      if (fd < 0 && result == EINTR)
        QUIT;
      errno = result;
    }
  while (fd < 0 && errno == EINTR && (flags & FEVAL_RETRY));

  if (fd < 0 || (fstat (fd, &finfo) == -1))
    {
      result = errno;
      if (fd >= 0)
	close (fd);
      errno = result;

file_error_and_exit:
      if (((flags & FEVAL_ENOENTOK) == 0) || errno != ENOENT)
	file_error (filename);

      if (flags & FEVAL_LONGJMP)
	{
	  last_command_exit_value = EXECUTION_FAILURE;
	  jump_to_top_level (EXITPROG);
	}

      return ((flags & FEVAL_BUILTIN) ? EXECUTION_FAILURE
      				      : ((errno == ENOENT && (flags & FEVAL_ENOENTOK) != 0) ? 0 : -1));
    }

  errfunc = ((flags & FEVAL_BUILTIN) ? builtin_error : internal_error);

  if (S_ISDIR (finfo.st_mode))
    {
      (*errfunc) (_("%s: is a directory"), filename);
      close (fd);
      return ((flags & FEVAL_BUILTIN) ? EXECUTION_FAILURE : -1);
    }
  else if ((flags & FEVAL_REGFILE) && S_ISREG (finfo.st_mode) == 0)
    {
      (*errfunc) (_("%s: not a regular file"), filename);
      close (fd);
      return ((flags & FEVAL_BUILTIN) ? EXECUTION_FAILURE : -1);
    }

  file_size = (size_t)finfo.st_size;
  /* Check for overflow with large files. */
  if (file_size != finfo.st_size || file_size + 1 < file_size)
    {
      (*errfunc) (_("%s: file is too large"), filename);
      close (fd);
      return ((flags & FEVAL_BUILTIN) ? EXECUTION_FAILURE : -1);
    }

  if (S_ISREG (finfo.st_mode) && file_size <= SSIZE_MAX)
    {
      string = (char *)xmalloc (1 + file_size);
      nr = read (fd, string, file_size);
      if (nr >= 0)
	string[nr] = '\0';
      if (nr != file_size)
	nr = -1;		/* XXX - didn't get the whole file */
    }
  else
    nr = zmapfd (fd, &string, 0);

  return_val = errno;
  close (fd);
  errno = return_val;

  if (nr < 0)		/* XXX was != file_size, not < 0 */
    {
      free (string);
      goto file_error_and_exit;
    }

  if (nr == 0)
    {
      free (string);
      return ((flags & FEVAL_BUILTIN) ? EXECUTION_SUCCESS : 1);
    }
      
  if ((flags & FEVAL_CHECKBINARY) && 
      check_binary_file (string, (nr > 80) ? 80 : nr))
    {
      free (string);
      (*errfunc) ("%s: %s", filename, _("cannot execute binary file"));
      return ((flags & FEVAL_BUILTIN) ? EX_BINARY_FILE : -1);
    }

  i = strlen (string);
  if (i < nr)
    {
      for (nnull = i = 0; i < nr; i++)
	if (string[i] == '\0')
          {
	    memmove (string+i, string+i+1, nr - i);
	    nr--;
	    /* Even if the `check binary' flag is not set, we want to avoid
	       sourcing files with more than 256 null characters -- that
	       probably indicates a binary file. */
	    if ((flags & FEVAL_BUILTIN) && ++nnull > 256)
	      {
		free (string);
		(*errfunc) ("%s: %s", filename, _("cannot execute binary file"));
		return ((flags & FEVAL_BUILTIN) ? EX_BINARY_FILE : -1);
	      }
          }
    }

  if (flags & FEVAL_UNWINDPROT)
    {
      begin_unwind_frame ("evalfile_internal");

      unwind_protect_int (return_catch_flag);
      unwind_protect_jmp_buf (return_catch);
      if (flags & FEVAL_NONINT)
	unwind_protect_int (interactive);
      unwind_protect_int (sourcelevel);
      unwind_protect_int (want_job_notifications);
      unwind_protect_int (retain_fifos);
    }
  else
    {
      COPY_PROCENV (return_catch, old_return_catch);
      if (flags & FEVAL_NONINT)
	old_interactive = interactive;
    }

  if (flags & FEVAL_NONINT)
    interactive = 0;

  return_catch_flag++;
  sourcelevel++;
  if (interactive_shell && shell_compatibility_level <= 52)
    want_job_notifications++;

  retain_fifos++;			/* XXX */

#if defined (ARRAY_VARS)
  GET_ARRAY_FROM_VAR ("FUNCNAME", funcname_v, funcname_a);
  GET_ARRAY_FROM_VAR ("BASH_SOURCE", bash_source_v, bash_source_a);
  GET_ARRAY_FROM_VAR ("BASH_LINENO", bash_lineno_v, bash_lineno_a);
#  if defined (DEBUGGER)
  GET_ARRAY_FROM_VAR ("BASH_ARGV", bash_argv_v, bash_argv_a);
  GET_ARRAY_FROM_VAR ("BASH_ARGC", bash_argc_v, bash_argc_a);
#  endif

  push_source (bash_source_a, (char *)filename);
  t = itos (executing_line_number ());
  array_push (bash_lineno_a, t);
  free (t);
  array_push (funcname_a, "source");	/* not exactly right */

  fa = (struct func_array_state *)xmalloc (sizeof (struct func_array_state));
  fa->source_a = bash_source_a;
  fa->source_v = bash_source_v;
  fa->lineno_a = bash_lineno_a;
  fa->lineno_v = bash_lineno_v;
  fa->funcname_a = funcname_a;
  fa->funcname_v = funcname_v;
  if (flags & FEVAL_UNWINDPROT)
    add_unwind_protect (uw_restore_funcarray_state, fa);

#  if defined (DEBUGGER)
  /* Have to figure out a better way to do this when `source' is supplied
     arguments */
  if ((flags & FEVAL_NOPUSHARGS) == 0)
    {
      if (shell_compatibility_level <= 44)
	init_bash_argv ();
      array_push (bash_argv_a, (char *)filename);	/* XXX - unconditionally? */
      tt[0] = '1'; tt[1] = '\0';
      array_push (bash_argc_a, tt);
      if (flags & FEVAL_UNWINDPROT)
	add_unwind_protect (uw_pop_args, 0);
    }
#  endif
#endif

  /* set the flags to be passed to parse_and_execute */
  pflags = SEVAL_RESETLINE|SEVAL_NOOPTIMIZE;
  pflags |= (flags & FEVAL_HISTORY) ? 0 : SEVAL_NOHIST;

  if (flags & FEVAL_BUILTIN)
    result = EXECUTION_SUCCESS;

  return_val = setjmp_nosigs (return_catch);

  /* If `return' was seen outside of a function, but in the script, then
     force parse_and_execute () to clean up. */
  if (return_val)
    {
      parse_and_execute_cleanup (-1);
      result = return_catch_value;
    }
  else
    result = parse_and_execute (string, filename, pflags);

  if (flags & FEVAL_UNWINDPROT)
    run_unwind_frame ("evalfile_internal");
  else
    {
      if (flags & FEVAL_NONINT)
	interactive = old_interactive;
#if defined (ARRAY_VARS)
      restore_funcarray_state (fa);
#  if defined (DEBUGGER)
      if ((flags & FEVAL_NOPUSHARGS) == 0)
	{
	  /* Don't need to call pop_args here until we do something better
	     when source is passed arguments (see above). */
	  array_pop (bash_argc_a);
	  array_pop (bash_argv_a);
	}
#  endif
#endif
      return_catch_flag--;
      sourcelevel--;
      if (interactive_shell && shell_compatibility_level <= 52)
	want_job_notifications--;
      retain_fifos--;
      COPY_PROCENV (old_return_catch, return_catch);
    }

  /* If we end up with EOF after sourcing a file, which can happen when the file
     doesn't end with a newline, pretend that it did. */
  if (current_token == yacc_EOF)
    push_token ('\n');		/* XXX */

  return ((flags & FEVAL_BUILTIN) ? result : 1);
}

int
maybe_execute_file (const char *fname, int force_noninteractive)
{
  char *filename;
  int result, flags;

  filename = bash_tilde_expand (fname, 0);
  flags = FEVAL_ENOENTOK|FEVAL_RETRY;
  if (force_noninteractive)
    flags |= FEVAL_NONINT;
  result = evalfile_internal (filename, flags);
  free (filename);
  return result;
}

int
force_execute_file (const char *fname, int force_noninteractive)
{
  char *filename;
  int result, flags;

  filename = bash_tilde_expand (fname, 0);
  flags = FEVAL_RETRY;
  if (force_noninteractive)
    flags |= FEVAL_NONINT;
  result = evalfile_internal (filename, flags);
  free (filename);
  return result;
}

#if defined (HISTORY)
int
fc_execute_file (const char *filename)
{
  int flags;

  /* We want these commands to show up in the history list if
     remember_on_history is set.  We use FEVAL_BUILTIN to return
     the result of parse_and_execute. */
  flags = FEVAL_ENOENTOK|FEVAL_HISTORY|FEVAL_REGFILE|FEVAL_BUILTIN;
  return (evalfile_internal (filename, flags));
}
#endif /* HISTORY */

int
source_file (const char *filename, int sflags)
{
  int flags, rval;

  flags = FEVAL_BUILTIN|FEVAL_UNWINDPROT|FEVAL_NONINT;
  if (sflags)
    flags |= FEVAL_NOPUSHARGS;
  /* POSIX shells exit if non-interactive and file error. */
  if (posixly_correct && interactive_shell == 0 && executing_command_builtin == 0)
    flags |= FEVAL_LONGJMP;
  rval = evalfile_internal (filename, flags);

  run_return_trap ();
  return rval;
}
