// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2020 Samsung Electronics Co., Ltd.
 *      http://www.samsung.com
 *
 * Samsung TN debugging code
 *
 */

#include <linux/of.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/ctype.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>
#include <linux/string.h>
#include <linux/proc_fs.h>
#include <linux/blkdev.h>
#include <linux/uio.h>
#include <linux/vmalloc.h>
#include <linux/mount.h>
#include <linux/sec_debug.h>
#include "sec_debug_internal.h"

/* "D9" for all DEBUG partition magic */
#define DPRT_HEADER_MAGIC	(0xD9A70214)
#define DPRT_HEADER_VERSION	(0xD9220214)

/* common header structure for DEBUG partition */
struct dprt_header {
		unsigned int dummy;
		unsigned int magic;
		unsigned int version;
		unsigned int start_offset;
		unsigned int nr_max_item;
		unsigned int per_item_size;
		unsigned int last_item_index;
		unsigned long reserved[8];
		unsigned long private[4];
};

#define DPRT_MAX_LEN_MEMBER_NAME		(16)
#define DPRT_MAX_MEMBERS				(32)

#define DPRT_BORE_DUMP_SIZE				0x7FE00
#define DRPT_DHST_DUMP_SIZE				(SZ_1M + DPRT_BORE_DUMP_SIZE)
#define SDGD_EACH_DUMP_SIZE				(SZ_1M * 2)

/* 16 members -> 1 BLOCK */
struct dprt_member {
		unsigned int offset;
		unsigned int size;
		unsigned int erase;
		unsigned long private;
		char name[DPRT_MAX_LEN_MEMBER_NAME];
};

enum data_member {
	SDGD_AUTO_DUMP,
	SDGD_SUMM_DUMP,
	SDGD_KERN_DUMP,
	NR_DATA_MEMBER
};

static struct block_device *sdg_bdev;
static const char *bdev_path;
static struct dprt_header *part_header;
static struct dprt_member members[DPRT_MAX_MEMBERS];
static int num_member_items;
static int sdg_data_this_offset;

static ssize_t sdg_part_read(void *buf, size_t bytes, loff_t pos)
{
	struct block_device *bdev;
	struct file file;
	struct kiocb kiocb;
	struct iov_iter iter;
	struct kvec iov = {.iov_base = buf, .iov_len = bytes};

	bdev = sdg_bdev;

	pr_debug("%s: start\n", __func__);

	memset(&file, 0, sizeof(struct file));
	file.f_mapping = bdev->bd_inode->i_mapping;
	file.f_flags = O_DSYNC | __O_SYNC | O_NOATIME;
	file.f_inode = bdev->bd_inode;
	file_ra_state_init(&file.f_ra, file.f_mapping);

	init_sync_kiocb(&kiocb, &file);
	kiocb.ki_pos = pos;
	iov_iter_kvec(&iter, READ, &iov, 1, bytes);

	pr_debug("%s: end\n", __func__);

	return generic_file_read_iter(&kiocb, &iter);
}

static int sdg_part_load_header(void)
{
	struct dprt_header *ph;
	char *header_buf = NULL;
	int size;

	size = sizeof(struct dprt_header);

	header_buf = vmalloc(size);
	if (!header_buf)
		return -ENOMEM;

	pr_debug("%s: header buf: %p\n", __func__, header_buf);

	memset(header_buf, 0, size);

	pr_debug("%s: offset %x, size %x\n", __func__, 0x0, size);
	if (!sdg_part_read(header_buf, size, 0x0)) {
		pr_err("%s: fail to read debug members\n", __func__);
		return -1;
	}

	part_header = (struct dprt_header *) header_buf;
	ph = part_header;

	pr_info("%s: magic = %x / version = %x / member_offset = %lx\n",
				__func__, ph->magic, ph->version, ph->private[0]);

	if (ph->magic != DPRT_HEADER_MAGIC || ph->version != DPRT_HEADER_VERSION) {
		pr_info("%s: header mismatch: magic = %x (expected: %x) / version = %x (expected: %x)\n",
			__func__, ph->magic, DPRT_HEADER_MAGIC, ph->version, DPRT_HEADER_VERSION);
		return -1;
	}

	return 0;
}

