/* Copyright 2007-2010 Jozsef Kadlecsik (kadlec@netfilter.org)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <assert.h>				/* assert */
#include <ctype.h>				/* isspace */
#include <errno.h>				/* errno */
#include <stdarg.h>				/* va_* */
#include <stdbool.h>				/* bool */
#include <stdio.h>				/* printf */
#include <stdlib.h>				/* exit */
#include <string.h>				/* str* */
#include <inttypes.h>				/* PRIu64 */

#include <config.h>

#include <libipset/debug.h>			/* D() */
#include <libipset/linux_ip_set.h>		/* IPSET_CMD_* */
#include <libipset/icmp.h>			/* id_to_icmp */
#include <libipset/icmpv6.h>			/* id_to_icmpv6 */
#include <libipset/data.h>			/* enum ipset_data */
#include <libipset/types.h>			/* IPSET_*_ARG */
#include <libipset/session.h>			/* ipset_envopt_parse */
#include <libipset/parse.h>			/* ipset_parse_family */
#include <libipset/print.h>			/* ipset_print_family */
#include <libipset/utils.h>			/* STREQ */
#include <libipset/ipset.h>			/* prototypes */
#include <libipset/ip_set_compiler.h>		/* compiler attributes */
#include <libipset/list_sort.h>			/* lists */
#include <libipset/xlate.h>			/* ipset_xlate_argv */

static char program_name[] = PACKAGE;
static char program_version[] = PACKAGE_VERSION;

#define MAX_CMDLINE_CHARS			1024
#define MAX_ARGS				32

/* The ipset structure */
struct ipset {
	ipset_custom_errorfn custom_error;
		/* Custom error message function */
	ipset_standard_errorfn standard_error;
		/* Standard error message function */
	struct ipset_session *session;		/* Session */
	uint32_t restore_line;			/* Restore lineno */
	bool interactive;			/* "Interactive" CLI */
	bool full_io;				/* Use session ios */
	bool no_vhi;				/* No version/help/interactive */
	char cmdline[MAX_CMDLINE_CHARS];	/* For restore mode */
	char *newargv[MAX_ARGS];
	int newargc;
	const char *filename;			/* Input/output filename */
	bool xlate;
	struct list_head xlate_sets;
};

struct ipset_xlate_set {
	struct list_head list;
	char name[IPSET_MAXNAMELEN];
	uint8_t netmask;
	uint8_t family;
	bool interval;
	const struct ipset_type *type;
};

/* Commands and environment options */

const struct ipset_commands ipset_commands[] = {
	/* Order is important */

	{	/* c[reate], --create, n[ew], -N */
		.cmd = IPSET_CMD_CREATE,
		.name = { "create", "new", "-N" },
		.has_arg = IPSET_MANDATORY_ARG2,
		.help = "SETNAME TYPENAME [type-specific-options]\n"
			"        Create a new set",
	},
	{	/* a[dd], --add, -A  */
		.cmd = IPSET_CMD_ADD,
		.name = { "add", "-A", NULL },
		.has_arg = IPSET_MANDATORY_ARG2,
		.help = "SETNAME ENTRY\n"
			"        Add entry to the named set",
	},
	{	/* d[el], --del, -D */
		.cmd = IPSET_CMD_DEL,
		.name = { "del", "-D", NULL },
		.has_arg = IPSET_MANDATORY_ARG2,
		.help = "SETNAME ENTRY\n"
			"        Delete entry from the named set",
	},
	{	/* t[est], --test, -T */
		.cmd = IPSET_CMD_TEST,
		.name = { "test", "-T", NULL },
		.has_arg = IPSET_MANDATORY_ARG2,
		.help = "SETNAME ENTRY\n"
			"        Test entry in the named set",
	},
	{	/* des[troy], --destroy, x, -X */
		.cmd = IPSET_CMD_DESTROY,
		.name = { "destroy", "x", "-X" },
		.has_arg = IPSET_OPTIONAL_ARG,
		.help = "[SETNAME]\n"
			"        Destroy a named set or all sets",
	},
	{	/* l[ist], --list, -L */
		.cmd = IPSET_CMD_LIST,
		.name = { "list", "-L", NULL },
		.has_arg = IPSET_OPTIONAL_ARG,
		.help = "[SETNAME]\n"
			"        List the entries of a named set or all sets",
	},
	{	/* s[save], --save, -S */
		.cmd = IPSET_CMD_SAVE,
		.name = { "save", "-S", NULL },
		.has_arg = IPSET_OPTIONAL_ARG,
		.help = "[SETNAME]\n"
			"        Save the named set or all sets to stdout",
	},
	{	/* r[estore], --restore, -R */
		.cmd = IPSET_CMD_RESTORE,
		.name = { "restore", "-R", NULL },
		.has_arg = IPSET_NO_ARG,
		.help = "\n"
			"        Restore a saved state",
	},
	{	/* f[lush], --flush, -F */
		.cmd = IPSET_CMD_FLUSH,
		.name = { "flush", "-F", NULL },
		.has_arg = IPSET_OPTIONAL_ARG,
		.help = "[SETNAME]\n"
			"        Flush a named set or all sets",
	},
	{	/* ren[ame], --rename, e, -E */
		.cmd = IPSET_CMD_RENAME,
		.name = { "rename", "e", "-E" },
		.has_arg = IPSET_MANDATORY_ARG2,
		.help = "FROM-SETNAME TO-SETNAME\n"
			"        Rename two sets",
	},
	{	/* sw[ap], --swap, w, -W */
		.cmd = IPSET_CMD_SWAP,
		.name = { "swap", "w", "-W" },
		.has_arg = IPSET_MANDATORY_ARG2,
		.help = "FROM-SETNAME TO-SETNAME\n"
			"        Swap the contect of two existing sets",
	},
	{	/* h[elp, --help, -H */
		.cmd = IPSET_CMD_HELP,
		.name = { "help", "-h", "-H" },
		.has_arg = IPSET_OPTIONAL_ARG,
		.help = "[TYPENAME]\n"
			"        Print help, and settype specific help",
	},
	{	/* v[ersion], --version, -V */
		.cmd = IPSET_CMD_VERSION,
		.name = { "version", "-v", "-V" },
		.has_arg = IPSET_NO_ARG,
		.help = "\n"
			"        Print version information",
	},
	{	/* q[uit] */
		.cmd = IPSET_CMD_QUIT,
		.name = { "quit", NULL },
		.has_arg = IPSET_NO_ARG,
		.help = "\n"
			"        Quit interactive mode",
	},
	{ },
};

/**
 * ipset_match_cmd - try to match as a prefix or letter-command
 * @arg: possible command string
 * @name: command and it's aliases
 *
 * Returns true if @arg is a known command.
 */
bool
ipset_match_cmd(const char *arg, const char * const name[])
{
	size_t len, skip = 0;
	int i;

	assert(arg);
	assert(name && name[0]);

	/* Ignore two leading dashes */
	if (arg[0] == '-' && arg[1] == '-')
		skip = 2;

	len = strlen(arg);
	if (len <= skip || (len == 1 && arg[0] == '-'))
		return false;

	for (i = 0; i < IPSET_CMD_ALIASES && name[i] != NULL; i++) {
		/* New command name options */
		if (STRNEQ(arg + skip, name[i], len - skip))
			return true;
	}
	return false;
}

/* Used up so far
 *
 *	-A		add
 *	-D		del
 *	-E		rename
 *	-f		-file
 *	-F		flush
 *	-h		help
 *	-H		help
 *	-L		list
 *	-n		-name
 *	-N		create
 *	-o		-output
 *	-r		-resolve
 *	-R		restore
 *	-s		-sorted
 *	-S		save
 *	-t		-terse
 *	-T		test
 *	-q		-quiet
 *	-X		destroy
 *	-v		version
 *	-V		version
 *	-W		swap
 *	-!		-exist
 */

