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

#define G_LOG_DOMAIN "FuEngine"

#include "config.h"

#include "fwupd-remote-private.h"

#include "fu-device-private.h"
#include "fu-engine-requirements.h"

static gboolean
fu_engine_requirements_require_vercmp_part(const gchar *compare,
					   const gchar *version_req,
					   const gchar *version,
					   FwupdVersionFormat fmt,
					   GError **error)
{
	gboolean ret = FALSE;
	gint rc = 0;

	if (g_strcmp0(compare, "eq") == 0) {
		rc = fu_version_compare(version, version_req, fmt);
		ret = rc == 0;
	} else if (g_strcmp0(compare, "ne") == 0) {
		rc = fu_version_compare(version, version_req, fmt);
		ret = rc != 0;
	} else if (g_strcmp0(compare, "lt") == 0) {
		rc = fu_version_compare(version, version_req, fmt);
		ret = rc < 0;
	} else if (g_strcmp0(compare, "gt") == 0) {
		rc = fu_version_compare(version, version_req, fmt);
		ret = rc > 0;
	} else if (g_strcmp0(compare, "le") == 0) {
		rc = fu_version_compare(version, version_req, fmt);
		ret = rc <= 0;
	} else if (g_strcmp0(compare, "ge") == 0) {
		rc = fu_version_compare(version, version_req, fmt);
		ret = rc >= 0;
	} else if (g_strcmp0(compare, "glob") == 0) {
		ret = g_pattern_match_simple(version_req, version);
	} else if (g_strcmp0(compare, "regex") == 0) {
		ret = g_regex_match_simple(version_req, version, 0, 0);
	} else {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "failed to compare [%s] and [%s]",
			    version_req,
			    version);
		return FALSE;
	}

	/* set error */
	if (!ret) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "failed predicate [%s %s %s]",
			    version_req,
			    compare,
			    version);
		return FALSE;
	}

	/* success */
	return TRUE;
}

typedef struct {
	FuRelease *release;
	FwupdInstallFlags install_flags;
	gchar *fwupd_version;
	gboolean has_hardware_req;
	gboolean has_not_hardware_req;
	gboolean has_id_requirement_glob;
	gboolean has_client_id_requirement_glob;
} FuEngineRequirementsHelper;

static void
fu_engine_requirements_helper_free(FuEngineRequirementsHelper *helper)
{
	g_object_unref(helper->release);
	g_free(helper->fwupd_version);
	g_free(helper);
}

static gboolean
fu_engine_requirements_check_fwupd_version(FuEngineRequirementsHelper *helper,
					   const gchar *fwupd_version_req,
					   GError **error)
{
	if (fu_version_compare(helper->fwupd_version,
			       fwupd_version_req,
			       FWUPD_VERSION_FORMAT_UNKNOWN) < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "needs %s >= %s",
			    FWUPD_DBUS_SERVICE,
			    fwupd_version_req);
		return FALSE;
	}
	return TRUE;
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuEngineRequirementsHelper, fu_engine_requirements_helper_free)

