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

#define G_LOG_DOMAIN "FuRelease"

#include "config.h"

#include "fu-device-private.h"
#include "fu-input-stream.h"
#include "fu-release-common.h"
#include "fu-release.h"

/**
 * FuRelease:
 *
 * An installable entity that has been loaded and verified for a specific device.
 *
 * See also: [class@FwupdRelease]
 */

struct _FuRelease {
	FwupdRelease parent_instance;
	FuEngineRequest *request;
	FuDevice *device;
	FwupdRemote *remote;
	FuConfig *config;
	FuInputStream *stream;
	gchar *update_request_id;
	gchar *device_version_old;
	gchar *firmware_basename;
	GPtrArray *soft_reqs; /* nullable, element-type XbNode */
	GPtrArray *hard_reqs; /* nullable, element-type XbNode */
	guint64 priority;
};

G_DEFINE_TYPE(FuRelease, fu_release, FWUPD_TYPE_RELEASE)

static gboolean
fu_release_ensure_trust_flags(FuRelease *self, XbNode *rel, GError **error);

gchar *
fu_release_to_string(FuRelease *self)
{
	const guint idt = 1;
	g_autoptr(GString) str = g_string_new(NULL);

	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);

	/* parent */
	fwupd_codec_add_string(FWUPD_CODEC(self), 0, str);

	/* instance */
	if (self->request != NULL)
		fwupd_codec_add_string(FWUPD_CODEC(self->request), idt, str);
	if (self->device != NULL)
		fwupd_codec_string_append(str, idt, "Device", fu_device_get_id(self->device));
	fwupd_codec_string_append(str, idt, "DeviceVersionOld", self->device_version_old);
	fwupd_codec_string_append(str, idt, "FirmwareBasename", self->firmware_basename);
	if (self->remote != NULL)
		fwupd_codec_string_append(str, idt, "Remote", fwupd_remote_get_id(self->remote));
	fwupd_codec_string_append_bool(str, idt, "HasConfig", self->config != NULL);
	fwupd_codec_string_append_bool(str, idt, "HasStream", self->stream != NULL);
	fwupd_codec_string_append(str, idt, "UpdateRequestId", self->update_request_id);
	if (self->soft_reqs != NULL)
		fwupd_codec_string_append_hex(str, idt, "SoftReqs", self->soft_reqs->len);
	if (self->hard_reqs != NULL)
		fwupd_codec_string_append_hex(str, idt, "HardReqs", self->hard_reqs->len);
	fwupd_codec_string_append_hex(str, idt, "Priority", self->priority);
	return g_string_free(g_steal_pointer(&str), FALSE);
}

/**
 * fu_release_set_request:
 * @self: a #FuRelease
 * @request: (nullable): a #FuEngineRequest
 *
 * Sets the user request which created this operation.
 **/
void
fu_release_set_request(FuRelease *self, FuEngineRequest *request)
{
	g_return_if_fail(FU_IS_RELEASE(self));
	g_set_object(&self->request, request);
}

/**
 * fu_release_get_request:
 * @self: a #FuRelease
 *
 * Gets the user request which created this operation.
 *
 * Returns: (transfer none) (nullable): request
 **/
FuEngineRequest *
fu_release_get_request(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->request;
}

/**
 * fu_release_get_device_version_old:
 * @self: a #FuRelease
 *
 * Gets the original [before update] device version.
 *
 * Returns: a string value, or %NULL if never set.
 **/
const gchar *
fu_release_get_device_version_old(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->device_version_old;
}

/**
 * fu_release_get_firmware_basename:
 * @self: a #FuRelease
 *
 * Gets the name of the update binary, typically `firmware.bin`
 *
 * Returns: a string value, or %NULL if never set.
 **/
const gchar *
fu_release_get_firmware_basename(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->firmware_basename;
}

void
fu_release_set_firmware_basename(FuRelease *self, const gchar *firmware_basename)
{
	g_return_if_fail(FU_IS_RELEASE(self));

	/* not changed */
	if (g_strcmp0(self->firmware_basename, firmware_basename) == 0)
		return;

	g_free(self->firmware_basename);
	self->firmware_basename = g_strdup(firmware_basename);
}

static void
fu_release_set_device_version_old(FuRelease *self, const gchar *device_version_old)
{
	g_return_if_fail(FU_IS_RELEASE(self));

	/* not changed */
	if (g_strcmp0(self->device_version_old, device_version_old) == 0)
		return;

	g_free(self->device_version_old);
	self->device_version_old = g_strdup(device_version_old);
}

/**
 * fu_release_set_device:
 * @self: a #FuRelease
 * @device: (nullable): a #FuDevice
 *
 * Sets the device this release should use when checking requirements.
 **/
void
fu_release_set_device(FuRelease *self, FuDevice *device)
{
	g_return_if_fail(FU_IS_RELEASE(self));

	/* make tests easier */
	fu_device_convert_instance_ids(device);

	g_set_object(&self->device, device);
	fu_release_set_device_version_old(self, fu_device_get_version(device));
}

/**
 * fu_release_get_device:
 * @self: a #FuRelease
 *
 * Gets the device this release was loaded for.
 *
 * Returns: (transfer none) (nullable): device
 **/
FuDevice *
fu_release_get_device(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->device;
}

/**
 * fu_release_get_stream:
 * @self: a #FuRelease
 *
 * Gets the firmware stream to use when installing this release.
 *
 * Returns: (transfer none) (nullable): data
 **/
FuInputStream *
fu_release_get_stream(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->stream;
}

/* private: for tests */
void
fu_release_set_stream(FuRelease *self, FuInputStream *stream)
{
	g_return_if_fail(FU_IS_RELEASE(self));
	g_return_if_fail(FU_IS_INPUT_STREAM(stream));
	g_set_object(&self->stream, stream);
}

/**
 * fu_release_get_soft_reqs:
 * @self: a #FuRelease
 *
 * Gets the additional soft requirements that need to be checked in the engine.
 *
 * Returns: (transfer none) (nullable) (element-type XbNode): nodes
 **/
GPtrArray *
fu_release_get_soft_reqs(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->soft_reqs;
}