const struct ipset_envopts ipset_envopts[] = {
	{ .name = { "-o", "-output" },
	  .has_arg = IPSET_MANDATORY_ARG,	.flag = IPSET_OPT_MAX,
	  .parse = ipset_parse_output,
	  .help = "plain|save|xml|json\n"
		  "       Specify output mode for listing sets.\n"
		  "       Default value for \"list\" command is mode \"plain\"\n"
		  "       and for \"save\" command is mode \"save\".",
	},
	{ .name = { "-s", "-sorted" },
	  .parse = ipset_envopt_parse,
	  .has_arg = IPSET_NO_ARG,	.flag = IPSET_ENV_SORTED,
	  .help = "\n"
		  "        Print elements sorted (if supported by the set type).",
	},
	{ .name = { "-q", "-quiet" },
	  .parse = ipset_envopt_parse,
	  .has_arg = IPSET_NO_ARG,	.flag = IPSET_ENV_QUIET,
	  .help = "\n"
		  "        Suppress any notice or warning message.",
	},
	{ .name = { "-r", "-resolve" },
	  .parse = ipset_envopt_parse,
	  .has_arg = IPSET_NO_ARG,	.flag = IPSET_ENV_RESOLVE,
	  .help = "\n"
		  "        Try to resolve IP addresses in the output (slow!)",
	},
	{ .name = { "-!", "-exist" },
	  .parse = ipset_envopt_parse,
	  .has_arg = IPSET_NO_ARG,	.flag = IPSET_ENV_EXIST,
	  .help = "\n"
		  "        Ignore errors when creating or adding sets or\n"
		  "        elements that do exist or when deleting elements\n"
		  "        that don't exist.",
	},
	{ .name = { "-n", "-name" },
	  .parse = ipset_envopt_parse,
	  .has_arg = IPSET_NO_ARG,	.flag = IPSET_ENV_LIST_SETNAME,
	  .help = "\n"
		  "        When listing, just list setnames from the kernel.\n",
	},
	{ .name = { "-t", "-terse" },
	  .parse = ipset_envopt_parse,
	  .has_arg = IPSET_NO_ARG,	.flag = IPSET_ENV_LIST_HEADER,
	  .help = "\n"
		  "        When listing, list setnames and set headers\n"
		  "        from kernel only.",
	},
	{ .name = { "-f", "-file" },
	  .parse = ipset_parse_filename,
	  .has_arg = IPSET_MANDATORY_ARG,	.flag = IPSET_OPT_MAX,
	  .help = "\n"
		  "        Read from the given file instead of standard\n"
		  "        input (restore) or write to given file instead\n"
		  "        of standard output (list/save).",
	},
	{ },
};

/**
 * ipset_match_option - strict option matching
 * @arg: possible option string
 * @name: known option and it's alias
 *
 * Two leading dashes are ignored.
 *
 * Returns true if @arg is a known option.
 */
bool
ipset_match_option(const char *arg, const char * const name[])
{
	assert(arg);
	assert(name && name[0]);

	/* Skip two leading dashes */
	if (arg[0] == '-' && arg[1] == '-')
		arg++, arg++;

	return STREQ(arg, name[0]) ||
	       (name[1] != NULL && STREQ(arg, name[1]));
}

/**
 * ipset_match_envopt - strict envopt matching
 * @arg: possible envopt string
 * @name: known envopt and it's alias
 *
 * One leading dash is ignored.
 *
 * Returns true if @arg is a known envopt.
 */
bool
ipset_match_envopt(const char *arg, const char * const name[])
{
	assert(arg);
	assert(name && name[0]);

	/* Skip one leading dash */
	if (arg[0] == '-' && arg[1] == '-')
		arg++;

	return STREQ(arg, name[0]) ||
	       (name[1] != NULL && STREQ(arg, name[1]));
}

static void
ipset_shift_argv(int *argc, char *argv[], int from)
{
	int i;

	assert(*argc >= from + 1);

	for (i = from + 1; i < *argc; i++)
		argv[i-1] = argv[i];
	argv[--(*argc)] = NULL;
	return;
}

/**
 * ipset_port_usage - prints the usage for the port parameter
 *
 * Print the usage for the port parameter to stdout.
 */
void
ipset_port_usage(void)
{
	int i;
	const char *name;

	printf("      [PROTO:]PORT is a valid pattern of the following:\n"
	       "           PORTNAME         TCP port name from /etc/services\n"
	       "           PORTNUMBER       TCP port number identifier\n"
	       "           tcp|sctp|udp|udplite:PORTNAME|PORTNUMBER\n"
	       "           icmp:CODENAME    supported ICMP codename\n"
	       "           icmp:TYPE/CODE   ICMP type/code value\n"
	       "           icmpv6:CODENAME  supported ICMPv6 codename\n"
	       "           icmpv6:TYPE/CODE ICMPv6 type/code value\n"
	       "           PROTO:0          all other protocols\n\n");

	printf("           Supported ICMP codenames:\n");
	i = 0;
	while ((name = id_to_icmp(i++)) != NULL)
		printf("               %s\n", name);
	printf("           Supported ICMPv6 codenames:\n");
	i = 0;
	while ((name = id_to_icmpv6(i++)) != NULL)
		printf("               %s\n", name);
}

/**
 * ipset_parse_filename - parse filename
 * @ipset: ipset structure
 * @opt: option kind of the data
 * @str: string to parse
 *
 * Parse filename of "-file" option, which can be used once only.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_parse_filename(struct ipset *ipset,
		     int opt UNUSED, const char *str)
{
	void *p = ipset_session_printf_private(ipset->session);

	if (ipset->filename)
		return ipset->custom_error(ipset, p, IPSET_PARAMETER_PROBLEM,
			"-file option cannot be used when full io is activated");
	ipset->filename = str;

	return 0;
}

/**
 * ipset_parse_output - parse output format name
 * @ipset: ipset structure
 * @opt: option kind of the data
 * @str: string to parse
 *
 * Parse output format names and set session mode.
 * The value is stored in the session.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_parse_output(struct ipset *ipset,
		   int opt UNUSED, const char *str)
{
	struct ipset_session *session;

	assert(ipset);
	assert(str);

	session = ipset_session(ipset);
	if (STREQ(str, "plain"))
		return ipset_session_output(session, IPSET_LIST_PLAIN);
	else if (STREQ(str, "xml"))
		return ipset_session_output(session, IPSET_LIST_XML);
	else if (STREQ(str, "json"))
		return ipset_session_output(session, IPSET_LIST_JSON);
	else if (STREQ(str, "save"))
		return ipset_session_output(session, IPSET_LIST_SAVE);

	return ipset_err(session,
		"Syntax error: unknown output mode '%s'", str);
}

/**
 * ipset_envopt_parse - parse/set environment option
 * @ipset: ipset structure
 * @opt: environment option
 * @arg: option argument (unused)
 *
 * Parse and set an environment option.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_envopt_parse(struct ipset *ipset, int opt,
		   const char *arg UNUSED)
{
	struct ipset_session *session;
	assert(ipset);

	session = ipset_session(ipset);
	switch (opt) {
	case IPSET_ENV_SORTED:
	case IPSET_ENV_QUIET:
	case IPSET_ENV_RESOLVE:
	case IPSET_ENV_EXIST:
	case IPSET_ENV_LIST_SETNAME:
	case IPSET_ENV_LIST_HEADER:
		ipset_envopt_set(session, opt);
		return 0;
	default:
		break;
	}
	return -1;
}

static int __attribute__((format(printf, 4, 5)))
default_custom_error(struct ipset *ipset, void *p UNUSED,
		     int status, const char *msg, ...)
{
	struct ipset_session *session = ipset_session(ipset);
	bool is_interactive = ipset_is_interactive(ipset);
	bool quiet = !is_interactive &&
		     session &&
		     ipset_envopt_test(session, IPSET_ENV_QUIET);

	if (status && msg && !quiet) {
		va_list args;

		fprintf(stderr, "%s v%s: ", program_name, program_version);
		va_start(args, msg);
		vfprintf(stderr, msg, args);
		va_end(args);
		if (status != IPSET_SESSION_PROBLEM)
			fprintf(stderr, "\n");

		if (status == IPSET_PARAMETER_PROBLEM)
			fprintf(stderr,
				"Try `%s help' for more information.\n",
				program_name);
	}
	/* Ignore errors in interactive mode */
	if (status && is_interactive) {
		if (session)
			ipset_session_report_reset(session);
		return -1;
	}

	D("status: %u", status);
	ipset_fini(ipset);
	exit(status > IPSET_VERSION_PROBLEM ? IPSET_OTHER_PROBLEM : status);
	/* Unreached */
	return -1;
}

