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

#include "config.h"

#include <string.h>

#include "fu-fastboot-device.h"

#define FASTBOOT_REMOVE_DELAY_RE_ENUMERATE 60000 /* ms */
#define FASTBOOT_TRANSACTION_TIMEOUT	   1000	 /* ms */
#define FASTBOOT_TRANSACTION_RETRY_MAX	   600
#define FASTBOOT_EP_IN			   0x81
#define FASTBOOT_EP_OUT			   0x01
#define FASTBOOT_CMD_BUFSZ		   64 /* bytes */

struct _FuFastbootDevice {
	FuUsbDevice parent_instance;
	guint blocksz;
	guint operation_delay;
};

G_DEFINE_TYPE(FuFastbootDevice, fu_fastboot_device, FU_TYPE_USB_DEVICE)

static void
fu_fastboot_device_to_string(FuDevice *device, guint idt, GString *str)
{
	FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device);
	fwupd_codec_string_append_hex(str, idt, "BlockSize", self->blocksz);
}

static gboolean
fu_fastboot_device_probe(FuDevice *device, GError **error)
{
	FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device);
	g_autoptr(FuUsbInterface) intf = NULL;

	/* find the correct fastboot interface */
	intf = fu_usb_device_get_interface(FU_USB_DEVICE(self), 0xff, 0x42, 0x03, error);
	if (intf == NULL)
		return FALSE;
	fu_usb_device_add_interface(FU_USB_DEVICE(self), fu_usb_interface_get_number(intf));
	return TRUE;
}

static gboolean
fu_fastboot_device_write(FuFastbootDevice *self, const guint8 *buf, gsize buflen, GError **error)
{
	gboolean ret;
	gsize actual_len = 0;
	g_autofree guint8 *buf2 = NULL;

	/* make mutable */
	fu_dump_raw(G_LOG_DOMAIN, "writing", buf, buflen);
	buf2 = fu_memdup_safe(buf, buflen, error);
	if (buf2 == NULL)
		return FALSE;
	ret = fu_usb_device_bulk_transfer(FU_USB_DEVICE(self),
					  FASTBOOT_EP_OUT,
					  buf2,
					  buflen,
					  &actual_len,
					  FASTBOOT_TRANSACTION_TIMEOUT,
					  NULL,
					  error);

	/* give device some time to handle action */
	fu_device_sleep(FU_DEVICE(self), self->operation_delay);

	if (!ret) {
		g_prefix_error_literal(error, "failed to do bulk transfer: ");
		return FALSE;
	}
	if (actual_len != buflen) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "only wrote %" G_GSIZE_FORMAT "bytes",
			    actual_len);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_fastboot_device_writestr(FuFastbootDevice *self, const gchar *str, GError **error)
{
	gsize buflen = strlen(str);
	if (buflen > FASTBOOT_CMD_BUFSZ - 4) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "fastboot limits writes to %i bytes",
			    FASTBOOT_CMD_BUFSZ - 4);
		return FALSE;
	}
	return fu_fastboot_device_write(self, (const guint8 *)str, buflen, error);
}

typedef enum {
	FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
	FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
} FuFastbootDeviceReadFlags;

