/* dot.conf - configuration file parser library
 * Copyright (C) 1999,2000,2001,2002 Lukas Schroeder <lukas@azzit.de>,
 *   and others.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 *
 */

/* -- dotconf.c - this code is responsible for the input, parsing and dispatching of options  */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Added by Stephen W. Boyer <sboyer@caldera.com>
 * for wildcard support in Include file paths
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>

/* -- AIX 4.3 compile time fix
 * by Eduardo Marcel Macan <macan@colband.com.br>
 *
 * modified by Stephen W. Boyer <sboyer@caldera.com>
 * for Unixware and OpenServer
 */

#if defined (_AIX43) || defined(UNIXWARE) || defined(OSR5)
#include <strings.h>
#endif

#include <stdarg.h>
#include <time.h>
#include <sys/stat.h>

#ifndef WIN32

#include <dirent.h>
#include <unistd.h>

#else /* ndef WIN32 */

#include "readdir.h"		/* WIN32 fix by Robert J. Buck */

#define strncasecmp strnicmp
typedef unsigned long ulong;
#define snprintf _snprintf
#define vsnprintf _vsnprintf
#endif /* !WIN32 */

#include <ctype.h>
#include "dotconf.h"
#include "dotconf_priv.h"

#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif

static char name[CFG_MAX_OPTION + 1];	/* option name */

/*
 * some 'magic' options that are predefined by dot.conf itself for
 * advanced functionality
 */
static DOTCONF_CB(dotconf_cb_include);	/* internal 'Include'     */
static DOTCONF_CB(dotconf_cb_includepath);	/* internal 'IncludePath' */

static configoption_t dotconf_options[] = {
	{"Include", ARG_STR, dotconf_cb_include, NULL, CTX_ALL},
	{"IncludePath", ARG_STR, dotconf_cb_includepath, NULL, CTX_ALL},
	LAST_CONTEXT_OPTION
};

static void skip_whitespace(signed char **cp, int n, char term)
{
	signed char *cp1 = *cp;
	while (isspace((int)*cp1) && *cp1 != term && n--)
		cp1++;
	*cp = cp1;
}

static void copy_word(signed char **dest, signed char **src, int max, char term)
{
	signed char *cp1 = *src;
	signed char *cp2 = *dest;
	while (max-- && !isspace((int)*cp1) && *cp1 != term)
		*cp2++ = *cp1++;
	*cp2 = 0;

	*src = cp1;
	*dest = cp2;
}

static const configoption_t *get_argname_fallback(const configoption_t *
						  options)
{
	int i;

	for (i = 0; (options[i].name && options[i].name[0]); i++) ;
	if (options[i].type == ARG_NAME && options[i].callback)
		return &options[i];
	return NULL;
}

char *dotconf_substitute_env(configfile_t * configfile, char *str)
{
	char *cp1, *cp2, *cp3, *eos, *eob;
	char *env_value;
	char env_name[CFG_MAX_VALUE + 1];
	char env_default[CFG_MAX_VALUE + 1];
	char tmp_value[CFG_MAX_VALUE + 1];

	memset(env_name, 0, CFG_MAX_VALUE + 1);
	memset(env_default, 0, CFG_MAX_VALUE + 1);
	memset(tmp_value, 0, CFG_MAX_VALUE + 1);

	cp1 = str;
	eob = cp1 + strlen(str) + 1;
	cp2 = tmp_value;
	eos = cp2 + CFG_MAX_VALUE + 1;

	while ((cp1 < eob) && (cp2 < eos) && (*cp1 != '\0')) {
		/* substitution needed ?? */
		if (*cp1 == '$' && *(cp1 + 1) == '{') {
			cp1 += 2;	/* skip ${ */
			cp3 = env_name;
			while ((cp1 < eob) && !(*cp1 == '}' || *cp1 == ':'))
				*cp3++ = *cp1++;
			*cp3 = '\0';	/* terminate */

			/* default substitution */
			if (*cp1 == ':' && *(cp1 + 1) == '-') {
				cp1 += 2;	/* skip :- */
				cp3 = env_default;
				while ((cp1 < eob) && (*cp1 != '}'))
					*cp3++ = *cp1++;
				*cp3 = '\0';	/* terminate */
			} else {
				while ((cp1 < eob) && (*cp1 != '}'))
					cp1++;
			}

			if (*cp1 != '}') {
				dotconf_warning(configfile, DCLOG_WARNING,
						ERR_PARSE_ERROR,
						"Unbalanced '{'");
			} else {
				cp1++;	/* skip } */
				if ((env_value = getenv(env_name)) != NULL) {
					strncat(cp2, env_value, eos - cp2);
					cp2 += strlen(env_value);
				} else {
					strncat(cp2, env_default, eos - cp2);
					cp2 += strlen(env_default);
				}
			}

		}

		*cp2++ = *cp1++;
	}
	*cp2 = '\0';		/* terminate buffer */

	free(str);
	return strdup(tmp_value);
}

