/*-
 * Copyright (c) 2007 Sean Farley <sean-freebsd@farley.org>
 * 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,
 *    without modification, immediately at the beginning of the file.
 * 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 ``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 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 <err.h>
#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>


/* Standard environ that is exposed to all code. */
extern char **environ;
static int environSize = 0;

/*
 * Array of environment variables built from environ.  Each element records:
 *	name:		Pointer to name=value string
 *	name length:	Length of name not counting '=' character
 *	value:		Pointer to value within same string as name
 *	value size:	Size (not length) of space for value not counting the
 *			nul character
 *	active state:	true/false value to signify whether variable is active.
 *			Useful since multiple variables with the same name can
 *			co-exist.  At most, one variable can be active at any
 *			one time.
 */
static struct envVars {
	size_t nameLen;
	size_t valueSize;
	char *name;
	char *value;
	bool active;
} *envVars = NULL;
static int envActive = 0;
static int envVarsSize = 0;


/* Deinitialization of new environment. */
static void __attribute__ ((destructor)) __clean_env(void);


/*
 * Inline strlen() for performance.
 */
static inline size_t
__strlen(const char *str)
{
	const char *s;

	for (s = str; *s != '\0'; ++s)
		;

	return (s - str);
}


/*
 * Comparison of an environment name=value to a name.
 */
static inline bool
strncmpeq(const char *nameValue, const char *name, size_t nameLen)
{
	size_t ndx;

	for (ndx = 0; ndx < nameLen; ndx++)
		if (nameValue[ndx] != name[ndx])
			return (false);

	if (nameValue[ndx] == '=')
		return (true);

	return (false);
}


/*
 * Comparison of an environment name=value to a name where the names are already
 * known to be of equal length.
 */
static inline bool
strcmpeq(const char *nameValue, const char *name)
{
	while (*nameValue != '=')
		if (*nameValue != *name)
			return (false);
		else {
			nameValue++;
			name++;
		}

	return (true);
}


/*
 * Using environment, returns pointer to value associated with name, if any,
 * else NULL.  Explicitly removes '=' in argument name in the search for the
 * variable.  If the onlyActive flag is set to true, only variables that are
 * active are returned else all are.
 */
static inline char *
__findenv(const char *name, size_t *nameLen, int *envNdx, bool onlyActive)
{
	/*
	 * Remove '=' from end of name for compatibility with other environment
	 * conventions.  See getenv(3).
	 */
	if (*nameLen > 0 && name[*nameLen - 1] == '=')
		(*nameLen)--;
	if (*nameLen == 0)
		return (NULL);

	/*
	 * Find environment variable from end of array (more likely to be
	 * active).
	 */
	for (; *envNdx >= 0; (*envNdx)--)
		if ((!onlyActive || envVars[*envNdx].active) &&
		    envVars[*envNdx].nameLen == *nameLen &&
		    strcmpeq(envVars[*envNdx].name, name))
			return (envVars[*envNdx].value);

	return (NULL);
}


/*
 * Using environ, returns pointer to value associated with name, if any, else
 * NULL.  Explicitly removes '=' in argument name in the search for the
 * variable.  Used on the original environ passed into the program.
 */
static char *
__findenv_environ(const char *name)
{
	int envNdx;
	size_t nameLen;

	if (name == NULL || environ == NULL)
		return (NULL);

	/*
	 * Remove '=' from end of name for compatibility with other environment
	 * conventions.  See getenv(3).
	 */
	nameLen = __strlen(name);
	if (nameLen > 0 && name[nameLen - 1] == '=')
		nameLen--;
	if (nameLen == 0)
		return (NULL);

	/* Find variable within environ. */
	for (envNdx = 0; environ[envNdx] != NULL; envNdx++)
		if (strncmpeq(environ[envNdx], name, nameLen))
			return (&(environ[envNdx][nameLen + sizeof("=") - 1]));

	return (NULL);
}


/*
 * Using the environment, rebuild the environ array for use by other C library
 * calls that depend upon it.
 */
static int
__rebuild_environ(int newEnvSize)
{
	int envNdx;
	int environNdx;

	/* Resize environ. */
	if (newEnvSize > environSize) {
		environSize = newEnvSize;
		environ = reallocf(environ, sizeof (*environ) * (environSize +
			    1));
		if (environ == NULL)
			return (-1);
	}
	envActive = newEnvSize;

	/* Assign active variables to environ. */
	for (envNdx = envVarsSize - 1, environNdx = 0; envNdx >= 0; envNdx--)
		if (envVars[envNdx].active)
			environ[environNdx++] = envVars[envNdx].name;
	environ[environNdx] = NULL;

	return (0);
}


/*
 * Using environ, build an environment for use by standard C library calls.
 */
