// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2018 Cyril Hrubis <chrubis@suse.cz>
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/utsname.h>

#define TST_NO_DEFAULT_MAIN
#include "tst_test.h"
#include "tst_private.h"
#include "tst_kconfig.h"
#include "tst_bool_expr.h"

static int kconfig_skip_check(void)
{
	char *skipped = getenv("KCONFIG_SKIP_CHECK");

	if (skipped) {
		tst_res(TINFO, "Skipping kernel config check as requested");
		return 1;
	}

	return 0;
}

static const char *kconfig_path(char *path_buf, size_t path_buf_len)
{
	const char *path = getenv("KCONFIG_PATH");
	struct utsname un;

	if (path) {
		if (!access(path, F_OK))
			return path;

		tst_res(TWARN, "KCONFIG_PATH='%s' does not exist", path);
	}

	if (!access("/proc/config.gz", F_OK))
		return "/proc/config.gz";

	uname(&un);

	/* Common install module path */
	snprintf(path_buf, path_buf_len, "/lib/modules/%s/build/.config", un.release);

	if (!access(path_buf, F_OK))
		return path_buf;

	snprintf(path_buf, path_buf_len, "/lib/modules/%s/config", un.release);

	if (!access(path_buf, F_OK))
		return path_buf;

	/* Debian and derivatives */
	snprintf(path_buf, path_buf_len, "/boot/config-%s", un.release);

	if (!access(path_buf, F_OK))
		return path_buf;

	/* Clear Linux */
	snprintf(path_buf, path_buf_len, "/lib/kernel/config-%s", un.release);

	if (!access(path_buf, F_OK))
		return path_buf;

	tst_res(TINFO, "Couldn't locate kernel config!");

	return NULL;
}

static char is_gzip;

static FILE *open_kconfig(void)
{
	FILE *fp;
	char buf[1064];
	char path_buf[1024];
	const char *path = kconfig_path(path_buf, sizeof(path_buf));

	if (!path)
		return NULL;

	tst_res(TINFO, "Parsing kernel config '%s'", path);

	is_gzip = !!strstr(path, ".gz");

	if (is_gzip) {
		snprintf(buf, sizeof(buf), "zcat '%s'", path);
		fp = popen(buf, "r");
	} else {
		fp = fopen(path, "r");
	}

	if (!fp)
		tst_brk(TBROK | TERRNO, "Failed to open '%s'", path);

	return fp;
}

static void close_kconfig(FILE *fp)
{
	if (is_gzip)
		pclose(fp);
	else
		fclose(fp);
}

static inline int kconfig_parse_line(const char *line,
                                     struct tst_kconfig_var *vars,
                                     unsigned int vars_len)
{
	unsigned int i, var_len = 0;
	const char *var;
	int is_not_set = 0;

	while (isspace(*line))
		line++;

	if (*line == '#') {
		if (!strstr(line, "is not set"))
			return 0;

		is_not_set = 1;
	}

	var = strstr(line, "CONFIG_");

	if (!var)
		return 0;

	for (;;) {
		switch (var[var_len]) {
		case 'A' ... 'Z':
		case '0' ... '9':
		case '_':
			var_len++;
		break;
		default:
			goto out;
		break;
		}
	}

out:

	for (i = 0; i < vars_len; i++) {
		const char *val;
		unsigned int val_len = 0;

		if (vars[i].id_len != var_len)
			continue;

		if (strncmp(vars[i].id, var, var_len))
			continue;

		if (is_not_set) {
			vars[i].choice = 'n';
			return 1;
		}

		val = var + var_len;

		while (isspace(*val))
			val++;

		if (*val != '=')
			return 0;

		val++;

		while (isspace(*val))
			val++;

		while (!isspace(val[val_len]))
			val_len++;

		if (val_len == 1) {
			switch (val[0]) {
			case 'y':
				vars[i].choice = 'y';
				return 1;
			case 'm':
				vars[i].choice = 'm';
				return 1;
			}
		}

		vars[i].choice = 'v';
		vars[i].val = strndup(val, val_len);
	}

	return 0;
}