int dotconf_warning(configfile_t * configfile, int type, unsigned long errnum,
		    const char *fmt, ...)
{
	va_list args;
	int retval = 0;

	va_start(args, fmt);
	if (configfile->errorhandler != 0) {	/* an errorhandler is registered */
		char msg[CFG_BUFSIZE];
		vsnprintf(msg, CFG_BUFSIZE, fmt, args);
		retval =
		    configfile->errorhandler(configfile, type, errnum, msg);
	} else {		/* no errorhandler, do-it-yourself */

		retval = 0;
		fprintf(stderr, "%s:%ld: ", configfile->filename,
			configfile->line);
		vfprintf(stderr, fmt, args);
		fprintf(stderr, "\n");
	}
	va_end(args);

	return retval;
}

int dotconf_register_options(configfile_t * configfile,
			     const configoption_t * options)
{
	int num = configfile->config_option_count;
	int ret = 0;
	configoption_t const **temp = configfile->config_options;

#define GROW_BY   10

	/* resize memoryblock for options blockwise */
	if (temp == NULL)
		temp = malloc(sizeof(configoption_t *) * (GROW_BY + 1));
	else {
		if (!(num % GROW_BY))
			temp = realloc(temp,
				       sizeof(configoption_t *) * (num +
								   GROW_BY +
								   1));
	}

#undef GROW_BY

	if (temp != NULL) {
		/* Allocation or reallocation was successful. */
		/* append new options */
		temp[configfile->config_option_count] = options;
		configfile->config_options = temp;
		configfile->config_options[++configfile->config_option_count] =
		    0;
		ret = 1;
	}

	return ret;
}

void dotconf_callback(configfile_t * configfile, callback_types type,
		      dotconf_callback_t callback)
{
	switch (type) {
	case ERROR_HANDLER:
		configfile->errorhandler = (dotconf_errorhandler_t) callback;
		break;
	case CONTEXT_CHECKER:
		configfile->contextchecker =
		    (dotconf_contextchecker_t) callback;
		break;
	default:
		break;
	}
}

int dotconf_continue_line(char *buffer, size_t length)
{
	/* ------ match [^\\]\\[\r]\n ------------------------------ */
	char *cp1 = buffer + length - 1;

	if (length < 2)
		return 0;

	if (*cp1-- != '\n')
		return 0;

	if (*cp1 == '\r')
		cp1--;

	if (*cp1-- != '\\')
		return 0;

	cp1[1] = 0;		/* strip escape character and/or newline */
	return (*cp1 != '\\');
}

int dotconf_get_next_line(char *buffer, size_t bufsize,
			  configfile_t * configfile)
{
	char *cp1, *cp2;
	char buf2[CFG_BUFSIZE];
	int length;

	if (configfile->eof)
		return 1;

	cp1 = fgets(buffer, CFG_BUFSIZE, configfile->stream);

	if (!cp1) {
		configfile->eof = 1;
		return 1;
	}

	configfile->line++;
	length = strlen(cp1);
	while (dotconf_continue_line(cp1, length)) {
		cp2 = fgets(buf2, CFG_BUFSIZE, configfile->stream);
		if (!cp2) {
			fprintf(stderr,
				"[dotconf] Parse error. Unexpected end of file at "
				"line %ld in file %s\n", configfile->line,
				configfile->filename);
			configfile->eof = 1;
			return 1;
		}
		configfile->line++;
		strcpy(cp1 + length - 2, cp2);
		length = strlen(cp1);
	}

	return 0;
}

char *dotconf_get_here_document(configfile_t * configfile, const char *delimit)
{
	/* it's a here-document: yeah, what a cool feature ;) */
	unsigned int limit_len;
	char here_string;
	char buffer[CFG_BUFSIZE];
	char *here_doc = 0;
	char here_limit[9];	/* max length for here-document delimiter: 8 */
	struct stat finfo;
	int offset = 0;

	if (configfile->size <= 0) {
		if (stat(configfile->filename, &finfo)) {
			dotconf_warning(configfile, DCLOG_EMERG, ERR_NOACCESS,
					"[emerg] could not stat currently read file (%s)\n",
					configfile->filename);
			return NULL;
		}
		configfile->size = finfo.st_size;
	}

	/*
	 * allocate a buffer of filesize bytes; should be enough to
	 * prevent buffer overflows
	 */
	here_doc = malloc(configfile->size);	/* allocate buffer memory */
	memset(here_doc, 0, configfile->size);

	here_string = 1;
	limit_len = snprintf(here_limit, 9, "%s", delimit);
	while (!dotconf_get_next_line(buffer, CFG_BUFSIZE, configfile)) {
		if (!strncmp(here_limit, buffer, limit_len - 1)) {
			here_string = 0;
			break;
		}
		offset +=
		    snprintf((here_doc + offset), configfile->size - offset - 1,
			     "%s", buffer);
	}
	if (here_string)
		dotconf_warning(configfile, DCLOG_WARNING, ERR_PARSE_ERROR,
				"Unterminated here-document!");

	here_doc[offset - 1] = '\0';	/* strip newline */

	return (char *)realloc(here_doc, offset);
}

const char *dotconf_invoke_command(configfile_t * configfile, command_t * cmd)
{
	const char *error = 0;

	error = cmd->option->callback(cmd, configfile->context);
	return error;
}