static gboolean
fu_engine_requirements_require_vercmp(XbNode *req,
				      const gchar *version,
				      FwupdVersionFormat fmt,
				      FuEngineRequirementsHelper *helper,
				      GError **error)
{
	const gchar *compare = xb_node_get_attr(req, "compare");
	const gchar *version_req = xb_node_get_attr(req, "version");
	g_auto(GStrv) split = NULL;

	if (version_req == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no version attribute supplied");
		return FALSE;
	}

	/* parse globbed version, e.g. `1.9.*=1.9.7|1.8.*=1.8.23|2.0.15`, or just `2.0.5` */
	split = g_strsplit(version_req, "|", 0);
	for (guint i = 0; split[i] != NULL; i++) {
		g_auto(GStrv) kv = g_strsplit(split[i], "=", 2);
		if (g_strv_length(kv) > 1) {
			helper->has_id_requirement_glob = TRUE;
			if (!g_pattern_match_simple(kv[0], version)) {
				g_debug("skipping vercmp %s as version %s", kv[0], version);
				continue;
			}
			g_debug("checking vercmp %s as version %s", kv[1], version);
			return fu_engine_requirements_require_vercmp_part(compare,
									  kv[1],
									  version,
									  fmt,
									  error);
		}
		return fu_engine_requirements_require_vercmp_part(compare,
								  kv[0],
								  version,
								  fmt,
								  error);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_engine_requirements_check_not_child(FuEngine *self,
				       XbNode *req,
				       FuDevice *device,
				       FuEngineRequirementsHelper *helper,
				       GError **error)
{
	GPtrArray *children = fu_device_get_children(device);

	/* only <firmware> supported */
	if (g_strcmp0(xb_node_get_element(req), "firmware") != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "cannot handle not-child %s requirement",
			    xb_node_get_element(req));
		return FALSE;
	}

	/* check each child */
	for (guint i = 0; i < children->len; i++) {
		FuDevice *child = g_ptr_array_index(children, i);
		const gchar *version = fu_device_get_version(child);
		if (version == NULL) {
			g_autofree gchar *id_display = fu_device_get_id_display(device);
			g_autofree gchar *id_display_child = fu_device_get_id_display(child);
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no version provided by %s, child of %s",
				    id_display_child,
				    id_display);
			return FALSE;
		}
		if (fu_engine_requirements_require_vercmp(req,
							  version,
							  fu_device_get_version_format(child),
							  helper,
							  NULL)) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "Not compatible with child device version %s",
				    version);
			return FALSE;
		}
	}
	return TRUE;
}

static gboolean
fu_engine_requirements_check_vendor_id(FuEngine *self,
				       XbNode *req,
				       FuDevice *device,
				       GError **error)
{
	GPtrArray *vendor_ids;
	const gchar *vendor_ids_metadata;
	g_autofree gchar *vendor_ids_device = NULL;

	/* devices without vendor IDs should not exist! */
	vendor_ids = fu_device_get_vendor_ids(device);
	if (vendor_ids->len == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "device [%s] has no vendor ID",
			    fu_device_get_id(device));
		return FALSE;
	}

	/* metadata with empty vendor IDs should not exist! */
	vendor_ids_metadata = xb_node_get_attr(req, "version");
	if (vendor_ids_metadata == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "metadata has no vendor ID");
		return FALSE;
	}

	/* it is always safe to use a regex, even for simple strings */
	vendor_ids_device = fu_strjoin("|", vendor_ids);
	if (!g_regex_match_simple(vendor_ids_metadata, vendor_ids_device, 0, 0)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "Not compatible with vendor %s: got %s",
			    vendor_ids_device,
			    vendor_ids_metadata);
		return FALSE;
	}

	/* success */
	return TRUE;
}

/* nocheck:name */
static gboolean
_fu_device_has_guids_any(FuDevice *self, gchar **guids)
{
	g_return_val_if_fail(FU_IS_DEVICE(self), FALSE);
	g_return_val_if_fail(guids != NULL, FALSE);
	for (guint i = 0; guids[i] != NULL; i++) {
		if (fu_device_has_guid(self, guids[i]))
			return TRUE;
	}
	return FALSE;
}

