// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2021 MediaTek Inc.
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/list.h>
#include <linux/device.h>
#include <linux/slab.h>

#include <linux/rpmb.h>
#include "rpmb-cdev.h"

static DEFINE_IDA(rpmb_ida);

/**
 * rpmb_dev_get - increase rpmb device ref counter
 *
 * @rdev: rpmb device
 */
struct rpmb_dev *rpmb_dev_get(struct rpmb_dev *rdev)
{
	return get_device(&rdev->dev) ? rdev : NULL;
}
EXPORT_SYMBOL_GPL(rpmb_dev_get);

/**
 * rpmb_dev_put - decrease rpmb device ref counter
 *
 * @rdev: rpmb device
 */
void rpmb_dev_put(struct rpmb_dev *rdev)
{
	put_device(&rdev->dev);
}
EXPORT_SYMBOL_GPL(rpmb_dev_put);

static int rpmb_request_verify(struct rpmb_dev *rdev, struct rpmb_data *rpmbd)
{
	u16 req_type, block_count, addr;
	u32 wc;
	struct rpmb_cmd *in_cmd = &rpmbd->icmd;
	struct rpmb_cmd *out_cmd = &rpmbd->ocmd;

	if (!in_cmd->frames || !in_cmd->nframes ||
	    !out_cmd->frames || !out_cmd->nframes)
		return -EINVAL;

	req_type = be16_to_cpu(in_cmd->frames[0].req_resp);
	block_count = be16_to_cpu(in_cmd->frames[0].block_count);
	addr = be16_to_cpu(in_cmd->frames[0].addr);
	wc = be32_to_cpu(in_cmd->frames[0].write_counter);

	if (rpmbd->req_type != req_type) {
		dev_notice(&rdev->dev, "rpmb req type doesn't match 0x%04X = 0x%04X\n",
			req_type, rpmbd->req_type);
		return -EINVAL;
	}

	switch (req_type) {
	case RPMB_PROGRAM_KEY:
		dev_dbg(&rdev->dev, "rpmb program key = 0x%1x blk = %d\n",
			req_type, block_count);
		break;
	case RPMB_GET_WRITE_COUNTER:
		dev_dbg(&rdev->dev, "rpmb get write counter = 0x%1x blk = %d\n",
			req_type, block_count);

		break;
	case RPMB_WRITE_DATA:
		dev_dbg(&rdev->dev, "rpmb write data = 0x%1x blk = %d addr = %d, wc = %d (0x%x)\n",
			req_type, block_count, addr, wc, wc);

		if (rdev->ops->reliable_wr_cnt &&
		    block_count > rdev->ops->reliable_wr_cnt) {
			dev_notice(&rdev->dev, "rpmb write data: block count %u > reliable wr count %u\n",
				block_count, rdev->ops->reliable_wr_cnt);
			return -EINVAL;
		}

		if (block_count > in_cmd->nframes) {
			dev_notice(&rdev->dev, "rpmb write data: block count %u > in frame count %u\n",
				block_count, in_cmd->nframes);
			return -EINVAL;
		}
		break;
	case RPMB_READ_DATA:
		dev_dbg(&rdev->dev, "rpmb read data = 0x%1x blk = %d addr = %d\n",
			req_type, block_count, addr);

		if (block_count > out_cmd->nframes) {
			dev_notice(&rdev->dev, "rpmb read data: block count %u > out frame count %u\n",
				block_count, out_cmd->nframes);
			return -EINVAL;
		}
		break;
	case RPMB_RESULT_READ:
		/* Internal command not supported */
		dev_notice(&rdev->dev, "NOTSUPPORTED rpmb resut read = 0x%1x blk = %d\n",
			req_type, block_count);
		return -EOPNOTSUPP;

	default:
		dev_notice(&rdev->dev, "Error rpmb invalid command = 0x%1x blk = %d\n",
			req_type, block_count);
		return -EINVAL;
	}

	return 0;
}

