/*
  Copyright (c) 2012-2015, Matthias Schiffer <mschiffer@universe-factory.net>
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright notice,
       this list of conditions and the following disclaimer.
    2. Redistributions in binary form must reproduce the above copyright notice,
       this list of conditions and the following disclaimer in the documentation
       and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/**
   \file

   Config scanner for the fastd configuration file format
*/


#include "lex.h"

#include <stdlib.h>


/** The scanner context */
struct fastd_lex {
	FILE *file;			/**< The input file */

	bool needspace;			/**< Specifies if some kind of whitespace (or similar separator like a semicolon) is needed before the next token is parsed */

	size_t start;			/**< The start of the current token in the input buffer */
	size_t end;			/**< The end of the input read into the input buffer so far */
	size_t tok_len;			/**< The number of characters in the current token */
	char buffer[1024];		/**< The input buffer */
};

/** A keyword with the corresponding token ID */
typedef struct keyword {
	const char *keyword;		/**< The keyword */
	int token;			/**< The numerical token ID as generated by the parser */
} keyword_t;


/**
   The list of known keywords

   The keyword list must be sorted so binary search can work.
*/
static const keyword_t keywords[] = {
	{ "addresses", TOK_ADDRESSES },
	{ "any", TOK_ANY },
	{ "as", TOK_AS },
	{ "async", TOK_ASYNC },
	{ "auto", TOK_AUTO },
	{ "bind", TOK_BIND },
	{ "capabilities", TOK_CAPABILITIES },
	{ "cipher", TOK_CIPHER },
	{ "connect", TOK_CONNECT },
	{ "debug", TOK_DEBUG },
	{ "debug2", TOK_DEBUG2 },
	{ "default", TOK_DEFAULT },
	{ "disestablish", TOK_DISESTABLISH },
	{ "down", TOK_DOWN },
	{ "drop", TOK_DROP },
	{ "early", TOK_EARLY },
	{ "error", TOK_ERROR },
	{ "establish", TOK_ESTABLISH },
	{ "fatal", TOK_FATAL },
	{ "float", TOK_FLOAT },
	{ "forward", TOK_FORWARD },
	{ "from", TOK_FROM },
	{ "group", TOK_GROUP },
	{ "handshakes", TOK_HANDSHAKES },
	{ "hide", TOK_HIDE },
	{ "include", TOK_INCLUDE },
	{ "info", TOK_INFO },
	{ "interface", TOK_INTERFACE },
	{ "ip", TOK_IP },
	{ "ipv4", TOK_IPV4 },
	{ "ipv6", TOK_IPV6 },
	{ "key", TOK_KEY },
	{ "level", TOK_LEVEL },
	{ "limit", TOK_LIMIT },
	{ "log", TOK_LOG },
	{ "mac", TOK_MAC },
	{ "mark", TOK_MARK },
	{ "method", TOK_METHOD },
	{ "mode", TOK_MODE },
	{ "mtu", TOK_MTU },
	{ "no", TOK_NO },
	{ "on", TOK_ON },
	{ "packet", TOK_PACKET },
	{ "peer", TOK_PEER },
	{ "peers", TOK_PEERS },
	{ "pmtu", TOK_PMTU },
	{ "port", TOK_PORT },
	{ "post-down", TOK_POST_DOWN },
	{ "pre-up", TOK_PRE_UP },
	{ "protocol", TOK_PROTOCOL },
	{ "remote", TOK_REMOTE },
	{ "secret", TOK_SECRET },
	{ "secure", TOK_SECURE },
	{ "socket", TOK_SOCKET },
	{ "status", TOK_STATUS },
	{ "stderr", TOK_STDERR },
	{ "sync", TOK_SYNC },
	{ "syslog", TOK_SYSLOG },
	{ "tap", TOK_TAP },
	{ "to", TOK_TO },
	{ "tun", TOK_TUN },
	{ "up", TOK_UP },
	{ "use", TOK_USE },
	{ "user", TOK_USER },
	{ "verbose", TOK_VERBOSE },
	{ "verify", TOK_VERIFY },
	{ "warn", TOK_WARN },
	{ "yes", TOK_YES },
};

/** Compares two keyword_t instances by their keyword */
static int compare_keywords(const void *v1, const void *v2) {
	const keyword_t *k1 = v1, *k2 = v2;
	return strcmp(k1->keyword, k2->keyword);
}


