/*
 * Copyright (C) 2018 Evan Lojewski
 * Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: GPL-2+
 */

#include "config.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#ifdef HAVE_MMAN_H
#include <sys/mman.h>
#endif

#ifdef HAVE_VALGRIND
#include <valgrind.h>
#endif /* HAVE_VALGRIND */

#include "fu-common.h"

#include "fu-bcm57xx-common.h"
#include "fu-bcm57xx-recovery-device.h"
#include "fu-bcm57xx-firmware.h"

/* offsets into BAR[0] */
#define REG_DEVICE_PCI_VENDOR_DEVICE_ID		0x6434
#define REG_NVM_SOFTWARE_ARBITRATION		0x7020
#define REG_NVM_ACCESS				0x7024
#define REG_NVM_COMMAND				0x7000
#define REG_NVM_ADDR				0x700c
#define REG_NVM_READ				0x7010
#define REG_NVM_WRITE				0x7008

/* offsets into BAR[2] */
#define REG_APE_MODE				0x0

typedef struct {
	guint8	*buf;
	gsize	 bufsz;
} FuBcm57xxMmap;

#define FU_BCM57XX_BAR_DEVICE	0
#define FU_BCM57XX_BAR_APE	1
#define FU_BCM57XX_BAR_MAX	3

struct _FuBcm57xxRecoveryDevice {
	FuUdevDevice		 parent_instance;
	FuBcm57xxMmap		 bar[FU_BCM57XX_BAR_MAX];
};

typedef union {
	guint32 r32;
	struct {
		guint32 reserved_0_0		: 1;
		guint32 Reset			: 1;
		guint32 reserved_2_2		: 1;
		guint32 Done			: 1;
		guint32 Doit			: 1;
		guint32 Wr			: 1;
		guint32 Erase			: 1;
		guint32 First			: 1;
		guint32 Last			: 1;
		guint32 reserved_15_9		: 7;
		guint32 WriteEnableCommand	: 1;
		guint32 WriteDisableCommand	: 1;
		guint32 reserved_31_18		: 14;
	} __attribute__((packed)) bits;
} BcmRegNVMCommand;

typedef union {
	guint32 r32;
	struct {
		guint32 ReqSet0			: 1;
		guint32 ReqSet1			: 1;
		guint32 ReqSet2			: 1;
		guint32 ReqSet3			: 1;
		guint32 ReqClr0			: 1;
		guint32 ReqClr1			: 1;
		guint32 ReqClr2			: 1;
		guint32 ReqClr3			: 1;
		guint32 ArbWon0			: 1;
		guint32 ArbWon1			: 1;
		guint32 ArbWon2			: 1;
		guint32 ArbWon3			: 1;
		guint32 Req0			: 1;
		guint32 Req1			: 1;
		guint32 Req2			: 1;
		guint32 Req3			: 1;
		guint32 reserved_31_16		: 16;
	} __attribute__((packed)) bits;
} BcmRegNVMSoftwareArbitration;

typedef union {
	guint32 r32;
	struct {
		guint32 Enable			: 1;
		guint32 WriteEnable		: 1;
		guint32 reserved_31_2		: 30;
	} __attribute__((packed)) bits;
} BcmRegNVMAccess;

typedef union {
	guint32 r32;
	struct {
		guint32 Reset			: 1;
		guint32 Halt			: 1;
		guint32 FastBoot		: 1;
		guint32 HostDiag		: 1;
		guint32 reserved_4_4		: 1;
		guint32 Event1			: 1;
		guint32 Event2			: 1;
		guint32 GRCint			: 1;
		guint32 reserved_8_8		: 1;
		guint32 SwapATBdword		: 1;
		guint32 reserved_10_10		: 1;
		guint32 SwapARBdword		: 1;
		guint32 reserved_13_12		: 2;
		guint32 Channel0Enable		: 1;
		guint32 Channel2Enable		: 1;
		guint32 reserved_17_16		: 2;
		guint32 MemoryECC		: 1;
		guint32 ICodePIPRdDisable	: 1;
		guint32 reserved_29_20		: 10;
		guint32 Channel1Enable		: 1;
		guint32 Channel3Enable		: 1;
	} __attribute__((packed)) bits;
} BcmRegAPEMode;

G_DEFINE_TYPE (FuBcm57xxRecoveryDevice, fu_bcm57xx_recovery_device, FU_TYPE_UDEV_DEVICE)