/**
 * rpmb_cmd_fixup - fixup rpmb command
 *
 * @rdev: rpmb device
 * @cmds: rpmb command list
 * @ncmds: number of commands
 *
 */
static void rpmb_cmd_fixup(struct rpmb_dev *rdev,
			   struct rpmb_cmd *cmds, u32 ncmds)
{
	int i;

	if (rdev->ops->type != RPMB_TYPE_EMMC)
		return;

	/* Fixup RPMB_READ_DATA specific to eMMC
	 * The block count of the RPMB read operation is not indicated
	 * in the original RPMB Data Read Request packet.
	 * This is different then implementation for other protocol
	 * standards.
	 */
	for (i = 0; i < ncmds; i++)
		if (cmds->frames->req_resp == cpu_to_be16(RPMB_READ_DATA)) {
			dev_dbg(&rdev->dev, "Fixing up READ_DATA frame to block_count=0\n");
			cmds->frames->block_count = 0;
		}
}

/**
 * rpmb_cmd_seq - send RPMB command sequence
 *
 * @rdev: rpmb device
 * @cmds: rpmb command list
 * @ncmds: number of commands
 *
 * Return: 0 on success
 *         -EINVAL on wrong parameters
 *         -EOPNOTSUPP if device doesn't support the requested operation
 *         < 0 if the operation fails
 */
int rpmb_cmd_seq(struct rpmb_dev *rdev, struct rpmb_cmd *cmds, u32 ncmds,
	u8 region)
{
	int err;

	if (!rdev || !cmds || !ncmds)
		return -EINVAL;

	mutex_lock(&rdev->lock);
	err = -EOPNOTSUPP;
	if (rdev->ops && rdev->ops->cmd_seq) {
		rpmb_cmd_fixup(rdev, cmds, ncmds);
		err = rdev->ops->cmd_seq(rdev->dev.parent, cmds, ncmds, region);
	}
	mutex_unlock(&rdev->lock);
	return err;
}
EXPORT_SYMBOL_GPL(rpmb_cmd_seq);

static void rpmb_cmd_set(struct rpmb_cmd *cmd, u32 flags,
			 struct rpmb_frame *frames, u32 nframes)
{
	cmd->flags = flags;
	cmd->frames = frames;
	cmd->nframes = nframes;
}
#ifdef RPMB_DEBUG
static void rpmb_dump_frame(u8 *data_frame)
{
	pr_notice("mac, frame[196] = 0x%x\n", data_frame[196]);
	pr_notice("mac, frame[197] = 0x%x\n", data_frame[197]);
	pr_notice("mac, frame[198] = 0x%x\n", data_frame[198]);
	pr_notice("data,frame[228] = 0x%x\n", data_frame[228]);
	pr_notice("data,frame[229] = 0x%x\n", data_frame[229]);
	pr_notice("nonce, frame[484] = 0x%x\n", data_frame[484]);
	pr_notice("nonce, frame[485] = 0x%x\n", data_frame[485]);
	pr_notice("nonce, frame[486] = 0x%x\n", data_frame[486]);
	pr_notice("nonce, frame[487] = 0x%x\n", data_frame[487]);
	pr_notice("wc, frame[500] = 0x%x\n", data_frame[500]);
	pr_notice("wc, frame[501] = 0x%x\n", data_frame[501]);
	pr_notice("wc, frame[502] = 0x%x\n", data_frame[502]);
	pr_notice("wc, frame[503] = 0x%x\n", data_frame[503]);
	pr_notice("addr, frame[504] = 0x%x\n", data_frame[504]);
	pr_notice("addr, frame[505] = 0x%x\n", data_frame[505]);
	pr_notice("blkcnt,frame[506] = 0x%x\n", data_frame[506]);
	pr_notice("blkcnt,frame[507] = 0x%x\n", data_frame[507]);
	pr_notice("result, frame[508] = 0x%x\n", data_frame[508]);
	pr_notice("result, frame[509] = 0x%x\n", data_frame[509]);
	pr_notice("type, frame[510] = 0x%x\n", data_frame[510]);
	pr_notice("type, frame[511] = 0x%x\n", data_frame[511]);
}
#endif
/**
 * rpmb_cmd_req - send rpmb request command
 *
 * @rdev: rpmb device
 * @rpmbd: rpmb request data
 *
 * Return: 0 on success
 *         -EINVAL on wrong parameters
 *         -EOPNOTSUPP if device doesn't support the requested operation
 *         < 0 if the operation fails
 */
