/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "array.h"
#include "crc32.h"
#include "str.h"
#include "str-parse.h"
#include "read-full.h"
#include "var-expand.h"
#include "unichar.h"
#include "settings-parser.h"

#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

struct boollist_removal {
	ARRAY_TYPE(const_string) *array;
	const char *key_suffix;
};

struct setting_parser_context {
	pool_t set_pool, parser_pool;
	int refcount;
        enum settings_parser_flags flags;

	const struct setting_parser_info *info;

	/* Pointer to structure containing the values */
	void *set_struct;
	ARRAY(struct boollist_removal) boollist_removals;

	char *error;
};

const char *set_value_unknown = "UNKNOWN_VALUE_WITH_VARIABLES";

#ifdef DEBUG_FAST
static const char *boollist_eol_sentry = "boollist-eol";
#endif
static const char *set_array_stop = "array-stop";

static void
setting_parser_copy_defaults(struct setting_parser_context *ctx,
			     const struct setting_parser_info *info)
{
	const struct setting_define *def;
	const char *p, **strp;

	if (info->defaults == NULL)
		return;

	memcpy(ctx->set_struct, info->defaults, info->struct_size);
	for (def = info->defines; def->key != NULL; def++) {
		switch (def->type) {
		case SET_ENUM: {
			/* fix enums by dropping everything after the
			   first ':' */
			strp = PTR_OFFSET(ctx->set_struct, def->offset);
			p = strchr(*strp, ':');
			if (p != NULL)
				*strp = p_strdup_until(ctx->set_pool, *strp, p);
			break;
		}
		default:
			break;
		}
	}
}

struct setting_parser_context *
settings_parser_init(pool_t set_pool, const struct setting_parser_info *root,
		     enum settings_parser_flags flags)
{
	struct setting_parser_context *ctx;
	pool_t parser_pool;

	parser_pool = pool_alloconly_create(MEMPOOL_GROWING"settings parser",
					    1024);
	ctx = p_new(parser_pool, struct setting_parser_context, 1);
	ctx->refcount = 1;
	ctx->set_pool = set_pool;
	ctx->parser_pool = parser_pool;
	ctx->flags = flags;

	ctx->info = root;
	if (root->struct_size > 0) {
		ctx->set_struct =
			p_malloc(ctx->set_pool, root->struct_size);
		setting_parser_copy_defaults(ctx, root);
	}

	pool_ref(ctx->set_pool);
	return ctx;
}

void settings_parser_ref(struct setting_parser_context *ctx)
{
	i_assert(ctx->refcount > 0);
	ctx->refcount++;
}

void settings_parser_unref(struct setting_parser_context **_ctx)
{
	struct setting_parser_context *ctx = *_ctx;

	if (ctx == NULL)
		return;
	*_ctx = NULL;

	i_assert(ctx->refcount > 0);
	if (--ctx->refcount > 0)
		return;
	i_free(ctx->error);
	pool_unref(&ctx->set_pool);
	pool_unref(&ctx->parser_pool);
}

unsigned int
setting_parser_info_get_define_count(const struct setting_parser_info *info)
{
	unsigned int count = 0;
	while (info->defines[count].key != NULL)
		count++;
	return count;
}

bool setting_parser_info_find_key(const struct setting_parser_info *info,
				  const char *key, unsigned int *idx_r)
{
	const char *suffix;

	for (unsigned int i = 0; info->defines[i].key != NULL; i++) {
		if (!str_begins(key, info->defines[i].key, &suffix))
			; /* mismatch */
		else if (suffix[0] == '\0') {
			/* full setting */
			while (i > 0 && info->defines[i].type == SET_ALIAS)
				i--;
			*idx_r = i;
			return TRUE;
		} else if (suffix[0] == '/' &&
			   (info->defines[i].type == SET_STRLIST ||
			    info->defines[i].type == SET_BOOLLIST)) {
			/* strlist key */
			*idx_r = i;
			return TRUE;
		}
	}
	return FALSE;
}

void *settings_parser_get_set(const struct setting_parser_context *ctx)
{
	return ctx->set_struct;
}

static void settings_parser_set_error(struct setting_parser_context *ctx,
				      const char *error)
{
	i_free(ctx->error);
	ctx->error = i_strdup(error);
}

const char *settings_parser_get_error(struct setting_parser_context *ctx)
{
	return ctx->error;
}

static const struct setting_define *
setting_define_find(const struct setting_parser_info *info, const char *key)
{
	const struct setting_define *list;

	for (list = info->defines; list->key != NULL; list++) {
		if (strcmp(list->key, key) == 0)
			return list;
	}
	return NULL;
}