char *dotconf_read_arg(configfile_t * configfile, signed char **line)
{
	int sq = 0, dq = 0;	/* single quote, double quote */
	int done;
	signed char *cp1 = *line;
	char *cp2, *eos;
	char buf[CFG_MAX_VALUE];

	memset(buf, 0, CFG_MAX_VALUE);
	done = 0;
	cp2 = buf;
	eos = cp2 + CFG_MAX_VALUE - 1;

	if (*cp1 == '#' || !*cp1)
		return NULL;

	skip_whitespace(&cp1, CFG_MAX_VALUE, 0);

	while ((*cp1 != '\0') && (cp2 != eos) && !done) {
		switch (*cp1) {
		case '\'':	/* single quote */
			if (dq)
				break;	/* already double quoting, break out */
			if (sq)
				sq--;	/* already single quoting, clear state */
			else if (!sq)
				sq++;	/* set state for single quoting */
			break;
		case '"':	/* double quote */
			if (sq)
				break;	/* already single quoting, break out */
			if (dq)
				dq--;	/* already double quoting, clear state */
			else if (!dq)
				dq++;	/* set state for double quoting */
			break;
		case '\\':	/* protected chars */
			if (!cp1[1])	/* dont protect NUL */
				break;
			*cp2++ = *(++cp1);
			cp1++;	/* skip the protected one */
			continue;
			break;
		default:
			break;
		}

		/* unquoted space: start a new option argument */
		if (isspace((int)*cp1) && !dq && !sq) {
			*cp2 = '\0';
			break;
		}
		/* unquoted, unescaped comment-hash ; break out, unless NO_INLINE_COMMENTS is set */
		else if (*cp1 == '#' && !dq && !sq
			 && !(configfile->flags & NO_INLINE_COMMENTS)) {
			/* 
			 * NOTE: 1.0.8a got the NO_INLINE_COMMENTS feature wrong: it
			 * skipped every argument starting with a #, instead of simply eating it!
			 */

			*cp2 = 0;
			*cp1 = 0;
			*line = cp1;
			return NULL;
		}
		/* not space or quoted: eat it; dont take quote if quoting */
		else if ((!isspace((int)*cp1) && !dq && !sq && *cp1 != '"'
			  && *cp1 != '\'')
			 || (dq && (*cp1 != '"')) || (sq && *cp1 != '\'')) {
			*cp2++ = *cp1;
		}

		cp1++;
	}

	*line = cp1;

	/* FIXME: escaping substitutes does not work
	   Subst ${HOME} \$\{HOME\}
	   BOTH! will be substituted, which is somewhat wrong, ain't it ?? :-(
	 */
	if ((configfile->flags & DONT_SUBSTITUTE) == DONT_SUBSTITUTE)
		return buf[0] ? strdup(buf) : NULL;
	return buf[0] ? dotconf_substitute_env(configfile, strdup(buf)) : NULL;
}

/* dotconf_find_command remains here for backwards compatability. it's
 * internally unused since dot.conf 1.0.9 because it cannot handle the
 * DUPLICATE_OPTION_NAMES flag
 */
configoption_t *dotconf_find_command(configfile_t * configfile,
				     const char *command)
{
	configoption_t *option;
	int i = 0, mod = 0, done = 0;

	for (option = 0, mod = 0; configfile->config_options[mod] && !done;
	     mod++)
		for (i = 0; configfile->config_options[mod][i].name[0]; i++) {
			if (!configfile->cmp_func(name,
						  configfile->
						  config_options[mod][i].name,
						  CFG_MAX_OPTION)) {
				option =
				    (configoption_t *) & configfile->
				    config_options[mod][i];
				/* TODO: this could be flagged: option overwriting by modules */
				done = 1;
				break;	/* found it; break out */
			}
		}

	/* handle ARG_NAME fallback */
	if ((option && option->name[0] == 0)
	    || configfile->config_options[mod - 1][i].type == ARG_NAME) {
		option =
		    (configoption_t *) & configfile->config_options[mod - 1][i];
	}

	return option;
}

