/*
 * Copyright 2015 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

/**
 * FuDfuTarget:
 *
 * This object allows uploading and downloading an image onto a
 * specific DFU-capable target.
 *
 * You only need to use this in preference to #FuDfuDevice if you only
 * want to update one target on the device. Most users will want to
 * update all the targets on the device at the same time.
 *
 * See also: [class@FuDfuDevice], [class@FuFirmware]
 */

#include "config.h"

#include <math.h>
#include <string.h>

#include "fu-dfu-common.h"
#include "fu-dfu-device.h"
#include "fu-dfu-sector.h"
#include "fu-dfu-target-private.h" /* waive-pre-commit */

#define DFU_TARGET_MANIFEST_MAX_POLLING_TRIES 200

typedef struct {
	gboolean done_setup;
	guint8 alt_setting;
	guint8 alt_idx;
	GPtrArray *sectors; /* of FuDfuSector */
} FuDfuTargetPrivate;

G_DEFINE_TYPE_WITH_PRIVATE(FuDfuTarget, fu_dfu_target, FU_TYPE_DEVICE)
#define GET_PRIVATE(o) (fu_dfu_target_get_instance_private(o))

static void
fu_dfu_target_init(FuDfuTarget *self)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	priv->sectors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_PARENT_NAME_PREFIX);
	fu_device_set_proxy_gtype(FU_DEVICE(self), FU_TYPE_DFU_DEVICE);
}

static void
fu_dfu_target_finalize(GObject *object)
{
	FuDfuTarget *self = FU_DFU_TARGET(object);
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);

	g_ptr_array_unref(priv->sectors);

	G_OBJECT_CLASS(fu_dfu_target_parent_class)->finalize(object);
}

static void
fu_dfu_target_to_string(FuDevice *device, guint idt, GString *str)
{
	FuDfuTarget *self = FU_DFU_TARGET(device);
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	fwupd_codec_string_append_hex(str, idt, "AltSetting", priv->alt_setting);
	fwupd_codec_string_append_hex(str, idt, "AltIdx", priv->alt_idx);
	for (guint i = 0; i < priv->sectors->len; i++) {
		FuDfuSector *sector = g_ptr_array_index(priv->sectors, i);
		g_autofree gchar *tmp1 = g_strdup_printf("Idx%02x", i);
		g_autofree gchar *tmp2 = fu_dfu_sector_to_string(sector);
		fwupd_codec_string_append(str, idt + 1, tmp1, tmp2);
	}
}

FuDfuSector *
fu_dfu_target_get_sector_for_addr(FuDfuTarget *self, guint32 addr)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);

	for (guint i = 0; i < priv->sectors->len; i++) {
		FuDfuSector *sector = g_ptr_array_index(priv->sectors, i);
		if (addr < fu_dfu_sector_get_address(sector))
			continue;
		if (addr > fu_dfu_sector_get_address(sector) + fu_dfu_sector_get_size(sector))
			continue;
		return sector;
	}
	return NULL;
}

static gboolean
fu_dfu_target_parse_sector(FuDfuTarget *self,
			   const gchar *dfuse_sector_id,
			   guint32 *addr,
			   guint16 zone,
			   guint16 number,
			   GError **error)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	FuDfuSectorCap cap = FU_DFU_SECTOR_CAP_NONE;
	FuDevice *proxy;
	gchar *tmp;
	guint32 addr_offset = 0;
	guint64 nr_sectors;
	guint64 sector_size;

	/* parse # of sectors */
	nr_sectors = g_ascii_strtoull(dfuse_sector_id, &tmp, 10); /* nocheck:blocked */
	if (nr_sectors > 999) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "invalid number of sectors: %s",
			    dfuse_sector_id);
		return FALSE;
	}

	/* check this is the delimiter */
	if (tmp[0] != '*') {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "invalid sector ID: %s",
			    dfuse_sector_id);
		return FALSE;
	}

	/* parse sector size */
	sector_size = g_ascii_strtoull(tmp + 1, &tmp, 10); /* nocheck:blocked */
	if (sector_size > 999) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "invalid sector size: %s",
			    dfuse_sector_id);
		return FALSE;
	}

	/* handle weirdness */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_ABSENT_SECTOR_SIZE)) {
		if (tmp[1] == '\0') {
			tmp[1] = tmp[0];
			tmp[0] = 'B';
		}
	}

	/* get multiplier */
	switch (tmp[0]) {
	case 'B': /* byte */
	case ' ': /* byte, ST reference bootloader :/ */
		break;
	case 'K': /* kilo */
		sector_size *= FU_KB;
		break;
	case 'M': /* mega */
		sector_size *= FU_MB;
		break;
	default:
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "invalid sector multiplier: %s",
			    tmp);
		return FALSE;
	}

	/* get sector type */
	switch (tmp[1]) {
	case 'a':
		cap = FU_DFU_SECTOR_CAP_READABLE;
		break;
	case 'b':
		cap = FU_DFU_SECTOR_CAP_ERASABLE;
		break;
	case 'c':
		cap = FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_ERASABLE;
		break;
	case 'd':
		cap = FU_DFU_SECTOR_CAP_WRITABLE;
		break;
	case 'e':
		cap = FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_WRITABLE;
		break;
	case 'f':
		cap = FU_DFU_SECTOR_CAP_ERASABLE | FU_DFU_SECTOR_CAP_WRITABLE;
		break;
	case 'g':
		cap = FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_ERASABLE |
		      FU_DFU_SECTOR_CAP_WRITABLE;
		break;
	default:
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "invalid sector type: %s",
			    tmp);
		return FALSE;
	}

	/* add all the sectors */
	for (guint i = 0; i < nr_sectors; i++) {
		FuDfuSector *sector;
		gsize sector_size_val;
		sector = fu_dfu_sector_new(*addr + addr_offset,
					   (guint32)sector_size,
					   (guint32)((nr_sectors * sector_size) - addr_offset),
					   zone,
					   number,
					   cap);
		g_ptr_array_add(priv->sectors, sector);

		/* sanity check */
		sector_size_val = fu_dfu_sector_get_size(sector);
		if (sector_size_val > G_MAXUINT32 - addr_offset) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "overflow at sector %u",
				    i);
			return FALSE;
		}
		addr_offset += sector_size_val;
	}

	/* update for next sector */
	*addr += addr_offset;
	return TRUE;
}