static int
get_bool(struct setting_parser_context *ctx, const char *value, bool *result_r)
{
	const char *error;
	int ret;
	if ((ret = str_parse_get_bool(value, result_r, &error)) < 0)
		settings_parser_set_error(ctx, error);
	return ret;
}

static int
get_uintmax(struct setting_parser_context *ctx, const char *value,
	    uintmax_t *result_r)
{
	if (str_to_uintmax(value, result_r) < 0) {
		settings_parser_set_error(ctx, t_strdup_printf(
			"Invalid number %s: %s", value,
			str_num_error(value)));
		return -1;
	}
	return 0;
}

static int
get_uint(struct setting_parser_context *ctx, const char *value,
	 unsigned int *result_r)
{
	if (settings_value_is_unlimited(value)) {
		*result_r = SET_UINT_UNLIMITED;
		return 0;
	}
	if (str_to_uint(value, result_r) < 0) {
		settings_parser_set_error(ctx, t_strdup_printf(
			"Invalid number %s: %s", value,
			str_num_error(value)));
		return -1;
	}
	return 0;
}

static int
get_octal(struct setting_parser_context *ctx, const char *value,
	  unsigned int *result_r)
{
	unsigned long long octal;

	if (*value != '0')
		return get_uint(ctx, value, result_r);

	if (str_to_ullong_oct(value, &octal) < 0) {
		settings_parser_set_error(ctx,
			t_strconcat("Invalid number: ", value, NULL));
		return -1;
	}
	*result_r = (unsigned int)octal;
	return 0;
}

static int get_enum(struct setting_parser_context *ctx, const char *value,
		    char **result_r, const char *allowed_values)
{
	const char *p;

	while (allowed_values != NULL) {
		p = strchr(allowed_values, ':');
		if (p == NULL) {
			if (strcmp(allowed_values, value) == 0)
				break;

			settings_parser_set_error(ctx,
				t_strconcat("Invalid value: ", value, NULL));
			return -1;
		}

		if (strncmp(allowed_values, value, p - allowed_values) == 0 &&
		    value[p - allowed_values] == '\0')
			break;

		allowed_values = p + 1;
	}

	*result_r = p_strdup(ctx->set_pool, value);
	return 0;
}

static int
get_file(struct setting_parser_context *ctx, bool dup_value, const char **value)
{
	if (**value == '\0')
		return 0;
	const char *content = strchr(*value, '\n');
	if (content != NULL) {
		if (dup_value)
			*value = p_strdup(ctx->set_pool, *value);
		return 0;
	}

	const char *error;
	if (settings_parse_read_file(*value, *value, ctx->set_pool, NULL,
				     "", value, &error) < 0) {
		settings_parser_set_error(ctx, error);
		return -1;
	}
	return 0;
}

static int
get_in_port_zero(struct setting_parser_context *ctx, const char *value,
	 in_port_t *result_r)
{
	if (net_str2port_zero(value, result_r) < 0) {
		settings_parser_set_error(ctx, t_strdup_printf(
			"Invalid port number %s", value));
		return -1;
	}
	return 0;
}

int settings_parse_read_file(const char *path, const char *value_path,
			     pool_t pool, struct stat *st_r,
			     const char *prefix, const char **output_r,
			     const char **error_r)
{
	struct stat st;
	int fd;

	if ((fd = open(path, O_RDONLY)) == -1) {
		*error_r = t_strdup_printf("open(%s) failed: %m", path);
		return -1;
	}
	if (fstat(fd, &st) < 0) {
		*error_r = t_strdup_printf("fstat(%s) failed: %m", path);
		i_close_fd(&fd);
		return -1;
	}
	size_t prefix_len = strlen(prefix);
	size_t value_path_len = strlen(value_path);
	size_t buf_size = MALLOC_ADD3(prefix_len, value_path_len, 1);
	buf_size = MALLOC_ADD3(buf_size, (size_t)st.st_size, 1);
	char *buf = p_malloc(pool, buf_size);
	memcpy(buf, prefix, prefix_len);
	memcpy(buf + prefix_len, value_path, value_path_len);
	buf[prefix_len + value_path_len] = '\n';

	int ret = read_full(fd, buf + prefix_len + value_path_len + 1,
			    st.st_size);
	i_close_fd(&fd);
	if (ret < 0) {
		*error_r = t_strdup_printf("read(%s) failed: %m", path);
		return -1;
	}
	if (ret == 0) {
		*error_r = t_strdup_printf(
			"read(%s) failed: Unexpected EOF", path);
		return -1;
	}
	if (memchr(buf + prefix_len + value_path_len + 1, '\0', st.st_size) != NULL) {
		*error_r = t_strdup_printf(
			"%s contains NUL characters - This is not supported",
			path);
		return -1;
	}

	if (st_r != NULL)
		*st_r = st;
	*output_r = buf;
	return 0;
}

