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

#define G_LOG_DOMAIN "FuBackend"

#include "config.h"

#include <fwupdplugin.h>

#include <glib-unix.h>
#include <glib/gstdio.h>
#include <linux/netlink.h>
#include <sys/socket.h>

#include "fu-context-private.h"
#include "fu-engine-struct.h"
#include "fu-udev-backend.h"
#include "fu-udev-device-private.h"

struct _FuUdevBackend {
	FuBackend parent_instance;
	gint netlink_fd;
	GHashTable *map_paths;	    /* of str:None */
	GHashTable *coldplug_cache; /* of str:FuUdevBackendColdplugCacheItem */
	GPtrArray *dpaux_devices;   /* of FuDpauxDevice */
	guint dpaux_devices_rescan_id;
	gboolean done_coldplug;
};

typedef struct {
	FuUdevDevice *udev_device;
	GError *error;
} FuUdevBackendColdplugCacheItem;

G_DEFINE_TYPE(FuUdevBackend, fu_udev_backend, FU_TYPE_BACKEND)

#define FU_UDEV_BACKEND_DPAUX_RESCAN_DELAY 5 /* s */

static void
fu_udev_backend_coldplug_cache_item_free(FuUdevBackendColdplugCacheItem *item)
{
	if (item->udev_device != NULL)
		g_object_unref(item->udev_device);
	if (item->error)
		g_error_free(item->error);
	g_free(item);
}

static void
fu_udev_backend_coldplug_cache_add_device(FuUdevBackend *self,
					  const gchar *fn,
					  FuUdevDevice *udev_device)
{
	if (!self->done_coldplug) {
		FuUdevBackendColdplugCacheItem *item = g_new0(FuUdevBackendColdplugCacheItem, 1);
		item->udev_device = g_object_ref(udev_device);
		g_hash_table_insert(self->coldplug_cache, g_strdup(fn), item);
	}
}

static void
fu_udev_backend_coldplug_cache_add_error(FuUdevBackend *self, const gchar *fn, GError *error)
{
	if (!self->done_coldplug) {
		FuUdevBackendColdplugCacheItem *item = g_new0(FuUdevBackendColdplugCacheItem, 1);
		item->error = g_error_copy(error);
		g_hash_table_insert(self->coldplug_cache, g_strdup(fn), item);
	}
}

static void
fu_udev_backend_to_string(FuBackend *backend, guint idt, GString *str)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);
	fwupd_codec_string_append_bool(str, idt, "DoneColdplug", self->done_coldplug);
}

static void
fu_udev_backend_rescan_dpaux_device(FuUdevBackend *self, FuDevice *dpaux_device)
{
	FuDevice *device_tmp;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(GError) error_local = NULL;

	/* find the device we enumerated */
	g_debug("looking for %s", fu_device_get_backend_id(dpaux_device));
	device_tmp =
	    fu_backend_lookup_by_id(FU_BACKEND(self), fu_device_get_backend_id(dpaux_device));

	/* open */
	fu_device_probe_invalidate(dpaux_device);
	locker = fu_device_locker_new(dpaux_device, &error_local);
	if (locker == NULL) {
		g_debug("failed to open device %s: %s",
			fu_device_get_backend_id(dpaux_device),
			error_local->message);
		if (device_tmp != NULL)
			fu_backend_device_removed(FU_BACKEND(self), FU_DEVICE(device_tmp));
		return;
	}
	if (device_tmp == NULL) {
		fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(dpaux_device));
		return;
	}
}

static gboolean
fu_udev_backend_rescan_dpaux_devices_cb(gpointer user_data)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(user_data);
	for (guint i = 0; i < self->dpaux_devices->len; i++) {
		FuDevice *dpaux_device = g_ptr_array_index(self->dpaux_devices, i);
		fu_udev_backend_rescan_dpaux_device(self, dpaux_device);
	}
	self->dpaux_devices_rescan_id = 0;
	return FALSE;
}