#ifdef __ppc64__
#define BARRIER()	__asm__ volatile ("sync 0\neieio\n" : : : "memory")
#else
#define BARRIER()	__asm__ volatile ("" : : : "memory");
#endif

static gboolean
fu_bcm57xx_recovery_device_bar_read (FuBcm57xxRecoveryDevice *self,
				     guint bar, gsize offset, guint32 *val,
				     GError **error)
{
	/* this should never happen */
	if (self->bar[bar].buf == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "BAR[%u] is not mapped!", bar);
		return FALSE;
	}

	BARRIER();
	return fu_memcpy_safe ((guint8 *) val, sizeof(*val), 0x0,	/* dst */
			       self->bar[bar].buf, self->bar[bar].bufsz, offset,
			       sizeof(*val), error);
}

static gboolean
fu_bcm57xx_recovery_device_bar_write (FuBcm57xxRecoveryDevice *self,
				      guint bar, gsize offset, guint32 val,
				      GError **error)
{
	/* this should never happen */
	if (self->bar[bar].buf == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "BAR[%u] is not mapped!", bar);
		return FALSE;
	}

	BARRIER();
	if (!fu_memcpy_safe (self->bar[bar].buf, self->bar[bar].bufsz, offset,	/* dst */
			     (const guint8 *) &val, sizeof(val), 0x0,		/* src */
			     sizeof(val), error))
		return FALSE;
	BARRIER();
	return TRUE;
}

static gboolean
fu_bcm57xx_recovery_device_nvram_disable (FuBcm57xxRecoveryDevice *self,
					  GError **error)
{
	BcmRegNVMAccess tmp;
	if (!fu_bcm57xx_recovery_device_bar_read (self, FU_BCM57XX_BAR_DEVICE,
						  REG_NVM_ACCESS, &tmp.r32, error))
		return FALSE;
	tmp.bits.Enable = FALSE;
	tmp.bits.WriteEnable = FALSE;
	return fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						     REG_NVM_ACCESS, tmp.r32, error);
}

static gboolean
fu_bcm57xx_recovery_device_nvram_enable (FuBcm57xxRecoveryDevice *self,
					 GError **error)
{
	BcmRegNVMAccess tmp;
	if (!fu_bcm57xx_recovery_device_bar_read (self, FU_BCM57XX_BAR_DEVICE,
						  REG_NVM_ACCESS, &tmp.r32, error))
		return FALSE;
	tmp.bits.Enable = TRUE;
	tmp.bits.WriteEnable = FALSE;
	return fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						     REG_NVM_ACCESS, tmp.r32, error);
}

static gboolean
fu_bcm57xx_recovery_device_nvram_enable_write (FuBcm57xxRecoveryDevice *self,
					       GError **error)
{
	BcmRegNVMAccess tmp;
	if (!fu_bcm57xx_recovery_device_bar_read (self, FU_BCM57XX_BAR_DEVICE,
						  REG_NVM_ACCESS, &tmp.r32, error))
		return FALSE;
	tmp.bits.Enable = TRUE;
	tmp.bits.WriteEnable = TRUE;
	return fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						     REG_NVM_ACCESS, tmp.r32, error);
}

static gboolean
fu_bcm57xx_recovery_device_nvram_acquire_lock (FuBcm57xxRecoveryDevice *self,
					       GError **error)
{
	BcmRegNVMSoftwareArbitration tmp = { 0 };
	g_autoptr(GTimer) timer = g_timer_new ();
	tmp.bits.ReqSet1 = 1;
	if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						   REG_NVM_SOFTWARE_ARBITRATION,
						   tmp.r32, error))
		return FALSE;
	do {
		if (!fu_bcm57xx_recovery_device_bar_read (self,
							  FU_BCM57XX_BAR_DEVICE,
							  REG_NVM_SOFTWARE_ARBITRATION,
							  &tmp.r32, error))
			return FALSE;
		if (tmp.bits.ArbWon1)
			return TRUE;
		if (g_timer_elapsed (timer, NULL) > 0.2)
			break;
	} while (TRUE);

	/* timed out */
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_TIMED_OUT,
			     "timed out trying to acquire lock #1");
	return FALSE;
}

static gboolean
fu_bcm57xx_recovery_device_nvram_release_lock (FuBcm57xxRecoveryDevice *self,
					       GError **error)
{
	BcmRegNVMSoftwareArbitration tmp = { 0 };
	tmp.r32 = 0;
	tmp.bits.ReqClr1 = 1;
	return fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						     REG_NVM_SOFTWARE_ARBITRATION, tmp.r32,
						     error);
}

