/*
 * Scanner for the configuration file
 */

%{

#include <setjmp.h>
#include <sys/stat.h>
#include <dirent.h>

#include "repmgr.h"
#include "configfile.h"

/*
 * flex emits a yy_fatal_error() function that it calls in response to
 * critical errors like malloc failure, file I/O errors, and detection of
 * internal inconsistency.  That function prints a message and calls exit().
 * Mutate it to instead call our handler, which jumps out of the parser.
 */
#undef fprintf
#define fprintf(file, fmt, msg) CONF_flex_fatal(msg)

enum
{
	CONF_ID = 1,
	CONF_STRING = 2,
	CONF_INTEGER = 3,
	CONF_REAL = 4,
	CONF_EQUALS = 5,
	CONF_UNQUOTED_STRING = 6,
	CONF_QUALIFIED_ID = 7,
	CONF_EOL = 99,
	CONF_ERROR = 100
};

static unsigned int ConfigFileLineno;
static const char *CONF_flex_fatal_errmsg;
static sigjmp_buf *CONF_flex_fatal_jmp;

static char *CONF_scanstr(const char *s);
static int	CONF_flex_fatal(const char *msg);

static bool ProcessConfigFile(const char *base_dir, const char *config_file, const char *calling_file, bool strict, int depth, KeyValueList *contents, ItemList *error_list, ItemList *warning_list);

static bool ProcessConfigFp(FILE *fp, const char *config_file, const char *calling_file, int depth, const char *base_dir, KeyValueList *contents, ItemList *error_list, ItemList *warning_list);

static bool ProcessConfigDirectory(const char *base_dir, const char *includedir, const char *calling_file, int depth, KeyValueList *contents, ItemList *error_list, ItemList *warning_list);

static char *AbsoluteConfigLocation(const char *base_dir, const char *location, const char *calling_file);

%}

%option 8bit
%option never-interactive
%option nodefault
%option noinput
%option nounput
%option noyywrap
%option warn
%option prefix="CONF_yy"


SIGN			("-"|"+")
DIGIT			[0-9]
HEXDIGIT		[0-9a-fA-F]

UNIT_LETTER		[a-zA-Z]

INTEGER			{SIGN}?({DIGIT}+|0x{HEXDIGIT}+){UNIT_LETTER}*

EXPONENT		[Ee]{SIGN}?{DIGIT}+
REAL			{SIGN}?{DIGIT}*"."{DIGIT}*{EXPONENT}?

LETTER			[A-Za-z_\200-\377]
LETTER_OR_DIGIT [A-Za-z_0-9\200-\377]

ID				{LETTER}{LETTER_OR_DIGIT}*
QUALIFIED_ID	{ID}"."{ID}

UNQUOTED_STRING {LETTER}({LETTER_OR_DIGIT}|[-._:/])*
STRING			\'([^'\\\n]|\\.|\'\')*\'

%%

\n				ConfigFileLineno++; return CONF_EOL;
[ \t\r]+		/* eat whitespace */
#.*				/* eat comment (.* matches anything until newline) */

{ID}			return CONF_ID;
{QUALIFIED_ID}	return CONF_QUALIFIED_ID;
{STRING}		return CONF_STRING;
{UNQUOTED_STRING} return CONF_UNQUOTED_STRING;
{INTEGER}		return CONF_INTEGER;
{REAL}			return CONF_REAL;
=				return CONF_EQUALS;

.				return CONF_ERROR;

%%


extern bool
ProcessRepmgrConfigFile(const char *config_file, const char *base_dir, ItemList *error_list, ItemList *warning_list)
{
	return ProcessConfigFile(base_dir, config_file, NULL, true, 0, NULL, error_list, warning_list);
}


extern bool
ProcessPostgresConfigFile(const char *config_file, const char *base_dir, bool strict, KeyValueList *contents, ItemList *error_list, ItemList *warning_list)
{
	return ProcessConfigFile(base_dir, config_file, NULL, strict, 0, contents, error_list, warning_list);
}