static int
default_standard_error(struct ipset *ipset, void *p)
{
	struct ipset_session *session = ipset_session(ipset);
	bool is_interactive = ipset_is_interactive(ipset);
	enum ipset_err_type err_type = ipset_session_report_type(session);

	if ((err_type == IPSET_WARNING || err_type == IPSET_NOTICE) &&
	    !ipset_envopt_test(session, IPSET_ENV_QUIET))
		fprintf(stderr, "%s%s",
			err_type == IPSET_WARNING ? "Warning: " : "",
			ipset_session_report_msg(session));
	if (err_type == IPSET_ERROR)
		return ipset->custom_error(ipset, p,
				IPSET_SESSION_PROBLEM, "%s",
				ipset_session_report_msg(session));

	if (!is_interactive) {
		ipset_fini(ipset);
		/* Warnings are not errors */
		exit(err_type <= IPSET_WARNING ? 0 : IPSET_OTHER_PROBLEM);
	}

	ipset_session_report_reset(session);
	return -1;
}

static void
default_help(void)
{
	const struct ipset_commands *c;
	const struct ipset_envopts *opt = ipset_envopts;

	printf("%s v%s\n\n"
	       "Usage: %s [options] COMMAND\n\nCommands:\n",
	       program_name, program_version, program_name);

	for (c = ipset_commands; c->cmd; c++)
		printf("%s %s\n", c->name[0], c->help);
	printf("\nOptions:\n");

	while (opt->flag) {
		if (opt->help)
			printf("%s %s\n", opt->name[0], opt->help);
		opt++;
	}
}

static void
reset_argv(struct ipset *ipset)
{
	int i;

	/* Reset */
	for (i = 1; i < ipset->newargc; i++) {
		if (ipset->newargv[i])
			free(ipset->newargv[i]);
		ipset->newargv[i] = NULL;
	}
	ipset->newargc = 1;
}

/* Build fake argv from parsed line */
static int
build_argv(struct ipset *ipset, char *buffer)
{
	void *p = ipset_session_printf_private(ipset->session);
	char *tmp, *arg;
	int i;
	bool quoted = false;

	reset_argv(ipset);
	arg = calloc(strlen(buffer) + 1, sizeof(*buffer));
	if (!arg)
		return ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
					   "Cannot allocate memory.");
	for (tmp = buffer, i = 0; *tmp; tmp++) {
		if ((ipset->newargc + 1) ==
		    (int)(sizeof(ipset->newargv)/sizeof(char *))) {
			free(arg);
			return ipset->custom_error(ipset,
					p, IPSET_PARAMETER_PROBLEM,
					"Line is too long to parse.");
		}
		switch (*tmp) {
		case '"':
			quoted = !quoted;
			if (*(tmp+1))
				continue;
			break;
		case ' ':
		case '\r':
		case '\n':
		case '\t':
			if (!quoted)
				break;
			arg[i++] = *tmp;
			continue;
		default:
			arg[i++] = *tmp;
			if (*(tmp+1))
				continue;
			break;
		}
		if (!*(tmp+1) && quoted) {
			free(arg);
			return ipset->custom_error(ipset,
				p, IPSET_PARAMETER_PROBLEM,
				"Missing close quote!");
		}
		if (!*arg)
			continue;
		ipset->newargv[ipset->newargc] =
			calloc(strlen(arg) + 1, sizeof(*arg));
		if (!ipset->newargv[ipset->newargc]) {
			free(arg);
			return ipset->custom_error(ipset,
				p, IPSET_OTHER_PROBLEM,
				"Cannot allocate memory.");
		}
		ipset_strlcpy(ipset->newargv[ipset->newargc++],
			      arg, strlen(arg) + 1);
		memset(arg, 0, strlen(arg) + 1);
		i = 0;
	}

	free(arg);
	return 0;
}

static int
restore(struct ipset *ipset)
{
	struct ipset_session *session = ipset_session(ipset);
	int ret = 0;
	FILE *f = stdin;	/* Default from stdin */

	if (ipset->filename) {
		ret = ipset_session_io_normal(session, ipset->filename,
					      IPSET_IO_INPUT);
		if (ret < 0)
			return ret;
		f = ipset_session_io_stream(session, IPSET_IO_INPUT);
	}
	return ipset_parse_stream(ipset, f);
}

static bool do_parse(const struct ipset_arg *arg, bool family)
{
	return !((family == true) ^ (arg->opt == IPSET_OPT_FAMILY));
}

static int
call_parser(struct ipset *ipset, int *argc, char *argv[],
	    const struct ipset_type *type, enum ipset_adt cmd, bool family)
{
	void *p = ipset_session_printf_private(ipset->session);
	const struct ipset_arg *arg;
	const char *optstr;
	const struct ipset_type *t = type;
	uint8_t revision = type->revision;
	int ret = 0, i = 1, j;

	/* Currently CREATE and ADT may have got additional arguments */
	if (type->cmd[cmd].args[0] == IPSET_ARG_NONE && *argc > 1)
		return ipset->custom_error(ipset,
				p, IPSET_PARAMETER_PROBLEM,
				"Unknown argument: `%s'", argv[i]);

	while (*argc > i) {
		ret = -1;
		for (j = 0; type->cmd[cmd].args[j] != IPSET_ARG_NONE; j++) {
			arg = ipset_keyword(type->cmd[cmd].args[j]);
			D("argc: %u, %s vs %s", i, argv[i], arg->name[0]);
			if (!(ipset_match_option(argv[i], arg->name)))
				continue;

			optstr = argv[i];
			/* Matched option */
			D("match %s, argc %u, i %u, %s",
			  arg->name[0], *argc, i + 1,
			  do_parse(arg, family) ? "parse" : "skip");
			i++;
			ret = 0;
			switch (arg->has_arg) {
			case IPSET_MANDATORY_ARG:
				if (*argc - i < 1)
					return ipset->custom_error(ipset, p,
						IPSET_PARAMETER_PROBLEM,
						"Missing mandatory argument "
						"of option `%s'",
						arg->name[0]);
				/* Fall through */
			case IPSET_OPTIONAL_ARG:
				if (*argc - i >= 1) {
					if (do_parse(arg, family)) {
						ret = ipset_call_parser(
							ipset->session,
							arg, argv[i]);
						if (ret < 0)
							return ret;
					}
					i++;
					break;
				}
				/* Fall through */
			default:
				if (do_parse(arg, family)) {
					ret = ipset_call_parser(
						ipset->session, arg, optstr);
					if (ret < 0)
						return ret;
				}
			}
			break;
		}
		if (ret < 0)
			goto err_unknown;
	}
	if (!family)
		*argc = 0;
	return ret;

err_unknown:
	while ((type = ipset_type_higher_rev(t)) != t) {
		for (j = 0; type->cmd[cmd].args[j] != IPSET_ARG_NONE; j++) {
			arg = ipset_keyword(type->cmd[cmd].args[j]);
			D("argc: %u, %s vs %s", i, argv[i], arg->name[0]);
			if (ipset_match_option(argv[i], arg->name))
				return ipset->custom_error(ipset, p,
					IPSET_PARAMETER_PROBLEM,
					"Argument `%s' is supported in the kernel module "
					"of the set type %s starting from the revision %u "
					"and you have installed revision %u only. "
					"Your kernel is behind your ipset utility.",
					argv[i], type->name,
					type->revision, revision);
		}
		t = type;
	}
	return ipset->custom_error(ipset, p, IPSET_PARAMETER_PROBLEM,
				   "Unknown argument: `%s'", argv[i]);
}

static enum ipset_adt
cmd2cmd(int cmd)
{
	switch (cmd) {
	case IPSET_CMD_ADD:
		return IPSET_ADD;
	case IPSET_CMD_DEL:
		return IPSET_DEL;
	case IPSET_CMD_TEST:
		return IPSET_TEST;
	case IPSET_CMD_CREATE:
		return IPSET_CREATE;
	default:
		return 0;
	}
}