gboolean
fu_dfu_target_parse_sectors(FuDfuTarget *self, const gchar *alt_name, GError **error)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	g_auto(GStrv) zones = NULL;

	/* not set */
	if (alt_name == NULL)
		return TRUE;

	/* from the Neo Freerunner */
	if (g_str_has_prefix(alt_name, "RAM 0x")) {
		FuDfuSector *sector;
		guint64 addr_tmp = 0;
		if (!fu_strtoull(alt_name + 6,
				 &addr_tmp,
				 0,
				 G_MAXUINT32,
				 FU_INTEGER_BASE_16,
				 error))
			return FALSE;
		g_debug("RAM description, so parsing");
		sector = fu_dfu_sector_new((guint32)addr_tmp,
					   0x0, /* size */
					   0x0, /* size_left */
					   0x0, /* zone */
					   0x0, /* number */
					   FU_DFU_SECTOR_CAP_ERASABLE | FU_DFU_SECTOR_CAP_READABLE |
					       FU_DFU_SECTOR_CAP_WRITABLE);
		g_ptr_array_add(priv->sectors, sector);
	}

	/* not a DfuSe alternative name */
	if (alt_name[0] != '@')
		return TRUE;

	/* clear any existing zones */
	g_ptr_array_set_size(priv->sectors, 0);

	/* parse zones */
	zones = g_strsplit(alt_name, "/", -1);
	fu_device_set_name(FU_DEVICE(self), g_strchomp(zones[0] + 1));
	for (guint i = 1; zones[i] != NULL; i += 2) {
		guint32 addr;
		guint64 addr_tmp = 0;
		g_auto(GStrv) sectors = NULL;

		/* parse address */
		if (!g_str_has_prefix(zones[i], "0x")) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "No sector address");
			return FALSE;
		}
		if (!fu_strtoull(zones[i] + 2,
				 &addr_tmp,
				 0,
				 G_MAXUINT32,
				 FU_INTEGER_BASE_16,
				 error)) {
			g_prefix_error_literal(error, "sector address invalid: ");
			return FALSE;
		}
		addr = (guint32)addr_tmp;

		/* no sectors?! */
		if (zones[i + 1] == NULL) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "No sector section");
			return FALSE;
		}

		/* parse sectors */
		sectors = g_strsplit(zones[i + 1], ",", -1);
		for (guint16 j = 0; sectors[j] != NULL; j++) {
			if (!fu_dfu_target_parse_sector(self,
							sectors[j],
							&addr,
							(i - 1) / 2,
							j,
							error)) {
				g_prefix_error(error, "failed to parse: '%s': ", sectors[j]);
				return FALSE;
			}
		}
	}

	/* success */
	return TRUE;
}

/**
 * fu_dfu_target_new: (skip)
 *
 * Creates a new DFU target, which represents an alt-setting on a
 * DFU-capable device.
 *
 * Returns: a #FuDfuTarget
 **/
FuDfuTarget *
fu_dfu_target_new(void)
{
	FuDfuTarget *self;
	self = g_object_new(FU_TYPE_DFU_TARGET, NULL);
	return self;
}

