/*
 * Copyright (C) 2021 Dell Inc.
 * Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2020 Mario Limonciello <mario.limonciello@dell.com>
 * Copyright (C) 2017 Intel Corporation.
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#define G_LOG_DOMAIN "FuFirmware"

#include "config.h"

#include "fu-byte-array.h"
#include "fu-bytes.h"
#include "fu-intel-thunderbolt-nvm.h"
#include "fu-intel-thunderbolt-struct.h"
#include "fu-mem.h"
#include "fu-string.h"
#include "fu-version-common.h"

/**
 * FuIntelThunderboltNvm:
 *
 * The Non-Volatile-Memory device specification. This is what you would find on the device SPI chip.
 *
 * See also: [class@FuFirmware]
 */

typedef struct {
	guint32 sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_LAST];
	FuIntelThunderboltNvmFamily family;
	gboolean is_host;
	gboolean is_native;
	gboolean has_pd;
	guint16 vendor_id;
	guint16 device_id;
	guint16 model_id;
	guint gen;
	guint ports;
	guint8 flash_size;
} FuIntelThunderboltNvmPrivate;

G_DEFINE_TYPE_WITH_PRIVATE(FuIntelThunderboltNvm, fu_intel_thunderbolt_nvm, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_intel_thunderbolt_nvm_get_instance_private(o))

#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_AVAILABLE_SECTIONS 0x0002
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_UCODE		   0x0003
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DEVICE_ID	   0x0005
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_VERSION		   0x0009
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_HOST	   0x0010
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLASH_SIZE	   0x0045
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_ARC_PARAMS	   0x0075
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_IS_NATIVE	   0x007B
#define FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DROM		   0x010E

#define FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_VENDOR_ID 0x0010
#define FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_MODEL_ID  0x0012

#define FU_INTEL_THUNDERBOLT_NVM_ARC_PARAMS_OFFSET_PD_POINTER 0x010C

/**
 * fu_intel_thunderbolt_nvm_get_vendor_id:
 * @self: a #FuFirmware
 *
 * Gets the vendor ID.
 *
 * Returns: an integer, or 0x0 for unset
 *
 * Since: 1.8.5
 **/
guint16
fu_intel_thunderbolt_nvm_get_vendor_id(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), G_MAXUINT16);
	return priv->vendor_id;
}

/**
 * fu_intel_thunderbolt_nvm_get_device_id:
 * @self: a #FuFirmware
 *
 * Gets the device ID.
 *
 * Returns: an integer, or 0x0 for unset
 *
 * Since: 1.8.5
 **/
guint16
fu_intel_thunderbolt_nvm_get_device_id(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	return priv->device_id;
}

/**
 * fu_intel_thunderbolt_nvm_is_host:
 * @self: a #FuFirmware
 *
 * Gets if the firmware is designed for a host controller rather than a device.
 *
 * Returns: %TRUE for controller, %FALSE for device
 *
 * Since: 1.8.5
 **/
gboolean
fu_intel_thunderbolt_nvm_is_host(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), FALSE);
	return priv->is_host;
}

/**
 * fu_intel_thunderbolt_nvm_is_native:
 * @self: a #FuFirmware
 *
 * Gets if the device is native, i.e. not in recovery mode.
 *
 * Returns: %TRUE if set
 *
 * Since: 1.8.5
 **/
gboolean
fu_intel_thunderbolt_nvm_is_native(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), FALSE);
	return priv->is_native;
}

/**
 * fu_intel_thunderbolt_nvm_has_pd:
 * @self: a #FuFirmware
 *
 * Gets if the device has power delivery capability.
 *
 * Returns: %TRUE if set
 *
 * Since: 1.8.5
 **/
gboolean
fu_intel_thunderbolt_nvm_has_pd(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), FALSE);
	return priv->has_pd;
}

/**
 * fu_intel_thunderbolt_nvm_get_model_id:
 * @self: a #FuFirmware
 *
 * Gets the model ID.
 *
 * Returns: an integer, or 0x0 for unset
 *
 * Since: 1.8.5
 **/
guint16
fu_intel_thunderbolt_nvm_get_model_id(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), 0x0);
	return priv->model_id;
}

/**
 * fu_intel_thunderbolt_nvm_get_flash_size:
 * @self: a #FuFirmware
 *
 * Gets the flash size.
 *
 * NOTE: This does not correspond to a size in bytes, or a power of 2 and is only useful for
 * comparison between firmware and device.
 *
 * Returns: an integer, or 0x0 for unset
 *
 * Since: 1.8.5
 **/
