/*
 * Stores active *-Lines (G-Lines etc) inside a .db file for persistency
 * (C) Copyright 2019 Gottem and the UnrealIRCd team
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 1, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

ModuleHeader MOD_HEADER = {
	"tkldb",
	"1.10",
	"Stores active TKL entries (*-Lines) persistently/across IRCd restarts",
	"UnrealIRCd Team",
	"unrealircd-6",
};

#define TKLDB_MAGIC 0x10101010
/* Database version */
#define TKLDB_VERSION 6260
/* Save tkls to file every <this> seconds */
#define TKLDB_SAVE_EVERY 300
/* The very first save after boot, apply this delta, this
 * so we don't coincide with other (potentially) expensive
 * I/O events like saving channeldb.
 */
#define TKLDB_SAVE_EVERY_DELTA +15

// #undef BENCHMARK
/* Benchmark results (2GHz Xeon Skylake, compiled with -O2, Linux):
 * 100,000 zlines:
 * - load db: 510 ms
 * - save db:  72 ms
 * Thus, saving does not take much time and can be done by a timer
 * which executes every 5 minutes.
 * Of course, exact figures will depend on the machine.
 */

#define FreeTKLRead() \
 	do { \
		/* Some of these might be NULL */ \
		if (tkl) \
			free_tkl(tkl); \
	} while(0)

#define WARN_WRITE_ERROR(fname) \
	do { \
		unreal_log(ULOG_ERROR, "tkldb", "TKLDB_FILE_WRITE_ERROR", NULL, \
			   "[tkldb] Error writing to temporary database file $filename: $system_error", \
			   log_data_string("filename", fname), \
			   log_data_string("system_error", unrealdb_get_error_string())); \
	} while(0)

#define R_SAFE(x) \
	do { \
		if (!(x)) { \
			config_warn("[tkldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
			unrealdb_close(db); \
			FreeTKLRead(); \
			return 0; \
		} \
	} while(0)

#define W_SAFE(x) \
	do { \
		if (!(x)) { \
			WARN_WRITE_ERROR(tmpfname); \
			unrealdb_close(db); \
			return 0; \
		} \
	} while(0)

#define IsMDErr(x, y, z) \
	do { \
		if (!(x)) { \
			config_error("A critical error occurred when registering ModData for %s: %s", MOD_HEADER.name, ModuleGetErrorStr((z)->handle)); \
			return MOD_FAILED; \
		} \
	} while(0)

/* Structs */
struct cfgstruct {
	char *database;
	char *db_secret;
};

/* Forward declarations */
void tkldb_moddata_free(ModData *md);
void setcfg(struct cfgstruct *cfg);
void freecfg(struct cfgstruct *cfg);
int tkldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int tkldb_config_posttest(int *errs);
int tkldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
EVENT(write_tkldb_evt);
int write_tkldb(void);
int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl);
int read_tkldb(void);

/* Globals variables */
const uint32_t tkldb_version = TKLDB_VERSION;
static struct cfgstruct cfg;
static struct cfgstruct test;

static long tkldb_next_event = 0;

MOD_TEST()
{
	memset(&cfg, 0, sizeof(cfg));
	memset(&test, 0, sizeof(test));
	setcfg(&test);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkldb_config_test);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, tkldb_config_posttest);
	return MOD_SUCCESS;
}

MOD_INIT()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);
	ModuleSetOptions(modinfo->handle, MOD_OPT_PRIORITY, -9999);

	LoadPersistentLong(modinfo, tkldb_next_event);

	setcfg(&cfg);

	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkldb_config_run);
	return MOD_SUCCESS;
}

MOD_LOAD()
{
	if (!tkldb_next_event)
	{
		/* If this is the first time that our module is loaded, then
		 * read the TKL DB and add all *-Lines.
		 */
		if (!read_tkldb())
		{
			char fname[512];
			snprintf(fname, sizeof(fname), "%s.corrupt", cfg.database);
			if (rename(cfg.database, fname) == 0)
				config_warn("[tkldb] Existing database renamed to %s and starting a new one...", fname);
			else
				config_warn("[tkldb] Failed to rename database from %s to %s: %s", cfg.database, fname, strerror(errno));
		}
		tkldb_next_event = TStime() + TKLDB_SAVE_EVERY + TKLDB_SAVE_EVERY_DELTA;
	}
	EventAdd(modinfo->handle, "tkldb_write_tkldb", write_tkldb_evt, NULL, 1000, 0);
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	if (loop.terminating)
		write_tkldb();
	freecfg(&test);
	freecfg(&cfg);
	SavePersistentLong(modinfo, tkldb_next_event);
	return MOD_SUCCESS;
}