/**
 * fu_dfu_target_get_sectors:
 * @self: a #FuDfuTarget
 *
 * Gets the sectors exported by the target.
 *
 * Returns: (transfer none) (element-type FuDfuSector): sectors
 **/
GPtrArray *
fu_dfu_target_get_sectors(FuDfuTarget *self)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_DFU_TARGET(self), NULL);
	return priv->sectors;
}

/**
 * fu_dfu_target_get_sector_default:
 * @self: a #FuDfuTarget
 *
 * Gets the default (first) sector exported by the target.
 *
 * Returns: (transfer none): a #FuDfuSector, or %NULL
 **/
FuDfuSector *
fu_dfu_target_get_sector_default(FuDfuTarget *self)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_DFU_TARGET(self), NULL);
	if (priv->sectors->len == 0)
		return NULL;
	return FU_DFU_SECTOR(g_ptr_array_index(priv->sectors, 0));
}

/**
 * fu_dfu_target_status_to_error_msg:
 * @status: a #FuDfuStatus, e.g. %FU_DFU_STATUS_ERR_ERASE
 *
 * Converts an enumerated value to an error description.
 *
 * Returns: a string
 **/
static const gchar *
fu_dfu_target_status_to_error_msg(FuDfuStatus status)
{
	if (status == FU_DFU_STATUS_OK)
		return "No error condition is present";
	if (status == FU_DFU_STATUS_ERR_TARGET)
		return "Firmware is not for designed this device";
	if (status == FU_DFU_STATUS_ERR_FILE)
		return "Firmware is for this device but fails verification";
	if (status == FU_DFU_STATUS_ERR_WRITE)
		return "Device is unable to write memory";
	if (status == FU_DFU_STATUS_ERR_ERASE)
		return "Memory erase function failed";
	if (status == FU_DFU_STATUS_ERR_CHECK_ERASED)
		return "Memory erase check failed";
	if (status == FU_DFU_STATUS_ERR_PROG)
		return "Program memory function failed";
	if (status == FU_DFU_STATUS_ERR_VERIFY)
		return "Programmed memory failed verification";
	if (status == FU_DFU_STATUS_ERR_ADDRESS)
		return "Cannot program memory due to address out of range";
	if (status == FU_DFU_STATUS_ERR_NOTDONE)
		return "Received zero-length download but data is incomplete";
	if (status == FU_DFU_STATUS_ERR_FIRMWARE)
		return "Device firmware is corrupt";
	if (status == FU_DFU_STATUS_ERR_VENDOR)
		return "Vendor-specific error";
	if (status == FU_DFU_STATUS_ERR_USBR)
		return "Device detected unexpected USB reset signaling";
	if (status == FU_DFU_STATUS_ERR_POR)
		return "Device detected unexpected power on reset";
	if (status == FU_DFU_STATUS_ERR_UNKNOWN)
		return "Something unexpected went wrong";
	if (status == FU_DFU_STATUS_ERR_STALLDPKT)
		return "Device stalled an unexpected request";
	return "Unknown error";
}

static gboolean
fu_dfu_target_manifest_wait(FuDfuTarget *self, GError **error)
{
	FuDevice *proxy;
	guint polling_count = 0;

	/* get the status */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_dfu_device_refresh(FU_DFU_DEVICE(proxy), 0, error))
		return FALSE;

	/* wait for FU_DFU_STATE_DFU_MANIFEST to not be set */
	while (fu_dfu_device_get_state(FU_DFU_DEVICE(proxy)) == FU_DFU_STATE_DFU_MANIFEST_SYNC ||
	       fu_dfu_device_get_state(FU_DFU_DEVICE(proxy)) == FU_DFU_STATE_DFU_MANIFEST) {
		g_debug("waiting for FU_DFU_STATE_DFU_MANIFEST to clear");

		if (polling_count++ > DFU_TARGET_MANIFEST_MAX_POLLING_TRIES) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "reach to max polling tries");
			return FALSE;
		}

		fu_device_sleep(proxy,
				fu_dfu_device_get_download_timeout(FU_DFU_DEVICE(proxy)) + 1000);
		if (!fu_dfu_device_refresh(FU_DFU_DEVICE(proxy), 0, error))
			return FALSE;
	}

	/* in an error state */
	if (fu_dfu_device_get_state(FU_DFU_DEVICE(proxy)) == FU_DFU_STATE_DFU_ERROR) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    fu_dfu_target_status_to_error_msg(
					fu_dfu_device_get_status(FU_DFU_DEVICE(proxy))));
		return FALSE;
	}

	return TRUE;
}

