/*
 * Copyright 2012 Andrew Duggan
 * Copyright 2012 Synaptics Inc.
 * Copyright 2019 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include <string.h>

#include "fu-synaptics-rmi-common.h"
#include "fu-synaptics-rmi-firmware.h"
#include "fu-synaptics-rmi-struct.h"

struct _FuSynapticsRmiFirmware {
	FuFirmware parent_instance;
	FuSynapticsRmiFirmwareKind kind;
	guint32 checksum;
	guint8 io;
	guint8 bootloader_version;
	guint32 build_id;
	guint32 package_id;
	guint16 product_info;
	gchar *product_id;
	guint32 sig_size;
};

G_DEFINE_TYPE(FuSynapticsRmiFirmware, fu_synaptics_rmi_firmware, FU_TYPE_FIRMWARE)

#define RMI_IMG_FW_OFFSET 0x100

#define RMI_IMG_V10_CNTR_ADDR_OFFSET 0x0c
#define RMI_IMG_MAX_CONTAINERS	     1024

static gboolean
fu_synaptics_rmi_firmware_parse_sbl_container_v10(FuSynapticsRmiFirmware *self,
						  GInputStream *stream,
						  const guint8 *buf,
						  gsize bufsz,
						  guint32 start_offset,
						  FuFirmwareParseFlags flags,
						  GError **error);

static gboolean
fu_synaptics_rmi_firmware_add_image(FuSynapticsRmiFirmware *self,
				    const gchar *id,
				    GInputStream *stream,
				    gsize offset,
				    gsize bufsz,
				    FuFirmwareParseFlags flags,
				    GError **error)
{
	g_autoptr(FuFirmware) img = fu_firmware_new();
	g_autoptr(GInputStream) partial_stream = NULL;
	partial_stream = fu_partial_input_stream_new(stream, offset, bufsz, error);
	if (partial_stream == NULL)
		return FALSE;
	if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error))
		return FALSE;
	fu_firmware_set_id(img, id);
	return fu_firmware_add_image(FU_FIRMWARE(self), img, error);
}

static gboolean
fu_synaptics_rmi_firmware_add_image_v10(FuSynapticsRmiFirmware *self,
					const gchar *id,
					GInputStream *stream,
					gsize offset,
					gsize bufsz,
					gsize sig_sz,
					FuFirmwareParseFlags flags,
					GError **error)
{
	if (!fu_synaptics_rmi_firmware_add_image(self, id, stream, offset, bufsz, flags, error))
		return FALSE;
	if (sig_sz != 0) {
		g_autoptr(GInputStream) partial_stream = NULL;
		g_autoptr(FuFirmware) img = fu_firmware_new();
		g_autofree gchar *sig_id = NULL;

		partial_stream = fu_partial_input_stream_new(stream, offset + bufsz, sig_sz, error);
		if (partial_stream == NULL)
			return FALSE;
		if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error))
			return FALSE;
		sig_id = g_strdup_printf("%s-signature", id);
		fu_firmware_set_id(img, sig_id);
		if (!fu_firmware_add_image(FU_FIRMWARE(self), img, error))
			return FALSE;
	}
	return TRUE;
}

static void
fu_synaptics_rmi_firmware_export(FuFirmware *firmware,
				 FuFirmwareExportFlags flags,
				 XbBuilderNode *bn)
{
	FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware);
	fu_xmlb_builder_insert_kx(bn, "kind", self->kind);
	fu_xmlb_builder_insert_kv(bn, "product_id", self->product_id);
	if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) {
		fu_xmlb_builder_insert_kx(bn, "bootloader_version", self->bootloader_version);
		fu_xmlb_builder_insert_kx(bn, "io", self->io);
		fu_xmlb_builder_insert_kx(bn, "checksum", self->checksum);
		fu_xmlb_builder_insert_kx(bn, "build_id", self->build_id);
		fu_xmlb_builder_insert_kx(bn, "package_id", self->package_id);
		fu_xmlb_builder_insert_kx(bn, "product_info", self->product_info);
		fu_xmlb_builder_insert_kx(bn, "sig_size", self->sig_size);
	}
}

static gboolean
fu_synaptics_rmi_firmware_parse_v10(FuSynapticsRmiFirmware *self,
				    GInputStream *stream,
				    FuFirmwareParseFlags flags,
				    GError **error)
{
	guint16 container_id;
	guint32 cntrs_len;
	guint32 offset;
	guint32 cntr_addr;
	guint8 product_id[RMI_PRODUCT_ID_LENGTH] = {0x0};
	gsize bufsz = 0;
	const guint8 *buf;
	guint32 signature_size;
	g_autoptr(FuStructRmiContainerDescriptor) st_dsc = NULL;
	g_autoptr(GBytes) fw = NULL;

	/* maybe stream later */
	fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error);
	if (fw == NULL)
		return FALSE;
	buf = g_bytes_get_data(fw, &bufsz);

	if (!fu_memread_uint32_safe(buf,
				    bufsz,
				    RMI_IMG_V10_CNTR_ADDR_OFFSET,
				    &cntr_addr,
				    G_LITTLE_ENDIAN,
				    error))
		return FALSE;
	g_debug("v10 RmiContainerDescriptor at 0x%x", cntr_addr);
	st_dsc = fu_struct_rmi_container_descriptor_parse_stream(stream, cntr_addr, error);
	if (st_dsc == NULL) {
		g_prefix_error_literal(error, "RmiContainerDescriptor invalid: ");
		return FALSE;
	}
	if (bufsz < sizeof(guint32) + st_dsc->buf->len) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "stream was too small");
		return FALSE;
	}
	container_id = fu_struct_rmi_container_descriptor_get_container_id(st_dsc);
	if (container_id != FU_RMI_CONTAINER_ID_TOP_LEVEL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "toplevel container_id invalid, got 0x%x expected 0x%x",
			    (guint)container_id,
			    (guint)FU_RMI_CONTAINER_ID_TOP_LEVEL);
		return FALSE;
	}
	offset = fu_struct_rmi_container_descriptor_get_content_address(st_dsc);
	if (offset > bufsz - sizeof(guint32) - st_dsc->buf->len) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "image offset invalid, got 0x%x, size 0x%x",
			    (guint)offset,
			    (guint)bufsz);
		return FALSE;
	}
	cntrs_len = fu_struct_rmi_container_descriptor_get_content_length(st_dsc) / 4;
	if (cntrs_len > RMI_IMG_MAX_CONTAINERS) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "too many containers in file [%u], maximum is %u",
			    cntrs_len,
			    (guint)RMI_IMG_MAX_CONTAINERS);
		return FALSE;
	}
	g_debug("offset=0x%x (cntrs_len=%u)", offset, cntrs_len);

	for (guint32 i = 0; i < cntrs_len; i++) {
		guint32 content_addr;
		guint32 addr;
		guint32 length;
		g_autoptr(FuStructRmiContainerDescriptor) st_dsc2 = NULL;

		if (offset > G_MAXUINT32 - 4) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "offset overflow at container %u",
				    i);
			return FALSE;
		}
		if (!fu_memread_uint32_safe(buf, bufsz, offset, &addr, G_LITTLE_ENDIAN, error))
			return FALSE;
		g_debug("parsing RmiContainerDescriptor at 0x%x", addr);

		st_dsc2 = fu_struct_rmi_container_descriptor_parse_stream(stream, addr, error);
		if (st_dsc2 == NULL)
			return FALSE;
		container_id = fu_struct_rmi_container_descriptor_get_container_id(st_dsc2);
		content_addr = fu_struct_rmi_container_descriptor_get_content_address(st_dsc2);
		length = fu_struct_rmi_container_descriptor_get_content_length(st_dsc2);
		signature_size = fu_struct_rmi_container_descriptor_get_signature_size(st_dsc2);
		g_debug("RmiContainerDescriptor 0x%02x @ 0x%x (len 0x%x) sig_size 0x%x",
			container_id,
			content_addr,
			length,
			signature_size);
		if (length == 0 || length > bufsz) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "length invalid, length 0x%x, size 0x%x",
				    (guint)length,
				    (guint)bufsz);
			return FALSE;
		}
		if (content_addr > bufsz - length) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "address invalid, got 0x%x (length 0x%x), size 0x%x",
				    (guint)content_addr,
				    (guint)length,
				    (guint)bufsz);
			return FALSE;
		}
		switch (container_id) {
		case FU_RMI_CONTAINER_ID_BL:
			if (!fu_memread_uint8_safe(buf,
						   bufsz,
						   content_addr,
						   &self->bootloader_version,
						   error))
				return FALSE;
			if (!fu_synaptics_rmi_firmware_parse_sbl_container_v10(self,
									       stream,
									       buf,
									       bufsz,
									       offset,
									       flags,
									       error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_UI:
		case FU_RMI_CONTAINER_ID_CORE_CODE:
			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "ui",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_FLASH_CONFIG:
			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "flash-config",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_UI_CONFIG:
		case FU_RMI_CONTAINER_ID_CORE_CONFIG:
			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "config",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_FIXED_LOCATION_DATA:
			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "fixed-location-data",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_EXTERNAL_TOUCH_AFE_CONFIG:
			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "afe-config",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_DISPLAY_CONFIG:
			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "display-config",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error))
				return FALSE;
			break;
		case FU_RMI_CONTAINER_ID_GENERAL_INFORMATION:
			if (length < 0x18 + RMI_PRODUCT_ID_LENGTH) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_FILE,
					    "content_addr invalid, got 0x%x (length 0x%x)",
					    content_addr,
					    (guint)length);
				return FALSE;
			}
			g_clear_pointer(&self->product_id, g_free);
			self->io = 1;
			if (!fu_memread_uint32_safe(buf,
						    bufsz,
						    content_addr,
						    &self->package_id,
						    G_LITTLE_ENDIAN,
						    error))
				return FALSE;
			if (!fu_memread_uint32_safe(buf,
						    bufsz,
						    content_addr + 0x04,
						    &self->build_id,
						    G_LITTLE_ENDIAN,
						    error))
				return FALSE;
			if (!fu_memcpy_safe(product_id,
					    sizeof(product_id),
					    0x0, /* dst */
					    buf,
					    bufsz,
					    content_addr + 0x18, /* src */
					    sizeof(product_id),
					    error))
				return FALSE;
			break;
		default:
			g_debug("unsupported container %s [0x%02x]",
				fu_rmi_container_id_to_string(container_id),
				container_id);
			break;
		}
		offset += 4;
	}
	if (product_id[0] != '\0') {
		g_free(self->product_id);
		self->product_id = g_strndup((const gchar *)product_id, sizeof(product_id));
	}
	return TRUE;
}