static int
settings_parse_strlist(struct setting_parser_context *ctx,
		       ARRAY_TYPE(const_string) *array,
		       const char *key, const char *value, const char **error_r)
{
	const char *const *items;
	const char *vkey, *vvalue;
	unsigned int i, count;

	/* If the next element after the visible array is set_array_stop, then
	   the strlist should not be modified any further. */
	if (array_is_created(array)) {
		items = array_get(array, &count);
		if (items[count] == set_array_stop)
			return 0;
	}

	const char *suffix = strchr(key, SETTINGS_SEPARATOR);
	if (suffix == NULL) {
		if (value[0] == '\0') {
			/* clear out the whole strlist */
			if (array_is_created(array))
				array_clear(array);
			return 0;
		}

		*error_r = t_strdup_printf(
			"Setting is a string list, use %s/key=value'", key);
		return -1;
	}
	key = settings_section_unescape(suffix + 1);
	vvalue = p_strdup(ctx->set_pool, value);

	if (!array_is_created(array))
		p_array_init(array, ctx->set_pool, 4);

	/* replace if it already exists */
	items = array_get(array, &count);
	for (i = 0; i < count; i += 2) {
		if (strcmp(items[i], key) == 0) {
			array_idx_set(array, i + 1, &vvalue);
			return 0;
		}
	}

	vkey = p_strdup(ctx->set_pool, key);
	array_push_back(array, &vkey);
	array_push_back(array, &vvalue);
	return 0;
}

int settings_parse_boollist_string(const char *value, pool_t pool,
				   ARRAY_TYPE(const_string) *dest,
				   const char **error_r)
{
	string_t *elem = t_str_new(32);
	const char *elem_dup;
	bool quoted = FALSE, end_of_quote = FALSE;
	for (unsigned int i = 0; value[i] != '\0'; i++) {
		switch (value[i]) {
		case '"':
			if (!quoted) {
				/* beginning of a string */
				if (str_len(elem) != 0) {
					*error_r = "'\"' in the middle of a string";
					return -1;
				}
				quoted = TRUE;
			} else if (end_of_quote) {
				*error_r = "Expected ',' or ' ' after '\"'";
				return -1;
			} else {
				/* end of a string */
				end_of_quote = TRUE;
			}
			break;
		case ' ':
		case ',':
			if (quoted && !end_of_quote) {
				/* inside a "quoted string" */
				str_append_c(elem, value[i]);
				break;
			}

			if (quoted || str_len(elem) > 0) {
				elem_dup = p_strdup(pool,
					settings_section_unescape(str_c(elem)));
				array_push_back(dest, &elem_dup);
				str_truncate(elem, 0);
			}
			quoted = FALSE;
			end_of_quote = FALSE;
			break;
		case '\\':
			if (quoted) {
				i++;
				if (value[i] == '\0') {
					*error_r = "Value ends with '\\'";
					return -1;
				}
			}
			/* fall through */
		default:
			if (end_of_quote) {
				*error_r = "Expected ',' or ' ' after '\"'";
				return -1;
			}
			str_append_c(elem, value[i]);
			break;
		}
	}
	if (quoted && !end_of_quote) {
		*error_r = "Missing ending '\"'";
		return -1;
	}
	if (quoted || str_len(elem) > 0) {
		elem_dup = p_strdup(pool, settings_section_unescape(str_c(elem)));
		array_push_back(dest, &elem_dup);
	}
	return 0;
}

const char *const *settings_boollist_get(const ARRAY_TYPE(const_string) *array)
{
	const char *const *strings = empty_str_array;
	unsigned int count;

	if (array_not_empty(array)) {
		strings = array_get(array, &count);
		i_assert(strings[count] == NULL);
#ifdef DEBUG_FAST
	i_assert(strings[count+1] == boollist_eol_sentry ||
		 (strings[count+1] == set_array_stop &&
		  strings[count+2] == boollist_eol_sentry));
#endif
	}
	return strings;

}

void settings_file_get(const char *value, pool_t path_pool,
		       struct settings_file *file_r)
{
	const char *p;

	if (*value == '\0') {
		file_r->path = "";
		file_r->content = "";
		return;
	}

	p = strchr(value, '\n');
	if (p == NULL)
		i_panic("Settings file value is missing LF");
	file_r->path = p_strdup_until(path_pool, value, p);
	file_r->content = p + 1;
}

