/*-
 *  Copyright (c) 2001  Peter Pentchev
 *  All rights reserved.
 * 
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 
 *  THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 *  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 *  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 *  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 *  SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/stat.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <libgen.h>

#include "penv.h"
#include "pe_cmd.h"
#include "pe_err.h"
#include "pe_log.h"
#include "pe_var.h"

#include "pathnames.h"

__RINGID("$Ringlet: c/misc/penv/penv.c,v 1.27 2004/01/06 16:38:10 roam Exp $");

unsigned	verbose, quiet;

char		pe_verstr[64] = "penv";

char		*pe_cenvdir;
int		pe_direx;
mode_t		pe_filemode;

static char	pe_conffile[NAME_MAX] = PE_CONFFILE;
static char	pe_basedir[PATH_MAX] = PE_BASEDIR;
static char	pe_envdir[PATH_MAX] = PE_ENVDIR;
static char	pe_modestr[4] = PE_FILEMODE;

static unsigned	pe_depth = PE_DEPTH;

static struct pe_var	pe_vars[] = {
	{"basedir",	PE_VT_STRING,	sizeof(pe_basedir),	pe_basedir, 0},
	{"conffile",	PE_VT_STRING,	sizeof(pe_conffile),	pe_conffile, 0},
	{"depth",	PE_VT_UINT,	0,			&pe_depth, 0},
	{"envdir_p",	PE_VT_STRING,	sizeof(pe_envdir),	pe_envdir, 0},
	{"filemode",	PE_VT_STRING,	sizeof(pe_modestr),	pe_modestr, 0},
	{NULL,		PE_VT_NULL,	0,			NULL, 0}
};

static const char	*pe_envvars[][2] = {
	{"PENV_BASEDIR",	"basedir"},
	{"PENV_CONFFILE",	"conffile"},
	{"PENV_DEPTH",		"depth"},
	{"PENV_ENVDIR_P",	"envdir_p"},
	{NULL,			NULL}
};

extern char	**environ;

static int	pe_confreq;
static int	donada;
static int	pe_clean;

static pe_err_t	pe_init(int, char *[]);
static pe_err_t	pe_close(void);
static pe_err_t	pe_doit(int, char *[]);

static pe_err_t	pe_getdirname(const char *base, unsigned depth, char **penv,
		int *pdirex);

static pe_err_t	pe_dirsfx(char **result, unsigned depth, char *path);
static pe_err_t	pe_basename(char **result, const char *path);
static pe_err_t	pe_dirname(char **result, const char *path);
static pe_err_t	pe_mkdir(const char *path);

static pe_err_t	pe_buildargv(int *pnargc, char **pnargv[],
		int argc, char *argv[], const char *envdir);

static pe_err_t	pe_userconf_name(char *name, size_t sz);

static pe_err_t	version(void);

/*
 * Function:
 *         pe_makeversion()                - generate version string
 * Inputs:
 *         none
 * Returns:
 *         pe_err_t
 * Modifies:
 *         pe_verstr
 */

static pe_err_t
pe_makeversion(void) {

	snprintf(pe_verstr, sizeof(pe_verstr), "penv v%d.%d"
#if PE_VER_PRE
	    "-pre%d"
#endif /* PE_VER_PRE */
#if PE_VER_PATCH
	    "p%d"
#endif /* PE_VER_PATCH */
	    ,
	    PE_VER_MAJ, PE_VER_MIN
#if PE_VER_PRE
	    , PE_VER_PRE
#endif /* PE_VER_PRE */
#if PE_VER_PATCH
	    , PE_VER_PATCH
#endif /* PE_VER_PATCH */
		);
	
	return (PE_ERR_NONE);
}

/*
 * Function:
 * 	pe_init				- general-purpose init function
 * Inputs:
 * 	argc, argv			- main() args for option processing
 * Returns:
 * 	pe_err_t
 * 	CMDLINE
 * Modifies:
 * 	verbose, quiet
 */