static gboolean
fu_synaptics_rmi_firmware_parse_v0x(FuSynapticsRmiFirmware *self,
				    GInputStream *stream,
				    FuFirmwareParseFlags flags,
				    GError **error)
{
	guint32 cfg_sz;
	guint32 img_sz;
	g_autoptr(FuStructRmiImg) st_img = NULL;

	/* main firmware */
	st_img = fu_struct_rmi_img_parse_stream(stream, 0x0, error);
	if (st_img == NULL)
		return FALSE;
	img_sz = fu_struct_rmi_img_get_image_size(st_img);
	if (img_sz > 0) {
		/* payload, then signature appended */
		if (self->sig_size > 0) {
			guint32 sig_offset;
			if (self->sig_size > img_sz) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "signature size 0x%x exceeds image size 0x%x",
					    self->sig_size,
					    img_sz);
				return FALSE;
			}
			sig_offset = img_sz - self->sig_size;
			if (!fu_synaptics_rmi_firmware_add_image(self,
								 "sig",
								 stream,
								 RMI_IMG_FW_OFFSET + sig_offset,
								 self->sig_size,
								 flags,
								 error))
				return FALSE;
		}
		if (!fu_synaptics_rmi_firmware_add_image(self,
							 "ui",
							 stream,
							 RMI_IMG_FW_OFFSET,
							 img_sz,
							 flags,
							 error))
			return FALSE;
	}

	/* config */
	cfg_sz = fu_struct_rmi_img_get_config_size(st_img);
	if (cfg_sz > 0) {
		if ((gsize)RMI_IMG_FW_OFFSET + (gsize)img_sz > G_MAXUINT32) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "config offset would overflow");
			return FALSE;
		}
		if (!fu_synaptics_rmi_firmware_add_image(self,
							 "config",
							 stream,
							 RMI_IMG_FW_OFFSET + img_sz,
							 cfg_sz,
							 flags,
							 error))
			return FALSE;
	}
	return TRUE;
}

