/*
 * Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <scsi/sg.h>

#include "fu-ata-device.h"
#include "fu-chunk.h"

#define FU_ATA_IDENTIFY_SIZE	512	/* bytes */
#define FU_ATA_BLOCK_SIZE	512	/* bytes */

struct ata_tf {
	guint8			dev;
	guint8			command;
	guint8			error;
	guint8			status;
	guint8			feat;
	guint8			nsect;
	guint8			lbal;
	guint8			lbam;
	guint8			lbah;
};

#define ATA_USING_LBA			(1 << 6)
#define ATA_STAT_DRQ			(1 << 3)
#define ATA_STAT_ERR			(1 << 0)

#define ATA_OP_IDENTIFY			0xec
#define ATA_OP_FLUSH_CACHE		0xe7
#define ATA_OP_DOWNLOAD_MICROCODE	0x92
#define ATA_OP_STANDBY_IMMEDIATE	0xe0

#define ATA_SUBCMD_MICROCODE_OBSOLETE			0x01
#define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE	0x03
#define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK		0x07
#define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS		0x0e
#define ATA_SUBCMD_MICROCODE_ACTIVATE			0x0f

#define SG_CHECK_CONDITION		0x02
#define SG_DRIVER_SENSE			0x08

#define SG_ATA_12			0xa1
#define SG_ATA_12_LEN			12

#define SG_ATA_PROTO_NON_DATA		(3 << 1)
#define SG_ATA_PROTO_PIO_IN		(4 << 1)
#define SG_ATA_PROTO_PIO_OUT		(5 << 1)

enum {
	SG_CDB2_TLEN_NODATA	= 0 << 0,
	SG_CDB2_TLEN_FEAT	= 1 << 0,
	SG_CDB2_TLEN_NSECT	= 2 << 0,

	SG_CDB2_TLEN_BYTES	= 0 << 2,
	SG_CDB2_TLEN_SECTORS	= 1 << 2,

	SG_CDB2_TDIR_TO_DEV	= 0 << 3,
	SG_CDB2_TDIR_FROM_DEV	= 1 << 3,

	SG_CDB2_CHECK_COND	= 1 << 5,
};

struct _FuAtaDevice {
	FuUdevDevice		 parent_instance;
	guint			 pci_depth;
	guint			 usb_depth;
	guint16			 transfer_blocks;
	guint8			 transfer_mode;
	guint32			 oui;
};

G_DEFINE_TYPE (FuAtaDevice, fu_ata_device, FU_TYPE_UDEV_DEVICE)

guint8
fu_ata_device_get_transfer_mode (FuAtaDevice *self)
{
	return self->transfer_mode;
}

guint16
fu_ata_device_get_transfer_blocks (FuAtaDevice *self)
{
	return self->transfer_blocks;
}

static gchar *
fu_ata_device_get_string (const guint16 *buf, guint start, guint end)
{
	g_autoptr(GString) str = g_string_new (NULL);
	for (guint i = start; i <= end; i++) {
		g_string_append_c (str, (gchar) (buf[i] >> 8));
		g_string_append_c (str, (gchar) (buf[i] & 0xff));
	}

	/* remove whitespace before returning */
	if (str->len > 0) {
		g_strstrip (str->str);
		if (str->str[0] == '\0')
			return NULL;
	}
	return g_string_free (g_steal_pointer (&str), FALSE);
}

static void
fu_ata_device_to_string (FuDevice *device, guint idt, GString *str)
{
	FuAtaDevice *self = FU_ATA_DEVICE (device);
	fu_common_string_append_kx (str, idt, "TransferMode", self->transfer_mode);
	fu_common_string_append_kx (str, idt, "TransferBlocks", self->transfer_blocks);
	if (self->oui != 0x0)
		fu_common_string_append_kx (str, idt, "OUI", self->oui);
	fu_common_string_append_ku (str, idt, "PciDepth", self->pci_depth);
	fu_common_string_append_ku (str, idt, "UsbDepth", self->usb_depth);
}