guint8
fu_intel_thunderbolt_nvm_get_flash_size(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), 0x0);
	return priv->flash_size;
}

static void
fu_intel_thunderbolt_nvm_export(FuFirmware *firmware,
				FuFirmwareExportFlags flags,
				XbBuilderNode *bn)
{
	FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware);
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	fu_xmlb_builder_insert_kx(bn, "vendor_id", priv->vendor_id);
	fu_xmlb_builder_insert_kx(bn, "device_id", priv->device_id);
	fu_xmlb_builder_insert_kx(bn, "model_id", priv->model_id);
	fu_xmlb_builder_insert_kv(bn,
				  "family",
				  fu_intel_thunderbolt_nvm_family_to_string(priv->family));
	fu_xmlb_builder_insert_kb(bn, "is_host", priv->is_host);
	fu_xmlb_builder_insert_kb(bn, "is_native", priv->is_native);
	fu_xmlb_builder_insert_kx(bn, "flash_size", priv->flash_size);
	fu_xmlb_builder_insert_kx(bn, "generation", priv->gen);
	fu_xmlb_builder_insert_kx(bn, "ports", priv->ports);
	fu_xmlb_builder_insert_kb(bn, "has_pd", priv->has_pd);
	for (guint i = 0; i < FU_INTEL_THUNDERBOLT_NVM_SECTION_LAST; i++) {
		if (priv->sections[i] != 0x0) {
			g_autofree gchar *tmp = g_strdup_printf("0x%x", priv->sections[i]);
			g_autoptr(XbBuilderNode) bc =
			    xb_builder_node_insert(bn,
						   "section",
						   "type",
						   fu_intel_thunderbolt_nvm_section_to_string(i),
						   "offset",
						   tmp,
						   NULL);
			g_assert(bc != NULL);
		}
	}
}

static inline gboolean
fu_intel_thunderbolt_nvm_valid_pd_pointer(guint32 pointer)
{
	return pointer != 0 && pointer != 0xFFFFFFFF;
}

static gboolean
fu_intel_thunderbolt_nvm_read_uint8(FuIntelThunderboltNvm *self,
				    FuIntelThunderboltNvmSection section,
				    guint32 offset,
				    guint8 *value,
				    GError **error)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_autoptr(GBytes) fw = NULL;

	/* get blob and read */
	fw = fu_firmware_get_bytes(FU_FIRMWARE(self), error);
	if (fw == NULL)
		return FALSE;
	return fu_memread_uint8_safe(g_bytes_get_data(fw, NULL),
				     g_bytes_get_size(fw),
				     priv->sections[section] + offset,
				     value,
				     error);
}

static gboolean
fu_intel_thunderbolt_nvm_read_uint16(FuIntelThunderboltNvm *self,
				     FuIntelThunderboltNvmSection section,
				     guint32 offset,
				     guint16 *value,
				     GError **error)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_autoptr(GBytes) fw = NULL;

	/* get blob and read */
	fw = fu_firmware_get_bytes(FU_FIRMWARE(self), error);
	if (fw == NULL)
		return FALSE;
	return fu_memread_uint16_safe(g_bytes_get_data(fw, NULL),
				      g_bytes_get_size(fw),
				      priv->sections[section] + offset,
				      value,
				      G_LITTLE_ENDIAN,
				      error);
}

static gboolean
fu_intel_thunderbolt_nvm_read_uint32(FuIntelThunderboltNvm *self,
				     FuIntelThunderboltNvmSection section,
				     guint32 offset,
				     guint32 *value,
				     GError **error)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	g_autoptr(GBytes) fw = NULL;

	/* get blob and read */
	fw = fu_firmware_get_bytes(FU_FIRMWARE(self), error);
	if (fw == NULL)
		return FALSE;
	return fu_memread_uint32_safe(g_bytes_get_data(fw, NULL),
				      g_bytes_get_size(fw),
				      priv->sections[section] + offset,
				      value,
				      G_LITTLE_ENDIAN,
				      error);
}

/*
 * Size of ucode sections is uint16 value saved at the start of the section,
 * it's in DWORDS (4-bytes) units and it doesn't include itself. We need the
 * offset to the next section, so we translate it to bytes and add 2 for the
 * size field itself.
 *
 * offset parameter must be relative to digital section
 */
