/*
 * Copyright (C) 2017 Christian J. Kellner <christian@kellner.me>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#include "fu-plugin-vfuncs.h"
#include "fu-hash.h"
#include "fu-device-metadata.h"
#include "fu-thunderbolt-image.h"

#define TBT_NVM_RETRY_TIMEOUT				200	/* ms */
#define FU_PLUGIN_THUNDERBOLT_UPDATE_TIMEOUT		60000	/* ms */

typedef void (*UEventNotify) (FuPlugin	  *plugin,
			      GUdevDevice *udevice,
			      const gchar *action,
			      gpointer     user_data);

struct FuPluginData {
	GUdevClient   *udev;
};

static gboolean
fu_plugin_thunderbolt_safe_kernel (FuPlugin *plugin, GError **error)
{
	g_autofree gchar *minimum_kernel = NULL;
	struct utsname name_tmp;

	memset (&name_tmp, 0, sizeof(struct utsname));
	if (uname (&name_tmp) < 0) {
		g_debug ("Failed to read current kernel version");
		return TRUE;
	}

	minimum_kernel = fu_plugin_get_config_value (plugin, "MinimumKernelVersion");
	if (minimum_kernel == NULL) {
		g_debug ("Ignoring kernel safety checks");
		return TRUE;
	}

	if (fu_common_vercmp_full (name_tmp.release,
				   minimum_kernel,
				   FWUPD_VERSION_FORMAT_TRIPLET) < 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "kernel %s may not have full Thunderbolt support",
			     name_tmp.release);
		return FALSE;
	}
	g_debug ("Using kernel %s (minimum %s)", name_tmp.release, minimum_kernel);

	return TRUE;
}

static gchar *
fu_plugin_thunderbolt_gen_id_from_syspath (const gchar *syspath)
{
	gchar *id;
	id = g_strdup_printf ("tbt-%s", syspath);
	g_strdelimit (id, "/:.-", '_');
	return id;
}


static gchar *
fu_plugin_thunderbolt_gen_id (GUdevDevice *device)
{
	const gchar *syspath = g_udev_device_get_sysfs_path (device);
	return fu_plugin_thunderbolt_gen_id_from_syspath (syspath);
}

static gboolean
udev_device_get_sysattr_guint64 (GUdevDevice *device,
				 const gchar *name,
				 guint64 *val_out,
				 GError **error)
{
	const gchar *sysfs;

	sysfs = g_udev_device_get_sysfs_attr (device, name);
	if (sysfs == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "missing sysfs attribute %s", name);
		return FALSE;
	}

	*val_out = g_ascii_strtoull (sysfs, NULL, 16);
	if (*val_out == 0x0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to parse %s", sysfs);
		return FALSE;
	}

	return TRUE;
}

static guint16
fu_plugin_thunderbolt_udev_get_uint16 (GUdevDevice *device,
				       const gchar *name,
				       GError **error)
{

	guint64 id = 0;

	if (!udev_device_get_sysattr_guint64 (device, name, &id, error))
		return 0x0;

	if (id > G_MAXUINT16) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "%s overflows",
			     name);
		return 0x0;
	}

	return (guint16) id;
}

static gboolean
fu_plugin_thunderbolt_is_host (GUdevDevice *device)
{
	g_autoptr(GUdevDevice) parent = NULL;
	const gchar *name;

	/* the (probably safe) assumption this code makes is
	 * that the thunderbolt device which is a direct child
	 * of the domain is the host controller device itself */
	parent = g_udev_device_get_parent (device);
	name = g_udev_device_get_name (parent);
	if (name == NULL)
		return FALSE;

	return g_str_has_prefix (name, "domain");
}

static GFile *
fu_plugin_thunderbolt_find_nvmem (GUdevDevice  *udevice,
				  gboolean      active,
				  GError      **error)
{
	const gchar *nvmem_dir = active ? "nvm_active" : "nvm_non_active";
	const gchar *devpath;
	const gchar *name;
	g_autoptr(GDir) d = NULL;

	devpath = g_udev_device_get_sysfs_path (udevice);
	if (G_UNLIKELY (devpath == NULL)) {
		g_set_error_literal (error,
			     FWUPD_ERROR, FWUPD_ERROR_INTERNAL,
			     "Could not determine sysfs path for device");
		return NULL;
	}

	d = g_dir_open (devpath, 0, error);
	if (d == NULL)
		return NULL;

	while ((name = g_dir_read_name (d)) != NULL) {
		if (g_str_has_prefix (name, nvmem_dir)) {
			g_autoptr(GFile) parent = g_file_new_for_path (devpath);
			g_autoptr(GFile) nvm_dir = g_file_get_child (parent, name);
			return g_file_get_child (nvm_dir, "nvmem");
		}
	}

	g_set_error_literal (error,
			     FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
			     "Could not find non-volatile memory location");
	return NULL;
}