static pe_err_t
pe_init(int argc, char *argv[]) {
	int ch, helpq, versq;
	unsigned i;
	const char *val;
	pe_err_t r;
	
	helpq = versq = 0;
	pe_makeversion();
	
	/* Process cmdline options */
	
	opterr = 0;
	while (ch = getopt(argc, argv, PE_OPTSTR), ch != EOF)
		switch (ch) {
			case 'c':
				if (r = pe_cmd_set(optarg, '\0'), r)
					return (r);
				break;
			case 'C':
				pe_clean = 1;
				break;
			case 'D':
				if (r = pe_var_set(pe_vars,
				    "depth", optarg), r)
					return (r);
				break;
			case 'd':
				if (r = pe_var_set(pe_vars,
				    "basedir", optarg), r)
					return (r);
				break;
			case 'f':
				if (r = pe_var_set(pe_vars,
				    "conffile", optarg), r)
					return (r);
				pe_confreq = 1;
				break;
			case 'h':
				helpq = 1;
				break;
			case 'm':
				if (r = pe_var_set(pe_vars,
				    "filemode", optarg), r)
					return (r);
				break;
			case 'n':
				donada = 1;
				break;
			case 'o':
				if (r = pe_var_parseline(pe_vars,
				    optarg, strlen(optarg)), r)
					return (r);
				break;
			case 'V':
				versq = 1;
				break;
			case 'q':
				quiet++;
				break;
			case 'v':
				verbose++;
				break;
			default:
				r = pe_cmd_set(NULL, ch);
				if (r == PE_ERR_NONE)
					break;
				else if (r != PE_ERR_NOCMD)
					return (r);
			case '?':
				usage();
				/* NOTREACHED */
		}
	
	argc -= optind; argv += optind;
	
	if (versq)
		version();
	if (helpq)
		usage();
	if (versq)
		/* no need for "|| helpq": usage() never returns */
		exit(PE_ERR_NONE);
	
	/* Process the environment variables */
	for (i = 0; pe_envvars[i][0] != NULL; i++)
		if (val = getenv(pe_envvars[i][0]), val != NULL)
			if (r = pe_var_set(pe_vars, pe_envvars[i][1], val), r)
				return (r);

	return (PE_ERR_NONE);
}

/*
 * Function:
 * 	pe_close			- general-purpose shutdown function
 * Inputs:
 * 	none
 * Returns:
 * 	pe_err_t
 * 	nothing for the present
 * Modifies:
 * 	nothing for the present
 */

static pe_err_t
pe_close(void) {
	
	return (PE_ERR_NONE);
}

/*
 * Function:
 * 	usage			- startup help info
 * Inputs:
 * 	none
 * Returns:
 * 	nothing
 * Modifies:
 * 	nothing, writes to stdout and exits
 */

void
usage(void) {
	static const char *msg[] = {
	    "penv [-c cmd] [-D depth] [-d basedir] [-f config] [-m mode]"
	    " [-o option]",
	    "\t[-ChnqVv] [command [args..]]",
	    "penv -L [-D depth] [-d basedir] [-f config] [-o option] [-nqv]",
	    "penv -p [-D depth] [-d basedir] [-f config] [-o option] [-nqv]",
	    "penv -S [-D depth] [-d basedir] [-f config] [-m mode]"
	    " [-o option] var=val..",
	    "penv -R [-D depth] [-d basedir] [-f config] [-o option] var..",
	    "\t-c\tperform a specified action,",
	    "\t\tpenv -c help lists the available actions;",
	    "\t-C\tclean the environment;",
	    "\t-D\tspecify the number of path components to be taken for",
	    "\t\tdetermining the environment directory;",
	    "\t-d\tspecify the base dir for locating environment directories",
	    "\t\t(default " PE_BASEDIR ");",
	    "\t-f\tspecify the config file name",
	    "\t\t(default " PE_CONFFILE ");",
	    "\t-h\tprint this help text and exit;",
	    "\t-L\tdo not execute a program, only list the environment;",
	    "\t-n\tdo not execute any programs, just print out what would",
	    "\t\thave been executed;",
	    "\t-o\tspecify an option to be parsed as if read from the start",
	    "\t\tof the configuration file;",
	    "\t-p\tdo not execute a program, only print the environment"
	    " directory;",
	    "\t-q\tquiet operation; multiple -q's make it even more quiet;",
	    "\t-R\treset the specified variables",
	    "\t\t(remove files from the environment directory);",
	    "\t-S\tset the specified variables to the specified values;",
	    "\t-V\tprint version information and exit;",
	    "\t-v\tverbose operation; multiple -v's increase verbosity level.",
	    NULL
	};
	unsigned i;
	
	for (i = 0; msg[i] != NULL; i++)
		fprintf(stderr, "%s\n", msg[i]);
	exit(PE_ERR_CMDLINE);
}