static gboolean
fu_engine_requirements_check_firmware(FuEngine *self,
				      XbNode *req,
				      FuEngineRequirementsHelper *helper,
				      GError **error)
{
	FuDevice *device = fu_release_get_device(helper->release);
	const gchar *version;
	const gchar *depth_str;
	gint64 depth = G_MAXINT64;
	g_autoptr(FuDevice) device_actual = g_object_ref(device);
	g_autoptr(GError) error_local = NULL;
	g_auto(GStrv) guids = NULL;

	/* self tests */
	if (device == NULL)
		return TRUE;

	/* look at the parent device */
	depth_str = xb_node_get_attr(req, "depth");
	if (depth_str != NULL) {
		if (!fu_strtoll(depth_str, &depth, -1, 10, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		for (gint64 i = 0; i < depth; i++) {
			FuDevice *device_tmp = fu_device_get_parent_internal(device_actual);
			if (device_tmp == NULL) {
				g_autofree gchar *id_display =
				    fu_device_get_id_display(device_actual);
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "No parent device for %s "
					    "(%" G_GINT64_FORMAT "/%" G_GINT64_FORMAT ")",
					    id_display,
					    i,
					    depth);
				return FALSE;
			}
			g_set_object(&device_actual, device_tmp);
		}
	}

	/* check fwupd version requirement */
	if (depth < 0) {
		if (!fu_engine_requirements_check_fwupd_version(helper, "1.9.7", error)) {
			g_prefix_error_literal(error, "requirement child firmware: ");
			return FALSE;
		}
	}

	/* old firmware version */
	if (xb_node_get_text(req) == NULL) {
		version = fu_device_get_version(device_actual);
		if (!fu_engine_requirements_require_vercmp(
			req,
			version,
			fu_device_get_version_format(device_actual),
			helper,
			&error_local)) {
			if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) {
				g_set_error(
				    error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "Not compatible with firmware version %s, requires >= %s",
				    version,
				    xb_node_get_attr(req, "version"));
				return FALSE;
			}
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "Not compatible with firmware version: %s",
				    error_local->message);
			return FALSE;
		}
		return TRUE;
	}

	/* bootloader version */
	if (g_strcmp0(xb_node_get_text(req), "bootloader") == 0) {
		version = fu_device_get_version_bootloader(device_actual);
		if (!fu_engine_requirements_require_vercmp(
			req,
			version,
			fu_device_get_version_format(device_actual),
			helper,
			&error_local)) {
			if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) {
				g_set_error(
				    error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "Not compatible with bootloader version %s, requires >= %s",
				    version,
				    xb_node_get_attr(req, "version"));
				return FALSE;
			}
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "Bootloader is not compatible");
			return FALSE;
		}
		return TRUE;
	}

	/* vendor ID */
	if (g_strcmp0(xb_node_get_text(req), "vendor-id") == 0) {
		if (helper->install_flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID)
			return TRUE;
		return fu_engine_requirements_check_vendor_id(self, req, device_actual, error);
	}

	/* child version */
	if (g_strcmp0(xb_node_get_text(req), "not-child") == 0)
		return fu_engine_requirements_check_not_child(self,
							      req,
							      device_actual,
							      helper,
							      error);

	/* another device, specified by GUID|GUID|GUID */
	guids = g_strsplit(xb_node_get_text(req), "|", -1);
	for (guint i = 0; guids[i] != NULL; i++) {
		if (!fwupd_guid_is_valid(guids[i])) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "%s is not a valid GUID",
				    guids[i]);
			return FALSE;
		}
	}

	/* find if any of the other devices exists */
	if (depth == G_MAXINT64) {
		g_autoptr(FuDevice) device_tmp = NULL;
		for (guint i = 0; guids[i] != NULL; i++) {
			g_autoptr(GPtrArray) devices =
			    fu_engine_get_devices_by_guid(self, guids[i], NULL);
			if (devices != NULL && devices->len > 0) {
				device_tmp = g_object_ref(g_ptr_array_index(devices, 0));
				break;
			}
		}
		if (device_tmp == NULL) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "No other device %s found",
				    xb_node_get_text(req));
			return FALSE;
		}
		g_set_object(&device_actual, device_tmp);

	} else if (depth == -1) {
		GPtrArray *children;
		FuDevice *child = NULL;

		/* look for a child */
		children = fu_device_get_children(device);
		for (guint i = 0; i < children->len; i++) {
			child = g_ptr_array_index(children, i);
			if (_fu_device_has_guids_any(child, guids))
				break;
			child = NULL;
		}
		if (child == NULL) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "No child found with GUID of %s",
				    xb_node_get_text(req));
			return FALSE;
		}
		g_set_object(&device_actual, child);

		/* look for a sibling */
	} else if (depth == 0) {
		FuDevice *child = NULL;
		FuDevice *parent = fu_device_get_parent_internal(device_actual);
		GPtrArray *children;

		/* no parent, so look for GUIDs on this device */
		if (parent == NULL) {
			if (!_fu_device_has_guids_any(device_actual, guids)) {
				g_autofree gchar *id_display =
				    fu_device_get_id_display(device_actual);
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "No GUID of %s on device %s",
					    xb_node_get_text(req),
					    id_display);
				return FALSE;
			}
			return TRUE;
		}
		children = fu_device_get_children(parent);
		for (guint i = 0; i < children->len; i++) {
			child = g_ptr_array_index(children, i);
			if (_fu_device_has_guids_any(child, guids))
				break;
			child = NULL;
		}
		if (child == NULL) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "No sibling found with GUID of %s",
				    xb_node_get_text(req));
			return FALSE;
		}
		g_set_object(&device_actual, child);

		/* verify the parent device has the GUID */
	} else {
		if (!_fu_device_has_guids_any(device_actual, guids)) {
			g_autofree gchar *id_display = fu_device_get_id_display(device_actual);
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "No GUID of %s on parent device %s",
				    xb_node_get_text(req),
				    id_display);
			return FALSE;
		}
	}

	/* get the version of the other device */
	version = fu_device_get_version(device_actual);
	if (version != NULL && xb_node_get_attr(req, "compare") != NULL &&
	    !fu_engine_requirements_require_vercmp(req,
						   version,
						   fu_device_get_version_format(device_actual),
						   helper,
						   &error_local)) {
		if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "Not compatible with %s version %s, requires >= %s",
				    fu_device_get_name(device_actual),
				    version,
				    xb_node_get_attr(req, "version"));
			return FALSE;
		}
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "Not compatible with %s: %s",
			    fu_device_get_name(device_actual),
			    error_local->message);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_engine_requirements_check_id(FuEngine *self,
				XbNode *req,
				FuEngineRequirementsHelper *helper,
				GError **error)
{
	FuContext *ctx = fu_engine_get_context(self);
	g_autoptr(GError) error_local = NULL;
	const gchar *version;

	/* sanity check */
	if (xb_node_get_text(req) == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no requirement value supplied");
		return FALSE;
	}
	version = fu_context_get_runtime_version(ctx, xb_node_get_text(req));
	if (version == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "no version available for %s",
			    xb_node_get_text(req));
		return FALSE;
	}
	if (!fu_engine_requirements_require_vercmp(req,
						   version,
						   FWUPD_VERSION_FORMAT_UNKNOWN,
						   helper,
						   &error_local)) {
		if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "Not compatible with %s version %s, requires >= %s",
				    xb_node_get_text(req),
				    version,
				    xb_node_get_attr(req, "version"));
			return FALSE;
		}
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "Not compatible with %s version: %s",
			    xb_node_get_text(req),
			    error_local->message);
		return FALSE;
	}

	g_debug("requirement %s %s %s -> %s passed",
		xb_node_get_attr(req, "version"),
		xb_node_get_attr(req, "compare"),
		version,
		xb_node_get_text(req));
	return TRUE;
}

