// SPDX-License-Identifier: GPL-2.0+
/*
 * Bootmethod for ChromiumOS
 *
 * Copyright 2023 Google LLC
 * Written by Simon Glass <sjg@chromium.org>
 */

#define LOG_CATEGORY UCLASS_BOOTSTD

#include <blk.h>
#include <bootdev.h>
#include <bootflow.h>
#include <bootm.h>
#include <bootmeth.h>
#include <display_options.h>
#include <dm.h>
#include <efi.h>
#include <malloc.h>
#include <mapmem.h>
#include <part.h>
#include <linux/sizes.h>
#include "bootmeth_cros.h"

static const efi_guid_t cros_kern_type = PARTITION_CROS_KERNEL;

/*
 * Layout of the ChromeOS kernel
 *
 * Partitions 2 and 4 contain kernels with type GUID_CROS_KERNEL
 *
 * Contents are:
 *
 * Offset	Contents
 *   0		struct vb2_keyblock
 *   m		struct vb2_kernel_preamble
 *   m + n	kernel buffer
 *
 * m is keyblock->keyblock_size
 * n is preamble->preamble_size
 *
 * The kernel buffer itself consists of various parts:
 *
 * Offset	Contents
 *   m + n	kernel image (Flat vmlinux binary or FIT)
 *   b - 8KB	Command line text
 *   b - 4KB	X86 setup block (struct boot_params, extends for about 16KB)
 *   b          X86 bootloader (continuation of setup block)
 *   b + 16KB	X86 setup block (copy, used for hold data pointed to)
 *
 * b is m + n + preamble->bootloader_address - preamble->body_load_address
 *
 * Useful metadata extends from b - 8KB through to b + 32 KB
 */

enum {
	PROBE_SIZE	= SZ_4K,	/* initial bytes read from partition */

	X86_SETUP_OFFSET = -0x1000,	/* setup offset relative to base */
	CMDLINE_OFFSET	= -0x2000,	/* cmdline offset relative to base */
	X86_KERNEL_OFFSET = 0x4000,	/* kernel offset relative to base */
};

/**
 * struct cros_priv - Private data
 *
 * This is read from the disk and recorded for use when the full kernel must
 * be loaded and booted
 *
 * @body_offset: Offset of kernel body from start of partition (in bytes)
 * @body_size: Size of kernel body in bytes
 * @part_start: Block offset of selected partition from the start of the disk
 * @body_load_address: Nominal load address for kernel body
 * @bootloader_address: Address of bootloader, after body is loaded at
 *	body_load_address
 * @bootloader_size:  Size of bootloader in bytes
 * @info_buf: Buffer containing ChromiumOS info
 */
struct cros_priv {
	ulong body_offset;
	ulong body_size;
	lbaint_t part_start;
	ulong body_load_address;
	ulong bootloader_address;
	ulong bootloader_size;
	void *info_buf;
};

static int cros_check(struct udevice *dev, struct bootflow_iter *iter)
{
	/* This only works on block and network devices */
	if (bootflow_iter_check_blk(iter))
		return log_msg_ret("blk", -ENOTSUPP);

	return 0;
}

static int copy_cmdline(const char *from, const char *uuid, char **bufp)
{
	const int maxlen = 2048;
	char buf[maxlen];
	char *cmd, *to, *end;
	int len;

	/* Allow space for cmdline + UUID */
	len = strnlen(from, sizeof(buf));
	if (len >= maxlen)
		return -E2BIG;

	log_debug("uuid %d %s\n", uuid ? (int)strlen(uuid) : 0, uuid);
	for (to = buf, end = buf + maxlen - UUID_STR_LEN - 1; *from; from++) {
		if (to >= end)
			return -E2BIG;
		if (from[0] == '%' && from[1] == 'U' && uuid &&
		    strlen(uuid) == UUID_STR_LEN) {
			strcpy(to, uuid);
			to += UUID_STR_LEN;
			from++;
		} else {
			*to++ = *from;
		}
	}
	*to = '\0';
	len = to - buf;
	cmd = strdup(buf);
	if (!cmd)
		return -ENOMEM;
	free(*bufp);
	*bufp = cmd;

	return 0;
}