/** Reads the next part of the input file into the input buffer */
static bool advance(fastd_lex_t *lex) {
	if (lex->start > 0) {
		memmove(lex->buffer, lex->buffer+lex->start, lex->end - lex->start);
		lex->end -= lex->start;
		lex->start = 0;
	}

	if (lex->end == sizeof(lex->buffer))
		return false;

	size_t l = fread(lex->buffer+lex->end, 1, sizeof(lex->buffer) - lex->end, lex->file);

	lex->end += l;
	return l;
}

/** Returns the current character (not yet added to the current token) */
static inline char current(fastd_lex_t *lex) {
	return lex->buffer[lex->start + lex->tok_len];
}

/** Returns the current token as a newly allocated string */
static char * get_token(fastd_lex_t *lex) {
	return fastd_strndup(lex->buffer+lex->start, lex->tok_len);
}

/** Tries to add the next character to the current token */
static bool next(YYLTYPE *yylloc, fastd_lex_t *lex, bool move) {
	if (lex->start + lex->tok_len >= lex->end)
		return false;

	if (current(lex) == '\n') {
		yylloc->last_column = 0;
		yylloc->last_line++;
	}
	else {
		yylloc->last_column++;
	}

	if (move)
		lex->start++;
	else
		lex->tok_len++;


	if (lex->start + lex->tok_len >= lex->end)
		return advance(lex);

	return true;
}

/** Removes the current token from the input buffer */
static void consume(fastd_lex_t *lex, bool needspace) {
	lex->start += lex->tok_len;
	lex->tok_len = 0;

	lex->needspace = needspace;
}

/** Signals an error caused by an I/O error */
static int io_error(YYSTYPE *yylval, UNUSED fastd_lex_t *lex) {
	yylval->error = "I/O error";
	return -1;
}

/** Signals an error caused by a syntax error */
static int syntax_error(YYSTYPE *yylval, fastd_lex_t *lex) {
	if (ferror(lex->file))
		return io_error(yylval, lex);

	yylval->error = "syntax error";
	return -1;
}

/** Skips a block comment */
static int consume_comment(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	char prev = 0;

	while (next(yylloc, lex, true)) {
		if (prev == '*' && current(lex) == '/') {
			next(yylloc, lex, true);
			consume(lex, false);
			return 0;
		}

		prev = current(lex);
	}

	if (ferror(lex->file))
		return io_error(yylval, lex);

	yylval->error = "unterminated block comment";
	return -1;
}

/** Signals an error caused by an unterminated string */
static int unterminated_string(YYSTYPE *yylval, fastd_lex_t *lex) {
	if (ferror(lex->file))
		return io_error(yylval, lex);

	yylval->error = "unterminated string";
	return -1;
}

/** Tries to process the current input as a string */
static int parse_string(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	char *buf = NULL;
	size_t len = 1024;
	size_t pos = 0;

	if (lex->needspace)
		return syntax_error(yylval, lex);

	buf = fastd_alloc(len);

	while (true) {
		if (!next(yylloc, lex, true)) {
			free(buf);
			return unterminated_string(yylval, lex);
		}

		char cur = current(lex);

		if (cur == '"')
			break;

		if (cur == '\\') {
			if (!next(yylloc, lex, true)) {
				free(buf);
				return unterminated_string(yylval, lex);
			}

			cur = current(lex);

			if (cur == '\n')
				continue;
		}

		if (pos >= len) {
			len *= 2;
			buf = fastd_realloc(buf, len);
		}

		buf[pos++] = cur;
	}

	yylval->str = fastd_string_stack_dupn(buf, pos);
	free(buf);

	next(yylloc, lex, true);
	consume(lex, true);

	return TOK_STRING;
}

/** Tries to process the current input as an IPv6 address */
static int parse_ipv6_address(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	if (lex->needspace)
		return syntax_error(yylval, lex);

	while (true) {
		if (!next(yylloc, lex, false))
			return syntax_error(yylval, lex);

		char cur = current(lex);

		if (!((cur >= '0' && cur <= '9') || (cur >= 'a' && cur <= 'f') || (cur >= 'A' && cur <= 'F') || cur == ':'))
			break;
	}

	char cur = current(lex);

	bool ifname = (cur == '%') ? lex->start + lex->tok_len + 1 : 0;

	bool ok = ifname || (cur == ']');

	if (ok) {
		lex->buffer[lex->start + lex->tok_len] = 0;
		ok = inet_pton(AF_INET6, lex->buffer+lex->start+1, ifname ? &yylval->addr6_scoped.addr : &yylval->addr6);
	}

	if (!ok)
		return syntax_error(yylval, lex);

	if (ifname) {
		consume(lex, false);

		size_t pos = 0;

		while (true) {
			if (!next(yylloc, lex, true))
				return syntax_error(yylval, lex);

			cur = current(lex);

			if (cur == ']')
				break;

			if (cur == '\\') {
				if (!next(yylloc, lex, true))
					return syntax_error(yylval, lex);

				cur = current(lex);
			}

			if (pos == sizeof(yylval->addr6_scoped.ifname)-1)
				return syntax_error(yylval, lex);

			yylval->addr6_scoped.ifname[pos++] = cur;
		}

		yylval->addr6_scoped.ifname[pos] = 0;
	}

	next(yylloc, lex, true);
	consume(lex, true);

	return ifname ? TOK_ADDR6_SCOPED : TOK_ADDR6;
}