static gboolean
fu_engine_requirements_check_hardware(FuEngine *self,
				      XbNode *req,
				      FuEngineRequirementsHelper *helper,
				      GError **error)
{
	FuContext *ctx = fu_engine_get_context(self);
	FuDevice *device = fu_release_get_device(helper->release);
	g_auto(GStrv) hwid_split = NULL;

	/* skip for tests */
	if (device == NULL || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED))
		return TRUE;

	/* sanity check */
	if (xb_node_get_text(req) == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no requirement value supplied");
		return FALSE;
	}

	/* split and treat as OR */
	hwid_split = g_strsplit(xb_node_get_text(req), "|", -1);
	for (guint i = 0; hwid_split[i] != NULL; i++) {
		if (fu_context_has_hwid_guid(ctx, hwid_split[i])) {
			g_debug("HWID provided %s", hwid_split[i]);
			return TRUE;
		}
	}

	/* nothing matched */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_INVALID_FILE,
		    "no HWIDs matched %s",
		    xb_node_get_text(req));
	return FALSE;
}

static gboolean
fu_engine_requirements_check_not_hardware(FuEngine *self,
					  XbNode *req,
					  FuEngineRequirementsHelper *helper,
					  GError **error)
{
	FuContext *ctx = fu_engine_get_context(self);
	g_auto(GStrv) hwid_split = NULL;

	/* check fwupd version requirement */
	if (!fu_engine_requirements_check_fwupd_version(helper, "1.9.10", error)) {
		g_prefix_error_literal(error, "requirement not_hardware: ");
		return FALSE;
	}

	/* sanity check */
	if (xb_node_get_text(req) == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no requirement value supplied");
		return FALSE;
	}

	/* split and treat as OR */
	hwid_split = g_strsplit(xb_node_get_text(req), "|", -1);
	for (guint i = 0; hwid_split[i] != NULL; i++) {
		if (fu_context_has_hwid_guid(ctx, hwid_split[i])) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "%s HWIDs matched",
				    hwid_split[i]);
			return FALSE;
		}
	}

	/* nothing matched */
	return TRUE;
}