/**
 * scan_part() - Scan a kernel partition to see if has a ChromeOS header
 *
 * This reads the first PROBE_SIZE of a partition, loookng for
 * VB2_KEYBLOCK_MAGIC
 *
 * @blk: Block device to scan
 * @partnum: Partition number to scan
 * @info: Please to put partition info
 * @hdrp: Return allocated keyblock header on success
 */
static int scan_part(struct udevice *blk, int partnum,
		     struct disk_partition *info, struct vb2_keyblock **hdrp)
{
	struct blk_desc *desc = dev_get_uclass_plat(blk);
	struct vb2_keyblock *hdr;
	efi_guid_t type;
	ulong num_blks;
	int ret;

	if (!partnum)
		return log_msg_ret("efi", -ENOENT);

	ret = part_get_info(desc, partnum, info);
	if (ret)
		return log_msg_ret("part", ret);

	/* Check for kernel partition type */
	log_debug("part %x: type=%s\n", partnum, info->type_guid);
	if (uuid_str_to_bin(info->type_guid, type.b, UUID_STR_FORMAT_GUID))
		return log_msg_ret("typ", -EINVAL);

	if (guidcmp(&cros_kern_type, &type))
		return log_msg_ret("typ", -ENOEXEC);

	/* Make a buffer for the header information */
	num_blks = PROBE_SIZE >> desc->log2blksz;
	log_debug("Reading header, blk=%s, start=%lx, blocks=%lx\n",
		  blk->name, (ulong)info->start, num_blks);
	hdr = memalign(SZ_1K, PROBE_SIZE);
	if (!hdr)
		return log_msg_ret("hdr", -ENOMEM);
	ret = blk_read(blk, info->start, num_blks, hdr);
	if (ret != num_blks) {
		free(hdr);
		return log_msg_ret("inf", -EIO);
	}

	if (memcmp(VB2_KEYBLOCK_MAGIC, hdr->magic, VB2_KEYBLOCK_MAGIC_SIZE)) {
		free(hdr);
		log_debug("no magic\n");
		return -ENOENT;
	}

	*hdrp = hdr;

	return 0;
}

/**
 * cros_read_buf() - Read information into a buf and parse it
 *
 * @bflow: Bootflow to update
 * @buf: Buffer to use
 * @size: Size of buffer and number of bytes to read thereinto
 * @start: Start offset to read from on disk
 * @before_base: Number of bytes to read before the bootloader base
 * @uuid: UUID string if supported, else NULL
 * Return: 0 if OK, -ENOMEM if out of memory, -EIO on read failure
 */
static int cros_read_buf(struct bootflow *bflow, void *buf, ulong size,
			 loff_t start, ulong before_base, const char *uuid)
{
	struct blk_desc *desc = dev_get_uclass_plat(bflow->blk);
	ulong base, setup, cmdline, kern_base;
	ulong num_blks;
	int ret;

	num_blks = size >> desc->log2blksz;
	log_debug("Reading info to %lx, blk=%s, size=%lx, blocks=%lx\n",
		  (ulong)map_to_sysmem(buf), bflow->blk->name, size, num_blks);
	ret = blk_read(bflow->blk, start, num_blks, buf);
	if (ret != num_blks)
		return log_msg_ret("inf", -EIO);
	base = map_to_sysmem(buf) + before_base;

	setup = base + X86_SETUP_OFFSET;
	cmdline = base + CMDLINE_OFFSET;
	kern_base = base + X86_KERNEL_OFFSET;
	log_debug("base %lx setup %lx cmdline %lx kern_base %lx\n", base,
		  setup, cmdline, kern_base);

#ifdef CONFIG_X86
	const char *version;

	version = zimage_get_kernel_version(map_sysmem(setup, 0),
					    map_sysmem(kern_base, 0));
	log_debug("version %s\n", version);
	if (version)
		bflow->name = strdup(version);
#endif
	if (!bflow->name)
		bflow->name = strdup("ChromeOS");
	if (!bflow->name)
		return log_msg_ret("nam", -ENOMEM);
	bflow->os_name = strdup("ChromeOS");
	if (!bflow->os_name)
		return log_msg_ret("os", -ENOMEM);

	ret = copy_cmdline(map_sysmem(cmdline, 0), uuid, &bflow->cmdline);
	if (ret)
		return log_msg_ret("cmd", ret);

	if (!bootflow_img_add(bflow, "setup",
			      (enum bootflow_img_t)IH_TYPE_X86_SETUP,
			      setup, 0x3000))
		return log_msg_ret("cri", -ENOMEM);

	bflow->x86_setup = map_sysmem(setup, 0);

	if (!bootflow_img_add(bflow, "cmdline", BFI_CMDLINE, cmdline, 0x1000))
		return log_msg_ret("crc", -ENOMEM);

	return 0;
}