void tkldb_moddata_free(ModData *md)
{
	if (md->i)
		md->i = 0;
}

void setcfg(struct cfgstruct *cfg)
{
	// Default: data/tkl.db
	safe_strdup(cfg->database, "tkl.db");
	convert_to_absolute_path(&cfg->database, PERMDATADIR);
}

void freecfg(struct cfgstruct *cfg)
{
	safe_free(cfg->database);
	safe_free(cfg->db_secret);
}

int tkldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	int errors = 0;
	ConfigEntry *cep;

	// We are only interested in set::tkldb::database
	if (type != CONFIG_SET)
		return 0;

	if (!ce || strcmp(ce->name, "tkldb"))
		return 0;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!cep->value)
		{
			config_error("%s:%i: blank set::tkldb::%s without value", cep->file->filename, cep->line_number, cep->name);
			errors++;
		} else
		if (!strcmp(cep->name, "database"))
		{
			convert_to_absolute_path(&cep->value, PERMDATADIR);
			safe_strdup(test.database, cep->value);
		} else
		if (!strcmp(cep->name, "db-secret"))
		{
			const char *err;
			if ((err = unrealdb_test_secret(cep->value)))
			{
				config_error("%s:%i: set::tkldb::db-secret: %s", cep->file->filename, cep->line_number, err);
				errors++;
				continue;
			}
			safe_strdup(test.db_secret, cep->value);
		} else
		{
			config_error("%s:%i: unknown directive set::tkldb::%s", cep->file->filename, cep->line_number, cep->name);
			errors++;
		}
	}

	*errs = errors;
	return errors ? -1 : 1;
}

int tkldb_config_posttest(int *errs)
{
	int errors = 0;
	char *errstr;

	if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
	{
		config_error("[tkldb] %s", errstr);
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

int tkldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry *cep;

	// We are only interested in set::tkldb::database
	if (type != CONFIG_SET)
		return 0;

	if (!ce || strcmp(ce->name, "tkldb"))
		return 0;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "database"))
			safe_strdup(cfg.database, cep->value);
		else if (!strcmp(cep->name, "db-secret"))
			safe_strdup(cfg.db_secret, cep->value);
	}
	return 1;
}

EVENT(write_tkldb_evt)
{
	if (tkldb_next_event > TStime())
		return;
	tkldb_next_event = TStime() + TKLDB_SAVE_EVERY;
	write_tkldb();
}

int write_tkldb(void)
{
	char tmpfname[512];
	UnrealDB *db;
	uint64_t tklcount;
	int index, index2;
	TKL *tkl;
#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

	// Write to a tempfile first, then rename it if everything succeeded
	snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
	db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
	if (!db)
	{
		WARN_WRITE_ERROR(tmpfname);
		return 0;
	}

	W_SAFE(unrealdb_write_int32(db, TKLDB_MAGIC));
	W_SAFE(unrealdb_write_int32(db, tkldb_version));

	// Count the *-Lines
	tklcount = 0;

	// First the ones in the hash table
	for (index = 0; index < TKLIPHASHLEN1; index++)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
			{
				if (tkl->flags & TKL_FLAG_CONFIG)
					continue; /* config entry */
				if (IsCentralSpamfilter(tkl))
					continue;
				tklcount++;
			}
		}
	}
	// Then the regular *-Lines
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tkl = tklines[index]; tkl; tkl = tkl->next)
		{
			if (tkl->flags & TKL_FLAG_CONFIG)
				continue; /* config entry */
			if (IsCentralSpamfilter(tkl))
				continue;
			tklcount++;
		}
	}
	W_SAFE(unrealdb_write_int64(db, tklcount));

	// Now write the actual *-Lines, first the ones in the hash table
	for (index = 0; index < TKLIPHASHLEN1; index++)
	{
		for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
		{
			for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
			{
				if (tkl->flags & TKL_FLAG_CONFIG)
					continue; /* config entry */
				if (IsCentralSpamfilter(tkl))
					continue;
				if (!write_tkline(db, tmpfname, tkl)) // write_tkline() closes the db on errors itself
					return 0;
			}
		}
	}
	// Then the regular *-Lines
	for (index = 0; index < TKLISTLEN; index++)
	{
		for (tkl = tklines[index]; tkl; tkl = tkl->next)
		{
			if (tkl->flags & TKL_FLAG_CONFIG)
				continue; /* config entry */
			if (IsCentralSpamfilter(tkl))
				continue;
			if (!write_tkline(db, tmpfname, tkl))
				return 0;
		}
	}

	// Everything seems to have gone well, attempt to close and rename the tempfile
	if (!unrealdb_close(db))
	{
		WARN_WRITE_ERROR(tmpfname);
		return 0;
	}