static gboolean
fu_intel_thunderbolt_nvm_read_ucode_section_len(FuIntelThunderboltNvm *self,
						guint32 offset,
						guint16 *value,
						GError **error)
{
	if (!fu_intel_thunderbolt_nvm_read_uint16(self,
						  FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
						  offset,
						  value,
						  error)) {
		g_prefix_error(error, "failed to read ucode section len: ");
		return FALSE;
	}
	*value *= sizeof(guint32);
	*value += sizeof(guint16);
	return TRUE;
}

/* assumes sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL].offset is already set */
static gboolean
fu_intel_thunderbolt_nvm_read_sections(FuIntelThunderboltNvm *self, GError **error)
{
	guint32 offset;
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);

	if (priv->gen >= 3 || priv->gen == 0) {
		if (!fu_intel_thunderbolt_nvm_read_uint32(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
			FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DROM,
			&offset,
			error))
			return FALSE;
		priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] =
		    offset + priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL];

		if (!fu_intel_thunderbolt_nvm_read_uint32(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
			FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_ARC_PARAMS,
			&offset,
			error))
			return FALSE;
		priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS] =
		    offset + priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL];
	}

	if (priv->is_host && priv->gen > 2) {
		/*
		 * To find the DRAM section, we have to jump from section to
		 * section in a chain of sections.
		 * available_sections location tells what sections exist at all
		 * (with a flag per section).
		 * ee_ucode_start_addr location tells the offset of the first
		 * section in the list relatively to the digital section start.
		 * After having the offset of the first section, we have a loop
		 * over the section list. If the section exists, we read its
		 * length (2 bytes at section start) and add it to current
		 * offset to find the start of the next section. Otherwise, we
		 * already have the next section offset...
		 */
		const guint8 DRAM_FLAG = 1 << 6;
		guint16 ucode_offset;
		guint8 available_sections = 0;

		if (!fu_intel_thunderbolt_nvm_read_uint8(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
			FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_AVAILABLE_SECTIONS,
			&available_sections,
			error)) {
			g_prefix_error(error, "failed to read available sections: ");
			return FALSE;
		}
		if (!fu_intel_thunderbolt_nvm_read_uint16(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
			FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_UCODE,
			&ucode_offset,
			error)) {
			g_prefix_error(error, "failed to read ucode offset: ");
			return FALSE;
		}
		offset = ucode_offset;
		if ((available_sections & DRAM_FLAG) == 0) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_FILE,
					    "cannot find needed FW sections in the FW image file");
			return FALSE;
		}

		for (guint8 i = 1; i < DRAM_FLAG; i <<= 1) {
			if (available_sections & i) {
				if (!fu_intel_thunderbolt_nvm_read_ucode_section_len(self,
										     offset,
										     &ucode_offset,
										     error))
					return FALSE;
				offset += ucode_offset;
			}
		}
		priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DRAM_UCODE] =
		    offset + priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL];
	}

	return TRUE;
}

static gboolean
fu_intel_thunderbolt_nvm_missing_needed_drom(FuIntelThunderboltNvm *self)
{
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	if (priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] != 0)
		return FALSE;
	if (priv->is_host && priv->gen < 3)
		return FALSE;
	return TRUE;
}

