/*
 *    Brutus source file implementing a configuration file class.
 *    Copyright (C) 2004-2008 OMC Denmark ApS.
 *
 *    This program 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 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program 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 this program; if not, write to the Free Software
 *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "config.h"
#include <fcntl.h>
#include <io.h>
#include <sys/stat.h>
#include <ctype.h>
#include <stdio.h>

#ifdef COMMENT_CHAR
#undef COMMENT_CHAR
#endif
#define COMMENT_CHAR '#'

#ifdef MAX_CONFIG_FILE_SIZE
#undef MAX_CONFIG_FILE_SIZE
#endif
#define MAX_CONFIG_FILE_SIZE (20480)

/* Will load the configuration file "file". Returns
 * zero on success or a positive number which indicates
 * a faulty configuration line or a negative number
 * which indicates that the configuration file is to
 * big or can not be read. */
int 
BRUTUS::Config::load(const char *file)
{
	if (!file)
		return -1;

	// initialize
	if (_items) {
		free(_items);
		_items = NULL;
	}
	if (_fbuf) {
		free(_fbuf);
		_fbuf = NULL;
	}
	_lnum = 1;

	// read file into buffer
	if (read_conf(file, &_fsize, (void**)&_fbuf))
		return -1;

	return itemize();
}

int 
BRUTUS::Config::itemize(void)
{
	size_t count = 0;
	char *p = _fbuf;

	// count maximum number of effective configuration lines
	for (size_t n = 0; n < _fsize; n++, p++) {
		// newline?
		if (*p == '\0')
			count++;
	}
	_items = (struct item*)malloc(sizeof(struct item) * (count + 1));

	// get token/value pairs
	p = _fbuf;
	count = 0;
	if ((wspace(*p)) || (*p == COMMENT_CHAR))
		if (get_next_line(&p))
			return _lnum;
	while (1) {
		if (!p)
			break;

		_items[count].token = p;

		// p will be somewhere within the value string when the function returns
		if (point_to_value(&p, &_items[count++].value))
			return _lnum;

		if (get_next_line(&p))
			return _lnum;
	}
	_items[count].token = NULL;
	_items[count].value = NULL;

	return 0;
}

/* Upon return val points to the first character in the null-
 * or whitespace-terminated value string.
 *
 * *str is a pointer to the first character in a null-terminated
 * configuration line.
 *
 * *str is modified by this function to point to the first
 * character in the value string.
 *
 * Will return (-1) on error or zero on success. */
int 
BRUTUS::Config::point_to_value(char **str, char **val)
{
	// not possible
	if (wspace(**str))
		return -1;

	// skip past token
	while ((!wspace(**str)) && (**str != '\0')) {
		(*str)++;
	}
	if (**str == '\0')
		return -1;

	// skip past whitespace to value
	while (wspace(**str)) {
		**str = '\0';
		(*str)++;
	}
	if (**str == '\0')
		return -1;

	*val = *str;

	// remove trailing whitespace
	char *tmp = *val;
	while (*tmp != '\0')
		tmp++;
	tmp--;
	while (wspace(*tmp)) {
		*tmp = '\0';
		tmp--;
	}

	return 0;
}

/* *str is a pointer to the first character
 * of the next configuration line skipping comments and
 * empty lines.
 *
 * *str is NULL on end-of-buffer.
 *
 * Returns (-1) on error or zero on success. */
int 
BRUTUS::Config::get_next_line(char **str)
{
	int retv = 0;
	size_t offset = *str - _fbuf;

	do {
		// skip printable characters
		while (**str != '\0') {
			(*str)++;

			offset++;
			if (offset == _fsize) {
				*str = NULL;
				goto out;
			}
		}

		// skip '\0's
		while (**str == '\0') {
			_lnum++;
			(*str)++;

			offset++;
			if (offset == _fsize) {
				*str = NULL;
				goto out;
			}
		}

		// quick check for floating text (<WHITESPACE>TEXT)
		if (wspace(**str)) {
			while (wspace(**str)) {
				(*str)++;
				offset++;
				if (offset == _fsize) {
					*str = NULL;
					goto out;
				}
			}

			// floating text
			if (**str != '\0') {
				retv = -1;
				goto out;
			}
			continue;
		}

		// configuration line?
		if (**str != COMMENT_CHAR)
			break;
	} while (1);

out:
	return retv;
}

/* Will return the corresponding value string or NULL
 * if non-existent. The token will be cleared if strip
 * is true. This makes multible like-named options
 * possible so as to sequentially read them all.
 *
 * The return value must be freed by callee. */
char*
BRUTUS::Config::get_value(const char *token)
{
	size_t n = 0;

	if (_items) {
		while (_items[n].value) {
			if ((_items[n].token) && !strcmp(token, _items[n].token)) {
				if (_stack && !strcmp(token, _stack))
					_items[n].token = NULL;
				return _strdup(_items[n].value);
			}
			n++;
		}
	}

	return NULL;
}

/* Reads all of file_name into *buf, as a set of adjoined
 * strings, correcting for CRLF.
 *
 * *size is the size of the file in bytes, not the size
 * of the buffer *buf. Callee must free() *buf if zero
 * is returned, otherwise not.
 *
 * I am reading one byte of the file at a time. This is to
 * avoid a second copy/allocation. Not that this method is
 * particular fast, but it makes the purpose so much clearer.
 * This function is not a bottleneck, therefore clarity before
 * speed.
 *
 * Returns -1 on error. */
int 
BRUTUS::Config::read_conf(const char *file_name, size_t *size, void **buf)
{
	FILE *file;
	int retv = 0;
	size_t len;
	char *tmp;
	char *first;
	struct stat fstatus;


	retv = stat(file_name, &fstatus);
	if (retv)
		return -1;

	if (fstatus.st_size > MAX_CONFIG_FILE_SIZE)
		return -1;

	tmp = (char*)malloc(fstatus.st_size);
	if (!tmp)
		return -1;
	memset((void*)tmp, '\0', fstatus.st_size);
	first = tmp;

	if (fopen_s(&file, file_name, "r")) {
		free(tmp);
		return -1;
	}

	char c;
	off_t pos = 0;
	while (1) {
		// For clarity, not speed
		len = fread((void*)&c, 1, 1, file);
		if (!len) {
			if (ferror(file))
				retv = -1;
			break;
		}

		// create null-terminated string
		if (c == '\n') {
			c = '\0';
		}

		// skip CR
		if (c != '\r') {
			*tmp = c;
			pos++;
			if (pos < fstatus.st_size)
				tmp++;
		}
	}
	if (file)
		fclose(file);

	if (retv)
		free(tmp);
	else {
		*buf = first;
		*size = pos;
	}

	return retv;
}

bool 
BRUTUS::Config::get_token_value_pair(const char *token,
				  const char *value)
{
	static size_t n = 0;

	if (!_items[n].token)
		return false;

	token = _items[n].token;
	value = _items[n].value;
	n++;

	return true;
}
