/* libnih
 *
 * config.c - configuration file parsing
 *
 * Copyright © 2009 Scott James Remnant <scott@netsplit.com>.
 * Copyright © 2009 Canonical Ltd.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2, as
 * published by the Free Software Foundation.
 *
 * 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */


#include <sys/types.h>

#include <limits.h>
#include <string.h>

#include <nih/macros.h>
#include <nih/alloc.h>
#include <nih/string.h>
#include <nih/file.h>
#include <nih/config.h>
#include <nih/logging.h>
#include <nih/error.h>
#include <nih/errors.h>


/* Prototypes for static functions */
static int              nih_config_block_end  (const char *file, size_t len,
					       size_t *lineno, size_t *pos,
					       const char *type,
					       size_t *endpos)
	__attribute__ ((warn_unused_result));
static NihConfigStanza *nih_config_get_stanza (const char *name,
					       NihConfigStanza *stanzas);


/**
 * nih_config_has_token:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number.
 *
 * Checks the current position in @file to see whether it has a parseable
 * token at this position; ie. we're not at the end of file, and the
 * current character is neither a comment or newline character.
 *
 * If this returns FALSE, it's normal to call nih_config_skip_comment()
 * to move to the next parseable point and check again.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * @pos is used as the offset within @file to begin, otherwise the start
 * is assumed.
 *
 * Returns: TRUE if the current character is before the end of file and
 * is neither a comment or newline, FALSE otherwise.
 **/
int
nih_config_has_token (const char *file,
		      size_t      len,
		      size_t     *pos,
		      size_t     *lineno)
{
	size_t p;

	nih_assert (file != NULL);

	p = (pos ? *pos : 0);
	if ((p < len) && (! strchr (NIH_CONFIG_CNL, file[p]))) {
		return TRUE;
	} else {
		return FALSE;
	}
}


/**
 * nih_config_token:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @dest: destination to copy to,
 * @delim: characters to stop on,
 * @dequote: remove quotes and escapes.
 * @toklen: pointer to store token length in.
 *
 * Parses a single token from @file which is stopped when any character
 * in @delim is encountered outside of a quoted string and not escaped
 * using a backslash.  The length of the parsed token is stored in @toklen
 * if given.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.  Usually when @dest is given, @file is instead the pointer to
 * the start of the token and @len is the difference between the start
 * and end of the token (NOT the return value from this function).
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * to @delim or past the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * To copy the token into another string, collapsing any newlines and
 * surrounding whitespace to a single space, pass @dest which should be
 * pre-allocated to the right size (obtained by calling this function
 * with NULL).
 *
 * If you also want quotes to be removed and escaped characters to be
 * replaced with the character itself, set @dequote to TRUE.
 *
 * Returns: zero on success, negative value on raised error.
 **/