void dotconf_set_command(configfile_t * configfile,
			 const configoption_t * option, signed char *args,
			 command_t * cmd)
{
	signed char *eob = args + strlen(args);

	/* fill in the command_t structure with values we already know */
	cmd->name = option->type == ARG_NAME ? name : option->name;
	cmd->option = (configoption_t *) option;
	cmd->context = configfile->context;
	cmd->configfile = configfile;
	cmd->data.list = (char **)calloc(CFG_VALUES, sizeof(char *));
	cmd->data.str = 0;

	if (option->type == ARG_RAW) {
		/* if it is an ARG_RAW type, save some time and call the
		   callback now */
		cmd->data.str = strdup(args);
	} else if (option->type == ARG_STR) {
		signed char *cp = args;

		/* check if it's a here-document and act accordingly */
		skip_whitespace(&cp, eob - cp, 0);

		if (!strncmp("<<", cp, 2)) {
			cmd->data.str =
			    dotconf_get_here_document(configfile, cp + 2);
			cmd->arg_count = 1;
		}
	}

	if (!(option->type == ARG_STR && cmd->data.str != 0)) {
		/* we only get here for non-heredocument lines */

		skip_whitespace(&args, eob - args, 0);

		cmd->arg_count = 0;
		while (cmd->arg_count < (CFG_VALUES - 1)
		       && (cmd->data.list[cmd->arg_count] =
			   dotconf_read_arg(configfile, &args))) {
			cmd->arg_count++;
		}

		skip_whitespace(&args, eob - args, 0);

		if (cmd->arg_count && cmd->data.list[cmd->arg_count - 1]
		    && *args)
			cmd->data.list[cmd->arg_count++] = strdup(args);

		/* has an option entry been found before or do we have to use a fallback? */
		if ((option->name && option->name[0] > 32)
		    || option->type == ARG_NAME) {
			/* found it, now check the type of args it wants */
			switch (option->type) {
			case ARG_TOGGLE:
				/* the value is true if the argument is Yes, On or 1 */
				if (cmd->arg_count < 1) {
					dotconf_warning(configfile,
							DCLOG_WARNING,
							ERR_WRONG_ARG_COUNT,
							"Missing argument to option '%s'",
							name);
					return;
				}

				cmd->data.value =
				    CFG_TOGGLED(cmd->data.list[0]);
				break;
			case ARG_INT:
				if (cmd->arg_count < 1) {
					dotconf_warning(configfile,
							DCLOG_WARNING,
							ERR_WRONG_ARG_COUNT,
							"Missing argument to option '%s'",
							name);
					return;
				}

				sscanf(cmd->data.list[0], "%li",
				       &cmd->data.value);
				break;

			case ARG_DOUBLE:
				if (cmd->arg_count < 1) {
					dotconf_warning(configfile,
							DCLOG_WARNING,
							ERR_WRONG_ARG_COUNT,
							"Missing argument to option '%s'",
							name);
					return;
				}

				cmd->data.dvalue = strtod(cmd->data.list[0], 0);
				break;

			case ARG_STR:
				if (cmd->arg_count < 1) {
					dotconf_warning(configfile,
							DCLOG_WARNING,
							ERR_WRONG_ARG_COUNT,
							"Missing argument to option '%s'",
							name);
					return;
				}

				cmd->data.str = strdup(cmd->data.list[0]);
				break;
			case ARG_NAME:	/* fall through */
			case ARG_LIST:
			case ARG_NONE:
			case ARG_RAW:	/* this has been handled before */
			default:
				break;
			}
		}
	}
}

void dotconf_free_command(command_t * command)
{
	int i;

	if (command->data.str)
		free(command->data.str);

	for (i = 0; i < command->arg_count; i++)
		free(command->data.list[i]);
	free(command->data.list);
}

const char *dotconf_handle_command(configfile_t * configfile, char *buffer)
{
	signed char *cp1;
	signed char *cp2;
	/* generic char pointer      */
	signed char *eob;	/* end of buffer; end of string  */
	const char *error;	/* error message we'll return */
	const char *context_error;	/* error message returned by contextchecker */
	command_t command;	/* command structure */
	int mod = 0;
	int next_opt_idx = 0;

	memset(&command, 0, sizeof(command_t));
	name[0] = 0;
	error = 0;
	context_error = 0;

	cp1 = buffer;
	eob = cp1 + strlen(cp1);

	skip_whitespace(&cp1, eob - cp1, 0);

	/* ignore comments and empty lines */
	if (!cp1 || !*cp1 || *cp1 == '#' || *cp1 == '\n' || *cp1 == EOF)
		return NULL;

	/* skip line if it only contains whitespace */
	if (cp1 == eob)
		return NULL;

	/* get first token: read the name of a possible option */
	cp2 = name;
	copy_word(&cp2, &cp1, MIN(eob - cp1, CFG_MAX_OPTION), 0);

	while (1) {
		const configoption_t *option;
		int done = 0;
		int opt_idx = 0;

		for (option = 0; configfile->config_options[mod] && !done;
		     mod++) {
			for (opt_idx = next_opt_idx;
			     configfile->config_options[mod][opt_idx].name[0];
			     opt_idx++) {
				if (!configfile->
				    cmp_func(name,
					     configfile->
					     config_options[mod][opt_idx].name,
					     CFG_MAX_OPTION)) {
					/* TODO: this could be flagged: option overwriting by modules */
					option =
					    (configoption_t *) & configfile->
					    config_options[mod][opt_idx];
					done = 1;
					break;	/* found one; break out */
				}
			}
		}

		if (!option)
			option =
			    get_argname_fallback(configfile->config_options[1]);

		if (!option || !option->callback) {
			if (error)
				return error;
			dotconf_warning(configfile, DCLOG_INFO,
					ERR_UNKNOWN_OPTION,
					"Unknown Config-Option: '%s'", name);
			return NULL;
		}

		/* set up the command structure (contextchecker wants this) */
		dotconf_set_command(configfile, option, cp1, &command);

		if (configfile->contextchecker)
			context_error =
			    configfile->contextchecker(&command,
						       command.option->context);

		if (!context_error)
			error = dotconf_invoke_command(configfile, &command);
		else {
			if (!error) {
				/* avoid returning another error then the first. This makes it easier to
				   reproduce problems. */
				error = context_error;
			}
		}

		dotconf_free_command(&command);

		if (!context_error
		    || !(configfile->flags & DUPLICATE_OPTION_NAMES)) {
			/* don't try more, just quit now. */
			break;
		}
	}

	return error;
}

const char *dotconf_command_loop_until_error(configfile_t * configfile)
{
	char buffer[CFG_BUFSIZE];

	while (!(dotconf_get_next_line(buffer, CFG_BUFSIZE, configfile))) {
		const char *error = dotconf_handle_command(configfile, buffer);
		if (error)
			return error;
	}
	return NULL;
}