static int sdg_part_load_data_header(struct dprt_member *dm)
{
	struct dprt_header *dh;
	char *header_buf = NULL;
	int sdg_data_offset;
	int size;

	sdg_data_offset = dm->offset;
	pr_debug("sdg data offset: %x\n", sdg_data_offset);

	if (!sdg_data_offset) {
		pr_err("data section is not initialized\n");
		return -1;
	}

	size = sizeof(struct dprt_header);

	header_buf = vmalloc(size);
	if (!header_buf)
		return -ENOMEM;

	pr_debug("%s: header buf: %p\n", __func__, header_buf);

	memset(header_buf, 0, size);

	pr_info("%s: offset %x, size %x\n", __func__, sdg_data_offset, size);
	if (!sdg_part_read(header_buf, size, sdg_data_offset)) {
		pr_err("%s: fail to read data member head\n", __func__);
		return -1;
	}

	dh = (struct dprt_header *) header_buf;
	pr_info("%s: start_offset = %x / per_item_size = %d/ last_item_index = %d\n",
				__func__, dh->start_offset, dh->per_item_size, dh->last_item_index);

	sdg_data_this_offset = dh->start_offset + dh->per_item_size * dh->last_item_index;

	return 0;
}

static int sdg_part_load_members(void)
{
	struct dprt_header *ph;
	struct dprt_member *p;
	unsigned long member_offset;
	char *member_buf = NULL;
	int i, size;

	ph = part_header;
	num_member_items = (int) ph->nr_max_item;

	/* Set Members offset & size*/
	member_offset = ph->private[0];
	size = num_member_items * sizeof(struct dprt_member);

	member_buf = vmalloc(size);
	if (!member_buf)
		return -ENOMEM;

	pr_debug("%s: member buf: %p\n", __func__, member_buf);

	memset(member_buf, 0, size);

	pr_info("%s: member buf: offset 0x%lx / size 0x%x\n", __func__, member_offset, size);
	if (!sdg_part_read(member_buf, size, member_offset)) {
		pr_err("%s: fail to read debug members\n", __func__);
		goto fail;
	}

	p = (struct dprt_member *)member_buf;
	for (i = 0; i < num_member_items; i++) {
		members[i].offset = p[i].offset;
		members[i].size = p[i].size;
		members[i].erase = p[i].erase;
		members[i].private = p[i].private;
		strncpy(members[i].name, p[i].name, DPRT_MAX_LEN_MEMBER_NAME);
	}

	vfree(member_buf);
	return 0;

fail:
	pr_err("fail to setting members\n");
	vfree(member_buf);
	return -1;
}

static int init_sdg_bdev(void)
{
	struct block_device *bdev;
	fmode_t mode = FMODE_READ | FMODE_WRITE;
	int err = 0;

	pr_debug("%s: start\n", __func__);

	bdev = blkdev_get_by_path(bdev_path, mode, NULL);
	if (IS_ERR(bdev)) {
		dev_t devt;

		pr_err("%s: plan b\n", __func__);
		devt = name_to_dev_t(bdev_path);
		if (devt == 0) {
			pr_err("'name_to_dev_t' failed!\n");
			err = -EPROBE_DEFER;
			goto err_blkdev;
		}

		bdev = blkdev_get_by_dev(devt, mode, NULL);
		if (IS_ERR(bdev)) {
			pr_err("'blkdev_get_by_dev' failed! (%ld)\n",
					PTR_ERR(bdev));
			err = -EPROBE_DEFER;
			goto err_blkdev;
		}
	}

	sdg_bdev = bdev;

	if (sdg_part_load_header()) {
		sdg_bdev = NULL;
		return -1;
	}
	if (sdg_part_load_members()) {
		sdg_bdev = NULL;
		return -1;
	}

	return 0;

err_blkdev:
	pr_err("can't find a block device - %s\n", bdev_path);
	return err;
}

