#include <errno.h>
#include <sys/stat.h>
#include "lib.h"
#include "unlink-directory.h"
#include "test-common.h"
#include "settings.h"
#include "fs-api.h"
#include "dict-private.h"

static void test_dict_set_get(struct dict *dict, const char *username,
			      const char *key, const char *value)
{
	const char *got_value, *error;
	struct dict_op_settings set = {
		.username = username,
	};
	struct dict_transaction_context *t = dict_transaction_begin(dict, &set);
	dict_set(t, key, value);
	if (dict_transaction_commit(&t, &error) < 0)
		i_fatal("dict_transaction_commit(%s) failed: %s", key, error);
	if (dict_lookup(dict, &set, pool_datastack_create(), key, &got_value,
			&error) < 0)
		i_fatal("dict_lookup(%s) failed: %s", key, error);
	test_assert_strcmp(got_value, value);
}

static bool test_file_exists(const char *path)
{
	struct stat st;
	if (stat(path, &st) < 0) {
		if (ENOTFOUND(errno)) return FALSE;
		i_fatal("stat(%s) failed: %m", path);
	}
	return TRUE;
}

static void test_dict_fs_set_get(void)
{
	test_begin("dict-fs get/set");
	const char *error;
	struct dict *dict;
	const char *const settings[] = {
		"dict", "fs",
		"dict/fs/dict_driver", "fs",
		"fs", "posix",
		"fs/posix/fs_driver", "posix",
		"fs_posix_prefix", ".test-dict/",
		NULL
	};
	struct settings_simple test_set;
	settings_simple_init(&test_set, settings);
	if (dict_init_auto(test_set.event, &dict, &error) <= 0)
		i_fatal("dict_init() failed: %s", error);

	/* shared paths */
	struct {
		const char *key;
		const char *path;
	} test_cases[] = {
		{ "shared/./key", ".test-dict/.../key" },
		{ "shared/../key", ".test-dict/..../key" },
		{ "shared/.../key", ".test-dict/...../key" },
		{ "shared/..../key", ".test-dict/....../key" },
		{ "shared/...../key", ".test-dict/......./key" },
		{ "shared/key/.", ".test-dict/key/..." },
		{ "shared/key/..", ".test-dict/key/...." },
		{ "shared/key/...", ".test-dict/key/....." },
		{ "shared/key/....", ".test-dict/key/......" },
		{ "shared/key/.....", ".test-dict/key/......." },
		{ "shared/key/.key", ".test-dict/key/.key" },
		{ "shared/key/..key", ".test-dict/key/..key" },
		{ "shared/key/...key", ".test-dict/key/...key" },
		{ "shared/.key/key", ".test-dict/.key/key" },
		{ "shared/..key/key", ".test-dict/..key/key" },
		{ "shared/...key/key", ".test-dict/...key/key" },
	};
	for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
		test_dict_set_get(dict, "testuser", test_cases[i].key, "1");
		test_assert(test_file_exists(test_cases[i].path));
	}

	/* per user paths */
	test_dict_set_get(dict, "testuser", "priv/value", "priv1");
	test_assert(test_file_exists(".test-dict/testuser/value"));
	test_dict_set_get(dict, "testuser", "priv/path/with/value", "priv2");
	test_assert(test_file_exists(".test-dict/testuser/path/with/value"));

	/* check that dots work correctly */
	test_dict_set_get(dict, "testuser", "shared/../test-dict-fs.c", "3");
	test_assert(test_file_exists(".test-dict/..../test-dict-fs.c"));
	test_dict_set_get(dict, "testuser", "shared/./test", "4");
	test_assert(test_file_exists(".test-dict/.../test"));
	test_dict_set_get(dict, "testuser", "shared/.test", "5");
	test_assert(test_file_exists(".test-dict/.test"));
	test_dict_set_get(dict, "testuser", "shared/..test", "6");
	test_assert(test_file_exists(".test-dict/..test"));

	/* make sure path traversal in usernames is prevented */
	const char *username_test_key = "priv/./key";
	struct {
		const char *username;
		const char *path;
	} username_test_cases[] = {
		{ ".testuser", ".test-dict/.testuser/...key" },
		{ "./testuser",   ".test-dict/.../testuser/.../key" },
		{ "./.testuser",   ".test-dict/.../.testuser/.../key" },
		{ "../testuser", ".test-dict/..../testuser/.../key" },
		{ "../.testuser", ".test-dict/..../.testuser/.../key" },
		{ "././testuser", ".test-dict/.../.../testuser/.../key" },
		{ "././.testuser", ".test-dict/.../.../testuser/.../key" },
		{ "./../testuser", ".test-dict/.../..../testuser/.../key" },
		{ "./../.testuser", ".test-dict/.../..../testuser/.../key" },
		{ ".././testuser", ".test-dict/..../.../testuser/.../key" },
		{ "./../.testuser", ".test-dict/.../..../testuser/.../key" },
		{ "../../testuser", ".test-dict/..../..../testuser/.../key" },
		{ "../../.testuser", ".test-dict/..../..../testuser/.../key" },
	};
	for (size_t i = 0; i < N_ELEMENTS(username_test_cases); i++) {
		test_dict_set_get(dict, username_test_cases[i].username,
				  username_test_key, "1");
		test_assert(test_file_exists(test_cases[i].path));
	}
	dict_deinit(&dict);

	if (unlink_directory(".test-dict", UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
		i_fatal("unlink_directory(.test_dict) failed: %s", error);
	settings_simple_deinit(&test_set);
	test_end();
}

int main(void)
{
	static void (*const test_functions[])(void) = {
		test_dict_fs_set_get,
		NULL
	};
	int ret;
	settings_info_register(&fs_setting_parser_info);
	dict_driver_register(&dict_driver_fs);
	ret = test_run(test_functions);
	dict_driver_unregister(&dict_driver_fs);
	return ret;
}