int
nih_config_token (const char *file,
		  size_t      len,
		  size_t     *pos,
		  size_t     *lineno,
		  char       *dest,
		  const char *delim,
		  int         dequote,
		  size_t     *toklen)
{
	size_t  p, ws = 0, nlws = 0, qc = 0, i = 0;
	int     slash = FALSE, quote = 0, nl = FALSE, ret = 0;

	nih_assert (file != NULL);
	nih_assert (delim != NULL);

	/* We keep track of the following:
	 *   slash  whether a \ is in effect
	 *   quote  whether " or ' is in effect (set to which)
	 *   ws     number of consecutive whitespace chars so far
	 *   nlws   number of whitespace/newline chars
	 *   nl     TRUE if we need to copy ws into nlws at first non-WS
	 *   qc     number of quote characters that need removing.
	 */

	for (p = (pos ? *pos : 0); p < len; p++) {
		int extra = 0, isq = FALSE;

		if (slash) {
			slash = FALSE;

			/* Escaped newline */
			if (file[p] == '\n') {
				nlws++;
				nl = TRUE;
				if (lineno)
					(*lineno)++;
				continue;
			} else if ((file[p] == '\\')
				   || strchr (NIH_CONFIG_WS, file[p])) {
				extra++;
				if (dequote)
					qc++;
			} else if (dest) {
				dest[i++] = '\\';
			}
		} else if (file[p] == '\\') {
			slash = TRUE;
			continue;
		} else if (quote) {
			if (file[p] == quote) {
				quote = 0;
				isq = TRUE;
			} else if (file[p] == '\n') {
				nl = TRUE;
				if (lineno)
					(*lineno)++;
				continue;
			} else if (strchr (NIH_CONFIG_WS, file[p])) {
				ws++;
				continue;
			}
		} else if ((file[p] == '\"') || (file[p] == '\'')) {
			quote = file[p];
			isq = TRUE;
		} else if (strchr (delim, file[p])) {
			break;
		} else if (strchr (NIH_CONFIG_WS, file[p])) {
			ws++;
			continue;
		}

		if (nl) {
			/* Newline is recorded as a single space;
			 * any surrounding whitespace is lost.
			 */
			nlws += ws;
			if (dest)
				dest[i++] = ' ';
		} else if (ws && dest) {
			/* Whitespace that we've encountered to date is
			 * copied as it is.
			 */
			memcpy (dest + i, file + p - ws - extra, ws);
			i += ws;
		}

		/* Extra characters (the slash) needs to be copied
		 * unless we're dequoting the string
		 */
		if (extra && dest && (! dequote)) {
			memcpy (dest + i, file + p - extra, extra);
			i += extra;
		}

		if (dest && (! (isq && dequote)))
			dest[i++] = file[p];

		if (isq && dequote)
			qc++;

		ws = 0;
		nl = FALSE;
		extra = 0;
	}

	/* Add the NULL byte */
	if (dest)
		dest[i++] = '\0';


	/* A trailing slash on the end of the file makes no sense. */
	if (slash) {
		nih_error_raise (NIH_CONFIG_TRAILING_SLASH,
				 _(NIH_CONFIG_TRAILING_SLASH_STR));
		ret = -1;
		goto finish;
	}

	/* Leaving quotes open is also generally bad. */
	if (quote) {
		nih_error_raise (NIH_CONFIG_UNTERMINATED_QUOTE,
				 _(NIH_CONFIG_UNTERMINATED_QUOTE_STR));
		ret = -1;
		goto finish;
	}


	/* The token length we return is the length of the token with any
	 * newlines and surrounding whitespace converted to a single
	 * character and any trailing whitespace removed.
	 *
	 * The actual end of the text read is returned in *pos.
	 */
	if (toklen)
		*toklen = p - (pos ? *pos : 0) - ws - nlws - qc;

finish:
	if (pos)
		*pos = p;

	return ret;
}

/**
 * nih_config_next_token:
 * @parent: parent object for returned token,
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @delim: characters to stop on,
 * @dequote: remove quotes and escapes.
 *
 * Extracts a single token from @file which is stopped when any character
 * in @delim is encountered outside of a quoted string and not escaped
 * using a backslash.  If @delim contains any whitespace character, then
 * all whitespace after the token is also consumed, but not returned,
 * including that with escaped newlines within it.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * to @delim or past the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * If you also want quotes to be removed and escaped characters to be
 * replaced with the character itself, set @dequote to TRUE.
 *
 * If @parent is not NULL, it should be a pointer to another object which
 * will be used as a parent for the returned token.  When all parents
 * of the returned token are freed, the returned token will also be
 * freed.
 *
 * Returns: the token found or NULL on raised error.
 **/
