view src/config/config-parser.c @ 10704:c26002b81f57 HEAD

config: $setting as value returns the setting's current value.
author Timo Sirainen <tss@iki.fi>
date Sat, 13 Feb 2010 08:07:31 +0200
parents 615eef3139c2
children bf4822f0846b
line wrap: on
line source

/* Copyright (c) 2005-2010 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "str.h"
#include "hash.h"
#include "strescape.h"
#include "istream.h"
#include "module-dir.h"
#include "settings-parser.h"
#include "service-settings.h"
#include "all-settings.h"
#include "config-filter.h"
#include "config-request.h"
#include "config-parser.h"

#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#ifdef HAVE_GLOB_H
#  include <glob.h>
#endif

#ifndef GLOB_BRACE
#  define GLOB_BRACE 0
#endif

#define IS_WHITE(c) ((c) == ' ' || (c) == '\t')

struct config_section_stack {
	struct config_section_stack *prev;

	struct config_filter filter;
	/* root=NULL-terminated list of parsers */
	struct config_module_parser *parsers;
	unsigned int pathlen;
};

struct input_stack {
	struct input_stack *prev;

	struct istream *input;
	const char *path;
	unsigned int linenum;
};

struct parser_context {
	pool_t pool;
	const char *path;

	ARRAY_DEFINE(all_parsers, struct config_filter_parser *);
	struct config_module_parser *root_parsers;
	struct config_section_stack *cur_section;
	struct input_stack *cur_input;

	struct config_filter_context *filter;
	unsigned int expand_values:1;
};

static const enum settings_parser_flags settings_parser_flags =
	SETTINGS_PARSER_FLAG_IGNORE_UNKNOWN_KEYS |
	SETTINGS_PARSER_FLAG_TRACK_CHANGES;

struct config_module_parser *config_module_parsers;
struct config_filter_context *config_filter;

static const char *info_type_name_find(const struct setting_parser_info *info)
{
	unsigned int i;

	for (i = 0; info->defines[i].key != NULL; i++) {
		if (info->defines[i].offset == info->type_offset)
			return info->defines[i].key;
	}
	i_panic("setting parser: Invalid type_offset value");
	return NULL;
}

static void config_add_type(struct setting_parser_context *parser,
			    const char *line, const char *section_name)
{
	const struct setting_parser_info *info;
	const char *p;
	string_t *str;
	int ret;

	info = settings_parse_get_prev_info(parser);
	if (info->type_offset == (size_t)-1)
		return;

	str = t_str_new(256);
	p = strchr(line, '=');
	str_append_n(str, line, p-line);
	str_append_c(str, SETTINGS_SEPARATOR);
	str_append(str, p+1);
	str_append_c(str, SETTINGS_SEPARATOR);
	str_append(str, info_type_name_find(info));
	str_append_c(str, '=');
	str_append(str, section_name);

	ret = settings_parse_line(parser, str_c(str));
	i_assert(ret > 0);
}

static int
config_apply_line(struct parser_context *ctx, const char *key,
		  const char *line, const char *section_name,
		  const char **error_r)
{
	struct config_module_parser *l;
	bool found = FALSE;
	int ret;

	for (l = ctx->cur_section->parsers; l->root != NULL; l++) {
		ret = settings_parse_line(l->parser, line);
		if (ret > 0) {
			found = TRUE;
			if (section_name != NULL)
				config_add_type(l->parser, line, section_name);
		} else if (ret < 0) {
			*error_r = settings_parser_get_error(l->parser);
			return -1;
		}
	}
	if (!found) {
		*error_r = t_strconcat("Unknown setting: ", key, NULL);
		return -1;
	}
	*error_r = NULL;
	return 0;
}

static const char *
fix_relative_path(const char *path, struct input_stack *input)
{
	const char *p;

	if (*path == '/')
		return path;

	p = strrchr(input->path, '/');
	if (p == NULL)
		return path;

	return t_strconcat(t_strdup_until(input->path, p+1), path, NULL);
}

static struct config_module_parser *
config_module_parsers_init(pool_t pool)
{
	struct config_module_parser *dest;
	unsigned int i, count;

	for (count = 0; all_roots[count] != NULL; count++) ;

	dest = p_new(pool, struct config_module_parser, count + 1);
	for (i = 0; i < count; i++) {
		dest[i].root = all_roots[i];
		dest[i].parser = settings_parser_init(pool, all_roots[i],
						      settings_parser_flags);
	}
	return dest;
}