static void
fu_udev_backend_rescan_dpaux_devices(FuUdevBackend *self)
{
	if (self->dpaux_devices_rescan_id != 0)
		g_source_remove(self->dpaux_devices_rescan_id);
	self->dpaux_devices_rescan_id =
	    g_timeout_add_seconds(FU_UDEV_BACKEND_DPAUX_RESCAN_DELAY,
				  fu_udev_backend_rescan_dpaux_devices_cb,
				  self);
}

static FuUdevDevice *
fu_udev_backend_create_device(FuUdevBackend *self, const gchar *fn, GError **error);

static void
fu_udev_backend_create_ddc_proxy(FuUdevBackend *self, FuUdevDevice *udev_device)
{
	g_autofree gchar *proxy_sysfs_path = NULL;
	g_autofree gchar *proxy_sysfs_real = NULL;
	g_autoptr(FuUdevDevice) proxy = NULL;
	g_autoptr(GError) error_local = NULL;

	proxy_sysfs_path =
	    g_build_filename(fu_udev_device_get_sysfs_path(udev_device), "ddc", NULL);
	proxy_sysfs_real = fu_path_make_absolute(proxy_sysfs_path, &error_local);
	if (proxy_sysfs_real == NULL) {
		g_debug("failed to resolve %s: %s", proxy_sysfs_path, error_local->message);
		return;
	}
	proxy = fu_udev_backend_create_device(self, proxy_sysfs_real, &error_local);
	if (proxy == NULL) {
		g_warning("failed to create DRM DDC device: %s", error_local->message);
		return;
	}
	fu_device_add_private_flag(FU_DEVICE(proxy), FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS);
	if (!fu_device_probe(FU_DEVICE(proxy), &error_local)) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT))
			return;
		g_warning("failed to probe DRM DDC device: %s", error_local->message);
		return;
	}
	fu_device_add_private_flag(FU_DEVICE(udev_device), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY);
	fu_device_set_proxy_gtype(FU_DEVICE(udev_device), FU_TYPE_I2C_DEVICE);
	fu_device_set_proxy(FU_DEVICE(udev_device), FU_DEVICE(proxy));
}

static GType
fu_udev_backend_get_gtype_for_subsystem_devtype(const gchar *subsystem, const gchar *devtype)
{
	struct {
		const gchar *subsystem;
		const gchar *devtype;
		GType gtype;
	} map[] = {
	    {"mei", NULL, FU_TYPE_MEI_DEVICE},
	    {"drm", NULL, FU_TYPE_DRM_DEVICE},
	    {"usb", "usb_device", FU_TYPE_USB_DEVICE},
	    {"i2c", NULL, FU_TYPE_I2C_DEVICE},
	    {"i2c-dev", NULL, FU_TYPE_I2C_DEVICE},
	    {"drm_dp_aux_dev", NULL, FU_TYPE_DPAUX_DEVICE},
	    {"hidraw", NULL, FU_TYPE_HIDRAW_DEVICE},
	    {"block", "disk", FU_TYPE_BLOCK_DEVICE},
	    {"block", "partition", FU_TYPE_BLOCK_PARTITION},
	    {"serio", NULL, FU_TYPE_SERIO_DEVICE},
	    {"pci", NULL, FU_TYPE_PCI_DEVICE},
	    {"video4linux", NULL, FU_TYPE_V4L_DEVICE},
	};
	for (guint i = 0; i < G_N_ELEMENTS(map); i++) {
		if (g_strcmp0(subsystem, map[i].subsystem) == 0 &&
		    (map[i].devtype == NULL || g_strcmp0(devtype, map[i].devtype) == 0)) {
			return map[i].gtype;
		}
	}
	return FU_TYPE_UDEV_DEVICE;
}