/* https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifiers-for-ide-devices */
static gchar *
fu_ata_device_pad_string_for_id (const gchar *name)
{
	GString *str = g_string_new (name);
	fu_common_string_replace (str, " ", "_");
	for (guint i = str->len; i < 40; i++)
		g_string_append_c (str, '_');
	return g_string_free (str, FALSE);
}

static gchar *
fu_ata_device_get_guid_safe (const guint16 *buf, guint16 addr_start)
{
	if (!fu_common_guid_is_plausible ((guint8 *) (buf + addr_start)))
		return NULL;
	return fwupd_guid_to_string ((const fwupd_guid_t *) (buf + addr_start),
				     FWUPD_GUID_FLAG_MIXED_ENDIAN);
}

static void
fu_ata_device_parse_id_maybe_dell (FuAtaDevice *self, const guint16 *buf)
{
	g_autofree gchar *component_id = NULL;
	g_autofree gchar *guid_efi = NULL;
	g_autofree gchar *guid_id = NULL;
	g_autofree gchar *guid = NULL;

	/* add extra component ID if set */
	component_id = fu_ata_device_get_string (buf, 137, 140);
	if (component_id == NULL ||
	   !g_str_is_ascii (component_id) ||
	    strlen (component_id) < 6) {
		g_debug ("invalid component ID, skipping");
		return;
	}

	/* do not add the FuUdevDevice instance IDs as generic firmware
	 * should not be used on these OEM-specific devices */
	fu_device_add_internal_flag (FU_DEVICE (self),
				     FU_DEVICE_INTERNAL_FLAG_NO_AUTO_INSTANCE_IDS);

	/* add instance ID *and* GUID as using no-auto-instance-ids */
	guid_id = g_strdup_printf ("STORAGE-DELL-%s", component_id);
	fu_device_add_instance_id (FU_DEVICE (self), guid_id);
	guid = fwupd_guid_hash_string (guid_id);
	fu_device_add_guid (FU_DEVICE (self), guid);

	/* also add the EFI GUID */
	guid_efi = fu_ata_device_get_guid_safe (buf, 129);
	if (guid_efi != NULL)
		fu_device_add_guid (FU_DEVICE (self), guid_efi);

	/* owned by Dell */
	fu_device_set_vendor (FU_DEVICE (self), "Dell");
	fu_device_add_vendor_id (FU_DEVICE (self), "ATA:0x1028");
}