static void
config_add_new_parser(struct parser_context *ctx)
{
	struct config_section_stack *cur_section = ctx->cur_section;
	struct config_filter_parser *parser;

	parser = p_new(ctx->pool, struct config_filter_parser, 1);
	parser->filter = cur_section->filter;
	if (ctx->cur_input->linenum == 0) {
		parser->file_and_line =
			p_strdup(ctx->pool, ctx->cur_input->path);
	} else {
		parser->file_and_line =
			p_strdup_printf(ctx->pool, "%s:%d",
					ctx->cur_input->path,
					ctx->cur_input->linenum);
	}
	parser->parsers = cur_section->prev == NULL ? ctx->root_parsers :
		config_module_parsers_init(ctx->pool);
	array_append(&ctx->all_parsers, &parser, 1);

	cur_section->parsers = parser->parsers;
}

static struct config_section_stack *
config_add_new_section(struct parser_context *ctx)
{
	struct config_section_stack *section;

	section = p_new(ctx->pool, struct config_section_stack, 1);
	section->prev = ctx->cur_section;
	section->filter = ctx->cur_section->filter;
	section->parsers = ctx->cur_section->parsers;
	return section;
}

static struct config_filter_parser *
config_filter_parser_find(struct parser_context *ctx,
			  const struct config_filter *filter)
{
	struct config_filter_parser *const *parsers;

	array_foreach(&ctx->all_parsers, parsers) {
		struct config_filter_parser *parser = *parsers;

		if (config_filters_equal(&parser->filter, filter))
			return parser;
	}
	return NULL;
}

static int
config_parse_net(struct parser_context *ctx, const char *value,
		 const char **host_r, struct ip_addr *ip_r,
		 unsigned int *bits_r, const char **error_r)
{
	struct ip_addr *ips;
	const char *p;
	unsigned int ip_count;
	int ret;

	if (net_parse_range(value, ip_r, bits_r) == 0)
		return 0;

	p = strchr(value, '/');
	if (p != NULL) {
		value = t_strdup_until(value, p);
		p++;
	}

	ret = net_gethostbyname(value, &ips, &ip_count);
	if (ret != 0) {
		*error_r = t_strdup_printf("gethostbyname(%s) failed: %s",
					   value, net_gethosterror(ret));
		return -1;
	}
	*host_r = p_strdup(ctx->pool, value);
	*ip_r = ips[0];
	if (p != NULL && is_numeric(p, '\0'))
		*bits_r = atoi(p);
	else
		*bits_r = IPADDR_IS_V4(&ips[0]) ? 32 : 128;
	return 0;
}

static bool
config_filter_add_new_filter(struct parser_context *ctx,
			     const char *key, const char *value,
			     const char **error_r)
{
	struct config_filter *filter = &ctx->cur_section->filter;
	struct config_filter *parent = &ctx->cur_section->prev->filter;
	struct config_filter_parser *parser;

	if (strcmp(key, "protocol") == 0) {
		if (parent->service != NULL)
			*error_r = "protocol must not be under protocol";
		else
			filter->service = p_strdup(ctx->pool, value);
	} else if (strcmp(key, "local") == 0) {
		if (parent->remote_bits > 0)
			*error_r = "local must not be under remote";
		else if (parent->service != NULL)
			*error_r = "local must not be under protocol";
		else if (config_parse_net(ctx, value, &filter->local_host,
					  &filter->local_net,
					  &filter->local_bits, error_r) < 0)
			;
		else if (parent->local_bits > filter->local_bits ||
			 (parent->local_bits > 0 &&
			  !net_is_in_network(&filter->local_net,
					     &parent->local_net,
					     parent->local_bits)))
			*error_r = "local not a subset of parent local";
	} else if (strcmp(key, "remote") == 0) {
		if (parent->service != NULL)
			*error_r = "remote must not be under protocol";
		else if (config_parse_net(ctx, value, &filter->remote_host,
					  &filter->remote_net,
					  &filter->remote_bits, error_r) < 0)
			;
		else if (parent->remote_bits > filter->remote_bits ||
			 (parent->remote_bits > 0 &&
			  !net_is_in_network(&filter->remote_net,
					     &parent->remote_net,
					     parent->remote_bits)))
			*error_r = "remote not a subset of parent remote";
	} else {
		return FALSE;
	}

	parser = config_filter_parser_find(ctx, filter);
	if (parser != NULL)
		ctx->cur_section->parsers = parser->parsers;
	else
		config_add_new_parser(ctx);
	return TRUE;
}