char *
nih_config_next_token (const void *parent,
		       const char *file,
		       size_t      len,
		       size_t     *pos,
		       size_t     *lineno,
		       const char *delim,
		       int         dequote)
{
	size_t  p, arg_start, arg_len, arg_end;
	char   *arg = NULL;

	nih_assert (file != NULL);

	p = (pos ? *pos : 0);
	arg_start = p;
	if (nih_config_token (file, len, &p, lineno, NULL, delim, dequote,
			      &arg_len) < 0)
		goto finish;

	arg_end = p;
	if (! arg_len) {
		nih_error_raise (NIH_CONFIG_EXPECTED_TOKEN,
				 _(NIH_CONFIG_EXPECTED_TOKEN_STR));
		goto finish;
	}

	nih_config_skip_whitespace (file, len, &p, lineno);

	/* Copy in the new token */
	arg = nih_alloc (parent, arg_len + 1);
	if (! arg)
		nih_return_system_error (NULL);

	if (nih_config_token (file + arg_start, arg_end - arg_start, NULL,
			      NULL, arg, delim, dequote, NULL) < 0)
		goto finish;

finish:
	if (pos)
		*pos = p;

	return arg;
}

/**
 * nih_config_next_arg:
 * @parent: parent object for returned argument,
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number.
 *
 * Extracts a single argument from @file, a dequoted token that is stopped
 * on any comment, space or newline character that is not quoted or escaped
 * with a backslash.  Any whitespace after the argument is also consumed,
 * but not returned, including that with escaped newlines within it.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * to @delim or past the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * If @parent is not NULL, it should be a pointer to another object which
 * will be used as a parent for the returned argument.  When all parents
 * of the returned argument are freed, the returned argument will also be
 * freed.
 *
 * Returns: the argument found or NULL on raised error.
 **/
char *
nih_config_next_arg (const void *parent,
		     const char *file,
		     size_t      len,
		     size_t     *pos,
		     size_t     *lineno)
{
	nih_assert (file != NULL);

	return nih_config_next_token (parent, file, len, pos, lineno,
				      NIH_CONFIG_CNLWS, TRUE);
}

/**
 * nih_config_next_line:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number.
 *
 * Skips to the end of the current line in @file, ignoring any tokens,
 * comments, etc. along the way.  If you want to ensure that no arguments
 * are missed, use nih_config_skip_comment() instead.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * @pos is used as the offset within @file to begin, and will be updated
 * to point to past the end of the line or file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 **/
void
nih_config_next_line (const char *file,
		      size_t      len,
		      size_t     *pos,
		      size_t     *lineno)
{
	nih_assert (file != NULL);
	nih_assert (pos != NULL);

	/* Spool forwards until the end of the line */
	while ((*pos < len) && (file[*pos] != '\n'))
		(*pos)++;

	/* Step over it */
	if (*pos < len) {
		if (lineno)
			(*lineno)++;
		(*pos)++;
	}
}


/**
 * nih_config_skip_whitespace:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number.
 *
 * Skips an amount of whitespace and finds either the next token or the end
 * of the current line in @file.  Escaped newlines within the whitespace
 * are treated as whitespace.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * @pos is used as the offset within @file to begin, and will be updated
 * to point to past the end of the line or file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 **/
void
nih_config_skip_whitespace (const char *file,
			    size_t      len,
			    size_t     *pos,
			    size_t     *lineno)
{
	nih_assert (file != NULL);
	nih_assert (pos != NULL);

	/* Skip any amount of whitespace between them, we also need to
	 * detect an escaped newline here.
	 */
	while (*pos < len) {
		if (file[*pos] == '\\') {
			/* Escape character, only continue scanning if
			 * the next character is newline
			 */
			if ((len - *pos > 1) && (file[*pos + 1] == '\n')) {
				(*pos)++;
			} else {
				break;
			}
		} else if (! strchr (NIH_CONFIG_WS, file[*pos])) {
			break;
		}

		if (file[*pos] == '\n')
			if (lineno)
				(*lineno)++;

		/* Whitespace characer */
		(*pos)++;
	}
}

/**
 * nih_config_skip_comment:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number.
 *
 * Skips a comment and finds the end of the current line in @file.  If the
 * current position does not point to the end of a line, or a comment,
 * then an error is raised.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * @pos is used as the offset within @file to begin, and will be updated
 * to point to past the end of the line or file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * Returns: zero on success, negative value on raised error.
 **/
