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

#include "lib.h"
#include "array.h"
#include "str.h"
#include "sort.h"
#include "module-dir.h"

#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <dlfcn.h>

#ifndef RTLD_GLOBAL
#  define RTLD_GLOBAL 0
#endif

#ifndef RTLD_NOW
#  define RTLD_NOW 0
#endif

static const char *module_name_drop_suffix(const char *name);

void *module_get_symbol_quiet(struct module *module, const char *symbol)
{
	/* clear out old errors */
	(void)dlerror();

	return dlsym(module->handle, symbol);
}

void *module_get_symbol(struct module *module, const char *symbol)
{
	const char *error;
	void *ret;

	ret = module_get_symbol_quiet(module, symbol);
	if (ret == NULL) {
		error = dlerror();
		if (error != NULL) {
			i_error("module %s: dlsym(%s) failed: %s",
				module->path, symbol, error);
			ret = NULL;
		}
	}
	return ret;
}

static void *get_symbol(struct module *module, const char *symbol, bool quiet)
{
	if (quiet)
		return module_get_symbol_quiet(module, symbol);

	return module_get_symbol(module, symbol);
}

static void module_free(struct module *module)
{
	if (module->deinit != NULL && module->initialized)
		module->deinit();
	/* dlclose()ing removes all symbols from valgrind's visibility.
	   if GDB environment is set, don't actually unload the module
	   (the GDB environment is used elsewhere too) */
	if (getenv("GDB") == NULL) {
		if (dlclose(module->handle) != 0)
			i_error("dlclose(%s) failed: %m", module->path);
	}
	i_free(module->path);
	i_free(module->name);
	i_free(module);
}

static bool
module_check_wrong_binary_dependency(const struct module_dir_load_settings *set,
				     struct module *module, const char **error_r)
{
	const char *symbol_name, *binary_dep, *const *names;
	string_t *errstr;

	if (set->binary_name == NULL)
		return TRUE;

	symbol_name = t_strconcat(module->name, "_binary_dependency", NULL);
	binary_dep = dlsym(module->handle, symbol_name);
	if (binary_dep == NULL)
		return TRUE;

	names = t_strsplit(binary_dep, " ");
	if (str_array_find(names, set->binary_name))
		return TRUE;

	errstr = t_str_new(128);
	str_printfa(errstr, "Can't load plugin %s: "
		    "Plugin is intended to be used only by ", module->name);
	if (names[1] == NULL)
		str_printfa(errstr, "%s binary", binary_dep);
	else
		str_printfa(errstr, "binaries: %s", binary_dep);
	str_printfa(errstr, " (we're %s)", set->binary_name);
	*error_r = str_c(errstr);
	return FALSE;
}

static bool
module_check_missing_plugin_dependencies(const struct module_dir_load_settings *set,
					 struct module *module,
					 struct module *all_modules,
					 const char **error_r)
{
	const char **deps;
	struct module *m;
	string_t *errmsg;
	size_t len;

	deps = dlsym(module->handle,
		     t_strconcat(module->name, "_dependencies", NULL));
	if (deps == NULL)
		return TRUE;

	for (; *deps != NULL; deps++) {
		len = strlen(*deps);
		for (m = all_modules; m != NULL; m = m->next) {
			if (strncmp(m->name, *deps, len) == 0 &&
			    (m->name[len] == '\0' ||
			     strcmp(m->name+len, "_plugin") == 0))
				break;
		}
		if (m == NULL) {
			errmsg = t_str_new(128);
			str_printfa(errmsg, "Plugin %s must be loaded also",
				    *deps);
			if (set->setting_name != NULL) {
				str_printfa(errmsg,
					    " (you must set: %s=$%s %s)",
					    set->setting_name,
					    set->setting_name, *deps);
			}
			*error_r = str_c(errmsg);
			return FALSE;
		}
	}
	return TRUE;
}

static void *quiet_dlopen(const char *path, int flags)
{
#ifndef __OpenBSD__
	return dlopen(path, flags);
#else
	void *handle;
	int fd;

	/* OpenBSD likes to print all "undefined symbol" errors to stderr.
	   Hide them by sending them to /dev/null. */
	fd = dup(STDERR_FILENO);
	if (fd == -1)
		i_fatal("dup() failed: %m");
	if (dup2(dev_null_fd, STDERR_FILENO) < 0)
		i_fatal("dup2() failed: %m");
	handle = dlopen(path, flags);
	if (dup2(fd, STDERR_FILENO) < 0)
		i_fatal("dup2() failed: %m");
	if (close(fd) < 0)
		i_error("close() failed: %m");
	return handle;
#endif
}