static gboolean
fu_synaptics_rmi_firmware_parse(FuFirmware *firmware,
				GInputStream *stream,
				FuFirmwareParseFlags flags,
				GError **error)
{
	FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware);
	gsize bufsz = 0;
	const guint8 *buf;
	g_autoptr(FuStructRmiImg) st_img = NULL;
	g_autoptr(GBytes) fw = NULL;

	/* maybe stream later */
	fw = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error);
	if (fw == NULL)
		return FALSE;
	buf = g_bytes_get_data(fw, &bufsz);

	/* sanity check */
	st_img = fu_struct_rmi_img_parse_stream(stream, 0x0, error);
	if (st_img == NULL)
		return FALSE;
	if (bufsz % 2 != 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "data not aligned to 16 bits");
		return FALSE;
	}
	if (bufsz < 4) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "stream was too small");
		return FALSE;
	}

	/* verify checksum */
	self->checksum = fu_struct_rmi_img_get_checksum(st_img);
	if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) {
		guint32 checksum_calculated =
		    fu_synaptics_rmi_generate_checksum(buf + 4, bufsz - 4);
		if (self->checksum != checksum_calculated) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "checksum verification failed, got 0x%08x, actual 0x%08x",
				    (guint)self->checksum,
				    (guint)checksum_calculated);
			return FALSE;
		}
	}

	/* parse legacy image */
	g_clear_pointer(&self->product_id, g_free);
	self->io = fu_struct_rmi_img_get_io_offset(st_img);
	self->bootloader_version = fu_struct_rmi_img_get_bootloader_version(st_img);
	if (self->io == 1) {
		self->build_id = fu_struct_rmi_img_get_fw_build_id(st_img);
		self->package_id = fu_struct_rmi_img_get_package_id(st_img);
	}
	self->product_id = fu_struct_rmi_img_get_product_id(st_img);
	self->product_info = fu_struct_rmi_img_get_product_info(st_img);
	fu_firmware_set_size(firmware, fu_struct_rmi_img_get_image_size(st_img));

	/* parse partitions, but ignore lockdown */
	switch (self->bootloader_version) {
	case 2:
	case 3:
	case 4:
	case 5:
	case 6:
		if ((self->io & 0x10) >> 1)
			self->sig_size = fu_struct_rmi_img_get_signature_size(st_img);
		if (!fu_synaptics_rmi_firmware_parse_v0x(self, stream, flags, error))
			return FALSE;
		self->kind = FU_SYNAPTICS_RMI_FIRMWARE_KIND_V0X;
		break;
	case 16:
	case 17:
		if (!fu_synaptics_rmi_firmware_parse_v10(self, stream, flags, error))
			return FALSE;
		self->kind = FU_SYNAPTICS_RMI_FIRMWARE_KIND_V10;
		break;
	default:
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "unsupported image version 0x%02x",
			    self->bootloader_version);
		return FALSE;
	}

	/* success */
	return TRUE;
}