gboolean
fu_dfu_target_check_status(FuDfuTarget *self, GError **error)
{
	FuDevice *proxy;
	FuDfuStatus status;
	g_autoptr(GTimer) timer = g_timer_new();

	/* get the status */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_dfu_device_refresh(FU_DFU_DEVICE(proxy), 0, error))
		return FALSE;

	/* wait for dfuDNBUSY to not be set */
	while (fu_dfu_device_get_state(FU_DFU_DEVICE(proxy)) == FU_DFU_STATE_DFU_DNBUSY) {
		g_debug("waiting for FU_DFU_STATE_DFU_DNBUSY to clear");
		fu_device_sleep(proxy, fu_dfu_device_get_download_timeout(FU_DFU_DEVICE(proxy)));
		if (!fu_dfu_device_refresh(FU_DFU_DEVICE(proxy), 0, error))
			return FALSE;
		/* this is a really long time to save fwupd in case
		 * the device has got wedged */
		if (g_timer_elapsed(timer, NULL) > 120.f) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "Stuck in DFU_DNBUSY");
			return FALSE;
		}
	}

	/* not in an error state */
	if (fu_dfu_device_get_state(FU_DFU_DEVICE(proxy)) != FU_DFU_STATE_DFU_ERROR)
		return TRUE;

	/* STM32-specific long errors */
	status = fu_dfu_device_get_status(FU_DFU_DEVICE(proxy));
	if (fu_dfu_device_get_version(FU_DFU_DEVICE(proxy)) == FU_DFU_FIRMARE_VERSION_DFUSE) {
		if (status == FU_DFU_STATUS_ERR_VENDOR) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "read protection is active");
			return FALSE;
		}
		if (status == FU_DFU_STATUS_ERR_TARGET) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "address is wrong or unsupported");
			return FALSE;
		}
	}

	/* use a proper error description */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    fu_dfu_target_status_to_error_msg(status));
	return FALSE;
}

/**
 * fu_dfu_target_use_alt_setting:
 * @self: a #FuDfuTarget
 * @error: (nullable): optional return location for an error
 *
 * Opens a DFU-capable target.
 *
 * Returns: %TRUE for success
 **/
static gboolean
fu_dfu_target_use_alt_setting(FuDfuTarget *self, GError **error)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	FuDevice *proxy;
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* use the correct setting */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (fu_device_has_flag(proxy, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		if (!fu_usb_device_set_interface_alt(
			FU_USB_DEVICE(proxy),
			fu_dfu_device_get_interface(FU_DFU_DEVICE(proxy)),
			priv->alt_setting,
			&error_local)) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "cannot set alternate setting 0x%02x on interface %i: %s",
				    priv->alt_setting,
				    fu_dfu_device_get_interface(FU_DFU_DEVICE(proxy)),
				    error_local->message);
			return FALSE;
		}
	}

	return TRUE;
}

/**
 * fu_dfu_target_setup:
 * @self: a #FuDfuTarget
 * @error: (nullable): optional return location for an error
 *
 * Opens a DFU-capable target.
 *
 * Returns: %TRUE for success
 **/
gboolean
fu_dfu_target_setup(FuDfuTarget *self, GError **error)
{
	FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self);
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	FuDevice *proxy;

	g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* already done */
	if (priv->done_setup)
		return TRUE;

	/* superclassed */
	if (klass->setup != NULL) {
		if (!klass->setup(self, error))
			return FALSE;
	}

	/* GD32VF103 devices features and peripheral list */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (priv->alt_setting == 0x0 &&
	    fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_GD32)) {
		/*             RB R8 R6 R4  VB V8
		 * Flash (KB) 128 64 32 16 128 64
		 *             TB T8 T6 T4  CB C8 C6 C4
		 * Flash (KB) 128 64 32 16 128 64 32 16
		 */
		const gchar *serial = fu_device_get_serial(proxy);
		if (serial == NULL || strlen(serial) < 4 || serial[3] != 'J') {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "GD32 serial number %s invalid",
				    serial);
			return FALSE;
		}
		if (serial[2] == '2') {
			fu_device_set_logical_id(FU_DEVICE(self),
						 "@Internal Flash  /0x8000000/8*1Kg");
		} else if (serial[2] == '4') {
			fu_device_set_logical_id(FU_DEVICE(self),
						 "@Internal Flash  /0x8000000/16*1Kg");
		} else if (serial[2] == '6') {
			fu_device_set_logical_id(FU_DEVICE(self),
						 "@Internal Flash  /0x8000000/32*1Kg");
		} else if (serial[2] == '8') {
			fu_device_set_logical_id(FU_DEVICE(self),
						 "@Internal Flash  /0x8000000/64*1Kg");
		} else if (serial[2] == 'B') {
			fu_device_set_logical_id(FU_DEVICE(self),
						 "@Internal Flash  /0x8000000/128*1Kg");
		} else if (serial[2] == 'D') {
			fu_device_set_logical_id(FU_DEVICE(self),
						 "@Internal Flash  /0x8000000/256*1Kg");
		} else {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "Unknown GD32 sector size: %c",
				    serial[2]);
			return FALSE;
		}
	}

	/* get string */
	if (priv->alt_idx != 0x00 && fu_device_get_logical_id(FU_DEVICE(self)) == NULL) {
		g_autofree gchar *alt_name = NULL;
		alt_name =
		    fu_usb_device_get_string_descriptor(FU_USB_DEVICE(proxy), priv->alt_idx, NULL);
		fu_device_set_logical_id(FU_DEVICE(self), alt_name);
	}

	/* parse the DfuSe format according to UM0424 */
	if (priv->sectors->len == 0) {
		if (!fu_dfu_target_parse_sectors(self,
						 fu_device_get_logical_id(FU_DEVICE(self)),
						 error))
			return FALSE;
	}

	/* add a dummy entry */
	if (priv->sectors->len == 0) {
		FuDfuSector *sector;
		sector = fu_dfu_sector_new(0x0, /* addr */
					   0x0, /* size */
					   0x0, /* size_left */
					   0x0, /* zone */
					   0x0, /* number */
					   FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_WRITABLE);
		g_debug("no UM0424 sector description in %s",
			fu_device_get_logical_id(FU_DEVICE(self)));
		g_ptr_array_add(priv->sectors, sector);
	}

	priv->done_setup = TRUE;
	return TRUE;
}