static gboolean
fu_fastboot_device_read(FuFastbootDevice *self,
			gchar **str,
			FuProgress *progress,
			FuFastbootDeviceReadFlags flags,
			GError **error)
{
	guint retries = 1;

	/* these commands may return INFO or take some time to complete */
	if (flags & FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL)
		retries = FASTBOOT_TRANSACTION_RETRY_MAX;

	for (guint i = 0; i < retries; i++) {
		gboolean ret;
		gsize actual_len = 0;
		guint8 buf[FASTBOOT_CMD_BUFSZ] = {0x00};
		g_autofree gchar *tmp = NULL;
		g_autoptr(GError) error_local = NULL;

		ret = fu_usb_device_bulk_transfer(FU_USB_DEVICE(self),
						  FASTBOOT_EP_IN,
						  buf,
						  sizeof(buf),
						  &actual_len,
						  FASTBOOT_TRANSACTION_TIMEOUT,
						  NULL,
						  &error_local);
		/* give device some time to handle action */
		fu_device_sleep(FU_DEVICE(self), self->operation_delay);

		if (!ret) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) {
				g_debug("ignoring %s", error_local->message);
				continue;
			}
			g_propagate_prefixed_error(error,
						   g_steal_pointer(&error_local),
						   "failed to do bulk transfer: ");
			return FALSE;
		}
		fu_dump_raw(G_LOG_DOMAIN, "read", buf, actual_len);
		if (actual_len < 4) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "only read %" G_GSIZE_FORMAT "bytes",
				    actual_len);
			return FALSE;
		}

		/* info */
		tmp = fu_memstrsafe(buf, sizeof(buf), 4, sizeof(buf) - 4, NULL);
		if (memcmp(buf, "INFO", 4) == 0) {
			if (g_strcmp0(tmp, "erasing flash") == 0)
				fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE);
			else if (g_strcmp0(tmp, "writing flash") == 0)
				fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
			else
				g_debug("INFO returned unknown: %s", tmp);
			continue;
		}

		/* success */
		if (memcmp(buf, "OKAY", 4) == 0 || memcmp(buf, "DATA", 4) == 0) {
			if (str != NULL)
				*str = g_steal_pointer(&tmp);
			return TRUE;
		}

		/* failure */
		if (memcmp(buf, "FAIL", 4) == 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_READ,
				    "failed to read response: %s",
				    tmp);
			return FALSE;
		}

		/* unknown failure */
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "failed to read response");
		return FALSE;
	}

	/* we timed out a *lot* */
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "no response to read");
	return FALSE;
}

static gboolean
fu_fastboot_device_getvar(FuFastbootDevice *self, const gchar *key, gchar **str, GError **error)
{
	g_autofree gchar *tmp = g_strdup_printf("getvar:%s", key);
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);
	g_autoptr(GError) error_local = NULL;

	if (!fu_fastboot_device_writestr(self, tmp, error))
		return FALSE;
	if (!fu_fastboot_device_read(self,
				     str,
				     progress,
				     FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
				     &error_local)) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ)) {
			g_debug("ignoring: %s", error_local->message);
		} else {
			g_propagate_prefixed_error(error,
						   g_steal_pointer(&error_local),
						   "failed to getvar %s: ",
						   key);
			return FALSE;
		}
	}
	return TRUE;
}

static gboolean
fu_fastboot_device_cmd(FuFastbootDevice *self,
		       const gchar *cmd,
		       FuProgress *progress,
		       FuFastbootDeviceReadFlags flags,
		       GError **error)
{
	if (!fu_fastboot_device_writestr(self, cmd, error))
		return FALSE;
	if (!fu_fastboot_device_read(self, NULL, progress, flags, error))
		return FALSE;
	return TRUE;
}

static gboolean
fu_fastboot_device_flash(FuFastbootDevice *self,
			 const gchar *partition,
			 FuProgress *progress,
			 GError **error)
{
	g_autofree gchar *tmp = g_strdup_printf("flash:%s", partition);
	return fu_fastboot_device_cmd(self,
				      tmp,
				      progress,
				      FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
				      error);
}

static gboolean
fu_fastboot_device_download(FuFastbootDevice *self,
			    GBytes *fw,
			    FuProgress *progress,
			    GError **error)
{
	gsize sz = g_bytes_get_size(fw);
	g_autofree gchar *tmp = g_strdup_printf("download:%08x", (guint)sz);
	g_autoptr(FuChunkArray) chunks = NULL;

	/* tell the client the size of data to expect */
	if (!fu_fastboot_device_cmd(self,
				    tmp,
				    progress,
				    FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
				    error))
		return FALSE;

	/* send the data in chunks */
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
	chunks = fu_chunk_array_new_from_bytes(fw,
					       FU_CHUNK_ADDR_OFFSET_NONE,
					       FU_CHUNK_PAGESZ_NONE,
					       self->blocksz);
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, fu_chunk_array_length(chunks));
	for (guint i = 0; i < fu_chunk_array_length(chunks); i++) {
		g_autoptr(FuChunk) chk = NULL;

		/* prepare chunk */
		chk = fu_chunk_array_index(chunks, i, error);
		if (chk == NULL)
			return FALSE;
		if (!fu_fastboot_device_write(self,
					      fu_chunk_get_data(chk),
					      fu_chunk_get_data_sz(chk),
					      error))
			return FALSE;
		fu_progress_step_done(progress);
	}
	if (!fu_fastboot_device_read(self,
				     NULL,
				     progress,
				     FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
				     error))
		return FALSE;
	return TRUE;
}