int rpmb_cmd_req(struct rpmb_dev *rdev, struct rpmb_data *rpmbd, u8 region)
{
	struct rpmb_cmd cmd[3];
	struct rpmb_frame *res_frame;
	u32 cnt_in, cnt_out;
	u32 ncmds;
	u16 type;
	int ret;

	if (!rdev || !rpmbd) {
		pr_notice("[RPMB] %s, -EINVAL in line %d!\n",
				__func__, __LINE__);
		return -EINVAL;
	}

	ret = rpmb_request_verify(rdev, rpmbd);
	if (ret)
		return ret;

	if (!rdev->ops || !rdev->ops->cmd_seq)
		return -EOPNOTSUPP;
#ifdef RPMB_DEBUG
	rpmb_dump_frame((u8 *)(rpmbd->icmd.frames));
#endif
	cnt_in = rpmbd->icmd.nframes;
	cnt_out = rpmbd->ocmd.nframes;
	type = rpmbd->req_type;
	switch (type) {
	case RPMB_PROGRAM_KEY:
		cnt_in = 1;
		cnt_out = 1;
		fallthrough;
	case RPMB_WRITE_DATA:
		rpmb_cmd_set(&cmd[0], RPMB_F_WRITE | RPMB_F_REL_WRITE,
			     rpmbd->icmd.frames, cnt_in);

		res_frame = rpmbd->ocmd.frames;
		memset(res_frame, 0, sizeof(*res_frame));
		res_frame->req_resp = cpu_to_be16(RPMB_RESULT_READ);
		rpmb_cmd_set(&cmd[1], RPMB_F_WRITE, res_frame, 1);

		rpmb_cmd_set(&cmd[2], 0, rpmbd->ocmd.frames, cnt_out);
		ncmds = 3;
		break;
	case RPMB_GET_WRITE_COUNTER:
		cnt_in = 1;
		cnt_out = 1;
		fallthrough;
	case RPMB_READ_DATA:
		rpmb_cmd_set(&cmd[0], RPMB_F_WRITE, rpmbd->icmd.frames, cnt_in);
		rpmb_cmd_set(&cmd[1], 0, rpmbd->ocmd.frames, cnt_out);
		ncmds = 2;
		break;
	default:
		pr_notice("[RPMB] %s, -EINVAL in line %d!\n",
				__func__, __LINE__);
		return -EINVAL;
	}

	mutex_lock(&rdev->lock);
	ret = rdev->ops->cmd_seq(rdev->dev.parent, cmd, ncmds, region);
	mutex_unlock(&rdev->lock);
#ifdef RPMB_DEBUG
	rpmb_dump_frame((u8 *)(rpmbd->ocmd.frames));
#endif
	return ret;
}
EXPORT_SYMBOL_GPL(rpmb_cmd_req);

u16 rpmb_get_rw_size(struct rpmb_dev *rdev)
{
	return rdev->ops->reliable_wr_cnt;
}
EXPORT_SYMBOL_GPL(rpmb_get_rw_size);

static void rpmb_dev_release(struct device *dev)
{
	struct rpmb_dev *rdev = to_rpmb_dev(dev);

	ida_simple_remove(&rpmb_ida, rdev->id);
	kfree(rdev);
}