bool settings_file_has_path(const char *value)
{
	/* value must be in <path><LF><content> format */
	const char *p = strchr(value, '\n');
	if (p == NULL)
		i_panic("Settings file value is missing LF");
	return p != value;
}

const char *settings_file_get_value(pool_t pool,
				    const struct settings_file *file)
{
	const char *path = file->path != NULL ? file->path : "";
	size_t path_len = strlen(path);
	size_t content_len = strlen(file->content);
	size_t value_size = MALLOC_ADD3(path_len, 1, content_len);
	value_size = MALLOC_ADD(value_size, 1);

	char *value = p_malloc(pool, value_size);
	memcpy(value, path, path_len);
	value[path_len] = '\n';
	memcpy(value + path_len + 1, file->content, content_len);
	return value;
}

void settings_boollist_finish(ARRAY_TYPE(const_string) *array, bool stop)
{
	array_append_zero(array);
	if (stop)
		array_push_back(array, &set_array_stop);
#ifdef DEBUG_FAST
	array_push_back(array, &boollist_eol_sentry);
	array_pop_back(array);
#endif
	if (stop)
		array_pop_back(array);
	array_pop_back(array);
}

bool settings_boollist_is_stopped(const ARRAY_TYPE(const_string) *array)
{
	/* The first element after the visible array is NULL. If the next element
	   after the NULL is set_array_stop, then the boollist is stopped. */
	unsigned int count;
	const char *const *values = array_get(array, &count);
	i_assert(values[count] == NULL);
	return values[count + 1] == set_array_stop;
}

static int
settings_parse_boollist(struct setting_parser_context *ctx,
			ARRAY_TYPE(const_string) *array,
			const char *key, const char *value)
{
	const char *const *elem, *error;

	if (!array_is_created(array))
		p_array_init(array, ctx->set_pool, 5);
	else {
		/* If the array is stopped, then the boollist should not
		   be modified any further. */
		if (settings_boollist_is_stopped(array))
			return 0;
	}

	key = strrchr(key, SETTINGS_SEPARATOR);
	if (key == NULL) {
		/* replace the whole boollist */
		array_clear(array);
		if (settings_parse_boollist_string(value, ctx->set_pool,
						   array, &error) < 0) {
			settings_parser_set_error(ctx, error);
			return -1;
		}
		/* keep it NULL-terminated for each access */
		settings_boollist_finish(array, FALSE);
		return 0;
	}
	key = settings_section_unescape(key + 1);

	bool value_bool;
	if (get_bool(ctx, value, &value_bool) < 0)
		return -1;

	elem = array_lsearch(array, &key, i_strcmp_p);
	if (elem == NULL && value_bool) {
		/* add missing element */
		key = p_strdup(ctx->set_pool, key);
		array_push_back(array, &key);
	} else if (!value_bool) {
		/* remove unwanted element */
		if (elem != NULL) {
			key = *elem;
			array_delete(array, array_ptr_to_idx(array, elem), 1);
		} else {
			key = p_strdup(ctx->parser_pool, key);
		}
		/* remember the removal for settings_parse_list_has_key() */
		if (!array_is_created(&ctx->boollist_removals))
			p_array_init(&ctx->boollist_removals, ctx->parser_pool, 2);
		struct boollist_removal *removal =
			array_append_space(&ctx->boollist_removals);
		removal->array = array;
		removal->key_suffix = key;
	}
	/* keep it NULL-terminated for each access */
	settings_boollist_finish(array, FALSE);
	return 0;
}