static gchar *
fu_plugin_thunderbolt_parse_version (const gchar *version_raw)
{
	g_auto(GStrv) split = NULL;
	if (version_raw == NULL)
		return NULL;
	split = g_strsplit (version_raw, ".", -1);
	if (g_strv_length (split) != 2)
		return NULL;
	return g_strdup_printf ("%02x.%02x",
				(guint) g_ascii_strtoull (split[0], NULL, 16),
				(guint) g_ascii_strtoull (split[1], NULL, 16));
}

static gchar *
fu_plugin_thunderbolt_udev_get_version (GUdevDevice *udevice)
{
	const gchar *version = NULL;

	for (guint i = 0; i < 50; i++) {
		version = g_udev_device_get_sysfs_attr (udevice, "nvm_version");
		if (version != NULL)
			break;
		g_debug ("Attempt %u: Failed to read NVM version", i);
		if (errno != EAGAIN)
			break;
		g_usleep (TBT_NVM_RETRY_TIMEOUT * 1000);
	}

	return fu_plugin_thunderbolt_parse_version (version);
}

static gboolean
fu_plugin_thunderbolt_is_native (GUdevDevice *udevice, gboolean *is_native, GError **error)
{
	gsize nr_chunks;
	g_autoptr(GFile) nvmem = NULL;
	g_autoptr(GBytes) controller_fw = NULL;
	g_autoptr(GInputStream) istr = NULL;

	nvmem = fu_plugin_thunderbolt_find_nvmem (udevice, TRUE, error);
	if (nvmem == NULL)
		return FALSE;

	/* read just enough bytes to read the status byte */
	nr_chunks = (FU_TBT_OFFSET_NATIVE + FU_TBT_CHUNK_SZ - 1) / FU_TBT_CHUNK_SZ;
	istr = G_INPUT_STREAM (g_file_read (nvmem, NULL, error));
	if (istr == NULL)
		return FALSE;
	controller_fw = g_input_stream_read_bytes (istr,
						   nr_chunks * FU_TBT_CHUNK_SZ,
						   NULL, error);
	if (controller_fw == NULL)
		return FALSE;

	return fu_thunderbolt_image_controller_is_native (controller_fw,
							  is_native,
							  error);
}

static gboolean
fu_plugin_thunderbolt_can_update (GUdevDevice *udevice)
{
	g_autoptr(GError) nvmem_error = NULL;
	g_autoptr(GFile) non_active_nvmem = NULL;

	non_active_nvmem = fu_plugin_thunderbolt_find_nvmem (udevice, FALSE,
							     &nvmem_error);
	if (non_active_nvmem == NULL) {
		g_debug ("%s", nvmem_error->message);
		return FALSE;
	}

	return TRUE;
}