static FuDevice *
fu_udev_backend_create_device_for_donor(FuBackend *backend, FuDevice *donor, GError **error)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);
	FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
	g_autoptr(FuDevice) device = NULL;
	GType gtype = FU_TYPE_UDEV_DEVICE;

	/* ignore zram and loop block devices -- of which there are dozens on systems with snap */
	if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(donor)), "block") == 0) {
		g_autofree gchar *basename =
		    g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(donor)));
		if (g_str_has_prefix(basename, "zram") || g_str_has_prefix(basename, "loop")) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "ignoring uninteresting %s block device",
				    basename);
			return NULL;
		}
	}

	/* create actual device with correct GType */
	gtype = fu_udev_backend_get_gtype_for_subsystem_devtype(
	    fu_udev_device_get_subsystem(FU_UDEV_DEVICE(donor)),
	    fu_udev_device_get_devtype(FU_UDEV_DEVICE(donor)));
	if (gtype == FU_TYPE_UDEV_DEVICE) {
		device = g_object_ref(donor);
	} else {
		device = g_object_new(gtype, "backend", FU_BACKEND(self), NULL);
		fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_ALL);
		if (!fu_device_probe(device, error)) {
			g_prefix_error_literal(error, "failed to probe: ");
			return NULL;
		}
	}

	/* these are used without a subclass */
	if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "msr") == 0)
		fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ);

	/* the DRM device has a i2c device that is used for communicating with the scaler */
	if (gtype == FU_TYPE_DRM_DEVICE)
		fu_udev_backend_create_ddc_proxy(self, FU_UDEV_DEVICE(device));

	/* notify plugins using fu_plugin_add_udev_subsystem() */
	if (fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)) != NULL) {
		g_autoptr(GPtrArray) possible_plugins = NULL;
		g_autofree gchar *subsystem =
		    fu_udev_device_get_subsystem_devtype(FU_UDEV_DEVICE(device));

		possible_plugins =
		    fu_context_get_plugin_names_for_udev_subsystem(ctx, subsystem, NULL);
		if (possible_plugins != NULL) {
			for (guint i = 0; i < possible_plugins->len; i++) {
				const gchar *plugin_name = g_ptr_array_index(possible_plugins, i);
				fu_device_add_possible_plugin(device, plugin_name);
			}
		}
	}

	/* set in fu-self-test */
	if (g_getenv("FWUPD_SELF_TEST") != NULL)
		fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_FAKE);
	return g_steal_pointer(&device);
}

static FuUdevDevice *
fu_udev_backend_create_device(FuUdevBackend *self, const gchar *fn, GError **error)
{
	FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
	g_autoptr(FuDevice) device = NULL;
	g_autoptr(FuUdevDevice) device_donor = NULL;
	g_autoptr(GError) error_local = NULL;

	/* query the cache to avoid scanning parent devices multiple times */
	if (!self->done_coldplug) {
		FuUdevBackendColdplugCacheItem *item =
		    g_hash_table_lookup(self->coldplug_cache, fn);
		if (item != NULL) {
			if (item->udev_device == NULL) {
				if (error != NULL)
					*error = g_error_copy(item->error);
				return NULL;
			}
			return g_object_ref(item->udev_device);
		}
	}

	/* use a donor device to probe for the subsystem and devtype */
	device_donor = fu_udev_device_new(ctx, fn);
	if (!fu_device_probe(FU_DEVICE(device_donor), &error_local)) {
		fu_udev_backend_coldplug_cache_add_error(self, fn, error_local);
		g_propagate_prefixed_error(error,
					   g_steal_pointer(&error_local),
					   "failed to probe donor: ");
		return NULL;
	}
	device = fu_udev_backend_create_device_for_donor(FU_BACKEND(self),
							 FU_DEVICE(device_donor),
							 &error_local);
	if (device == NULL) {
		fu_udev_backend_coldplug_cache_add_error(self, fn, error_local);
		g_propagate_error(error, g_steal_pointer(&error_local));
		return NULL;
	}
	fu_udev_backend_coldplug_cache_add_device(self, fn, FU_UDEV_DEVICE(device));
	return FU_UDEV_DEVICE(g_steal_pointer(&device));
}