#ifdef _WIN32
	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
	unlink(cfg.database);
#endif
	if (rename(tmpfname, cfg.database) < 0)
	{
		config_error("[tkldb] Error renaming '%s' to '%s': %s (DATABASE NOT SAVED)", tmpfname, cfg.database, strerror(errno));
		return 0;
	}
#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	config_status("[tkldb] Benchmark: SAVE DB: %lld microseconds",
		(long long)(((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
	return 1;
}

/** Write a TKL entry */
int write_tkline(UnrealDB *db, const char *tmpfname, TKL *tkl)
{
	char tkltype;
	char buf[256];

	/* First, write the common attributes */
	tkltype = tkl_typetochar(tkl->type);
	W_SAFE(unrealdb_write_char(db, tkltype)); // TKL char

	W_SAFE(unrealdb_write_str(db, tkl->set_by));
	W_SAFE(unrealdb_write_int64(db, tkl->set_at));
	W_SAFE(unrealdb_write_int64(db, tkl->expire_at));
	W_SAFE(unrealdb_write_str(db, tkl->id));     /* since TKLDB_VERSION 6260 */
	W_SAFE(unrealdb_write_str(db, tkl->spamfilter_id)); /* since TKLDB_VERSION 6260 */

	/* Hit-stat fields for all TKL types (since TKLDB_VERSION 6260): hits, lasthit. */
	W_SAFE(unrealdb_write_int64(db, tkl->hits));
	W_SAFE(unrealdb_write_int64(db, tkl->lasthit));

	if (TKLIsServerBan(tkl))
	{
		char *usermask = tkl->ptr.serverban->usermask;
		if (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT)
		{
			snprintf(buf, sizeof(buf), "%%%s", tkl->ptr.serverban->usermask);
			usermask = buf;
		}
		W_SAFE(unrealdb_write_str(db, usermask));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.serverban->hostmask));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.serverban->reason));
	} else
	if (TKLIsBanException(tkl))
	{
		char *usermask = tkl->ptr.banexception->usermask;
		if (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT)
		{
			snprintf(buf, sizeof(buf), "%%%s", tkl->ptr.banexception->usermask);
			usermask = buf;
		}
		W_SAFE(unrealdb_write_str(db, usermask));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.banexception->hostmask));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.banexception->bantypes));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.banexception->reason));
	} else
	if (TKLIsNameBan(tkl))
	{
		char *hold = tkl->ptr.nameban->hold ? "H" : "*";
		W_SAFE(unrealdb_write_str(db, hold));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.nameban->name));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.nameban->reason));
	} else
	if (TKLIsSpamfilter(tkl))
	{
		char *match_type = unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type);
		char *target = spamfilter_target_inttostring(tkl->ptr.spamfilter->target);
		char action = banact_valtochar(tkl->ptr.spamfilter->action->action); // NOTE: only writes single action value

		W_SAFE(unrealdb_write_str(db, match_type));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->match->str));
		W_SAFE(unrealdb_write_str(db, target));
		W_SAFE(unrealdb_write_char(db, action));
		W_SAFE(unrealdb_write_str(db, tkl->ptr.spamfilter->tkl_reason));
		W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->tkl_duration));
		/* Spamfilter-only hit-stat fields (since TKLDB_VERSION 6260): hits_except, lasthit_except. */
		W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->hits_except));
		W_SAFE(unrealdb_write_int64(db, tkl->ptr.spamfilter->lasthit_except));
	}

	return 1;
}