static void
fu_plugin_thunderbolt_add (FuPlugin *plugin, GUdevDevice *device)
{
	FuDevice *dev_tmp;
	const gchar *name = NULL;
	const gchar *uuid;
	const gchar *vendor;
	const gchar *devpath;
	const gchar *devtype;
	gboolean is_host;
	gboolean is_safemode = FALSE;
	gboolean is_native = FALSE;
	guint16 did;
	guint16 vid;
	guint16 gen;
	g_autofree gchar *id = NULL;
	g_autofree gchar *version = NULL;
	g_autofree gchar *vendor_id = NULL;
	g_autofree gchar *device_id = NULL;
	g_autofree gchar *domain_id = NULL;
	g_autoptr(FuDevice) dev = NULL;
	g_autoptr(GError) error_vid = NULL;
	g_autoptr(GError) error_did = NULL;
	g_autoptr(GError) error_gen = NULL;
	g_autoptr(GError) error_setup = NULL;

	uuid = g_udev_device_get_sysfs_attr (device, "unique_id");
	if (uuid == NULL) {
		/* most likely the domain itself, ignore */
		return;
	}

	devpath = g_udev_device_get_sysfs_path (device);

	devtype = g_udev_device_get_devtype (device);
	if (g_strcmp0 (devtype, "thunderbolt_device") != 0) {
		g_debug ("ignoring %s device at %s", devtype, devpath);
		return;
	}

	g_debug ("adding udev device: %s at %s", uuid, devpath);

	id = fu_plugin_thunderbolt_gen_id (device);
	dev_tmp = fu_plugin_cache_lookup (plugin, id);
	if (dev_tmp != NULL) {
		/* devices that are force-powered are re-added */
		g_debug ("ignoring duplicate %s", id);
		return;
	}

	/* these may be missing on ICL or later */
	vid = fu_plugin_thunderbolt_udev_get_uint16 (device, "vendor", &error_vid);
	if (vid == 0x0)
		g_debug ("failed to get Vendor ID: %s", error_vid->message);

	did = fu_plugin_thunderbolt_udev_get_uint16 (device, "device", &error_did);
	if (did == 0x0)
		g_debug ("failed to get Device ID: %s", error_did->message);

	/* requires kernel 5.5 or later, non-fatal if not available */
	gen = fu_plugin_thunderbolt_udev_get_uint16 (device, "generation", &error_gen);
	if (gen == 0)
		g_debug ("Unable to read generation: %s", error_gen->message);

	dev = fu_device_new ();

	is_host = fu_plugin_thunderbolt_is_host (device);

	version = fu_plugin_thunderbolt_udev_get_version (device);
	/* test for safe mode */
	if (is_host && version == NULL) {
		g_autoptr(GError) error_local = NULL;
		g_autofree gchar *test_safe = NULL;
		g_autofree gchar *safe_path = NULL;
		/* glib can't return a properly mapped -ENODATA but the
		 * kernel only returns -ENODATA or -EAGAIN */
		safe_path = g_build_path ("/", devpath, "nvm_version", NULL);
		if (!g_file_get_contents (safe_path, &test_safe, NULL, &error_local) &&
		    !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
			g_warning ("%s is in safe mode --  VID/DID will "
				   "need to be set by another plugin",
				   devpath);
			version = g_strdup ("00.00");
			is_safemode = TRUE;
			device_id = g_strdup ("TBT-safemode");
			fu_device_set_metadata_boolean (dev, FU_DEVICE_METADATA_TBT_IS_SAFE_MODE, TRUE);
		}
		fu_plugin_add_report_metadata (plugin, "ThunderboltSafeMode",
					       is_safemode ? "True" : "False");
	}
	if (!is_safemode) {
		if (fu_plugin_thunderbolt_can_update (device)) {
			/* USB4 controllers don't have a concept of legacy vs native
			 * so don't try to read a native attribute from their NVM */
			if (is_host && gen < 4) {
				g_autofree gchar *domain = g_path_get_basename (devpath);
				g_autoptr(GError) native_error = NULL;
				if (!fu_plugin_thunderbolt_is_native (device,
								      &is_native,
								      &native_error)) {
					g_warning ("failed to get native mode status: %s",
						   native_error->message);
					return;
				}
				fu_plugin_add_report_metadata (plugin,
							       "ThunderboltNative",
							       is_native ? "True" : "False");
				domain_id = g_strdup_printf ("TBT-%04x%04x%s-controller%s",
								(guint) vid,
								(guint) did,
								is_native ? "-native" : "",
								domain);

			}
			vendor_id = g_strdup_printf ("TBT:0x%04X", (guint) vid);
			device_id = g_strdup_printf ("TBT-%04x%04x%s",
						     (guint) vid,
						     (guint) did,
						     is_native ? "-native" : "");
			fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_UPDATABLE);
			fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
		} else {
			device_id = g_strdup ("TBT-fixed");
			fu_device_set_update_error (dev, "Missing non-active nvmem");
		}
	} else {
		fu_device_set_update_error (dev, "Device is in safe mode");
	}

	fu_device_set_physical_id (dev, uuid);

	fu_device_set_metadata (dev, "sysfs-path", devpath);
	if (!is_host)
		name = g_udev_device_get_sysfs_attr (device, "device_name");
	if (name == NULL) {
		if (gen == 4)
			name = "USB4 Controller";
		else
			name = "Thunderbolt Controller";
	}
	fu_device_set_name (dev, name);
	if (is_host)
		fu_device_set_summary (dev, "Unmatched performance for high-speed I/O");
	fu_device_add_icon (dev, "thunderbolt");
	fu_device_set_protocol (dev, "com.intel.thunderbolt");

	fu_device_set_quirks (dev, fu_plugin_get_quirks (plugin));
	vendor = g_udev_device_get_sysfs_attr (device, "vendor_name");
	if (vendor != NULL)
		fu_device_set_vendor (dev, vendor);
	if (vendor_id != NULL)
		fu_device_set_vendor_id (dev, vendor_id);
	if (device_id != NULL)
		fu_device_add_instance_id (dev, device_id);
	if (domain_id != NULL)
		fu_device_add_instance_id (dev, domain_id);
	if (version != NULL)
		fu_device_set_version (dev, version, FWUPD_VERSION_FORMAT_PAIR);
	if (is_host)
		fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_INTERNAL);
	fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_REQUIRE_AC);

	/* we never open the device, so convert the instance IDs */
	if (!fu_device_setup (dev, &error_setup)) {
		g_warning ("failed to setup: %s", error_setup->message);
		return;
	}
	fu_plugin_cache_add (plugin, id, dev);
	fu_plugin_device_add (plugin, dev);

	/* inhibit the idle sleep of the daemon */
	fu_plugin_add_rule (plugin, FU_PLUGIN_RULE_INHIBITS_IDLE,
			    "thunderbolt requires device wakeup");
}