static gboolean
fu_engine_requirements_check_phased_update(FuEngine *self,
					   XbNode *req,
					   FuEngineRequirementsHelper *helper,
					   GError **error)
{
	FwupdRemote *remote;
	const gchar *archive_filename;
	const gchar *host_machine_id;
	gint32 seed;
	guint64 phased_update = 0;

	/* i'm feeling lucky */
	if (helper->install_flags & FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) {
		g_info("ignoring phased_update requirement");
		return TRUE;
	}

	/* check fwupd version requirement */
	if (!fu_engine_requirements_check_fwupd_version(helper, "2.0.17", error)) {
		g_prefix_error_literal(error, "requirement phased_update: ");
		return FALSE;
	}

	/* sanity check */
	remote = fu_release_get_remote(helper->release);
	if (remote == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no assigned remote for firmware release");
		return FALSE;
	}
	if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_NO_PHASED_UPDATES)) {
		g_info("ignoring phased update requirement due to remote policy");
		return TRUE;
	}
	if (xb_node_get_text(req) == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no phased_update value supplied");
		return FALSE;
	}
	if (!fu_strtoull(xb_node_get_text(req),
			 &phased_update,
			 2,
			 1024,
			 FU_INTEGER_BASE_AUTO,
			 error)) {
		g_prefix_error_literal(error, "expected integer for phased_update: ");
		return FALSE;
	}

	/* use multiple seeds, but return uniformly distributed random numbers */
	archive_filename = fu_release_get_filename(helper->release);
	host_machine_id = fu_engine_get_host_machine_id(self);
	if (host_machine_id == NULL || archive_filename == NULL) {
		seed = (gint32)fwupd_remote_get_mtime(remote);
	} else {
		guint32 seed_buf[4] = {0};
		guint seed_bufsz = 0;
		g_autoptr(GRand) rand = NULL;

		seed_buf[seed_bufsz++] = g_str_hash(host_machine_id);
		seed_buf[seed_bufsz++] = g_str_hash(archive_filename);
		seed_buf[seed_bufsz++] = (guint32)fwupd_remote_get_mtime(remote);
		rand = g_rand_new_with_seed_array(seed_buf, seed_bufsz);
		seed = g_rand_int_range(rand, 0, G_MAXINT);
	}

	/* does seed divide perfectly into the phased update factor */
	if (seed % phased_update != 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "remote mtime meant that deployment was delayed");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_engine_requirements_check_client(FuEngine *self,
				    XbNode *req,
				    FuEngineRequirementsHelper *helper,
				    GError **error)
{
	FuEngineRequest *request = fu_release_get_request(helper->release);
	FwupdFeatureFlags feature_flags;
	g_auto(GStrv) feature_split = NULL;

	/* sanity check */
	if (xb_node_get_text(req) == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no requirement value supplied");
		return FALSE;
	}

	/* split and treat as AND */
	feature_split = g_strsplit(xb_node_get_text(req), "|", -1);
	feature_flags = fu_engine_request_get_feature_flags(request);
	for (guint i = 0; feature_split[i] != NULL; i++) {
		FuEngineCapabilityFlags capability_flag;
		FwupdFeatureFlags feature_flag;

		/* client feature */
		feature_flag = fwupd_feature_flag_from_string(feature_split[i]);
		if (feature_flag != FWUPD_FEATURE_FLAG_UNKNOWN) {
			if ((feature_flags & feature_flag) == 0) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "client feature requirement %s not supported",
					    feature_split[i]);
				return FALSE;
			}
			continue;
		}

		/* assumed by the daemon version, see https://github.com/fwupd/fwupd/pull/8949 */
		capability_flag = fu_engine_capability_flags_from_string(feature_split[i]);
		if (capability_flag != FU_ENGINE_CAPABILITY_FLAG_UNKNOWN) {
			if (capability_flag == FU_ENGINE_CAPABILITY_FLAG_ID_REQUIREMENT_GLOB) {
				helper->has_client_id_requirement_glob = TRUE;
				continue;
			}
		}

		/* not recognized */
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "client requirement %s unknown",
			    feature_split[i]);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_engine_requirements_check_hard(FuEngine *self,
				  XbNode *req,
				  FuEngineRequirementsHelper *helper,
				  GError **error)
{
	FuContext *ctx = fu_engine_get_context(self);

	/* ensure component requirement */
	if (g_strcmp0(xb_node_get_element(req), "id") == 0)
		return fu_engine_requirements_check_id(self, req, helper, error);

	/* ensure firmware requirement */
	if (g_strcmp0(xb_node_get_element(req), "firmware") == 0)
		return fu_engine_requirements_check_firmware(self, req, helper, error);

	/* ensure hardware requirement */
	if (g_strcmp0(xb_node_get_element(req), "hardware") == 0) {
		helper->has_hardware_req = TRUE;
		if (!fu_context_has_flag(ctx, FU_CONTEXT_FLAG_LOADED_HWINFO))
			return TRUE;
		return fu_engine_requirements_check_hardware(self, req, helper, error);
	}
	if (g_strcmp0(xb_node_get_element(req), "not_hardware") == 0) {
		helper->has_not_hardware_req = TRUE;
		if (!fu_context_has_flag(ctx, FU_CONTEXT_FLAG_LOADED_HWINFO))
			return TRUE;
		return fu_engine_requirements_check_not_hardware(self, req, helper, error);
	}

	/* support slowing down deployments */
	if (g_strcmp0(xb_node_get_element(req), "phased_update") == 0)
		return fu_engine_requirements_check_phased_update(self, req, helper, error);

	/* ensure client requirement */
	if (g_strcmp0(xb_node_get_element(req), "client") == 0)
		return fu_engine_requirements_check_client(self, req, helper, error);

	/* not supported */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_SUPPORTED,
		    "cannot handle requirement type %s",
		    xb_node_get_element(req));
	return FALSE;
}