struct class rpmb_class = {
#if IS_ENABLED(CONFIG_ARCH_MEDIATEK)
	.name = "rpmb_dummy",
#else
	.name = "rpmb",
#endif
	.owner = THIS_MODULE,
	.dev_release = rpmb_dev_release,
};
EXPORT_SYMBOL(rpmb_class);

/**
 * rpmb_dev_find_device - return first matching rpmb device
 *
 * @data: data for the match function
 * @match: the matching function
 *
 * Return: matching rpmb device or NULL on failure
 */
struct rpmb_dev *rpmb_dev_find_device(void *data,
		     int (*match)(struct device *dev, const void *data))
{
	struct device *dev;

	dev = class_find_device(&rpmb_class, NULL, data, match);

	return dev ? to_rpmb_dev(dev) : NULL;
}
EXPORT_SYMBOL_GPL(rpmb_dev_find_device);

static int match_by_type(struct device *dev, const void *data)
{
	struct rpmb_dev *rdev = to_rpmb_dev(dev);
	enum rpmb_type *type = (enum rpmb_type *)data;

	return (*type == RPMB_TYPE_ANY || rdev->ops->type == *type);
}

/**
 * rpmb_dev_get_by_type - return first registered rpmb device
 *      with matching type.
 *      If run with RPMB_TYPE_ANY the first an probably only
 *      device is returned
 *
 * @type: rpbm underlying device type
 *
 * Return: matching rpmb device or NULL/ERR_PTR on failure
 */
struct rpmb_dev *rpmb_dev_get_by_type(enum rpmb_type type)
{
	if (type > RPMB_TYPE_MAX)
		return ERR_PTR(-EINVAL);

	return rpmb_dev_find_device(&type, match_by_type);
}
EXPORT_SYMBOL_GPL(rpmb_dev_get_by_type);

static int match_by_parent(struct device *dev, const void *data)
{
	const struct device *parent = data;

	return (parent && dev->parent == parent);
}

/**
 * rpmb_dev_find_by_device - retrieve rpmb device from the parent device
 *
 * @parent: parent device of the rpmb device
 *
 * Return: NULL if there is no rpmb device associated with the parent device
 */
struct rpmb_dev *rpmb_dev_find_by_device(struct device *parent)
{
	if (!parent)
		return NULL;

	return rpmb_dev_find_device(parent, match_by_parent);
}
EXPORT_SYMBOL_GPL(rpmb_dev_find_by_device);

static ssize_t type_show(struct device *dev,
			 struct device_attribute *attr, char *buf)
{
	struct rpmb_dev *rdev = to_rpmb_dev(dev);
	ssize_t ret;

	switch (rdev->ops->type) {
	case RPMB_TYPE_EMMC:
		ret = sprintf(buf, "EMMC\n");
		break;
	case RPMB_TYPE_UFS:
		ret = sprintf(buf, "UFS\n");
		break;
	case RPMB_TYPE_SIM:
		ret = sprintf(buf, "SIM\n");
		break;
	default:
		ret = sprintf(buf, "UNKNOWN\n");
		break;
	}

	return ret;
}
static DEVICE_ATTR_RO(type);

static ssize_t id_read(struct file *file, struct kobject *kobj,
		       struct bin_attribute *attr, char *buf,
		       loff_t off, size_t count)
{
	struct device *dev = kobj_to_dev(kobj);
	struct rpmb_dev *rdev = to_rpmb_dev(dev);
	size_t sz = min_t(size_t, rdev->ops->dev_id_len, PAGE_SIZE);

	if (!rdev->ops->dev_id)
		return 0;

	return memory_read_from_buffer(buf, count, &off, rdev->ops->dev_id, sz);
}
static BIN_ATTR_RO(id, 0);

static ssize_t reliable_wr_cnt_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	struct rpmb_dev *rdev = to_rpmb_dev(dev);

	return sprintf(buf, "%u\n", rdev->ops->reliable_wr_cnt);
}
static DEVICE_ATTR_RO(reliable_wr_cnt);