static gboolean
fu_intel_thunderbolt_nvm_parse(FuFirmware *firmware,
			       GBytes *fw,
			       gsize offset,
			       FwupdInstallFlags flags,
			       GError **error)
{
	FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware);
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	guint8 tmp = 0;
	guint16 version_raw = 0;
	struct {
		guint16 device_id;
		guint gen;
		FuIntelThunderboltNvmFamily family;
		guint ports;
	} hw_info_arr[] = {{0x156D, 2, FU_INTEL_THUNDERBOLT_NVM_FAMILY_FALCON_RIDGE, 2},
			   {0x156B, 2, FU_INTEL_THUNDERBOLT_NVM_FAMILY_FALCON_RIDGE, 1},
			   {0x157E, 2, FU_INTEL_THUNDERBOLT_NVM_FAMILY_WIN_RIDGE, 1},
			   {0x1578, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE, 2},
			   {0x1576, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE, 1},
			   {0x15C0, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE, 1},
			   {0x15D3, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE_C, 2},
			   {0x15DA, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE_C, 1},
			   {0x15E7, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE, 1},
			   {0x15EA, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE, 2},
			   {0x15EF, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE, 2},
			   {0x15EE, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_BB, 0},
			   {0x0B26, 4, FU_INTEL_THUNDERBOLT_NVM_FAMILY_GOSHEN_RIDGE, 2},
			   {0x5786, 5, FU_INTEL_THUNDERBOLT_NVM_FAMILY_BARLOW_RIDGE, 2},
			   /* Maple ridge devices
			    * NOTE: These are expected to be flashed via UEFI capsules *not*
			    * Thunderbolt plugin Flashing via fwupd will require matching kernel
			    * work. They're left here only for parsing the binaries
			    */
			   {0x1136, 4, FU_INTEL_THUNDERBOLT_NVM_FAMILY_MAPLE_RIDGE, 2},
			   {0x1137, 4, FU_INTEL_THUNDERBOLT_NVM_FAMILY_MAPLE_RIDGE, 2},
			   {0}};
	g_autofree gchar *version = NULL;
	g_autoptr(FuFirmware) img_payload = NULL;
	g_autoptr(GBytes) fw_payload = NULL;

	/* add this straight away */
	priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] = offset;

	/* is native */
	if (!fu_intel_thunderbolt_nvm_read_uint8(
		self,
		FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
		FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_IS_NATIVE,
		&tmp,
		error)) {
		g_prefix_error(error, "failed to read native: ");
		return FALSE;
	}
	priv->is_native = tmp & 0x20;

	/* we're only reading the first chunk */
	if (g_bytes_get_size(fw) == 0x80)
		return TRUE;

	/* host or device */
	if (!fu_intel_thunderbolt_nvm_read_uint8(self,
						 FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
						 FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_HOST,
						 &tmp,
						 error)) {
		g_prefix_error(error, "failed to read is-host: ");
		return FALSE;
	}
	priv->is_host = tmp & (1 << 1);

	/* device ID */
	if (!fu_intel_thunderbolt_nvm_read_uint16(self,
						  FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
						  FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DEVICE_ID,
						  &priv->device_id,
						  error)) {
		g_prefix_error(error, "failed to read device-id: ");
		return FALSE;
	}

	/* this is best-effort */
	for (guint i = 0; hw_info_arr[i].device_id != 0; i++) {
		if (hw_info_arr[i].device_id == priv->device_id) {
			priv->family = hw_info_arr[i].family;
			priv->gen = hw_info_arr[i].gen;
			priv->ports = hw_info_arr[i].ports;
			break;
		}
	}
	if (priv->family == FU_INTEL_THUNDERBOLT_NVM_FAMILY_UNKNOWN) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "unknown NVM family");
		return FALSE;
	}
	if (priv->ports == 0 && priv->is_host) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "unknown controller: %x",
			    priv->device_id);
		return FALSE;
	}

	/* read sections from file */
	if (!fu_intel_thunderbolt_nvm_read_sections(self, error))
		return FALSE;
	if (fu_intel_thunderbolt_nvm_missing_needed_drom(self)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_READ,
				    "cannot find required drom section");
		return FALSE;
	}

	/* vendor:model */
	if (priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] != 0x0) {
		if (!fu_intel_thunderbolt_nvm_read_uint16(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM,
			FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_VENDOR_ID,
			&priv->vendor_id,
			error)) {
			g_prefix_error(error, "failed to read vendor-id: ");
			return FALSE;
		}
		if (!fu_intel_thunderbolt_nvm_read_uint16(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM,
			FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_MODEL_ID,
			&priv->model_id,
			error)) {
			g_prefix_error(error, "failed to read model-id: ");
			return FALSE;
		}
	}

	/* versions */
	switch (priv->family) {
	case FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE:
	case FU_INTEL_THUNDERBOLT_NVM_FAMILY_GOSHEN_RIDGE:
	case FU_INTEL_THUNDERBOLT_NVM_FAMILY_BARLOW_RIDGE:
		if (!fu_intel_thunderbolt_nvm_read_uint16(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
			FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_VERSION,
			&version_raw,
			error)) {
			g_prefix_error(error, "failed to read version: ");
			return FALSE;
		}
		fu_firmware_set_version_raw(FU_FIRMWARE(self), version_raw);
		version = fu_version_from_uint16(version_raw, FWUPD_VERSION_FORMAT_BCD);
		fu_firmware_set_version(FU_FIRMWARE(self), version);
		break;
	default:
		break;
	}

	if (priv->is_host) {
		switch (priv->family) {
		case FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE:
		case FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE_C:
		case FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE:
			/* used for comparison between old and new image, not a raw number */
			if (!fu_intel_thunderbolt_nvm_read_uint8(
				self,
				FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL,
				FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLASH_SIZE,
				&tmp,
				error)) {
				g_prefix_error(error, "failed to read flash size: ");
				return FALSE;
			}
			priv->flash_size = tmp & 0x07;
			break;
		default:
			break;
		}
	}

	/* we're only reading enough to get the vendor-id and model-id */
	if (offset == 0x0 &&
	    g_bytes_get_size(fw) < priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS])
		return TRUE;

	/* has PD */
	if (priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS] != 0x0) {
		guint32 pd_pointer = 0x0;
		if (!fu_intel_thunderbolt_nvm_read_uint32(
			self,
			FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS,
			FU_INTEL_THUNDERBOLT_NVM_ARC_PARAMS_OFFSET_PD_POINTER,
			&pd_pointer,
			error)) {
			g_prefix_error(error, "failed to read pd-pointer: ");
			return FALSE;
		}
		priv->has_pd = fu_intel_thunderbolt_nvm_valid_pd_pointer(pd_pointer);
	}

	/* as as easy-to-grab payload blob */
	if (offset > 0) {
		fw_payload = fu_bytes_new_offset(fw, offset, g_bytes_get_size(fw) - offset, error);
		if (fw_payload == NULL)
			return FALSE;
	} else {
		fw_payload = g_bytes_ref(fw);
	}
	img_payload = fu_firmware_new_from_bytes(fw_payload);
	fu_firmware_set_id(img_payload, FU_FIRMWARE_ID_PAYLOAD);
	if (!fu_firmware_add_image_full(firmware, img_payload, error))
		return FALSE;

	/* success */
	return TRUE;
}

