root/drivers/char/ipmi/ipmi_si_hotmod.c
// 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");
}