static void
fu_plugin_thunderbolt_remove (FuPlugin *plugin, GUdevDevice *device)
{
	FuDevice *dev;
	g_autofree gchar *id = NULL;

	id = fu_plugin_thunderbolt_gen_id (device);
	dev = fu_plugin_cache_lookup (plugin, id);
	if (dev == NULL)
		return;

	/* on supported systems other plugins may use a GPIO to force
	 * power on supported devices even when in low power mode --
	 * this will happen in coldplug_prepare and prepare_for_update */
	if (fu_plugin_thunderbolt_is_host (device) &&
	    !fu_device_has_flag (dev, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) &&
	    fu_device_get_metadata_boolean (dev, FU_DEVICE_METADATA_TBT_CAN_FORCE_POWER)) {
		g_debug ("ignoring remove event as force powered");
		return;
	}

	fu_plugin_device_remove (plugin, dev);
	fu_plugin_cache_remove (plugin, id);
}

static void
fu_plugin_thunderbolt_change (FuPlugin *plugin, GUdevDevice *device)
{
	FuDevice *dev;
	g_autofree gchar *version = NULL;
	g_autofree gchar *id = NULL;

	id = fu_plugin_thunderbolt_gen_id (device);
	dev = fu_plugin_cache_lookup (plugin, id);
	if (dev == NULL) {
		g_warning ("got change event for unknown device, adding instead");
		fu_plugin_thunderbolt_add (plugin, device);
		return;
	}

	version = fu_plugin_thunderbolt_udev_get_version (device);
	fu_device_set_version (dev, version, FWUPD_VERSION_FORMAT_PAIR);
}

static gboolean
udev_uevent_cb (GUdevClient *udev,
		const gchar *action,
		GUdevDevice *device,
		gpointer     user_data)
{
	FuPlugin *plugin = (FuPlugin *) user_data;

	if (action == NULL)
		return TRUE;

	g_debug ("uevent for %s: %s", g_udev_device_get_sysfs_path (device), action);

	if (g_str_equal (action, "add")) {
		fu_plugin_thunderbolt_add (plugin, device);
	} else if (g_str_equal (action, "remove")) {
		fu_plugin_thunderbolt_remove (plugin, device);
	} else if (g_str_equal (action, "change")) {
		fu_plugin_thunderbolt_change (plugin, device);
	}

	return TRUE;
}

static FuPluginValidation
fu_plugin_thunderbolt_validate_firmware (GUdevDevice  *udevice,
					 GBytes       *blob_fw,
					 GError      **error)
{
	g_autoptr(GFile) nvmem = NULL;
	g_autoptr(GBytes) controller_fw = NULL;
	gchar *content;
	gsize length;

	nvmem = fu_plugin_thunderbolt_find_nvmem (udevice, TRUE, error);
	if (nvmem == NULL)
		return VALIDATION_FAILED;

	if (!g_file_load_contents (nvmem, NULL, &content, &length, NULL, error))
		return VALIDATION_FAILED;

	controller_fw = g_bytes_new_take (content, length);
	return fu_thunderbolt_image_validate (controller_fw, blob_fw, error);
}