static int
config_filter_parser_check(struct parser_context *ctx,
			   const struct config_module_parser *p,
			   const char **error_r)
{
	for (; p->root != NULL; p++) {
		settings_parse_var_skip(p->parser);
		if (!settings_parser_check(p->parser, ctx->pool, error_r))
			return -1;
	}
	return 0;
}

static int
config_all_parsers_check(struct parser_context *ctx,
			 struct config_filter_context *new_filter,
			 const char **error_r)
{
	struct config_filter_parser *const *parsers;
	struct config_module_parser *tmp_parsers;
	unsigned int i, count;
	pool_t tmp_pool;
	int ret = 0;

	tmp_pool = pool_alloconly_create("config parsers check", 1024*32);
	parsers = array_get(&ctx->all_parsers, &count);
	i_assert(count > 0 && parsers[count-1] == NULL);
	count--;
	for (i = 0; i < count && ret == 0; i++) {
		if (config_filter_parsers_get(new_filter, tmp_pool,
					      &parsers[i]->filter,
					      &tmp_parsers, error_r) < 0) {
			ret = -1;
			break;
		}

		ret = config_filter_parser_check(ctx, tmp_parsers, error_r);
		config_filter_parsers_free(tmp_parsers);
		p_clear(tmp_pool);
	}
	pool_unref(&tmp_pool);
	return ret;
}

static int
str_append_file(string_t *str, const char *key, const char *path,
		const char **error_r)
{
	unsigned char buf[1024];
	int fd;
	ssize_t ret;

	fd = open(path, O_RDONLY);
	if (fd == -1) {
		*error_r = t_strdup_printf("%s: Can't open file %s: %m",
					   key, path);
		return -1;
	}
	while ((ret = read(fd, buf, sizeof(buf))) > 0)
		str_append_n(str, buf, ret);
	if (ret < 0) {
		*error_r = t_strdup_printf("%s: read(%s) failed: %m",
					   key, path);
	}
	(void)close(fd);
	return ret < 0 ? -1 : 0;
}

static int settings_add_include(struct parser_context *ctx, const char *path,
				bool ignore_errors, const char **error_r)
{
	struct input_stack *tmp, *new_input;
	int fd;

	for (tmp = ctx->cur_input; tmp != NULL; tmp = tmp->prev) {
		if (strcmp(tmp->path, path) == 0)
			break;
	}
	if (tmp != NULL) {
		*error_r = t_strdup_printf("Recursive include file: %s", path);
		return -1;
	}

	if ((fd = open(path, O_RDONLY)) == -1) {
		if (ignore_errors)
			return 0;

		*error_r = t_strdup_printf("Couldn't open include file %s: %m",
					   path);
		return -1;
	}

	new_input = t_new(struct input_stack, 1);
	new_input->prev = ctx->cur_input;
	new_input->path = t_strdup(path);
	new_input->input = i_stream_create_fd(fd, (size_t)-1, TRUE);
	i_stream_set_return_partial_line(new_input->input, TRUE);
	ctx->cur_input = new_input;
	return 0;
}

static int
settings_include(struct parser_context *ctx, const char *pattern,
		 bool ignore_errors, const char **error_r)
{
#ifdef HAVE_GLOB
	glob_t globbers;
	unsigned int i;

	switch (glob(pattern, GLOB_BRACE, NULL, &globbers)) {
	case 0:
		break;
	case GLOB_NOSPACE:
		*error_r = "glob() failed: Not enough memory";
		return -1;
	case GLOB_ABORTED:
		*error_r = "glob() failed: Read error";
		return -1;
	case GLOB_NOMATCH:
		if (ignore_errors)
			return 0;
		*error_r = "No matches";
		return -1;
	default:
		*error_r = "glob() failed: Unknown error";
		return -1;
	}

	/* iterate throuth the different files matching the globbing */
	for (i = 0; i < globbers.gl_pathc; i++) {
		if (settings_add_include(ctx, globbers.gl_pathv[i],
					 ignore_errors, error_r) < 0)
			return -1;
	}
	globfree(&globbers);
	return 0;
#else
	return settings_add_include(ctx, pattern, ignore_errors, error_r);
#endif
}