int dotconf_command_loop(configfile_t * configfile)
{
	/* ------ returns: 0 for failure -- !0 for success ------------------------------------------ */
	char buffer[CFG_BUFSIZE];

	while (!(dotconf_get_next_line(buffer, CFG_BUFSIZE, configfile))) {
		const char *error = dotconf_handle_command(configfile, buffer);
		if (error != NULL) {
			if (dotconf_warning(configfile, DCLOG_ERR, 0, error))
				return 0;
		}
	}
	return 1;
}

void dotconf_cleanup(configfile_t * configfile)
{
	if (configfile->stream)
		fclose(configfile->stream);

	if (configfile->filename)
		free(configfile->filename);

	if (configfile->config_options)
		free(configfile->config_options);

	if (configfile->includepath)
		free(configfile->includepath);

	free(configfile);
}

configfile_t *dotconf_create(char *fname, const configoption_t * options,
			     context_t * context, unsigned long flags)
{
	char *dc_env = NULL;
	int registered = 0;
	configfile_t *new_cfg = calloc(1, sizeof(configfile_t));
	char *path = NULL;
	char *cwd = NULL;

	if (!new_cfg)
		return NULL;

	/*
	 * From here on, we can use dotconf_cleanup to free resources
	 * when errors occur.  All of our pointers are NULL, because
	 * we allocated with calloc.  When an error occurs, dotconf_cleanup
	 * will free all of the resources that were allocated prior to the
	 * error.
	 */

	new_cfg->context = context;
	new_cfg->flags = flags;
	if (new_cfg->flags & CASE_INSENSITIVE)
		new_cfg->cmp_func = strncasecmp;
	else
		new_cfg->cmp_func = strncmp;

	new_cfg->stream = fopen(fname, "r");
	if (new_cfg->stream == NULL) {
		fprintf(stderr, "Error opening configuration file '%s'\n",
			fname);
		dotconf_cleanup(new_cfg);
		return NULL;
	}

	registered = dotconf_register_options(new_cfg, dotconf_options);
	if (!registered) {
		dotconf_cleanup(new_cfg);
		return NULL;
	}

	registered = dotconf_register_options(new_cfg, options);
	if (!registered) {
		dotconf_cleanup(new_cfg);
		return NULL;
	}

	new_cfg->filename = strdup(fname);
	if (!new_cfg->filename) {
		dotconf_cleanup(new_cfg);
		return NULL;
	}

	new_cfg->includepath = malloc(CFG_MAX_FILENAME);
	if (!new_cfg->includepath) {
		dotconf_cleanup(new_cfg);
		return NULL;
	}

	new_cfg->includepath[0] = 0x00;

	/*
	 * take default includepath from environment if present
	 * Otherwise, resolve the path of the configuration file and use that
	 * as default includepath.
	 */
	dc_env = getenv(CFG_INCLUDEPATH_ENV);
	if (dc_env != NULL) {
		snprintf(new_cfg->includepath, CFG_MAX_FILENAME, "%s", dc_env);
	} else {
		path = get_path(fname);
		if (path != NULL) {
			if (path[0] == '/') {
				snprintf(new_cfg->includepath, CFG_MAX_FILENAME, "%s", path);
			} else {
				cwd = get_cwd();
				if (cwd != NULL) {
					snprintf(new_cfg->includepath, CFG_MAX_FILENAME,
						"%s/%s", cwd, path);
					free(cwd);
				}
			}
			free(path);
		}
	}
	return new_cfg;
}

/* ------ internal utility function that verifies if a character is in the WILDCARDS list -- */
int dotconf_is_wild_card(char value)
{
	int retval = 0;
	int i;
	int wildcards_len = strlen(WILDCARDS);

	for (i = 0; i < wildcards_len; i++) {
		if (value == WILDCARDS[i]) {
			retval = 1;
			break;
		}
	}

	return retval;
}

/* ------ internal utility function that calls the appropriate routine for the wildcard passed in -- */
int dotconf_handle_wild_card(command_t * cmd, char wild_card, char *path,
			     char *pre, char *ext)
{
	int retval = 0;

	switch (wild_card) {
	case '*':

		retval = dotconf_handle_star(cmd, path, pre, ext);

		break;

	case '?':

		retval = dotconf_handle_question_mark(cmd, path, pre, ext);

		break;

	default:
		retval = -1;
	}

	return retval;
}

/* ------ internal utility function that frees allocated memory from dotcont_find_wild_card -- */
void dotconf_wild_card_cleanup(char *path, char *pre)
{

	if (path != NULL) {
		free(path);
	}

	if (pre != NULL) {
		free(pre);
	}

}