/**
 * fu_release_get_soft_reqs:
 * @self: a #FuRelease
 *
 * Gets the additional hard requirements that need to be checked in the engine.
 *
 * Returns: (transfer none) (nullable) (element-type XbNode): nodes
 **/
GPtrArray *
fu_release_get_hard_reqs(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->hard_reqs;
}

/**
 * fu_release_get_update_request_id:
 * @self: a #FuRelease
 *
 * Gets the update request ID as specified from `LVFS::UpdateRequestId`.
 *
 * Returns: a string value, or %NULL if never set.
 **/
const gchar *
fu_release_get_update_request_id(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->update_request_id;
}

static void
fu_release_set_update_request_id(FuRelease *self, const gchar *update_request_id)
{
	g_return_if_fail(FU_IS_RELEASE(self));

	/* not changed */
	if (g_strcmp0(self->update_request_id, update_request_id) == 0)
		return;

	g_free(self->update_request_id);
	self->update_request_id = g_strdup(update_request_id);
}

/**
 * fu_release_set_remote:
 * @self: a #FuRelease
 * @remote: (nullable): a #FwupdRemote
 *
 * Sets the remote this release should use when loading. This is typically set by the engine by
 *watching the `remote-id` property to be set and then querying the internal cached list of
 *`FuRemote`s.
 **/
void
fu_release_set_remote(FuRelease *self, FwupdRemote *remote)
{
	g_return_if_fail(FU_IS_RELEASE(self));
	g_set_object(&self->remote, remote);
}

/**
 * fu_release_get_remote:
 * @self: a #FuRelease
 *
 * Gets the remote this release should use when loading.
 *
 * Returns: (transfer none): a #FwupdRemote, or %NULL if never set
 **/
FwupdRemote *
fu_release_get_remote(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), NULL);
	return self->remote;
}

/**
 * fu_release_set_config:
 * @self: a #FuRelease
 * @config: (nullable): a #FuConfig
 *
 * Sets the config to use when loading. The config may be used for things like ordering attributes
 * like protocol priority.
 **/
void
fu_release_set_config(FuRelease *self, FuConfig *config)
{
	g_return_if_fail(FU_IS_RELEASE(self));
	g_set_object(&self->config, config);
}

static gboolean
fu_release_locale_is_valid(const gchar *locale)
{
	/* locale should only contain alphanumeric, underscore, hyphen */
	for (gsize i = 0; locale[i] != '\0'; i++) {
		if (!g_ascii_isalnum(locale[i]) && locale[i] != '_' && locale[i] != '-')
			return FALSE;
	}
	return TRUE;
}

static gchar *
fu_release_get_localized_xpath(FuRelease *self, const gchar *element)
{
	GString *xpath = g_string_new(element);
	const gchar *locale = NULL;

	/* optional; not set in tests */
	if (self->request != NULL)
		locale = fu_engine_request_get_locale(self->request);

	/* prefer the users locale if set */
	if (locale != NULL && fu_release_locale_is_valid(locale)) {
		g_autofree gchar *xpath_locale = NULL;
		xpath_locale = g_strdup_printf("%s[@xml:lang='%s']|", element, locale);
		g_string_prepend(xpath, xpath_locale);
	}
	return g_string_free(xpath, FALSE);
}

/* convert hex and decimal versions to dotted style */
static gchar *
fu_release_get_release_version(FuRelease *self, const gchar *version, GError **error)
{
	/* already dotted notation */
	if (g_strstr_len(version, -1, ".") != NULL)
		return g_strdup(version);

	/* fallback for ESRT-derived UEFI devices */
	if (fu_device_has_private_flag(self->device, FU_DEVICE_PRIVATE_FLAG_LAZY_VERFMT)) {
		guint64 version_raw = 0;
		if (!fu_strtoull(version,
				 &version_raw,
				 1,
				 G_MAXUINT64,
				 FU_INTEGER_BASE_AUTO,
				 error)) {
			return NULL;
		}
		return fu_device_convert_version(self->device, version_raw, error);
	}

	/* fallback */
	return g_strdup(version);
}

static gboolean
fu_release_load_test_result(FuRelease *self, XbNode *n, GError **error)
{
	const gchar *tmp;
	g_autoptr(FwupdReport) report = fwupd_report_new();
	g_autoptr(GPtrArray) custom = NULL;
	g_autoptr(XbNode) os = NULL;
	g_autoptr(XbNode) vendor_name = NULL;

	tmp = xb_node_get_attr(n, "date");
	if (tmp != NULL) {
		g_autoptr(GDateTime) dt = NULL;
		g_autofree gchar *iso8601 = g_strdup_printf("%sT00:00:00Z", tmp);
		dt = g_date_time_new_from_iso8601(iso8601, NULL);
		if (dt != NULL)
			fwupd_report_set_created(report, g_date_time_to_unix(dt));
	}
	tmp = xb_node_query_text(n, "device", NULL);
	if (tmp != NULL)
		fwupd_report_set_device_name(report, tmp);
	tmp = xb_node_query_text(n, "previous_version", NULL);
	if (tmp != NULL) {
		fwupd_report_set_version_old(report, tmp);
		if (fu_version_compare(fu_release_get_version(self),
				       tmp,
				       FWUPD_VERSION_FORMAT_UNKNOWN) > 0) {
			fwupd_report_add_flag(report, FWUPD_REPORT_FLAG_IS_UPGRADE);
		}
	}
	vendor_name = xb_node_query_first(n, "vendor_name", NULL);
	if (vendor_name != NULL) {
		guint64 vendor_id = xb_node_get_attr_as_uint(vendor_name, "id");
		fwupd_report_set_vendor(report, xb_node_get_text(vendor_name));
		if (vendor_id != G_MAXUINT64)
			fwupd_report_set_vendor_id(report, vendor_id);
	}
	os = xb_node_query_first(n, "os", NULL);
	if (os != NULL) {
		tmp = xb_node_get_attr(os, "version");
		if (tmp != NULL)
			fwupd_report_set_distro_version(report, tmp);
		tmp = xb_node_get_attr(os, "variant");
		if (tmp != NULL)
			fwupd_report_set_distro_variant(report, tmp);
		fwupd_report_set_distro_id(report, xb_node_get_text(os));
	}
	if (fu_release_get_remote_id(self) != NULL)
		fwupd_report_set_remote_id(report, fu_release_get_remote_id(self));
	custom = xb_node_query(n, "custom/value", 0, NULL);
	if (custom != NULL) {
		for (guint i = 0; i < custom->len; i++) {
			XbNode *c = g_ptr_array_index(custom, i);
			if (g_strcmp0(xb_node_get_attr(c, "key"), "FromOEM") == 0) {
				fwupd_report_add_flag(report, FWUPD_REPORT_FLAG_FROM_OEM);
				continue;
			}
			if (xb_node_get_attr(c, "key") == NULL || xb_node_get_text(c) == NULL) {
				g_debug("ignoring metadata: %s=%s",
					xb_node_get_attr(c, "key"),
					xb_node_get_text(c));
				continue;
			}
			fwupd_report_add_metadata_item(report,
						       xb_node_get_attr(c, "key"),
						       xb_node_get_text(c));
		}
	}

	/* success */
	fwupd_release_add_report(FWUPD_RELEASE(self), report);
	return TRUE;
}