static gboolean
fu_fastboot_device_setup(FuDevice *device, GError **error)
{
	FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device);
	g_autofree gchar *product = NULL;
	g_autofree gchar *serialno = NULL;
	g_autofree gchar *secure = NULL;
	g_autofree gchar *version_device = NULL;
	g_autofree gchar *version_bootloader = NULL;

	/* FuUsbDevice->setup */
	if (!FU_DEVICE_CLASS(fu_fastboot_device_parent_class)->setup(device, error))
		return FALSE;

	/* product */
	if (!fu_fastboot_device_getvar(self, "product", &product, error))
		return FALSE;
	if (product != NULL && product[0] != '\0') {
		g_autofree gchar *tmp = g_strdup_printf("Fastboot %s", product);
		fu_device_set_name(device, tmp);
	}

	/* device version */
	if (!fu_fastboot_device_getvar(self, "version-device", &version_device, error))
		return FALSE;
	if (version_device != NULL && version_device[0] != '\0') {
		FwupdVersionFormat version_format = fu_version_guess_format(version_device);
		if (version_format != FWUPD_VERSION_FORMAT_UNKNOWN)
			fu_device_set_version_format(device, version_format);
		fu_device_set_version(device, version_device);
	}

	/* bootloader version */
	if (!fu_fastboot_device_getvar(self, "version-bootloader", &version_bootloader, error))
		return FALSE;
	if (version_bootloader != NULL && version_bootloader[0] != '\0')
		fu_device_set_version_bootloader(device, version_bootloader);

	/* serialno */
	if (!fu_fastboot_device_getvar(self, "serialno", &serialno, error))
		return FALSE;
	if (serialno != NULL && serialno[0] != '\0')
		fu_device_set_serial(device, serialno);

	/* secure */
	if (!fu_fastboot_device_getvar(self, "secure", &secure, error))
		return FALSE;
	if (secure != NULL && secure[0] != '\0') {
		fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
	} else {
		fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_fastboot_device_write_qfil_part(FuFastbootDevice *self,
				   FuFirmware *firmware,
				   XbNode *part,
				   FuProgress *progress,
				   GError **error)
{
	GBytes *data;
	const gchar *fn;
	const gchar *partition;

	/* not all partitions have images */
	fn = xb_node_query_text(part, "img_name", NULL);
	if (fn == NULL)
		return TRUE;

	/* find filename */
	data = fu_firmware_get_image_by_id_bytes(firmware, fn, error);
	if (data == NULL)
		return FALSE;

	/* get the partition name */
	partition = xb_node_query_text(part, "name", error);
	if (partition == NULL)
		return FALSE;
	if (g_str_has_prefix(partition, "0:"))
		partition += 2;

	/* flash the partition */
	if (!fu_fastboot_device_download(self, data, progress, error))
		return FALSE;
	return fu_fastboot_device_flash(self, partition, progress, error);
}

static gboolean
fu_fastboot_device_write_motorola_part(FuFastbootDevice *self,
				       FuFirmware *firmware,
				       XbNode *part,
				       FuProgress *progress,
				       GError **error)
{
	const gchar *op = xb_node_get_attr(part, "operation");

	/* oem */
	if (g_strcmp0(op, "oem") == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "OEM commands are not supported");
		return FALSE;
	}

	/* getvar */
	if (g_strcmp0(op, "getvar") == 0) {
		const gchar *var = xb_node_get_attr(part, "var");
		g_autofree gchar *tmp = NULL;

		/* check required args */
		if (var == NULL) {
			tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL);
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "required var for part: %s",
				    tmp);
			return FALSE;
		}

		/* just has to be non-empty */
		if (!fu_fastboot_device_getvar(self, var, &tmp, error))
			return FALSE;
		if (tmp == NULL || tmp[0] == '\0') {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "failed to getvar %s",
				    var);
			return FALSE;
		}
		return TRUE;
	}

	/* erase */
	if (g_strcmp0(op, "erase") == 0) {
		const gchar *partition = xb_node_get_attr(part, "partition");
		g_autofree gchar *cmd = g_strdup_printf("erase:%s", partition);

		/* check required args */
		if (partition == NULL) {
			g_autofree gchar *tmp = NULL;
			tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL);
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "required partition for part: %s",
				    tmp);
			return FALSE;
		}

		/* erase the partition */
		return fu_fastboot_device_cmd(self,
					      cmd,
					      progress,
					      FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
					      error);
	}

	/* flash */
	if (g_strcmp0(op, "flash") == 0) {
		GBytes *data;
		const gchar *filename = xb_node_get_attr(part, "filename");
		const gchar *partition = xb_node_get_attr(part, "partition");
		struct {
			GChecksumType kind;
			const gchar *str;
		} csum_kinds[] = {{G_CHECKSUM_MD5, "MD5"},
				  {G_CHECKSUM_SHA1, "SHA1"},
				  {G_CHECKSUM_SHA256, "SHA256"},
				  {0, NULL}};

		/* check required args */
		if (partition == NULL || filename == NULL) {
			g_autofree gchar *tmp = NULL;
			tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL);
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "required partition and filename: %s",
				    tmp);
			return FALSE;
		}

		/* find filename */
		data = fu_firmware_get_image_by_id_bytes(firmware, filename, error);
		if (data == NULL)
			return FALSE;

		/* checksum is optional */
		for (guint i = 0; csum_kinds[i].str != NULL; i++) {
			const gchar *csum;
			g_autofree gchar *csum_actual = NULL;

			/* not provided */
			csum = xb_node_get_attr(part, csum_kinds[i].str);
			if (csum == NULL)
				continue;

			/* check is valid */
			csum_actual = g_compute_checksum_for_bytes(csum_kinds[i].kind, data);
			if (g_strcmp0(csum, csum_actual) != 0) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "%s invalid, expected %s, got %s",
					    filename,
					    csum,
					    csum_actual);
				return FALSE;
			}
		}

		/* flash the partition */
		if (!fu_fastboot_device_download(self, data, progress, error))
			return FALSE;
		return fu_fastboot_device_flash(self, partition, progress, error);
	}

	/* dumb operation that doesn't expect a response */
	if (g_strcmp0(op, "boot") == 0 || g_strcmp0(op, "continue") == 0 ||
	    g_strcmp0(op, "reboot") == 0 || g_strcmp0(op, "reboot-bootloader") == 0 ||
	    g_strcmp0(op, "powerdown") == 0) {
		return fu_fastboot_device_cmd(self,
					      op,
					      progress,
					      FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
					      error);
	}

	/* unknown */
	g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown operation %s", op);
	return FALSE;
}