guint32
fu_synaptics_rmi_firmware_get_sig_size(FuSynapticsRmiFirmware *self)
{
	return self->sig_size;
}

static GByteArray *
fu_synaptics_rmi_firmware_write_v0x(FuSynapticsRmiFirmware *self, GError **error)
{
	gsize bufsz = 0;
	guint32 csum;
	g_autoptr(FuFirmware) img = NULL;
	g_autoptr(GByteArray) buf = g_byte_array_new();
	g_autoptr(FuStructRmiImg) st_img = fu_struct_rmi_img_new();
	g_autoptr(GBytes) buf_blob = NULL;

	/* default image */
	img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), "ui", error);
	if (img == NULL)
		return NULL;
	buf_blob = fu_firmware_write(img, error);
	if (buf_blob == NULL)
		return NULL;
	bufsz = g_bytes_get_size(buf_blob);

	/* create empty block */
	fu_struct_rmi_img_set_bootloader_version(st_img, 0x2); /* not hierarchical */
	if (self->product_id != NULL) {
		if (!fu_struct_rmi_img_set_product_id(st_img, self->product_id, error))
			return NULL;
	}
	fu_struct_rmi_img_set_product_info(st_img, 0x1234);
	fu_struct_rmi_img_set_image_size(st_img, bufsz);
	fu_struct_rmi_img_set_config_size(st_img, bufsz);
	fu_byte_array_append_array(buf, st_img->buf);
	fu_byte_array_set_size(buf, RMI_IMG_FW_OFFSET + 0x4 + bufsz, 0x00);
	fu_memwrite_uint32(buf->data + RMI_IMG_FW_OFFSET, 0xDEAD, G_LITTLE_ENDIAN); /* img */
	fu_memwrite_uint32(buf->data + RMI_IMG_FW_OFFSET + bufsz,
			   0xBEEF,
			   G_LITTLE_ENDIAN); /* config */

	/* fixup checksum */
	csum = fu_synaptics_rmi_generate_checksum(buf->data + 4, buf->len - 4);
	fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_CHECKSUM, csum, G_LITTLE_ENDIAN);

	/* success */
	return g_steal_pointer(&buf);
}