static guint
fu_release_get_uri_scheme_prio(FuRelease *self, const gchar *scheme)
{
	g_auto(GStrv) uri_schemes = NULL;

	uri_schemes = fu_config_get_value_strv(self->config, "fwupd", "UriSchemes");
	if (uri_schemes != NULL) {
		for (guint i = 0; uri_schemes[i] != NULL; i++) {
			if (g_strcmp0(uri_schemes[i], scheme) == 0)
				return i;
		}
	}
	return G_MAXUINT;
}

static gboolean
fu_release_load_artifact(FuRelease *self, XbNode *artifact, GError **error)
{
	const gchar *filename;
	guint64 size;
	g_autoptr(GPtrArray) locations = NULL;
	g_autoptr(GPtrArray) checksums = NULL;
	g_autoptr(GPtrArray) test_result = NULL;

	/* filename */
	filename = xb_node_query_text(artifact, "filename", NULL);
	if (filename != NULL) {
		g_autofree gchar *fn_lowercase = g_ascii_strdown(filename, -1);
		if (!g_str_has_suffix(fn_lowercase, ".cab")) {
			/* some firmware archives was signed with <artifact type="binary"> where the
			 * checksums were the *content* checksums, not the *container* checksum */
			g_debug("ignoring non-binary artifact entry: %s", filename);
			return TRUE;
		}
	}
	if (filename != NULL)
		fu_release_set_filename(self, filename);

	/* location */
	locations = xb_node_query(artifact, "location", 0, NULL);
	if (locations != NULL) {
		for (guint i = 0; i < locations->len; i++) {
			XbNode *n = g_ptr_array_index(locations, i);

			/* check the scheme is allowed */
			if (self->config != NULL) {
				g_autofree gchar *scheme =
				    fu_release_uri_get_scheme(xb_node_get_text(n));
				if (scheme != NULL) {
					guint prio = fu_release_get_uri_scheme_prio(self, scheme);
					if (prio == G_MAXUINT)
						continue;
				}
			}

			/* build the complete URI */
			if (self->remote != NULL) {
				g_autofree gchar *uri = NULL;
				uri = fwupd_remote_build_firmware_uri(self->remote,
								      xb_node_get_text(n),
								      NULL);
				if (uri != NULL) {
					fwupd_release_add_location(FWUPD_RELEASE(self), uri);
					continue;
				}
			}
			fwupd_release_add_location(FWUPD_RELEASE(self), xb_node_get_text(n));
		}
	}

	/* checksum */
	checksums = xb_node_query(artifact, "checksum", 0, NULL);
	if (checksums != NULL) {
		for (guint i = 0; i < checksums->len; i++) {
			XbNode *n = g_ptr_array_index(checksums, i);
			fwupd_release_add_checksum(FWUPD_RELEASE(self), xb_node_get_text(n));
		}
	}

	/* test results */
	test_result = xb_node_query(artifact, "testing/test_result", 0, NULL);
	if (test_result != NULL) {
		for (guint i = 0; i < test_result->len; i++) {
			XbNode *n = g_ptr_array_index(test_result, i);
			if (!fu_release_load_test_result(self, n, error))
				return FALSE;
		}
	}

	/* size */
	size = xb_node_query_text_as_uint(artifact, "size[@type='installed']", NULL);
	if (size != G_MAXUINT64)
		fwupd_release_set_size(FWUPD_RELEASE(self), size);

	/* success */
	return TRUE;
}

static gint
fu_release_scheme_compare_cb(gconstpointer a, gconstpointer b, gpointer user_data)
{
	FuRelease *self = FU_RELEASE(user_data);
	const gchar *location1 = *((const gchar **)a);
	const gchar *location2 = *((const gchar **)b);
	g_autofree gchar *scheme1 = fu_release_uri_get_scheme(location1);
	g_autofree gchar *scheme2 = fu_release_uri_get_scheme(location2);
	guint prio1 = fu_release_get_uri_scheme_prio(self, scheme1);
	guint prio2 = fu_release_get_uri_scheme_prio(self, scheme2);
	if (prio1 < prio2)
		return -1;
	if (prio1 > prio2)
		return 1;
	return 0;
}

static gboolean
fu_release_check_requirements_version_check(FuRelease *self, GError **error)
{
	if (self->hard_reqs != NULL) {
		for (guint i = 0; i < self->hard_reqs->len; i++) {
			XbNode *req = g_ptr_array_index(self->hard_reqs, i);
			if (g_strcmp0(xb_node_get_element(req), "firmware") == 0 &&
			    xb_node_get_text(req) == NULL) {
				return TRUE;
			}
		}
	}
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "no firmware requirement");
	return FALSE;
}

static gchar *
fu_release_verfmts_to_string(GPtrArray *verfmts)
{
	GString *str = g_string_new(NULL);
	for (guint i = 0; i < verfmts->len; i++) {
		XbNode *verfmt = g_ptr_array_index(verfmts, i);
		const gchar *tmp = xb_node_get_text(verfmt);
		g_string_append_printf(str, "%s;", tmp);
	}
	if (str->len > 0)
		g_string_truncate(str, str->len - 1);
	return g_string_free(str, FALSE);
}

