/*
 * Copyright (c) 2004 Sendmail, Inc. and its suppliers.
 *	All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 */

#include "sm/generic.h"
SM_RCSID("@(#)$Id: sm-conf-parse.c,v 1.12 2006/01/09 19:06:25 ca Exp $")

#if SM_LIBCONF_ALONE
#include <errno.h>
#include <ctype.h>
#include "sm-conf.h"
#else /* SM_LIBCONF_ALONE */
#include "sm/error.h"
#include "sm/ctype.h"
#include "sm/memops.h"
#include "sm/sm-conf.h"
#endif /* SM_LIBCONF_ALONE */

#include "sm-conf-state.h"
#include "sm-conf-token.h"
#include "sm-conf-node.h"
#include "sm-conf-parse.h"

/* SM-CONF-PARSE.C -- semantic analysis of the configuration file. */

#define SM_CONF_ERROR_MAX		15

/*
**  SM_CONF_PARSE_VALUE -- parse a value
**
**	<value> ::= <name> | "{" <value> [, <value>...] "}"
**
**	Parameters:
**		smc -- a configuration file handle.
**		value_out -- output parameter
**
**	Returns:
**		0 on success,
**		a Unix errno or a negative error code on error.
*/

static int
sm_conf_parse_value(sm_conf_T *smc, sm_conf_node_T **value_out)
{
	sm_conf_token_T			tok;
	enum sm_conf_token_type_E	token_type;
	sm_conf_node_T			*list, *value;
	int				err;
	int				saw_comma;
	char				tbuf[SM_CONF_TOKEN_BUFFER_SIZE];
	char				ebuf[SM_CONF_ERROR_BUFFER_SIZE];

	SM_IS_CONF(smc);
	SM_REQUIRE(value_out != NULL);

	*value_out = NULL;

	sm_memzero(&tok, sizeof(tok));
	tok.sm_magic = SM_CONF_TOKEN_MAGIC;

	switch (sm_conf_token(smc, &tok, 0))
	{
	  case SM_CONF_TOKEN_STRING:
	  case SM_CONF_TOKEN_ATOM:
		*value_out = sm_conf_value_new(smc,
			tok.sct_text, tok.sct_text_n, &tok);
		if  (*value_out == NULL)
			return ENOMEM;
		return 0;

	  case SM_CONF_TOKEN_OBRACE:
		break;

	  case SM_CONF_TOKEN_ERROR:
		SM_ASSERT(tok.sct_error != 0);
		err = sm_conf_error_add(smc, "%s:%d: "
			"scanner error while reading value: %s: %s",
			smc->smc_name, tok.sct_line,
			sm_conf_token_string(&tok, tbuf, sizeof tbuf),
			sm_conf_strerror(tok.sct_error, ebuf, sizeof ebuf));
		SM_ASSERT(*value_out == NULL);
		if (  (err == 0)
		   && (err = tok.sct_error) == 0)
			err = SM_CONF_ERR_SYNTAX;
		return err;

	  default:
		err = sm_conf_error_add(smc, "%s:%d: expected value, got %s",
			smc->smc_name, tok.sct_line,
			sm_conf_token_string(&tok, tbuf, sizeof tbuf));
		return err == 0 ? SM_CONF_ERR_SYNTAX : err;
	}

	/* parsed a "{" */

	if ((list = sm_conf_list_new(smc, &tok)) == NULL)
		return ENOMEM;

	do
	{
		/* parse list member <value> */
		if ((err = sm_conf_parse_value(smc, &value)) != 0)
		{
			sm_conf_node_destroy(smc, list);
			return err;
		}

		SM_ASSERT(value != NULL);
		sm_conf_list_append(smc, list, value);

		/* consume commas */
		saw_comma = 0;
		while (  (token_type = sm_conf_token_lookahead(smc))
		      == SM_CONF_TOKEN_COMMA)
		{
			(void)sm_conf_token(smc, &tok, 0);
			saw_comma++;
		}

		if (  token_type == SM_CONF_TOKEN_ERROR
		   || (token_type != SM_CONF_TOKEN_CBRACE && saw_comma != 1))
		{
			sm_conf_node_destroy(smc, list);
			token_type = sm_conf_token(smc, &tok, 0);

			if (token_type == SM_CONF_TOKEN_ERROR)
			{
				SM_ASSERT(tok.sct_error != 0);
				err = sm_conf_error_add(smc, "%s:%d: "
					"scanner error while reading value: "
					"%s: %s",
					smc->smc_name, tok.sct_line,
					sm_conf_token_string(&tok,
						tbuf, sizeof tbuf),
					sm_conf_strerror(tok.sct_error,
						ebuf, sizeof ebuf));
				if (err == 0)
					err = tok.sct_error;
			}
			else if (saw_comma > 1)
				err = sm_conf_error_add(smc, "%s:%d: "
					"expected list element, got comma",
					smc->smc_name, tok.sct_line);
			else
				err = sm_conf_error_add(smc, "%s:%d: "
					"expected comma between list elements, "
					"got %s",
					smc->smc_name, tok.sct_line,
					sm_conf_token_string(&tok,
						tbuf, sizeof tbuf));
			return err != 0 ? err : SM_CONF_ERR_SYNTAX;
		}

	} while (token_type != SM_CONF_TOKEN_CBRACE);

	/* Consume "}" */
	(void)sm_conf_token(smc, &tok, 0);

	*value_out = list;
	SM_ASSERT(*value_out != NULL);

	return 0;
}