/*
 * function:
 * 	pe_version		- output version info
 * Inputs:
 * 	none
 * Returns:
 * 	PE_ERR_NONE
 * Modifies:
 * 	nothing, writes to stdout
 */

pe_err_t
version(void) {
	
	printf("%s\n", pe_verstr);
	
	pe_verbose(1, "%s\n", "Built on " __DATE__ ", " __TIME__);
#ifdef __GNUC__
	pe_verbose(2, "%s\n", "Compiler: GNU C " __VERSION__);
	pe_verbose(2, "%s\n", "Platform: " PE_OS " " PE_OSREL ": " PE_OSHOST);
#endif /* __GNUC__ */

	return (PE_ERR_NONE);
}

/*
 * Function:
 * 	pe_userconf_name		- build the name of a per-user
 * 					  configuration file
 * Inputs:
 * 	name				- the array to store the filename in
 * 	sz				- max length of the name
 * Returns:
 * 	pe_err_t
 * 	nothing so far
 * Modifies:
 * 	on success, stores the per-user configuration file name into 'name'
 */

static pe_err_t
pe_userconf_name(char *name, size_t sz) {
	const char *val;

	if (val = getenv("HOME"), val != NULL)
		snprintf(name, sz, "%s/%s", val, PE_USERCONF);
	else if (val = getenv("LOGNAME"), val != NULL)
		snprintf(name, sz, "/home/%s/%s", val, PE_USERCONF);
	else if (val = getenv("USER"), val != NULL)
		snprintf(name, sz, "/home/%s/%s", val, PE_USERCONF);
	else
		snprintf(name, sz, "~/%s", PE_USERCONF);
	return (PE_ERR_NONE);
}

/*
 * Function:
 * 	pe_doit				- perform the actual work
 * Inputs:
 * 	argc, argv			- main() args
 * Returns:
 * 	pe_err_t
 * 	EXEC				- execvp() failed
 * 	error code from pe_readconf(), pe_getdirname(), pe_buildargv()
 * Modifies:
 *	nothing by itself; writes args to stdout
 */

static pe_err_t
pe_doit(int argc, char *argv[]) {
	char userconf[PATH_MAX], *next;
	pe_err_t r;

	pe_cenvdir = NULL;
	r = PE_ERR_NONE;

	do {
		/* fake loop so I can break out */
		
		if (r = pe_userconf_name(userconf, sizeof(userconf)), r)
			break;
		if (r = pe_var_readfile(pe_vars, userconf, 0), r)
			break;
		if (r = pe_var_readfile(pe_vars, pe_conffile, pe_confreq), r)
			break;

		/* Convert the file mode to an umask */
		pe_filemode = (mode_t)strtoul(pe_modestr, &next, 8);
		if (*pe_modestr == '\0' || *next != '\0')
			return (PE_ERR_CMDLINE);

		if (r = pe_getdirname(pe_basedir, pe_depth, &pe_cenvdir,
		    &pe_direx), r)
			break;
		if (r = pe_cmd_exec(argc, argv), r)
			break;
	} while (0);

	free(pe_cenvdir);
	pe_cenvdir = NULL;
	return (r);
}