static void
fu_ata_device_parse_vendor_name (FuAtaDevice *self, const gchar *name)
{
	struct {
		const gchar	*prefix;			/* in CAPS */
		guint16		 vid;
		const gchar	*name;
	} map_name[] = {
		/* vendor matches */
		{ "ADATA*",	0x1cc1,	"ADATA" },
		{ "APACER*",	0x0000,	"Apacer" },		/* not in pci.ids */
		{ "APPLE*",	0x106b,	"Apple" },
		{ "CORSAIR*",	0x1987,	"Corsair" },		/* identifies as Phison */
		{ "CRUCIAL*",	0xc0a9,	"Crucial" },
		{ "FUJITSU*",	0x10cf,	"Fujitsu" },
		{ "GIGABYTE*",	0x1458,	"Gigabyte" },
		{ "HGST*",	0x101c,	"Western Digital" },
		{ "HITACHI*",	0x101c,	"Western Digital" },	/* was acquired by WD */
		{ "HITACHI*",	0x1054,	"Hitachi" },
		{ "HP SSD*",	0x103c,	"HP" },
		{ "INTEL*",	0x8086,	"Intel" },
		{ "KINGSPEC*",	0x0000,	"KingSpec" },		/* not in pci.ids */
		{ "KINGSTON*",	0x2646,	"Kingston" },
		{ "LITEON*",	0x14a4,	"LITE-ON" },
		{ "MAXTOR*",	0x115f,	"Maxtor" },
		{ "MICRON*",	0x1344,	"Micron" },
		{ "OCZ*",	0x1179,	"Toshiba" },
		{ "PNY*",	0x196e,	"PNY" },
		{ "QEMU*",	0x1b36,	"QEMU" },		/* identifies as Red Hat! */
		{ "SAMSUNG*",	0x144d,	"Samsung" },
		{ "SANDISK*",	0x15b7,	"SanDisk" },
		{ "SEAGATE*",	0x1bb1,	"Seagate" },
		{ "SK HYNIX*",	0x1c5c,	"SK hynix", },
		{ "SUPERMICRO*",0x15d9,	"SuperMicro" },
		{ "TOSHIBA*",	0x1179,	"Toshiba" },
		{ "WDC*",	0x101c,	"Western Digital" },
		{ NULL,		0x0000,	NULL }
	};
	struct {
		const gchar	*prefix;			/* in CAPS */
		guint16		 vid;
		const gchar	*name;
	} map_fuzzy[] = {
		/* fuzzy name matches -- also see legacy list at:
		 * https://github.com/linuxhw/hw-probe/blob/master/hw-probe.pl#L647 */
		{ "001-*",	0x1bb1,	"Seagate" },
		{ "726060*",	0x101c,	"Western Digital" },
		{ "CT*",	0xc0a9,	"Crucial" },
		{ "DT0*",	0x1179,	"Toshiba" },
		{ "EZEX*",	0x101c,	"Western Digital" },
		{ "GB0*",	0x1590,	"HPE" },
		{ "GOODRAM*",	0x1987,	"Phison" },
		{ "H??54*",	0x101c,	"Western Digital" },
		{ "H??72?0*",	0x101c,	"Western Digital" },
		{ "HDWG*",	0x1179,	"Toshiba" },
		{ "M?0??CA*",	0x1179,	"Toshiba" },		/* enterprise */
		{ "M4-CT*",	0xc0a9,	"Crucial" },
		{ "MA*",	0x10cf,	"Fujitsu", },
		{ "MB*",	0x10cf,	"Fujitsu", },
		{ "MK0*",	0x1590,	"HPE" },
		{ "MTFDDAK*",	0x1344,	"Micron" },
		{ "NIM*",	0x0000,	"Nimbus", },		/* no PCI ID */
		{ "SATADOM*",	0x0000,	"Innodisk", },		/* no PCI ID */
		{ "SSD 860*",	0x144d,	"Samsung" },
		{ "SSDPR*",	0x1987,	"Phison" },
		{ "SSDSC?K*",	0x8086,	"Intel" },
		{ "ST*",	0x1bb1,	"Seagate", },
		{ "TEAM*",	0x0000,	"Team Group" },		/* not in pci.ids */
		{ "TS*",	0x8564,	"Transcend" },
		{ "VK0*",	0x1590,	"HPE" },
		{ "WD*",	0x101c,	"Western Digital" },
		{ NULL,		0x0000,	NULL }
	};
	struct {
		const gchar	*prefix;			/* in CAPS */
		guint16		 vid;
		const gchar	*name;
	} map_version[] = {
		/* fuzzy version matches */
		{ "CS2111*",	0x196e,	"PNY" },
		{ "S?FM*",	0x1987,	"Phison" },
		{ NULL,		0x0000,	NULL }
	};
	g_autofree gchar *name_up = g_ascii_strup (name, -1);
	g_autofree gchar *vendor_id = NULL;

	/* find match */
	for (guint i = 0; map_name[i].prefix != NULL; i++) {
		if (fu_common_fnmatch (map_name[i].prefix, name_up)) {
			name += strlen (map_name[i].prefix) - 1;
			fu_device_set_vendor (FU_DEVICE (self), map_name[i].name);
			vendor_id = g_strdup_printf ("ATA:0x%X", map_name[i].vid);
			break;
		}
	}

	/* fall back to fuzzy match */
	if (vendor_id == NULL) {
		for (guint i = 0; map_fuzzy[i].prefix != NULL; i++) {
			if (fu_common_fnmatch (map_fuzzy[i].prefix, name_up)) {
				fu_device_set_vendor (FU_DEVICE (self), map_fuzzy[i].name);
				vendor_id = g_strdup_printf ("ATA:0x%X", map_fuzzy[i].vid);
				break;
			}
		}
	}

	/* fall back to version */
	if (vendor_id == NULL) {
		g_autofree gchar *version_up = g_ascii_strup (fu_device_get_version (FU_DEVICE (self)), -1);
		for (guint i = 0; map_version[i].prefix != NULL; i++) {
			if (fu_common_fnmatch (map_version[i].prefix, version_up)) {
				fu_device_set_vendor (FU_DEVICE (self), map_version[i].name);
				vendor_id = g_strdup_printf ("ATA:0x%X", map_version[i].vid);
				break;
			}
		}
	}

	/* devices without a vendor ID will not be UPGRADABLE */
	if (vendor_id != NULL)
		fu_device_add_vendor_id (FU_DEVICE (self), vendor_id);

	/* remove leading junk */
	while (name[0] == ' ' || name[0] == '_' || name[0] == '-')
		name += 1;

	/* if changed */
	if (g_strcmp0 (fu_device_get_name (FU_DEVICE (self)), name) != 0)
		fu_device_set_name (FU_DEVICE (self), name);
}