void tst_kconfig_read(struct tst_kconfig_var vars[], size_t vars_len)
{
	char line[128];
	unsigned int vars_found = 0;

	FILE *fp = open_kconfig();
	if (!fp)
		tst_brk(TBROK, "Cannot parse kernel .config");

	while (fgets(line, sizeof(line), fp)) {
		if (kconfig_parse_line(line, vars, vars_len))
			vars_found++;

		if (vars_found == vars_len)
			goto exit;
	}

exit:
	close_kconfig(fp);
}

static size_t array_len(const char *const kconfigs[])
{
	size_t i = 0;

	while (kconfigs[++i]);

	return i;
}

static const char *strnchr(const char *s, int c, unsigned int len)
{
	unsigned int i;

	for (i = 0; i < len; i++) {
		if (s[i] == c)
			return s + i;
	}

	return NULL;
}

static inline unsigned int get_len(const char* kconfig, unsigned int len)
{
	const char *sep = strnchr(kconfig, '=', len);

	if (!sep)
		return len;

	return sep - kconfig;
}

static void print_err(FILE *f, const struct tst_expr_tok *var,
                      size_t spaces, const char *err)
{
	size_t i;

	for (i = 0; i < var->tok_len; i++)
		fputc(var->tok[i], f);

	fputc('\n', f);

	while (spaces--)
		fputc(' ', f);

	fprintf(f, "^\n%s\n\n", err);
}

static int validate_var(const struct tst_expr_tok *var)
{
	size_t i = 7;

	if (var->tok_len < 7 || strncmp(var->tok, "CONFIG_", 7)) {
		print_err(stderr, var, 0, "Expected CONFIG_ prefix");
		return 1;
	}

	while (var->tok[i]) {
		char c;

		if (i >= var->tok_len)
			return 0;

		c = var->tok[i];

		if ((c >= 'A' && c <= 'Z') || c == '_') {
			i++;
			continue;
		}

		if (c >= '0' && c <= '9') {
			i++;
			continue;
		}

		if (c == '=') {
			i++;
			break;
		}

		print_err(stderr, var, i, "Unexpected character in variable name");
		return 1;
	}

	if (i >= var->tok_len) {

		if (var->tok[i-1] == '=') {
			print_err(stderr, var, i, "Missing value");
			return -1;
		}

		return 0;
	}

	if (var->tok[i] == '"') {
		do {
			i++;
		} while (i < var->tok_len && var->tok[i] != '"');

		if (i < var->tok_len - 1) {
			print_err(stderr, var, i, "Garbage after a string");
			return 1;
		}

		if (var->tok[i] != '"') {
			print_err(stderr, var, i, "Untermianted string");
			return 1;
		}

		return 0;
	}

	do {
		i++;
	} while (i < var->tok_len && isalnum(var->tok[i]));

	if (i < var->tok_len) {
		print_err(stderr, var, i, "Invalid character in variable value");
		return 1;
	}

	return 0;
}

static int validate_vars(struct tst_expr *const exprs[], unsigned int expr_cnt)
{
	unsigned int i;
	const struct tst_expr_tok *j;
	unsigned int ret = 0;

	for (i = 0; i < expr_cnt; i++) {
		for (j = exprs[i]->rpn; j; j = j->next) {
			if (j->op == TST_OP_VAR)
				ret |= validate_var(j);
		}
	}

	return ret;
}


static inline unsigned int get_var_cnt(struct tst_expr *const exprs[],
                                       unsigned int expr_cnt)
{
	unsigned int i;
	const struct tst_expr_tok *j;
	unsigned int cnt = 0;

	for (i = 0; i < expr_cnt; i++) {
		for (j = exprs[i]->rpn; j; j = j->next) {
			if (j->op == TST_OP_VAR)
				cnt++;
		}
	}

	return cnt;
}

static const struct tst_kconfig_var *find_var(const struct tst_kconfig_var vars[],
                                        unsigned int var_cnt,
                                        const char *var)
{
	unsigned int i;

	for (i = 0; i < var_cnt; i++) {
		if (!strcmp(vars[i].id, var))
			return &vars[i];
	}

	return NULL;
}

/*
 * Fill in the kconfig variables array from the expressions. Also makes sure
 * that each variable is copied to the array exaclty once.
 */
static inline unsigned int populate_vars(struct tst_expr *exprs[],
                                         unsigned int expr_cnt,
                                    struct tst_kconfig_var vars[])
{
	unsigned int i;
	struct tst_expr_tok *j;
	unsigned int cnt = 0;