static gboolean
fu_plugin_thunderbolt_trigger_update (GUdevDevice  *udevice,
				      GError      **error)
{

	const gchar *devpath;
	ssize_t n;
	int fd;
	int r;
	g_autofree gchar *auth_path = NULL;

	devpath = g_udev_device_get_sysfs_path (udevice);
	auth_path = g_build_filename (devpath, "nvm_authenticate", NULL);

	fd = open (auth_path, O_WRONLY | O_CLOEXEC);
	if (fd < 0) {
		g_set_error (error, G_IO_ERROR,
			     g_io_error_from_errno (errno),
			     "could not open 'nvm_authenticate': %s",
			     g_strerror (errno));
		return FALSE;
	}

	do {
		n = write (fd, "1", 1);
		if (n < 1 && errno != EINTR) {
			g_set_error (error, G_IO_ERROR,
				     g_io_error_from_errno (errno),
				     "could not write to 'nvm_authenticate': %s",
				     g_strerror (errno));
			(void) close (fd);
			return FALSE;
		}
	} while (n < 1);

	r = close (fd);
	if (r < 0 && errno != EINTR) {
		g_set_error (error, G_IO_ERROR,
			     g_io_error_from_errno (errno),
			     "could not close 'nvm_authenticate': %s",
			     g_strerror (errno));
		return FALSE;
	}

	return TRUE;
}

static gboolean
fu_plugin_thunderbolt_write_firmware (FuDevice     *device,
				      GUdevDevice  *udevice,
				      GBytes       *blob_fw,
				      GError      **error)
{
	gsize fw_size;
	gsize nwritten;
	gssize n;
	g_autoptr(GFile) nvmem = NULL;
	g_autoptr(GOutputStream) os = NULL;

	nvmem = fu_plugin_thunderbolt_find_nvmem (udevice, FALSE, error);
	if (nvmem == NULL)
		return FALSE;

	os = (GOutputStream *) g_file_append_to (nvmem,
						 G_FILE_CREATE_NONE,
						 NULL,
						 error);

	if (os == NULL)
		return FALSE;

	nwritten = 0;
	fw_size = g_bytes_get_size (blob_fw);
	fu_device_set_progress_full (device, nwritten, fw_size);

	do {
		g_autoptr(GBytes) fw_data = NULL;

		fw_data = g_bytes_new_from_bytes (blob_fw,
						  nwritten,
						  fw_size - nwritten);

		n = g_output_stream_write_bytes (os,
						 fw_data,
						 NULL,
						 error);
		if (n < 0)
			return FALSE;

		nwritten += n;
		fu_device_set_progress_full (device, nwritten, fw_size);

	} while (nwritten < fw_size);

	if (nwritten != fw_size) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_WRITE,
				     "Could not write all data to nvmem");
		return FALSE;
	}

	return g_output_stream_close (os, NULL, error);
}

/* virtual functions */

void
fu_plugin_init (FuPlugin *plugin)
{
	FuPluginData *data = fu_plugin_alloc_data (plugin, sizeof (FuPluginData));
	const gchar *subsystems[] = { "thunderbolt", NULL };

	fu_plugin_set_build_hash (plugin, FU_BUILD_HASH);
	data->udev = g_udev_client_new (subsystems);
	g_signal_connect (data->udev, "uevent",
			  G_CALLBACK (udev_uevent_cb), plugin);

	/* dell-dock plugin uses a slower bus for flashing */
	fu_plugin_add_rule (plugin, FU_PLUGIN_RULE_BETTER_THAN, "dell_dock");
}

void
fu_plugin_destroy (FuPlugin *plugin)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	g_object_unref (data->udev);
}

static gboolean
fu_plugin_thunderbolt_coldplug (FuPlugin *plugin, GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	GList *devices;

	devices = g_udev_client_query_by_subsystem (data->udev, "thunderbolt");
	for (GList *l = devices; l != NULL; l = l->next) {
		GUdevDevice *device = l->data;
		fu_plugin_thunderbolt_add (plugin, device);
	}

	g_list_foreach (devices, (GFunc) g_object_unref, NULL);
	g_list_free (devices);

	return TRUE;
}