/**
 * cros_read_info() - Read information and fill out the bootflow
 *
 * @bflow: Bootflow to update
 * @uuid: UUID string if supported, else NULL
 * @preamble: Kernel preamble information
 * Return: 0 if OK, -ENOMEM if out of memory, -EIO on read failure
 */
static int cros_read_info(struct bootflow *bflow, const char *uuid,
			  const struct vb2_kernel_preamble *preamble)
{
	struct cros_priv *priv = bflow->bootmeth_priv;
	struct udevice *blk = bflow->blk;
	struct blk_desc *desc = dev_get_uclass_plat(blk);
	ulong offset, size, before_base;
	void *buf;
	int ret;

	log_debug("Kernel preamble at %lx, version major %x, minor %x\n",
		  (ulong)map_to_sysmem(preamble),
		  preamble->header_version_major,
		  preamble->header_version_minor);

	log_debug("  - load_address %lx, bl_addr %lx, bl_size %lx\n",
		  (ulong)preamble->body_load_address,
		  (ulong)preamble->bootloader_address,
		  (ulong)preamble->bootloader_size);

	priv->body_size = preamble->body_signature.data_size;
	priv->body_load_address = preamble->body_load_address;
	priv->bootloader_address = preamble->bootloader_address;
	priv->bootloader_size = preamble->bootloader_size;
	log_debug("Kernel body at %lx size %lx\n", priv->body_offset,
		  priv->body_size);

	/* Work out how many bytes to read before the bootloader base */
	before_base = -CMDLINE_OFFSET;

	/* Read the cmdline through to the end of the bootloader */
	size = priv->bootloader_size + before_base;
	offset = priv->body_offset +
		(priv->bootloader_address - priv->body_load_address) +
		CMDLINE_OFFSET;
	buf = malloc(size);
	if (!buf)
		return log_msg_ret("buf", -ENOMEM);

	ret = cros_read_buf(bflow, buf, size,
			    priv->part_start + (offset >> desc->log2blksz),
			    before_base, uuid);
	if (ret) {
		/* Clear this since the buffer is invalid */
		bflow->x86_setup = NULL;
		free(buf);
		return log_msg_ret("pro", ret);
	}
	priv->info_buf = buf;

	if (!bootflow_img_add(bflow, "kernel",
			      (enum bootflow_img_t)IH_TYPE_KERNEL, 0,
			      priv->body_size))
		return log_msg_ret("crk", -ENOMEM);

	return 0;
}

static int cros_read_kernel(struct bootflow *bflow)
{
	struct blk_desc *desc = dev_get_uclass_plat(bflow->blk);
	struct cros_priv *priv = bflow->bootmeth_priv;
	ulong base, setup;
	ulong num_blks;
	void *buf;
	int ret;

	bflow->size = priv->body_size;

	buf = memalign(SZ_1K, priv->body_size);
	if (!buf)
		return log_msg_ret("buf", -ENOMEM);

	/* Check that the header is not smaller than permitted */
	if (priv->body_offset < PROBE_SIZE)
		return log_msg_ret("san", EFAULT);

	/* Read kernel body */
	num_blks = priv->body_size >> desc->log2blksz;
	log_debug("Reading body to %lx, blk=%s, size=%lx, blocks=%lx\n",
		  (ulong)map_to_sysmem(buf), bflow->blk->name, priv->body_size,
		  num_blks);
	ret = blk_read(bflow->blk,
		       priv->part_start + (priv->body_offset >> desc->log2blksz),
		       num_blks, buf);
	if (ret != num_blks)
		return log_msg_ret("inf", -EIO);
	base = map_to_sysmem(buf) + priv->bootloader_address -
		priv->body_load_address;
	setup = base + X86_SETUP_OFFSET;

	bflow->buf = buf;
	bflow->x86_setup = map_sysmem(setup, 0);

	return 0;
}