static GByteArray *
fu_synaptics_rmi_firmware_write_v10(FuSynapticsRmiFirmware *self, GError **error)
{
	gsize bufsz;
	guint32 csum;
	g_autoptr(FuFirmware) img = NULL;
	g_autoptr(GByteArray) buf = g_byte_array_new();
	g_autoptr(FuStructRmiContainerDescriptor) st_dsc2 =
	    fu_struct_rmi_container_descriptor_new();
	g_autoptr(FuStructRmiContainerDescriptor) st_dsc = fu_struct_rmi_container_descriptor_new();
	g_autoptr(GBytes) buf_blob = NULL;

	/* header | desc_hdr | offset_table | desc | flash_config |
	 *        \0x0       \0x20          \0x24  \0x44          |0x48 */
	guint32 offset_table[] = {/* offset to first descriptor */
				  GUINT32_TO_LE(RMI_IMG_FW_OFFSET + 0x24),
				  /* nocheck:blocked */};
	fu_struct_rmi_container_descriptor_set_container_id(st_dsc,
							    FU_RMI_CONTAINER_ID_FLASH_CONFIG);
	fu_struct_rmi_container_descriptor_set_content_address(st_dsc, RMI_IMG_FW_OFFSET + 0x44);

	/* default image */
	img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), "ui", error);
	if (img == NULL)
		return NULL;
	buf_blob = fu_firmware_write(img, error);
	if (buf_blob == NULL)
		return NULL;
	bufsz = g_bytes_get_size(buf_blob);
	fu_struct_rmi_container_descriptor_set_content_length(st_dsc, bufsz);

	/* create empty block */
	fu_byte_array_set_size(buf, RMI_IMG_FW_OFFSET + 0x48, 0x00);
	buf->data[FU_STRUCT_RMI_IMG_OFFSET_IO_OFFSET] = 0x1;
	buf->data[FU_STRUCT_RMI_IMG_OFFSET_BOOTLOADER_VERSION] = 16; /* hierarchical */
	if (self->product_id != NULL) {
		gsize product_id_sz = strlen(self->product_id);
		if (!fu_memcpy_safe(buf->data,
				    buf->len,
				    FU_STRUCT_RMI_IMG_OFFSET_PRODUCT_ID, /* dst */
				    (const guint8 *)self->product_id,
				    product_id_sz,
				    0x0, /* src */
				    product_id_sz,
				    error))
			return NULL;
	}
	/* nocheck:memread */
	fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_FW_BUILD_ID,
			   0x1234,
			   G_LITTLE_ENDIAN);
	fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_PACKAGE_ID,
			   0x4321,
			   G_LITTLE_ENDIAN);
	fu_memwrite_uint16(buf->data + FU_STRUCT_RMI_IMG_OFFSET_PRODUCT_INFO,
			   0x3456,
			   G_LITTLE_ENDIAN);
	fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_IMAGE_SIZE, bufsz, G_LITTLE_ENDIAN);
	fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_CONFIG_SIZE,
			   bufsz,
			   G_LITTLE_ENDIAN);
	fu_memwrite_uint32(buf->data + RMI_IMG_V10_CNTR_ADDR_OFFSET,
			   RMI_IMG_FW_OFFSET,
			   G_LITTLE_ENDIAN);

	/* hierarchical section */
	fu_struct_rmi_container_descriptor_set_container_id(st_dsc2, FU_RMI_CONTAINER_ID_TOP_LEVEL);
	fu_struct_rmi_container_descriptor_set_content_length(st_dsc2, 0x1 * 4); /* bytes */
	fu_struct_rmi_container_descriptor_set_content_address(st_dsc2,
							       RMI_IMG_FW_OFFSET +
								   0x20); /* offset to table */
	memcpy(buf->data + RMI_IMG_FW_OFFSET + 0x00,			  /* nocheck:blocked */
	       st_dsc2->buf->data,
	       st_dsc2->buf->len);
	memcpy(buf->data + RMI_IMG_FW_OFFSET + 0x20, /* nocheck:blocked */
	       offset_table,
	       sizeof(offset_table));
	memcpy(buf->data + RMI_IMG_FW_OFFSET + 0x24, /* nocheck:blocked */
	       st_dsc->buf->data,
	       st_dsc->buf->len);
	fu_memwrite_uint32(buf->data + RMI_IMG_FW_OFFSET + 0x44,
			   0xfeed,
			   G_LITTLE_ENDIAN); /* flash_config */

	/* fixup checksum */
	csum = fu_synaptics_rmi_generate_checksum(buf->data + 4, buf->len - 4);
	fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_CHECKSUM, csum, G_LITTLE_ENDIAN);

	/* success */
	return g_steal_pointer(&buf);
}

