// SPDX-License-Identifier: BSD-2-Clause
/*
  Copyright (c) 2012-2020, Matthias Schiffer <mschiffer@universe-factory.net>
  All rights reserved.
*/

/**
   \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 },
	{ "force", TOK_FORCE },
	{ "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 },
	{ "multitap", TOK_MULTITAP },
	{ "no", TOK_NO },
	{ "on", TOK_ON },
	{ "packet", TOK_PACKET },
	{ "peer", TOK_PEER },
	{ "peers", TOK_PEERS },
	{ "persist", TOK_PERSIST },
	{ "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(FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *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(FASTD_CONFIG_STYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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(FASTD_CONFIG_STYPE *yylval, FASTD_CONFIG_LTYPE *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;
}