static int
settings_parse(struct setting_parser_context *ctx,
	       const struct setting_define *def,
	       const char *key, const char *value, bool dup_value)
{
	void *ptr;
	const void *ptr2;
	const char *error;
	int ret;

	if (value == set_value_unknown) {
		/* setting value is unknown - preserve the exact pointer */
		dup_value = FALSE;
	}

	i_free(ctx->error);

	while (def->type == SET_ALIAS) {
		i_assert(def != ctx->info->defines);
		def--;
	}

	ptr = PTR_OFFSET(ctx->set_struct, def->offset);
	switch (def->type) {
	case SET_BOOL:
		if (get_bool(ctx, value, (bool *)ptr) < 0)
			return -1;
		break;
	case SET_UINTMAX:
		if (get_uintmax(ctx, value, (uintmax_t *)ptr) < 0)
			return -1;
		break;
	case SET_UINT:
		if (get_uint(ctx, value, (unsigned int *)ptr) < 0)
			return -1;
		break;
	case SET_UINT_OCT:
		if (get_octal(ctx, value, (unsigned int *)ptr) < 0)
			return -1;
		break;
	case SET_TIME:
		if (settings_value_is_unlimited(value)) {
			*(unsigned int *)ptr = SET_TIME_INFINITE;
			return 0;
		}
		if (str_parse_get_interval(value, (unsigned int *)ptr, &error) < 0) {
			settings_parser_set_error(ctx, error);
			return -1;
		}
		break;
	case SET_TIME_MSECS:
		if (settings_value_is_unlimited(value)) {
			*(unsigned int *)ptr = SET_TIME_MSECS_INFINITE;
			return 0;
		}
		if (str_parse_get_interval_msecs(value, (unsigned int *)ptr, &error) < 0) {
			settings_parser_set_error(ctx, error);
			return -1;
		}
		break;
	case SET_SIZE:
		if (settings_value_is_unlimited(value)) {
			*(uoff_t *)ptr = SET_SIZE_UNLIMITED;
			return 0;
		}
		if (str_parse_get_size(value, (uoff_t *)ptr, &error) < 0) {
			settings_parser_set_error(ctx, error);
			return -1;
		}
		break;
	case SET_IN_PORT:
		if (get_in_port_zero(ctx, value, (in_port_t *)ptr) < 0)
			return -1;
		break;
	case SET_STR:
	case SET_STR_NOVARS:
		if (HAS_ANY_BITS(def->flags, SET_FLAG_UNICODE_NFC) &&
		    value != set_value_unknown) {
			if (uni_utf8_to_nfc(value, strlen(value), &value) < 0) {
				settings_parser_set_error(ctx,
					"Value contains invalid Unicode code point");
				return -1;
			}
			dup_value = TRUE;
		}
		if (dup_value)
			value = p_strdup(ctx->set_pool, value);
		*((const char **)ptr) = value;
		break;
	case SET_FILE: {
		/* only expand first line, if there */
		const char *path = t_strcut(value, '\n');
		if (strstr(path, "%{") != NULL) {
			if (t_var_expand(value, NULL, &value, &error) < 0) {
				settings_parser_set_error(ctx, error);
				return -1;
			}
		}
		/* Read the file directly to get the content */
		if (get_file(ctx, dup_value, &value) < 0) {
			/* We may be running settings_check()s in doveconf at a
			   time when the file couldn't yet be opened. To avoid
			   unnecessary errors, set the value unknown. */
			*((const char **)ptr) = set_value_unknown;
			return -1;
		}
		*((const char **)ptr) = value;
		break;
	}
	case SET_ENUM:
		/* get the available values from default string */
		i_assert(ctx->info->defaults != NULL);
		ptr2 = CONST_PTR_OFFSET(ctx->info->defaults, def->offset);
		if (get_enum(ctx, value, (char **)ptr,
			     *(const char *const *)ptr2) < 0)
			return -1;
		break;
	case SET_STRLIST:
		T_BEGIN {
			ret = settings_parse_strlist(ctx, ptr, key, value,
						     &error);
			if (ret < 0)
				settings_parser_set_error(ctx, error);
		} T_END;
		if (ret < 0)
			return -1;
		break;
	case SET_BOOLLIST:
		T_BEGIN {
			ret = settings_parse_boollist(ctx, ptr, key, value);
		} T_END;
		if (ret < 0)
			return -1;
		break;
	case SET_FILTER_ARRAY: {
		/* Add filter names to the array. Userdb can add more simply
		   by giving e.g. "namespace+=newname" without it removing the
		   existing ones. */
		ARRAY_TYPE(const_string) *arr = ptr;
		const char *const *list =
			t_strsplit(value, SETTINGS_FILTER_ARRAY_SEPARATORS);
		unsigned int i, count = str_array_length(list);
		if (!array_is_created(arr))
			p_array_init(arr, ctx->set_pool, count);
		else {
			/* If the next element after the visible array is
			   set_array_stop, then the named list filter
			   should not be modified any further. */
			unsigned int old_count;
			const char *const *old_values =
				array_get(arr, &old_count);
			if (old_values[old_count] == set_array_stop)
				break;
		}
		unsigned int insert_pos = 0;
		for (i = 0; i < count; i++) {
			const char *value = p_strdup(ctx->set_pool,
				settings_section_unescape(list[i]));
			if (array_lsearch(arr, &value, i_strcmp_p) != NULL)
				continue; /* ignore duplicates */
			if ((ctx->flags & SETTINGS_PARSER_FLAG_INSERT_FILTERS) != 0)
				array_insert(arr, insert_pos++, &value, 1);
			else
				array_push_back(arr, &value);
		}
		/* Make sure the next element after the array is accessible for
		   the set_array_stop check. */
		array_append_zero(arr);
		array_pop_back(arr);
		break;
	}
	case SET_FILTER_NAME:
		settings_parser_set_error(ctx, t_strdup_printf(
			"Setting is a named filter, use '%s {'", key));
		return -1;
	case SET_ALIAS:
		i_unreached();
	}
	return 0;
}