gboolean
fu_dfu_target_download_chunk(FuDfuTarget *self,
			     guint16 index,
			     GByteArray *buf,
			     guint timeout_ms,
			     FuProgress *progress,
			     GError **error)
{
	FuDevice *proxy;
	g_autoptr(GError) error_local = NULL;
	gsize actual_length;

	/* fall back to default */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (timeout_ms == 0)
		timeout_ms = fu_dfu_device_get_timeout(FU_DFU_DEVICE(proxy));

	/* low level packet debugging */
	fu_dump_raw(G_LOG_DOMAIN, "Message", buf->data, buf->len);
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_CLASS,
					    FU_USB_RECIPIENT_INTERFACE,
					    FU_DFU_REQUEST_DNLOAD,
					    index,
					    fu_dfu_device_get_interface(FU_DFU_DEVICE(proxy)),
					    buf->data,
					    buf->len,
					    &actual_length,
					    timeout_ms,
					    NULL,
					    &error_local)) {
		/* refresh the error code */
		fu_dfu_device_error_fixup(FU_DFU_DEVICE(proxy), &error_local);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "cannot download data: %s",
			    error_local->message);
		return FALSE;
	}

	/* for STM32 devices, the action only occurs when we do GetStatus --
	 * and it can take a long time to complete! */
	if (fu_dfu_device_get_version(FU_DFU_DEVICE(proxy)) == FU_DFU_FIRMARE_VERSION_DFUSE) {
		if (!fu_dfu_device_refresh(FU_DFU_DEVICE(proxy), 35000, error))
			return FALSE;
	}

	/* wait for the device to write contents to the EEPROM */
	if (buf->len == 0 && fu_dfu_device_get_download_timeout(FU_DFU_DEVICE(proxy)) > 0)
		fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY);
	if (fu_dfu_device_get_download_timeout(FU_DFU_DEVICE(proxy)) > 0) {
		g_debug("sleeping for %ums…",
			fu_dfu_device_get_download_timeout(FU_DFU_DEVICE(proxy)));
		fu_device_sleep(FU_DEVICE(proxy),
				fu_dfu_device_get_download_timeout(FU_DFU_DEVICE(proxy)));
	}

	/* find out if the write was successful, waiting for BUSY to clear */
	if (!fu_dfu_target_check_status(self, error)) {
		g_prefix_error_literal(error, "cannot wait for busy: ");
		return FALSE;
	}

	g_assert_cmpint(actual_length, ==, buf->len);
	return TRUE;
}

GBytes *
fu_dfu_target_upload_chunk(FuDfuTarget *self,
			   guint16 index,
			   gsize buf_sz,
			   FuProgress *progress,
			   GError **error)
{
	FuDevice *proxy;
	g_autoptr(GError) error_local = NULL;
	guint8 *buf;
	gsize actual_length;

	/* unset */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return NULL;
	if (buf_sz == 0)
		buf_sz = (gsize)fu_dfu_device_get_transfer_size(FU_DFU_DEVICE(proxy));

	buf = g_new0(guint8, buf_sz);
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_CLASS,
					    FU_USB_RECIPIENT_INTERFACE,
					    FU_DFU_REQUEST_UPLOAD,
					    index,
					    fu_dfu_device_get_interface(FU_DFU_DEVICE(proxy)),
					    buf,
					    buf_sz,
					    &actual_length,
					    fu_dfu_device_get_timeout(FU_DFU_DEVICE(proxy)),
					    NULL,
					    &error_local)) {
		/* refresh the error code */
		fu_dfu_device_error_fixup(FU_DFU_DEVICE(proxy), &error_local);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "cannot upload data: %s",
			    error_local->message);
		return NULL;
	}

	/* low level packet debugging */
	fu_dump_raw(G_LOG_DOMAIN, "Message", buf, actual_length);

	return g_bytes_new_take(buf, actual_length);
}