static gboolean
fu_release_check_verfmt_compatible(FuRelease *self, FwupdVersionFormat fmt_rel)
{
	FwupdVersionFormat fmt_dev = fu_device_get_version_format(self->device);
	if (fmt_dev == FWUPD_VERSION_FORMAT_BCD && fmt_rel == FWUPD_VERSION_FORMAT_PAIR)
		return TRUE;
	return fmt_dev == fmt_rel;
}

static gboolean
fu_release_check_verfmt(FuRelease *self,
			GPtrArray *verfmts,
			FwupdInstallFlags flags,
			GError **error)
{
	FwupdVersionFormat fmt_dev = fu_device_get_version_format(self->device);
	g_autofree gchar *verfmts_str = NULL;

	/* no device format */
	if (fmt_dev == FWUPD_VERSION_FORMAT_UNKNOWN && (flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		verfmts_str = fu_release_verfmts_to_string(verfmts);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "release version format '%s' but no device version format",
			    verfmts_str);
		return FALSE;
	}

	/* compare all version formats */
	for (guint i = 0; i < verfmts->len; i++) {
		XbNode *verfmt = g_ptr_array_index(verfmts, i);
		const gchar *tmp = xb_node_get_text(verfmt);
		FwupdVersionFormat fmt_rel = fwupd_version_format_from_string(tmp);
		if (fu_release_check_verfmt_compatible(self, fmt_rel))
			return TRUE;
	}
	verfmts_str = fu_release_verfmts_to_string(verfmts);
	if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Firmware version formats were different, "
			    "device was '%s' and release is '%s'",
			    fwupd_version_format_to_string(fmt_dev),
			    verfmts_str);
		return FALSE;
	}
	g_warning("ignoring version format difference %s:%s",
		  fwupd_version_format_to_string(fmt_dev),
		  verfmts_str);
	return TRUE;
}

/* these can all be done without the daemon */
static gboolean
fu_release_check_requirements(FuRelease *self,
			      XbNode *component,
			      FwupdInstallFlags install_flags,
			      GError **error)
{
	const gchar *branch_new;
	const gchar *branch_old;
	const gchar *protocol;
	gboolean matches_guid = FALSE;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) provides = NULL;

	/* does this component provide a GUID the device has */
	provides = xb_node_query(component, "provides/firmware[@type='flashed']", 0, &error_local);
	if (provides == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "No supported devices found: %s",
			    error_local->message);
		return FALSE;
	}
	for (guint i = 0; i < provides->len; i++) {
		XbNode *provide = g_ptr_array_index(provides, i);
		if (fu_device_has_guid(self->device, xb_node_get_text(provide))) {
			matches_guid = TRUE;
			break;
		}
	}
	if (!matches_guid) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "No supported devices found");
		return FALSE;
	}

	/* device requires a version check */
	if (fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED)) {
		if (!fu_release_check_requirements_version_check(self, error)) {
			g_prefix_error_literal(error,
					       "device requires firmware with a version check: ");
			return FALSE;
		}
	}

	/* does the protocol match */
	protocol = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateProtocol']", NULL);
	if (fu_device_get_protocols(self->device)->len != 0 && protocol != NULL &&
	    !fu_device_has_protocol(self->device, protocol) &&
	    (install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		g_autofree gchar *str = NULL;
		g_autofree gchar *id_display = fu_device_get_id_display(self->device);

		str = fu_strjoin("|", fu_device_get_protocols(self->device));
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Device %s does not support %s, only %s",
			    id_display,
			    protocol,
			    str);
		return FALSE;
	}

	/* check the device is not locked */
	if (fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_LOCKED)) {
		g_autofree gchar *id_display = fu_device_get_id_display(self->device);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Device %s is locked",
			    id_display);
		return FALSE;
	}

	/* check the branch is not switching */
	branch_new = xb_node_query_text(component, "branch", NULL);
	branch_old = fu_device_get_branch(self->device);
	if ((install_flags & FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) == 0 &&
	    g_strcmp0(branch_old, branch_new) != 0) {
		g_autofree gchar *id_display = fu_device_get_id_display(self->device);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Device %s would switch firmware branch from %s to %s",
			    id_display,
			    branch_old != NULL ? branch_old : "default",
			    branch_new != NULL ? branch_new : "default");
		return FALSE;
	}

	/* no update abilities */
	if (!fu_engine_request_has_feature_flag(self->request, FWUPD_FEATURE_FLAG_SHOW_PROBLEMS) &&
	    !fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_UPDATABLE)) {
		g_autofree gchar *id_display = fu_device_get_id_display(self->device);
		g_autoptr(GString) str = g_string_new(NULL);
		g_string_append_printf(str,
				       "Device %s does not currently allow updates",
				       id_display);
		if (fu_device_get_update_error(self->device) != NULL) {
			g_string_append_printf(str,
					       ": %s",
					       fu_device_get_update_error(self->device));
		}
		g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, str->str);
		return FALSE;
	}

	/* success */
	return TRUE;
}

/**
 * fu_release_check_version:
 * @self: a #FuRelease
 * @component: (not nullable): a #XbNode
 * @install_flags: a #FwupdInstallFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE
 * @error: (nullable): optional return location for an error
 *
 * Checks the component against this release, specifically that the device can be upgraded with this
 * new firmware version.
 *
 * Returns: %TRUE if the requirements passed
 **/
gboolean
fu_release_check_version(FuRelease *self,
			 XbNode *component,
			 FwupdInstallFlags install_flags,
			 GError **error)
{
	const gchar *version;
	const gchar *version_lowest;
	const gchar *version_highest;
	gint vercmp;

	g_return_val_if_fail(FU_IS_RELEASE(self), FALSE);
	g_return_val_if_fail(XB_IS_NODE(component), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* skip */
	if (self->device == NULL)
		return TRUE;
	if (self->request != NULL &&
	    fu_engine_request_has_flag(self->request, FU_ENGINE_REQUEST_FLAG_NO_REQUIREMENTS)) {
		return TRUE;
	}

	/* allow using no-version-expected on emulated devices, or when not build as supported */
	if (fu_device_has_private_flag(self->device, FU_DEVICE_PRIVATE_FLAG_NO_VERSION_EXPECTED)) {
#ifdef SUPPORTED_BUILD
		if (!fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATED)) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "only emulated devices can install releases with no "
					    "version when -Dsupported_build");
			return FALSE;
		}
