// SPDX-License-Identifier: GPL-2.0-only
/*
 * Some devices made by H3C use a "VFS" filesystem to store firmware images.
 * This parses the start of the filesystem to read the length of the first
 * file (the kernel image). It then searches for the rootfs after the end of
 * the file data. This driver assumes that the filesystem was generated by
 * mkh3cvfs, and only works if the filesystem matches the expected layout,
 * which includes the file name of the kernel image.
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/types.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>

#include "mtdsplit.h"

#define	VFS_ERASEBLOCK_SIZE		0x10000
#define	VFS_BLOCK_SIZE			0x400
#define VFS_BLOCKS_PER_ERASEBLOCK	(VFS_ERASEBLOCK_SIZE / VFS_BLOCK_SIZE)

#define FORMAT_FLAG_OFFSET		0x0

#define FORMAT_FLAG			(VFS_ERASEBLOCK_SIZE << 12 | VFS_BLOCK_SIZE)

#define FILE_ENTRY_OFFSET		0x800

#define FILE_ENTRY_FLAGS		0x3f
#define FILE_ENTRY_PARENT_BLOCK		0
#define FILE_ENTRY_PARENT_INDEX 	0
#define FILE_ENTRY_DATA_BLOCK	 	2
#define FILE_ENTRY_NAME			"openwrt-kernel.bin"

#define NR_PARTS			2

struct file_entry {
	uint8_t flags;

	uint8_t res0[5];

	uint16_t year;
	uint8_t month;
	uint8_t day;
	uint8_t hour;
	uint8_t minute;
	uint8_t second;

	uint8_t res1[3];

	uint32_t length;

	uint32_t parent_block;
	uint16_t parent_index;

	uint8_t res2[2];

	uint32_t data_block;

	char name[96];
} __attribute__ ((packed));

static inline size_t block_offset(int block)
{
	return VFS_ERASEBLOCK_SIZE * (block / (VFS_BLOCKS_PER_ERASEBLOCK-1))
		+ VFS_BLOCK_SIZE * (1 + (block % (VFS_BLOCKS_PER_ERASEBLOCK-1)));
}

static inline int block_count(size_t size)
{
	return (size + VFS_BLOCK_SIZE - 1) / VFS_BLOCK_SIZE;
}

static int mtdsplit_h3c_vfs_parse(struct mtd_info *mtd,
				  const struct mtd_partition **pparts,
				  struct mtd_part_parser_data *data)
{
	struct mtd_partition *parts;
	uint32_t format_flag;
	struct file_entry file_entry;
	size_t retlen;
	int err;
	size_t kernel_size;
	size_t expected_offset;
	size_t rootfs_offset;

	if (mtd->erasesize != VFS_ERASEBLOCK_SIZE)
		return -EINVAL;

	/* Check format flag */
	err = mtd_read(mtd, FORMAT_FLAG_OFFSET, sizeof(format_flag), &retlen,
		       (void *) &format_flag);
	if (err)
		return err;

	if (retlen != sizeof(format_flag))
		return -EIO;

	if (format_flag != FORMAT_FLAG) {
		pr_debug("mtdsplit_h3c_vfs: unexpected format flag %08x\n",
			 format_flag);
		return -ENOENT;
	}

	/* Check file entry */
	err = mtd_read(mtd, FILE_ENTRY_OFFSET, sizeof(file_entry), &retlen,
		       (void *) &file_entry);
	if (err)
		return err;

	if (retlen != sizeof(file_entry))
		return -EIO;

	if (file_entry.flags != FILE_ENTRY_FLAGS)
		return -ENOENT;

	if (file_entry.parent_block != FILE_ENTRY_PARENT_BLOCK)
		return -ENOENT;

	if (file_entry.parent_index != FILE_ENTRY_PARENT_INDEX)
		return -ENOENT;

	if (file_entry.data_block != FILE_ENTRY_DATA_BLOCK)
		return -ENOENT;

	if (strncmp(file_entry.name, FILE_ENTRY_NAME, sizeof(file_entry.name)) != 0)
		return -ENOENT;

	/* Find rootfs offset */
	kernel_size = block_offset(file_entry.data_block +
				   block_count(file_entry.length) - 1) +
		      VFS_BLOCK_SIZE;

	expected_offset = mtd_roundup_to_eb(kernel_size, mtd);

	err = mtd_find_rootfs_from(mtd, expected_offset, mtd->size,
				   &rootfs_offset, NULL);
	if (err)
		return err;

	parts = kcalloc(NR_PARTS, sizeof(*parts), GFP_KERNEL);
	if (!parts)
		return -ENOMEM;

	parts[0].name = KERNEL_PART_NAME;
	parts[0].offset = 0;
	parts[0].size = rootfs_offset;

	parts[1].name = ROOTFS_PART_NAME;
	parts[1].offset = rootfs_offset;
	parts[1].size = mtd->size - rootfs_offset;

	*pparts = parts;
	return NR_PARTS;
}

static const struct of_device_id mtdsplit_h3c_vfs_of_match_table[] = {
	{ .compatible = "h3c,vfs-firmware" },
	{},
};
MODULE_DEVICE_TABLE(of, mtdsplit_h3c_vfs_of_match_table);

static struct mtd_part_parser mtdsplit_h3c_vfs_parser = {
	.owner = THIS_MODULE,
	.name = "h3c-vfs",
	.of_match_table = mtdsplit_h3c_vfs_of_match_table,
	.parse_fn = mtdsplit_h3c_vfs_parse,
	.type = MTD_PARSER_TYPE_FIRMWARE,
};

module_mtd_part_parser(mtdsplit_h3c_vfs_parser);