static bool
ProcessConfigFile(const char *base_dir, const char *config_file, const char *calling_file, bool strict, int depth, KeyValueList *contents, ItemList *error_list, ItemList *warning_list)
{
	char	   *abs_path;
	bool		success = true;
	FILE	   *fp;

	/*
	 * Reject file name that is all-blank (including empty), as that leads to
	 * confusion --- we'd try to read the containing directory as a file.
	 */
	if (strspn(config_file, " \t\r\n") == strlen(config_file))
	{
		return false;
	}

	/*
	 * Reject too-deep include nesting depth.  This is just a safety check to
	 * avoid dumping core due to stack overflow if an include file loops back
	 * to itself.  The maximum nesting depth is pretty arbitrary.
	 */
	if (depth > 10)
	{
		item_list_append_format(error_list,
								_("could not open configuration file \"%s\": maximum nesting depth exceeded"),
								config_file);
		return false;
	}

	abs_path = AbsoluteConfigLocation(base_dir, config_file, calling_file);

	/* Reject direct recursion */
	if (calling_file && strcmp(abs_path, calling_file) == 0)
	{
		item_list_append_format(error_list,
								_("configuration file recursion in \"%s\""),
								calling_file);
		pfree(abs_path);
		return false;
	}

	fp = fopen(abs_path, "r");
	if (!fp)
	{
		if (strict == false)
		{
			item_list_append_format(error_list,
									"skipping configuration file \"%s\"",
									abs_path);
		}
		else
		{
			item_list_append_format(error_list,
									"could not open configuration file \"%s\": %s",
									abs_path,
									strerror(errno));
			success = false;
		}
	}
	else
	{
		success = ProcessConfigFp(fp, abs_path, calling_file, depth + 1, base_dir, contents, error_list, warning_list);
	}

	free(abs_path);

	return success;
}

static bool
ProcessConfigFp(FILE *fp, const char *config_file, const char *calling_file, int depth, const char *base_dir, KeyValueList *contents, ItemList *error_list, ItemList *warning_list)
{
	volatile bool OK = true;
	volatile YY_BUFFER_STATE lex_buffer = NULL;
	sigjmp_buf	flex_fatal_jmp;
	int			errorcount;
	int			token;

	if (sigsetjmp(flex_fatal_jmp, 1) == 0)
	{
		CONF_flex_fatal_jmp = &flex_fatal_jmp;
	}
	else
	{
		/*
		 * Regain control after a fatal, internal flex error.  It may have
		 * corrupted parser state.  Consequently, abandon the file, but trust
		 * that the state remains sane enough for yy_delete_buffer().
		 */
		item_list_append_format(error_list,
								"%s at file \"%s\" line %u",
								CONF_flex_fatal_errmsg, config_file, ConfigFileLineno);
		OK = false;
		goto cleanup;
	}

	/*
	 * Parse
	 */
	ConfigFileLineno = 1;
	errorcount = 0;

	lex_buffer = yy_create_buffer(fp, YY_BUF_SIZE);
	yy_switch_to_buffer(lex_buffer);

	/* This loop iterates once per logical line */
	while ((token = yylex()))
	{
		char	   *opt_name = NULL;
		char	   *opt_value = NULL;

		if (token == CONF_EOL)	/* empty or comment line */
			continue;

		/* first token on line is option name */
		if (token != CONF_ID && token != CONF_QUALIFIED_ID)
			goto parse_error;
		opt_name = pstrdup(yytext);

		/* next we have an optional equal sign; discard if present */
		token = yylex();
		if (token == CONF_EQUALS)
			token = yylex();

		/* now we must have the option value */
		if (token != CONF_ID &&
			token != CONF_STRING &&
			token != CONF_INTEGER &&
			token != CONF_REAL &&
			token != CONF_UNQUOTED_STRING)
			goto parse_error;
		if (token == CONF_STRING)	/* strip quotes and escapes */
			opt_value = CONF_scanstr(yytext);
		else
			opt_value = pstrdup(yytext);

		/* now we'd like an end of line, or possibly EOF */
		token = yylex();
		if (token != CONF_EOL)
		{
			if (token != 0)
				goto parse_error;
			/* treat EOF like \n for line numbering purposes, cf bug 4752 */
			ConfigFileLineno++;
		}

		/* Handle include files */
		if (base_dir != NULL && strcasecmp(opt_name, "include_dir") == 0)
		{
			/*
			 * An include_dir directive isn't a variable and should be
			 * processed immediately.
			 */
			if (!ProcessConfigDirectory(base_dir, opt_value, config_file,
										depth + 1, contents,
									    error_list, warning_list))
				OK = false;
			yy_switch_to_buffer(lex_buffer);
			pfree(opt_name);
			pfree(opt_value);
		}
		else if (base_dir != NULL && strcasecmp(opt_name, "include_if_exists") == 0)
		{
			if (!ProcessConfigFile(base_dir, opt_value, config_file,
								   false, depth + 1, contents,
								   error_list, warning_list))
				OK = false;

			yy_switch_to_buffer(lex_buffer);
			pfree(opt_name);
			pfree(opt_value);
		}
		else if (base_dir != NULL && strcasecmp(opt_name, "include") == 0)
		{
			if (!ProcessConfigFile(base_dir, opt_value, config_file,
								   true, depth + 1, contents,
								   error_list, warning_list))
				OK = false;

			yy_switch_to_buffer(lex_buffer);
			pfree(opt_name);
			pfree(opt_value);
		}
		else
		{
			/* OK, process the option name and value */
			if (contents != NULL)
			{
				key_value_list_replace_or_set(contents,
											  opt_name,
											  opt_value);
			}
			else
			{
				parse_configuration_item(error_list,
										 warning_list,
										 opt_name,
										 opt_value);
			}

		}


		/* break out of loop if read EOF, else loop for next line */
		if (token == 0)
			break;
		continue;

parse_error:
		/* release storage if we allocated any on this line */
		if (opt_name)
			pfree(opt_name);
		if (opt_value)
			pfree(opt_value);

		/* report the error */
		if (token == CONF_EOL || token == 0)
		{
			item_list_append_format(error_list,
									_("syntax error in file \"%s\" line %u, near end of line"),
								    config_file, ConfigFileLineno - 1);
		}
		else
		{
			item_list_append_format(error_list,
									_("syntax error in file \"%s\" line %u, near token \"%s\""),
									config_file, ConfigFileLineno, yytext);
		}
		OK = false;
		errorcount++;

		/*
		 * To avoid producing too much noise when fed a totally bogus file,
		 * give up after 100 syntax errors per file (an arbitrary number).
		 * Also, if we're only logging the errors at DEBUG level anyway, might
		 * as well give up immediately.  (This prevents postmaster children
		 * from bloating the logs with duplicate complaints.)
		 */
		if (errorcount >= 100)
		{
			fprintf(stderr,
 				   _("too many syntax errors found, abandoning file \"%s\"\n"),
				   config_file);
			break;
		}

		/* resync to next end-of-line or EOF */
		while (token != CONF_EOL && token != 0)
			token = yylex();
		/* break out of loop on EOF */
		if (token == 0)
			break;
	}

cleanup:
	yy_delete_buffer(lex_buffer);

	return OK;
}