static bool versions_equal(const char *str1, const char *str2)
{
	while (*str1 == *str2) {
		if (*str1 == '\0' || *str1 == '(')
			return TRUE;
		str1++;
		str2++;
	}
	return FALSE;
}

static int
module_load(const char *path, const char *name,
	    const struct module_dir_load_settings *set,
	    struct module *all_modules,
	    struct module **module_r, const char **error_r)
{
	void *handle;
	struct module *module;
	const char *const *module_version;
	void (*preinit)(void);

	*module_r = NULL;
	*error_r = NULL;

	if (set->ignore_dlopen_errors) {
		handle = quiet_dlopen(path, RTLD_GLOBAL | RTLD_NOW);
		if (handle == NULL) {
			if (set->debug) {
				i_debug("Skipping module %s, "
					"because dlopen() failed: %s "
					"(this is usually intentional, "
					"so just ignore this message)",
					name, dlerror());
			}
			return 0;
		}
	} else {
		handle = dlopen(path, RTLD_GLOBAL | RTLD_NOW);
		if (handle == NULL) {
			*error_r = t_strdup_printf("dlopen() failed: %s",
						   dlerror());
#ifdef RTLD_LAZY
			/* try to give a better error message by lazily loading
			   the plugin and checking its dependencies */
			handle = dlopen(path, RTLD_LAZY);
			if (handle == NULL)
				return -1;
#else
			return -1;
#endif
		}
	}

	module = i_new(struct module, 1);
	module->path = i_strdup(path);
	module->name = i_strdup(name);
	module->handle = handle;

	module_version = set->abi_version == NULL ? NULL :
		get_symbol(module, t_strconcat(name, "_version", NULL), TRUE);
	if (module_version != NULL &&
	    !versions_equal(*module_version, set->abi_version)) {
		*error_r = t_strdup_printf(
			"Module is for different ABI version %s (we have %s)",
			*module_version, set->abi_version);
		module_free(module);
		return -1;
	}

	/* get our init func */
	module->init = (void (*)(struct module *))
		get_symbol(module, t_strconcat(name, "_init", NULL),
			   !set->require_init_funcs);
	module->deinit = (void (*)(void))
		get_symbol(module, t_strconcat(name, "_deinit", NULL),
			   !set->require_init_funcs);
	preinit = (void (*)(void))
		get_symbol(module, t_strconcat(name, "_preinit", NULL),
			   TRUE);
	if (preinit != NULL)
		preinit();

	if ((module->init == NULL || module->deinit == NULL) &&
	    set->require_init_funcs) {
		*error_r = t_strdup_printf(
			"Module doesn't have %s function",
			module->init == NULL ? "init" : "deinit");
	} else if (!module_check_wrong_binary_dependency(set, module, error_r)) {
		/* failed */
	} else if (!module_check_missing_plugin_dependencies(set, module,
							     all_modules, error_r)) {
		/* failed */
	}

	if (*error_r != NULL) {
		module->deinit = NULL;
		module_free(module);
		return -1;
	}

	if (set->debug)
		i_debug("Module loaded: %s", path);
	*module_r = module;
	return 1;
}

static int module_name_cmp(const char *const *n1, const char *const *n2)
{
	const char *s1 = *n1, *s2 = *n2;

	(void)str_begins(s1, "lib", &s1);
	(void)str_begins(s2, "lib", &s2);
	return strcmp(s1, s2);
}

static bool module_want_load(const struct module_dir_load_settings *set,
			     const char **names, const char *name)
{
	if (set->filter_callback != NULL) {
		if (!set->filter_callback(name, set->filter_context))
			return FALSE;
	}
	if (names == NULL)
		return TRUE;

	for (; *names != NULL; names++) {
		if (strcmp(*names, name) == 0) {
			*names = "";
			return TRUE;
		}
	}
	return FALSE;
}

static void check_duplicates(ARRAY_TYPE(const_string) *names,
			     const char *name, const char *dir)
{
	const char *const *names_p, *base_name, *tmp;
	unsigned int i, count;

	base_name = module_file_get_name(name);
	names_p = array_get(names, &count);
	for (i = 0; i < count; i++) T_BEGIN {
		tmp = module_file_get_name(names_p[i]);

		if (strcmp(tmp, base_name) == 0)
			i_fatal("Multiple files for module %s: %s/%s, %s/%s",
				base_name, dir, name, dir, names_p[i]);
	} T_END;
}