static gboolean
fu_ata_device_parse_id (FuAtaDevice *self, const guint8 *buf, gsize sz, GError **error)
{
	FuDevice *device = FU_DEVICE (self);
	gboolean has_oui_quirk = FALSE;
	guint16 xfer_min = 1;
	guint16 xfer_max = 0xffff;
	guint16 id[FU_ATA_IDENTIFY_SIZE/2];
	g_autofree gchar *name = NULL;
	g_autofree gchar *sku = NULL;

	/* check size */
	if (sz != FU_ATA_IDENTIFY_SIZE) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "ID incorrect size, got 0x%02x",
			     (guint) sz);
		return FALSE;
	}

	/* read LE buffer */
	for (guint i = 0; i < sz / 2; i++)
		id[i] = fu_common_read_uint16 (buf + (i * 2), G_LITTLE_ENDIAN);

	/* verify drive correctly supports DOWNLOAD_MICROCODE */
	if (!(id[83] & 1 && id[86] & 1)) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_FAILED,
				     "DOWNLOAD_MICROCODE not supported by device");
		return FALSE;
	}

	fu_ata_device_parse_id_maybe_dell (self, id);

	/* firmware will be applied when the device restarts */
	if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS)
		fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);

	/* the newer, segmented transfer mode */
	if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE ||
	    self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS) {
		xfer_min = id[234];
		if (xfer_min == 0x0 || xfer_min == 0xffff)
			xfer_min = 1;
		xfer_max = id[235];
		if (xfer_max == 0x0 || xfer_max == 0xffff)
			xfer_max = xfer_min;
	}

	/* fall back to a sane block size */
	if (self->transfer_blocks == 0x0)
		self->transfer_blocks = xfer_min;
	else if (self->transfer_blocks == 0xffff)
		self->transfer_blocks = xfer_max;

	/* get values in case the kernel didn't */
	if (fu_device_get_serial (device) == NULL) {
		g_autofree gchar *tmp = NULL;
		tmp = fu_ata_device_get_string (id, 10, 19);
		if (tmp != NULL)
			fu_device_set_serial (device, tmp);
	}
	if (fu_device_get_version (device) == NULL) {
		g_autofree gchar *tmp = NULL;
		tmp = fu_ata_device_get_string (id, 23, 26);
		if (tmp != NULL)
			fu_device_set_version (device, tmp);
	}

	/* get OUI if set */
	self->oui = ((guint32) (id[108] & 0x0fff)) << 12 |
		    ((guint32) (id[109] & 0xfff0)) >> 4;
	if (self->oui > 0x0) {
		g_autofree gchar *tmp = NULL;
		tmp = g_strdup_printf ("OUI\\%06x", self->oui);
		fu_device_add_instance_id_full (device, tmp, FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS);
		has_oui_quirk = fu_device_get_vendor (FU_DEVICE (self)) != NULL;
	}
	if (self->oui > 0x0) {
		g_autofree gchar *vendor_id = NULL;
		vendor_id = g_strdup_printf ("OUI:%06x", self->oui);
		fu_device_add_vendor_id (device, vendor_id);
	}

	/* if not already set using the vendor block or a OUI quirk */
	name = fu_ata_device_get_string (id, 27, 46);
	if (name != NULL && !has_oui_quirk)
		fu_ata_device_parse_vendor_name (self, name);

	/* 8 byte additional product identifier == SKU? */
	sku = fu_ata_device_get_string (id, 170, 173);
	if (sku != NULL)
		g_debug ("SKU=%s", sku);

	/* add extra GUIDs if none detected from identify block */
	if (name != NULL && fu_device_get_guids (device)->len == 0) {
		g_autofree gchar *name_pad = fu_ata_device_pad_string_for_id (name);
		if (name_pad != NULL &&
		    fu_device_get_version (device) != NULL) {
			g_autofree gchar *tmp = NULL;
			tmp = g_strdup_printf ("IDE\\%s%s", name_pad,
					       fu_device_get_version (device));
			fu_device_add_instance_id (device, tmp);
		}
		if (name_pad != NULL) {
			g_autofree gchar *tmp = NULL;
			tmp = g_strdup_printf ("IDE\\0%s", name_pad);
			fu_device_add_instance_id (device, tmp);
		}

		/* add the name fallback */
		fu_device_add_instance_id (device, name);
	}

	return TRUE;
}