static void
check_mandatory(struct ipset *ipset,
		const struct ipset_type *type, enum ipset_cmd command)
{
	enum ipset_adt cmd = cmd2cmd(command);
	struct ipset_session *session = ipset->session;
	void *p = ipset_session_printf_private(session);
	uint64_t flags = ipset_data_flags(ipset_session_data(session));
	uint64_t mandatory = type->cmd[cmd].need;
	const struct ipset_arg *arg;
	int i;

	/* Range can be expressed by ip/cidr */
	if (flags & IPSET_FLAG(IPSET_OPT_CIDR))
		flags |= IPSET_FLAG(IPSET_OPT_IP_TO);

	mandatory &= ~flags;
	if (!mandatory)
		return;
	if (type->cmd[cmd].args[0] == IPSET_ARG_NONE) {
		ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
			"There are missing mandatory flags "
			"but can't check them. "
			"It's a bug, please report the problem.");
		return;
	}

	for (i = 0; type->cmd[cmd].args[i] != IPSET_ARG_NONE; i++) {
		arg = ipset_keyword(type->cmd[cmd].args[i]);
		if (mandatory & IPSET_FLAG(arg->opt)) {
			ipset->custom_error(ipset, p, IPSET_PARAMETER_PROBLEM,
				   "Mandatory option `%s' is missing",
				   arg->name[0]);
			return;
		}
	}
}

static const char *
cmd2name(enum ipset_cmd cmd)
{
	const struct ipset_commands *c;

	for (c = ipset_commands; c->cmd; c++)
		if (cmd == c->cmd)
			return c->name[0];
	return "unknown command";
}

static const char *
session_family(struct ipset_session *session)
{
	switch (ipset_data_family(ipset_session_data(session))) {
	case NFPROTO_IPV4:
		return "inet";
	case NFPROTO_IPV6:
		return "inet6";
	default:
		return "unspec";
	}
}

static void
check_allowed(struct ipset *ipset,
	      const struct ipset_type *type, enum ipset_cmd command)
{
	struct ipset_session *session = ipset->session;
	void *p = ipset_session_printf_private(session);
	uint64_t flags = ipset_data_flags(ipset_session_data(session));
	enum ipset_adt cmd = cmd2cmd(command);
	uint64_t allowed = type->cmd[cmd].full;
	uint64_t cmdflags = command == IPSET_CMD_CREATE
				? IPSET_CREATE_FLAGS : IPSET_ADT_FLAGS;
	const struct ipset_arg *arg;
	enum ipset_opt i;
	int j;

	/* Range can be expressed by ip/cidr or from-to */
	if (allowed & IPSET_FLAG(IPSET_OPT_IP_TO))
		allowed |= IPSET_FLAG(IPSET_OPT_CIDR);

	for (i = IPSET_OPT_IP; i < IPSET_OPT_FLAGS; i++) {
		if (!(cmdflags & IPSET_FLAG(i)) ||
		    (allowed & IPSET_FLAG(i)) ||
		    !(flags & IPSET_FLAG(i)))
			continue;
		/* Not allowed element-expressions */
		switch (i) {
		case IPSET_OPT_CIDR:
			ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
				"IP/CIDR range is not allowed in command %s "
				"with set type %s and family %s",
				cmd2name(command), type->name,
				session_family(ipset->session));
			return;
		case IPSET_OPT_IP_TO:
			ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
				"FROM-TO IP range is not allowed in command %s "
				"with set type %s and family %s",
				cmd2name(command), type->name,
				session_family(ipset->session));
			return;
		case IPSET_OPT_PORT_TO:
			ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
				"FROM-TO port range is not allowed in command %s "
				"with set type %s and family %s",
				cmd2name(command), type->name,
				session_family(ipset->session));
			return;
		default:
			break;
		}
		/* Other options */
		if (type->cmd[cmd].args[0] == IPSET_ARG_NONE) {
			ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
				"There are not allowed options (%u) "
				"but option list is empty. "
				"It's a bug, please report the problem.", i);
			return;
		}
		for (j = 0; type->cmd[cmd].args[j] != IPSET_ARG_NONE; j++) {
			arg = ipset_keyword(type->cmd[cmd].args[j]);
			if (arg->opt != i)
				continue;
			ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
				"%s parameter is not allowed in command %s "
				"with set type %s and family %s",
				arg->name[0],
				cmd2name(command), type->name,
				session_family(ipset->session));
			return;
		}
		ipset->custom_error(ipset, p, IPSET_OTHER_PROBLEM,
			"There are not allowed options (%u) "
			"but can't resolve them. "
			"It's a bug, please report the problem.", i);
		return;
	}
}

static const struct ipset_type *
type_find(const char *name)
{
	const struct ipset_type *t = ipset_types();

	while (t) {
		if (ipset_match_typename(name, t))
			return t;
		t = t->next;
	}
	return NULL;
}

static enum ipset_adt cmd_help_order[] = {
	IPSET_CREATE,
	IPSET_ADD,
	IPSET_DEL,
	IPSET_TEST,
	IPSET_CADT_MAX,
};

static const char *cmd_prefix[] = {
	[IPSET_CREATE] = "create SETNAME",
	[IPSET_ADD]    = "add    SETNAME",
	[IPSET_DEL]    = "del    SETNAME",
	[IPSET_TEST]   = "test   SETNAME",
};

static struct ipset_xlate_set *
ipset_xlate_set_get(struct ipset *ipset, const char *name)
{
	struct ipset_xlate_set *set;

	list_for_each_entry(set, &ipset->xlate_sets, list) {
		if (!strcmp(set->name, name))
			return set;
	}

	return NULL;
}