static gboolean
fu_fastboot_device_write_motorola(FuFastbootDevice *self,
				  FuFirmware *firmware,
				  FuProgress *progress,
				  GError **error)
{
	GBytes *data;
	g_autoptr(GPtrArray) parts = NULL;
	g_autoptr(XbBuilder) builder = xb_builder_new();
	g_autoptr(XbBuilderSource) source = xb_builder_source_new();
	g_autoptr(XbSilo) silo = NULL;

	/* load the manifest of operations */
	data = fu_firmware_get_image_by_id_bytes(firmware, "flashfile.xml", error);
	if (data == NULL)
		return FALSE;
	if (!xb_builder_source_load_bytes(source, data, XB_BUILDER_SOURCE_FLAG_NONE, error)) {
		fwupd_error_convert(error);
		return FALSE;
	}
	xb_builder_import_source(builder, source);
	silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error);
	if (silo == NULL) {
		fwupd_error_convert(error);
		return FALSE;
	}

	/* get all the operation parts */
	parts = xb_silo_query(silo, "parts/part", 0, error);
	if (parts == NULL) {
		fwupd_error_convert(error);
		return FALSE;
	}
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, parts->len);
	for (guint i = 0; i < parts->len; i++) {
		XbNode *part = g_ptr_array_index(parts, i);
		if (!fu_fastboot_device_write_motorola_part(self,
							    firmware,
							    part,
							    fu_progress_get_child(progress),
							    error))
			return FALSE;
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_fastboot_device_write_qfil(FuFastbootDevice *self,
			      FuFirmware *firmware,
			      FuProgress *progress,
			      GError **error)
{
	GBytes *data;
	g_autoptr(GPtrArray) parts = NULL;
	g_autoptr(XbBuilder) builder = xb_builder_new();
	g_autoptr(XbBuilderSource) source = xb_builder_source_new();
	g_autoptr(XbSilo) silo = NULL;

	/* load the manifest of operations */
	data = fu_firmware_get_image_by_id_bytes(firmware, "partition_nand.xml", error);
	if (data == NULL)
		return FALSE;
	if (!xb_builder_source_load_bytes(source, data, XB_BUILDER_SOURCE_FLAG_NONE, error)) {
		fwupd_error_convert(error);
		return FALSE;
	}
	xb_builder_import_source(builder, source);
	silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error);
	if (silo == NULL) {
		fwupd_error_convert(error);
		return FALSE;
	}

	/* get all the operation parts */
	parts = xb_silo_query(silo, "nandboot/partitions/partition", 0, error);
	if (parts == NULL) {
		fwupd_error_convert(error);
		return FALSE;
	}
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, parts->len);
	for (guint i = 0; i < parts->len; i++) {
		XbNode *part = g_ptr_array_index(parts, i);
		if (!fu_fastboot_device_write_qfil_part(self,
							firmware,
							part,
							fu_progress_get_child(progress),
							error))
			return FALSE;
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_fastboot_device_write_firmware(FuDevice *device,
				  FuFirmware *firmware,
				  FuProgress *progress,
				  FwupdInstallFlags flags,
				  GError **error)
{
	FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device);
	g_autoptr(FuFirmware) manifest = NULL;

	/* load the manifest of operations */
	manifest = fu_firmware_get_image_by_id(firmware, "partition_nand.xml", NULL);
	if (manifest != NULL)
		return fu_fastboot_device_write_qfil(self, firmware, progress, error);
	manifest = fu_firmware_get_image_by_id(firmware, "flashfile.xml", NULL);
	if (manifest != NULL)
		return fu_fastboot_device_write_motorola(self, firmware, progress, error);

	/* not supported */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "manifest not supported");
	return FALSE;
}