	for (i = 0; i < expr_cnt; i++) {
		for (j = exprs[i]->rpn; j; j = j->next) {
			const struct tst_kconfig_var *var;

			if (j->op != TST_OP_VAR)
				continue;

			vars[cnt].id_len = get_len(j->tok, j->tok_len);

			if (vars[cnt].id_len + 1 >= sizeof(vars[cnt].id))
				tst_brk(TBROK, "kconfig var id too long!");

			strncpy(vars[cnt].id, j->tok, vars[cnt].id_len);
			vars[cnt].id[vars[cnt].id_len] = 0;
			vars[cnt].choice = 0;
			vars[cnt].val = NULL;

			var = find_var(vars, cnt, vars[cnt].id);

			if (var)
				j->priv = var;
			else
				j->priv = &vars[cnt++];
		}
	}

	return cnt;
}

static int map(struct tst_expr_tok *expr)
{
	const struct tst_kconfig_var *var = expr->priv;

	if (var->choice == 0)
		return 0;

	const char *val = strnchr(expr->tok, '=', expr->tok_len);

	/* CONFIG_FOO evaluates to true if y or m */
	if (!val)
		return var->choice == 'y' || var->choice == 'm';

	val++;

	unsigned int len = expr->tok_len - (val - expr->tok);
	char choice = 'v';

	if (!strncmp(val, "n", len))
		choice = 'n';

	if (!strncmp(val, "y", len))
		choice = 'y';

	if (!strncmp(val, "m", len))
		choice = 'm';

	if (choice != 'v')
		return var->choice == choice;

	if (var->choice != 'v')
		return 0;

	if (strlen(var->val) != len)
		return 0;

	return !strncmp(val, var->val, len);
}

static void dump_vars(const struct tst_expr *expr)
{
	const struct tst_expr_tok *i;
	const struct tst_kconfig_var *var;

	tst_res(TINFO, "Variables:");

	for (i = expr->rpn; i; i = i->next) {
		if (i->op != TST_OP_VAR)
			continue;

		var = i->priv;

		if (!var->choice) {
			tst_res(TINFO, " %s Undefined", var->id);
			continue;
		}

		if (var->choice == 'v') {
			tst_res(TINFO, " %s=%s", var->id, var->val);
			continue;
		}

		tst_res(TINFO, " %s=%c", var->id, var->choice);
	}
}

int tst_kconfig_check(const char *const kconfigs[])
{
	size_t expr_cnt = array_len(kconfigs);
	struct tst_expr *exprs[expr_cnt];
	unsigned int i, var_cnt;
	int ret = 0;

	if (kconfig_skip_check())
		return 0;

	for (i = 0; i < expr_cnt; i++) {
		exprs[i] = tst_bool_expr_parse(kconfigs[i]);

		if (!exprs[i])
			tst_brk(TBROK, "Invalid kconfig expression!");
	}

	if (validate_vars(exprs, expr_cnt))
		tst_brk(TBROK, "Invalid kconfig variables!");

	var_cnt = get_var_cnt(exprs, expr_cnt);
	struct tst_kconfig_var vars[var_cnt];

	var_cnt = populate_vars(exprs, expr_cnt, vars);

	tst_kconfig_read(vars, var_cnt);

	for (i = 0; i < expr_cnt; i++) {
		int val = tst_bool_expr_eval(exprs[i], map);

		if (val != 1) {
			ret = 1;
			tst_res(TINFO, "Constraint '%s' not satisfied!", kconfigs[i]);
			dump_vars(exprs[i]);
		}

		tst_bool_expr_free(exprs[i]);
	}

	for (i = 0; i < var_cnt; i++) {
		if (vars[i].choice == 'v')
			free(vars[i].val);
	}

	return ret;
}

char tst_kconfig_get(const char *confname)
{
	struct tst_kconfig_var var;

	if (kconfig_skip_check())
		return 0;

	var.id_len = strlen(confname);

	if (var.id_len >= sizeof(var.id))
		tst_brk(TBROK, "Kconfig var name \"%s\" too long", confname);

	strcpy(var.id, confname);
	var.choice = 0;
	var.val = NULL;

	tst_kconfig_read(&var, 1);

	if (var.choice == 'v')
		free(var.val);

	return var.choice;
}
