/*
 * Copyright (c) 2001-2004 The Trustees of Indiana University.  
 *                         All rights reserved.
 * Copyright (c) 1998-2001 University of Notre Dame. 
 *                         All rights reserved.
 * Copyright (c) 1994-1998 The Ohio State University.  
 *                         All rights reserved.
 * 
 * This file is part of the LAM/MPI software package.  For license
 * information, see the LICENSE file in the top level directory of the
 * LAM/MPI source distribution.
 * 
 * $HEADER$
 *
 * Id: asc_parse.c,v 6.33 2004/01/09 18:03:34 jsquyres Exp $
 *
 *	Function:	- application schema parsing
 */

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

#include <all_list.h>
#include <all_opt.h>
#include <args.h>
#include <app_schema.h>
#include <lam.h>
#include <ndi.h>
#include <sfh.h>
#include <lamdebug.h>

/*
 * local functions
 */
static int		parseline(LIST *, char *, int, char **);
static int		add_var(int *, char ***, char *);

/*
 *	asc_parse
 *
 *	Function:	- parse an application schema
 *			- returns line # in case of parsing error
 *	Accepts:	- app. schema file 
 *			- ptr line # (returned value)
 *	Returns:	- app. schema descriptor or NULL
 */
LIST *
asc_parse(const char *appfile, int *plineno, char **env)
{
	int		fd;			/* file desc. */
	int		nbytes;			/* # bytes read */
	int		bufsize;		/* buffer size */
	char		*filebuf;		/* file buffer */
	struct stat	fileinfo;		/* file information */
	LIST		*appd;			/* app. schema desc. */
/*
 * Get file info.
 */
	if (stat(appfile, &fileinfo)) return((LIST *) 0);

	bufsize = (int) fileinfo.st_size;
	if (bufsize <= 0) return((LIST *) 0);
/*
 * Allocate a file buffer.
 */
	filebuf = malloc((unsigned) bufsize);
	if (filebuf == 0) return((LIST *) 0);
/*
 * Open and read the file.
 */
	fd = open(appfile, O_RDONLY, 0);
	if (fd < 0) {
		free(filebuf);
		return((LIST *) 0);
	}

	nbytes = read(fd, filebuf, bufsize);
	close(fd);

	if (nbytes < bufsize) {
		if (nbytes >= 0) errno = EIO;
		free(filebuf);
		return((LIST *) 0);
	}
/*
 * Parse the app. schema buffer.
 */
	appd = asc_bufparse(filebuf, bufsize, plineno, env);

	free(filebuf);
	return(appd);
}

/*
 *	asc_bufparse
 *
 *	Function:	- parse an application schema from a buffer
 *			- returns line # in case of parsing error
 *	Accepts:	- buffer containing the app. schema
 *			- buffer size
 *			- ptr line # (returned value)
 *	Returns:	- app. schema descriptor or NULL
 */
LIST *
asc_bufparse(char *appbuf, int bufsize, int *pline, char **env)
{
	int		linesize;		/* size of parsed line */
	int		fl_comment;		/* comment mode flag */
	int		i;			/* favourite index */
	char		*p;			/* favourite pointer */
	LIST		*appd;			/* app. schema desc. */
/*
 * File must be pure ASCII.
 */
	*pline = 0;

	for (i = 0, p = appbuf; i < bufsize; ++i, ++p) {

		if ( ! isascii(*p)) {
			errno = ENOEXEC;
			return(0);
		}
	}
	
	appd = al_init(sizeof(struct aschema), (int (*)()) 0);
	if (appd == 0) return(0);
/*
 * Loop over the app. schema lines.
 */
	*pline = 1;
	fl_comment = 0;

	for ( ; bufsize > 0; ++appbuf, --bufsize) {
/*
 * Handle newline characters.
 */
		if (*appbuf == '\n') {
			++(*pline);
			fl_comment = 0;
		}
/*
 * Skip leading spaces and comments.
 */
		else if (isspace((int) *appbuf) || fl_comment) {
		}
/*
 * Skip comments.
 */
		else if (*appbuf == '#') {
			fl_comment = 1;
/*
 * Reject scripts.
 */
			if (*pline == 1 && bufsize > 2 && *(appbuf+1) == '!') {
				return(0);
			}
		}
/*
 * Parse the line.
 */
		else {
			linesize = parseline(appd, appbuf, bufsize, env);

			if (linesize < 0) {
				asc_free(appd);
				appd = 0;
				bufsize = 0;
			} else if (--linesize >= 0) {
				bufsize -= linesize;
				appbuf += linesize;
			}
		}
	}

	return(appd);
}