static void
fu_udev_backend_device_add_from_device(FuUdevBackend *self, FuUdevDevice *device)
{
	/* DP AUX devices are *weird* and can only read the DPCD when a DRM device is attached */
	if (g_strcmp0(fu_udev_device_get_subsystem(device), "drm_dp_aux_dev") == 0) {
		/* add and rescan, regardless of if we can open it */
		g_ptr_array_add(self->dpaux_devices, g_object_ref(device));
		fu_udev_backend_rescan_dpaux_devices(self);

		/* open -- this might seem redundant, but it means the device is added at daemon
		 * coldplug rather than a few seconds later */
		if (!self->done_coldplug) {
			g_autoptr(FuDeviceLocker) locker = NULL;
			g_autoptr(GError) error_local = NULL;

			locker = fu_device_locker_new(FU_DEVICE(device), &error_local);
			if (locker == NULL) {
				g_debug("failed to open device %s: %s",
					fu_device_get_backend_id(FU_DEVICE(device)),
					error_local->message);
				return;
			}
			fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device));
		}
		return;
	}

	/* success */
	fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device));
}

static void
fu_udev_backend_remove_device(FuUdevBackend *self, const gchar *sysfs_path)
{
	FuDevice *device_tmp;

	/* find the device we enumerated */
	device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfs_path);
	if (device_tmp != NULL) {
		g_debug("UDEV %s removed", sysfs_path);

		/* rescan all the DP AUX devices if it or any DRM device disappears */
		if (g_ptr_array_remove(self->dpaux_devices, device_tmp) ||
		    g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device_tmp)), "drm") ==
			0) {
			fu_udev_backend_rescan_dpaux_devices(self);
		}
		fu_backend_device_removed(FU_BACKEND(self), device_tmp);
	}
}

static gint
fu_udev_backend_device_number_sort_cb(gconstpointer a, gconstpointer b)
{
	FuUdevDevice *device1 = *((FuUdevDevice **)a);
	FuUdevDevice *device2 = *((FuUdevDevice **)b);
	if (fu_udev_device_get_number(device1) < fu_udev_device_get_number(device2))
		return -1;
	if (fu_udev_device_get_number(device1) > fu_udev_device_get_number(device2))
		return 1;
	return 0;
}

static void
fu_udev_backend_coldplug_subsystem(FuUdevBackend *self, const gchar *fn)
{
	const gchar *basename;
	g_autoptr(GDir) dir = NULL;
	g_autoptr(GError) error_dir = NULL;
	g_autoptr(GPtrArray) devices =
	    g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);

	dir = g_dir_open(fn, 0, &error_dir);
	if (dir == NULL) {
		if (!g_error_matches(error_dir, G_FILE_ERROR, G_FILE_ERROR_NOENT))
			g_debug("ignoring: %s", error_dir->message);
		return;
	}
	while ((basename = g_dir_read_name(dir)) != NULL) {
		g_autofree gchar *fn_full = g_build_filename(fn, basename, NULL);
		g_autofree gchar *fn_real = NULL;
		g_autoptr(GError) error_local = NULL;
		g_autoptr(FuUdevDevice) device = NULL;

		if (!g_file_test(fn_full, G_FILE_TEST_IS_DIR))
			continue;
		fn_real = fu_path_make_absolute(fn_full, &error_local);
		if (fn_real == NULL) {
			g_warning("failed to get symlink target for %s: %s",
				  fn_full,
				  error_local->message);
			continue;
		}
		if (g_hash_table_contains(self->map_paths, fn_real)) {
			g_debug("skipping duplicate %s", fn_real);
			continue;
		}
		device = fu_udev_backend_create_device(self, fn_real, &error_local);
		if (device == NULL) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED))
				continue;
			g_warning("failed to create device from %s: %s",
				  fn_real,
				  error_local->message);
			continue;
		}
		g_hash_table_add(self->map_paths, g_steal_pointer(&fn_real));
		g_ptr_array_add(devices, g_steal_pointer(&device));
	}

	/* sort by device order (so video0 comes before video9) and add as a device */
	g_ptr_array_sort(devices, fu_udev_backend_device_number_sort_cb);
	for (guint i = 0; i < devices->len; i++) {
		FuUdevDevice *device = g_ptr_array_index(devices, i);
		fu_udev_backend_device_add_from_device(self, device);
	}
}