/*
 * Read and parse all config files in a subdirectory in alphabetical order
 *
 * includedir is the absolute or relative path to the subdirectory to scan.
 *
 * See ProcessConfigFp for further details.
 */
static bool
ProcessConfigDirectory(const char *base_dir, const char *includedir, const char *calling_file, int depth, KeyValueList *contents, ItemList *error_list, ItemList *warning_list)
{
	char	   *directory;
	DIR		   *d;
	struct dirent *de;
	char	  **filenames;
	int			num_filenames;
	int			size_filenames;
	bool		status;

	/*
	 * Reject directory name that is all-blank (including empty), as that
	 * leads to confusion --- we'd read the containing directory, typically
	 * resulting in recursive inclusion of the same file(s).
	 */
	if (strspn(includedir, " \t\r\n") == strlen(includedir))
	{
		item_list_append_format(error_list,
								_("empty configuration directory name: \"%s\""),
								includedir);

		return false;
	}

	directory = AbsoluteConfigLocation(base_dir, includedir, calling_file);
	d = opendir(directory);
	if (d == NULL)
	{
		item_list_append_format(error_list,
								_("could not open configuration directory \"%s\": %s"),
								directory,
								strerror(errno));
		status = false;
		goto cleanup;
	}

	/*
	 * Read the directory and put the filenames in an array, so we can sort
	 * them prior to processing the contents.
	 */
	size_filenames = 32;
	filenames = (char **) palloc(size_filenames * sizeof(char *));
	num_filenames = 0;

	while ((de = readdir(d)) != NULL)
	{
		struct stat st;
		char		filename[MAXPGPATH];

		/*
		 * Only parse files with names ending in ".conf".  Explicitly reject
		 * files starting with ".".  This excludes things like "." and "..",
		 * as well as typical hidden files, backup files, and editor debris.
		 */
		if (strlen(de->d_name) < 6)
			continue;
		if (de->d_name[0] == '.')
			continue;
		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
			continue;

		join_path_components(filename, directory, de->d_name);
		canonicalize_path(filename);
		if (stat(filename, &st) == 0)
		{
			if (!S_ISDIR(st.st_mode))
			{
				/* Add file to array, increasing its size in blocks of 32 */
				if (num_filenames >= size_filenames)
				{
					size_filenames += 32;
					filenames = (char **) repalloc(filenames,
											size_filenames * sizeof(char *));
				}
				filenames[num_filenames] = pstrdup(filename);
				num_filenames++;
			}
		}
		else
		{
			/*
			 * stat does not care about permissions, so the most likely reason
			 * a file can't be accessed now is if it was removed between the
			 * directory listing and now.
			 */
			item_list_append_format(error_list,
									_("could not stat file \"%s\": %s"),
									filename, strerror(errno));
			status = false;
			goto cleanup;
		}
	}

	if (num_filenames > 0)
	{
		int			i;

		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
		for (i = 0; i < num_filenames; i++)
		{
			if (!ProcessConfigFile(base_dir, filenames[i], calling_file,
								   true, depth, contents,
								   error_list, warning_list))
			{
				status = false;
				goto cleanup;
			}
		}
	}
	status = true;


cleanup:
	if (d)
		closedir(d);
	pfree(directory);
	return status;
}