pe_err_t
pe_c_printpath(int argc, char *argv[] __unused) {

	if (argc > 0)
		usage();

	printf("%s\n", (pe_cenvdir == NULL)? "": pe_cenvdir);
	return (PE_ERR_NONE);
}

pe_err_t
pe_c_exec(int argc, char *argv[]) {
	int nargc, i;
	char **nargv;
	char *cleanenv[] = {
		NULL
	};
	pe_err_t r;
	
	if (argc < 1)
		usage();

	nargc = 0;
	nargv = NULL;
	r = PE_ERR_NONE;

	do {
		/* fake loop so I can break out */
		
		if (pe_cenvdir != NULL)
			pe_verbose(2, "Using envdir %s\n", pe_cenvdir);
		else
			pe_verbose(2, "No envdir\n");
		r = pe_buildargv(&nargc, &nargv, argc, argv, pe_cenvdir);
		if (r != PE_ERR_NONE)
			break;
		
		if (verbose || donada) {
			for (i = 0; i < nargc-1; i++)
				pe_verbose(0, "%s%s",
				    nargv[i], (i < nargc-2? " ": "\n"));
		}

		if (!donada) {
			/* So let's do it! */
			if (pe_clean)
				environ = cleanenv;
			execvp(nargv[0], nargv);
			r = PE_ERR_EXEC;
		}
	} while (0);

	return (r);
}

pe_err_t
pe_c_mkdir(int argc, char *argv[] __unused) {

	if (argc > 0)
		usage();
	return (pe_mkdir(pe_cenvdir));
}

/*
 * Function:
 * 	pe_mkdir		- create a directory and higher-level dirs
 * Inputs:
 * 	path			- directory to create
 * Returns:
 * 	pe_err_t
 * 	MKDIR			- mkdir(2) failed
 * 	INT			- mkdir(2) returned ENOENT on a TLD
 * Modifies:
 * 	nothing, creates directories
 */

static pe_err_t
pe_mkdir(const char *path) {
	char *base;
	pe_err_t r;

	base = NULL;

	if ((mkdir(path, 0755) == 0) || (errno == EEXIST))
		return (PE_ERR_NONE);
	if (errno != ENOENT)
		return (PE_ERR_MKDIR);
	
	if (r = pe_dirname(&base, path), r)
		return (r);
	/* Sanity check: did we fail to create a top-level dir? */
	if (!strcmp(base, ".") || !strcmp(base, "/"))
		return (PE_ERR_INT);	/* FIXME: this should be OSERR */
	r = pe_mkdir(base);
	free(base);
	if (r != PE_ERR_NONE)
		return (r);
	if (mkdir(path, 0755) == -1)
		return (PE_ERR_MKDIR);

	return (PE_ERR_NONE);
}

/* FIXME: document everything from here onwards */

static pe_err_t
pe_getdirname(const char *base, unsigned depth, char **penv, int *pdirex) {
	char *envdir, *cwd, *dirsfx;
	int found, fd, direx;
	struct stat sbuf;
	pe_err_t r;

	envdir = cwd = dirsfx = NULL;
	found = direx = 0;

	do {
		if (cwd = getcwd(NULL, PATH_MAX), cwd == NULL)
			return (PE_ERR_GETCWD);

		if (r = pe_dirsfx(&dirsfx, depth, cwd), r)
			break;
		if (dirsfx != NULL)
			pe_verbose(2, "Using dir suffix %s\n", dirsfx);
		else
			pe_verbose(2, "No dir suffix\n");

		/* Okay, we found us a pathname! */
		asprintf(&envdir, "%s/%s", base, dirsfx);
		if (envdir == NULL)
			return (PE_ERR_NOMEM);

		/* Now let's just check if this thing really exists.. */
		if (fd = open(envdir, O_RDONLY), fd == -1)
			break;
		if (fstat(fd, &sbuf) == -1) {
			r = PE_ERR_FSTAT;
			break;
		}
		close(fd);
		if (!S_ISDIR(sbuf.st_mode))
			break;
		direx = 1;
	} while (0);

	free(dirsfx);
	if (r == PE_ERR_NONE) {
		*penv = envdir;
		*pdirex = direx;
	}
	return (PE_ERR_NONE);
}