static gboolean
fu_bcm57xx_recovery_device_nvram_wait_done (FuBcm57xxRecoveryDevice *self, GError **error)
{
	BcmRegNVMCommand tmp = { 0 };
	g_autoptr(GTimer) timer = g_timer_new ();
	do {
		if (!fu_bcm57xx_recovery_device_bar_read (self,
							  FU_BCM57XX_BAR_DEVICE,
							  REG_NVM_COMMAND, &tmp.r32,
							  error))
			return FALSE;
		if (tmp.bits.Done)
			return TRUE;
		if (g_timer_elapsed (timer, NULL) > 0.2)
			break;
	} while (TRUE);

	/* timed out */
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_TIMED_OUT,
			     "timed out");
	return FALSE;
}

static gboolean
fu_bcm57xx_recovery_device_nvram_clear_done (FuBcm57xxRecoveryDevice *self, GError **error)
{
	BcmRegNVMCommand tmp = { 0 };
	tmp.bits.Done = 1;
	return fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						 REG_NVM_COMMAND, tmp.r32, error);
}

static gboolean
fu_bcm57xx_recovery_device_nvram_read (FuBcm57xxRecoveryDevice *self,
				       guint32 address, guint32 *buf, gsize bufsz,
				       GError **error)
{
	for (guint i = 0; i < bufsz; i++) {
		BcmRegNVMCommand tmp = { 0 };
		guint32 val32 = 0;
		if (!fu_bcm57xx_recovery_device_nvram_clear_done (self, error))
			return FALSE;
		if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
						       REG_NVM_ADDR, address, error))
			return FALSE;
		tmp.bits.Doit = 1;
		tmp.bits.First = i == 0;
		tmp.bits.Last = i == bufsz - 1;

		if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
							   REG_NVM_COMMAND, tmp.r32, error))
			return FALSE;
		if (!fu_bcm57xx_recovery_device_nvram_wait_done (self, error)) {
			g_prefix_error (error, "failed to read @0x%x: ", address);
			return FALSE;
		}
		if (!fu_bcm57xx_recovery_device_bar_read (self, FU_BCM57XX_BAR_DEVICE,
							  REG_NVM_READ, &val32, error))
			return FALSE;
		buf[i] = GUINT32_FROM_BE(val32);
		address += sizeof(guint32);
		fu_device_set_progress_full (FU_DEVICE (self), i, bufsz);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_bcm57xx_recovery_device_nvram_write (FuBcm57xxRecoveryDevice *self,
					guint32 address, const guint32 *buf, gsize bufsz_dwrds,
					GError **error)
{
	const guint32 page_size_dwrds = 64;

	/* can only write in pages of 64 dwords */
	if (bufsz_dwrds % page_size_dwrds != 0 ||
	    (address * sizeof(guint32)) % page_size_dwrds != 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "can only write aligned with page size 0x%x",
			     page_size_dwrds);
		return FALSE;
	}

	for (guint i = 0; i < bufsz_dwrds; i++) {
		BcmRegNVMCommand tmp = { 0 };
		if (!fu_bcm57xx_recovery_device_nvram_clear_done (self, error))
			return FALSE;
		if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
							   REG_NVM_WRITE, GUINT32_TO_BE(buf[i]), error))
			return FALSE;
		if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
							   REG_NVM_ADDR, address, error))
			return FALSE;
		tmp.bits.Wr = TRUE;
		tmp.bits.Doit = TRUE;
		tmp.bits.First = i % page_size_dwrds == 0;
		tmp.bits.Last = (i + 1) % page_size_dwrds == 0;
		if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_DEVICE,
							   REG_NVM_COMMAND, tmp.r32, error))
			return FALSE;
		if (!fu_bcm57xx_recovery_device_nvram_wait_done (self, error)) {
			g_prefix_error (error, "failed to write @0x%x: ", address);
			return FALSE;
		}
		address += sizeof(guint32);
		fu_device_set_progress_full (FU_DEVICE (self), i, bufsz_dwrds);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_bcm57xx_recovery_device_detach (FuDevice *device, GError **error)
{
	/* unbind tg3 */
	return fu_device_unbind_driver (device, error);
}