int
nih_config_skip_comment (const char *file,
			 size_t      len,
			 size_t     *pos,
			 size_t     *lineno)
{
	nih_assert (file != NULL);
	nih_assert (pos != NULL);

	if (nih_config_has_token (file, len, pos, lineno)) {
		nih_error_raise (NIH_CONFIG_UNEXPECTED_TOKEN,
				 _(NIH_CONFIG_UNEXPECTED_TOKEN_STR));
		return -1;
	}

	nih_config_next_line (file, len, pos, lineno);

	return 0;
}


/**
 * nih_config_parse_args:
 * @parent: parent object for returned array,
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number.
 *
 * Extracts a list of arguments from @file, each argument is separated
 * by whitespace and parsing is stopped when a newline is encountered
 * outside of a quoted string and not escaped using a backslash.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * past the end of the line or the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * The arguments are returned as a NULL-terminated array, with each argument
 * dequoted before being returned.
 *
 * If @parent is not NULL, it should be a pointer to another object which
 * will be used as a parent for the returned array.  When all parents
 * of the returned array are freed, the returned array will also be
 * freed.
 *
 * Returns: the list of arguments found or NULL on raised error.
 **/
char **
nih_config_parse_args (const void *parent,
		       const char *file,
		       size_t      len,
		       size_t     *pos,
		       size_t     *lineno)
{
	char   **args;
	size_t   p, nargs;

	nih_assert (file != NULL);

	/* Begin with an empty array */
	nargs = 0;
	args = nih_str_array_new (parent);
	if (! args)
		nih_return_system_error (NULL);

	/* Loop through the arguments until we hit a comment or newline */
	p = (pos ? *pos : 0);
	while (nih_config_has_token (file, len, &p, lineno)) {
		char *arg;

		arg = nih_config_next_arg (args, file, len, &p, lineno);
		if (! arg) {
			nih_free (args);
			args = NULL;
			goto finish;
		}

		if (! nih_str_array_addp (&args, parent, &nargs, arg)) {
			nih_error_raise_system ();
			nih_free (args);
			return NULL;
		}
	}

	/* nih_config_has_token has returned FALSE, we must be either past
	 * the end of the file, or at a comment or newline.
	 */
	if (nih_config_skip_comment (file, len, &p, lineno) < 0)
		nih_assert_not_reached ();

finish:
	if (pos)
		*pos = p;

	return args;
}

/**
 * nih_config_parse_command:
 * @parent: parent object for returned string,
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 *
 * Extracts a command and its arguments from @file, stopping when a
 * newline is encountered outside of a quoted string and not escaped
 * using a blackslash.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * past the end of the line or the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * The command is returned as a string allocated with nih_alloc().
 *
 * If @parent is not NULL, it should be a pointer to another object which
 * will be used as a parent for the returned string.  When all parents
 * of the returned string are freed, the returned string will also be
 * freed.
 *
 * Returns: the command found or NULL on raised error.
 **/
char *
nih_config_parse_command (const void *parent,
			  const char *file,
			  size_t      len,
			  size_t     *pos,
			  size_t     *lineno)
{
	char   *cmd = NULL;
	size_t  p, cmd_start, cmd_len, cmd_end;

	nih_assert (file != NULL);

	/* Find the length of string up to the first unescaped comment
	 * or newline.
	 */
	p = (pos ? *pos : 0);
	cmd_start = p;
	if (nih_config_token (file, len, &p, lineno, NULL,
			      NIH_CONFIG_CNL, FALSE, &cmd_len) < 0)
		goto finish;

	cmd_end = p;

	/* nih_config_token will eat up to the end of the file, a comment
	 * or a newline; so this must always succeed.
	 */
	if (nih_config_skip_comment (file, len, &p, lineno) < 0)
		nih_assert_not_reached ();

	/* Now copy the string into the destination. */
	cmd = nih_alloc (parent, cmd_len + 1);
	if (! cmd)
		nih_return_system_error (NULL);

	if (nih_config_token (file + cmd_start, cmd_end - cmd_start, NULL,
			      NULL, cmd, NIH_CONFIG_CNL, FALSE, NULL) < 0)
		goto finish;

finish:
	if (pos)
		*pos = p;

	return cmd;
}