static bool
settings_find_key(struct setting_parser_context *ctx, const char *key,
		  bool allow_filter_name, const struct setting_define **def_r)
{
	const struct setting_define *def;
	const char *end, *parent_key;

	/* try to find the exact key */
	def = setting_define_find(ctx->info, key);
	if (def != NULL && (def->type != SET_FILTER_NAME ||
			    allow_filter_name)) {
		*def_r = def;
		return TRUE;
	}

	/* try to find list/key prefix */
	end = strrchr(key, SETTINGS_SEPARATOR);
	if (end == NULL)
		return FALSE;

	parent_key = t_strdup_until(key, end);
	def = setting_define_find(ctx->info, parent_key);
	if (def != NULL && (def->type == SET_STRLIST ||
			    def->type == SET_BOOLLIST)) {
		*def_r = def;
		return TRUE;
	}
	return FALSE;
}

static int
settings_parse_keyvalue_real(struct setting_parser_context *ctx,
			     const char *key, const char *value, bool dup_value)
{
	const struct setting_define *def;

	if (!settings_find_key(ctx, key, FALSE, &def)) {
		settings_parser_set_error(ctx,
			t_strconcat("Unknown setting: ", key, NULL));
		return 0;
	}

	if (settings_parse(ctx, def, key, value, dup_value) < 0)
		return -1;
	return 1;
}

int settings_parse_keyvalue(struct setting_parser_context *ctx,
			    const char *key, const char *value)
{
	return settings_parse_keyvalue_real(ctx, key, value, TRUE);
}

int settings_parse_keyidx_value(struct setting_parser_context *ctx,
				unsigned int key_idx, const char *key,
				const char *value)
{
	return settings_parse(ctx, &ctx->info->defines[key_idx],
			      key, value, TRUE);
}

int settings_parse_keyvalue_nodup(struct setting_parser_context *ctx,
				  const char *key, const char *value)
{
	return settings_parse_keyvalue_real(ctx, key, value, FALSE);
}

int settings_parse_keyidx_value_nodup(struct setting_parser_context *ctx,
				      unsigned int key_idx, const char *key,
				      const char *value)
{
	return settings_parse(ctx, &ctx->info->defines[key_idx],
			      key, value, FALSE);
}

void settings_parse_array_stop(struct setting_parser_context *ctx,
			       unsigned int key_idx)
{
	i_assert(ctx->info->defines[key_idx].type == SET_FILTER_ARRAY ||
		 ctx->info->defines[key_idx].type == SET_BOOLLIST ||
		 ctx->info->defines[key_idx].type == SET_STRLIST);

	ARRAY_TYPE(const_string) *arr =
		PTR_OFFSET(ctx->set_struct, ctx->info->defines[key_idx].offset);
	if (!array_is_created(arr))
		p_array_init(arr, ctx->set_pool, 1);

	if (ctx->info->defines[key_idx].type == SET_BOOLLIST)
		settings_boollist_finish(arr, TRUE);
	else {
		/* Use the next element hidden after the array to keep
		   the stop-state */
		array_push_back(arr, &set_array_stop);
		array_pop_back(arr);
	}
}

static int boollist_removal_cmp(const struct boollist_removal *r1,
				const struct boollist_removal *r2)
{
	if (r1->array != r2->array)
		return 1;
	return strcmp(r1->key_suffix, r2->key_suffix);
}

bool settings_parse_list_has_key(struct setting_parser_context *ctx,
				 unsigned int key_idx,
				 const char *key_suffix)
{
	const struct setting_define *def = &ctx->info->defines[key_idx];
	unsigned int skip = UINT_MAX;

	switch (def->type) {
	case SET_STRLIST:
		skip = 2;
		break;
	case SET_BOOLLIST:
		skip = 1;
		if (!array_is_created(&ctx->boollist_removals))
			break;

		struct boollist_removal lookup = {
			.array = PTR_OFFSET(ctx->set_struct, def->offset),
			.key_suffix = key_suffix,
		};
		if (array_lsearch(&ctx->boollist_removals, &lookup,
				  boollist_removal_cmp) != NULL)
			return TRUE;
		break;
	default:
		i_unreached();
	}

	ARRAY_TYPE(const_string) *array =
		PTR_OFFSET(ctx->set_struct, def->offset);
	if (!array_is_created(array))
		return FALSE;

	unsigned int i, count;
	const char *const *items = array_get(array, &count);
	for (i = 0; i < count; i += skip) {
		if (strcmp(items[i], key_suffix) == 0)
			return TRUE;
	}
	return FALSE;
}