/** Read all entries from the TKL db */
int read_tkldb(void)
{
	UnrealDB *db;
	TKL *tkl = NULL;
	uint32_t magic = 0;
	uint32_t version;
	uint64_t cnt;
	uint64_t tklcount = 0;
	uint64_t v;
	int added_cnt = 0;
	char c;
	char *str;

#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

	db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
	if (!db)
	{
		if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
		{
			/* Database does not exist. Could be first boot */
			config_warn("[tkldb] No database present at '%s', will start a new one", cfg.database);
			return 1;
		} else
		if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
		{
			/* Re-open as unencrypted */
			db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
			if (!db)
			{
				/* This should actually never happen, unless some weird I/O error */
				config_warn("[tkldb] Unable to open the database file '%s': %s", cfg.database, unrealdb_get_error_string());
				return 0;
			}
		} else
		{
			config_warn("[tkldb] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
			return 0;
		}
	}

	/* The database starts with a "magic value" - unless it's some old version or corrupt */
	R_SAFE(unrealdb_read_int32(db, &magic));
	if (magic != TKLDB_MAGIC)
	{
		config_warn("[tkldb] Database '%s' uses an old and unsupported format OR is corrupt", cfg.database);
		config_status("If you are upgrading from UnrealIRCd 4 (or 5.0.0-alpha1) then we suggest you to "
		              "delete the existing database. Just keep at least 1 server linked during the upgrade "
		              "process to preserve your global *LINES and Spamfilters.");
		unrealdb_close(db);
		return 0;
	}

	/* Now do a version check */
	R_SAFE(unrealdb_read_int32(db, &version));
	if (version < 4999)
	{
		config_warn("[tkldb] Database '%s' uses an unsupport - possibly old - format (%ld).", cfg.database, (long)version);
		unrealdb_close(db);
		return 0;
	}
	if (version > tkldb_version)
	{
		config_warn("[tkldb] Database '%s' has version %lu while we only support %lu. Did you just downgrade UnrealIRCd? Sorry this is not suported",
			cfg.database, (unsigned long)tkldb_version, (unsigned long)version);
		unrealdb_close(db);
		return 0;
	}

	R_SAFE(unrealdb_read_int64(db, &tklcount));

	for (cnt = 0; cnt < tklcount; cnt++)
	{
		int do_not_add = 0;
		TKL *added = NULL;

		tkl = safe_alloc(sizeof(TKL));

		/* First, fetch the TKL type.. */
		R_SAFE(unrealdb_read_char(db, &c));
		tkl->type = tkl_chartotype(c);
		if (!tkl->type)
		{
			/* We can't continue reading the DB if we don't know the TKL type,
			 * since we don't know how long the entry will be, we can't skip it.
			 * This is "impossible" anyway, unless we some day remove a TKL type
			 * in core UnrealIRCd. In which case we should add some skipping code
			 * here to gracefully handle that situation ;)
			 */
			config_warn("[tkldb] Invalid type '%c' encountered - STOPPED READING DATABASE!", tkl->type);
			FreeTKLRead();
			break; /* we MUST stop reading */
		}

		/* Read the common types (same for all TKLs) */
		R_SAFE(unrealdb_read_str(db, &tkl->set_by));
		R_SAFE(unrealdb_read_int64(db, &v));
		tkl->set_at = v;
		R_SAFE(unrealdb_read_int64(db, &v));
		tkl->expire_at = v;

		/* id and spamfilter_id were added in TKLDB_VERSION 6260 */
		if (version >= 6260)
		{
			R_SAFE(unrealdb_read_str(db, &str));
			strlcpy(tkl->id, str, sizeof(tkl->id));
			safe_free(str);
			R_SAFE(unrealdb_read_str(db, &str));
			strlcpy(tkl->spamfilter_id, str, sizeof(tkl->spamfilter_id));
			safe_free(str);
			/* Hit-stat fields for all TKL types (hits, lasthit). */
			R_SAFE(unrealdb_read_int64(db, &v));
			tkl->hits = v;
			R_SAFE(unrealdb_read_int64(db, &v));
			tkl->lasthit = v;
		}

		/* Save some CPU... if it's already expired then don't bother adding */
		if (tkl->expire_at != 0 && tkl->expire_at <= TStime())
			do_not_add = 1;

		/* Now handle all the specific types */
		if (TKLIsServerBan(tkl))
		{
			int softban = 0;

			tkl->ptr.serverban = safe_alloc(sizeof(ServerBan));

			/* Usermask - but taking into account that the
			 * %-prefix means a soft ban.
			 */
			R_SAFE(unrealdb_read_str(db, &str));
			if (*str == '%')
			{
				softban = 1;
				safe_strdup(tkl->ptr.serverban->usermask, str+1);
			} else {
				safe_strdup(tkl->ptr.serverban->usermask, str);
			}
			safe_free(str);

			/* And the other 2 fields.. */
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.serverban->hostmask));
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.serverban->reason));

			if (find_tkl_serverban(tkl->type, tkl->ptr.serverban->usermask,
			                       tkl->ptr.serverban->hostmask, softban))
			{
				do_not_add = 1;
			}

			/* Let's special-case this, or at least for now,
			 * so we don't restore config or central spamfilters,
			 * which was a bug in 6.1.2.{12}
			 */
			if (!strcmp(tkl->set_by, "-config-"))
				do_not_add = 1;

			if (!do_not_add)
			{
				added = tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask,
				                  tkl->ptr.serverban->hostmask,
				                  NULL,
				                  tkl->ptr.serverban->reason,
				                  tkl->set_by, tkl->expire_at,
				                  tkl->set_at, softban, 0);
			}
		} else
		if (TKLIsBanException(tkl))
		{
			int softban = 0;

			tkl->ptr.banexception = safe_alloc(sizeof(BanException));

			/* Usermask - but taking into account that the
			 * %-prefix means a soft ban.
			 */
			R_SAFE(unrealdb_read_str(db, &str));
			if (*str == '%')
			{
				softban = 1;
				safe_strdup(tkl->ptr.banexception->usermask, str+1);
			} else {
				safe_strdup(tkl->ptr.banexception->usermask, str);
			}
			safe_free(str);

			/* And the other 3 fields.. */
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.banexception->hostmask));
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.banexception->bantypes));
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.banexception->reason));

			if (find_tkl_banexception(tkl->type, tkl->ptr.banexception->usermask,
			                          tkl->ptr.banexception->hostmask, softban))
			{
				do_not_add = 1;
			}

			if (!do_not_add)
			{
				added = tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask,
				                     tkl->ptr.banexception->hostmask,
				                     NULL,
				                     tkl->ptr.banexception->reason,
				                     tkl->set_by, tkl->expire_at,
				                     tkl->set_at, softban,
				                     tkl->ptr.banexception->bantypes,
				                     0);
			}
		} else
		if (TKLIsNameBan(tkl))
		{
			tkl->ptr.nameban = safe_alloc(sizeof(NameBan));

			R_SAFE(unrealdb_read_str(db, &str));
			if (*str == 'H')
				tkl->ptr.nameban->hold = 1;
			safe_free(str);
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.nameban->name));
			R_SAFE(unrealdb_read_str(db, &tkl->ptr.nameban->reason));

			if (find_tkl_nameban(tkl->type, tkl->ptr.nameban->name,
			                     tkl->ptr.nameban->hold))
			{
				do_not_add = 1;
			}

			if (!do_not_add)
			{
				added = tkl_add_nameban(tkl->type, tkl->ptr.nameban->name,
				                tkl->ptr.nameban->hold,
				                tkl->ptr.nameban->reason,
				                tkl->set_by, tkl->expire_at,
				                tkl->set_at, 0);
			}
		} else
		if (TKLIsSpamfilter(tkl))
		{
			int match_method;
			const char *err = NULL;

			tkl->ptr.spamfilter = safe_alloc(sizeof(Spamfilter));

			/* Match method */
			R_SAFE(unrealdb_read_str(db, &str));
			match_method = unreal_match_method_strtoval(str);
			if (!match_method)
			{
				config_warn("[tkldb] Unhandled spamfilter match method '%s' -- spamfilter entry not added", str);
				do_not_add = 1;
			}
			safe_free(str);

			/* Match string (eg: regex) */
			R_SAFE(unrealdb_read_str(db, &str));
			tkl->ptr.spamfilter->match = unreal_create_match(match_method, str, &err);
			if (!tkl->ptr.spamfilter->match)
			{
				config_warn("[tkldb] Spamfilter '%s' does not compile: %s -- spamfilter entry not added", str, err);
				do_not_add = 1;
			}
			safe_free(str);

			/* Target (eg: cpn) */
			R_SAFE(unrealdb_read_str(db, &str));
			tkl->ptr.spamfilter->target = spamfilter_gettargets(str, NULL);
			if (!tkl->ptr.spamfilter->target)
			{
				config_warn("[tkldb] Spamfilter '%s' without any valid targets (%s) -- spamfilter entry not added",
					tkl->ptr.spamfilter->match->str, str);
				do_not_add = 1;
			}
			safe_free(str);

			/* Action */
			R_SAFE(unrealdb_read_char(db, &c));
			tkl->ptr.spamfilter->action = banact_value_to_struct(banact_chartoval(c)); // NOTE: only reads single action value
			if (!tkl->ptr.spamfilter->action || banact_config_only(tkl->ptr.spamfilter->action->action))
			{
				config_warn("[tkldb] Spamfilter '%s' without valid action (%c) -- spamfilter entry not added",
					tkl->ptr.spamfilter->match->str, c);
				do_not_add = 1;
			}

			R_SAFE(unrealdb_read_str(db, &tkl->ptr.spamfilter->tkl_reason));
			R_SAFE(unrealdb_read_int64(db, &v));
			tkl->ptr.spamfilter->tkl_duration = v;
			/* Spamfilter-only hit-stat fields (hits_except, lasthit_except). */
			if (version >= 6260)
			{
				R_SAFE(unrealdb_read_int64(db, &v));
				tkl->ptr.spamfilter->hits_except = v;
				R_SAFE(unrealdb_read_int64(db, &v));
				tkl->ptr.spamfilter->lasthit_except = v;
			}

			if (!do_not_add &&
			    find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str,
			                        tkl->ptr.spamfilter->action->action,
			                        tkl->ptr.spamfilter->target))
			{
				do_not_add = 1;
			}

			/* Let's special-case this, or at least for now,
			 * so we don't restore config or central spamfilters,
			 * which was a bug in 6.1.2.{12}
			 */
			if (!strcmp(tkl->set_by, "-config-"))
				do_not_add = 1;

			if (!do_not_add)
			{
				added = tkl_add_spamfilter(tkl->type, NULL, tkl->ptr.spamfilter->target,
				                   tkl->ptr.spamfilter->action,
				                   tkl->ptr.spamfilter->match,
				                   NULL,
				                   NULL,
				                   tkl->set_by, tkl->expire_at, tkl->set_at,
				                   tkl->ptr.spamfilter->tkl_duration,
				                   tkl->ptr.spamfilter->tkl_reason,
				                   INPUT_CONVERSION_DEFAULT, 0, 0);
				/* Further down in the code we free fields of the TKL entry,
				 * this is generally fine since almost all fields are copied
				 * by tkl_add_spamfilter(), however some are not.
				 * For the following types tkl_add_spamfilter() actually
				 * steals the reference, instead of duplicating,
				 * so we have to set them to NULL here:
				 */
				tkl->ptr.spamfilter->match = NULL;
				tkl->ptr.spamfilter->except = NULL;
				tkl->ptr.spamfilter->action = NULL;
			}
		} else
		{
			config_warn("[tkldb] Unhandled type!! TKLDB is missing support for type %ld -- STOPPED reading db entries!", (long)tkl->type);
			FreeTKLRead();
			break; /* we MUST stop reading */
		}

		if (added)
		{
			strlcpy(added->id, tkl->id, sizeof(added->id));
			strlcpy(added->spamfilter_id, tkl->spamfilter_id, sizeof(added->spamfilter_id));
			added->hits = tkl->hits;
			added->lasthit = tkl->lasthit;
			if (TKLIsSpamfilter(added))
			{
				added->ptr.spamfilter->hits_except = tkl->ptr.spamfilter->hits_except;
				added->ptr.spamfilter->lasthit_except = tkl->ptr.spamfilter->lasthit_except;
			}
		}

		if (!do_not_add)
			added_cnt++;

		FreeTKLRead();
	}

	unrealdb_close(db);

	if (added_cnt)
		config_status("[tkldb] Re-added %d *-Lines", added_cnt);

#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	unreal_log(ULOG_DEBUG, "tkldb", "TKLDB_BENCHMARK", NULL,
	           "[tkldb] Benchmark: LOAD DB: $time_msec microseconds",
	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
	return 1;
}