/* ------ internal utility function to check for wild cards in file path -- */
/* ------ path and pre must be freed by the developer ( dotconf_wild_card_cleanup) -- */
int dotconf_find_wild_card(char *filename, char *wildcard, char **path,
			   char **pre, char **ext)
{
	int retval = -1;
	int prefix_len = 0;
	int tmp_count = 0;
	char *tmp = 0;
	int found_path = 0;

	int len = strlen(filename);

	if (wildcard != NULL && len > 0 && path != NULL && pre != NULL
	    && ext != NULL) {
		prefix_len = strcspn(filename, WILDCARDS);	/* find any wildcard in WILDCARDS */

		if (prefix_len < len) {	/* Wild card found */
			tmp = filename + prefix_len;
			tmp_count = prefix_len + 1;

			while (tmp != filename && *(tmp) != '/') {
				tmp--;
				tmp_count--;
			}

			if (*(tmp) == '/') {
				*path = (char *)malloc(tmp_count + 1);
				found_path = 1;

			} else

				*path = (char *)malloc(1);

			*pre =
			    (char *)
			    malloc((prefix_len -
				    (tmp_count - (found_path ? 0 : 1))) + 1);

			if (*path && *pre) {
				if (found_path)
					strncpy(*path, filename, tmp_count);
				(*path)[tmp_count] = '\0';

				strncpy(*pre, (tmp + (found_path ? 1 : 0)),
					(prefix_len -
					 (tmp_count - (found_path ? 0 : 1))));
				(*pre)[(prefix_len -
					(tmp_count - (found_path ? 0 : 1)))] =
				    '\0';

				*ext = filename + prefix_len;
				*wildcard = (**ext);
				(*ext)++;

				retval = prefix_len;

			}

		}

	}

	return retval;
}

/* ------ internal utility function that compares two stings from back to front -- */
int dotconf_strcmp_from_back(const char *s1, const char *s2)
{
	int retval = 0;
	int i, j;
	int len_1 = strlen(s1);
	int len_2 = strlen(s2);

	for (i = len_1, j = len_2; (i >= 0 && j >= 0); i--, j--) {
		if (s1[i] != s2[j]) {
			retval = -1;
			break;
		}
	}

	return retval;
}

/* ------ internal utility function that determins if a string matches the '?' criteria -- */
int dotconf_question_mark_match(char *dir_name, char *pre, char *ext)
{
	int retval = -1;
	int dir_name_len = strlen(dir_name);
	int pre_len = strlen(pre);
	int ext_len = strlen(ext);
	int w_card_check = strcspn(ext, WILDCARDS);

	if ((w_card_check < ext_len) && (strncmp(dir_name, pre, pre_len) == 0)
	    && (strcmp(dir_name, ".") != 0) && (strcmp(dir_name, "..") != 0)) {
		retval = 1;	/* Another wildcard found */

	} else {

		if ((dir_name_len >= pre_len) &&
		    (strncmp(dir_name, pre, pre_len) == 0) &&
		    (strcmp(dir_name, ".") != 0) &&
		    (strcmp(dir_name, "..") != 0)) {
			retval = 0;	/* Matches no other wildcards */
		}

	}

	return retval;
}

/* ------ internal utility function that determins if a string matches the '*' criteria -- */
int dotconf_star_match(char *dir_name, char *pre, char *ext)
{
	int retval = -1;
	int dir_name_len = strlen(dir_name);
	int pre_len = strlen(pre);
	int ext_len = strlen(ext);
	int w_card_check = strcspn(ext, WILDCARDS);

	if ((w_card_check < ext_len) && (strncmp(dir_name, pre, pre_len) == 0)
	    && (strcmp(dir_name, ".") != 0) && (strcmp(dir_name, "..") != 0)) {
		retval = 1;	/* Another wildcard found */

	} else {

		if ((dir_name_len >= (ext_len + pre_len)) &&
		    (dotconf_strcmp_from_back(dir_name, ext) == 0) &&
		    (strncmp(dir_name, pre, pre_len) == 0) &&
		    (strcmp(dir_name, ".") != 0) &&
		    (strcmp(dir_name, "..") != 0)) {
			retval = 0;	/* Matches no other wildcards */
		}

	}

	return retval;
}