const void *
settings_parse_get_value(struct setting_parser_context *ctx,
			 const char **key, enum setting_type *type_r)
{
	const struct setting_define *def;

	if (!settings_find_key(ctx, *key, TRUE, &def))
		return NULL;

	while (def->type == SET_ALIAS) {
		i_assert(def != ctx->info->defines);
		def--;
		/* Replace the key with the unaliased key. We assume here that
		   lists don't have aliases, because the key replacement
		   would only need to replace the key prefix then. */
		i_assert(def->type != SET_STRLIST && def->type != SET_BOOLLIST);
		*key = def->key;
	}
	*type_r = def->type;
	return PTR_OFFSET(ctx->set_struct, def->offset);
}

bool settings_check(struct event *event, const struct setting_parser_info *info,
		    pool_t pool, void *set, const char **error_r)
{
	bool valid;

	if (info->check_func != NULL) {
		T_BEGIN {
			valid = info->check_func(set, pool, error_r);
		} T_END_PASS_STR_IF(!valid, error_r);
		if (!valid)
			return FALSE;
	}
	if (info->ext_check_func != NULL) {
		T_BEGIN {
			valid = info->ext_check_func(event, set, pool, error_r);
		} T_END_PASS_STR_IF(!valid, error_r);
		if (!valid)
			return FALSE;
	}
	return TRUE;
}

bool settings_parser_check(struct setting_parser_context *ctx, pool_t pool,
			   struct event *event, const char **error_r)
{
	return settings_check(event, ctx->info, pool,
			      ctx->set_struct, error_r);
}

unsigned int settings_hash(const struct setting_parser_info *info,
			   const void *set, const char *const *except_fields)
{
	unsigned int crc = 0;

	for (unsigned int i = 0; info->defines[i].key != NULL; i++) {
		if (except_fields != NULL &&
		    str_array_find(except_fields, info->defines[i].key))
			continue;

		const void *p = CONST_PTR_OFFSET(set, info->defines[i].offset);
		switch (info->defines[i].type) {
		case SET_BOOL: {
			const bool *b = p;
			crc = crc32_data_more(crc, b, sizeof(*b));
			break;
		}
		case SET_UINTMAX: {
			const uintmax_t *i = p;
			crc = crc32_data_more(crc, i, sizeof(*i));
			break;
		}
		case SET_UINT:
		case SET_UINT_OCT:
		case SET_TIME:
		case SET_TIME_MSECS: {
			const unsigned int *i = p;
			crc = crc32_data_more(crc, i, sizeof(*i));
			break;
		}
		case SET_SIZE: {
			const uoff_t *s = p;
			crc = crc32_data_more(crc, s, sizeof(*s));
			break;
		}
		case SET_IN_PORT: {
			const in_port_t *port = p;
			crc = crc32_data_more(crc, port, sizeof(*port));
			break;
		}
		case SET_STR:
		case SET_STR_NOVARS:
		case SET_ENUM: {
			const char *const *str = p;
			crc = crc32_str_more(crc, *str);
			break;
		}
		case SET_FILE: {
			const char *const *str = p;
			const char *lf = strchr(*str, '\n');
			if (lf == NULL)
				i_panic("Settings file value is missing LF");
			if (lf == *str) {
				/* no filename - need to hash the content */
				crc = crc32_str_more(crc, *str + 1);
			} else {
				/* hashing the filename is enough */
				crc = crc32_data_more(crc, *str, lf - *str);
			}
			break;
		}
		case SET_STRLIST:
		case SET_BOOLLIST:
		case SET_FILTER_ARRAY: {
			const ARRAY_TYPE(const_string) *list = p;
			if (array_is_created(list)) {
				const char *str;
				array_foreach_elem(list, str)
					crc = crc32_str_more(crc, str);
			}
			break;
		}
		case SET_ALIAS:
		case SET_FILTER_NAME:
			break;
		}
	}
	return crc;
}