static gboolean
fu_synaptics_rmi_firmware_build(FuFirmware *firmware, XbNode *n, GError **error)
{
	FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware);
	const gchar *product_id;
	guint64 tmp;

	/* either 0x or 10 */
	tmp = xb_node_query_text_as_uint(n, "kind", NULL);
	if (tmp != G_MAXUINT64)
		self->kind = tmp;

	/* any string */
	product_id = xb_node_query_text(n, "product_id", NULL);
	if (product_id != NULL) {
		gsize product_id_sz = strlen(product_id);
		if (product_id_sz > RMI_PRODUCT_ID_LENGTH) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "product_id not supported, %u of %u bytes",
				    (guint)product_id_sz,
				    (guint)RMI_PRODUCT_ID_LENGTH);
			return FALSE;
		}
		g_free(self->product_id);
		self->product_id = g_strdup(product_id);
	}

	/* success */
	return TRUE;
}

static GByteArray *
fu_synaptics_rmi_firmware_write(FuFirmware *firmware, GError **error)
{
	FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware);

	/* two supported container formats */
	if (self->kind == FU_SYNAPTICS_RMI_FIRMWARE_KIND_V0X)
		return fu_synaptics_rmi_firmware_write_v0x(self, error);
	if (self->kind == FU_SYNAPTICS_RMI_FIRMWARE_KIND_V10)
		return fu_synaptics_rmi_firmware_write_v10(self, error);

	/* not supported */
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "kind not supported");
	return NULL;
}

static void
fu_synaptics_rmi_firmware_init(FuSynapticsRmiFirmware *self)
{
	fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM);
	fu_firmware_add_image_gtype(FU_FIRMWARE(self), FU_TYPE_FIRMWARE);
	fu_firmware_set_images_max(FU_FIRMWARE(self), RMI_IMG_MAX_CONTAINERS);
	fu_firmware_set_size_max(FU_FIRMWARE(self), 16 * FU_MB);
}