static gboolean
fu_fastboot_device_set_quirk_kv(FuDevice *device,
				const gchar *key,
				const gchar *value,
				GError **error)
{
	FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device);
	guint64 tmp = 0;

	/* load from quirks */
	if (g_strcmp0(key, "FastbootBlockSize") == 0) {
		if (!fu_strtoull(value, &tmp, 0x40, 0x100000, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->blocksz = tmp;
		return TRUE;
	}
	if (g_strcmp0(key, "FastbootOperationDelay") == 0) {
		if (!fu_strtoull(value, &tmp, 0, G_MAXSIZE, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->operation_delay = tmp;
		return TRUE;
	}

	/* failed */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "quirk key not supported");
	return FALSE;
}

static gboolean
fu_fastboot_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device);
	if (!fu_fastboot_device_cmd(self,
				    "reboot",
				    progress,
				    FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
				    error))
		return FALSE;
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
	return TRUE;
}

static void
fu_fastboot_device_set_progress(FuDevice *device, 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, 1, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload");
}

static void
fu_fastboot_device_init(FuFastbootDevice *self)
{
	/* this is a safe default, even using USBv1 */
	self->blocksz = 512;
	/* no delay is applied by default after a read or write operation */
	self->operation_delay = 0;
	fu_device_add_protocol(FU_DEVICE(self), "com.google.fastboot");
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
	fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS);
	fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID);
	fu_device_set_remove_delay(FU_DEVICE(self), FASTBOOT_REMOVE_DELAY_RE_ENUMERATE);
	fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ZIP_FIRMWARE);
	fu_usb_device_set_claim_retry_count(FU_USB_DEVICE(self), 5);
}

static void
fu_fastboot_device_class_init(FuFastbootDeviceClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->probe = fu_fastboot_device_probe;
	device_class->setup = fu_fastboot_device_setup;
	device_class->write_firmware = fu_fastboot_device_write_firmware;
	device_class->attach = fu_fastboot_device_attach;
	device_class->to_string = fu_fastboot_device_to_string;
	device_class->set_quirk_kv = fu_fastboot_device_set_quirk_kv;
	device_class->set_progress = fu_fastboot_device_set_progress;
}