#ifndef HAVE_UDEV_HOTPLUG
static gboolean
fu_udev_backend_devnode_wait_cb(FuDevice *device, gpointer user_data, GError **error)
{
	const gchar *dev_file = fu_udev_device_get_device_file(FU_UDEV_DEVICE(device));

	/* if the device has no associated /dev/ node (e.g. pure sysfs), we skip it */
	if (dev_file != NULL && !g_file_test(dev_file, G_FILE_TEST_EXISTS)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "device node %s does not exist",
			    dev_file);
		return FALSE;
	}

	/* success */
	return TRUE;
}
#endif

/* if enabled, systemd takes the kernel event, runs the udev rules (which might
 * rename devices) and then re-broadcasts on the udev netlink socket */
static gboolean
fu_udev_backend_netlink_parse_blob(FuUdevBackend *self, GBytes *blob, GError **error)
{
	FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
	FuUdevAction action = FU_UDEV_ACTION_UNKNOWN;
	const gchar *sysfsdir;
#ifdef HAVE_UDEV_HOTPLUG
	const guint8 *buf;
	gsize bufsz = 0;
	g_autoptr(FuStructUdevMonitorNetlinkHeader) st_hdr = NULL;
	g_autoptr(FuUdevDevice) device_donor = NULL;
	g_autoptr(GBytes) blob_payload = NULL;

	sysfsdir = fu_context_get_path(ctx, FU_PATH_KIND_SYSFSDIR, error);
	if (sysfsdir == NULL)
		return FALSE;

	/* parse the buffer */
	st_hdr = fu_struct_udev_monitor_netlink_header_parse_bytes(blob, 0x0, error);
	if (st_hdr == NULL)
		return FALSE;
	blob_payload =
	    fu_bytes_new_offset(blob,
				fu_struct_udev_monitor_netlink_header_get_properties_off(st_hdr),
				fu_struct_udev_monitor_netlink_header_get_properties_len(st_hdr),
				error);
	if (blob_payload == NULL)
		return FALSE;

	/* split into lines */
	buf = g_bytes_get_data(blob_payload, &bufsz);
	for (gsize i = 0; i < bufsz; i++) {
		g_autofree gchar *kvstr = NULL;
		g_auto(GStrv) kv = NULL;

		kvstr = fu_memstrsafe(buf, bufsz, i, bufsz - i, error);
		if (kvstr == NULL)
			return FALSE;
		kv = g_strsplit(kvstr, "=", 2);
		if (g_strcmp0(kv[0], "ACTION") == 0) {
			action = fu_udev_action_from_string(kv[1]);
			if (action == FU_UDEV_ACTION_UNKNOWN) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "unknown action %s",
					    kv[1]);
				return FALSE;
			}

			/* we do not care about these */
			if (action == FU_UDEV_ACTION_BIND || action == FU_UDEV_ACTION_UNBIND)
				return TRUE;
		} else if (g_strcmp0(kv[0], "DEVPATH") == 0) {
			g_autofree gchar *sysfspath = g_build_filename(sysfsdir, kv[1], NULL);

			/* something changed */
			if (action == FU_UDEV_ACTION_CHANGE) {
				FuDevice *device_tmp =
				    fu_backend_lookup_by_id(FU_BACKEND(self), sysfspath);
				if (device_tmp == NULL)
					return TRUE;
				if (g_strcmp0(
					fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device_tmp)),
					"drm") == 0)
					fu_udev_backend_rescan_dpaux_devices(self);
				g_set_object(&device_donor, FU_UDEV_DEVICE(device_tmp));
			}

			/* something got removed */
			if (action == FU_UDEV_ACTION_REMOVE) {
				fu_udev_backend_remove_device(self, sysfspath);
				return TRUE;
			}

			/* something got added */
			if (action == FU_UDEV_ACTION_ADD) {
				if (device_donor != NULL) {
					g_set_error_literal(error,
							    FWUPD_ERROR,
							    FWUPD_ERROR_INVALID_DATA,
							    "already have a donor device");
					return FALSE;
				}
				device_donor = fu_udev_device_new(ctx, sysfspath);
			}
		} else if (g_strcmp0(kv[0], "SUBSYSTEM") == 0 && device_donor != NULL) {
			fu_udev_device_set_subsystem(device_donor, kv[1]);
		} else if (g_strcmp0(kv[0], "DEVTYPE") == 0 && device_donor != NULL) {
			fu_udev_device_set_devtype(device_donor, kv[1]);
		} else if (device_donor != NULL) {
			fu_udev_device_add_property(device_donor, kv[0], kv[1]);
		}

		/* next! */
		i += strlen(kvstr);
	}

	/* notify the engine */
	if (action == FU_UDEV_ACTION_CHANGE) {
		if (device_donor == NULL) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_FOUND,
					    "no device to change");
			return FALSE;
		}
		fu_backend_device_changed(FU_BACKEND(self), FU_DEVICE(device_donor));
	}

	/* now create the actual device from the donor */
	if (action == FU_UDEV_ACTION_ADD) {
		g_autoptr(FuUdevDevice) device_actual = NULL;
		if (device_donor == NULL) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "no new device to add");
			return FALSE;
		}
		device_actual =
		    FU_UDEV_DEVICE(fu_udev_backend_create_device_for_donor(FU_BACKEND(self),
									   FU_DEVICE(device_donor),
									   error));
		if (device_actual == NULL)
			return FALSE;

		/* success */
		fu_udev_backend_device_add_from_device(self, device_actual);
	}