static int
__build_env(void)
{
	char **env;
	int activeNdx;
	int envNdx;
	size_t nameLen;

	/* Check for non-existant environment. */
	if (environ == NULL)
		return (0);

	/* Count environment variables. */
	for (env = environ, envVarsSize = 0; *env != NULL; env++)
		envVarsSize++;

	/* Create new environment. */
	envVars = malloc(sizeof (*envVars) * envVarsSize);
	if (envVars == NULL)
		return (-1);

	/* Copy environ values and keep track of them. */
	for (envNdx = envVarsSize - 1; envNdx >= 0; envNdx--) {
		envVars[envNdx].name =
		    strdup(environ[envVarsSize - envNdx - 1]);
		if (envVars[envNdx].name == NULL)
			return (-1);
		envVars[envNdx].value = strstr(envVars[envNdx].name, "=");
		if (envVars[envNdx].value != NULL) {
			envVars[envNdx].value++;
			envVars[envNdx].valueSize =
			    __strlen(envVars[envNdx].value);
		}
		else
			errx(EXIT_FAILURE, "environ corrupt");

		/*
		 * Find most current version of variable to make active.  This
		 * will prevent multiple active variables from being created
		 * during this initialization phase.
		 */
		nameLen = envVars[envNdx].value - envVars[envNdx].name - 1;
		envVars[envNdx].nameLen = nameLen;
		activeNdx = envVarsSize - 1;
		if (__findenv(envVars[envNdx].name, &nameLen, &activeNdx,
			    false) == NULL)
			errx(EXIT_FAILURE, "environ corrupt");
		envVars[activeNdx].active = true;
	}

	/* Create a new environ. */
	environ = NULL;
	return (__rebuild_environ(envVarsSize));
}


/*
 * Deallocate the environment built from environ as well as environ then set
 * both to NULL.  Eases debugging of memory leaks.
 */
static void
__clean_env(void)
{
	int envNdx;

	/* Deallocate environment and environ if created by (un)setenv(). */
	if (envVars != NULL) {
		for (envNdx = 0; envNdx < envVarsSize; envNdx++)
			free(envVars[envNdx].name);
		free(envVars);
		free(environ);
		envVars = NULL;
		environ = NULL;
	}

	return;
}


/*
 * Returns the value of a variable or NULL if none are found.
 */
char *
getenv(const char *name)
{
	int envNdx;
	size_t nameLen;

	if (name == NULL)
		return (NULL);

	/* Find environment variable via environ or rebuilt environment. */
	if (envVars == NULL)
		return (__findenv_environ(name));
	else {
		nameLen = __strlen(name);
		envNdx = envVarsSize - 1;
		return (__findenv(name, &nameLen, &envNdx, true));
	}
}


/*
 * Set the value of a variable.  Older settings are labeled as inactive.  If an
 * older setting has enough room to store the new value, it will be reused.  No
 * previous variables are ever freed here to avoid causing a segmentation fault
 * in a user's code.
 */
int
setenv(const char *name, const char *value, int overwrite)
{
	bool reuse;
	char *env;
	int envNdx;
	int newEnvActive;
	size_t nameLen;
	size_t valueLen;

	/* Initialize environment. */
	if (envVars == NULL && __build_env() == -1)
		return (-1);

	/* Check for malformed name. */
	if (name == NULL || (nameLen = __strlen(name)) == 0 || name[0] == '=') {
		errno = EINVAL;
		return (-1);
	}

	/*
	 * Remove '=' from beginning of value for compatibility with other
	 * environment conventions.  See setenv(3).
	 */
	if (value[0] == '=')
		value++;

	/* Find existing environment variable large enough to use. */
	envNdx = envVarsSize - 1;
	newEnvActive = envActive;
	valueLen = __strlen(value);
	reuse = false;
	if (__findenv(name, &nameLen, &envNdx, false) != NULL) {
		/* Deactivate entry if overwrite is allowed. */
		if (envVars[envNdx].active) {
			if (overwrite == 0)
				return (0);
			envVars[envNdx].active = false;
			newEnvActive--;
		}

		/* Entry is large enough to reuse. */
		if (envVars[envNdx].valueSize >= valueLen)
			reuse = true;
	}

	/* Create new variable if none was found of sufficient size. */
	if (! reuse) {
		/* Enlarge environment. */
		envNdx = envVarsSize;
		envVarsSize++;
		envVars = reallocf(envVars, sizeof (*envVars) * envVarsSize);
		if (envVars == NULL)
			return (-1);

		/* Create environment entry. */
		envVars[envNdx].name = malloc(nameLen + sizeof ("=") +
		    valueLen);
		if (envVars[envNdx].name == NULL)
			return (-1);
		envVars[envNdx].nameLen = nameLen;
		envVars[envNdx].valueSize = valueLen;

		/* Save name of name/value pair. */
		env = stpcpy(envVars[envNdx].name, name);
		if ((envVars[envNdx].name)[nameLen] != '=')
			env = stpcpy(env, "=");
	}
	else
		env = envVars[envNdx].value;

	/* Save value of name/value pair. */
	strcpy(env, value);
	envVars[envNdx].value = env;
	envVars[envNdx].active = true;
	newEnvActive++;

	/* No need to rebuild environ if the variable was reused. */
	if (reuse)
		return (0);
	else
		return (__rebuild_environ(newEnvActive));
}


/*
 * putenv() API call using setenv().
 */
int
putenv(const char *string)
{
	char *equal;
	char *name;
	int rval;

	if ((name = strdup(string)) == NULL)
		return (-1);
	if ((equal = strchr(name, '=')) == NULL) {
		free(name);
		return (-1);
	}
	*equal = '\0';
	rval = setenv(name, equal + 1, 1);
	free(name);

	return (rval);
}


/*
 * Unset all variables with the same name by flagging them as inactive.  No
 * variable is ever freed.
 */
void
unsetenv(const char *name)
{
	int envNdx;
	size_t nameLen;

	if (name == NULL)
		return;

	/* Initialize environment. */
	if (envVars == NULL && __build_env() == -1)
		return;

	/* Deactivate specified variable. */
	envNdx = envVarsSize - 1;
	nameLen = __strlen(name);
	if (__findenv(name, &nameLen, &envNdx, true) != NULL) {
		envVars[envNdx].active = false;
		__rebuild_environ(envActive - 1);
	}

	return;
}