enum config_line_type {
	CONFIG_LINE_TYPE_SKIP,
	CONFIG_LINE_TYPE_ERROR,
	CONFIG_LINE_TYPE_KEYVALUE,
	CONFIG_LINE_TYPE_KEYFILE,
	CONFIG_LINE_TYPE_KEYVARIABLE,
	CONFIG_LINE_TYPE_SECTION_BEGIN,
	CONFIG_LINE_TYPE_SECTION_END,
	CONFIG_LINE_TYPE_INCLUDE,
	CONFIG_LINE_TYPE_INCLUDE_TRY
};

static enum config_line_type
config_parse_line(struct parser_context *ctx, char *line, string_t *full_line,
		  const char **key_r, const char **value_r)
{
	const char *key;
	unsigned int len;
	char *p;

	*key_r = NULL;
	*value_r = NULL;

	/* @UNSAFE: line is modified */

	/* skip whitespace */
	while (IS_WHITE(*line))
		line++;

	/* ignore comments or empty lines */
	if (*line == '#' || *line == '\0')
		return CONFIG_LINE_TYPE_SKIP;

	/* strip away comments. pretty kludgy way really.. */
	for (p = line; *p != '\0'; p++) {
		if (*p == '\'' || *p == '"') {
			char quote = *p;
			for (p++; *p != quote && *p != '\0'; p++) {
				if (*p == '\\' && p[1] != '\0')
					p++;
			}
			if (*p == '\0')
				break;
		} else if (*p == '#') {
			if (!IS_WHITE(p[-1])) {
				i_warning("Configuration file %s line %u: "
					  "Ambiguous '#' character in line, treating it as comment. "
					  "Add a space before it to remove this warning.",
					  ctx->cur_input->path,
					  ctx->cur_input->linenum);
			}
			*p = '\0';
			break;
		}
	}

	/* remove whitespace from end of line */
	len = strlen(line);
	while (IS_WHITE(line[len-1]))
		len--;
	line[len] = '\0';

	if (len > 0 && line[len-1] == '\\') {
		/* continues in next line */
		len--;
		while (IS_WHITE(line[len-1]))
			len--;
		str_append_n(full_line, line, len);
		str_append_c(full_line, ' ');
		return CONFIG_LINE_TYPE_SKIP;
	}
	if (str_len(full_line) > 0) {
		str_append(full_line, line);
		line = str_c_modifiable(full_line);
	}

	/* a) key = value
	   b) section_type [section_name] {
	   c) } */
	key = line;
	while (!IS_WHITE(*line) && *line != '\0' && *line != '=')
		line++;
	if (IS_WHITE(*line)) {
		*line++ = '\0';
		while (IS_WHITE(*line)) line++;
	}
	*key_r = key;
	*value_r = line;

	if (strcmp(key, "!include") == 0)
		return CONFIG_LINE_TYPE_INCLUDE;
	if (strcmp(key, "!include_try") == 0)
		return CONFIG_LINE_TYPE_INCLUDE_TRY;

	if (*line == '=') {
		/* a) */
		*line++ = '\0';
		while (IS_WHITE(*line)) line++;

		if (*line == '<') {
			*value_r = line + 1;
			return CONFIG_LINE_TYPE_KEYFILE;
		}
		if (*line == '$') {
			*value_r = line + 1;
			return CONFIG_LINE_TYPE_KEYVARIABLE;
		}

		len = strlen(line);
		if (len > 0 &&
		    ((*line == '"' && line[len-1] == '"') ||
		     (*line == '\'' && line[len-1] == '\''))) {
			line[len-1] = '\0';
			line = str_unescape(line+1);
		}
		*value_r = line;
		return CONFIG_LINE_TYPE_KEYVALUE;
	}

	if (strcmp(key, "}") == 0 && *line == '\0')
		return CONFIG_LINE_TYPE_SECTION_END;

	/* b) + errors */
	line[-1] = '\0';

	if (*line == '{')
		*value_r = "";
	else {
		/* get section name */
		*value_r = line;
		while (!IS_WHITE(*line) && *line != '\0')
			line++;

		if (*line != '\0') {
			*line++ = '\0';
			while (IS_WHITE(*line))
				line++;
		}
		if (*line != '{') {
			*value_r = "Expecting '='";
			return CONFIG_LINE_TYPE_ERROR;
		}
		if (line[1] != '\0') {
			*value_r = "Garbage after '{'";
			return CONFIG_LINE_TYPE_ERROR;
		}
	}
	return CONFIG_LINE_TYPE_SECTION_BEGIN;
}