static void
fu_synaptics_rmi_firmware_finalize(GObject *obj)
{
	FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(obj);
	g_free(self->product_id);
	G_OBJECT_CLASS(fu_synaptics_rmi_firmware_parent_class)->finalize(obj);
}

static void
fu_synaptics_rmi_firmware_class_init(FuSynapticsRmiFirmwareClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass);
	object_class->finalize = fu_synaptics_rmi_firmware_finalize;
	firmware_class->parse = fu_synaptics_rmi_firmware_parse;
	firmware_class->export = fu_synaptics_rmi_firmware_export;
	firmware_class->build = fu_synaptics_rmi_firmware_build;
	firmware_class->write = fu_synaptics_rmi_firmware_write;
}

static gboolean
fu_synaptics_rmi_firmware_parse_sbl_container_v10(FuSynapticsRmiFirmware *self,
						  GInputStream *stream,
						  const guint8 *buf,
						  gsize bufsz,
						  guint32 start_offset,
						  FuFirmwareParseFlags flags,
						  GError **error)
{
	guint32 cntr_addr;
	guint32 offset;
	guint32 cntrs_len;
	g_autoptr(FuStructRmiContainerDescriptor) st_dsc = NULL;

	if (!fu_memread_uint32_safe(buf, bufsz, start_offset, &cntr_addr, G_LITTLE_ENDIAN, error))
		return FALSE;

	st_dsc = fu_struct_rmi_container_descriptor_parse_stream(stream, cntr_addr, error);
	if (st_dsc == NULL) {
		g_prefix_error_literal(error, "RmiContainerDescriptor invalid: ");
		return FALSE;
	}

	offset = fu_struct_rmi_container_descriptor_get_content_address(st_dsc);
	cntrs_len = fu_struct_rmi_container_descriptor_get_content_length(st_dsc) / 4;
	if (cntrs_len > RMI_IMG_MAX_CONTAINERS) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "too many containers in file [%u], maximum is %u",
			    cntrs_len,
			    (guint)RMI_IMG_MAX_CONTAINERS);
		return FALSE;
	}
	for (guint32 i = 0; i < cntrs_len; i++) {
		guint32 addr_offset = offset + i * 4;
		guint16 container_id;
		guint32 addr;
		guint32 length;
		guint32 content_addr;
		guint32 signature_size;
		g_autoptr(FuStructRmiContainerDescriptor) st_dsc2 = NULL;

		if (!fu_memread_uint32_safe(buf, bufsz, addr_offset, &addr, G_LITTLE_ENDIAN, error))
			return FALSE;

		st_dsc2 = fu_struct_rmi_container_descriptor_parse_stream(stream, addr, error);
		if (st_dsc2 == NULL) {
			g_prefix_error_literal(error, "RmiContainerDescriptor invalid: ");
			return FALSE;
		}

		container_id = fu_struct_rmi_container_descriptor_get_container_id(st_dsc2);
		content_addr = fu_struct_rmi_container_descriptor_get_content_address(st_dsc2);
		length = fu_struct_rmi_container_descriptor_get_content_length(st_dsc2);
		signature_size = fu_struct_rmi_container_descriptor_get_signature_size(st_dsc2);

		g_debug("RmiContainerDescriptor 0x%02x @ 0x%x (len 0x%x) sig_size 0x%x",
			container_id,
			content_addr,
			length,
			signature_size);
		if (length == 0) {
			g_debug("no SBL image found");
			continue;
		}

		if (container_id == FU_RMI_CONTAINER_ID_BL_IMAGE) {
			if (signature_size != 0)
				g_debug("SBL signature size : 0x%x", signature_size);

			if (!fu_synaptics_rmi_firmware_add_image_v10(self,
								     "sbl",
								     stream,
								     content_addr,
								     length,
								     signature_size,
								     flags,
								     error)) {
				g_prefix_error_literal(error, "failed to add SBL image: ");
				return FALSE;
			}
		}
	}

	/* success */
	return TRUE;
}