static gboolean
fu_bcm57xx_recovery_device_attach (FuDevice *device, GError **error)
{
	g_autoptr(GError) error_local = NULL;

	/* bind tg3, which might fail if the module is not compiled */
	if (!fu_device_bind_driver (device, "pci", "tg3", &error_local)) {
		if (g_error_matches (error_local,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED)) {
			g_warning ("failed to bind tg3: %s", error_local->message);
		} else {
			g_propagate_prefixed_error (error,
						    g_steal_pointer (&error_local),
						    "failed to bind tg3: ");
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_bcm57xx_recovery_device_activate (FuDevice *device, GError **error)
{
	BcmRegAPEMode mode = { 0 };
	FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE (device);

	/* halt */
	mode.bits.Halt = 1;
	mode.bits.FastBoot = 0;
	if (!fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_APE,
						   REG_APE_MODE, mode.r32, error))
		return FALSE;

	/* boot */
	mode.bits.Halt = 0;
	mode.bits.FastBoot = 0;
	mode.bits.Reset = 1;
	return fu_bcm57xx_recovery_device_bar_write (self, FU_BCM57XX_BAR_APE,
						     REG_APE_MODE, mode.r32, error);
}

static GBytes *
fu_bcm57xx_recovery_device_dump_firmware (FuDevice *device, GError **error)
{
	FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE (device);
	gsize bufsz_dwrds = fu_device_get_firmware_size_max (FU_DEVICE (self)) / sizeof(guint32);
	g_autofree guint32 *buf_dwrds = g_new0 (guint32, bufsz_dwrds);
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(FuDeviceLocker) locker2 = NULL;

	/* read from hardware */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_READ);
	locker = fu_device_locker_new_full (self,
					    (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_acquire_lock,
					    (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_release_lock,
					    error);
	if (locker == NULL)
		return NULL;
	locker2 = fu_device_locker_new_full (self,
					     (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_enable,
					     (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_disable,
					     error);
	if (locker2 == NULL)
		return NULL;
	if (!fu_bcm57xx_recovery_device_nvram_read (self, 0x0, buf_dwrds, bufsz_dwrds, error))
		return NULL;
	if (!fu_device_locker_close (locker2, error))
		return NULL;
	return g_bytes_new (buf_dwrds, bufsz_dwrds * sizeof(guint32));
}

static FuFirmware *
fu_bcm57xx_recovery_device_prepare_firmware (FuDevice *device,
					     GBytes *fw,
					     FwupdInstallFlags flags,
					     GError **error)
{
	g_autoptr(FuFirmware) firmware_bin = fu_firmware_new ();
	g_autoptr(FuFirmware) firmware_tmp = fu_bcm57xx_firmware_new ();

	/* check is a NVRAM backup */
	if (!fu_firmware_parse (firmware_tmp, fw, flags, error)) {
		g_prefix_error (error, "failed to parse new firmware: ");
		return NULL;
	}
	if (!fu_bcm57xx_firmware_is_backup (FU_BCM57XX_FIRMWARE (firmware_tmp))) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "can only recover with backup firmware");
		return NULL;
	}
	if (!fu_firmware_parse (firmware_bin, fw, flags, error))
		return NULL;
	return g_steal_pointer (&firmware_bin);
}

static gboolean
fu_bcm57xx_recovery_device_write_firmware (FuDevice *device,
					   FuFirmware *firmware,
					   FwupdInstallFlags flags,
					   GError **error)
{
	FuBcm57xxRecoveryDevice *self= FU_BCM57XX_RECOVERY_DEVICE (device);
	const guint8 *buf;
	gsize bufsz = 0;
	gsize bufsz_dwrds;
	g_autofree guint32 *buf_dwrds = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(FuDeviceLocker) locker2 = NULL;
	g_autoptr(GBytes) blob = NULL;

	/* build the images into one linear blob of the correct size */
	fu_device_set_status (device, FWUPD_STATUS_DECOMPRESSING);
	blob = fu_firmware_write (firmware, error);
	if (blob == NULL)
		return FALSE;

	/* align into uint32_t buffer */
	buf = g_bytes_get_data (blob, &bufsz);
	bufsz_dwrds = bufsz / sizeof(guint32);
	buf_dwrds = g_new0 (guint32, bufsz_dwrds);
	if (!fu_memcpy_safe ((guint8 *) buf_dwrds, bufsz_dwrds * sizeof(guint32), 0x0,	/* dst */
			     buf, bufsz, 0x0,						/* src */
			     bufsz, error))
		return FALSE;

	/* hit hardware */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
	locker = fu_device_locker_new_full (self,
					    (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_acquire_lock,
					    (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_release_lock,
					    error);
	if (locker == NULL)
		return FALSE;
	locker2 = fu_device_locker_new_full (self,
					     (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_enable_write,
					     (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_disable,
					     error);
	if (locker2 == NULL)
		return FALSE;
	if (!fu_bcm57xx_recovery_device_nvram_write (self, 0x0, buf_dwrds, bufsz_dwrds, error))
		return FALSE;
	if (!fu_device_locker_close (locker2, error))
		return FALSE;
	if (!fu_device_locker_close (locker, error))
		return FALSE;

	/* reset APE */
	return fu_device_activate (device, error);
}

static gboolean
fu_bcm57xx_recovery_device_setup (FuDevice *device, GError **error)
{
	FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE (device);
	guint32 fwversion = 0;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(FuDeviceLocker) locker2 = NULL;

	locker = fu_device_locker_new_full (self,
					    (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_acquire_lock,
					    (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_release_lock,
					    error);
	if (locker == NULL)
		return FALSE;
	locker2 = fu_device_locker_new_full (self,
					     (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_enable,
					     (FuDeviceLockerFunc) fu_bcm57xx_recovery_device_nvram_disable,
					     error);
	if (locker2 == NULL)
		return FALSE;

	/* get NVRAM version */
	if (!fu_bcm57xx_recovery_device_nvram_read (self, BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERSION,
					   &fwversion, 1, error))
		return FALSE;
	if (fwversion != 0x0) {
		g_autofree gchar *fwversion_str = NULL;

		/* this is only set on the OSS firmware */
		fwversion_str = fu_common_version_from_uint32 (GUINT32_FROM_BE(fwversion),
							       FWUPD_VERSION_FORMAT_TRIPLET);
		fu_device_set_version_format (device, FWUPD_VERSION_FORMAT_TRIPLET);
		fu_device_set_version (device, fwversion_str);
		fu_device_set_version_raw (device, fwversion);
		fu_device_set_branch (device, BCM_FW_BRANCH_OSS_FIRMWARE);
	} else {
		guint32 bufver[4] = { 0x0 };
		guint32 veraddr = 0;
		g_autoptr(Bcm57xxVeritem) veritem = NULL;

		/* fall back to the string, e.g. '5719-v1.43' */
		if (!fu_bcm57xx_recovery_device_nvram_read (self,
							    BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERADDR,
							    &veraddr, 1, error))
			return FALSE;
		veraddr = GUINT32_FROM_BE(veraddr);
		if (veraddr > BCM_PHYS_ADDR_DEFAULT)
			veraddr -= BCM_PHYS_ADDR_DEFAULT;
		if (!fu_bcm57xx_recovery_device_nvram_read (self,
						   BCM_NVRAM_STAGE1_BASE + veraddr,
						   bufver, 4, error))
			return FALSE;
		veritem = fu_bcm57xx_veritem_new ((guint8 *) bufver, sizeof(bufver));
		if (veritem != NULL) {
			fu_device_set_version (device, veritem->version);
			fu_device_set_branch (device, veritem->branch);
			fu_device_set_version_format (device, veritem->verfmt);
		}
	}

	return TRUE;
}

static gboolean
fu_bcm57xx_recovery_device_open (FuDevice *device, GError **error)
{
#ifdef HAVE_MMAN_H
	FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE (device);
	FuUdevDevice *udev_device = FU_UDEV_DEVICE (device);
	const gchar *sysfs_path = fu_udev_device_get_sysfs_path (udev_device);
#endif

#ifdef RUNNING_ON_VALGRIND
	/* this can't work */
	if (RUNNING_ON_VALGRIND) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_NOT_SUPPORTED,
				     "cannot mmap'ing BARs when using valgrind");
		return FALSE;
	}
#endif

#ifdef HAVE_MMAN_H
	/* map BARs */
	for (guint i = 0; i < FU_BCM57XX_BAR_MAX; i++) {
		int memfd;
		struct stat st;
		g_autofree gchar *fn = NULL;
		g_autofree gchar *resfn = NULL;

		/* open 64 bit resource */
		resfn = g_strdup_printf ("resource%u", i * 2);
		fn = g_build_filename (sysfs_path, resfn, NULL);
		memfd = open (fn, O_RDWR | O_SYNC);
		if (memfd < 0) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_NOT_FOUND,
				     "error opening %s", fn);
			return FALSE;
		}
		if (fstat (memfd, &st) < 0) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_NOT_SUPPORTED,
				     "could not stat %s", fn);
			close (memfd);
			return FALSE;
		}

		/* mmap */
		if (g_getenv ("FWUPD_BCM57XX_VERBOSE") != NULL)
			g_debug ("mapping BAR[%u] %s for 0x%x bytes", i, fn, (guint) st.st_size);
		self->bar[i].buf = (guint8 *) mmap (0, st.st_size,
						    PROT_READ | PROT_WRITE,
						    MAP_SHARED, memfd, 0);
		self->bar[i].bufsz = st.st_size;
		close (memfd);
		if (self->bar[i].buf == MAP_FAILED) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_NOT_SUPPORTED,
				     "could not mmap %s: %s",
				     fn, strerror(errno));
			return FALSE;
		}
	}

	/* success */
	return TRUE;
#else
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "mmap() not supported as sys/mman.h not available");
	return FALSE;
#endif
}

static gboolean
fu_bcm57xx_recovery_device_close (FuDevice *device, GError **error)
{
#ifdef HAVE_MMAN_H
	FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE (device);

	/* unmap BARs */
	for (guint i = 0; i < FU_BCM57XX_BAR_MAX; i++) {
		if (self->bar[i].buf == NULL)
			continue;
		if (g_getenv ("FWUPD_BCM57XX_VERBOSE") != NULL)
			g_debug ("unmapping BAR[%u]", i);
		munmap (self->bar[i].buf, self->bar[i].bufsz);
		self->bar[i].buf = NULL;
		self->bar[i].bufsz = 0;
	}

	/* success */
	return TRUE;
#else
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "munmap() not supported as sys/mman.h not available");
	return FALSE;
#endif
}