/*
 *		scanstr
 *
 * Strip the quotes surrounding the given string, and collapse any embedded
 * '' sequences and backslash escapes.
 *
 * the string returned is palloc'd and should eventually be pfree'd by the
 * caller.
 */
static char *
CONF_scanstr(const char *s)
{
	char	   *newStr;
	int			len,
				i,
				j;

	Assert(s != NULL && s[0] == '\'');
	len = strlen(s);
	Assert(s != NULL);

	Assert(len >= 2);
	Assert(s[len - 1] == '\'');

	/* Skip the leading quote; we'll handle the trailing quote below */
	s++, len--;

	/* Since len still includes trailing quote, this is enough space */
	newStr = palloc(len);

	for (i = 0, j = 0; i < len; i++)
	{
		if (s[i] == '\\')
		{
			i++;
			switch (s[i])
			{
				case 'b':
					newStr[j] = '\b';
					break;
				case 'f':
					newStr[j] = '\f';
					break;
				case 'n':
					newStr[j] = '\n';
					break;
				case 'r':
					newStr[j] = '\r';
					break;
				case 't':
					newStr[j] = '\t';
					break;
				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
					{
						int			k;
						long		octVal = 0;

						for (k = 0;
							 s[i + k] >= '0' && s[i + k] <= '7' && k < 3;
							 k++)
							octVal = (octVal << 3) + (s[i + k] - '0');
						i += k - 1;
						newStr[j] = ((char) octVal);
					}
					break;
				default:
					newStr[j] = s[i];
					break;
			}					/* switch */
		}
		else if (s[i] == '\'' && s[i + 1] == '\'')
		{
			/* doubled quote becomes just one quote */
			newStr[j] = s[++i];
		}
		else
			newStr[j] = s[i];
		j++;
	}

	/* We copied the ending quote to newStr, so replace with \0 */
	Assert(j > 0 && j <= len);
	newStr[--j] = '\0';

	return newStr;
}

/*
 * Given a configuration file or directory location that may be a relative
 * path, return an absolute one.  We consider the location to be relative to
 * the directory holding the calling file, or to DataDir if no calling file.
 */
static char *
AbsoluteConfigLocation(const char *base_dir, const char *location, const char *calling_file)
{
	char		abs_path[MAXPGPATH];

	if (is_absolute_path(location))
		return strdup(location);

    if (calling_file != NULL)
	{
		strlcpy(abs_path, calling_file, sizeof(abs_path));
		get_parent_directory(abs_path);
		join_path_components(abs_path, abs_path, location);
		canonicalize_path(abs_path);
	}
	else if (base_dir != NULL)
	{
		join_path_components(abs_path, base_dir, location);
		canonicalize_path(abs_path);
	}
	else
	{
		strlcpy(abs_path, location, sizeof(abs_path));
	}

	return strdup(abs_path);
}


/*
 * Flex fatal errors bring us here.  Stash the error message and jump back to
 * ParseConfigFp().  Assume all msg arguments point to string constants; this
 * holds for flex 2.5.31 (earliest we support) and flex 2.5.35 (latest as of
 * this writing).  Otherwise, we would need to copy the message.
 *
 * We return "int" since this takes the place of calls to fprintf().
*/
static int
CONF_flex_fatal(const char *msg)
{
	CONF_flex_fatal_errmsg = msg;
	siglongjmp(*CONF_flex_fatal_jmp, 1);
	return 0;					/* keep compiler quiet */
}