/*
 *	parseline
 *
 *	Function:	- parse a single app. schema line
 *			- update the app. schema desc.
 *	Accepts:	- ptr app. schema desc.
 *			- line buffer
 *			- buffer size
 *			- top level environment (if any)
 *	Returns:	- parsed line size or LAMERROR
 */
static int
parseline(LIST *appd, char *buf, int bufsize, char **env)
{
  int		lsize;			/* line size */
  int i;
  char          *cmdline;               /* command line */
  char          *progname;              /* program name for this line */
  char          *p;                     /* favorite pointer */
  int           orig_cmdc;              /* Original cmdc / cmdv */
  char **       orig_cmdv;              /* from buf */
  int		cmdc;			/* argv/argc faking */
  char **	cmdv;			
  int           tmp_cmdc;               /* tmp cmd line info */
  char **	tmp_cmdv;
  struct apparg * procargv;		/* process argv */
  struct appenv * procenv;		/* process env */
  struct aschema proc;			/* process list entry */
  OPT *ad;                              /* Options for app schema */

  /* prep the proc description */
  LAM_ZERO_ME(proc);
  proc.asc_errno = 0;
  proc.asc_proc_cnt = -1;
  proc.asc_srcnode = -1;
  proc.asc_node = -1;

  /* 
   * Set up the options.  Do early for asc_compat()
   */
  ad = ao_init();
  ao_setopt(ad, "c", 0, 1, AOINT);
  ao_setopt(ad, "np", "c", 1, AOINT);
  ao_setopt(ad, "s", 0, 1, 0);
  ao_setopt(ad, "x", 0, 1, 0);
  ao_setopt(ad, "wd", 0, 1, 0);

  /* Copy the line to a temporary buffer. */
  p = strchr(buf, '\n');
  lsize = (p == 0) ? bufsize : (int) (p - buf);

  /*
   * Allocate one extra char for terminator and 2 extra so we can put
   * a fake argv[0] at the start and then use the usual parsing
   * routines.  
   */
  cmdline = malloc((unsigned) lsize + 1 + 2);
  if (cmdline == 0) {
    ao_free(ad);
    return(LAMERROR);
  }

  /*
   * Insert the fake argv[0] and then copy the line.
   */
  strcpy(cmdline, "x ");
  memcpy(cmdline + 2, buf, lsize);
  cmdline[lsize + 2] = '\0';

  /*
   * Split it into a command line argv.  Don't free to the blody end,
   * as all the code used believes that we are using the argv/argc
   * from main, which means they expect it to be there until end of
   * time.  Many moons wasted on this. 
   */
  orig_cmdv = sfh_argv_break_quoted(cmdline, ' ', "\\'\"");
  free(cmdline);
  if (orig_cmdv == 0) {
    ao_free(ad);
    return(LAMERROR);
  }
  orig_cmdc = sfh_argv_count(orig_cmdv);
  cmdv = orig_cmdv;
  cmdc = orig_cmdc;

  /*
   * Put in -- form
   */
  if (asc_compat(&cmdc, &cmdv, ad)) {
    ao_free(ad);
    sfh_argv_free(orig_cmdv);
    return(LAMERROR);
  }

  /*
   * Parse the options.  We used to do this by hand, which seemed
   * crazy just to avoid pulling in all_opt.  Maybe back when this
   * thing was actually running on a multicomputer...  
   */
  if (ao_parse(ad, &cmdc, cmdv)) {
    if (orig_cmdv != cmdv) {
      sfh_argv_free(orig_cmdv);
    }
    sfh_argv_free(cmdv);
    ao_free(ad);
    return LAMERROR;
  }

  /*
   * Parse the node identifiers.  Would be easier if ndi_parse removed
   * from the array like everyone else... 
   */
  proc.asc_nodelist = ndi_parse(cmdc, cmdv, &tmp_cmdv);
  if (proc.asc_nodelist == 0) {
    if (orig_cmdv != cmdv) {
      sfh_argv_free(orig_cmdv);
    }
    sfh_argv_free(cmdv);
    return(LAMERROR);
  }

  /* Fix up cmdv and cmdc - from here on, cmdv and orig_cmdv are different*/
  if (orig_cmdv != cmdv) {
#if 0  /* BWB: Fix me later */
    sfh_argv_free(cmdv);
#endif
  }
  cmdv = tmp_cmdv;
  cmdc = sfh_argv_count(cmdv);
  tmp_cmdv = 0;

  /*
   * We should be left with the fake command name and the single
   * program name.  Copy name into progname
   */
  if (sfh_argv_count(cmdv) != 2) {
    sfh_argv_free(orig_cmdv);
    sfh_argv_free(cmdv);
    errno = EUSAGE;
    return(LAMERROR);
  }
  progname = strdup(cmdv[1]);
#if 0 /* BWB: Fix me later */
  sfh_argv_free(cmdv);
#endif
  cmdc = 0;
  cmdv = 0;
  tmp_cmdc = 0;
  tmp_cmdv = 0;

  /*
   * Start filling in proc structure.
   */
  if (ao_taken(ad, "s")) {
    proc.asc_srcnode = ndi_parse1(ao_param(ad, "s", 0, 0));
    if ((proc.asc_srcnode < 0)
	&& (proc.asc_srcnode != LOCAL)) {
      ao_free(ad);
      sfh_argv_free(orig_cmdv);
      return(LAMERROR);
    }
  }

  if (ao_taken(ad, "c")) {
    ao_intparam(ad, "c", 0, 0, &(proc.asc_proc_cnt));
  } else if (ao_taken(ad, "np")) {
    ao_intparam(ad, "np", 0, 0, &(proc.asc_proc_cnt));
  }

  /*
   * Build the process's argv structure.
   */
  procargv = (struct apparg *) malloc(sizeof(struct apparg));
  if (procargv == 0) {
    ao_free(ad);
    sfh_argv_free(orig_cmdv);
    return(LAMERROR);
  }

  procargv->apa_argc = 0;
  procargv->apa_argv = 0;
  procargv->apa_refcount = 1;

  /* build argument list */
  if (ao_tail(ad, &tmp_cmdc, &tmp_cmdv)) {
    ao_free(ad);
    sfh_argv_free(orig_cmdv);
  }

  sfh_argv_add(&(procargv->apa_argc), &(procargv->apa_argv), progname);
  for (i = 0 ; i < tmp_cmdc ; ++i) {
    sfh_argv_add(&(procargv->apa_argc), &(procargv->apa_argv), tmp_cmdv[i]);
  }
  /* No need to free tmp_cmdv, as it is just a pointer into orig_cmdv */
  tmp_cmdv = 0;
  tmp_cmdc = 0;
  free(progname);

  /* degenerate case - but it would be bad not to double check */
  if (procargv->apa_argv == 0) {
    ao_free(ad);
    sfh_argv_free(orig_cmdv);
    free(procargv);
    return(LAMERROR);
  }

  proc.asc_args = procargv;

  /*
   * Build the process's env structure.
   */
  procenv = (struct appenv *) malloc(sizeof(struct appenv));
  if (procenv == 0) {
    ao_free(ad);
    sfh_argv_free(orig_cmdv);
    sfh_argv_free(procargv->apa_argv);
    free(procargv);
    return(LAMERROR);
  }

  procenv->ape_refcount = 1;
  procenv->ape_envv = 0;
  procenv->ape_wrkdir = 0;
  
  if (env) {
    if ((procenv->ape_envv = sfh_argv_dup(env)) == 0) {
      ao_free(ad);
      sfh_argv_free(orig_cmdv);
      sfh_argv_free(procargv->apa_argv);
      free(procargv);
      free(procenv);
      return(LAMERROR);
    }
  }

  if (ao_taken(ad, "x")) {
    /*
     * Add env vars specified by -x options on this line.
     */
    if (asc_environment(0, ao_param(ad, "x", 0, 0), &procenv->ape_envv)) {
      ao_free(ad);
      sfh_argv_free(orig_cmdv);
      sfh_argv_free(procargv->apa_argv);
      free(procargv);
      sfh_argv_free(procenv->ape_envv);
      free(procenv);
    }
  }

  if (ao_taken(ad, "wd")) {
    procenv->ape_wrkdir = strdup(ao_param(ad, "wd", 0, 0));
    if (procenv->ape_wrkdir == 0) {
      ao_free(ad);
      sfh_argv_free(orig_cmdv);
      sfh_argv_free(procargv->apa_argv);
      free(procargv);
      sfh_argv_free(procenv->ape_envv);
      free(procenv);
    }
  }

  proc.asc_env = procenv;

  /*
   * Add ourselves to the list
   */
  if (al_append(appd, &proc) == 0) {
    sfh_argv_free(procargv->apa_argv);
    free(procargv);
    if (procenv->ape_wrkdir) {
      free(procenv->ape_wrkdir);
    }
    sfh_argv_free(procenv->ape_envv);
    free(procenv);
    return(LAMERROR);
  }

  /*
   * all done... Clean up
   */
  ao_free(ad);
#if 0 /* BWB: Fix me later */
  sfh_argv_free(orig_cmdv);
#endif

  return(lsize);
}