#endif
		return TRUE;
	}

	/* ensure device has a version */
	version = fu_device_get_version(self->device);
	if (version == NULL) {
		g_autofree gchar *id_display = fu_device_get_id_display(self->device);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Device %s has no firmware version",
			    id_display);
		return FALSE;
	}

	/* check the version formats match if set in the release */
	if ((install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0 &&
	    (install_flags & FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) == 0) {
		g_autoptr(GPtrArray) verfmts =
		    xb_node_query(component, "custom/value[@key='LVFS::VersionFormat']", 0, NULL);
		if (verfmts != NULL) {
			if (!fu_release_check_verfmt(self, verfmts, install_flags, error))
				return FALSE;
		}
	}

	/* compare to the supported versions, if it exists */
	version_lowest = fu_device_get_version_lowest(self->device);
	if (version_lowest != NULL &&
	    fu_version_compare(version_lowest,
			       fu_release_get_version(self),
			       fu_device_get_version_format(self->device)) > 0 &&
	    (install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Specified firmware is older than the minimum "
			    "required version '%s < %s'",
			    fu_release_get_version(self),
			    version_lowest);
		return FALSE;
	}
	version_highest = fu_device_get_version_highest(self->device);
	if (version_highest != NULL &&
	    fu_version_compare(version_highest,
			       fu_release_get_version(self),
			       fu_device_get_version_format(self->device)) < 0 &&
	    (install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Specified firmware is newer than the maximum "
			    "allowed version '%s > %s'",
			    fu_release_get_version(self),
			    version_highest);
		return FALSE;
	}

	/* is this a downgrade or re-install */
	vercmp = fu_version_compare(version,
				    fu_release_get_version(self),
				    fu_device_get_version_format(self->device));
	if (fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE) &&
	    vercmp >= 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "Device only supports version upgrades");
		return FALSE;
	}
	if (vercmp == 0 && (install_flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_VERSION_SAME,
			    "Specified firmware is already installed '%s'",
			    fu_release_get_version(self));
		return FALSE;
	}
	if (vercmp > 0)
		fu_release_add_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE);
	if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE) &&
	    (install_flags & FWUPD_INSTALL_FLAG_ALLOW_OLDER) == 0 &&
	    (install_flags & FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_VERSION_NEWER,
			    "Specified firmware is older than installed '%s < %s'",
			    fu_release_get_version(self),
			    version);
		return FALSE;
	}

	/* success */
	return TRUE;
}

void
fu_release_set_priority(FuRelease *self, guint64 priority)
{
	g_return_if_fail(FU_IS_RELEASE(self));
	self->priority = priority;
}

guint64
fu_release_get_priority(FuRelease *self)
{
	g_return_val_if_fail(FU_IS_RELEASE(self), 0);
	return self->priority;
}

/**
 * fu_release_load:
 * @self: a #FuRelease
 * @cabinet: (nullable): a #FuCabinet
 * @component: (not nullable): a #XbNode
 * @rel_optional: (nullable): a #XbNode
 * @install_flags: a #FwupdInstallFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE
 * @error: (nullable): optional return location for an error
 *
 * Loads then checks any requirements of this release. This will typically involve checking
 * that the device can accept the component (the GUIDs match) and that the device can be
 * upgraded with this firmware version.
 *
 * Returns: %TRUE if the release was loaded and the requirements passed
 **/