/*
**  SM_CONF_PARSE_IS_KEYWORD -- check whether token is a keyword (syntax only)
**  Implement keyword policy beyond what the tokenizer requires.
**
**	Parameters:
**		token -- token to check.
**
**	Returns:
**		true iff token is a keyword
*/

static bool
sm_conf_parse_is_keyword(sm_conf_token_T const *token)
{
	size_t	i;

	SM_IS_CONF_TOKEN(token);

	if (  token->sct_type != SM_CONF_TOKEN_ATOM
	   || token->sct_text_n <= 0)
		return false;

	if (*token->sct_text != '_' && !ISALPHA(*token->sct_text))
		return false;

	for (i = 1; i < token->sct_text_n; i++)
		if (token->sct_text[i] != '_' && !ISALNUM(token->sct_text[i]))
			return false;

	return true;
}


/*
**  SM_CONF_PARSE_ENTRY -- parse an entry
**
**	<entry>   ::= <keyword> [<name>] "{" <entries> "}" [";"]
**		      <option-name> "=" <value> ";"
**
**	Parameters:
**		smc -- a configuration file handle.
**		container -- node
**
**	Returns:
**		0 on success,
**		a Unix errno or a negative error code on error.
*/

static int
sm_conf_parse_entry(sm_conf_T *smc, sm_conf_node_T *container)
{
	sm_conf_token_T			first, second;
	enum sm_conf_token_type_E	token_type;
	sm_conf_node_T			*value;
	int				err;
	char				tbuf[SM_CONF_TOKEN_BUFFER_SIZE];
	char				ebuf[SM_CONF_ERROR_BUFFER_SIZE];
	int				value_is_compound;

	sm_memzero(&first, sizeof(first));
	sm_memzero(&second, sizeof(second));
	first.sm_magic = SM_CONF_TOKEN_MAGIC;
	second.sm_magic = SM_CONF_TOKEN_MAGIC;

	switch (token_type = sm_conf_token(smc, &first,
		SM_CONF_TOKEN_FLAG_IDENTIFIER))
	{
	  case SM_CONF_TOKEN_EOF:
		return SM_CONF_ERR_EOF;

	  case SM_CONF_TOKEN_STRING:
	  case SM_CONF_TOKEN_ATOM:
		if (!sm_conf_parse_is_keyword(&first))
		{
			err = sm_conf_error_add(smc, "%s:%d: "
				"%s is not a valid identifier or keyword",
				smc->smc_name, first.sct_line,
				sm_conf_token_string(&first,
					tbuf, sizeof tbuf));
			if (err != 0)
				return err;
			/* continue to find more errors in the same pass. */
		}
		break;

	  case SM_CONF_TOKEN_ERROR:
		SM_ASSERT(first.sct_error != 0);
		err = sm_conf_error_add(smc, "%s:%d: "
			"scanner error while reading name or keyword: %s: %s",
			smc->smc_name, first.sct_line,
			sm_conf_token_string(&first, tbuf, sizeof tbuf),
			sm_conf_strerror(first.sct_error, ebuf, sizeof ebuf));
		return err == 0 ? first.sct_error : err;

	  default:

		if (  token_type == SM_CONF_TOKEN_CBRACE
		   && smc->smc_error_head != NULL)

			/*
			**  If we already had some errors, leave this one
			**  alone -- it's probably just us losing context.
			*/

			return SM_CONF_ERR_SYNTAX;

		err = sm_conf_error_add(smc, "%s:%d: "
			"expected name or keyword, got %s",
			smc->smc_name, first.sct_line,
			sm_conf_token_string(&first, tbuf, sizeof tbuf));
		return err == 0 ? SM_CONF_ERR_SYNTAX : err;
	}

	second.sct_text = NULL;
	second.sct_text_n = 0;

	value = NULL;
	err = 0;
	token_type = sm_conf_token_lookahead(smc);
	if (token_type != SM_CONF_TOKEN_OBRACE)
		token_type = sm_conf_token(smc, &second,
			SM_CONF_TOKEN_FLAG_IDENTIFIER);

	switch (token_type)
	{
	  case SM_CONF_TOKEN_EQUAL:
		/* <name> "=" ("{" <values> "}" [";"] | <value> ";" ) */
		value_is_compound =
			sm_conf_token_lookahead(smc) == SM_CONF_TOKEN_OBRACE;

		if ((err = sm_conf_parse_value(smc, &value)) != 0)
		{
			if (err == SM_CONF_ERR_EOF)
			{
				err = sm_conf_error_add(smc, "%s:%d: "
					"EOF after \"=\" -- expected value",
					smc->smc_name, first.sct_line);
				if (err == 0)
					err = SM_CONF_ERR_SYNTAX;
			}
			return err;
		}
		sm_conf_section_append(smc, container,
			first.sct_text, first.sct_text_n, value);
		value = NULL;

		if (sm_conf_token_lookahead(smc) == SM_CONF_TOKEN_SEMI)
			(void)sm_conf_token(smc, &first, 0);
		else if (!value_is_compound)
			err = sm_conf_error_add(smc, "%s:%d: ';' expected",
				smc->smc_name, first.sct_line);
		break;

	  case SM_CONF_TOKEN_STRING:
	  case SM_CONF_TOKEN_ATOM:

		/* <keyword> <name> "{" <entries> "}" [";"] */
		/* Fall through */

	  case SM_CONF_TOKEN_OBRACE:
		value = sm_conf_section_new(smc,
			first.sct_text,
			first.sct_text_n,
			second.sct_text,
			second.sct_text_n,
			&first);
		if (value == NULL)
			return ENOMEM;

		/* consume "{" */
		if (sm_conf_token(smc, &first, 0) != SM_CONF_TOKEN_OBRACE)
		{
			err = sm_conf_error_add(smc, "%s:%d: "
				"missing '=' before, or '{' after, %s",
				smc->smc_name, first.sct_line,
				sm_conf_token_string(&second,
					tbuf, sizeof tbuf));

			if (err == 0)
				err = SM_CONF_ERR_SYNTAX;
			break;
		}

		while (sm_conf_token_lookahead(smc) != SM_CONF_TOKEN_CBRACE)
			if ((err = sm_conf_parse_entry(smc, value)) != 0)
				break;

		if (err != 0)
		{
			if (err == SM_CONF_ERR_EOF)
			{
				err = sm_conf_error_add(smc, "%s:%d: "
					"\"{\" without matching \"}\"",
					smc->smc_name, first.sct_line);
				if (err == 0)
					err = SM_CONF_ERR_SYNTAX;
				return err;
			}
			break;
		}

		/* consume "}" */
		(void)sm_conf_token(smc, &second, 0);
		sm_conf_section_append(smc, container, NULL, 0, value);
		value = NULL;

		/* consume optional empty trailing [";"] */
		if (sm_conf_token_lookahead(smc) == SM_CONF_TOKEN_SEMI)
			(void)sm_conf_token(smc, &first, 0);
		break;

	  case SM_CONF_TOKEN_ERROR:
		SM_ASSERT(second.sct_error != 0);
		err = sm_conf_error_add(smc, "%s:%d: "
			"scanner error reading %s: %s",
			smc->smc_name, second.sct_line,
			sm_conf_token_string(&second, tbuf, sizeof tbuf),
			sm_conf_strerror(second.sct_error,
				ebuf, sizeof ebuf));
		if (err == 0)
			err = second.sct_error;
		break;

	  default:
		err = sm_conf_error_add(smc, "%s:%d: expected '{' or '=' "
			"after a name or keyword, got %s",
			smc->smc_name, second.sct_line,
			sm_conf_token_string(&second, tbuf, sizeof tbuf));
		return err == 0 ? SM_CONF_ERR_SYNTAX : err;
	}

	return err;
}