static int
ipset_parser(struct ipset *ipset, int oargc, char *oargv[])
{
	int ret = 0;
	enum ipset_cmd cmd = IPSET_CMD_NONE;
	int i;
	char *arg0 = NULL, *arg1 = NULL;
	const struct ipset_envopts *opt;
	const struct ipset_commands *command;
	const struct ipset_type *type = NULL;
	struct ipset_session *session = ipset->session;
	void *p = ipset_session_printf_private(session);
	int argc = oargc;
	char *argv[MAX_ARGS] = {};

	if (argc > MAX_ARGS)
		return ipset->custom_error(ipset,
				p, IPSET_PARAMETER_PROBLEM,
				"Line is too long to parse.");

	/* We need a local copy because of ipset_shift_argv */
	memcpy(argv, oargv, sizeof(char *) * argc);

	/* Set session lineno to report parser errors correctly */
	ipset_session_lineno(session, ipset->restore_line);

	/* Commandline parsing, somewhat similar to that of 'ip' */

	/* First: parse core options */
	for (opt = ipset_envopts; opt->flag; opt++) {
		for (i = 1; i < argc; ) {
			if (!ipset_match_envopt(argv[i], opt->name)) {
				i++;
				continue;
			}
			/* Shift off matched option */
			ipset_shift_argv(&argc, argv, i);
			switch (opt->has_arg) {
			case IPSET_MANDATORY_ARG:
				if (i + 1 > argc)
					return ipset->custom_error(ipset, p,
						IPSET_PARAMETER_PROBLEM,
						"Missing mandatory argument "
						"to option %s",
						opt->name[0]);
				/* Fall through */
			case IPSET_OPTIONAL_ARG:
				if (i + 1 <= argc) {
					ret = opt->parse(ipset, opt->flag,
							 argv[i]);
					if (ret < 0)
						return ipset->standard_error(ipset, p);
					ipset_shift_argv(&argc, argv, i);
				}
				break;
			case IPSET_NO_ARG:
				ret = opt->parse(ipset, opt->flag,
						 opt->name[0]);
				if (ret < 0)
					return ipset->standard_error(ipset, p);
				break;
			default:
				break;
			}
		}
	}

	/* Second: parse command */
	for (command = ipset_commands;
		 argc > 1 && command->cmd && cmd == IPSET_CMD_NONE;
	     command++) {
		if (!ipset_match_cmd(argv[1], command->name))
			continue;

		if (ipset->restore_line != 0 &&
		    (command->cmd == IPSET_CMD_RESTORE ||
		     command->cmd == IPSET_CMD_VERSION ||
		     command->cmd == IPSET_CMD_HELP))
			return ipset->custom_error(ipset, p,
				IPSET_PARAMETER_PROBLEM,
				"Command `%s' is invalid "
				"in restore mode.",
				command->name[0]);
		if (ipset->interactive && command->cmd == IPSET_CMD_RESTORE) {
			printf("Restore command is not supported "
			       "in interactive mode\n");
			return 0;
		}

		/* Shift off matched command arg */
		ipset_shift_argv(&argc, argv, 1);
		cmd = command->cmd;
		switch (command->has_arg) {
		case IPSET_MANDATORY_ARG:
		case IPSET_MANDATORY_ARG2:
			if (argc < 2)
				return ipset->custom_error(ipset, p,
					IPSET_PARAMETER_PROBLEM,
					"Missing mandatory argument "
					"to command %s",
					command->name[0]);
			/* Fall through */
		case IPSET_OPTIONAL_ARG:
			arg0 = argv[1];
			if (argc >= 2)
				/* Shift off first arg */
				ipset_shift_argv(&argc, argv, 1);
			break;
		default:
			break;
		}
		if (command->has_arg == IPSET_MANDATORY_ARG2) {
			if (argc < 2)
				return ipset->custom_error(ipset, p,
					IPSET_PARAMETER_PROBLEM,
					"Missing second mandatory "
					"argument to command %s",
					command->name[0]);
			arg1 = argv[1];
			/* Shift off second arg */
			ipset_shift_argv(&argc, argv, 1);
		}
		break;
	}

	/* Third: catch interactive mode, handle help, version */
	switch (cmd) {
	case IPSET_CMD_NONE:
		if (ipset->interactive) {
			printf("No command specified\n");
			if (session)
				ipset_envopt_parse(ipset, 0, "reset");
			return 0;
		}
		if (argc > 1 && STREQ(argv[1], "-")) {
			if (ipset->no_vhi)
				return 0;
			ipset->interactive = true;
			printf("%s> ", program_name);
			while (fgets(ipset->cmdline,
				     sizeof(ipset->cmdline), stdin)) {
				/* Execute line: ignore soft errors */
				if (ipset_parse_line(ipset, ipset->cmdline) < 0)
					ipset->standard_error(ipset, p);
				printf("%s> ", program_name);
			}
			return ipset->custom_error(ipset, p,
						   IPSET_NO_PROBLEM, NULL);
		}
		if (argc > 1)
			return ipset->custom_error(ipset,
				p, IPSET_PARAMETER_PROBLEM,
				"No command specified: unknown argument %s",
				argv[1]);
		return ipset->custom_error(ipset, p, IPSET_PARAMETER_PROBLEM,
					   "No command specified.");
	case IPSET_CMD_VERSION:
		if (ipset->no_vhi)
			return 0;
		printf("%s v%s, protocol version: %u\n",
		       program_name, program_version, IPSET_PROTOCOL);
		/* Check kernel protocol version */
		ipset_cmd(session, IPSET_CMD_NONE, 0);
		if (ipset_session_report_type(session) != IPSET_NO_ERROR)
			ipset->standard_error(ipset, p);
		if (ipset->interactive)
			return 0;
		return ipset->custom_error(ipset, p, IPSET_NO_PROBLEM, NULL);
	case IPSET_CMD_HELP:
		if (ipset->no_vhi)
			return 0;
		default_help();

		if (ipset->interactive ||
		    !ipset_envopt_test(session, IPSET_ENV_QUIET)) {
			if (arg0) {
				const struct ipset_arg *arg;
				int k;
				enum ipset_adt c;

				/* Type-specific help, without kernel checking */
				type = type_find(arg0);
				if (!type)
					return ipset->custom_error(ipset, p,
						IPSET_PARAMETER_PROBLEM,
						"Unknown settype: `%s'", arg0);
				printf("\n%s type specific options:\n\n", type->name);
				for (i = 0; cmd_help_order[i] != IPSET_CADT_MAX; i++) {
					c = cmd_help_order[i];
					printf("%s %s %s\n",
						cmd_prefix[c], type->name, type->cmd[c].help);
					for (k = 0; type->cmd[c].args[k] != IPSET_ARG_NONE; k++) {
						arg = ipset_keyword(type->cmd[c].args[k]);
						if (!arg->help || arg->help[0] == '\0')
							continue;
						printf("               %s\n", arg->help);
					}
				}
				printf("\n%s\n", type->usage);
				if (type->usagefn)
					type->usagefn();
				if (type->family == NFPROTO_UNSPEC)
					printf("\nType %s is family neutral.\n",
					       type->name);
				else if (type->family == NFPROTO_IPSET_IPV46)
					printf("\nType %s supports inet "
					       "and inet6.\n",
					       type->name);
				else
					printf("\nType %s supports family "
					       "%s only.\n",
					       type->name,
					       type->family == NFPROTO_IPV4
						? "inet" : "inet6");
			} else {
				printf("\nSupported set types:\n");
				type = ipset_types();
				while (type) {
					printf("    %s\t%s%u\t%s\n",
					       type->name,
					       strlen(type->name) < 12 ? "\t" : "",
					       type->revision,
					       type->description);
					type = type->next;
				}
			}
		}
		if (ipset->interactive)
			return 0;
		return ipset->custom_error(ipset, p, IPSET_NO_PROBLEM, NULL);
	case IPSET_CMD_QUIT:
		return ipset->custom_error(ipset, p, IPSET_NO_PROBLEM, NULL);
	default:
		break;
	}

	/* Forth: parse command args and issue the command */
	switch (cmd) {
	case IPSET_CMD_CREATE:
		/* Args: setname typename [type specific options] */
		ret = ipset_parse_setname(session, IPSET_SETNAME, arg0);
		if (ret < 0)
			return ipset->standard_error(ipset, p);

		ret = ipset_parse_typename(session, IPSET_OPT_TYPENAME, arg1);
		if (ret < 0)
			return ipset->standard_error(ipset, p);

		type = ipset_type_get(session, cmd);
		if (type == NULL)
			return ipset->standard_error(ipset, p);

		/* Parse create options: first check INET family */
		ret = call_parser(ipset, &argc, argv, type, IPSET_CREATE, true);
		if (ret < 0)
			return ipset->standard_error(ipset, p);
		else if (ret)
			return ret;

		/* Parse create options: then check all options */
		ret = call_parser(ipset, &argc, argv, type, IPSET_CREATE, false);
		if (ret < 0)
			return ipset->standard_error(ipset, p);
		else if (ret)
			return ret;

		/* Check mandatory, then allowed options */
		check_mandatory(ipset, type, cmd);
		check_allowed(ipset, type, cmd);

		break;
	case IPSET_CMD_LIST:
	case IPSET_CMD_SAVE:
		if (ipset->filename != NULL) {
			ret = ipset_session_io_normal(session,
					ipset->filename, IPSET_IO_OUTPUT);
			if (ret < 0)
				return ret;
		}
		/* Fall through to parse optional setname */
		fallthrough;
	case IPSET_CMD_DESTROY:
	case IPSET_CMD_FLUSH:
		/* Args: [setname] */
		if (arg0) {
			ret = ipset_parse_setname(session,
						  IPSET_SETNAME, arg0);
			if (ret < 0)
				return ipset->standard_error(ipset, p);
		}
		break;

	case IPSET_CMD_RENAME:
	case IPSET_CMD_SWAP:
		/* Args: from-setname to-setname */
		ret = ipset_parse_setname(session, IPSET_SETNAME, arg0);
		if (ret < 0)
			return ipset->standard_error(ipset, p);
		ret = ipset_parse_setname(session, IPSET_OPT_SETNAME2, arg1);
		if (ret < 0)
			return ipset->standard_error(ipset, p);
		break;

	case IPSET_CMD_RESTORE:
		/* Restore mode */
		if (argc > 1)
			return ipset->custom_error(ipset,
				p, IPSET_PARAMETER_PROBLEM,
				"Unknown argument %s", argv[1]);
		return IPSET_CMD_RESTORE;
	case IPSET_CMD_ADD:
	case IPSET_CMD_DEL:
	case IPSET_CMD_TEST:
		D("ADT: setname %s", arg0);
		/* Args: setname ip [options] */
		ret = ipset_parse_setname(session, IPSET_SETNAME, arg0);
		if (ret < 0)
			return ipset->standard_error(ipset, p);

		if (!ipset->xlate) {
			type = ipset_type_get(session, cmd);
		} else {
			const struct ipset_xlate_set *xlate_set;

			xlate_set = ipset_xlate_set_get(ipset, arg0);
			if (xlate_set) {
				ipset_session_data_set(session, IPSET_OPT_TYPE,
						       xlate_set->type);
				ipset_session_data_set(session, IPSET_OPT_FAMILY,
						       &xlate_set->family);
				type = xlate_set->type;
			}
		}
		if (type == NULL)
			return ipset->standard_error(ipset, p);

		ret = ipset_parse_elem(session, type->last_elem_optional, arg1);
		if (ret < 0)
			return ipset->standard_error(ipset, p);

		/* Parse additional ADT options */
		ret = call_parser(ipset, &argc, argv, type, cmd2cmd(cmd), false);
		if (ret < 0)
			return ipset->standard_error(ipset, p);
		else if (ret)
			return ret;

		/* Check mandatory, then allowed options */
		check_mandatory(ipset, type, cmd);
		check_allowed(ipset, type, cmd);

		break;
	default:
		break;
	}

	if (argc > 1)
		return ipset->custom_error(ipset, p, IPSET_PARAMETER_PROBLEM,
			"Unknown argument %s", argv[1]);

	return cmd;
}