/** Tries to process the current input as an IPv4 address */
static int parse_ipv4_address(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	if (lex->needspace)
		return syntax_error(yylval, lex);

	while (next(yylloc, lex, false)) {
		char cur = current(lex);

		if (!((cur >= '0' && cur <= '9') || cur == '.'))
			break;
	}

	char *token = get_token(lex);
	bool ok = inet_pton(AF_INET, token, &yylval->addr4);

	free(token);

	if (!ok)
		return syntax_error(yylval, lex);

	consume(lex, true);

	return TOK_ADDR4;
}

/** Tries to process the current input as a number */
static int parse_number(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	bool digitonly = true;

	if (lex->needspace)
		return syntax_error(yylval, lex);

	while (next(yylloc, lex, false)) {
		char cur = current(lex);

		if (cur == '.' && digitonly)
			return parse_ipv4_address(yylval, yylloc, lex);

		if (!(cur >= '0' && cur <= '9')) {
			if ((cur >= 'a' && cur <= 'z') || (cur >= 'A' && cur <= 'Z'))
				digitonly = false;
			else
				break;
		}
	}

	char *endptr, *token = get_token(lex);
	yylval->uint64 = strtoull(token, &endptr, 0);

	bool ok = !*endptr;
	free(token);

	if (!ok)
		return syntax_error(yylval, lex);

	consume(lex, true);

	return TOK_UINT;
}

/** Tries to process the current input as a keyword */
static int parse_keyword(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	if (lex->needspace)
		return syntax_error(yylval, lex);

	while (next(yylloc, lex, false)) {
		char cur = current(lex);

		if (!((cur >= 'a' && cur <= 'z') || (cur >= '0' && cur <= '9') || cur == '-'))
			break;
	}

	char *token = get_token(lex);
	const keyword_t key = { .keyword = token };
	const keyword_t *ret = bsearch(&key, keywords, array_size(keywords), sizeof(keyword_t), compare_keywords);
	free(token);

	if (!ret)
		return syntax_error(yylval, lex);

	consume(lex, true);

	return ret->token;
}


/** Initializes a new scanner for the given file */
fastd_lex_t * fastd_lex_init(FILE *file) {
	fastd_lex_t *lex = fastd_new0(fastd_lex_t);
	lex->file = file;

	advance(lex);

	return lex;
}

/** Destroys the scanner */
void fastd_lex_destroy(fastd_lex_t *lex) {
	free(lex);
}

/** Returns a single lexeme of the scanned file */
int fastd_lex(YYSTYPE *yylval, YYLTYPE *yylloc, fastd_lex_t *lex) {
	int token;

	while (lex->end > lex->start) {
		yylloc->first_line = yylloc->last_line;
		yylloc->first_column = yylloc->last_column+1;

		switch (current(lex)) {
		case ' ':
		case '\n':
		case '\t':
		case '\r':
			next(yylloc, lex, true);
			consume(lex, false);
			continue;

		case ';':
		case ':':
		case '{':
		case '}':
			token = current(lex);
			next(yylloc, lex, true);
			consume(lex, false);
			return token;

		case '/':
			if (!next(yylloc, lex, true))
				return syntax_error(yylval, lex);

			if (current(lex) == '*') {
				token = consume_comment(yylval, yylloc, lex);
				if (token)
					return token;

				continue;
			}

			if (current(lex) != '/')
				return syntax_error(yylval, lex);

			/* fall-through */
		case '#':
			while (next(yylloc, lex, true)) {
				if (current(lex) == '\n')
					break;
			}

			next(yylloc, lex, true);
			consume(lex, false);
			continue;

		case '"':
			return parse_string(yylval, yylloc, lex);

		case '[':
			return parse_ipv6_address(yylval, yylloc, lex);

		case '0' ... '9':
			return parse_number(yylval, yylloc, lex);

		case 'a' ... 'z':
			return parse_keyword(yylval, yylloc, lex);

		default:
			return syntax_error(yylval, lex);
		}
	}

	if (ferror(lex->file))
		return io_error(yylval, lex);

	return 0;
}