gboolean
fu_release_load(FuRelease *self,
		FuCabinet *cabinet,
		XbNode *component,
		XbNode *rel_optional,
		FwupdInstallFlags install_flags,
		GError **error)
{
	const gchar *tmp;
	guint64 tmp64;
	GBytes *blob_basename;
	g_autofree gchar *name_xpath = NULL;
	g_autofree gchar *namevs_xpath = NULL;
	g_autofree gchar *summary_xpath = NULL;
	g_autofree gchar *description_xpath = NULL;
	g_autoptr(GPtrArray) cats = NULL;
	g_autoptr(GPtrArray) tags = NULL;
	g_autoptr(GPtrArray) issues = NULL;
	g_autoptr(XbNode) artifact = NULL;
	g_autoptr(XbNode) description = NULL;
	g_autoptr(XbNode) rel = NULL;
	g_autoptr(GError) error_soft = NULL;
	g_autoptr(GError) error_hard = NULL;

	g_return_val_if_fail(FU_IS_RELEASE(self), FALSE);
	g_return_val_if_fail(cabinet == NULL || FU_IS_CABINET(cabinet), FALSE);
	g_return_val_if_fail(XB_IS_NODE(component), FALSE);
	g_return_val_if_fail(rel_optional == NULL || XB_IS_NODE(rel_optional), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* set from the component */
	tmp = xb_node_query_text(component, "id", NULL);
	if (tmp != NULL)
		fwupd_release_set_appstream_id(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "url[@type='homepage']", NULL);
	if (tmp != NULL)
		fwupd_release_set_homepage(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "project_license", NULL);
	if (tmp != NULL)
		fwupd_release_set_license(FWUPD_RELEASE(self), tmp);
	name_xpath = fu_release_get_localized_xpath(self, "name");
	tmp = xb_node_query_text(component, name_xpath, NULL);
	if (tmp != NULL)
		fwupd_release_set_name(FWUPD_RELEASE(self), tmp);
	summary_xpath = fu_release_get_localized_xpath(self, "summary");
	tmp = xb_node_query_text(component, summary_xpath, NULL);
	if (tmp != NULL)
		fwupd_release_set_summary(FWUPD_RELEASE(self), tmp);
	namevs_xpath = fu_release_get_localized_xpath(self, "name_variant_suffix");
	tmp = xb_node_query_text(component, namevs_xpath, NULL);
	if (tmp != NULL)
		fwupd_release_set_name_variant_suffix(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "branch", NULL);
	if (tmp != NULL)
		fwupd_release_set_branch(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "developer_name", NULL);
	if (tmp != NULL)
		fwupd_release_set_vendor(FWUPD_RELEASE(self), tmp);

	/* use default release */
	if (rel_optional == NULL) {
		g_autoptr(GError) error_local = NULL;
		g_autoptr(XbQuery) query = NULL;
		query = xb_query_new_full(xb_node_get_silo(component),
					  "releases/release",
					  XB_QUERY_FLAG_FORCE_NODE_CACHE,
					  error);
		if (query == NULL)
			return FALSE;
		rel = xb_node_query_first_full(component, query, &error_local);
		if (rel == NULL) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "failed to get default release: %s",
				    error_local->message);
			return FALSE;
		}
	} else {
		rel = g_object_ref(rel_optional);
	}

	/* find the remote */
	tmp = xb_node_query_text(component, "../custom/value[@key='fwupd::RemoteId']", NULL);
	if (tmp != NULL)
		fwupd_release_set_remote_id(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "../custom/value[@key='LVFS::Distributor']", NULL);
	if (tmp != NULL && g_str_has_prefix(tmp, "community"))
		fwupd_release_add_flag(FWUPD_RELEASE(self), FWUPD_RELEASE_FLAG_IS_COMMUNITY);

	/* use the metadata to set the device attributes */
	if (!fu_release_ensure_trust_flags(self, rel, error))
		return FALSE;

	/* per-release priority wins, but fallback to per-component priority */
	tmp64 = xb_node_get_attr_as_uint(rel, "priority");
	if (tmp64 == G_MAXUINT64)
		tmp64 = xb_node_get_attr_as_uint(component, "priority");
	if (tmp64 != G_MAXUINT64)
		fu_release_set_priority(self, tmp64);

	/* the version is fixed up with the device format */
	tmp = xb_node_get_attr(rel, "version");
	if (tmp == NULL) {
		g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "version unset");
		return FALSE;
	}
	if (self->device != NULL) {
		g_autofree gchar *version_rel = NULL;
		version_rel = fu_release_get_release_version(self, tmp, error);
		if (version_rel == NULL)
			return FALSE;
		fwupd_release_set_version(FWUPD_RELEASE(self), version_rel);
	} else {
		/* historical releases have ->convert_version() already done */
		if (fu_release_get_version(self) == NULL)
			fu_release_set_version(self, tmp);
	}

	/* optional release ID -- currently a integer but maybe namespaced in the future */
	fwupd_release_set_id(FWUPD_RELEASE(self), xb_node_get_attr(rel, "id"));

	/* this is the more modern way to do this */
	artifact = xb_node_query_first(rel, "artifacts/artifact[@type='binary']", NULL);
	if (artifact != NULL) {
		if (!fu_release_load_artifact(self, artifact, error))
			return FALSE;
	}
	description_xpath = fu_release_get_localized_xpath(self, "description");
	description = xb_node_query_first(rel, description_xpath, NULL);
	if (description != NULL) {
		g_autofree gchar *xml = NULL;
		g_autoptr(GString) str = NULL;
		xml = xb_node_export(description, XB_NODE_EXPORT_FLAG_ONLY_CHILDREN, NULL);
		str = g_string_new(xml);
		if (self->device != NULL && self->request != NULL &&
		    fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_AFFECTS_FDE) &&
		    !fu_engine_request_has_feature_flag(self->request,
							FWUPD_FEATURE_FLAG_FDE_WARNING)) {
			g_string_prepend(
			    str,
			    "<p>Some of the platform secrets may be invalidated when "
			    "updating this firmware. Please ensure you have the volume "
			    "recovery key before continuing.</p>");
		}
		if (fwupd_release_has_flag(FWUPD_RELEASE(self), FWUPD_RELEASE_FLAG_IS_COMMUNITY) &&
		    self->request != NULL &&
		    !fu_engine_request_has_feature_flag(self->request,
							FWUPD_FEATURE_FLAG_COMMUNITY_TEXT)) {
			g_string_prepend(
			    str,
			    "<p>This firmware is provided by LVFS community "
			    "members and is not provided (or supported) by the original "
			    "hardware vendor. "
			    "Installing this update may also void any device warranty.</p>");
		}
		if (str->len > 0)
			fwupd_release_set_description(FWUPD_RELEASE(self), str->str);
	}
	if (fwupd_release_get_locations(FWUPD_RELEASE(self))->len == 0) {
		tmp = xb_node_query_text(rel, "location", NULL);
		if (tmp != NULL) {
			g_autofree gchar *uri = NULL;
			if (self->remote != NULL)
				uri = fwupd_remote_build_firmware_uri(self->remote, tmp, NULL);
			if (uri == NULL)
				uri = g_strdup(tmp);
			fwupd_release_add_location(FWUPD_RELEASE(self), uri);
		}
	}
	if (fwupd_release_get_locations(FWUPD_RELEASE(self))->len == 0 && self->remote != NULL &&
	    fwupd_remote_get_kind(self->remote) == FWUPD_REMOTE_KIND_DIRECTORY) {
		tmp = xb_node_query_text(component,
					 "../custom/value[@key='fwupd::FilenameCache']",
					 NULL);
		if (tmp != NULL) {
			g_autofree gchar *uri = g_strdup_printf("file://%s", tmp);
			fwupd_release_add_location(FWUPD_RELEASE(self), uri);
		}
	}
	if (fu_release_get_filename(self) == NULL) {
		tmp = xb_node_query_attr(rel, "checksum[@target='container']", "filename", NULL);
		if (tmp != NULL)
			fu_release_set_filename(self, tmp);
	}
	tmp = xb_node_query_text(rel, "url[@type='details']", NULL);
	if (tmp != NULL)
		fwupd_release_set_details_url(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(rel, "url[@type='source']", NULL);
	if (tmp != NULL)
		fwupd_release_set_source_url(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(rel, "url[@type='sbom']", NULL);
	if (tmp != NULL)
		fwupd_release_set_sbom_url(FWUPD_RELEASE(self), tmp);
	if (fwupd_release_get_checksums(FWUPD_RELEASE(self))->len == 0) {
		g_autoptr(GPtrArray) checksums = NULL;
		checksums = xb_node_query(rel, "checksum[@target='container']", 0, NULL);
		if (checksums != NULL) {
			for (guint i = 0; i < checksums->len; i++) {
				XbNode *n = g_ptr_array_index(checksums, i);
				if (xb_node_get_text(n) == NULL)
					continue;
				fwupd_release_add_checksum(FWUPD_RELEASE(self),
							   xb_node_get_text(n));
			}
		}
	}
	if (fwupd_release_get_size(FWUPD_RELEASE(self)) == 0) {
		tmp64 = xb_node_query_text_as_uint(rel, "size[@type='installed']", NULL);
		if (tmp64 != G_MAXUINT64)
			fwupd_release_set_size(FWUPD_RELEASE(self), tmp64);
	}
	if (fwupd_release_get_size(FWUPD_RELEASE(self)) == 0) {
		GBytes *sz = xb_node_get_data(rel, "fwupd::ReleaseSize");
		if (sz != NULL) {
			const guint64 *sizeptr = g_bytes_get_data(sz, NULL);
			fwupd_release_set_size(FWUPD_RELEASE(self), *sizeptr);
		}
	}
	tmp = xb_node_get_attr(rel, "urgency");
	if (tmp != NULL)
		fwupd_release_set_urgency(FWUPD_RELEASE(self),
					  fwupd_release_urgency_from_string(tmp));
	tmp64 = xb_node_get_attr_as_uint(rel, "install_duration");
	if (tmp64 != G_MAXUINT64)
		fwupd_release_set_install_duration(FWUPD_RELEASE(self), tmp64);
	tmp64 = xb_node_get_attr_as_uint(rel, "timestamp");
	if (tmp64 != G_MAXUINT64)
		fwupd_release_set_created(FWUPD_RELEASE(self), tmp64);
	cats = xb_node_query(component, "categories/category", 0, NULL);
	if (cats != NULL) {
		for (guint i = 0; i < cats->len; i++) {
			XbNode *n = g_ptr_array_index(cats, i);
			fwupd_release_add_category(FWUPD_RELEASE(self), xb_node_get_text(n));
		}
	}
	tags = xb_node_query(component, "tags/tag[@namespace=$'lvfs']", 0, NULL);
	if (tags != NULL) {
		for (guint i = 0; i < tags->len; i++) {
			XbNode *tag = g_ptr_array_index(tags, i);
			fwupd_release_add_tag(FWUPD_RELEASE(self), xb_node_get_text(tag));
		}
	}
	issues = xb_node_query(rel, "issues/issue", 0, NULL);
	if (issues != NULL) {
		for (guint i = 0; i < issues->len; i++) {
			XbNode *n = g_ptr_array_index(issues, i);
			fwupd_release_add_issue(FWUPD_RELEASE(self), xb_node_get_text(n));
		}
	}
	tmp = xb_node_query_text(component, "screenshots/screenshot/caption", NULL);
	if (tmp != NULL)
		fwupd_release_set_detach_caption(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "screenshots/screenshot/image", NULL);
	if (tmp != NULL) {
		if (self->remote != NULL) {
			g_autofree gchar *img = NULL;
			img = fwupd_remote_build_firmware_uri(self->remote, tmp, error);
			if (img == NULL)
				return FALSE;
			fwupd_release_set_detach_image(FWUPD_RELEASE(self), img);
		} else {
			fwupd_release_set_detach_image(FWUPD_RELEASE(self), tmp);
		}
	}
	tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateProtocol']", NULL);
	if (tmp != NULL)
		fwupd_release_set_protocol(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateMessage']", NULL);
	if (tmp != NULL)
		fwupd_release_set_update_message(FWUPD_RELEASE(self), tmp);
	tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateImage']", NULL);
	if (tmp != NULL) {
		if (self->remote != NULL) {
			g_autofree gchar *img = NULL;
			img = fwupd_remote_build_firmware_uri(self->remote, tmp, error);
			if (img == NULL)
				return FALSE;
			fwupd_release_set_update_image(FWUPD_RELEASE(self), img);
		} else {
			fwupd_release_set_update_image(FWUPD_RELEASE(self), tmp);
		}
	}
	tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateRequestId']", NULL);
	if (tmp != NULL)
		fu_release_set_update_request_id(self, tmp);

	/* hard and soft requirements */
	self->hard_reqs = xb_node_query(component, "requires/*", 0, &error_hard);
	if (self->hard_reqs == NULL) {
		if (!g_error_matches(error_hard, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
		    !g_error_matches(error_hard, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) {
			g_propagate_error(error, g_steal_pointer(&error_hard));
			fwupd_error_convert(error);
			return FALSE;
		}
	}
	self->soft_reqs = xb_node_query(component, "suggests/*|recommends/*", 0, &error_soft);
	if (self->soft_reqs == NULL) {
		if (!g_error_matches(error_soft, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
		    !g_error_matches(error_soft, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) {
			g_propagate_error(error, g_steal_pointer(&error_soft));
			fwupd_error_convert(error);
			return FALSE;
		}
	}

	/* get per-release firmware stream */
	blob_basename = xb_node_get_data(rel, "fwupd::FirmwareBasename");
	if (cabinet != NULL && blob_basename != NULL) {
		g_autoptr(FuFirmware) img = NULL;

		/* firmware basenames should be short */
		self->firmware_basename = fu_strsafe_bytes(blob_basename, 1 * FU_KB);
		if (self->firmware_basename == NULL) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "firmware basename too large or invalid");
			return FALSE;
		}

		img = fu_firmware_get_image_by_id(FU_FIRMWARE(cabinet),
						  self->firmware_basename,
						  error);
		if (img == NULL) {
			g_prefix_error(error, "failed to find %s: ", self->firmware_basename);
			return FALSE;
		}
		self->stream = fu_firmware_get_stream(img, error);
		if (self->stream == NULL)
			return FALSE;
	}

	/* to build the firmware */
	tmp = g_object_get_data(G_OBJECT(component), "fwupd::BuilderScript");
	if (tmp != NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "fwupd::BuilderScript is no longer supported");
		return FALSE;
	}

	/* sort the locations by scheme */
	if (self->config != NULL) {
		g_ptr_array_sort_with_data(fwupd_release_get_locations(FWUPD_RELEASE(self)),
					   fu_release_scheme_compare_cb,
					   self);
	}

	/* check requirements for device */
	if (self->device != NULL && self->request != NULL &&
	    !fu_engine_request_has_flag(self->request, FU_ENGINE_REQUEST_FLAG_NO_REQUIREMENTS)) {
		if (!fu_release_check_requirements(self, component, install_flags, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_release_ensure_trust_flags(FuRelease *self, XbNode *rel, GError **error)
{
	GBytes *blob;

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

	/* in the self tests */
	if (g_getenv("FWUPD_SELF_TEST") != NULL) {
		fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD);
		fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA);
		return TRUE;
	}

	/* populated from an actual cab archive */
	blob = xb_node_get_data(rel, "fwupd::ReleaseFlags");
	if (blob != NULL) {
		FwupdReleaseFlags flags = FWUPD_RELEASE_FLAG_NONE;
		if (!fu_memcpy_safe((guint8 *)&flags,
				    sizeof(flags),
				    0x0, /* dst */
				    g_bytes_get_data(blob, NULL),
				    g_bytes_get_size(blob),
				    0x0, /* src */
				    sizeof(flags),
				    error))
			return FALSE;
		if (flags & FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD)
			fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD);
		if (flags & FWUPD_RELEASE_FLAG_TRUSTED_METADATA)
			fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA);
	}

	/* do not require signatures for anything installed to the immutable datadir */
	if (fu_release_get_flags(self) == FWUPD_RELEASE_FLAG_NONE && self->remote != NULL) {
		g_debug("remote %s has kind=%s and so marking as trusted",
			fwupd_remote_get_id(self->remote),
			fwupd_remote_kind_to_string(fwupd_remote_get_kind(self->remote)));
		if (fwupd_remote_get_kind(self->remote) == FWUPD_REMOTE_KIND_LOCAL ||
		    fwupd_remote_get_kind(self->remote) == FWUPD_REMOTE_KIND_DIRECTORY) {
			fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD);
			fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA);
		} else {
			fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA);
		}
	}

	/* success */
	return TRUE;
}