static int config_parse_finish(struct parser_context *ctx, const char **error_r)
{
	struct config_filter_context *new_filter;
	const char *error;
	int ret;

	new_filter = config_filter_init(ctx->pool);
	(void)array_append_space(&ctx->all_parsers);
	config_filter_add_all(new_filter, array_idx(&ctx->all_parsers, 0));

	if ((ret = config_all_parsers_check(ctx, new_filter, &error)) < 0) {
		*error_r = t_strdup_printf("Error in configuration file %s: %s",
					   ctx->path, error);
	}

	if (config_filter != NULL)
		config_filter_deinit(&config_filter);
	config_module_parsers = ctx->root_parsers;
	config_filter = new_filter;
	return ret;
}

static const void *
config_get_value(struct parser_context *ctx, const char *key,
		 enum setting_type *type_r)
{
	struct config_module_parser *l;
	const void *value;

	for (l = ctx->cur_section->parsers; l->root != NULL; l++) {
		value = settings_parse_get_value(l->parser, key, type_r);
		if (value != NULL)
			return value;
	}
	return NULL;
}

static int config_write_value(struct parser_context *ctx,
			      string_t *str, enum config_line_type type,
			      const char *key, const char *value,
			      const char **errormsg_r)
{
	const void *var_value;
	enum setting_type var_type;
	bool dump;

	switch (type) {
	case CONFIG_LINE_TYPE_KEYVALUE:
		str_append(str, value);
		break;
	case CONFIG_LINE_TYPE_KEYFILE:
		if (!ctx->expand_values) {
			str_append_c(str, '<');
			str_append(str, value);
		} else {
			if (str_append_file(str, key, value, errormsg_r) < 0) {
				/* file reading failed */
				return -1;
			}
		}
		break;
	case CONFIG_LINE_TYPE_KEYVARIABLE:
		if (!ctx->expand_values) {
			str_append_c(str, '$');
			str_append(str, value);
		} else {
			var_value = config_get_value(ctx, value, &var_type);
			if (var_value == NULL) {
				*errormsg_r = t_strconcat("Unknown variable: $",
							  value, NULL);
				return -1;
			}
			if (!config_export_type(str, var_value, NULL,
						var_type, TRUE, &dump)) {
				*errormsg_r = t_strconcat("Invalid variable: $",
							  value, NULL);
				return -1;
			}
		}
		break;
	default:
		i_unreached();
	}
	return 0;
}

int config_parse_file(const char *path, bool expand_values,
		      const char **error_r)
{
	struct input_stack root;
	struct parser_context ctx;
	unsigned int pathlen = 0;
	unsigned int i, count, counter = 0;
	const char *errormsg, *key, *value, *section_name;
	string_t *str, *full_line;
	enum config_line_type type;
	char *line;
	int fd, ret = 0;

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		*error_r = t_strdup_printf("open(%s) failed: %m", path);
		return 0;
	}

	memset(&ctx, 0, sizeof(ctx));
	ctx.pool = pool_alloconly_create("config file parser", 1024*64);
	ctx.path = path;

	for (count = 0; all_roots[count] != NULL; count++) ;
	ctx.root_parsers =
		p_new(ctx.pool, struct config_module_parser, count+1);
	for (i = 0; i < count; i++) {
		ctx.root_parsers[i].root = all_roots[i];
		ctx.root_parsers[i].parser =
			settings_parser_init(ctx.pool, all_roots[i],
					     settings_parser_flags);
	}

	memset(&root, 0, sizeof(root));
	root.path = path;
	ctx.cur_input = &root;
	ctx.expand_values = expand_values;

	p_array_init(&ctx.all_parsers, ctx.pool, 128);
	ctx.cur_section = p_new(ctx.pool, struct config_section_stack, 1);
	config_add_new_parser(&ctx);

	str = t_str_new(256);
	full_line = t_str_new(512);
	errormsg = NULL;
	ctx.cur_input->input = i_stream_create_fd(fd, (size_t)-1, TRUE);
	i_stream_set_return_partial_line(ctx.cur_input->input, TRUE);