void
fu_dfu_target_set_alt_idx(FuDfuTarget *self, guint8 alt_idx)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	priv->alt_idx = alt_idx;
}

void
fu_dfu_target_set_alt_setting(FuDfuTarget *self, guint8 alt_setting)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	priv->alt_setting = alt_setting;
}

gboolean
fu_dfu_target_attach(FuDfuTarget *self, FuProgress *progress, GError **error)
{
	FuDevice *proxy;
	FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self);

	/* ensure populated */
	if (!fu_dfu_target_setup(self, error))
		return FALSE;

	/* implemented as part of a superclass */
	if (klass->attach != NULL)
		return klass->attach(self, progress, error);

	/* normal DFU mode just needs a bus reset */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	return fu_dfu_device_reset(FU_DFU_DEVICE(proxy), progress, error);
}

static FuChunk *
fu_dfu_target_upload_element_dfu(FuDfuTarget *self,
				 guint32 address,
				 gsize expected_size,
				 gsize maximum_size,
				 FuProgress *progress,
				 GError **error)
{
	FuDfuDevice *proxy;
	GBytes *chunk_tmp;
	guint percentage_size = expected_size > 0 ? expected_size : maximum_size;
	gsize total_size = 0;
	g_autoptr(GBytes) contents = NULL;
	g_autoptr(GPtrArray) chunks = NULL;

	proxy = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self), error));
	if (proxy == NULL)
		return NULL;

	/* update UI */
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ);

	/* get all the chunks from the hardware */
	chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref);
	for (guint16 idx = 0; idx < G_MAXUINT16; idx++) {
		guint32 chunk_size;

		/* read chunk of data */
		chunk_tmp = fu_dfu_target_upload_chunk(self,
						       idx,
						       0, /* device transfer size */
						       progress,
						       error);
		if (chunk_tmp == NULL)
			return NULL;

		/* keep a sum of all the chunks */
		chunk_size = (guint32)g_bytes_get_size(chunk_tmp);
		if (chunk_size > G_MAXSIZE - total_size) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "overflow at chunk #%04x",
				    idx);
			return NULL;
		}
		total_size += chunk_size;
		if (total_size > maximum_size)
			break;

		/* add to array */
		g_debug("got #%04x chunk of size %" G_GUINT32_FORMAT, idx, chunk_size);
		g_ptr_array_add(chunks, chunk_tmp);

		/* update UI */
		if (chunk_size > 0)
			fu_progress_set_percentage_full(progress, total_size, percentage_size);

		/* detect short write as EOF */
		if (chunk_size < fu_dfu_device_get_transfer_size(proxy))
			break;
	}

	/* check final size */
	if (expected_size > 0) {
		if (total_size != expected_size) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "invalid size, got %" G_GSIZE_FORMAT ", "
				    "expected %" G_GSIZE_FORMAT,
				    total_size,
				    expected_size);
			return NULL;
		}
	}

	/* done */
	fu_progress_set_percentage(progress, 100);

	/* create new image */
	contents = fu_dfu_utils_bytes_join_array(chunks, error);
	if (contents == NULL)
		return NULL;
	return fu_chunk_bytes_new(contents);
}

static FuChunk *
fu_dfu_target_upload_element(FuDfuTarget *self,
			     guint32 address,
			     gsize expected_size,
			     gsize maximum_size,
			     FuProgress *progress,
			     GError **error)
{
	FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self);

	/* implemented as part of a superclass */
	if (klass->upload_element != NULL) {
		return klass
		    ->upload_element(self, address, expected_size, maximum_size, progress, error);
	}
	return fu_dfu_target_upload_element_dfu(self,
						address,
						expected_size,
						maximum_size,
						progress,
						error);
}

static guint32
fu_dfu_target_get_size_of_zone(FuDfuTarget *self, guint16 zone)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	guint32 len = 0;
	for (guint i = 0; i < priv->sectors->len; i++) {
		FuDfuSector *sector = g_ptr_array_index(priv->sectors, i);
		guint32 sector_size;
		if (fu_dfu_sector_get_zone(sector) != zone)
			continue;

		/* sanity check */
		sector_size = fu_dfu_sector_get_size(sector);
		if (sector_size > G_MAXUINT32 - len) {
			g_warning("DFU zone %u size overflow, returning partial size", zone);
			return G_MAXUINT32;
		}
		len += sector_size;
	}
	return len;
}