static struct dprt_member *get_sdg_part_member_by_name(const char *name)
{
	int i;

	for (i = 0; i < num_member_items; i++)
		if (!strncmp(name, members[i].name, DPRT_MAX_LEN_MEMBER_NAME))
			return &members[i];

	pr_err("fail to match member: %s\n", name);
	return NULL;
}

static int get_sdg_part_data_member_offset(int member)
{
	if (!sdg_data_this_offset)
		return 0;

	return sdg_data_this_offset + SDGD_EACH_DUMP_SIZE * member;
}

static ssize_t secdbg_part_read_member(char __user *buf, int size, int target_offset, size_t len, loff_t *offset, loff_t dm_offset)
{
	loff_t pos = dm_offset;
	ssize_t count, ret = 0;
	char *base = NULL;

	count = min(len, (size_t)(size - pos));
	pr_debug("%s: count: %ld\n", __func__, count);

	base = vmalloc(size);
	pr_debug("%s: base: %p\n", __func__, base);

	if (!sdg_part_read(base, size, target_offset)) {
		pr_err("%s: fail to read\n", __func__);
		goto fail;
	}

	if (copy_to_user(buf, base + pos, count)) {
		pr_err("%s: fail to copy to use\n", __func__);
		ret = -EFAULT;
	} else {
		*offset += count;
		ret = count;
	}

fail:
	vfree(base);
	return ret;
}

static ssize_t secdbg_part_dhst_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
	ssize_t ret = 0;
	ssize_t in_dhst = 0;
	struct dprt_member *dh, *db;
	loff_t bore_offset = *offset;

	if (!sdg_bdev) {
		pr_debug("%s: re-init sdg bdev\n", __func__);
		ret = (ssize_t)init_sdg_bdev();
		if (ret)
			return ret;
	}

	dh = get_sdg_part_member_by_name("dhst_dump");
	if (!dh) {
		pr_err("%s: fail to get dhst_dump\n", __func__);
		return ret;
	}
	pr_debug("%s: dprt_member(dhst_dump): %p\n", __func__, dh);
	pr_debug("%s: %s: size 0x%x / offset 0x%x\n", __func__, dh->name, dh->size, dh->offset);

	in_dhst = (size_t)(dh->size - *offset);
	if (in_dhst > 0) {
		pr_debug("%s: dhst_dump: f_offset(%lld) p_offset(%lld)\n", __func__, *offset, *offset);
		ret = secdbg_part_read_member(buf, dh->size, dh->offset, len, offset, *offset);
	} else {
		db = get_sdg_part_member_by_name("bore_dump");
		if (!db) {
			pr_err("%s: fail to get bore_dump\n", __func__);
			return ret;
		}
		pr_debug("%s: dprt_member(bore_dump): %p\n", __func__, db);
		pr_debug("%s: %s: size 0x%x / offset 0x%x\n", __func__, db->name, db->size, db->offset);

		bore_offset -= dh->size;
		pr_debug("%s: bore_dump: f_offset(%lld) p_offset(%lld)\n", __func__, *offset, bore_offset);
		ret = secdbg_part_read_member(buf, db->size, db->offset, len, offset, bore_offset);
	}

	return ret;
}

static ssize_t secdbg_part_bore_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
	ssize_t ret = 0;
	struct dprt_member *dm;

	if (!sdg_bdev) {
		pr_err("%s: re-init sdg bdev\n", __func__);
		ret = (ssize_t)init_sdg_bdev();
		if (ret)
			return ret;
	}

	dm = get_sdg_part_member_by_name("bore_dump");
	if (!dm) {
		pr_err("%s: fail to get sdg_part_member\n", __func__);
		return ret;
	}
	pr_debug("%s: dprt_member(bore_dump): %p\n", __func__, dm);
	pr_debug("%s: %s: size 0x%x / offset 0x%x\n", __func__, dm->name, dm->size, dm->offset);

	ret = secdbg_part_read_member(buf, dm->size, dm->offset, len, offset, *offset);

	return ret;
}

