/*
 * Copyright 2022 Richard Hughes <richard@hughsie.com>
 * Copyright 2022 Google LLC
 * Written by Simon Glass <sjg@chromium.org>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <fcntl.h>
#include <stdio.h>

#include "fu-vbe-simple-device.h"

/**
 * @skip_offset: This allows an initial part of the image to be skipped when
 * writing. This means that the first part of the image is ignored, with just
 * the latter part being written. For example, if this is 0x200 then the first
 * 512 bytes of the image (which must be present in the image) are skipped and
 * the bytes after that are written to the store offset.
 */
struct _FuVbeSimpleDevice {
	FuVbeDevice parent_instance;
	gchar *storage; /* e.g. "mmc1" */
	gchar *devname; /* e.g. /dev/mmcblk1 */
	guint32 area_start;
	guint32 area_size;
	guint32 skip_offset;
	gint fd;
};

G_DEFINE_TYPE(FuVbeSimpleDevice, fu_vbe_simple_device, FU_TYPE_VBE_DEVICE)

static void
fu_vbe_simple_device_to_string(FuDevice *device, guint idt, GString *str)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);
	fwupd_codec_string_append(str, idt, "Storage", self->storage);
	fwupd_codec_string_append(str, idt, "Devname", self->devname);
	fwupd_codec_string_append_hex(str, idt, "AreaStart", self->area_start);
	fwupd_codec_string_append_hex(str, idt, "AreaSize", self->area_size);
	fwupd_codec_string_append_hex(str, idt, "SkipOffset", self->skip_offset);
}

static gboolean
fu_vbe_simple_device_parse_devnum(const gchar *str, guint *value, GError **error)
{
	guint64 val64 = 0;

	/* skip non-numeric part */
	while (*str != '\0' && g_ascii_isdigit(*str))
		str++;

	/* convert to uint */
	if (!fu_strtoull(str, &val64, 0x0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error))
		return FALSE;
	if (value != NULL)
		*value = val64;
	return TRUE;
}

static gboolean
fu_vbe_simple_device_probe(FuDevice *device, GError **error)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);
	FuFdtImage *fdt_node;

	g_return_val_if_fail(FU_IS_VBE_DEVICE(self), FALSE);

	/* FuVbeDevice->probe */
	if (!FU_DEVICE_CLASS(fu_vbe_simple_device_parent_class)->probe(device, error))
		return FALSE;

	fdt_node = fu_vbe_device_get_fdt_node(FU_VBE_DEVICE(self));
	if (!fu_fdt_image_get_attr_str(fdt_node, "storage", &self->storage, error))
		return FALSE;

	/* if this is an absolute path, use it */
	if (g_str_has_prefix(self->storage, "/")) {
		self->devname = g_strdup(self->storage);
	} else {
		guint devnum = 0;

		/* obtain the 1 from "mmc1" */
		if (!fu_vbe_simple_device_parse_devnum(self->storage, &devnum, error)) {
			g_prefix_error(error, "cannot parse storage property %s: ", self->storage);
			return FALSE;
		}
		if (g_str_has_prefix(self->storage, "mmc")) {
			self->devname = g_strdup_printf("/dev/mmcblk%u", devnum);
		} else {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "unsupported 'storage' media '%s'",
				    self->storage);
			return FALSE;
		}
	}

	/* get area */
	if (!fu_fdt_image_get_attr_u32(fdt_node, "area-start", &self->area_start, error))
		return FALSE;
	if (!fu_fdt_image_get_attr_u32(fdt_node, "area-size", &self->area_size, error))
		return FALSE;

	/* an optional skip offset to skip everything, which could be useful for testing */
	fu_fdt_image_get_attr_u32(fdt_node, "skip-offset", &self->skip_offset, NULL);
	if (self->skip_offset > self->area_size) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "store offset 0x%x is larger than size 0x%x",
			    (guint)self->skip_offset,
			    (guint)self->area_size);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_vbe_simple_device_open(FuDevice *device, GError **error)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);

	/* open device */
	self->fd = open(self->devname, O_RDWR);
	if (self->fd == -1) {
#ifdef HAVE_ERRNO_H
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "cannot open %s [%s]",
			    self->devname,
			    fwupd_strerror(errno));
#else
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "cannot open %s",
			    self->devname);
		return FALSE;
#endif
	}

	/* success */
	return TRUE;
}

static gboolean
fu_vbe_simple_device_close(FuDevice *device, GError **error)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);

	/* close device */
	close(self->fd);
	self->fd = -1;

	/* success */
	return TRUE;
}