/**
 * fu_release_get_action_id:
 *
 * Gets the PolicyKit action ID to use for the install operation.
 *
 * Returns: string, e.g. `org.freedesktop.fwupd.update-internal-trusted`
 **/
const gchar *
fu_release_get_action_id(FuRelease *self)
{
	/* relax authentication checks for removable devices */
	if (!fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_INTERNAL)) {
		if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) {
			if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD))
				return "org.freedesktop.fwupd.downgrade-hotplug-trusted";
			return "org.freedesktop.fwupd.downgrade-hotplug";
		}
		if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD))
			return "org.freedesktop.fwupd.update-hotplug-trusted";
		return "org.freedesktop.fwupd.update-hotplug";
	}

	/* internal device */
	if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) {
		if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD))
			return "org.freedesktop.fwupd.downgrade-internal-trusted";
		return "org.freedesktop.fwupd.downgrade-internal";
	}
	if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD))
		return "org.freedesktop.fwupd.update-internal-trusted";
	return "org.freedesktop.fwupd.update-internal";
}

/**
 * fu_release_compare:
 * @release1: first task to compare.
 * @release2: second task to compare.
 *
 * Compares two releases.
 *
 * Returns: 1, 0 or -1 if @release1 is greater, equal, or less than @release2, respectively.
 **/
