/* SPDX-License-Identifier: LGPL-2.1-only */
/*
 * Copyright (c) 2010-2013 Thomas Graf <tgraf@suug.ch>
 */

/**
 * @ingroup tc
 * @defgroup classid ClassID Management
 * @{
 */

#include "nl-default.h"

#include <sys/stat.h>
#include <search.h>

#include <netlink/netlink.h>
#include <netlink/utils.h>
#include <netlink/route/tc.h>

#include "nl-route.h"
#include "nl-aux-core/nl-core.h"

struct classid_map
{
	uint32_t		classid;
	char *			name;
	struct nl_list_head	name_list;
};

#define CLASSID_NAME_HT_SIZ 256

static struct nl_list_head tbl_name[CLASSID_NAME_HT_SIZ];

static void *id_root = NULL;

static int compare_id(const void *pa, const void *pb)
{
	const struct classid_map *ma = pa;
	const struct classid_map *mb = pb;

	if (ma->classid < mb->classid)
		return -1;

	if (ma->classid > mb->classid)
		return 1;

	return 0;
}

/* djb2 */
static unsigned int classid_tbl_hash(const char *str)
{
	unsigned long hash = 5381;
	int c;

	while ((c = *str++))
		hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

	return hash % CLASSID_NAME_HT_SIZ;
}

static int classid_lookup(const char *name, uint32_t *result)
{
	struct classid_map *map;
	int n = classid_tbl_hash(name);

	nl_list_for_each_entry(map, &tbl_name[n], name_list) {
		if (!strcasecmp(map->name, name)) {
			*result = map->classid;
			return 0;
		}
	}

	return -NLE_OBJ_NOTFOUND;
}

static char *name_lookup(const uint32_t classid)
{
	void *res;
	struct classid_map cm = {
		.classid = classid,
		.name = "search entry",
	};

	if ((res = tfind(&cm, &id_root, &compare_id)))
		return (*(struct classid_map **) res)->name;

	return NULL;
}

/**
 * @name Traffic Control Handle Translations
 * @{
 */

/**
 * Convert a traffic control handle to a character string (Reentrant).
 * @arg handle		traffic control handle
 * @arg buf		destination buffer
 * @arg len		buffer length
 *
 * Converts a tarffic control handle to a character string in the
 * form of \c MAJ:MIN and stores it in the specified destination buffer.
 *
 * @return The destination buffer or the type encoded in hexidecimal
 *         form if no match was found.
 */
char *rtnl_tc_handle2str(uint32_t handle, char *buf, size_t len)
{
	if (TC_H_ROOT == handle)
		snprintf(buf, len, "root");
	else if (TC_H_UNSPEC == handle)
		snprintf(buf, len, "none");
	else if (TC_H_INGRESS == handle)
		snprintf(buf, len, "ingress");
	else {
		char *name;

		if ((name = name_lookup(handle)))
			snprintf(buf, len, "%s", name);
		else if (0 == TC_H_MAJ(handle))
			snprintf(buf, len, ":%x", TC_H_MIN(handle));
		else if (0 == TC_H_MIN(handle))
			snprintf(buf, len, "%x:", TC_H_MAJ(handle) >> 16);
		else
			snprintf(buf, len, "%x:%x",
				TC_H_MAJ(handle) >> 16, TC_H_MIN(handle));
	}

	return buf;
}

/**
 * Convert a charactering strint to a traffic control handle
 * @arg str		traffic control handle as character string
 * @arg res		destination buffer
 *
 * Converts the provided character string specifying a traffic
 * control handle to the corresponding numeric value.
 *
 * The handle must be provided in one of the following formats:
 *  - NAME
 *  - root
 *  - none
 *  - MAJ:
 *  - :MIN
 *  - NAME:MIN
 *  - MAJ:MIN
 *  - MAJMIN
 *
 * @return 0 on success or a negative error code
 */
int rtnl_tc_str2handle(const char *str, uint32_t *res)
{
	char *colon, *end;
	uint32_t h;
	int err;

	if (!strcasecmp(str, "root")) {
		*res = TC_H_ROOT;
		return 0;
	}

	if (!strcasecmp(str, "none")) {
		*res = TC_H_UNSPEC;
		return 0;
	}

	if (!strcasecmp(str, "ingress")) {
		*res = TC_H_INGRESS;
		return 0;
	}

	h = strtoul(str, &colon, 16);

	/* MAJ is not a number */
	if (colon == str) {
not_a_number:
		if (*colon == ':') {
			/* :YYYY */
			h = 0;
		} else {
			size_t len;
			char name[64] = { 0 };

			if (!(colon = strpbrk(str, ":"))) {
				/* NAME */
				return classid_lookup(str, res);
			} else {
				/* NAME:YYYY */
				len = colon - str;
				if (len >= sizeof(name))
					return -NLE_INVAL;

				memcpy(name, str, len);

				if ((err = classid_lookup(name, &h)) < 0)
					return err;

				/* Name must point to a qdisc alias */
				if (TC_H_MIN(h))
					return -NLE_INVAL;

				/* NAME: is not allowed */
				if (colon[1] == '\0')
					return -NLE_INVAL;

				goto update;
			}
		}
	}

	if (':' == *colon) {
		/* check if we would lose bits */
		if (TC_H_MAJ(h))
			return -NLE_RANGE;
		h <<= 16;

		if ('\0' == colon[1]) {
			/* XXXX: */
			*res = h;
		} else {
			/* XXXX:YYYY */
			uint32_t l;

update:
			l = strtoul(colon+1, &end, 16);

			/* check if we overlap with major part */
			if (TC_H_MAJ(l))
				return -NLE_RANGE;

			if ('\0' != *end)
				return -NLE_INVAL;

			*res = (h | l);
		}
	} else if ('\0' == *colon) {
		/* XXXXYYYY */
		*res = h;
	} else
		goto not_a_number;

	return 0;
}