prevfile:
	while ((line = i_stream_read_next_line(ctx.cur_input->input)) != NULL) {
		ctx.cur_input->linenum++;
		type = config_parse_line(&ctx, line, full_line,
					 &key, &value);
		switch (type) {
		case CONFIG_LINE_TYPE_SKIP:
			break;
		case CONFIG_LINE_TYPE_ERROR:
			errormsg = value;
			break;
		case CONFIG_LINE_TYPE_KEYVALUE:
		case CONFIG_LINE_TYPE_KEYFILE:
		case CONFIG_LINE_TYPE_KEYVARIABLE:
			str_truncate(str, pathlen);
			str_append(str, key);
			str_append_c(str, '=');

			if (config_write_value(&ctx, str, type, key, value, &errormsg) < 0)
				break;
			(void)config_apply_line(&ctx, key, str_c(str), NULL, &errormsg);
			break;
		case CONFIG_LINE_TYPE_SECTION_BEGIN:
			ctx.cur_section = config_add_new_section(&ctx);
			ctx.cur_section->pathlen = pathlen;

			if (config_filter_add_new_filter(&ctx, key, value,
							 &errormsg)) {
				/* new filter */
				break;
			}

			/* new config section */
			if (*value == '\0') {
				/* no section name, use a counter */
				section_name = dec2str(counter++);
			} else {
				section_name = settings_section_escape(value);
			}
			str_truncate(str, pathlen);
			str_append(str, key);
			pathlen = str_len(str);

			str_append_c(str, '=');
			str_append(str, section_name);

			if (config_apply_line(&ctx, key, str_c(str), value, &errormsg) < 0)
				break;

			str_truncate(str, pathlen);
			str_append_c(str, SETTINGS_SEPARATOR);
			str_append(str, section_name);
			str_append_c(str, SETTINGS_SEPARATOR);
			pathlen = str_len(str);
			break;
		case CONFIG_LINE_TYPE_SECTION_END:
			if (ctx.cur_section->prev == NULL)
				errormsg = "Unexpected '}'";
			else {
				pathlen = ctx.cur_section->pathlen;
				ctx.cur_section = ctx.cur_section->prev;
			}
			break;
		case CONFIG_LINE_TYPE_INCLUDE:
		case CONFIG_LINE_TYPE_INCLUDE_TRY:
			(void)settings_include(&ctx, fix_relative_path(value, ctx.cur_input),
					       type == CONFIG_LINE_TYPE_INCLUDE_TRY,
					       &errormsg);
			break;
		}

		if (errormsg != NULL) {
			*error_r = t_strdup_printf(
				"Error in configuration file %s line %d: %s",
				ctx.cur_input->path, ctx.cur_input->linenum,
				errormsg);
			ret = -2;
			break;
		}
		str_truncate(full_line, 0);
	}

	i_stream_destroy(&ctx.cur_input->input);
	ctx.cur_input = ctx.cur_input->prev;
	if (line == NULL && ctx.cur_input != NULL)
		goto prevfile;

	if (ret == 0)
		ret = config_parse_finish(&ctx, error_r);
	return ret < 0 ? ret : 1;
}

void config_parse_load_modules(void)
{
	struct module_dir_load_settings mod_set;
	struct module *modules, *m;
	const struct setting_parser_info **roots;
	ARRAY_DEFINE(new_roots, const struct setting_parser_info *);
	ARRAY_TYPE(service_settings) new_services;
	struct service_settings *const *services, *service_set;
	unsigned int i, count;

	memset(&mod_set, 0, sizeof(mod_set));
	modules = module_dir_load(CONFIG_MODULE_DIR, NULL, &mod_set);
	module_dir_init(modules);

	i_array_init(&new_roots, 64);
	i_array_init(&new_services, 64);
	for (m = modules; m != NULL; m = m->next) {
		roots = module_get_symbol_quiet(m,
			t_strdup_printf("%s_set_roots", m->name));
		if (roots != NULL) {
			for (i = 0; roots[i] != NULL; i++)
				array_append(&new_roots, &roots[i], 1);
		}

		service_set = module_get_symbol_quiet(m,
			t_strdup_printf("%s_service_settings", m->name));
		if (service_set != NULL)
			array_append(&new_services, &service_set, 1);
	}
	if (array_count(&new_roots) > 0) {
		/* modules added new settings. add the defaults and start
		   using the new list. */
		for (i = 0; all_roots[i] != NULL; i++)
			array_append(&new_roots, &all_roots[i], 1);
		(void)array_append_space(&new_roots);
		all_roots = array_idx(&new_roots, 0);
	}
	if (array_count(&new_services) > 0) {
		/* module added new services. update the defaults. */
		services = array_get(default_services, &count);
		for (i = 0; i < count; i++)
			array_append(&new_services, &services[i], 1);
		*default_services = new_services;
	}
}