static int cros_read_bootflow(struct udevice *dev, struct bootflow *bflow)
{
	const struct vb2_kernel_preamble *preamble;
	struct disk_partition info;
	struct vb2_keyblock *hdr;
	const char *uuid = NULL;
	struct cros_priv *priv;
	int ret;

	log_debug("starting, part=%x\n", bflow->part);

	/* Check for kernel partitions */
	ret = scan_part(bflow->blk, bflow->part, &info, &hdr);
	if (ret) {
		log_debug("- scan failed: err=%d\n", ret);
		return log_msg_ret("scan", ret);
	}

	priv = malloc(sizeof(struct cros_priv));
	if (!priv) {
		free(hdr);
		return log_msg_ret("buf", -ENOMEM);
	}
	bflow->bootmeth_priv = priv;

	log_debug("Selected partition %d, header at %lx\n", bflow->part,
		  (ulong)map_to_sysmem(hdr));

	/* Grab a few things from the preamble */
	preamble = (void *)hdr + hdr->keyblock_size;
	priv->body_offset = hdr->keyblock_size + preamble->preamble_size;
	priv->part_start = info.start;

	/* Now read everything we can learn about kernel */
#if CONFIG_IS_ENABLED(PARTITION_UUIDS)
	uuid = info.uuid;
#endif
	ret = cros_read_info(bflow, uuid, preamble);
	preamble = NULL;
	free(hdr);
	if (ret) {
		free(priv->info_buf);
		free(priv);
		return log_msg_ret("inf", ret);
	}
	bflow->size = priv->body_size;
	bflow->state = BOOTFLOWST_READY;

	return 0;
}

static int cros_read_file(struct udevice *dev, struct bootflow *bflow,
			 const char *file_path, ulong addr,
			  enum bootflow_img_t type, ulong *sizep)
{
	return -ENOSYS;
}

#if CONFIG_IS_ENABLED(BOOTSTD_FULL)
static int cros_read_all(struct udevice *dev, struct bootflow *bflow)
{
	int ret;

	if (bflow->buf)
		return log_msg_ret("ld", -EALREADY);
	ret = cros_read_kernel(bflow);
	if (ret)
		return log_msg_ret("rd", ret);

	return 0;
}
#endif /* BOOTSTD_FULL */

static int cros_boot(struct udevice *dev, struct bootflow *bflow)
{
	int ret;

	if (!bflow->buf) {
		ret = cros_read_kernel(bflow);
		if (ret)
			return log_msg_ret("rd", ret);
	}

	if (IS_ENABLED(CONFIG_X86)) {
		ret = zboot_run(map_to_sysmem(bflow->buf), bflow->size, 0, 0,
				map_to_sysmem(bflow->x86_setup),
				bflow->cmdline);
	} else {
		ret = bootm_boot_start(map_to_sysmem(bflow->buf),
				       bflow->cmdline);
	}

	return log_msg_ret("go", ret);
}

static int cros_bootmeth_bind(struct udevice *dev)
{
	struct bootmeth_uc_plat *plat = dev_get_uclass_plat(dev);

	plat->desc = "ChromiumOS boot";
	plat->flags = BOOTMETHF_ANY_PART;

	return 0;
}

static struct bootmeth_ops cros_bootmeth_ops = {
	.check		= cros_check,
	.read_bootflow	= cros_read_bootflow,
	.read_file	= cros_read_file,
	.boot		= cros_boot,
#if CONFIG_IS_ENABLED(BOOTSTD_FULL)
	.read_all	= cros_read_all,
#endif /* BOOTSTD_FULL */
};

static const struct udevice_id cros_bootmeth_ids[] = {
	{ .compatible = "u-boot,cros" },
	{ }
};

U_BOOT_DRIVER(bootmeth_cros) = {
	.name		= "bootmeth_cros",
	.id		= UCLASS_BOOTMETH,
	.of_match	= cros_bootmeth_ids,
	.ops		= &cros_bootmeth_ops,
	.bind		= cros_bootmeth_bind,
};