/* Workhorses */

/**
 * ipset_parse_argv - parse and argv array and execute the command
 * @ipset: ipset structure
 * @argc: length of the array
 * @argv: array of strings
 *
 * Parse an array of strings and execute the ipset command.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_parse_argv(struct ipset *ipset, int oargc, char *oargv[])
{
	struct ipset_session *session = ipset->session;
	void *p = ipset_session_printf_private(session);
	enum ipset_cmd cmd;
	int ret;

	cmd = ipset_parser(ipset, oargc, oargv);
	if (cmd < 0)
		return cmd;

	if (cmd == IPSET_CMD_RESTORE)
		return restore(ipset);

	ret = ipset_cmd(session, cmd, ipset->restore_line);
	D("ret %d", ret);
	/* In the case of warning, the return code is success */
	if (ret < 0 || ipset_session_report_type(session) > IPSET_NO_ERROR)
		ipset->standard_error(ipset, p);

	return ret;
}

/**
 * ipset_parse_line - parse a string as a command line and execute it
 * @ipset: ipset structure
 * @line: string of line
 *
 * Parse a string as a command line and execute the ipset command.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_parse_line(struct ipset *ipset, char *line)
{
	char *c = line;
	int ret;

	reset_argv(ipset);

	while (isspace(c[0]))
		c++;
	if (c[0] == '\0' || c[0] == '#') {
		if (ipset->interactive)
			printf("%s> ", program_name);
		return 0;
	}
	/* Build fake argv, argc */
	ret = build_argv(ipset, c);
	if (ret < 0)
		return ret;
	/* Parse and execute line */
	return ipset_parse_argv(ipset, ipset->newargc, ipset->newargv);
}

/**
 * ipset_parse_stream - parse an stream and execute the commands
 * @ipset: ipset structure
 * @f: stream
 *
 * Parse an already opened file as stream and execute the commands.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_parse_stream(struct ipset *ipset, FILE *f)
{
	struct ipset_session *session = ipset_session(ipset);
	void *p = ipset_session_printf_private(session);
	int ret = 0;
	char *c;

	while (fgets(ipset->cmdline, sizeof(ipset->cmdline), f)) {
		ipset->restore_line++;
		c = ipset->cmdline;
		while (isspace(c[0]))
			c++;
		if (c[0] == '\0' || c[0] == '#')
			continue;
		else if (STREQ(c, "COMMIT\n") || STREQ(c, "COMMIT\r\n")) {
			ret = ipset_commit(ipset->session);
			if (ret < 0)
				ipset->standard_error(ipset, p);
			continue;
		}
		/* Build faked argv, argc */
		ret = build_argv(ipset, c);
		if (ret < 0)
			return ret;

		/* Execute line */
		ret = ipset_parse_argv(ipset, ipset->newargc, ipset->newargv);
		if (ret < 0)
			ipset->standard_error(ipset, p);
	}
	/* implicit "COMMIT" at EOF */
	ret = ipset_commit(ipset->session);
	if (ret < 0)
		ipset->standard_error(ipset, p);

	return ret;
}

/**
 * ipset_session - returns the session pointer of an ipset structure
 * @ipset: ipset structure
 *
 * Returns the session pointer of an ipset structure.
 */
struct ipset_session *
ipset_session(struct ipset *ipset)
{
	return ipset->session;
}

/**
 * ipset_is_interactive - is the interactive mode enabled?
 * @ipset: ipset structure
 *
 * Returns true if the interactive mode is enabled.
 */
bool
ipset_is_interactive(struct ipset *ipset)
{
	return ipset->interactive;
}

/**
 * ipset_custom_printf - set custom print functions
 * @ipset: ipset structure
 * @custom_error: custom error function
 * @standard_error: standard error function
 * @print_outfn: output/printing function
 * @p: pointer to private data area
 *
 * The function makes possible to set custom error and
 * output functions for the library. The private data
 * pointer can be used to pass arbitrary data to these functions.
 * If a function argument is NULL, the default printing function is set.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_custom_printf(struct ipset *ipset,
		    ipset_custom_errorfn custom_error,
		    ipset_standard_errorfn standard_error,
		    ipset_print_outfn print_outfn,
		    void *p)
{
	ipset->no_vhi = !!(custom_error || standard_error || print_outfn);
	ipset->custom_error =
		custom_error ? custom_error : default_custom_error;
	ipset->standard_error =
		standard_error ? standard_error : default_standard_error;

	return ipset_session_print_outfn(ipset->session, print_outfn, p);
}

/**
 * ipset_init - initialize ipset library interface
 *
 * Initialize the ipset library interface.
 *
 * Returns the created ipset structure for success or NULL for failure.
 */
struct ipset *
ipset_init(void)
{
	struct ipset *ipset;

	ipset = calloc(1, sizeof(struct ipset));
	if (ipset == NULL)
		return NULL;
	ipset->newargv[0] =
		calloc(strlen(program_name) + 1, sizeof(*program_name));
	if (!ipset->newargv[0]) {
		free(ipset);
		return NULL;
	}
	ipset_strlcpy(ipset->newargv[0], program_name,
		      strlen(program_name) + 1);
	ipset->newargc = 1;
	ipset->session = ipset_session_init(NULL, NULL);
	if (ipset->session == NULL) {
		free(ipset->newargv[0]);
		free(ipset);
		return NULL;
	}
	ipset_custom_printf(ipset, NULL, NULL, NULL, NULL);

	INIT_LIST_HEAD(&ipset->xlate_sets);

	return ipset;
}

/**
 * ipset_fini - destroy an ipset library interface
 * @ipset: ipset structure
 *
 * Destroys an ipset library interface
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_fini(struct ipset *ipset)
{
	struct ipset_xlate_set *xlate_set, *next;

	assert(ipset);

	if (ipset->session)
		ipset_session_fini(ipset->session);
	reset_argv(ipset);
	if (ipset->newargv[0])
		free(ipset->newargv[0]);

	list_for_each_entry_safe(xlate_set, next, &ipset->xlate_sets, list)
		free(xlate_set);

	free(ipset);
	return 0;
}

/* Ignore the set family, use inet. */
static const char *ipset_xlate_family(uint8_t family UNUSED)
{
	return "inet";
}

enum ipset_xlate_set_type {
	IPSET_XLATE_TYPE_UNKNOWN	= 0,
	IPSET_XLATE_TYPE_HASH_MAC,
	IPSET_XLATE_TYPE_HASH_IP,
	IPSET_XLATE_TYPE_HASH_IP_MAC,
	IPSET_XLATE_TYPE_HASH_NET_IFACE,
	IPSET_XLATE_TYPE_HASH_NET_PORT,
	IPSET_XLATE_TYPE_HASH_NET_PORT_NET,
	IPSET_XLATE_TYPE_HASH_NET_NET,
	IPSET_XLATE_TYPE_HASH_NET,
	IPSET_XLATE_TYPE_HASH_IP_PORT_NET,
	IPSET_XLATE_TYPE_HASH_IP_PORT_IP,
	IPSET_XLATE_TYPE_HASH_IP_MARK,
	IPSET_XLATE_TYPE_HASH_IP_PORT,
	IPSET_XLATE_TYPE_BITMAP_PORT,
	IPSET_XLATE_TYPE_BITMAP_IP_MAC,
	IPSET_XLATE_TYPE_BITMAP_IP,
};