/* private */
gboolean
fu_dfu_target_upload(FuDfuTarget *self,
		     FuFirmware *firmware,
		     FuProgress *progress,
		     FuDfuTargetTransferFlags flags,
		     GError **error)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	FuDevice *proxy;
	FuDfuSector *sector;
	guint16 zone_cur;
	guint32 zone_size = 0;
	guint32 zone_last = G_MAXUINT;
	g_autoptr(FuFirmware) image = NULL;

	g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* ensure populated */
	if (!fu_dfu_target_setup(self, error))
		return FALSE;

	/* can the target do this? */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_CAN_UPLOAD)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "target cannot do uploading");
		return FALSE;
	}

	/* use correct alt */
	if (!fu_dfu_target_use_alt_setting(self, error))
		return FALSE;

	/* no open?! */
	if (priv->sectors->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no sectors defined for target");
		return FALSE;
	}

	/* create a new image */
	image = fu_firmware_new();
	fu_firmware_set_id(image, fu_device_get_logical_id(FU_DEVICE(self)));
	fu_firmware_set_idx(image, priv->alt_setting);

	/* get all the sectors for the device */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, priv->sectors->len);
	for (guint i = 0; i < priv->sectors->len; i++) {
		g_autoptr(FuChunk) chk = NULL;

		/* only upload to the start of any zone:sector */
		sector = g_ptr_array_index(priv->sectors, i);
		zone_cur = fu_dfu_sector_get_zone(sector);
		if (zone_cur == zone_last)
			continue;

		/* get the size of the entire continuous zone */
		zone_size = fu_dfu_target_get_size_of_zone(self, zone_cur);
		zone_last = zone_cur;

		/* get the first element from the hardware */
		g_debug("starting upload from 0x%08x (0x%04x)",
			fu_dfu_sector_get_address(sector),
			zone_size);
		chk = fu_dfu_target_upload_element(self,
						   fu_dfu_sector_get_address(sector),
						   0,	      /* expected */
						   zone_size, /* maximum */
						   fu_progress_get_child(progress),
						   error);
		if (chk == NULL)
			return FALSE;

		/* this chunk was uploaded okay */
		fu_firmware_add_chunk(image, chk);
		fu_progress_step_done(progress);
	}

	/* success */
	return fu_firmware_add_image(firmware, image, error);
}

static gboolean
fu_dfu_target_download_element_dfu(FuDfuTarget *self,
				   FuChunk *chk,
				   FuProgress *progress,
				   FuDfuTargetTransferFlags flags,
				   GError **error)
{
	FuDevice *proxy;
	guint32 nr_chunks;
	guint16 transfer_size;
	g_autoptr(GBytes) bytes = NULL;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	transfer_size = fu_dfu_device_get_transfer_size(FU_DFU_DEVICE(proxy));

	/* round up as we have to transfer incomplete blocks */
	bytes = fu_chunk_get_bytes(chk);
	nr_chunks = (guint)ceil((gdouble)g_bytes_get_size(bytes) / (gdouble)transfer_size);
	if (nr_chunks == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "zero-length firmware");
		return FALSE;
	}
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
	for (guint32 i = 0; i < nr_chunks + 1; i++) {
		gsize length;
		guint32 offset;
		g_autoptr(GByteArray) buf = g_byte_array_new();

		/* calculate the offset into the chunk data */
		offset = i * transfer_size;

		/* we have to write one final zero-sized chunk for EOF */
		if (i < nr_chunks) {
			g_autoptr(GBytes) bytes_tmp = NULL;
			length = g_bytes_get_size(bytes) - offset;
			if (length > transfer_size)
				length = transfer_size;
			bytes_tmp = fu_bytes_new_offset(bytes, offset, length, error);
			if (bytes_tmp == NULL)
				return FALSE;
			fu_byte_array_append_bytes(buf, bytes_tmp);
		}
		g_debug("writing #%04x chunk of size 0x%x", i, buf->len);
		if (!fu_dfu_target_download_chunk(self, i, buf, 0, progress, error)) {
			g_prefix_error(error, "failed to write chunk %u: ", i);
			return FALSE;
		}

		/* update UI */
		fu_progress_set_percentage_full(progress, i + 1, nr_chunks + 1);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_dfu_target_download_element(FuDfuTarget *self,
			       FuChunk *chk,
			       FuProgress *progress,
			       FuDfuTargetTransferFlags flags,
			       GError **error)
{
	FuDevice *proxy;
	FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self);

	/* progress */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (flags & FU_DFU_TARGET_TRANSFER_FLAG_VERIFY &&
	    fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_CAN_UPLOAD)) {
		fu_progress_set_id(progress, G_STRLOC);
		fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, NULL);
		fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 4, NULL);
	} else {
		fu_progress_set_id(progress, G_STRLOC);
		fu_progress_set_steps(progress, 1);
	}

	/* implemented as part of a superclass */
	if (klass->download_element != NULL) {
		if (!klass->download_element(self,
					     chk,
					     fu_progress_get_child(progress),
					     flags,
					     error))
			return FALSE;
	} else {
		if (!fu_dfu_target_download_element_dfu(self,
							chk,
							fu_progress_get_child(progress),
							flags,
							error))
			return FALSE;
	}
	fu_progress_step_done(progress);

	/* verify */
	if (flags & FU_DFU_TARGET_TRANSFER_FLAG_VERIFY &&
	    fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_CAN_UPLOAD)) {
		g_autoptr(GBytes) bytes = NULL;
		g_autoptr(GBytes) bytes_tmp = NULL;
		g_autoptr(FuChunk) chunk_tmp = NULL;
		bytes = fu_chunk_get_bytes(chk);
		chunk_tmp = fu_dfu_target_upload_element(self,
							 fu_chunk_get_address(chk),
							 g_bytes_get_size(bytes),
							 g_bytes_get_size(bytes),
							 fu_progress_get_child(progress),
							 error);
		if (chunk_tmp == NULL)
			return FALSE;
		bytes_tmp = fu_chunk_get_bytes(chunk_tmp);
		if (!fu_bytes_compare(bytes_tmp, bytes, error)) {
			g_prefix_error_literal(error, "verify failed: ");
			return FALSE;
		}
		fu_progress_step_done(progress);
	}

	return TRUE;
}