/*
**  SM_CONF_PARSE -- parse what we read from a configuration file.
**
**	Parameters:
**		smc -- a configuration file handle.
**
**	Returns:
**		0 on success,
**		a Unix errno or a negative error code on error.
*/

int
sm_conf_parse(sm_conf_T *smc)
{
	int		err;
	sm_conf_node_T	*root;

	root = sm_conf_section_new(smc, NULL, 0, NULL, 0, NULL);
	if (root == NULL)
		return ENOMEM;

	err = 0;
	for (;;)
	{
		int e;
		if ((e = sm_conf_parse_entry(smc, root)) == SM_CONF_ERR_EOF)
			break;

		if (e != 0)
		{
			sm_conf_token_T			tok;
			enum sm_conf_token_type_E	type;

			sm_memzero(&tok, sizeof(tok));
			tok.sm_magic = SM_CONF_TOKEN_MAGIC;

			if (err == 0)
				err = e;
			if (e != SM_CONF_ERR_SYNTAX)
				break;
			if (smc->smc_error_n > SM_CONF_ERROR_MAX)
				break;

			/* Skip past the next semicolon or open brace */
			while (  (type = sm_conf_token(smc, &tok, 0))
					!= SM_CONF_TOKEN_EOF
			      && type != SM_CONF_TOKEN_SEMI
			      && type != SM_CONF_TOKEN_OBRACE)
				;

			if (type == SM_CONF_TOKEN_EOF)
				break;
		}
	}

	if (err == 0 && smc->smc_error_n != 0)
		err = SM_CONF_ERR_SYNTAX;
	if (err == 0)
	{
		sm_conf_node_terminate(smc, root);
		smc->smc_root = root;
	}
	else
		sm_conf_node_destroy(smc, root);
	return err;
}


syntax highlighted by Code2HTML, v. 0.9.1