struct module *module_dir_find(struct module *modules, const char *name)
{
	struct module *module;
	size_t len = strlen(name);

	for (module = modules; module != NULL; module = module->next) {
		if (strncmp(module->name, name, len) == 0) {
			if (module->name[len] == '\0' ||
			    strcmp(module->name + len, "_plugin") == 0)
				return module;
		}
	}
	return NULL;
}

static bool module_is_loaded(struct module *modules, const char *name)
{
	return module_dir_find(modules, name) != NULL;
}

static const char **module_names_fix(const char *const *module_names)
{
	const char **fixed_names;
	unsigned int i, j;

	if (module_names == NULL)
		return NULL;

	/* allow giving the module names also in non-base form.
	   convert them in here. */
	fixed_names = t_new(const char *, str_array_length(module_names) + 1);
	for (i = 0; module_names[i] != NULL; i++)
		fixed_names[i] = module_file_get_name(module_names[i]);

	/* @UNSAFE: drop duplicates */
	i_qsort(fixed_names, i, sizeof(*fixed_names), i_strcmp_p);
	for (i = j = 1; fixed_names[i] != NULL; i++) {
		if (strcmp(fixed_names[i-1], fixed_names[i]) != 0)
			fixed_names[j++] = fixed_names[i];
	}
	return fixed_names;
}

static bool
module_dir_is_all_loaded(struct module *old_modules, const char **module_names)
{
	unsigned int i;

	for (i = 0; module_names[i] != NULL; i++) {
		if (!module_is_loaded(old_modules, module_names[i]))
			return FALSE;
	}
	return TRUE;
}

static int
module_dir_load_real(struct module **_modules,
		     const char *dir, const char **module_names,
		     const struct module_dir_load_settings *set,
		     const char **error_r)
{
	DIR *dirp;
	struct dirent *d;
	const char *name, *p, *error, *const *names_p;
	struct module *modules, *module, **module_pos, *old_modules = *_modules;
	unsigned int i, count;
	ARRAY_TYPE(const_string) names;
	pool_t pool;
	int ret;

	*error_r = NULL;

	if (module_names != NULL) {
		if (module_dir_is_all_loaded(old_modules, module_names))
			return 0;
	}

	if (set->debug)
		i_debug("Loading modules from directory: %s", dir);

	dirp = opendir(dir);
	if (dirp == NULL) {
		*error_r = t_strdup_printf("opendir(%s) failed: %m", dir);
		if (module_names != NULL) {
			/* we were given a list of modules to load.
			   we can't fail. */
			return -1;
		}
		return errno == ENOENT ? 0 : -1;
	}

	pool = pool_alloconly_create("module loader", 4096);
	p_array_init(&names, pool, 32);

	modules = NULL;
	for (errno = 0; (d = readdir(dirp)) != NULL; errno = 0) {
		name = d->d_name;

		if (name[0] == '.')
			continue;

		p = strstr(name, MODULE_SUFFIX);
		if (p == NULL || strlen(p) != 3)
			continue;

		T_BEGIN {
			check_duplicates(&names, name, dir);
		} T_END;

		name = p_strdup(pool, d->d_name);
		array_push_back(&names, &name);
	}
	if (errno != 0)
		*error_r = t_strdup_printf("readdir(%s) failed: %m", dir);
	if (closedir(dirp) < 0 && *error_r == NULL)
		*error_r = t_strdup_printf("closedir(%s) failed: %m", dir);
	if (*error_r != NULL) {
		pool_unref(&pool);
		return -1;
	}

	array_sort(&names, module_name_cmp);
	names_p = array_get(&names, &count);

	modules = old_modules;
	module_pos = &modules;
	while (*module_pos != NULL)
		module_pos = &(*module_pos)->next;
	for (i = 0; i < count; i++) T_BEGIN {
		const char *path, *stripped_name, *suffixless_name;

		name = names_p[i];
		stripped_name = module_file_get_name(name);
		suffixless_name = module_name_drop_suffix(stripped_name);
		if (!module_want_load(set, module_names, suffixless_name) ||
		    module_is_loaded(old_modules, suffixless_name))
			module = NULL;
		else {
			path = t_strconcat(dir, "/", name, NULL);
			ret = module_load(path, stripped_name, set, modules, &module, &error);
			if (ret >= 0)
				;
			else if (module_names != NULL) {
				*error_r = t_strdup_printf("Couldn't load required plugin %s: %s",
							   path, error);
				i = count;
			} else {
				i_error("Couldn't load plugin %s: %s", path, error);
			}
		}

		if (module != NULL) {
			*module_pos = module;
			module_pos = &module->next;
		}
	} T_END;
	pool_unref(&pool);

	if (module_names != NULL && *error_r == NULL && !set->ignore_missing) {
		/* make sure all modules were found */
		for (; *module_names != NULL; module_names++) {
			if (**module_names != '\0') {
				*error_r = t_strdup_printf("Plugin '%s' not found from directory %s",
					*module_names, dir);
				break;
			}
		}
	}
	*_modules = modules;
	return *error_r != NULL ? -1 : 0;
}