/*
 *	asc_free
 *
 *	Function:	- free an application schema descriptor
 *	Accepts:	- app. schema desc.
 */
void
asc_free(LIST *appd)
{
	struct aschema	*pp;			/* ptr process entry */
	struct apparg	*pa;			/* ptr process args */
	struct appenv	*pe;			/* ptr process env */

	if (appd == 0) return;

	for (pp = (struct aschema *) al_top(appd); pp;
			pp = (struct aschema *) al_next(appd, pp)) {

		if (pp->asc_nodelist) {
			al_free(pp->asc_nodelist);
		}

		pa = pp->asc_args;
		if (--pa->apa_refcount == 0) {
			sfh_argv_free(pa->apa_argv);
			free((char *) pa);
		}

		pe = pp->asc_env;
		if (--pe->ape_refcount == 0) {
		  sfh_argv_free(pe->ape_envv);
		  if (pe->ape_wrkdir) {
		    free(pe->ape_wrkdir);
		  }
		  free((char *) pe);
		}
	}

	al_free(appd);
}


/*
 *	asc_compat
 *
 *	Function:	- convert argv into "--" style argv
 *	Accepts:	- argument count (inout)
 *			- argument vector (inout)
 *                      - options descriptor (to parse argv)
 *	Returns:	- 0 (success) or -1 (error)
 *
 *      Notes: - If we have to modify the argv array, we reallocate a
 *               new one, since uargv may be a pointer to the argv 
 *               array for main sitting on the stack and modifying 
 *               that would be bad.  This also means that we can't
 *               free the old argv, so you should if appropriate.
 */