static gboolean
fu_ata_device_probe (FuDevice *device, GError **error)
{
	FuAtaDevice *self = FU_ATA_DEVICE (device);
	GUdevDevice *udev_device = fu_udev_device_get_dev (FU_UDEV_DEVICE (device));

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

	/* check is valid */
	if (g_strcmp0 (g_udev_device_get_devtype (udev_device), "disk") != 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "is not correct devtype=%s, expected disk",
			     g_udev_device_get_devtype (udev_device));
		return FALSE;
	}
	if (!g_udev_device_get_property_as_boolean (udev_device, "ID_ATA_SATA") ||
	    !g_udev_device_get_property_as_boolean (udev_device, "ID_ATA_DOWNLOAD_MICROCODE")) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "has no ID_ATA_DOWNLOAD_MICROCODE");
		return FALSE;
	}

	/* set the physical ID */
	if (!fu_udev_device_set_physical_id (FU_UDEV_DEVICE (device), "scsi", error))
		return FALSE;

	/* look at the PCI and USB depth to work out if in an external enclosure */
	self->pci_depth = fu_udev_device_get_slot_depth (FU_UDEV_DEVICE (device), "pci");
	self->usb_depth = fu_udev_device_get_slot_depth (FU_UDEV_DEVICE (device), "usb");
	if (self->pci_depth <= 2 && self->usb_depth <= 2) {
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_INTERNAL);
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE);
	}

	return TRUE;
}

static guint64
fu_ata_device_tf_to_pack_id (struct ata_tf *tf)
{
	guint32 lba24 = (tf->lbah << 16) | (tf->lbam << 8) | (tf->lbal);
	guint32 lbah = tf->dev & 0x0f;
	return (((guint64) lbah) << 24) | (guint64) lba24;
}