#else
	g_auto(GStrv) split = fu_strsplit_bytes(blob, "@", 2);

	sysfsdir = fu_context_get_path(ctx, FU_PATH_KIND_SYSFSDIR, error);
	if (sysfsdir == NULL)
		return FALSE;

	action = fu_udev_action_from_string(split[0]);
	if (action == FU_UDEV_ACTION_ADD) {
		g_autofree gchar *sysfspath = g_build_filename(sysfsdir, split[1], NULL);
		g_autoptr(FuUdevDevice) device =
		    fu_udev_backend_create_device(self, sysfspath, error);
		if (device == NULL)
			return FALSE;
		if (!fu_device_retry_full(FU_DEVICE(device),
					  fu_udev_backend_devnode_wait_cb,
					  200, /* retries */
					  10,  /* ms */
					  NULL,
					  error))
			return FALSE;
		fu_udev_backend_device_add_from_device(self, device);
	} else if (action == FU_UDEV_ACTION_REMOVE) {
		g_autofree gchar *sysfspath = g_build_filename(sysfsdir, split[1], NULL);
		fu_udev_backend_remove_device(self, sysfspath);
	} else if (action == FU_UDEV_ACTION_CHANGE) {
		g_autofree gchar *sysfspath = g_build_filename(sysfsdir, split[1], NULL);
		FuDevice *device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfspath);
		if (device_tmp == NULL) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_FOUND,
					    "no device to change");
			return FALSE;
		}
		fu_backend_device_changed(FU_BACKEND(self), device_tmp);
	}
#endif

	/* success */
	return TRUE;
}

static gboolean
fu_udev_backend_netlink_cb(gint fd, GIOCondition condition, gpointer user_data)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(user_data);
	gssize len;
	guint8 buf[10240] = {0x0};
	g_autoptr(GBytes) blob = NULL;
	g_autoptr(GError) error_local = NULL;

	len = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
	if (len < 0)
		return TRUE;
	blob = g_bytes_new(buf, len);
	if (!fu_udev_backend_netlink_parse_blob(self, blob, &error_local)) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) ||
		    g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
			g_debug("ignoring netlink message: %s", error_local->message);
			return TRUE;
		}
		g_warning("ignoring netlink message: %s", error_local->message);
		return TRUE;
	}
	return TRUE;
}