/* ------ internal utility function that determins matches for filenames with   -- */
/* ------ a '?' in name and calls the Internal Include function on that filename -- */
int dotconf_handle_question_mark(command_t * cmd, char *path, char *pre,
				 char *ext)
{
	configfile_t *included;
	DIR *dh = 0;
	struct dirent *dirptr = 0;
	int i;

	char new_pre[CFG_MAX_FILENAME];
	char already_matched[CFG_MAX_FILENAME];

	char wc = '\0';

	char *new_path = 0;
	char *wc_path = 0;
	char *wc_pre = 0;
	char *wc_ext = 0;
	char *temp = NULL;

	int pre_len;
	int new_path_len;
	int name_len = 0;
	int alloced = 0;
	int match_state = 0;

	pre_len = strlen(pre);

	if ((dh = opendir(path)) != NULL) {
		while ((dirptr = readdir(dh)) != NULL) {
			match_state =
			    dotconf_question_mark_match(dirptr->d_name, pre,
							ext);

			if (match_state >= 0) {
				name_len = strlen(dirptr->d_name);
				new_path_len =
				    strlen(path) + name_len + strlen(ext) + 1;

				if (!alloced) {
					if ((new_path =
					     (char *)malloc(new_path_len)) ==
					    NULL) {
						return -1;
					}

					alloced = new_path_len;

				} else {

					if (new_path_len > alloced) {
						temp =
						    realloc(new_path,
							    new_path_len);
						if (temp == NULL) {
							free(new_path);
							return -1;
						}
						new_path = temp;
						alloced = new_path_len;

					}

				}

				if (match_state == 1) {

					strncpy(new_pre, dirptr->d_name,
						(name_len >
						 pre_len) ? (pre_len +
							     1) : pre_len);
					new_pre[(name_len >
						 pre_len) ? (pre_len +
							     1) : pre_len] =
					    '\0';

					sprintf(new_path, "%s%s%s", path,
						new_pre, ext);

					if (strcmp(new_path, already_matched) ==
					    0) {
						continue;	/* Already searched this expression */

					} else {

						strcpy(already_matched,
						       new_path);

					}

					if (dotconf_find_wild_card
					    (new_path, &wc, &wc_path, &wc_pre,
					     &wc_ext) >= 0) {
						if (dotconf_handle_wild_card
						    (cmd, wc, wc_path, wc_pre,
						     wc_ext) < 0) {
							dotconf_warning(cmd->
									configfile,
									DCLOG_WARNING,
									ERR_INCLUDE_ERROR,
									"Error occured while processing wildcard %c\n"
									"Filename is '%s'\n",
									wc,
									new_path);

							free(new_path);
							dotconf_wild_card_cleanup
							    (wc_path, wc_pre);
							return -1;
						}

						dotconf_wild_card_cleanup
						    (wc_path, wc_pre);
						continue;
					}

				}

				sprintf(new_path, "%s%s", path, dirptr->d_name);

				if (access(new_path, R_OK)) {
					dotconf_warning(cmd->configfile,
							DCLOG_WARNING,
							ERR_INCLUDE_ERROR,
							"Cannot open %s for inclusion.\n"
							"IncludePath is '%s'\n",
							new_path,
							cmd->configfile->
							includepath);
					return -1;
				}

				included =
				    dotconf_create(new_path,
						   cmd->configfile->
						   config_options[1],
						   cmd->configfile->context,
						   cmd->configfile->flags);
				if (included) {
					for (i = 2;
					     cmd->configfile->config_options[i];
					     i++)
						dotconf_register_options
						    (included,
						     cmd->configfile->
						     config_options[i]);
					included->errorhandler =
					    cmd->configfile->errorhandler;
					included->contextchecker =
					    cmd->configfile->contextchecker;
					dotconf_command_loop(included);
					dotconf_cleanup(included);
				}

			}

		}

		closedir(dh);
		free(new_path);

	}

	return 0;
}

/* ------ internal utility function that determins matches for filenames with   --- */
/* ------ a '*' in name and calls the Internal Include function on that filename -- */
int dotconf_handle_star(command_t * cmd, char *path, char *pre, char *ext)
{
	configfile_t *included;
	DIR *dh = 0;
	struct dirent *dirptr = 0;

	char new_pre[CFG_MAX_FILENAME];
	char new_ext[CFG_MAX_FILENAME];
	char already_matched[CFG_MAX_FILENAME];

	char wc = '\0';

	char *new_path = 0;
	char *s_ext = 0;
	char *t_ext = 0;
	char *sub = 0;
	char *wc_path = 0;
	char *wc_pre = 0;
	char *wc_ext = 0;
	char *temp = NULL;

	int pre_len;
	int new_path_len;
	int name_len = 0;
	int alloced = 0;
	int match_state = 0;
	int t_ext_count = 0;
	int sub_count = 0;

	pre_len = strlen(pre);
	memset(already_matched, 0, CFG_MAX_FILENAME);
	s_ext = ext;

	while (dotconf_is_wild_card(*s_ext)) {	/* remove trailing wild-cards proceeded by * */
		s_ext++;
	}

	t_ext = s_ext;

	while (t_ext != NULL && !(dotconf_is_wild_card(*t_ext))
	       && *t_ext != '\0') {
		t_ext++;	/* find non-wild-card string */
		t_ext_count++;
	}

	strncpy(new_ext, s_ext, t_ext_count);
	new_ext[t_ext_count] = '\0';

	if ((dh = opendir(path)) != NULL) {
		while ((dirptr = readdir(dh)) != NULL) {
			sub_count = 0;
			t_ext_count = 0;

			match_state =
			    dotconf_star_match(dirptr->d_name, pre, s_ext);

			if (match_state >= 0) {
				name_len = strlen(dirptr->d_name);
				new_path_len =
				    strlen(path) + name_len + strlen(s_ext) + 1;

				if (!alloced) {
					if ((new_path =
					     (char *)malloc(new_path_len)) ==
					    NULL) {
						return -1;
					}

					alloced = new_path_len;

				} else {

					if (new_path_len > alloced) {
						temp =
						    realloc(new_path,
							    new_path_len);
						if (temp == NULL) {
							free(new_path);
							return -1;
						}
						new_path = temp;
						alloced = new_path_len;

					}

				}

				if (match_state == 1) {

					if ((sub =
					     strstr((dirptr->d_name + pre_len),
						    new_ext)) == NULL) {
						continue;
					}

					while (sub != dirptr->d_name) {
						sub--;
						sub_count++;
					}

					if (sub_count + t_ext_count > name_len) {
						continue;
					}

					strncpy(new_pre, dirptr->d_name,
						(sub_count + t_ext_count));
					new_pre[sub_count + t_ext_count] = '\0';
					strcat(new_pre, new_ext);

					sprintf(new_path, "%s%s%s", path,
						new_pre, t_ext);

					if (strcmp(new_path, already_matched) ==
					    0) {
						continue;	/* Already searched this expression */

					} else {

						strcpy(already_matched,
						       new_path);

					}

					if (dotconf_find_wild_card
					    (new_path, &wc, &wc_path, &wc_pre,
					     &wc_ext) >= 0) {
						if (dotconf_handle_wild_card
						    (cmd, wc, wc_path, wc_pre,
						     wc_ext) < 0) {
							dotconf_warning(cmd->
									configfile,
									DCLOG_WARNING,
									ERR_INCLUDE_ERROR,
									"Error occured while processing wildcard %c\n"
									"Filename is '%s'\n",
									wc,
									new_path);

							free(new_path);
							dotconf_wild_card_cleanup
							    (wc_path, wc_pre);
							return -1;
						}

						dotconf_wild_card_cleanup
						    (wc_path, wc_pre);
						continue;
					}

				}

				sprintf(new_path, "%s%s", path, dirptr->d_name);

				if (access(new_path, R_OK)) {
					dotconf_warning(cmd->configfile,
							DCLOG_WARNING,
							ERR_INCLUDE_ERROR,
							"Cannot open %s for inclusion.\n"
							"IncludePath is '%s'\n",
							new_path,
							cmd->configfile->
							includepath);
					return -1;
				}

				included =
				    dotconf_create(new_path,
						   cmd->configfile->
						   config_options[1],
						   cmd->configfile->context,
						   cmd->configfile->flags);
				if (included) {
					included->errorhandler =
					    cmd->configfile->errorhandler;
					included->contextchecker =
					    cmd->configfile->contextchecker;
					dotconf_command_loop(included);
					dotconf_cleanup(included);
				}

			}

		}

		closedir(dh);
		free(new_path);

	}

	return 0;
}