static gboolean
fu_ata_device_command (FuAtaDevice *self, struct ata_tf *tf,
		       gint dxfer_direction, guint timeout_ms,
		       guint8 *dxferp, gsize dxfer_len, GError **error)
{
	guint8 cdb[SG_ATA_12_LEN] = { 0x0 };
	guint8 sb[32] = { 0x0 };
	sg_io_hdr_t io_hdr = { 0x0 };

	/* map _TO_DEV to PIO mode */
	if (dxfer_direction == SG_DXFER_TO_DEV)
		cdb[1] = SG_ATA_PROTO_PIO_OUT;
	else if (dxfer_direction == SG_DXFER_FROM_DEV)
		cdb[1] = SG_ATA_PROTO_PIO_IN;
	else
		cdb[1] = SG_ATA_PROTO_NON_DATA;

	/* libata workaround: don't demand sense data for IDENTIFY */
	if (dxfer_len > 0) {
		cdb[2] |= SG_CDB2_TLEN_NSECT | SG_CDB2_TLEN_SECTORS;
		cdb[2] |= dxfer_direction == SG_DXFER_TO_DEV ? SG_CDB2_TDIR_TO_DEV : SG_CDB2_TDIR_FROM_DEV;
	} else {
		cdb[2] = SG_CDB2_CHECK_COND;
	}

	/* populate non-LBA48 CDB */
	cdb[0] = SG_ATA_12;
	cdb[3] = tf->feat;
	cdb[4] = tf->nsect;
	cdb[5] = tf->lbal;
	cdb[6] = tf->lbam;
	cdb[7] = tf->lbah;
	cdb[8] = tf->dev;
	cdb[9] = tf->command;
	if (g_getenv ("FWUPD_ATA_VERBOSE") != NULL) {
		fu_common_dump_raw (G_LOG_DOMAIN, "CBD", cdb, sizeof(cdb));
		if (dxfer_direction == SG_DXFER_TO_DEV && dxferp != NULL) {
			fu_common_dump_raw (G_LOG_DOMAIN, "outgoing_data",
					    dxferp, dxfer_len);
		}
	}

	/* hit hardware */
	io_hdr.interface_id	= 'S';
	io_hdr.mx_sb_len	= sizeof(sb);
	io_hdr.dxfer_direction	= dxfer_direction;
	io_hdr.dxfer_len	= dxfer_len;
	io_hdr.dxferp		= dxferp;
	io_hdr.cmdp		= cdb;
	io_hdr.cmd_len		= SG_ATA_12_LEN;
	io_hdr.sbp		= sb;
	io_hdr.pack_id		= fu_ata_device_tf_to_pack_id (tf);
	io_hdr.timeout		= timeout_ms;
	if (!fu_udev_device_ioctl (FU_UDEV_DEVICE (self),
				   SG_IO, (guint8 *) &io_hdr,
				   NULL, error))
		return FALSE;
	if (g_getenv ("FWUPD_ATA_VERBOSE") != NULL) {
		g_debug ("ATA_%u status=0x%x, host_status=0x%x, driver_status=0x%x",
			io_hdr.cmd_len, io_hdr.status, io_hdr.host_status, io_hdr.driver_status);
		fu_common_dump_raw (G_LOG_DOMAIN, "SB", sb, sizeof(sb));
	}

	/* error check */
	if (io_hdr.status && io_hdr.status != SG_CHECK_CONDITION) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "bad status: 0x%x", io_hdr.status);
		return FALSE;
	}
	if (io_hdr.host_status) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "bad host status: 0x%x", io_hdr.host_status);
		return FALSE;
	}
	if (io_hdr.driver_status && (io_hdr.driver_status != SG_DRIVER_SENSE)) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "bad driver status: 0x%x", io_hdr.driver_status);
		return FALSE;
	}

	/* repopulate ata_tf */
	tf->error  = sb[8 + 3];
	tf->nsect  = sb[8 + 5];
	tf->lbal   = sb[8 + 7];
	tf->lbam   = sb[8 + 9];
	tf->lbah   = sb[8 + 11];
	tf->dev    = sb[8 + 12];
	tf->status = sb[8 + 13];
	if (g_getenv ("FWUPD_ATA_VERBOSE") != NULL) {
		g_debug ("ATA_%u stat=%02x err=%02x nsect=%02x lbal=%02x "
			 "lbam=%02x lbah=%02x dev=%02x",
			 io_hdr.cmd_len, tf->status, tf->error, tf->nsect, tf->lbal,
			 tf->lbam, tf->lbah, tf->dev);
	}

	/* io error */
	if (tf->status & (ATA_STAT_ERR | ATA_STAT_DRQ)) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "I/O error, ata_op=0x%02x ata_status=0x%02x ata_error=0x%02x",
			     tf->command, tf->status, tf->error);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_ata_device_setup (FuDevice *device, GError **error)
{
	FuAtaDevice *self = FU_ATA_DEVICE (device);
	struct ata_tf tf = { 0x0 };
	guint8 id[FU_ATA_IDENTIFY_SIZE];

	/* get ID block */
	tf.dev = ATA_USING_LBA;
	tf.command = ATA_OP_IDENTIFY;
	tf.nsect = 1; /* 512 bytes */
	if (!fu_ata_device_command (self, &tf, SG_DXFER_FROM_DEV, 1000,
				    id, sizeof(id), error)) {
		g_prefix_error (error, "failed to IDENTIFY: ");
		return FALSE;
	}
	if (g_getenv ("FWUPD_ATA_VERBOSE") != NULL)
		fu_common_dump_raw (G_LOG_DOMAIN, "IDENTIFY", id, sizeof(id));
	if (!fu_ata_device_parse_id (self, id, sizeof(id), error))
		return FALSE;

	/* success */
	return TRUE;
}