int
asc_compat(int *uargc, char ***uargv, OPT *aod)
{
    int			argc = *uargc;
    char		**argv = *uargv;
    int 		newargc = 0;		/* new argument count */
    char		**newargv = 0;		/* new argument vector */
    int			i,j;
    int                 nparams;

    /*
     * If there is already a "--" or a "-c" option then there is
     * nothing to do.  
     */
    for (i = 1; i < argc; i++) {
	if (strcmp(argv[i], "--") == 0) {
	    return(0);
	}
    }

    /*
     * Add the command name to the new command line.
     */
    if (sfh_argv_add(&newargc, &newargv, argv[0])) {
	return(-1);
    }

    /*
     * iterate over arguments until something not in aod or a node
     * specifier is found.  Assume that is the binary or app schema
     * name.
     */
    for (i = 1; i < argc; i++) {
      if (argv[i][0] == '-' && 
	  strlen(argv[i]) > 1 && 
	  ao_exam_isopt(aod, argv[i] + 1)) {
	/*
	 * It's an option 
	 */
	ao_exam_nparams(aod, argv[i] + 1, &nparams);

	/* add the option */
	if (sfh_argv_add(&newargc, &newargv, argv[i]))
	  return -1;

	/* Eat the params to the option.  If this is a variable, eat
	 * until the next thing that could be an option (starts with
	 * -, which handles the -- case).  This isn't perfect, but
	 * then options parsing shouldn't require an AI to figure out
	 * what the user wanted. */
	if (nparams != AOVARNUM) {
	  for (j = 0 ; j < nparams ; ++j) {
	    /* bounds checking is good */
	    if (i + 1 >= argc) 
	      return -1;

	    if (sfh_argv_add(&newargc, &newargv, argv[i + 1]))
	      return -1;
	    ++i;
	  }
	} else {
	  /* Note: This case is not well tested, as it is not
	   * currently used anywhere in the LAM/MPI tree.  But it
	   * might be some day! */
	  while (i + 1 < argc && (argv[i + 1])[0] != '-') {
	    if (sfh_argv_add(&newargc, &newargv, argv[i + 1]))
	      return -1;
	    ++i;
	  }
	}

      } else if (ndi_parse1(argv[i]) != NOTNODEID) {
	/*
	 * It's a node identifier.  
	 */
	if (sfh_argv_add(&newargc, &newargv, argv[i])) {
	  return(-1);
	}
      } else {
	/*
	 * Must now have the binary or application schema name.
	 * Append it to the new argument vector and then if there are
	 * trailing arguments add a "--" option.
	 */
	    if (sfh_argv_add(&newargc, &newargv, argv[i])) {
		return(-1);
	    }	

	    if (i + 1 < argc) {
		if (sfh_argv_add(&newargc, &newargv, "--")) {
		    return(-1);
		}
	    }

	    i++;
	    break;
	}
    }
/*
 * Append user program arguments to the new argument vector.
 */
    for (; i < argc; i++) {
	if (sfh_argv_add(&newargc, &newargv, argv[i])) {
	    return(-1);
	}
    }

    *uargc = newargc;
    *uargv = newargv;

    return(0);
}