/**
 * nih_config_parse_block:
 * @parent: parent object for returned string,
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @type: block identifier.
 *
 * Extracts a block of text from @line, stopping when the pharse "end @type"
 * is encountered without any quotes or blackslash escaping within it.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * past the end of the block or the end of the file.
 *
 * Either @file or @pos should point to the start of the block, after the
 * opening stanza, rather than the start of the stanza that opens it.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * The block is returned as a string allocated with nih_alloc().
 *
 * If @parent is not NULL, it should be a pointer to another object which
 * will be used as a parent for the returned string.  When all parents
 * of the returned string are freed, the returned string will also be
 * freed.
 *
 * Returns: the text contained within the block or NULL on raised error.
 **/
char *
nih_config_parse_block (const void *parent,
			const char *file,
			size_t      len,
			size_t     *pos,
			size_t     *lineno,
			const char *type)
{
	char    *block = NULL;
	size_t   p, pp, sh_start, sh_len, sh_end, ws;
	int      lines;

	nih_assert (file != NULL);
	nih_assert (type != NULL);

	/* We need to find the end of the block which is a line that looks
	 * like:
	 *
	 * 	WS? end WS @type CNLWS?
	 *
	 * Just to make things more difficult for ourselves, we work out the
	 * common whitespace on the start of the block lines and remember
	 * not to copy those out later
	 */
	p = (pos ? *pos : 0);
	sh_start = p;
	sh_end = 0;
	ws = 0;
	lines = 0;

	while (! nih_config_block_end (file, len, &p, lineno, type, &sh_end)) {
		size_t line_start;

		lines++;
		line_start = p;

		if (lines == 1) {
			/* Count whitespace on the first line */
			while ((p < len) && strchr (NIH_CONFIG_WS, file[p]))
				p++;

			ws = p - line_start;
		} else {
			/* Compare how much whitespace matches the
			 * first line; and decrease the count if it's
			 * not as much.
			 */
			while ((p < len) && (p - line_start < ws)
			       && (file[sh_start + p - line_start] == file[p]))
				p++;

			if (p - line_start < ws)
				ws = p - line_start;
		}

		nih_config_next_line (file, len, &p, lineno);

		if (p >= len) {
			nih_error_raise (NIH_CONFIG_UNTERMINATED_BLOCK,
					 _(NIH_CONFIG_UNTERMINATED_BLOCK_STR));
			goto finish;
		}
	}

	/* Copy the fragment into a string, removing common whitespace from
	 * the start.  We can be less strict here because we already know
	 * the contents, etc.
	 */
	sh_len = sh_end - sh_start - (ws * lines);
	block = nih_alloc (parent, sh_len + 1);
	if (! block)
		nih_return_system_error (NULL);

	block[0] = '\0';

	pp = sh_start;
	while (pp < sh_end) {
		size_t line_start;

		pp += ws;
		line_start = pp;

		while (file[pp++] != '\n')
			;

		strncat (block, file + line_start, pp - line_start);
	}

finish:
	if (pos)
		*pos = p;

	return block;
}

/**
 * nih_config_skip_block:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @type: block identifier,
 * @endpos: pointer to end of block.
 *
 * Skips over a block of text from @file, stopping when the phrase
 * "end @type" is encountered without any quotes or blackslash escaping
 * within it.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * past the end of the block and block marker or the end of the file.
 *
 * Either @file or @pos should point to the start of the block, after the
 * opening stanza, rather than the start of the stanza that opens it.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * @endpos will be set to the end of the block and the start of the block
 * marker, this is useful for determining the length of the block skipped,
 * to parse it for example.
 *
 * Returns: zero on success, negative value on raised error.
 **/