char *get_cwd(void)
{
	char *buf = calloc(1, CFG_MAX_FILENAME);

	if (buf == NULL)
		return NULL;
	getcwd(buf, CFG_MAX_FILENAME);
	return buf;
}

char *get_path(char *name)
{
	char *tmp;
	char *buf = NULL;
	int len = 0;

	tmp = strrchr(name, '/');
	if (tmp == NULL)
		return NULL;
	buf = calloc(1, CFG_MAX_FILENAME);
	if (buf == NULL)
		return NULL;
	if (tmp == name) {
		sprintf(buf, "/");
	} else {
		len = tmp - name + 1;
		if (len > CFG_MAX_FILENAME)
			len -= 1;
	}
		snprintf(buf, len, "%s", name);
	return buf;
}

/* ------ callbacks of the internal option (Include, IncludePath) ------------------------------- */
DOTCONF_CB(dotconf_cb_include)
{
	char *filename = 0;
	configfile_t *included;

	char wild_card;
	char *path = 0;
	char *pre = 0;
	char *ext = 0;

	if (cmd->configfile->includepath
	    && cmd->data.str[0] != '/'
	    && cmd->configfile->includepath[0] != '\0') {
		/* relative file AND include path is used */
		int len, inclen;
		char *sl;

		inclen = strlen(cmd->configfile->includepath);
		if ((len =
		     (strlen(cmd->data.str) + inclen + 1)) ==
		    CFG_MAX_FILENAME) {
			dotconf_warning(cmd->configfile, DCLOG_WARNING,
					ERR_INCLUDE_ERROR,
					"Absolute filename too long (>%d)",
					CFG_MAX_FILENAME);
			return NULL;
		}

		if (cmd->configfile->includepath[inclen - 1] == '/')
			sl = "";
		else {
			sl = "/";
			len++;
		}

		filename = malloc(len);
		snprintf(filename, len, "%s%s%s",
			 cmd->configfile->includepath, sl, cmd->data.str);
	} else			/* fully qualified, or no includepath */
		filename = strdup(cmd->data.str);

	/* Added wild card support here */
	if (dotconf_find_wild_card(filename, &wild_card, &path, &pre, &ext) >=
	    0) {
		if (dotconf_handle_wild_card(cmd, wild_card, path, pre, ext) <
		    0) {
			dotconf_warning(cmd->configfile, DCLOG_WARNING,
					ERR_INCLUDE_ERROR,
					"Error occured while attempting to process %s for inclusion.\n"
					"IncludePath is '%s'\n", filename,
					cmd->configfile->includepath);
		}

		dotconf_wild_card_cleanup(path, pre);
		free(filename);
		return NULL;
	}

	if (access(filename, R_OK)) {
		dotconf_warning(cmd->configfile, DCLOG_WARNING,
				ERR_INCLUDE_ERROR,
				"Cannot open %s for inclusion.\n"
				"IncludePath is '%s'\n", filename,
				cmd->configfile->includepath);
		free(filename);
		return NULL;
	}

	included = dotconf_create(filename, cmd->configfile->config_options[1],
				  cmd->configfile->context,
				  cmd->configfile->flags);
	if (included) {
		included->contextchecker =
		    (dotconf_contextchecker_t) cmd->configfile->contextchecker;
		included->errorhandler =
		    (dotconf_errorhandler_t) cmd->configfile->errorhandler;

		dotconf_command_loop(included);
		dotconf_cleanup(included);
	}

	free(filename);
	return NULL;
}

DOTCONF_CB(dotconf_cb_includepath)
{
	char *env = getenv(CFG_INCLUDEPATH_ENV);
	/* environment overrides configuration file setting */
	if (!env)
		snprintf(cmd->configfile->includepath, CFG_MAX_FILENAME, "%s",
			 cmd->data.str);
	return NULL;
}