static gboolean
fu_engine_requirements_check_soft(FuEngine *self,
				  XbNode *req,
				  FuEngineRequirementsHelper *helper,
				  GError **error)
{
	g_autoptr(GError) error_local = NULL;
	if (!fu_engine_requirements_check_hard(self, req, helper, &error_local)) {
		if (helper->install_flags & FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) {
			g_info("ignoring soft-requirement: %s", error_local->message);
			return TRUE;
		}
		g_propagate_error(error, g_steal_pointer(&error_local));
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_engine_requirements_is_specific_req(XbNode *req)
{
	if (g_strcmp0(xb_node_get_element(req), "firmware") == 0 &&
	    xb_node_get_attr(req, "depth") != NULL)
		return TRUE;
	if (g_strcmp0(xb_node_get_element(req), "hardware") == 0)
		return TRUE;
	return FALSE;
}

static gchar *
fu_engine_requirements_get_newest_fwupd_version(FuEngine *self, FuRelease *release, GError **error)
{
	GPtrArray *reqs = fu_release_get_hard_reqs(release);
	g_autoptr(GString) newest_version = g_string_new("1.0.0");

	/* trivial case */
	if (reqs == NULL)
		return g_string_free(g_steal_pointer(&newest_version), FALSE);

	/* find the newest fwupd requirement */
	for (guint i = 0; i < reqs->len; i++) {
		XbNode *req = g_ptr_array_index(reqs, i);
		if (g_strcmp0(xb_node_get_text(req), FWUPD_DBUS_SERVICE) == 0 &&
		    g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) {
			const gchar *version = xb_node_get_attr(req, "version");
			g_auto(GStrv) split = NULL;

			if (version == NULL) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_FOUND,
					    "no version provided for requirement %s",
					    xb_node_get_text(req));
				return NULL;
			}

			/* only care about the fallback version if using globs */
			split = g_strsplit(version, "|", -1);
			for (guint j = 0; split[j] != NULL; j++) {
				if (g_strstr_len(split[j], -1, "=") != NULL)
					continue;
				/* is this newer than what we have */
				if (fu_version_compare(split[j],
						       newest_version->str,
						       FWUPD_VERSION_FORMAT_UNKNOWN) > 0) {
					g_string_assign(newest_version, split[j]);
				}
			}
		}
	}

	/* success */
	return g_string_free(g_steal_pointer(&newest_version), FALSE);
}