gint
fu_release_compare(FuRelease *release1, FuRelease *release2)
{
	FuDevice *device1 = fu_release_get_device(release1);
	FuDevice *device2 = fu_release_get_device(release2);

	/* device order, lower is better */
	if (device1 != NULL && device2 != NULL && device1 != device2) {
		if (fu_device_get_order(device1) < fu_device_get_order(device2))
			return -1;
		if (fu_device_get_order(device1) > fu_device_get_order(device2))
			return 1;
	}

	/* release priority, higher is better */
	if (release1->priority > release2->priority)
		return -1;
	if (release1->priority < release2->priority)
		return 1;

	/* remote priority, higher is better */
	if (release1->remote != NULL && release2->remote != NULL) {
		if (fwupd_remote_get_priority(release1->remote) >
		    fwupd_remote_get_priority(release2->remote))
			return -1;
		if (fwupd_remote_get_priority(release1->remote) <
		    fwupd_remote_get_priority(release2->remote))
			return 1;
	}

	/* FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES has to be from oldest to newest */
	return fu_version_compare(fu_release_get_version(release1),
				  fu_release_get_version(release2),
				  device1 != NULL ? fu_device_get_version_format(device1)
						  : FWUPD_VERSION_FORMAT_UNKNOWN);
}

static void
fu_release_init(FuRelease *self)
{
	fu_release_set_flags(self, FWUPD_RELEASE_FLAG_NONE);
}

static void
fu_release_finalize(GObject *obj)
{
	FuRelease *self = FU_RELEASE(obj);

	g_free(self->update_request_id);
	g_free(self->device_version_old);
	g_free(self->firmware_basename);
	if (self->request != NULL)
		g_object_unref(self->request);
	if (self->device != NULL)
		g_object_unref(self->device);
	if (self->remote != NULL)
		g_object_unref(self->remote);
	if (self->config != NULL)
		g_object_unref(self->config);
	if (self->stream != NULL)
		g_object_unref(self->stream);
	if (self->soft_reqs != NULL)
		g_ptr_array_unref(self->soft_reqs);
	if (self->hard_reqs != NULL)
		g_ptr_array_unref(self->hard_reqs);

	G_OBJECT_CLASS(fu_release_parent_class)->finalize(obj);
}

static void
fu_release_class_init(FuReleaseClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	object_class->finalize = fu_release_finalize;
}

FuRelease *
fu_release_new(void)
{
	FuRelease *self;
	self = g_object_new(FU_TYPE_RELEASE, NULL);
	return FU_RELEASE(self);
}