/**
 * fu_dfu_target_download:
 * @self: a #FuDfuTarget
 * @image: a #FuFirmware
 * @flags: DFU target flags, e.g. %FU_DFU_TARGET_TRANSFER_FLAG_VERIFY
 * @error: (nullable): optional return location for an error
 *
 * Downloads firmware from the host to the target, optionally verifying
 * the transfer.
 *
 * Returns: %TRUE for success
 **/
gboolean
fu_dfu_target_download(FuDfuTarget *self,
		       FuFirmware *image,
		       FuProgress *progress,
		       FuDfuTargetTransferFlags flags,
		       GError **error)
{
	FuDevice *proxy;
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	g_autoptr(GPtrArray) chunks = NULL;

	g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE);
	g_return_val_if_fail(FU_IS_FIRMWARE(image), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* ensure populated */
	if (!fu_dfu_target_setup(self, error))
		return FALSE;

	/* can the target do this? */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "target cannot do downloading");
		return FALSE;
	}

	/* use correct alt */
	if (!fu_dfu_target_use_alt_setting(self, error))
		return FALSE;

	/* download all chunks in the image to the device */
	chunks = fu_firmware_get_chunks(image, error);
	if (chunks == NULL)
		return FALSE;
	if (chunks->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "no image chunks");
		return FALSE;
	}
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);
		g_debug("downloading chunk at 0x%04x", (guint)fu_chunk_get_address(chk));

		/* auto-detect missing firmware address -- this assumes
		 * that the first target is the main program memory and that
		 * there is only one element in the firmware file */
		if (flags & FU_DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC &&
		    fu_chunk_get_address(chk) == 0x0 && chunks->len == 1 &&
		    priv->sectors->len > 0) {
			FuDfuSector *sector = g_ptr_array_index(priv->sectors, 0);
			g_debug("fixing up firmware address from 0x0 to 0x%x",
				fu_dfu_sector_get_address(sector));
			fu_chunk_set_address(chk, fu_dfu_sector_get_address(sector));
		}

		/* download to device */
		if (!fu_dfu_target_download_element(self,
						    chk,
						    fu_progress_get_child(progress),
						    flags,
						    error))
			return FALSE;
		fu_progress_step_done(progress);
	}

	if (fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_MANIFEST_POLL) &&
	    fu_device_has_private_flag(proxy, FU_DFU_DEVICE_FLAG_MANIFEST_TOL))
		if (!fu_dfu_target_manifest_wait(self, error))
			return FALSE;

	/* success */
	return TRUE;
}

/**
 * fu_dfu_target_get_alt_setting:
 * @self: a #FuDfuTarget
 *
 * Gets the alternate setting to use for this interface.
 *
 * Returns: the alternative setting, typically zero
 **/
guint8
fu_dfu_target_get_alt_setting(FuDfuTarget *self)
{
	FuDfuTargetPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(FU_IS_DFU_TARGET(self), G_MAXUINT8);
	return priv->alt_setting;
}

static void
fu_dfu_target_class_init(FuDfuTargetClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->to_string = fu_dfu_target_to_string;
	object_class->finalize = fu_dfu_target_finalize;
}