static struct attribute *rpmb_attrs[] = {
	&dev_attr_type.attr,
	&dev_attr_reliable_wr_cnt.attr,
	NULL,
};

static struct bin_attribute *rpmb_bin_attributes[] = {
	&bin_attr_id,
	NULL,
};

static struct attribute_group rpmb_attr_group = {
	.attrs = rpmb_attrs,
	.bin_attrs = rpmb_bin_attributes,
};

static const struct attribute_group *rpmb_attr_groups[] = {
	&rpmb_attr_group,
	NULL
};

/**
 * rpmb_dev_unregister - unregister RPMB partition from the RPMB subsystem
 *
 * @dev: parent device of the rpmb device
 */
int rpmb_dev_unregister(struct device *dev)
{
	struct rpmb_dev *rdev;

	if (!dev)
		return -EINVAL;

	rdev = rpmb_dev_find_by_device(dev);
	if (!rdev) {
		dev_notice(dev, "no disk found %s\n", dev_name(dev->parent));
		return -ENODEV;
	}

	rpmb_dev_put(rdev);

	mutex_lock(&rdev->lock);
	rpmb_cdev_del(rdev);
	device_del(&rdev->dev);
	mutex_unlock(&rdev->lock);

	rpmb_dev_put(rdev);

	return 0;
}
EXPORT_SYMBOL_GPL(rpmb_dev_unregister);

/**
 * rpmb_dev_register - register RPMB partition with the RPMB subsystem
 *
 * @dev: storage device of the rpmb device
 * @ops: device specific operations
 */
struct rpmb_dev *rpmb_dev_register(struct device *dev,
				   const struct rpmb_ops *ops)
{
	struct rpmb_dev *rdev;
	int id;
	int ret;

	if (!dev || !ops)
		return ERR_PTR(-EINVAL);

	if (!ops->cmd_seq)
		return ERR_PTR(-EINVAL);

	if (ops->type == RPMB_TYPE_ANY || ops->type > RPMB_TYPE_MAX)
		return ERR_PTR(-EINVAL);

	rdev = kzalloc(sizeof(*rdev), GFP_KERNEL);
	if (!rdev)
		return ERR_PTR(-ENOMEM);

	id = ida_simple_get(&rpmb_ida, 0, 0, GFP_KERNEL);
	if (id < 0) {
		ret = id;
		goto exit;
	}

	mutex_init(&rdev->lock);
	rdev->ops = ops;
	rdev->id = id;

#if IS_ENABLED(CONFIG_ARCH_MEDIATEK)
	dev_set_name(&rdev->dev, "rpmb_dummy%d", id);
#else
	dev_set_name(&rdev->dev, "rpmb%d", id);
#endif
	rdev->dev.class = &rpmb_class;
	rdev->dev.parent = dev;
	rdev->dev.groups = rpmb_attr_groups;

	rpmb_cdev_prepare(rdev);

	ret = device_register(&rdev->dev);
	if (ret)
		goto exit;

	rpmb_cdev_add(rdev);

	dev_dbg(&rdev->dev, "registered disk\n");

	return rdev;

exit:
	if (id >= 0)
		ida_simple_remove(&rpmb_ida, id);
	kfree(rdev);
	return ERR_PTR(ret);
}
EXPORT_SYMBOL_GPL(rpmb_dev_register);

static int __init rpmb_init(void)
{
	ida_init(&rpmb_ida);
	class_register(&rpmb_class);
	return rpmb_cdev_init();
}

static void __exit rpmb_exit(void)
{
	rpmb_cdev_exit();
	class_unregister(&rpmb_class);
	ida_destroy(&rpmb_ida);
}

subsys_initcall(rpmb_init);
module_exit(rpmb_exit);

MODULE_AUTHOR("Intel Corporation");
MODULE_DESCRIPTION("RPMB class");
MODULE_LICENSE("GPL");