/*
 *	asc_environment
 *
 *	Function:	- build job environment
 *	Accepts:	- export all LAM_MPI_* vars?
 *			- explicit list of variables to export
 *			- environment (inout)
 *	Returns:	- 0 or LAMERROR
 */
int
asc_environment(int exportme, char *varlist, char ***env)
{
	extern char	**environ;		/* current environment */
	char		**p;
	char		*v;
	int		nenv;
        char bad_string[] = "LAM_MPI_SESSION_PREFIX=";

	nenv = sfh_argv_count(*env);

	if (exportme)
/*
 * Export all environment variables of form LAM_MPI_* or LAM_IMPI_* or 
 * IMPI_*.  Special case: do *not* export LAM_MPI_SESSION_PREFIX.
 */
		for (p = environ; p && *p; p++) {
		  if (strncmp(*p, "LAM_MPI_", 8) == 0 &&
                      strncmp(*p, bad_string, sizeof(bad_string) - 1) != 0) {
		    if (sfh_argv_add(&nenv, env, *p))
		      return LAMERROR;
		  }
#if LAM_WANT_IMPI
		  else if (strncmp(*p, "LAM_IMPI_", 8) == 0) {
		    if (sfh_argv_add(&nenv, env, *p))
		      return LAMERROR;
		  }
		  else if (strncmp(*p, "IMPI_", 5) == 0) {
		    if (sfh_argv_add(&nenv, env, *p))
		      return LAMERROR;
		  }
#endif
/*
 * Also export the PBS_ENVIRONMENT and PBS_JOBID vars, if they exist.
 * This allows us to use the right unix socket names on the other side.
 */
		  else if (strncmp(*p, "PBS_ENVIRONMENT=", 16) == 0) {
		    if (sfh_argv_add(&nenv, env, *p))
		      return LAMERROR;
		  }
		  else if (strncmp(*p, "PBS_JOBID=", 10) == 0) {
		    if (sfh_argv_add(&nenv, env, *p))
		      return LAMERROR;
		  }
		}
/*
 * Export explicitly specified variables.
 */
	if (varlist) {
		v = varlist + strlen(varlist);

		while (v != varlist) {
			/* search backwards for , */
			for ( ; *v != ',' && v != varlist; v--) ;

			if (v != varlist) {
				if (*(v-1) != '\\') {
					/* it's not escaped so we have a var */
					if (add_var(&nenv, env, v+1))
						return(LAMERROR);
					*v = 0;
				}
			
				v--;
			}
		}

		/* add first var */
		if (add_var(&nenv, env, varlist))
			return(LAMERROR);
	}

	return 0;
}

/*
 *	add_var
 *
 *	Function:	- add variable to environment vector
 *	Accepts:	- length of environment vector (inout)
 *			- environment vector (inout)
 *			- variable
 *	Returns:	- 0 or LAMERROR
 */
static int
add_var(int *nenv, char ***env, char *var)
{
	char		*val;
	char		*v;
	int		len;
	int		i, j;

	len = strlen(var);
	if (len == 0)
		return 0;
/*
 * Copy removing any escapes.
 */
	v = malloc(len+1);

	for (i = 0, j = 0; i < len; i++) {
		if (var[i] != '\\' || var[i+1] != ',') {
			v[j++] = var[i];
		} else {
			v[j++] = ',';
			i++;
		}
	}
	v[j] = 0;
/*
 * If there is an equals the variable's value has been given.
 */
	if (strchr(v, '=')) {
		if (sfh_argv_add(nenv, env, v))
			return LAMERROR;
	} else {
/*
 * Get value from the current environment.
 */
		if ((val = getenv(v))) {
			if ((var = malloc(strlen(v) + strlen(val) + 2)) == 0)
				return LAMERROR;
			strcpy(var, v);
			strcat(var, "=");
			strcat(var, val);

			if (sfh_argv_add(nenv, env, var))
				return LAMERROR;
			free(var);
		}
	}

	free(v);
	return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1