int
nih_config_skip_block (const char *file,
		       size_t      len,
		       size_t     *pos,
		       size_t     *lineno,
		       const char *type,
		       size_t     *endpos)
{
	size_t p;
	int    ret = 0;

	nih_assert (file != NULL);
	nih_assert (type != NULL);

	p = (pos ? *pos : 0);

	while (! nih_config_block_end (file, len, &p, lineno, type, endpos)) {
		nih_config_next_line (file, len, &p, lineno);

		if (p >= len) {
			nih_error_raise (NIH_CONFIG_UNTERMINATED_BLOCK,
					 _(NIH_CONFIG_UNTERMINATED_BLOCK_STR));
			ret = -1;
			goto finish;
		}
	}

finish:
	if (pos)
		*pos = p;

	return ret;
}

/**
 * nih_config_block_end:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @type: block identifier,
 * @endpos: pointer to end of block.
 *
 * Determines whether the current line contains an end of block marker,
 * and if so, sets @endpos to the end  of the block.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file.  @pos
 * will be updated to point past the end of the block and the end block
 * marker or the end of the file.
 *
 * @lineno will be incremented each time a new line is discovered.
 *
 * Returns: TRUE if at the end of the block, FALSE otherwise.
 **/
static int
nih_config_block_end (const char *file,
		      size_t      len,
		      size_t     *pos,
		      size_t     *lineno,
		      const char *type,
		      size_t     *endpos)
{
	size_t p;

	nih_assert (file != NULL);
	nih_assert (pos != NULL);
	nih_assert (type != NULL);

	p = *pos;

	/* Skip initial whitespace */
	while ((p < len) && strchr (NIH_CONFIG_WS, file[p]))
		p++;

	/* Check the first word (check we have at least 4 chars because of
	 * the need for whitespace immediately after)
	 */
	if ((len - p < 4) || strncmp (file + p, "end", 3))
		return FALSE;

	/* Must be whitespace after */
	if (! strchr (NIH_CONFIG_WS, file[p + 3]))
		return FALSE;

	/* Find the second word */
	p += 3;
	while ((p < len) && strchr (NIH_CONFIG_WS, file[p]))
		p++;

	/* Check the second word */
	if ((len - p < strlen (type))
	    || strncmp (file + p, type, strlen (type)))
		return FALSE;

	/* May be followed by whitespace */
	p += strlen (type);
	while ((p < len) && strchr (NIH_CONFIG_WS, file[p]))
		p++;

	/* May be a comment, in which case eat up to the newline
	 */
	if ((p < len) && (file[p] == '#')) {
		while ((p < len) && (file[p] != '\n'))
			p++;
	}

	/* Should be end of string, or a newline */
	if ((p < len) && (file[p] != '\n'))
		return FALSE;

	/* Point past the new line */
	if (p < len) {
		if (lineno)
			(*lineno)++;
		p++;
	}

	/* Set endpos to the beginning of the line (which is the end of the
	 * script) but update pos to point past this line.
	 */
	if (endpos)
		*endpos = *pos;
	*pos = p;

	return TRUE;
}


/**
 * nih_config_get_stanza:
 * @name: name of stanza,
 * @stanzas: table of stanza handlers.
 *
 * Locates the handler for the @name stanza in the @stanzas table.  The
 * last entry in the table should have NULL for both the name and handler
 * function pointers.
 *
 * If any entry exists with the stanza name "", this is returned instead
 * of NULL if no specific entry is found.
 *
 * Returns: stanza found or NULL if no handler for @name.
 **/
static NihConfigStanza *
nih_config_get_stanza (const char      *name,
		       NihConfigStanza *stanzas)
{
	NihConfigStanza *stanza, *catch = NULL;

	for (stanza = stanzas; (stanza->name && stanza->handler); stanza++) {
		if (! strlen (stanza->name))
			catch = stanza;

		if (! strcmp (stanza->name, name))
			return stanza;
	}

	return catch;
}

/**
 * nih_config_parse_stanza:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @stanzas: table of stanza handlers,
 * @data: pointer to pass to stanza handler.
 *
 * Extracts a configuration stanza from @file and calls the handler
 * function for that stanza found in the @stanzas table to handle the
 * rest of the line from thereon in.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * to @delim or past the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * Returns: zero on success or negative value on raised error.
 **/