static gboolean
fu_udev_backend_netlink_setup(FuUdevBackend *self, GError **error)
{
	struct sockaddr_nl nls = {
	    .nl_family = AF_NETLINK,
	    .nl_pid = getpid(),
#ifdef HAVE_UDEV_HOTPLUG
	    .nl_groups = FU_UDEV_MONITOR_NETLINK_GROUP_UDEV,
#else
	    .nl_groups = FU_UDEV_MONITOR_NETLINK_GROUP_KERNEL,
#endif
	};
	g_autoptr(GSource) source = NULL;

	/* minijail -p prevents access */
	if (nls.nl_pid <= 2) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "failed to get PID, perhaps sandboxed?");
		return FALSE;
	}

	self->netlink_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
	if (self->netlink_fd < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "failed to connect to netlink: %s",
			    fwupd_strerror(errno));
		return FALSE;
	}
	if (bind(self->netlink_fd, (void *)&nls, sizeof(nls))) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "bind to udev socket failed: %s",
			    fwupd_strerror(errno));
		return FALSE;
	}
	source = g_unix_fd_source_new(self->netlink_fd, G_IO_IN);
	g_source_set_static_name(source, "netlink");
	g_source_set_callback(source, (GSourceFunc)fu_udev_backend_netlink_cb, self, NULL);
	g_source_set_can_recurse(source, TRUE);
	g_source_attach(source, NULL);

	/* success */
	return TRUE;
}

static gboolean
fu_udev_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error)
{
	FuContext *ctx = fu_backend_get_context(backend);
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);
	const gchar *sysfsdir;
	g_autoptr(GPtrArray) udev_subsystems = fu_context_get_udev_subsystems(ctx);

	sysfsdir = fu_context_get_path(ctx, FU_PATH_KIND_SYSFSDIR, error);
	if (sysfsdir == NULL)
		return FALSE;

	/* get all devices of class */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, udev_subsystems->len);
	for (guint i = 0; i < udev_subsystems->len; i++) {
		const gchar *subsystem = g_ptr_array_index(udev_subsystems, i);
		g_autofree gchar *class_fn = NULL;
		g_autofree gchar *bus_fn = NULL;

		/* we only care about subsystems, not subsystem:devtype matches */
		if (g_strstr_len(subsystem, -1, ":") != NULL) {
			fu_progress_step_done(progress);
			continue;
		}

		class_fn = g_build_filename(sysfsdir, "class", subsystem, NULL);
		if (g_file_test(class_fn, G_FILE_TEST_EXISTS)) {
			fu_udev_backend_coldplug_subsystem(self, class_fn);
			fu_progress_step_done(progress);
			continue;
		}
		bus_fn = g_build_filename(sysfsdir, "bus", subsystem, "devices", NULL);
		if (g_file_test(bus_fn, G_FILE_TEST_EXISTS)) {
			fu_udev_backend_coldplug_subsystem(self, bus_fn);
			fu_progress_step_done(progress);
			continue;
		}
		fu_progress_step_done(progress);
	}

	/* success */
	g_hash_table_remove_all(self->coldplug_cache);
	self->done_coldplug = TRUE;
	return TRUE;
}