bool settings_equal(const struct setting_parser_info *info,
		    const void *set1, const void *set2,
		    const char *const *except_fields)
{
	for (unsigned int i = 0; info->defines[i].key != NULL; i++) {
		if (except_fields != NULL &&
		    str_array_find(except_fields, info->defines[i].key))
			continue;

		const void *p1 = CONST_PTR_OFFSET(set1, info->defines[i].offset);
		const void *p2 = CONST_PTR_OFFSET(set2, info->defines[i].offset);
		switch (info->defines[i].type) {
		case SET_BOOL: {
			const bool *b1 = p1, *b2 = p2;
			if (*b1 != *b2)
				return FALSE;
			break;
		}
		case SET_UINTMAX: {
			const uintmax_t *i1 = p1, *i2 = p2;
			if (*i1 != *i2)
				return FALSE;
			break;
		}
		case SET_UINT:
		case SET_UINT_OCT:
		case SET_TIME:
		case SET_TIME_MSECS: {
			const unsigned int *i1 = p1, *i2 = p2;
			if (*i1 != *i2)
				return FALSE;
			break;
		}
		case SET_SIZE: {
			const uoff_t *s1 = p1, *s2 = p2;
			if (*s1 != *s2)
				return FALSE;
			break;
		}
		case SET_IN_PORT: {
			const in_port_t *port1 = p1, *port2 = p2;
			if (*port1 != *port2)
				return FALSE;
			break;
		}
		case SET_STR:
		case SET_STR_NOVARS:
		case SET_ENUM:
		case SET_FILE: {
			const char *const *str1 = p1, *const *str2 = p2;
			if (strcmp(*str1, *str2) != 0)
				return FALSE;
			break;
		}
		case SET_STRLIST:
		case SET_BOOLLIST:
		case SET_FILTER_ARRAY: {
			const ARRAY_TYPE(const_string) *list1 = p1, *list2 = p2;
			if (array_is_empty(list1)) {
				if (!array_is_empty(list2))
					return FALSE;
				break;
			}
			if (array_is_empty(list2))
				return FALSE;

			unsigned int i, count1, count2;
			const char *const *str1 = array_get(list1, &count1);
			const char *const *str2 = array_get(list2, &count2);
			if (count1 != count2)
				return FALSE;
			for (i = 0; i < count1; i++) {
				if (strcmp(str1[i], str2[i]) != 0)
					return FALSE;
			}
			break;
		}
		case SET_ALIAS:
		case SET_FILTER_NAME:
			break;
		}
	}
	return TRUE;
}

void *settings_defaults_dup(pool_t pool, const struct setting_parser_info *info)
{
	void *dup = p_malloc(pool, info->struct_size);
	memcpy(dup, info->defaults, info->struct_size);
	memcpy(PTR_OFFSET(dup, info->pool_offset1 - 1), &pool, sizeof(pool));
	return dup;
}

const char *settings_section_escape(const char *name)
{
#define CHAR_NEED_ESCAPE(c) \
	((c) == '=' || (c) == SETTINGS_SEPARATOR || (c) == '\\' || (c) == ' ' || (c) == ',')
	string_t *str;
	unsigned int i;

	for (i = 0; name[i] != '\0'; i++) {
		if (CHAR_NEED_ESCAPE(name[i]))
			break;
	}
	if (name[i] == '\0') {
		if (i == 0)
			return "\\.";
		return name;
	}

	str = t_str_new(i + strlen(name+i) + 8);
	str_append_data(str, name, i);
	for (; name[i] != '\0'; i++) {
		switch (name[i]) {
		case '=':
			str_append(str, "\\e");
			break;
		case SETTINGS_SEPARATOR:
			str_append(str, "\\s");
			break;
		case '\\':
			str_append(str, "\\\\");
			break;
		case ' ':
			str_append(str, "\\_");
			break;
		case ',':
			str_append(str, "\\+");
			break;
		default:
			str_append_c(str, name[i]);
			break;
		}
	}
	return str_c(str);
}

const char *settings_section_unescape(const char *name)
{
	const char *p = strchr(name, '\\');
	if (p == NULL)
		return name;

	string_t *str = t_str_new(strlen(name));
	str_append_data(str, name, p - name);
	while (p[1] != '\0') {
		switch (p[1]) {
		case 'e':
			str_append_c(str, '=');
			break;
		case 's':
			str_append_c(str, SETTINGS_SEPARATOR);
			break;
		case '\\':
			str_append_c(str, '\\');
			break;
		case '_':
			str_append_c(str, ' ');
			break;
		case '+':
			str_append_c(str, ',');
			break;
		case '.':
			/* empty string */
			break;
		default:
			/* not supposed to happen */
			str_append_c(str, '\\');
			str_append_c(str, p[1]);
			break;
		}
		name = p+2;
		p = strchr(name, '\\');
		if (p == NULL) {
			str_append(str, name);
			return str_c(str);
		}
		str_append_data(str, name, p - name);
	}
	/* ends with '\\' - not supposed to happen */
	str_append_c(str, '\\');
	return str_c(str);
}

static enum settings_binary config_binary = SETTINGS_BINARY_OTHER;

void settings_set_config_binary(enum settings_binary binary)
{
	config_binary = binary;
}

enum settings_binary settings_get_config_binary(void)
{
	return config_binary;
}