static FuFdtImage *
fu_vbe_simple_device_get_cfg_compatible(FuVbeSimpleDevice *self,
					FuFirmware *firmware,
					GError **error)
{
	gchar **device_compatible;
	g_autoptr(FuFdtImage) fdt_configurations = NULL;
	g_autoptr(GPtrArray) img_configurations = NULL;
	g_autofree gchar *str = NULL;

	/* get all configurations */
	fdt_configurations =
	    fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(firmware),
					      "/" FU_FIT_FIRMWARE_ID_CONFIGURATIONS,
					      error);
	if (fdt_configurations == NULL)
		return NULL;
	img_configurations = fu_firmware_get_images(firmware);

	/* look for a configuration with the device compatible strings in priority order */
	device_compatible = fu_vbe_device_get_compatible(FU_VBE_DEVICE(self));
	for (guint j = 0; device_compatible[j] != NULL; j++) {
		for (guint i = 0; i < img_configurations->len; i++) {
			FuFdtImage *img = g_ptr_array_index(img_configurations, i);
			g_auto(GStrv) img_compatible = NULL;
			if (!fu_fdt_image_get_attr_strlist(img,
							   FU_FIT_FIRMWARE_ATTR_COMPATIBLE,
							   &img_compatible,
							   error))
				return NULL;
			if (g_strv_contains((const gchar *const *)img_compatible,
					    device_compatible[j]))
				return g_object_ref(img);
		}
	}

	/* failure */
	str = g_strjoinv(", ", device_compatible);
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_FOUND,
		    "no images found that match %s",
		    str);
	return NULL;
}

static FuFirmware *
fu_vbe_simple_device_prepare_firmware(FuDevice *device,
				      GInputStream *stream,
				      FuProgress *progress,
				      FuFirmwareParseFlags flags,
				      GError **error)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);
	g_autofree gchar *version = NULL;
	g_auto(GStrv) firmware_ids = NULL;
	g_autoptr(FuFdtImage) img_cfg = NULL;
	g_autoptr(FuFirmware) firmware = fu_fit_firmware_new();
	g_autoptr(FuFirmware) firmware_container = fu_firmware_new();

	/* parse all images */
	if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error))
		return NULL;

	/* look for a compatible configuration */
	img_cfg = fu_vbe_simple_device_get_cfg_compatible(self, firmware, error);
	if (img_cfg == NULL)
		return NULL;
	if (!fu_fdt_image_get_attr_str(img_cfg, FU_FIT_FIRMWARE_ATTR_VERSION, &version, error))
		return NULL;

	/* check the firmware images exists */
	if (!fu_fdt_image_get_attr_strlist(img_cfg, "firmware", &firmware_ids, error))
		return NULL;
	for (guint i = 0; firmware_ids[i] != NULL; i++) {
		g_autofree gchar *path = NULL;
		g_autoptr(FuFdtImage) img_firmware = NULL;
		path = g_strdup_printf("/%s/%s", FU_FIT_FIRMWARE_ID_IMAGES, firmware_ids[i]);
		img_firmware =
		    fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(firmware), path, error);
		if (img_firmware == NULL)
			return NULL;
		fu_firmware_add_image(firmware_container, FU_FIRMWARE(img_firmware));
	}

	/* success: return the container */
	return g_steal_pointer(&firmware_container);
}

static gboolean
fu_vbe_simple_device_write_firmware_img(FuVbeSimpleDevice *self,
					FuFdtImage *img,
					FuProgress *progress,
					GError **error)
{
	const guint8 *buf;
	gssize rc;
	gsize bufsz = 0;
	gsize seek_to;
	guint32 store_offset = 0;
	g_autoptr(GBytes) blob = NULL;

	/* get data */
	blob = fu_fdt_image_get_attr(img, FU_FIT_FIRMWARE_ATTR_DATA, error);
	if (blob == NULL)
		return FALSE;
	buf = g_bytes_get_data(blob, &bufsz);
	fu_fdt_image_get_attr_u32(img, "store-offset", &store_offset, NULL);

	/* sanity check */
	if (store_offset + bufsz > self->area_size) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "image '%s' store_offset=0x%x, bufsz=0x%x, area_size=0x%x",
			    fu_firmware_get_id(FU_FIRMWARE(img)),
			    (guint)store_offset,
			    (guint)bufsz,
			    (guint)self->area_size);
		return FALSE;
	}
	if (self->skip_offset >= bufsz) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "image '%s' skip_offset=0x%x, bufsz=0x%x, area_size=0x%x",
			    fu_firmware_get_id(FU_FIRMWARE(img)),
			    (guint)store_offset,
			    (guint)bufsz,
			    (guint)self->area_size);
		return FALSE;
	}

	/* seek to correct address */
	seek_to = self->area_start + store_offset + self->skip_offset;
	g_debug("writing image '%s' bufsz 0x%x (skipping 0x%x) to store_offset 0x%x, seek 0x%x\n",
		fu_firmware_get_id(FU_FIRMWARE(img)),
		(guint)bufsz,
		(guint)self->skip_offset,
		(guint)store_offset,
		(guint)seek_to);
	rc = lseek(self->fd, seek_to, SEEK_SET);
	if (rc < 0) {
#ifdef HAVE_ERRNO_H
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "cannot seek file '%s' to 0x%x [%s]",
			    self->devname,
			    (guint)seek_to,
			    fwupd_strerror(errno));