static gboolean
fu_ata_device_activate (FuDevice *device, GError **error)
{
	FuAtaDevice *self = FU_ATA_DEVICE (device);
	struct ata_tf tf = { 0x0 };


	/* flush cache and put drive in standby to prepare to activate */
	tf.dev = ATA_USING_LBA;
	tf.command = ATA_OP_FLUSH_CACHE;
	if (!fu_ata_device_command (self, &tf, SG_DXFER_NONE,
				    120 * 1000, /* a long time! */
				    NULL, 0, error)) {
		g_prefix_error (error, "failed to flush cache immediate: ");
		return FALSE;
	}
	tf.command = ATA_OP_STANDBY_IMMEDIATE;
	if (!fu_ata_device_command (self, &tf, SG_DXFER_NONE,
				    120 * 1000, /* a long time! */
				    NULL, 0, error)) {
		g_prefix_error (error, "failed to standby immediate: ");
		return FALSE;
	}

	/* load the new firmware */
	tf.dev = 0xa0 | ATA_USING_LBA;
	tf.command = ATA_OP_DOWNLOAD_MICROCODE;
	tf.feat = ATA_SUBCMD_MICROCODE_ACTIVATE;
	if (!fu_ata_device_command (self, &tf, SG_DXFER_NONE,
				    120 * 1000, /* a long time! */
				    NULL, 0, error)) {
		g_prefix_error (error, "failed to activate firmware: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_ata_device_fw_download (FuAtaDevice *self,
			   guint32 idx,
			   guint32 addr,
			   const guint8 *data,
			   guint32 data_sz,
			   GError **error)
{
	struct ata_tf tf = { 0x0 };
	guint32 block_count = data_sz / FU_ATA_BLOCK_SIZE;
	guint32 buffer_offset = addr / FU_ATA_BLOCK_SIZE;

	/* write block */
	tf.dev = 0xa0 | ATA_USING_LBA;
	tf.command = ATA_OP_DOWNLOAD_MICROCODE;
	tf.feat = self->transfer_mode;
	tf.nsect = block_count & 0xff;
	tf.lbal = block_count >> 8;
	tf.lbam = buffer_offset & 0xff;
	tf.lbah = buffer_offset >> 8;
	if (!fu_ata_device_command (self, &tf, SG_DXFER_TO_DEV,
				    120 * 1000, /* a long time! */
				    (guint8 *) data, data_sz, error)) {
		g_prefix_error (error, "failed to write firmware @0x%0x: ",
				(guint) addr);
		return FALSE;
	}

	/* check drive status */
	if (tf.nsect == 0x0)
		return TRUE;

	/* drive wants more data, or thinks it is all done */
	if (tf.nsect == 0x1 || tf.nsect == 0x2)
		return TRUE;

	/* the offset was set up incorrectly */
	if (tf.nsect == 0x4) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "alignment error");
		return FALSE;
	}

	/* other error */
	g_set_error (error,
		     G_IO_ERROR,
		     G_IO_ERROR_INVALID_DATA,
		     "unknown return code 0x%02x",
		     tf.nsect);
	return FALSE;
}