static enum ipset_xlate_set_type ipset_xlate_set_type(const char *typename)
{
	if (!strcmp(typename, "hash:mac"))
		return IPSET_XLATE_TYPE_HASH_MAC;
	else if (!strcmp(typename, "hash:ip"))
		return IPSET_XLATE_TYPE_HASH_IP;
	else if (!strcmp(typename, "hash:ip,mac"))
		return IPSET_XLATE_TYPE_HASH_IP_MAC;
	else if (!strcmp(typename, "hash:net,iface"))
		return IPSET_XLATE_TYPE_HASH_NET_IFACE;
	else if (!strcmp(typename, "hash:net,port"))
		return IPSET_XLATE_TYPE_HASH_NET_PORT;
	else if (!strcmp(typename, "hash:net,port,net"))
		return IPSET_XLATE_TYPE_HASH_NET_PORT_NET;
	else if (!strcmp(typename, "hash:net,net"))
		return IPSET_XLATE_TYPE_HASH_NET_NET;
	else if (!strcmp(typename, "hash:net"))
		return IPSET_XLATE_TYPE_HASH_NET;
	else if (!strcmp(typename, "hash:ip,port,net"))
		return IPSET_XLATE_TYPE_HASH_IP_PORT_NET;
	else if (!strcmp(typename, "hash:ip,port,ip"))
		return IPSET_XLATE_TYPE_HASH_IP_PORT_IP;
	else if (!strcmp(typename, "hash:ip,mark"))
		return IPSET_XLATE_TYPE_HASH_IP_MARK;
	else if (!strcmp(typename, "hash:ip,port"))
		return IPSET_XLATE_TYPE_HASH_IP_PORT;
	else if (!strcmp(typename, "hash:ip"))
		return IPSET_XLATE_TYPE_HASH_IP;
	else if (!strcmp(typename, "bitmap:port"))
		return IPSET_XLATE_TYPE_BITMAP_PORT;
	else if (!strcmp(typename, "bitmap:ip,mac"))
		return IPSET_XLATE_TYPE_BITMAP_IP_MAC;
	else if (!strcmp(typename, "bitmap:ip"))
		return IPSET_XLATE_TYPE_BITMAP_IP;

	return IPSET_XLATE_TYPE_UNKNOWN;
}

#define NFT_SET_INTERVAL	(1 << 0)

static const char *
ipset_xlate_type_to_nftables(int family, enum ipset_xlate_set_type type,
			     uint32_t *flags)
{
	switch (type) {
	case IPSET_XLATE_TYPE_HASH_MAC:
		return "ether_addr";
	case IPSET_XLATE_TYPE_HASH_IP:
		if (family == AF_INET)
			return "ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_IP_MAC:
		if (family == AF_INET)
			return "ipv4_addr . ether_addr";
		else if (family == AF_INET6)
			return "ipv6_addr . ether_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_NET_IFACE:
		*flags |= NFT_SET_INTERVAL;
		if (family == AF_INET)
			return "ipv4_addr . ifname";
		else if (family == AF_INET6)
			return "ipv6_addr . ifname";
		break;
	case IPSET_XLATE_TYPE_HASH_NET_PORT:
		*flags |= NFT_SET_INTERVAL;
		if (family == AF_INET)
			return "ipv4_addr . inet_proto . inet_service";
		else if (family == AF_INET6)
			return "ipv6_addr . inet_proto . inet_service";
		break;
	case IPSET_XLATE_TYPE_HASH_NET_PORT_NET:
		*flags |= NFT_SET_INTERVAL;
		if (family == AF_INET)
			return "ipv4_addr . inet_proto . inet_service . ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr . inet_proto . inet_service . ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_NET_NET:
		*flags |= NFT_SET_INTERVAL;
		if (family == AF_INET)
			return "ipv4_addr . ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr . ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_NET:
		*flags |= NFT_SET_INTERVAL;
		if (family == AF_INET)
			return "ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_IP_PORT_NET:
		*flags |= NFT_SET_INTERVAL;
		if (family == AF_INET)
			return "ipv4_addr . inet_proto . inet_service . ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr . inet_proto . inet_service . ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_IP_PORT_IP:
		if (family == AF_INET)
			return "ipv4_addr . inet_proto . inet_service . ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr . inet_proto . inet_service . ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_HASH_IP_MARK:
		if (family == AF_INET)
			return "ipv4_addr . mark";
		else if (family == AF_INET6)
			return "ipv6_addr . mark";
		break;
	case IPSET_XLATE_TYPE_HASH_IP_PORT:
		if (family == AF_INET)
			return "ipv4_addr . inet_proto . inet_service";
		else if (family == AF_INET6)
			return "ipv6_addr . inet_proto . inet_service";
		break;
	case IPSET_XLATE_TYPE_BITMAP_PORT:
		return "inet_service";
	case IPSET_XLATE_TYPE_BITMAP_IP_MAC:
		if (family == AF_INET)
			return "ipv4_addr . ether_addr";
		else if (family == AF_INET6)
			return "ipv6_addr . ether_addr";
		break;
	case IPSET_XLATE_TYPE_BITMAP_IP:
		if (family == AF_INET)
			return "ipv4_addr";
		else if (family == AF_INET6)
			return "ipv6_addr";
		break;
	case IPSET_XLATE_TYPE_UNKNOWN:
		break;
	default:
		break;
	}
	/* This should not ever happen. */
	return "unknown";
}