static ssize_t secdbg_part_summ_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
	ssize_t ret = 0;
	struct dprt_member *dm;
	int sdg_data_summ_offset;

	if (!sdg_bdev) {
		pr_notice("%s: re-init sdg bdev\n", __func__);
		ret = (ssize_t)init_sdg_bdev();
		if (ret)
			return ret;
	}

	if (!sdg_data_this_offset) {
		dm = get_sdg_part_member_by_name("data");
		if (!dm) {
			pr_err("%s: fail to get sdg_part_member\n", __func__);
			return ret;
		}
		pr_debug("%s: dprt_member(data)): %p\n", __func__, dm);
		pr_debug("%s: %s: size 0x%x / offset 0x%x\n", __func__, dm->name, dm->size, dm->offset);

		pr_debug("%s: init sdg_data_this_offset\n", __func__);
		ret = sdg_part_load_data_header(dm);
		if (ret)
			return ret;
	}
	pr_debug("%s: sdg_data_this_offset: 0x%x\n", __func__, sdg_data_this_offset);

	sdg_data_summ_offset = get_sdg_part_data_member_offset(SDGD_SUMM_DUMP);
	if (!sdg_data_summ_offset)
		return 0;
	pr_debug("%s: sdg_data_summ_offset: 0x%x\n", __func__, sdg_data_summ_offset);

	ret = secdbg_part_read_member(buf, SDGD_EACH_DUMP_SIZE, sdg_data_summ_offset, len, offset, *offset);

	return ret;
}

int secdbg_part_init_bdev_path(struct device *dev)
{
	int ret = 0;

	pr_debug("%s: start\n", __func__);

	ret = of_property_read_string(dev->of_node, "bdev_path", &bdev_path);
	if (ret < 0) {
		pr_err("%s: failed\n", __func__);
		return -ENODEV;
	}

	pr_info("%s: bdev_path = %s\n", __func__, bdev_path);
	return 0;
}
EXPORT_SYMBOL(secdbg_part_init_bdev_path);

static const struct proc_ops dpart_dhst_file_ops = {
	.proc_read = secdbg_part_dhst_read,
};

static const struct proc_ops dpart_bore_file_ops = {
	.proc_read = secdbg_part_bore_read,
};

static const struct proc_ops dpart_summ_file_ops = {
	.proc_read = secdbg_part_summ_read,
};

static int __init secdbg_dprt_init(void)
{
	struct proc_dir_entry *dhst_entry;
	struct proc_dir_entry *bore_entry;
	struct proc_dir_entry *summ_entry;

#if IS_ENABLED(CONFIG_SEC_DEBUG_DISABLE_DPRT)
	pr_err("%s: disable configuration enabled\n", __func__);
	return 0;
#endif

	dhst_entry = proc_create("debug_history", S_IFREG | 0444, NULL, &dpart_dhst_file_ops);
	if (!dhst_entry) {
		pr_err("%s: failed to create proc entry debug part debug history log\n", __func__);
		return 0;
	}

	bore_entry = proc_create("boot_reset", S_IFREG | 0444, NULL, &dpart_bore_file_ops);
	if (!bore_entry) {
		pr_err("%s: failed to create proc entry debug part boot reset log\n", __func__);
		return 0;
	}

	summ_entry = proc_create("reset_summary", S_IFREG | 0444, NULL, &dpart_summ_file_ops);
	if (!summ_entry) {
		pr_err("%s: failed to create proc entry debug part dump summary\n", __func__);
		return 0;
	}

	pr_info("%s: success to create proc entry\n", __func__);

	proc_set_size(dhst_entry, (size_t)(DRPT_DHST_DUMP_SIZE));
	proc_set_size(bore_entry, (size_t)(DPRT_BORE_DUMP_SIZE));
	proc_set_size(summ_entry, (size_t)(SDGD_EACH_DUMP_SIZE));

	return 0;
}
module_init(secdbg_dprt_init);

static void __exit secdbg_dprt_exit(void)
{
}
module_exit(secdbg_dprt_exit);

MODULE_DESCRIPTION("Samsung Debug Dprt driver");
MODULE_LICENSE("GPL v2");