gboolean
fu_engine_requirements_check(FuEngine *self,
			     FuRelease *release,
			     FwupdInstallFlags flags,
			     GError **error)
{
	FuDevice *device = fu_release_get_device(release);
	GPtrArray *reqs;
	gboolean has_specific_requirement = FALSE;
	g_autoptr(FuEngineRequirementsHelper) helper = g_new0(FuEngineRequirementsHelper, 1);

	g_return_val_if_fail(FU_IS_ENGINE(self), FALSE);
	g_return_val_if_fail(FU_IS_RELEASE(release), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* create a small helper with common data */
	helper->release = g_object_ref(release);
	helper->install_flags = flags;

	/* get the newest fwupd version requirement */
	helper->fwupd_version =
	    fu_engine_requirements_get_newest_fwupd_version(self, release, error);
	if (helper->fwupd_version == NULL)
		return FALSE;

	/* sanity check */
	if (device != NULL && !fu_device_is_updatable(device)) {
		g_autofree gchar *id_display = fu_device_get_id_display(device);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "%s is not updatable",
			    id_display);
		return FALSE;
	}

	/* verify protocol */
	if (device != NULL && fu_release_get_protocol(release) != NULL &&
	    !fu_device_has_protocol(device, fu_release_get_protocol(release))) {
		g_autofree gchar *protocols = fu_strjoin(",", fu_device_get_protocols(device));
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "release needs protocol %s but device has %s",
			    fu_release_get_protocol(release),
			    protocols);
		return FALSE;
	}

	/* hard requirements */
	reqs = fu_release_get_hard_reqs(release);
	if (reqs != NULL) {
		for (guint i = 0; i < reqs->len; i++) {
			XbNode *req = g_ptr_array_index(reqs, i);
			if (!fu_engine_requirements_check_hard(self, req, helper, error))
				return FALSE;
			if (fu_engine_requirements_is_specific_req(req))
				has_specific_requirement = TRUE;
		}
	}

	/* it does not make sense to allowlist and denylist at the same time */
	if (helper->has_hardware_req && helper->has_not_hardware_req) {
		g_set_error_literal(
		    error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_SUPPORTED,
		    "using hardware and not_hardware at the same time is not supported");
		return FALSE;
	}

	/* if we're using ID requirements with globs we have to have a client requirement */
	if (helper->has_id_requirement_glob && !helper->has_client_id_requirement_glob) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "using <id> version requirements with globs also needs "
				    "<client>id-requirement-glob</client>");
		return FALSE;
	}

	/* if a device uses a generic ID (i.e. not matching the OEM) then check to make sure the
	 * firmware is specific enough, e.g. by using a CHID or depth requirement */
	if (device != NULL && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) &&
	    fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES) &&
	    !has_specific_requirement) {
#ifdef SUPPORTED_BUILD
		g_set_error_literal(
		    error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_SUPPORTED,
		    "generic GUID requires a CHID, child, parent or sibling requirement");
		return FALSE;
#else
		if ((flags & FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) == 0) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "generic GUID requires --force, a CHID, child, parent "
					    "or sibling requirement");
			return FALSE;
		}
		g_info("ignoring enforce-requires requirement due to --force");
#endif
	}

	/* soft requirements */
	reqs = fu_release_get_soft_reqs(release);
	if (reqs != NULL) {
		for (guint i = 0; i < reqs->len; i++) {
			XbNode *req = g_ptr_array_index(reqs, i);
			if (!fu_engine_requirements_check_soft(self, req, helper, error))
				return FALSE;
		}
	}

	/* success */
	return TRUE;
}