static gboolean
fu_ata_device_write_firmware (FuDevice *device,
			      FuFirmware *firmware,
			      FwupdInstallFlags flags,
			      GError **error)
{
	FuAtaDevice *self = FU_ATA_DEVICE (device);
	guint32 chunksz = (guint32) self->transfer_blocks * FU_ATA_BLOCK_SIZE;
	guint max_size = 0xffff * FU_ATA_BLOCK_SIZE;
	g_autoptr(GBytes) fw = NULL;
	g_autoptr(GPtrArray) chunks = NULL;

	/* get default image */
	fw = fu_firmware_get_image_default_bytes (firmware, error);
	if (fw == NULL)
		return FALSE;

	/* only one block allowed */
	if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK)
		max_size = 0xffff;

	/* check is valid */
	if (g_bytes_get_size (fw) > max_size) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "firmware is too large, maximum size is %u",
			     max_size);
		return FALSE;
	}
	if (g_bytes_get_size (fw) % FU_ATA_BLOCK_SIZE != 0) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "firmware is not multiple of block size %i",
			     FU_ATA_BLOCK_SIZE);
		return FALSE;
	}

	/* write each block */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
	chunks = fu_chunk_array_new_from_bytes (fw, 0x00, 0x00, chunksz);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index (chunks, i);
		if (!fu_ata_device_fw_download (self,
						fu_chunk_get_idx (chk),
						fu_chunk_get_address (chk),
						fu_chunk_get_data (chk),
						fu_chunk_get_data_sz (chk),
						error)) {
			g_prefix_error (error, "failed to write chunk %u: ", i);
			return FALSE;
		}
		fu_device_set_progress_full (device, (gsize) i, (gsize) chunks->len + 1);
	}

	/* success! */
	fu_device_add_flag (device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION);
	fu_device_set_progress (device, 100);
	return TRUE;
}

static gboolean
fu_ata_device_set_quirk_kv (FuDevice *device,
			    const gchar *key,
			    const gchar *value,
			    GError **error)
{
	FuAtaDevice *self = FU_ATA_DEVICE (device);
	if (g_strcmp0 (key, "AtaTransferMode") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE &&
		    tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS &&
		    tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK) {
			g_set_error_literal (error,
					     G_IO_ERROR,
					     G_IO_ERROR_NOT_SUPPORTED,
					     "AtaTransferMode only supports "
					     "values 0x3, 0x7 or 0xe");
			return FALSE;
		}
		self->transfer_mode = (guint8) tmp;
		return TRUE;
	}
	if (g_strcmp0 (key, "AtaTransferBlocks") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp > 0xffff) {
			g_set_error_literal (error,
					     G_IO_ERROR,
					     G_IO_ERROR_NOT_SUPPORTED,
					     "AtaTransferBlocks only supports "
					     "values <= 0xffff");
			return FALSE;
		}
		self->transfer_blocks = (guint16) tmp;
		return TRUE;
	}
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "quirk key not supported");
	return FALSE;
}

static void
fu_ata_device_init (FuAtaDevice *self)
{
	/* we chose this default as _DOWNLOAD_CHUNKS_ACTIVATE applies the
	 * firmware straight away and the kernel might not like the unexpected
	 * ATA restart and panic */
	self->transfer_mode = ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS;
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_REQUIRE_AC);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_internal_flag (FU_DEVICE (self), FU_DEVICE_INTERNAL_FLAG_INHERIT_ACTIVATION);
	fu_device_set_summary (FU_DEVICE (self), "ATA Drive");
	fu_device_add_icon (FU_DEVICE (self), "drive-harddisk");
	fu_device_add_protocol (FU_DEVICE (self), "org.t13.ata");
	fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_PLAIN);
	fu_udev_device_set_flags (FU_UDEV_DEVICE (self), FU_UDEV_DEVICE_FLAG_OPEN_READ);
}

static void
fu_ata_device_finalize (GObject *object)
{
	G_OBJECT_CLASS (fu_ata_device_parent_class)->finalize (object);
}

static void
fu_ata_device_class_init (FuAtaDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	object_class->finalize = fu_ata_device_finalize;
	klass_device->to_string = fu_ata_device_to_string;
	klass_device->set_quirk_kv = fu_ata_device_set_quirk_kv;
	klass_device->setup = fu_ata_device_setup;
	klass_device->activate = fu_ata_device_activate;
	klass_device->write_firmware = fu_ata_device_write_firmware;
	klass_device->probe = fu_ata_device_probe;
}

FuAtaDevice *
fu_ata_device_new_from_blob (const guint8 *buf, gsize sz, GError **error)
{
	g_autoptr(FuAtaDevice) self = g_object_new (FU_TYPE_ATA_DEVICE, NULL);
	if (!fu_ata_device_parse_id (self, buf, sz, error))
		return NULL;
	return g_steal_pointer (&self);
}
