// SPDX-License-Identifier: GPL-2.0+
/*
 * ipmi_si_hotmod.c
 *
 * Handling for dynamically adding/removing IPMI devices through
 * a module parameter (and thus sysfs).
 */

#define pr_fmt(fmt) "ipmi_hotmod: " fmt

#include <linux/moduleparam.h>
#include <linux/ipmi.h>
#include <linux/atomic.h>
#include "ipmi_si.h"
#include "ipmi_plat_data.h"

static int hotmod_handler(const char *val, const struct kernel_param *kp);

module_param_call(hotmod, hotmod_handler, NULL, NULL, 0200);
MODULE_PARM_DESC(hotmod,
		 "Add and remove interfaces.  See Documentation/driver-api/ipmi.rst in the kernel sources for the gory details.");

/*
 * Parms come in as <op1>[:op2[:op3...]].  ops are:
 *   add|remove,kcs|bt|smic,mem|i/o,<address>[,<opt1>[,<opt2>[,...]]]
 * Options are:
 *   rsp=<regspacing>
 *   rsi=<regsize>
 *   rsh=<regshift>
 *   irq=<irq>
 *   ipmb=<ipmb addr>
 */
enum hotmod_op { HM_ADD, HM_REMOVE };
struct hotmod_vals {
	const char *name;
	const int  val;
};

static const struct hotmod_vals hotmod_ops[] = {
	{ "add",	HM_ADD },
	{ "remove",	HM_REMOVE },
	{ NULL }
};

static const struct hotmod_vals hotmod_si[] = {
	{ "kcs",	SI_KCS },
	{ "smic",	SI_SMIC },
	{ "bt",		SI_BT },
	{ NULL }
};

static const struct hotmod_vals hotmod_as[] = {
	{ "mem",	IPMI_MEM_ADDR_SPACE },
	{ "i/o",	IPMI_IO_ADDR_SPACE },
	{ NULL }
};

static int parse_str(const struct hotmod_vals *v, unsigned int *val, char *name,
		     const char **curr)
{
	char *s;
	int  i;

	s = strchr(*curr, ',');
	if (!s) {
		pr_warn("No hotmod %s given\n", name);
		return -EINVAL;
	}
	*s = '\0';
	s++;
	for (i = 0; v[i].name; i++) {
		if (strcmp(*curr, v[i].name) == 0) {
			*val = v[i].val;
			*curr = s;
			return 0;
		}
	}

	pr_warn("Invalid hotmod %s '%s'\n", name, *curr);
	return -EINVAL;
}

static int check_hotmod_int_op(const char *curr, const char *option,
			       const char *name, unsigned int *val)
{
	char *n;

	if (strcmp(curr, name) == 0) {
		if (!option) {
			pr_warn("No option given for '%s'\n", curr);
			return -EINVAL;
		}
		*val = simple_strtoul(option, &n, 0);
		if ((*n != '\0') || (*option == '\0')) {
			pr_warn("Bad option given for '%s'\n", curr);
			return -EINVAL;
		}
		return 1;
	}
	return 0;
}

static int parse_hotmod_str(const char *curr, enum hotmod_op *op,
			    struct ipmi_plat_data *h)
{
	char *s, *o;
	int rv;
	unsigned int ival;

	h->iftype = IPMI_PLAT_IF_SI;
	rv = parse_str(hotmod_ops, &ival, "operation", &curr);
	if (rv)
		return rv;
	*op = ival;

	rv = parse_str(hotmod_si, &ival, "interface type", &curr);
	if (rv)
		return rv;
	h->type = ival;

	rv = parse_str(hotmod_as, &ival, "address space", &curr);
	if (rv)
		return rv;
	h->space = ival;

	s = strchr(curr, ',');
	if (s) {
		*s = '\0';
		s++;
	}
	rv = kstrtoul(curr, 0, &h->addr);
	if (rv) {
		pr_warn("Invalid hotmod address '%s': %d\n", curr, rv);
		return rv;
	}

	while (s) {
		curr = s;
		s = strchr(curr, ',');
		if (s) {
			*s = '\0';
			s++;
		}
		o = strchr(curr, '=');
		if (o) {
			*o = '\0';
			o++;
		}
		rv = check_hotmod_int_op(curr, o, "rsp", &h->regspacing);
		if (rv < 0)
			return rv;
		else if (rv)
			continue;
		rv = check_hotmod_int_op(curr, o, "rsi", &h->regsize);
		if (rv < 0)
			return rv;
		else if (rv)
			continue;
		rv = check_hotmod_int_op(curr, o, "rsh", &h->regshift);
		if (rv < 0)
			return rv;
		else if (rv)
			continue;
		rv = check_hotmod_int_op(curr, o, "irq", &h->irq);
		if (rv < 0)
			return rv;
		else if (rv)
			continue;
		rv = check_hotmod_int_op(curr, o, "ipmb", &h->slave_addr);
		if (rv < 0)
			return rv;
		else if (rv)
			continue;

		pr_warn("Invalid hotmod option '%s'\n", curr);
		return -EINVAL;
	}

	h->addr_source = SI_HOTMOD;
	return 0;
}

static atomic_t hotmod_nr;

static int hotmod_handler(const char *val, const struct kernel_param *kp)
{
	int  rv;
	struct ipmi_plat_data h;
	char *str, *curr, *next;

	str = kstrdup(val, GFP_KERNEL);
	if (!str)
		return -ENOMEM;

	/* Kill any trailing spaces, as we can get a "\n" from echo. */
	for (curr = strstrip(str); curr; curr = next) {
		enum hotmod_op op;

		next = strchr(curr, ':');
		if (next) {
			*next = '\0';
			next++;
		}

		memset(&h, 0, sizeof(h));
		rv = parse_hotmod_str(curr, &op, &h);
		if (rv)
			goto out;

		if (op == HM_ADD) {
			ipmi_platform_add("hotmod-ipmi-si",
					  atomic_inc_return(&hotmod_nr),
					  &h);
		} else {
			struct device *dev;

			dev = ipmi_si_remove_by_data(h.space, h.type, h.addr);
			if (dev && dev_is_platform(dev)) {
				struct platform_device *pdev;

				pdev = to_platform_device(dev);
				if (strcmp(pdev->name, "hotmod-ipmi-si") == 0)
					platform_device_unregister(pdev);
			}
			put_device(dev);
		}
	}
	rv = strlen(val);
out:
	kfree(str);
	return rv;
}

void ipmi_si_hotmod_exit(void)
{
	ipmi_remove_platform_device_by_name("hotmod-ipmi-si");
}