/* can only write version 3 NVM */
static GByteArray *
fu_intel_thunderbolt_nvm_write(FuFirmware *firmware, GError **error)
{
	FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware);
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	guint32 digital_size = 0x120;
	guint32 drom_offset = 0 + digital_size;
	guint32 drom_size = 0x20;
	guint32 arc_param_offset = drom_offset + drom_size;
	guint32 arc_param_size = 0x120;
	g_autoptr(GByteArray) buf = g_byte_array_new();

	/* minimal size */
	fu_byte_array_set_size(buf, arc_param_offset + arc_param_size, 0x0);

	/* digital section */
	if (!fu_memwrite_uint8_safe(buf->data,
				    buf->len,
				    FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_AVAILABLE_SECTIONS,
				    0x0,
				    error))
		return NULL;
	if (!fu_memwrite_uint16_safe(buf->data,
				     buf->len,
				     FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_UCODE,
				     0x0,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;
	if (!fu_memwrite_uint8_safe(buf->data,
				    buf->len,
				    FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_IS_NATIVE,
				    priv->is_native ? 0x20 : 0x0,
				    error))
		return NULL;
	if (!fu_memwrite_uint8_safe(buf->data,
				    buf->len,
				    FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_HOST,
				    priv->is_host ? 0x2 : 0x0,
				    error))
		return NULL;
	if (!fu_memwrite_uint32_safe(buf->data,
				     buf->len,
				     FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DEVICE_ID,
				     priv->device_id,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;
	if (!fu_memwrite_uint16_safe(buf->data,
				     buf->len,
				     FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_VERSION,
				     fu_firmware_get_version_raw(firmware),
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;
	if (!fu_memwrite_uint8_safe(buf->data,
				    buf->len,
				    FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLASH_SIZE,
				    priv->flash_size,
				    error))
		return NULL;

	/* drom section */
	if (!fu_memwrite_uint32_safe(buf->data,
				     buf->len,
				     FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DROM,
				     drom_offset,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;
	if (!fu_memwrite_uint16_safe(buf->data,
				     buf->len,
				     drom_offset + FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_VENDOR_ID,
				     priv->vendor_id,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;
	if (!fu_memwrite_uint16_safe(buf->data,
				     buf->len,
				     drom_offset + FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_MODEL_ID,
				     priv->model_id,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;

	/* ARC param section */
	if (!fu_memwrite_uint32_safe(buf->data,
				     buf->len,
				     FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_ARC_PARAMS,
				     arc_param_offset,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;
	if (!fu_memwrite_uint32_safe(buf->data,
				     buf->len,
				     arc_param_offset +
					 FU_INTEL_THUNDERBOLT_NVM_ARC_PARAMS_OFFSET_PD_POINTER,
				     priv->has_pd ? 0x1 : 0x0,
				     G_LITTLE_ENDIAN,
				     error))
		return NULL;

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

static gboolean
fu_intel_thunderbolt_nvm_check_compatible(FuFirmware *firmware,
					  FuFirmware *firmware_other,
					  FwupdInstallFlags flags,
					  GError **error)
{
	FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware);
	FuIntelThunderboltNvm *other = FU_INTEL_THUNDERBOLT_NVM(firmware_other);
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	FuIntelThunderboltNvmPrivate *priv_other = GET_PRIVATE(other);

	if (priv->is_host != priv_other->is_host) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "incorrect firmware mode, got %s, expected %s",
			    priv->is_host ? "host" : "device",
			    priv_other->is_host ? "host" : "device");
		return FALSE;
	}
	if (priv->vendor_id != priv_other->vendor_id) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "incorrect device vendor, got 0x%04x, expected 0x%04x",
			    priv->vendor_id,
			    priv_other->vendor_id);
		return FALSE;
	}
	if (priv->device_id != priv_other->device_id) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "incorrect device type, got 0x%04x, expected 0x%04x",
			    priv->device_id,
			    priv_other->device_id);
		return FALSE;
	}
	if ((flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) == 0) {
		if (priv->model_id != priv_other->model_id) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "incorrect device model, got 0x%04x, expected 0x%04x",
				    priv->model_id,
				    priv_other->model_id);
			return FALSE;
		}
		/* old firmware has PD but new doesn't (we don't care about other way around) */
		if (priv->has_pd && !priv_other->has_pd) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_FILE,
					    "incorrect PD section");
			return FALSE;
		}
		if (priv->flash_size != priv_other->flash_size) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "incorrect flash size, got 0x%x and expected 0x%x",
				    priv->flash_size,
				    priv_other->flash_size);
			return FALSE;
		}
	}
	return TRUE;
}

static gboolean
fu_intel_thunderbolt_nvm_build(FuFirmware *firmware, XbNode *n, GError **error)
{
	FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware);
	FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self);
	const gchar *tmp;

	/* simple properties */
	tmp = xb_node_query_text(n, "vendor_id", NULL);
	if (tmp != NULL) {
		guint64 val = 0;
		if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, error))
			return FALSE;
		priv->vendor_id = val;
	}
	tmp = xb_node_query_text(n, "device_id", NULL);
	if (tmp != NULL) {
		guint64 val = 0;
		if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, error))
			return FALSE;
		priv->device_id = val;
	}
	tmp = xb_node_query_text(n, "model_id", NULL);
	if (tmp != NULL) {
		guint64 val = 0;
		if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, error))
			return FALSE;
		priv->model_id = val;
	}
	tmp = xb_node_query_text(n, "family", NULL);
	if (tmp != NULL) {
		priv->family = fu_intel_thunderbolt_nvm_family_from_string(tmp);
		if (priv->family == FU_INTEL_THUNDERBOLT_NVM_FAMILY_UNKNOWN) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "unknown family: %s",
				    tmp);
			return FALSE;
		}
	}
	tmp = xb_node_query_text(n, "flash_size", NULL);
	if (tmp != NULL) {
		guint64 val = 0;
		if (!fu_strtoull(tmp, &val, 0x0, 0x07, error))
			return FALSE;
		priv->flash_size = val;
	}
	tmp = xb_node_query_text(n, "is_host", NULL);
	if (tmp != NULL) {
		if (!fu_strtobool(tmp, &priv->is_host, error))
			return FALSE;
	}
	tmp = xb_node_query_text(n, "is_native", NULL);
	if (tmp != NULL) {
		if (!fu_strtobool(tmp, &priv->is_native, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

static void
fu_intel_thunderbolt_nvm_init(FuIntelThunderboltNvm *self)
{
	fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID);
	fu_firmware_set_images_max(FU_FIRMWARE(self), 1024);
}

static void
fu_intel_thunderbolt_nvm_class_init(FuIntelThunderboltNvmClass *klass)
{
	FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
	klass_firmware->export = fu_intel_thunderbolt_nvm_export;
	klass_firmware->parse = fu_intel_thunderbolt_nvm_parse;
	klass_firmware->write = fu_intel_thunderbolt_nvm_write;
	klass_firmware->build = fu_intel_thunderbolt_nvm_build;
	klass_firmware->check_compatible = fu_intel_thunderbolt_nvm_check_compatible;
}

/**
 * fu_intel_thunderbolt_nvm_new:
 *
 * Creates a new #FuFirmware of Intel NVM format
 *
 * Since: 1.8.5
 **/
FuFirmware *
fu_intel_thunderbolt_nvm_new(void)
{
	return FU_FIRMWARE(g_object_new(FU_TYPE_INTEL_THUNDERBOLT_NVM, NULL));
}