static gboolean
fu_udev_backend_setup(FuBackend *backend,
		      FuBackendSetupFlags flags,
		      FuProgress *progress,
		      GError **error)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);

	/* set up hotplug events */
	if (flags & FU_BACKEND_SETUP_FLAG_USE_HOTPLUG) {
		if (!fu_udev_backend_netlink_setup(self, error)) {
			g_prefix_error_literal(error, "failed to set up netlink: ");
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static FuDevice *
fu_udev_backend_get_device_parent(FuBackend *backend,
				  FuDevice *device,
				  const gchar *subsystem,
				  GError **error)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);
	g_autofree gchar *sysfs_path = NULL;
	g_autoptr(FuUdevDevice) device_new = NULL;

	/* emulated */
	if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) {
		GType gtype = FU_TYPE_UDEV_DEVICE;
		g_autofree gchar *physical_id = NULL;
		if (subsystem != NULL) {
			g_auto(GStrv) split = g_strsplit(subsystem, ":", 2);
			gtype = fu_udev_backend_get_gtype_for_subsystem_devtype(split[0], split[1]);
			physical_id =
			    g_strconcat(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)),
					"-parent:",
					subsystem,
					NULL);
		} else {
			physical_id =
			    g_strconcat(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)),
					"-parent",
					NULL);
		}
		device_new = g_object_new(gtype,
					  "context",
					  fu_backend_get_context(backend),
					  "physical-id",
					  physical_id,
					  NULL);
		fu_device_add_flag(FU_DEVICE(device_new), FWUPD_DEVICE_FLAG_EMULATED);
		return FU_DEVICE(g_steal_pointer(&device_new));
	}

	sysfs_path = g_strdup(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)));
	if (sysfs_path == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "sysfs path undefined");
		return NULL;
	}

	if (!g_file_test(sysfs_path, G_FILE_TEST_EXISTS)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "sysfs path '%s' doesn't exist, perhaps a transient device.",
			    sysfs_path);
		return NULL;
	}

	/* lets just walk up the directories */
	while (1) {
		g_autofree gchar *dirname = NULL;
		g_autoptr(GError) error_local = NULL;

		/* done? */
		dirname = g_path_get_dirname(sysfs_path);
		if (g_strcmp0(dirname, ".") == 0 || g_strcmp0(dirname, "/") == 0)
			break;

		/* check has matching subsystem and devtype */
		device_new = fu_udev_backend_create_device(self, dirname, &error_local);
		if (device_new != NULL) {
			if (fu_udev_device_match_subsystem(device_new, subsystem)) {
				if (subsystem != NULL) {
					g_auto(GStrv) split = g_strsplit(subsystem, ":", 2);
					fu_udev_device_set_subsystem(device_new, split[0]);
				}
				return FU_DEVICE(g_steal_pointer(&device_new));
			}
		} else {
			if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND))
				g_warning("failed to create device: %s", error_local->message);
		}

		/* just swap, and go deeper */
		g_free(sysfs_path);
		sysfs_path = g_steal_pointer(&dirname);
	}

	/* failed */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_FOUND,
		    "no parent with subsystem %s",
		    subsystem);
	return NULL;
}

static FuDevice *
fu_udev_backend_create_device_impl(FuBackend *backend, const gchar *backend_id, GError **error)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);
	return FU_DEVICE(fu_udev_backend_create_device(self, backend_id, error));
}

static void
fu_udev_backend_finalize(GObject *object)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(object);
	if (self->dpaux_devices_rescan_id != 0)
		g_source_remove(self->dpaux_devices_rescan_id);
	if (self->netlink_fd > 0)
		g_close(self->netlink_fd, NULL);
	g_hash_table_unref(self->map_paths);
	g_hash_table_unref(self->coldplug_cache);
	g_ptr_array_unref(self->dpaux_devices);
	G_OBJECT_CLASS(fu_udev_backend_parent_class)->finalize(object);
}

static void
fu_udev_backend_init(FuUdevBackend *self)
{
	self->map_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
	self->coldplug_cache =
	    g_hash_table_new_full(g_str_hash,
				  g_str_equal,
				  g_free,
				  (GDestroyNotify)fu_udev_backend_coldplug_cache_item_free);
	self->dpaux_devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
}

static void
fu_udev_backend_class_init(FuUdevBackendClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuBackendClass *backend_class = FU_BACKEND_CLASS(klass);
	object_class->finalize = fu_udev_backend_finalize;
	backend_class->coldplug = fu_udev_backend_coldplug;
	backend_class->setup = fu_udev_backend_setup;
	backend_class->to_string = fu_udev_backend_to_string;
	backend_class->get_device_parent = fu_udev_backend_get_device_parent;
	backend_class->create_device = fu_udev_backend_create_device_impl;
	backend_class->create_device_for_donor = fu_udev_backend_create_device_for_donor;
}

FuBackend *
fu_udev_backend_new(FuContext *ctx)
{
	return FU_BACKEND(g_object_new(FU_TYPE_UDEV_BACKEND,
				       "name",
				       "udev",
				       "context",
				       ctx,
				       "device-gtype",
				       FU_TYPE_UDEV_DEVICE,
				       NULL));
}