static int ipset_xlate(struct ipset *ipset, enum ipset_cmd cmd,
		       const char *table)
{
	const char *set, *typename, *nft_type;
	const struct ipset_type *ipset_type;
	struct ipset_xlate_set *xlate_set;
	enum ipset_xlate_set_type type;
	struct ipset_session *session;
	const uint32_t *cadt_flags;
	const uint32_t *timeout;
	const uint32_t *maxelem;
	struct ipset_data *data;
	const uint8_t *netmask;
	const char *comment;
	uint32_t flags = 0;
	uint8_t family;
	char buf[64];
	bool concat;
	char *term;

	session = ipset_session(ipset);
	data = ipset_session_data(session);

	set = ipset_data_get(data, IPSET_SETNAME);
	family = ipset_data_family(data);

	switch (cmd) {
	case IPSET_CMD_CREATE:
		/* Not supported. */
		if (ipset_data_test(data, IPSET_OPT_MARKMASK)) {
			printf("# %s", ipset->cmdline);
			break;
		}
		cadt_flags = ipset_data_get(data, IPSET_OPT_CADT_FLAGS);

		/* Ignore:
		 * - IPSET_FLAG_WITH_COMMENT
		 * - IPSET_FLAG_WITH_FORCEADD
		 */
		if (cadt_flags &&
		    (*cadt_flags & (IPSET_FLAG_BEFORE |
				   IPSET_FLAG_PHYSDEV |
				   IPSET_FLAG_NOMATCH |
				   IPSET_FLAG_WITH_SKBINFO |
				   IPSET_FLAG_IFACE_WILDCARD))) {
			printf("# %s", ipset->cmdline);
			break;
		}

		typename = ipset_data_get(data, IPSET_OPT_TYPENAME);
		type = ipset_xlate_set_type(typename);
		nft_type = ipset_xlate_type_to_nftables(family, type, &flags);

		printf("add set %s %s %s { type %s; ",
		       ipset_xlate_family(family), table, set, nft_type);
		if (cadt_flags) {
			if (*cadt_flags & IPSET_FLAG_WITH_COUNTERS)
				printf("counter; ");
		}
		timeout = ipset_data_get(data, IPSET_OPT_TIMEOUT);
		if (timeout)
			printf("timeout %us; ", *timeout);
		maxelem = ipset_data_get(data, IPSET_OPT_MAXELEM);
		if (maxelem)
			printf("size %u; ", *maxelem);

		netmask = ipset_data_get(data, IPSET_OPT_NETMASK);
		if (netmask &&
		    ((family == AF_INET && *netmask < 32) ||
		     (family == AF_INET6 && *netmask < 128)))
			flags |= NFT_SET_INTERVAL;

		if (flags & NFT_SET_INTERVAL)
			printf("flags interval; ");

		/* These create-specific options are safe to be ignored:
		 * - IPSET_OPT_GC
		 * - IPSET_OPT_HASHSIZE
		 * - IPSET_OPT_PROBES
		 * - IPSET_OPT_RESIZE
		 * - IPSET_OPT_SIZE
		 * - IPSET_OPT_FORCEADD
		 *
		 * Ranges and CIDR are safe to be ignored too:
		 * - IPSET_OPT_IP_FROM
		 * - IPSET_OPT_IP_TO
		 * - IPSET_OPT_PORT_FROM
		 * - IPSET_OPT_PORT_TO
		 */

		printf("}\n");

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

		snprintf(xlate_set->name, sizeof(xlate_set->name), "%s", set);
		ipset_type = ipset_types();
		while (ipset_type) {
			if (!strcmp(ipset_type->name, typename))
				break;
			ipset_type = ipset_type->next;
		}

		xlate_set->family = family;
		xlate_set->type = ipset_type;
		if (netmask) {
			xlate_set->netmask = *netmask;
			xlate_set->interval = true;
		}
		list_add_tail(&xlate_set->list, &ipset->xlate_sets);
		break;
	case IPSET_CMD_DESTROY:
		printf("del set %s %s %s\n",
		       ipset_xlate_family(family), table, set);
		break;
	case IPSET_CMD_FLUSH:
		if (!set) {
			printf("# %s", ipset->cmdline);
		} else {
			printf("flush set %s %s %s\n",
			       ipset_xlate_family(family), table, set);
		}
		break;
	case IPSET_CMD_RENAME:
		printf("# %s", ipset->cmdline);
		return -1;
	case IPSET_CMD_SWAP:
		printf("# %s", ipset->cmdline);
		return -1;
	case IPSET_CMD_LIST:
		if (!set) {
			printf("list sets %s %s\n",
			       ipset_xlate_family(family), table);
		} else {
			printf("list set %s %s %s\n",
			       ipset_xlate_family(family), table, set);
		}
		break;
	case IPSET_CMD_SAVE:
		printf("# %s", ipset->cmdline);
		return -1;
	case IPSET_CMD_ADD:
	case IPSET_CMD_DEL:
	case IPSET_CMD_TEST:
		/* Not supported. */
		if (ipset_data_test(data, IPSET_OPT_NOMATCH) ||
		    ipset_data_test(data, IPSET_OPT_SKBINFO) ||
		    ipset_data_test(data, IPSET_OPT_SKBMARK) ||
		    ipset_data_test(data, IPSET_OPT_SKBPRIO) ||
		    ipset_data_test(data, IPSET_OPT_SKBQUEUE) ||
		    ipset_data_test(data, IPSET_OPT_IFACE_WILDCARD)) {
			printf("# %s", ipset->cmdline);
			break;
		}
		printf("%s element %s %s %s { ",
		       cmd == IPSET_CMD_ADD ? "add" :
				cmd == IPSET_CMD_DEL ? "delete" : "get",
		       ipset_xlate_family(family), table, set);

		xlate_set = (struct ipset_xlate_set *)
				ipset_xlate_set_get(ipset, set);
		if (xlate_set && xlate_set->interval)
			netmask = &xlate_set->netmask;
		else
			netmask = NULL;

		concat = false;
		if (ipset_data_test(data, IPSET_OPT_IP)) {
			ipset_print_data(buf, sizeof(buf), data, IPSET_OPT_IP, 0);
			printf("%s", buf);
			if (netmask)
				printf("/%u ", *netmask);
			else
				printf(" ");

			concat = true;
		}
		if (ipset_data_test(data, IPSET_OPT_MARK)) {
			ipset_print_mark(buf, sizeof(buf), data, IPSET_OPT_MARK, 0);
			printf("%s%s ", concat ? ". " : "", buf);
		}
		if (ipset_data_test(data, IPSET_OPT_IFACE)) {
			ipset_print_data(buf, sizeof(buf), data, IPSET_OPT_IFACE, 0);
			printf("%s%s ", concat ? ". " : "", buf);
		}
		if (ipset_data_test(data, IPSET_OPT_ETHER)) {
			ipset_print_ether(buf, sizeof(buf), data, IPSET_OPT_ETHER, 0);
			size_t i;

			for (i = 0; i < strlen(buf); i++)
				buf[i] = tolower(buf[i]);

			printf("%s%s ", concat ? ". " : "", buf);
			concat = true;
		}
		if (ipset_data_test(data, IPSET_OPT_PORT)) {
			ipset_print_proto_port(buf, sizeof(buf), data, IPSET_OPT_PORT, 0);
			term = strchr(buf, ':');
			if (term) {
				*term = '\0';
				printf("%s%s ", concat ? ". " : "", buf);
			}
			ipset_print_data(buf, sizeof(buf), data, IPSET_OPT_PORT, 0);
			printf("%s%s ", concat ? ". " : "", buf);
		}
		if (ipset_data_test(data, IPSET_OPT_IP2)) {
			ipset_print_ip(buf, sizeof(buf), data, IPSET_OPT_IP2, 0);
			printf("%s%s", concat ? ". " : "", buf);
			if (netmask)
				printf("/%u ", *netmask);
			else
				printf(" ");
		}
		if (ipset_data_test(data, IPSET_OPT_PACKETS) &&
		    ipset_data_test(data, IPSET_OPT_BYTES)) {
			const uint64_t *pkts, *bytes;

			pkts = ipset_data_get(data, IPSET_OPT_PACKETS);
			bytes = ipset_data_get(data, IPSET_OPT_BYTES);

			printf("counter packets %" PRIu64 " bytes %" PRIu64 " ",
			       *pkts, *bytes);
		}
		timeout = ipset_data_get(data, IPSET_OPT_TIMEOUT);
		if (timeout)
			printf("timeout %us ", *timeout);

		comment = ipset_data_get(data, IPSET_OPT_ADT_COMMENT);
		if (comment)
			printf("comment \"%s\" ", comment);

		printf("}\n");
		break;
	case IPSET_CMD_GET_BYNAME:
		printf("# %s", ipset->cmdline);
		return -1;
	case IPSET_CMD_GET_BYINDEX:
		printf("# %s", ipset->cmdline);
		return -1;
	default:
		break;
	}

	return 0;
}

static int ipset_xlate_restore(struct ipset *ipset)
{
	struct ipset_session *session = ipset_session(ipset);
	struct ipset_data *data = ipset_session_data(session);
	void *p = ipset_session_printf_private(session);
	enum ipset_cmd cmd;
	FILE *f = stdin;
	int ret = 0;
	char *c;

	if (ipset->filename) {
		f = fopen(ipset->filename, "r");
		if (!f) {
			fprintf(stderr, "cannot open file `%s'\n", ipset->filename);
			return -1;
		}
	}

	/* TODO: Allow to specify the table name other than 'global'. */
	printf("add table inet global\n");

	while (fgets(ipset->cmdline, sizeof(ipset->cmdline), f)) {
		ipset->restore_line++;
		c = ipset->cmdline;
		while (isspace(c[0]))
			c++;
		if (c[0] == '\0' || c[0] == '#')
			continue;
		else if (STREQ(c, "COMMIT\n") || STREQ(c, "COMMIT\r\n"))
			continue;

		ret = build_argv(ipset, c);
		if (ret < 0)
			break;

		cmd = ipset_parser(ipset, ipset->newargc, ipset->newargv);
		if (cmd < 0)
			ipset->standard_error(ipset, p);

		/* TODO: Allow to specify the table name other than 'global'. */
		ret = ipset_xlate(ipset, cmd, "global");
		if (ret < 0)
			break;

		ipset_data_reset(data);
	}

	if (ipset->filename)
		fclose(f);

	return ret;
}

int ipset_xlate_argv(struct ipset *ipset, int argc, char *argv[])
{
	enum ipset_cmd cmd;
	int ret;

	ipset->xlate = true;

	cmd = ipset_parser(ipset, argc, argv);
	if (cmd < 0)
		return cmd;

	if (cmd == IPSET_CMD_RESTORE) {
		ret = ipset_xlate_restore(ipset);
	} else {
		fprintf(stderr, "This command is not supported, "
				"use `ipset-translate restore < file'\n");
		ret = -1;
	}

	return ret;
}