static pe_err_t
pe_dirsfx(char **result, unsigned depth, char *path) {
	unsigned d;
	char *comp;
	char *cpath;
	char *res, *nres;
	int exhausted;
	pe_err_t r;

	exhausted = 0;
	cpath = path;
	res = NULL;
	r = PE_ERR_NONE;

	for (d = 0; d < depth; d++) {
		/* Find the last component.. */
		if (r = pe_basename(&comp, cpath), r)
			break;
		if (!strcmp(comp, ".") || !strcmp(comp, "/")) {
			free(res);
			res = NULL;
			break;
		}

		/* ..add it to the newly built path.. */
		if (d == 0)
			nres = strdup(comp);
		else
			asprintf(&nres, "%s/%s", comp, res);
		if (nres == NULL) {
			r = PE_ERR_NOMEM;
			break;
		}
		free(res);
		res = nres;

		/* ..and remove it from the path. */
		if (d < depth - 1) {
			if (r = pe_dirname(&cpath, cpath), r)
				break;
			if (!strcmp(cpath, ".")) {
				free(res);
				res = NULL;
				break;
			}
		}
	}

	free(comp);
	if (r == PE_ERR_NONE)
		*result = res;
	return (r);
}

static pe_err_t
pe_basename(char **result, const char *path) {
	char *base, *b;

	if (base = basename(path), base == NULL)
		return (PE_ERR_BASENAME);
	if (b = strdup(base), b == NULL)
		return (PE_ERR_NOMEM);
	*result = b;
	return (PE_ERR_NONE);
}

static pe_err_t
pe_dirname(char **result, const char *path) {
	char *dir, *b;

	if (dir = dirname(path), dir == NULL)
		return (PE_ERR_DIRNAME);
	if (b = strdup(dir), b == NULL)
		return (PE_ERR_NOMEM);
	*result = b;
	return (PE_ERR_NONE);
}

static pe_err_t
pe_buildargv(int *pnargc, char **pnargv[], int argc, char *argv[],
		const char *envdir) {
	int nargc, add;
	char **nargv;

	if (pe_direx)
		add = 2;
	else
		add = 0;

	nargc = argc + 1 + add;
	if (nargv = calloc(nargc, sizeof(*nargv)), nargv == NULL)
		return (PE_ERR_NOMEM);
	memcpy(nargv + add, argv, argc * sizeof(*nargv));
	nargv[nargc-1] = NULL;

	if (pe_direx) {
		if (nargv[0] = strdup(pe_envdir), nargv[0] == NULL)
			return (PE_ERR_NOMEM);
		if (nargv[1] = strdup(envdir), nargv[1] == NULL)
			return (PE_ERR_NOMEM);
	}

	*pnargc = nargc;
	*pnargv = nargv;
	return (PE_ERR_NONE);
}

/*
 *   M A I N   F U N C T I O N
 */

int
main(int argc, char *argv[]) {
	pe_err_t r, cr;
	
	if (r = pe_init(argc, argv), r)
		return (pe_prerror("init", r));
	
	argc -= optind;
	argv += optind;
	if (r = pe_doit(argc, argv), r)
		pe_prerror("doit", r);
	
	if (cr = pe_close(), cr)
		pe_prerror("close", cr);
	
	return (r? r: cr);
}


syntax highlighted by Code2HTML, v. 0.9.1