gboolean
fu_plugin_startup (FuPlugin *plugin, GError **error)
{
	return fu_plugin_thunderbolt_safe_kernel (plugin, error);
}

gboolean
fu_plugin_coldplug (FuPlugin *plugin, GError **error)
{
	return fu_plugin_thunderbolt_coldplug (plugin, error);
}

gboolean
fu_plugin_recoldplug (FuPlugin *plugin, GError **error)
{
	return fu_plugin_thunderbolt_coldplug (plugin, error);
}

gboolean
fu_plugin_update (FuPlugin *plugin,
		  FuDevice *dev,
		  GBytes *blob_fw,
		  FwupdInstallFlags flags,
		  GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	const gchar *devpath;
	g_autoptr(GUdevDevice) udevice = NULL;
	g_autoptr(GError) error_local = NULL;
	gboolean install_force = (flags & FWUPD_INSTALL_FLAG_FORCE) != 0;
	gboolean device_ignore_validation = fu_device_has_flag (dev, FWUPD_DEVICE_FLAG_IGNORE_VALIDATION);
	FuPluginValidation validation;

	devpath = fu_device_get_metadata (dev, "sysfs-path");
	g_return_val_if_fail (devpath, FALSE);

	udevice = g_udev_client_query_by_sysfs_path (data->udev, devpath);
	if (udevice == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_FOUND,
			     "could not find thunderbolt device at %s",
			     devpath);
		return FALSE;
	}

	validation = fu_plugin_thunderbolt_validate_firmware (udevice,
							      blob_fw,
							      &error_local);
	if (validation != VALIDATION_PASSED) {
		g_autofree gchar* msg = NULL;
		switch (validation) {
		case VALIDATION_FAILED:
			msg = g_strdup_printf ("could not validate firmware: %s",
					       error_local->message);
			break;
		case UNKNOWN_DEVICE:
			msg = g_strdup ("firmware validation seems to be passed but the device is unknown");
			break;
		default:
			break;
		}
		if (!install_force && !device_ignore_validation) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "%s. "
				     "See https://github.com/fwupd/fwupd/wiki/Thunderbolt:-Validation-failed-or-unknown-device for more information.",
				     msg);
			return FALSE;
		}
		g_warning ("%s", msg);
	}

	fu_device_set_status (dev, FWUPD_STATUS_DEVICE_WRITE);
	if (!fu_plugin_thunderbolt_write_firmware (dev, udevice, blob_fw, error)) {
		g_prefix_error (error,
				"could not write firmware to thunderbolt device at %s: ",
				devpath);
		return FALSE;
	}

	if (!fu_plugin_thunderbolt_trigger_update (udevice, error)) {
		g_prefix_error (error, "could not start thunderbolt device upgrade: ");
		return FALSE;
	}

	fu_device_set_status (dev, FWUPD_STATUS_DEVICE_RESTART);
	fu_device_set_remove_delay (dev, FU_PLUGIN_THUNDERBOLT_UPDATE_TIMEOUT);
	fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);

	return TRUE;
}

gboolean
fu_plugin_update_attach (FuPlugin *plugin,
			 FuDevice *dev,
			 GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	const gchar *devpath;
	const gchar *attribute;
	guint64 status;
	g_autoptr(GUdevDevice) udevice = NULL;

	devpath = fu_device_get_metadata (dev, "sysfs-path");
	udevice = g_udev_client_query_by_sysfs_path (data->udev, devpath);
	if (udevice == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_FOUND,
			     "could not find thunderbolt device at %s",
			     devpath);
		return FALSE;
	}

	/* now check if the update actually worked */
	attribute = g_udev_device_get_sysfs_attr (udevice, "nvm_authenticate");
	if (attribute == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to find nvm_authenticate attribute for %s",
			     fu_device_get_name (dev));
		return FALSE;
	}
	status = g_ascii_strtoull (attribute, NULL, 16);
	if (status == G_MAXUINT64 && errno == ERANGE) {
		g_set_error (error, G_IO_ERROR,
			     g_io_error_from_errno (errno),
			     "failed to read 'nvm_authenticate: %s",
			     g_strerror (errno));
		return FALSE;
	}

	/* anything else then 0x0 means we got an error */
	if (status != 0x0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "update failed (status %" G_GINT64_MODIFIER "x)", status);
		return FALSE;
	}

	return TRUE;
}