int
nih_config_parse_stanza (const char      *file,
			 size_t           len,
			 size_t          *pos,
			 size_t          *lineno,
			 NihConfigStanza *stanzas,
			 void            *data)
{
	NihConfigStanza *stanza;
	nih_local char  *name = NULL;
	size_t           p;
	int              ret = -1;

	nih_assert (file != NULL);
	nih_assert (stanzas != NULL);

	p = (pos ? *pos : 0);

	/* Get the next dequoted argument from the file */
	name = nih_config_next_token (NULL, file, len, &p, lineno,
				      NIH_CONFIG_CNLWS, FALSE);
	if (! name)
		goto finish;

	/* Lookup the stanza for it */
	stanza = nih_config_get_stanza (name, stanzas);
	if (! stanza)
		nih_return_error (-1, NIH_CONFIG_UNKNOWN_STANZA,
				  _(NIH_CONFIG_UNKNOWN_STANZA_STR));

	ret = stanza->handler (data, stanza, file, len, &p, lineno);

finish:
	if (pos)
		*pos = p;

	return ret;
}


/**
 * nih_config_parse_file:
 * @file: file or string to parse,
 * @len: length of @file,
 * @pos: offset within @file,
 * @lineno: line number,
 * @stanzas: table of stanza handlers,
 * @data: pointer to pass to stanza handler.
 *
 * Parses configuration file lines from @file, skipping initial whitespace,
 * blank lines and comments while calling nih_config_parse_stanza() for
 * anything else.
 *
 * @file may be a memory mapped file, in which case @pos should be given
 * as the offset within and @len should be the length of the file as a
 * whole.
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * to @delim or past the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * Returns: zero on success, negative value on raised error.
 **/
int
nih_config_parse_file (const char      *file,
		       size_t           len,
		       size_t          *pos,
		       size_t          *lineno,
		       NihConfigStanza *stanzas,
		       void            *data)
{
	int    ret = -1;
	size_t p;

	nih_assert (file != NULL);
	nih_assert (stanzas != NULL);

	p = (pos ? *pos : 0);

	while (p < len) {
		/* Skip initial whitespace */
		while ((p < len) && strchr (NIH_CONFIG_WS, file[p]))
			p++;

		/* Skip lines with only comments in them; because has_token
		 * returns FALSE we know we're either past the end of the
		 * file, at a comment, or a newline.
		 */
		if (! nih_config_has_token (file, len, &p, lineno)) {
			if (nih_config_skip_comment (file, len,
						     &p, lineno) < 0)
				nih_assert_not_reached ();

			continue;
		}

		/* Must have a stanza, parse it */
		if (nih_config_parse_stanza (file, len, &p, lineno,
						     stanzas, data) < 0)
			goto finish;
	}

	ret = 0;

finish:
	if (pos)
		*pos = p;

	return ret;
}

/**
 * nih_config_parse:
 * @filename: name of file to parse,
 * @pos: offset within @file,
 * @lineno: line number,
 * @stanzas: table of stanza handlers,
 * @data: pointer to pass to stanza handler.
 *
 * Reads @filename into memory and them parses configuration lines from it
 * using nih_config_parse_file().
 *
 * If @pos is given then it will be used as the offset within @file to
 * begin (otherwise the start is assumed), and will be updated to point
 * to @delim or past the end of the file.
 *
 * If @lineno is given it will be incremented each time a new line is
 * discovered in the file.
 *
 * Returns: zero on success, negative value on raised error.
 **/
int
nih_config_parse (const char      *filename,
		  size_t          *pos,
		  size_t          *lineno,
		  NihConfigStanza *stanzas,
		  void            *data)
{
	nih_local char *file = NULL;
	size_t          len;
	int             ret;

	nih_assert (filename != NULL);

	file = nih_file_read (NULL, filename, &len);
	if (! file)
		return -1;

	if (lineno)
		*lineno = 1;

	ret = nih_config_parse_file (file, len, pos, lineno, stanzas, data);

	return ret;
}