static void free_nothing(void *arg)
{
}

static void classid_map_free(struct classid_map *map)
{
	if (!map)
		return;

	free(map->name);
	free(map);
}

static void clear_hashtable(void)
{
	int i;

	for (i = 0; i < CLASSID_NAME_HT_SIZ; i++) {
		struct classid_map *map, *n;

		nl_list_for_each_entry_safe(map, n, &tbl_name[i], name_list)
			classid_map_free(map);

		nl_init_list_head(&tbl_name[i]);

	}

	if (id_root) {
		tdestroy(&id_root, &free_nothing);
		id_root = NULL;
	}
}

static int classid_map_add(uint32_t classid, const char *name)
{
	struct classid_map *map;
	int n;

	if (!(map = calloc(1, sizeof(*map))))
		return -NLE_NOMEM;

	map->classid = classid;
	map->name = strdup(name);

	n = classid_tbl_hash(map->name);
	nl_list_add_tail(&map->name_list, &tbl_name[n]);

	if (!tsearch((void *) map, &id_root, &compare_id)) {
		classid_map_free(map);
		return -NLE_NOMEM;
	}

	return 0;
}

/**
 * (Re-)read classid file
 *
 * Rereads the contents of the classid file (typically found at the location
 * /etc/libnl/classid) and refreshes the classid maps.
 *
 * @return 0 on success or a negative error code.
 */
int rtnl_tc_read_classid_file(void)
{
	static time_t last_read;
	struct stat st;
	char buf[256], *path;
	FILE *fd;
	int err;

	if (build_sysconf_path(&path, "classid") < 0)
		return -NLE_NOMEM;

	/* if stat fails, just (re-)read the file */
	if (stat(path, &st) == 0) {
		/* Don't re-read file if file is unchanged */
		if (last_read == st.st_mtime) {
			err = 0;
			goto errout;
		}
	}

	if (!(fd = fopen(path, "re"))) {
		err = -nl_syserr2nlerr(errno);
		goto errout;
	}

	clear_hashtable();

	while (fgets(buf, sizeof(buf), fd)) {
		uint32_t classid;
		char *ptr, *tok;

		/* ignore comments and empty lines */
		if (*buf == '#' || *buf == '\n' || *buf == '\r')
			continue;

		/* token 1 */
		if (!(tok = strtok_r(buf, " \t", &ptr))) {
			err = -NLE_INVAL;
			goto errout_close;
		}

		if ((err = rtnl_tc_str2handle(tok, &classid)) < 0)
			goto errout_close;

		if (!(tok = strtok_r(NULL, " \t\n\r#", &ptr))) {
			err = -NLE_INVAL;
			goto errout_close;
		}

		if ((err = classid_map_add(classid, tok)) < 0)
			goto errout_close;
	}

	err = 0;
	last_read = st.st_mtime;

errout_close:
	fclose(fd);
errout:
	free(path);

	return err;

}

int rtnl_classid_generate(const char *name, uint32_t *result, uint32_t parent)
{
	static uint32_t base = 0x4000 << 16;
	uint32_t classid;
	char *path;
	FILE *fd;
	int err = 0;

	if (parent == TC_H_ROOT || parent == TC_H_INGRESS) {
		do {
			base += (1 << 16);
			if (base == TC_H_MAJ(TC_H_ROOT))
				base = 0x4000 << 16;
		} while (name_lookup(base));

		classid = base;
	} else {
		classid = TC_H_MAJ(parent);
		do {
			if (TC_H_MIN(++classid) == TC_H_MIN(TC_H_ROOT))
				return -NLE_RANGE;
		} while (name_lookup(classid));
	}

	NL_DBG(2, "Generated new classid %#x\n", classid);

	if (build_sysconf_path(&path, "classid") < 0)
		return -NLE_NOMEM;

	if (!(fd = fopen(path, "ae"))) {
		err = -nl_syserr2nlerr(errno);
		goto errout;
	}

	fprintf(fd, "%x:", TC_H_MAJ(classid) >> 16);
	if (TC_H_MIN(classid))
		fprintf(fd, "%x", TC_H_MIN(classid));
	fprintf(fd, "\t\t\t%s\n", name);

	fclose(fd);

	if (classid_map_add(classid, name) < 0) {
		/*
		 * Error adding classid map, re-read classid file is best
		 * option here. It is likely to fail as well but better
		 * than nothing, entry was added to the file already anyway.
		 */
		rtnl_tc_read_classid_file();
	}

	*result = classid;
	err = 0;
errout:
	free(path);

	return err;
}

/** @} */

static void _nl_init classid_init(void)
{
	int err, i;

	for (i = 0; i < CLASSID_NAME_HT_SIZ; i++)
		nl_init_list_head(&tbl_name[i]);

	if ((err = rtnl_tc_read_classid_file()) < 0)
		NL_DBG(1, "Failed to read classid file: %s\n", nl_geterror(err));
}

static void free_map(void *map)
{
	free(((struct classid_map *)map)->name);
	free(map);
}

static void _nl_exit classid_exit(void)
{
	tdestroy(id_root, free_map);
}
/** @} */