static void
fu_bcm57xx_recovery_device_init (FuBcm57xxRecoveryDevice *self)
{
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NO_GUID_MATCHING);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_IGNORE_VALIDATION);
	fu_device_set_protocol (FU_DEVICE (self), "com.broadcom.bcm57xx");
	fu_device_add_icon (FU_DEVICE (self), "network-wired");
	fu_device_set_logical_id (FU_DEVICE (self), "recovery");

	/* other values are set from a quirk */
	fu_device_set_firmware_size (FU_DEVICE (self), BCM_FIRMWARE_SIZE);

	/* no BARs mapped */
	for (guint i = 0; i < FU_BCM57XX_BAR_MAX; i++) {
		self->bar[i].buf = NULL;
		self->bar[i].bufsz = 0;
	}
}

static gboolean
fu_bcm57xx_recovery_device_probe (FuDevice *device, GError **error)
{
	/* FuUdevDevice->probe */
	if (!FU_DEVICE_CLASS (fu_bcm57xx_recovery_device_parent_class)->probe (device, error))
		return FALSE;
	return fu_udev_device_set_physical_id (FU_UDEV_DEVICE (device), "pci", error);
}

static void
fu_bcm57xx_recovery_device_class_init (FuBcm57xxRecoveryDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	klass_device->activate = fu_bcm57xx_recovery_device_activate;
	klass_device->prepare_firmware = fu_bcm57xx_recovery_device_prepare_firmware;
	klass_device->setup = fu_bcm57xx_recovery_device_setup;
	klass_device->reload = fu_bcm57xx_recovery_device_setup;
	klass_device->open = fu_bcm57xx_recovery_device_open;
	klass_device->close = fu_bcm57xx_recovery_device_close;
	klass_device->write_firmware = fu_bcm57xx_recovery_device_write_firmware;
	klass_device->dump_firmware = fu_bcm57xx_recovery_device_dump_firmware;
	klass_device->attach = fu_bcm57xx_recovery_device_attach;
	klass_device->detach = fu_bcm57xx_recovery_device_detach;
	klass_device->probe = fu_bcm57xx_recovery_device_probe;
}

FuBcm57xxRecoveryDevice *
fu_bcm57xx_recovery_device_new (void)
{
	FuUdevDevice *self = g_object_new (FU_TYPE_BCM57XX_RECOVERY_DEVICE, NULL);
	return FU_BCM57XX_RECOVERY_DEVICE (self);
}