#else
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "cannot seek file '%s' to 0x%x",
			    self->devname,
			    (guint)seek_to);
#endif
		return FALSE;
	}

	/* write buffer */
	rc = write(self->fd, buf + self->skip_offset, bufsz - self->skip_offset);
	if (rc < 0) {
#ifdef HAVE_ERRNO_H
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "cannot write file '%s' [%s]",
			    self->devname,
			    fwupd_strerror(errno));
#else
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "cannot write file '%s'",
			    self->devname);
#endif
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_vbe_simple_device_write_firmware(FuDevice *device,
				    FuFirmware *firmware,
				    FuProgress *progress,
				    FwupdInstallFlags flags,
				    GError **error)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);
	g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware);

	g_return_val_if_fail(FU_IS_VBE_DEVICE(self), FALSE);

	/* write each firmware image */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, imgs->len);
	for (guint i = 0; i < imgs->len; i++) {
		FuFdtImage *img = g_ptr_array_index(imgs, i);
		if (!fu_vbe_simple_device_write_firmware_img(self,
							     img,
							     fu_progress_get_child(progress),
							     error))
			return FALSE;
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static GBytes *
fu_vbe_simple_device_upload(FuDevice *device, FuProgress *progress, GError **error)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device);
	gssize rc;
	g_autoptr(GByteArray) buf = g_byte_array_new();
	g_autoptr(GPtrArray) chunks = NULL;

	/* notify UI */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ);

	/* seek to start */
	rc = lseek(self->fd, self->area_start, SEEK_SET);
	if (rc < 0) {
#ifdef HAVE_ERRNO_H
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_READ,
			    "cannot seek file %s to 0x%x [%s]",
			    self->devname,
			    (guint)self->area_start,
			    fwupd_strerror(errno));
#else
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_READ,
			    "cannot seek file %s to 0x%x",
			    self->devname,
			    (guint)self->area_start);
#endif
		return NULL;
	}

	/* process in chunks */
	chunks = fu_chunk_array_new(NULL, self->area_size - self->area_start, 0x0, 0x0, 0x100000);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);
		g_autofree guint8 *tmpbuf = g_malloc0(fu_chunk_get_data_sz(chk));

		rc = read(self->fd, tmpbuf, fu_chunk_get_data_sz(chk));
		if (rc != (gssize)fu_chunk_get_data_sz(chk)) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_READ,
				    "incomplete read of %s @0x%x",
				    self->devname,
				    (guint)fu_chunk_get_address(chk));
			return NULL;
		}
		g_byte_array_append(buf, tmpbuf, fu_chunk_get_data_sz(chk));
		fu_progress_step_done(progress);
	}

	/* success */
	return g_bytes_new(buf->data, buf->len);
}

static void
fu_vbe_simple_device_set_progress(FuDevice *self, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload");
}

static void
fu_vbe_simple_device_init(FuVbeSimpleDevice *self)
{
	fu_device_set_name(FU_DEVICE(self), "simple");
	fu_device_set_vendor(FU_DEVICE(self), "U-Boot");
	fu_device_build_vendor_id(FU_DEVICE(self), "VBE", "U-Boot");
	fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET);
	fu_device_set_version_lowest(FU_DEVICE(self), "0.0.1");
}

static void
fu_vbe_simple_device_constructed(GObject *obj)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(obj);
	fu_device_add_instance_id(FU_DEVICE(self), "bb3b05a8-ebef-11ec-be98-d3a15278be95");
}

static void
fu_vbe_simple_device_finalize(GObject *obj)
{
	FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(obj);
	g_free(self->devname);
	g_free(self->storage);
	G_OBJECT_CLASS(fu_vbe_simple_device_parent_class)->finalize(obj);
}

static void
fu_vbe_simple_device_class_init(FuVbeSimpleDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	object_class->constructed = fu_vbe_simple_device_constructed;
	object_class->finalize = fu_vbe_simple_device_finalize;
	device_class->to_string = fu_vbe_simple_device_to_string;
	device_class->probe = fu_vbe_simple_device_probe;
	device_class->open = fu_vbe_simple_device_open;
	device_class->close = fu_vbe_simple_device_close;
	device_class->set_progress = fu_vbe_simple_device_set_progress;
	device_class->prepare_firmware = fu_vbe_simple_device_prepare_firmware;
	device_class->write_firmware = fu_vbe_simple_device_write_firmware;
	device_class->dump_firmware = fu_vbe_simple_device_upload;
}