int module_dir_try_load_missing(struct module **modules,
				const char *dir, const char *const *module_names,
				const struct module_dir_load_settings *set,
				const char **error_r)
{
	int ret;

	T_BEGIN {
		ret = module_dir_load_real(modules, dir,
					   module_names_fix(module_names),
					   set, error_r);
	} T_END_PASS_STR_IF(ret < 0, error_r);
	if (ret < 0 && module_names != NULL) {
		*error_r = t_strdup_printf("%s (trying to load modules: %s)",
			*error_r, t_strarray_join(module_names, " "));
	}
	return ret;
}

struct module *
module_dir_load_missing(struct module *old_modules,
			const char *dir, const char *const *module_names,
			const struct module_dir_load_settings *set)
{
	struct module *new_modules = old_modules;
	const char *error;

	if (module_dir_try_load_missing(&new_modules, dir, module_names,
					set, &error) < 0) {
		if (module_names != NULL)
			i_fatal("%s", error);
		else
			i_error("%s", error);
	}
	return new_modules;
}

void module_dir_init(struct module *modules)
{
	struct module *module;

	for (module = modules; module != NULL; module = module->next) {
		if (!module->initialized) {
			module->initialized = TRUE;
			if (module->init != NULL) T_BEGIN {
				module->init(module);
			} T_END;
		}
	}
}

void module_dir_deinit(struct module *modules)
{
	struct module *module, **rev;
	unsigned int i, count = 0;

	for (module = modules; module != NULL; module = module->next) {
		if (module->deinit != NULL && module->initialized)
			count++;
	}

	if (count == 0)
		return;

	/* @UNSAFE: deinitialize in reverse order */
	T_BEGIN {
		rev = t_new(struct module *, count);
		for (i = 0, module = modules; i < count; ) {
			if (module->deinit != NULL && module->initialized) {
				rev[count-i-1] = module;
				i++;
			}
			module = module->next;
		}

		for (i = 0; i < count; i++) {
			module = rev[i];

			T_BEGIN {
				module->deinit();
			} T_END;
			module->initialized = FALSE;
		}
	} T_END;
}

void module_dir_unload(struct module **modules)
{
	struct module *module, *next;

	/* Call all modules' deinit() first, so that they may still call each
	   others' functions. */
	module_dir_deinit(*modules);

	for (module = *modules; module != NULL; module = next) {
		next = module->next;
		module_free(module);
	}

	*modules = NULL;
}

struct module *module_dir_load(const char *dir, const char *const *module_names,
			       const struct module_dir_load_settings *set)
{
	return module_dir_load_missing(NULL, dir, module_names, set);
}

const char *module_file_get_name(const char *fname)
{
	const char *p;

	/* [lib][nn_]name(.so) */
	(void)str_begins(fname, "lib", &fname);

	for (p = fname; *p != '\0'; p++) {
		if (*p < '0' || *p > '9')
			break;
	}
	if (*p == '_')
		fname = p + 1;

	p = strstr(fname, MODULE_SUFFIX);
	if (p == NULL)
		return fname;

	return t_strdup_until(fname, p);
}

static const char *module_name_drop_suffix(const char *name)
{
	size_t len;

	len = strlen(name);
	if (len > 7 && strcmp(name + len - 7, "_plugin") == 0)
		name = t_strndup(name, len - 7);
	return name;
}

const char *module_get_plugin_name(struct module *module)
{
	return module_name_drop_suffix(module->name);
}
