/*
 * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#define G_LOG_DOMAIN "FuCommon"

#include <config.h>

#ifdef HAVE_GIO_UNIX
#include <gio/gunixinputstream.h>
#endif
#include <glib/gstdio.h>

#ifdef HAVE_KENV_H
#include <kenv.h>
#endif

#ifdef HAVE_CPUID_H
#include <cpuid.h>
#endif

#ifdef HAVE_UTSNAME_H
#include <sys/utsname.h>
#endif

#ifdef HAVE_LIBARCHIVE
#include <archive.h>
#include <archive_entry.h>
#endif
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "fwupd-error.h"

#include "fu-common-private.h"
#include "fu-common-version.h"
#include "fu-firmware.h"
#include "fu-volume-private.h"

/**
 * fu_common_rmtree:
 * @directory: a directory name
 * @error: (nullable): optional return location for an error
 *
 * Recursively removes a directory.
 *
 * Returns: %TRUE for success, %FALSE otherwise
 *
 * Since: 0.9.7
 **/
gboolean
fu_common_rmtree(const gchar *directory, GError **error)
{
	const gchar *filename;
	g_autoptr(GDir) dir = NULL;

	g_return_val_if_fail(directory != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* try to open */
	g_debug("removing %s", directory);
	dir = g_dir_open(directory, 0, error);
	if (dir == NULL)
		return FALSE;

	/* find each */
	while ((filename = g_dir_read_name(dir))) {
		g_autofree gchar *src = NULL;
		src = g_build_filename(directory, filename, NULL);
		if (g_file_test(src, G_FILE_TEST_IS_DIR)) {
			if (!fu_common_rmtree(src, error))
				return FALSE;
		} else {
			if (g_unlink(src) != 0) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "Failed to delete: %s",
					    src);
				return FALSE;
			}
		}
	}
	if (g_remove(directory) != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Failed to delete: %s",
			    directory);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_common_get_file_list_internal(GPtrArray *files, const gchar *directory, GError **error)
{
	const gchar *filename;
	g_autoptr(GDir) dir = NULL;

	/* try to open */
	dir = g_dir_open(directory, 0, error);
	if (dir == NULL)
		return FALSE;

	/* find each */
	while ((filename = g_dir_read_name(dir))) {
		g_autofree gchar *src = g_build_filename(directory, filename, NULL);
		if (g_file_test(src, G_FILE_TEST_IS_DIR)) {
			if (!fu_common_get_file_list_internal(files, src, error))
				return FALSE;
		} else {
			g_ptr_array_add(files, g_steal_pointer(&src));
		}
	}
	return TRUE;
}

/**
 * fu_common_get_files_recursive:
 * @path: a directory name
 * @error: (nullable): optional return location for an error
 *
 * Returns every file found under @directory, and any subdirectory.
 * If any path under @directory cannot be accessed due to permissions an error
 * will be returned.
 *
 * Returns: (transfer container) (element-type utf8): array of files, or %NULL for error
 *
 * Since: 1.0.6
 **/
GPtrArray *
fu_common_get_files_recursive(const gchar *path, GError **error)
{
	g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);

	g_return_val_if_fail(path != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	if (!fu_common_get_file_list_internal(files, path, error))
		return NULL;
	return g_steal_pointer(&files);
}

/**
 * fu_common_mkdir:
 * @dirname: a directory name
 * @error: (nullable): optional return location for an error
 *
 * Creates any required directories, including any parent directories.
 *
 * Returns: %TRUE for success
 *
 * Since: 1.7.1
 **/
gboolean
fu_common_mkdir(const gchar *dirname, GError **error)
{
	g_return_val_if_fail(dirname != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!g_file_test(dirname, G_FILE_TEST_IS_DIR))
		g_debug("creating path %s", dirname);
	if (g_mkdir_with_parents(dirname, 0755) == -1) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Failed to create '%s': %s",
			    dirname,
			    g_strerror(errno));
		return FALSE;
	}
	return TRUE;
}

/**
 * fu_common_mkdir_parent:
 * @filename: a full pathname
 * @error: (nullable): optional return location for an error
 *
 * Creates any required directories, including any parent directories.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.9.7
 **/
gboolean
fu_common_mkdir_parent(const gchar *filename, GError **error)
{
	g_autofree gchar *parent = NULL;

	g_return_val_if_fail(filename != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	parent = g_path_get_dirname(filename);
	return fu_common_mkdir(parent, error);
}

/**
 * fu_common_set_contents_bytes:
 * @filename: a filename
 * @bytes: data to write
 * @error: (nullable): optional return location for an error
 *
 * Writes a blob of data to a filename, creating the parent directories as
 * required.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.9.5
 **/
gboolean
fu_common_set_contents_bytes(const gchar *filename, GBytes *bytes, GError **error)
{
	const gchar *data;
	gsize size;
	g_autoptr(GFile) file = NULL;
	g_autoptr(GFile) file_parent = NULL;

	g_return_val_if_fail(filename != NULL, FALSE);
	g_return_val_if_fail(bytes != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	file = g_file_new_for_path(filename);
	file_parent = g_file_get_parent(file);
	if (!g_file_query_exists(file_parent, NULL)) {
		if (!g_file_make_directory_with_parents(file_parent, NULL, error))
			return FALSE;
	}
	data = g_bytes_get_data(bytes, &size);
	g_debug("writing %s with %" G_GSIZE_FORMAT " bytes", filename, size);
	return g_file_set_contents(filename, data, size, error);
}

/**
 * fu_common_get_contents_bytes:
 * @filename: a filename
 * @error: (nullable): optional return location for an error
 *
 * Reads a blob of data from a file.
 *
 * Returns: a #GBytes, or %NULL for failure
 *
 * Since: 0.9.7
 **/
GBytes *
fu_common_get_contents_bytes(const gchar *filename, GError **error)
{
	gchar *data = NULL;
	gsize len = 0;

	g_return_val_if_fail(filename != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	if (!g_file_get_contents(filename, &data, &len, error))
		return NULL;
	g_debug("reading %s with %" G_GSIZE_FORMAT " bytes", filename, len);
	return g_bytes_new_take(data, len);
}

/**
 * fu_common_get_contents_fd:
 * @fd: a file descriptor
 * @count: the maximum number of bytes to read
 * @error: (nullable): optional return location for an error
 *
 * Reads a blob from a specific file descriptor.
 *
 * Note: this will close the fd when done
 *
 * Returns: (transfer full): a #GBytes, or %NULL
 *
 * Since: 0.9.5
 **/
GBytes *
fu_common_get_contents_fd(gint fd, gsize count, GError **error)
{
#ifdef HAVE_GIO_UNIX
	g_autoptr(GInputStream) stream = NULL;

	g_return_val_if_fail(fd > 0, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* read the entire fd to a data blob */
	stream = g_unix_input_stream_new(fd, TRUE);
	return fu_common_get_contents_stream(stream, count, error);
#else
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Not supported as <glib-unix.h> is unavailable");
	return NULL;
#endif
}

/**
 * fu_common_get_contents_stream:
 * @stream: input stream
 * @count: the maximum number of bytes to read
 * @error: (nullable): optional return location for an error
 *
 * Reads a blob from a specific input stream.
 *
 * Returns: (transfer full): a #GBytes, or %NULL
 *
 * Since: 1.7.4
 **/
GBytes *
fu_common_get_contents_stream(GInputStream *stream, gsize count, GError **error)
{
	guint8 tmp[0x8000] = {0x0};
	g_autoptr(GByteArray) buf = g_byte_array_new();
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* this is invalid */
	if (count == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "A maximum read size must be specified");
		return NULL;
	}

	/* read from stream in 32kB chunks */
	while (TRUE) {
		gssize sz;
		sz = g_input_stream_read(stream, tmp, sizeof(tmp), NULL, &error_local);
		if (sz == 0)
			break;
		if (sz < 0) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_FILE,
					    error_local->message);
			return NULL;
		}
		g_byte_array_append(buf, tmp, sz);
		if (buf->len > count) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "cannot read from fd: 0x%x > 0x%x",
				    buf->len,
				    (guint)count);
			return NULL;
		}
	}
	return g_byte_array_free_to_bytes(g_steal_pointer(&buf));
}

#ifdef HAVE_LIBARCHIVE
static gboolean
fu_common_extract_archive_entry(struct archive_entry *entry, const gchar *dir)
{
	const gchar *tmp;
	g_autofree gchar *buf = NULL;

	/* no output file */
	if (archive_entry_pathname(entry) == NULL)
		return FALSE;

	/* update output path */
	tmp = archive_entry_pathname(entry);
	buf = g_build_filename(dir, tmp, NULL);
	archive_entry_update_pathname_utf8(entry, buf);
	return TRUE;
}
#endif

/**
 * fu_common_extract_archive:
 * @blob: data archive as a blob
 * @dir: a directory name to extract to
 * @error: (nullable): optional return location for an error
 *
 * Extracts an archive to a directory.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.9.7
 **/
gboolean
fu_common_extract_archive(GBytes *blob, const gchar *dir, GError **error)
{
#ifdef HAVE_LIBARCHIVE
	gboolean ret = TRUE;
	int r;
	struct archive *arch = NULL;
	struct archive_entry *entry;

	g_return_val_if_fail(blob != NULL, FALSE);
	g_return_val_if_fail(dir != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* decompress anything matching either glob */
	g_debug("decompressing into %s", dir);
	arch = archive_read_new();
	archive_read_support_format_all(arch);
	archive_read_support_filter_all(arch);
	r = archive_read_open_memory(arch,
				     (void *)g_bytes_get_data(blob, NULL),
				     (size_t)g_bytes_get_size(blob));
	if (r != 0) {
		ret = FALSE;
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Cannot open: %s",
			    archive_error_string(arch));
		goto out;
	}
	for (;;) {
		gboolean valid;
		r = archive_read_next_header(arch, &entry);
		if (r == ARCHIVE_EOF)
			break;
		if (r != ARCHIVE_OK) {
			ret = FALSE;
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Cannot read header: %s",
				    archive_error_string(arch));
			goto out;
		}

		/* only extract if valid */
		valid = fu_common_extract_archive_entry(entry, dir);
		if (!valid)
			continue;
		r = archive_read_extract(arch, entry, 0);
		if (r != ARCHIVE_OK) {
			ret = FALSE;
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Cannot extract: %s",
				    archive_error_string(arch));
			goto out;
		}
	}
out:
	if (arch != NULL) {
		archive_read_close(arch);
		archive_read_free(arch);
	}
	return ret;
#else
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "missing libarchive support");
	return FALSE;
#endif
}

static void
fu_common_add_argv(GPtrArray *argv, const gchar *fmt, ...) G_GNUC_PRINTF(2, 3);

static void
fu_common_add_argv(GPtrArray *argv, const gchar *fmt, ...)
{
	va_list args;
	g_autofree gchar *tmp = NULL;
	g_auto(GStrv) split = NULL;

	va_start(args, fmt);
	tmp = g_strdup_vprintf(fmt, args);
	va_end(args);

	split = g_strsplit(tmp, " ", -1);
	for (guint i = 0; split[i] != NULL; i++)
		g_ptr_array_add(argv, g_strdup(split[i]));
}

/**
 * fu_common_find_program_in_path:
 * @basename: the program to search
 * @error: (nullable): optional return location for an error
 *
 * Looks for a program in the PATH variable
 *
 * Returns: a new #gchar, or %NULL for error
 *
 * Since: 1.1.2
 **/
gchar *
fu_common_find_program_in_path(const gchar *basename, GError **error)
{
	gchar *fn = g_find_program_in_path(basename);
	if (fn == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "missing executable %s in PATH",
			    basename);
		return NULL;
	}
	return fn;
}

static gboolean
fu_common_test_namespace_support(GError **error)
{
	/* test if CONFIG_USER_NS is valid */
	if (!g_file_test("/proc/self/ns/user", G_FILE_TEST_IS_SYMLINK)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "missing CONFIG_USER_NS in kernel");
		return FALSE;
	}
	if (g_file_test("/proc/sys/kernel/unprivileged_userns_clone", G_FILE_TEST_EXISTS)) {
		g_autofree gchar *clone = NULL;
		if (!g_file_get_contents("/proc/sys/kernel/unprivileged_userns_clone",
					 &clone,
					 NULL,
					 error))
			return FALSE;
		if (g_ascii_strtoll(clone, NULL, 10) == 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "unprivileged user namespace clones disabled by distro");
			return FALSE;
		}
	}
	return TRUE;
}

/**
 * fu_common_firmware_builder:
 * @bytes: the data to use
 * @script_fn: Name of the script to run in the tarball, e.g. `startup.sh`
 * @output_fn: Name of the generated firmware, e.g. `firmware.bin`
 * @error: (nullable): optional return location for an error
 *
 * Builds a firmware file using tools from the host session in a bubblewrap
 * jail. Several things happen during build:
 *
 * 1. The @bytes data is untarred to a temporary location
 * 2. A bubblewrap container is set up
 * 3. The startup.sh script is run inside the container
 * 4. The firmware.bin is extracted from the container
 * 5. The temporary location is deleted
 *
 * Returns: a new #GBytes, or %NULL for error
 *
 * Since: 0.9.7
 **/
GBytes *
fu_common_firmware_builder(GBytes *bytes,
			   const gchar *script_fn,
			   const gchar *output_fn,
			   GError **error)
{
	gint rc = 0;
	g_autofree gchar *argv_str = NULL;
	g_autofree gchar *bwrap_fn = NULL;
	g_autofree gchar *localstatebuilderdir = NULL;
	g_autofree gchar *localstatedir = NULL;
	g_autofree gchar *output2_fn = NULL;
	g_autofree gchar *standard_error = NULL;
	g_autofree gchar *standard_output = NULL;
	g_autofree gchar *tmpdir = NULL;
	g_autoptr(GBytes) firmware_blob = NULL;
	g_autoptr(GPtrArray) argv = g_ptr_array_new_with_free_func(g_free);

	g_return_val_if_fail(bytes != NULL, NULL);
	g_return_val_if_fail(script_fn != NULL, NULL);
	g_return_val_if_fail(output_fn != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* find bwrap in the path */
	bwrap_fn = fu_common_find_program_in_path("bwrap", error);
	if (bwrap_fn == NULL)
		return NULL;

	/* test if CONFIG_USER_NS is valid */
	if (!fu_common_test_namespace_support(error))
		return NULL;

	/* untar file to temp location */
	tmpdir = g_dir_make_tmp("fwupd-gen-XXXXXX", error);
	if (tmpdir == NULL)
		return NULL;
	if (!fu_common_extract_archive(bytes, tmpdir, error))
		return NULL;

	/* this is shared with the plugins */
	localstatedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR_PKG);
	localstatebuilderdir = g_build_filename(localstatedir, "builder", NULL);

	/* launch bubblewrap and generate firmware */
	g_ptr_array_add(argv, g_steal_pointer(&bwrap_fn));
	fu_common_add_argv(argv, "--die-with-parent");
	fu_common_add_argv(argv, "--ro-bind /usr /usr");
	fu_common_add_argv(argv, "--ro-bind /lib /lib");
	fu_common_add_argv(argv, "--ro-bind-try /lib64 /lib64");
	fu_common_add_argv(argv, "--ro-bind /bin /bin");
	fu_common_add_argv(argv, "--ro-bind /sbin /sbin");
	fu_common_add_argv(argv, "--dir /tmp");
	fu_common_add_argv(argv, "--dir /var");
	fu_common_add_argv(argv, "--bind %s /tmp", tmpdir);
	if (g_file_test(localstatebuilderdir, G_FILE_TEST_EXISTS))
		fu_common_add_argv(argv, "--ro-bind %s /boot", localstatebuilderdir);
	fu_common_add_argv(argv, "--dev /dev");
	fu_common_add_argv(argv, "--chdir /tmp");
	fu_common_add_argv(argv, "--unshare-all");
	fu_common_add_argv(argv, "/tmp/%s", script_fn);
	g_ptr_array_add(argv, NULL);
	argv_str = g_strjoinv(" ", (gchar **)argv->pdata);
	g_debug("running '%s' in %s", argv_str, tmpdir);
	if (!g_spawn_sync("/tmp",
			  (gchar **)argv->pdata,
			  NULL,
			  G_SPAWN_SEARCH_PATH,
			  NULL,
			  NULL, /* child_setup */
			  &standard_output,
			  &standard_error,
			  &rc,
			  error)) {
		g_prefix_error(error, "failed to run '%s': ", argv_str);
		return NULL;
	}
	if (standard_output != NULL && standard_output[0] != '\0')
		g_debug("console output was: %s", standard_output);
	if (rc != 0) {
		FwupdError code = FWUPD_ERROR_INTERNAL;
		if (errno == ENOTTY)
			code = FWUPD_ERROR_PERMISSION_DENIED;
		g_set_error(error,
			    FWUPD_ERROR,
			    code,
			    "failed to build firmware: %s",
			    standard_error);
		return NULL;
	}

	/* get generated file */
	output2_fn = g_build_filename(tmpdir, output_fn, NULL);
	firmware_blob = fu_common_get_contents_bytes(output2_fn, error);
	if (firmware_blob == NULL)
		return NULL;

	/* cleanup temp directory */
	if (!fu_common_rmtree(tmpdir, error))
		return NULL;

	/* success */
	return g_steal_pointer(&firmware_blob);
}

typedef struct {
	FuOutputHandler handler_cb;
	gpointer handler_user_data;
	GMainLoop *loop;
	GSource *source;
	GInputStream *stream;
	GCancellable *cancellable;
	guint timeout_id;
} FuCommonSpawnHelper;

static void
fu_common_spawn_create_pollable_source(FuCommonSpawnHelper *helper);

static gboolean
fu_common_spawn_source_pollable_cb(GObject *stream, gpointer user_data)
{
	FuCommonSpawnHelper *helper = (FuCommonSpawnHelper *)user_data;
	gchar buffer[1024];
	gssize sz;
	g_auto(GStrv) split = NULL;
	g_autoptr(GError) error = NULL;

	/* read from stream */
	sz = g_pollable_input_stream_read_nonblocking(G_POLLABLE_INPUT_STREAM(stream),
						      buffer,
						      sizeof(buffer) - 1,
						      NULL,
						      &error);
	if (sz < 0) {
		if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
			g_warning("failed to get read from nonblocking fd: %s", error->message);
		}
		return G_SOURCE_REMOVE;
	}

	/* no read possible */
	if (sz == 0)
		g_main_loop_quit(helper->loop);

	/* emit lines */
	if (helper->handler_cb != NULL) {
		buffer[sz] = '\0';
		split = g_strsplit(buffer, "\n", -1);
		for (guint i = 0; split[i] != NULL; i++) {
			if (split[i][0] == '\0')
				continue;
			helper->handler_cb(split[i], helper->handler_user_data);
		}
	}

	/* set up the source for the next read */
	fu_common_spawn_create_pollable_source(helper);
	return G_SOURCE_REMOVE;
}

static void
fu_common_spawn_create_pollable_source(FuCommonSpawnHelper *helper)
{
	if (helper->source != NULL)
		g_source_destroy(helper->source);
	helper->source =
	    g_pollable_input_stream_create_source(G_POLLABLE_INPUT_STREAM(helper->stream),
						  helper->cancellable);
	g_source_attach(helper->source, NULL);
	g_source_set_callback(helper->source,
			      (GSourceFunc)fu_common_spawn_source_pollable_cb,
			      helper,
			      NULL);
}

static void
fu_common_spawn_helper_free(FuCommonSpawnHelper *helper)
{
	g_object_unref(helper->cancellable);
	if (helper->stream != NULL)
		g_object_unref(helper->stream);
	if (helper->source != NULL)
		g_source_destroy(helper->source);
	if (helper->loop != NULL)
		g_main_loop_unref(helper->loop);
	if (helper->timeout_id != 0)
		g_source_remove(helper->timeout_id);
	g_free(helper);
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCommonSpawnHelper, fu_common_spawn_helper_free)
#pragma clang diagnostic pop

#ifndef _WIN32
static gboolean
fu_common_spawn_timeout_cb(gpointer user_data)
{
	FuCommonSpawnHelper *helper = (FuCommonSpawnHelper *)user_data;
	g_cancellable_cancel(helper->cancellable);
	g_main_loop_quit(helper->loop);
	helper->timeout_id = 0;
	return G_SOURCE_REMOVE;
}

static void
fu_common_spawn_cancelled_cb(GCancellable *cancellable, FuCommonSpawnHelper *helper)
{
	/* just propagate */
	g_cancellable_cancel(helper->cancellable);
}
#endif

/**
 * fu_common_spawn_sync:
 * @argv: the argument list to run
 * @handler_cb: (scope call) (nullable): optional #FuOutputHandler
 * @handler_user_data: (nullable): the user data to pass to @handler_cb
 * @timeout_ms: a timeout in ms, or 0 for no limit
 * @cancellable: (nullable): optional #GCancellable
 * @error: (nullable): optional return location for an error
 *
 * Runs a subprocess and waits for it to exit. Any output on standard out or
 * standard error will be forwarded to @handler_cb as whole lines.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.9.7
 **/
gboolean
fu_common_spawn_sync(const gchar *const *argv,
		     FuOutputHandler handler_cb,
		     gpointer handler_user_data,
		     guint timeout_ms,
		     GCancellable *cancellable,
		     GError **error)
{
	g_autoptr(FuCommonSpawnHelper) helper = NULL;
	g_autoptr(GSubprocess) subprocess = NULL;
	g_autofree gchar *argv_str = NULL;
#ifndef _WIN32
	gulong cancellable_id = 0;
#endif

	g_return_val_if_fail(argv != NULL, FALSE);
	g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* create subprocess */
	argv_str = g_strjoinv(" ", (gchar **)argv);
	g_debug("running '%s'", argv_str);
	subprocess =
	    g_subprocess_newv(argv,
			      G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_MERGE,
			      error);
	if (subprocess == NULL)
		return FALSE;

#ifndef _WIN32
	/* watch for process to exit */
	helper = g_new0(FuCommonSpawnHelper, 1);
	helper->handler_cb = handler_cb;
	helper->handler_user_data = handler_user_data;
	helper->loop = g_main_loop_new(NULL, FALSE);
	helper->stream = g_subprocess_get_stdout_pipe(subprocess);

	/* always create a cancellable, and connect up the parent */
	helper->cancellable = g_cancellable_new();
	if (cancellable != NULL) {
		cancellable_id = g_cancellable_connect(cancellable,
						       G_CALLBACK(fu_common_spawn_cancelled_cb),
						       helper,
						       NULL);
	}

	/* allow timeout */
	if (timeout_ms > 0) {
		helper->timeout_id = g_timeout_add(timeout_ms, fu_common_spawn_timeout_cb, helper);
	}
	fu_common_spawn_create_pollable_source(helper);
	g_main_loop_run(helper->loop);
	g_cancellable_disconnect(cancellable, cancellable_id);
	if (g_cancellable_set_error_if_cancelled(helper->cancellable, error))
		return FALSE;
#endif
	return g_subprocess_wait_check(subprocess, cancellable, error);
}

/**
 * fu_common_write_uint16:
 * @buf: a writable buffer
 * @val_native: a value in host byte-order
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Writes a value to a buffer using a specified endian.
 *
 * Since: 1.0.3
 **/
void
fu_common_write_uint16(guint8 *buf, guint16 val_native, FuEndianType endian)
{
	guint16 val_hw;
	switch (endian) {
	case G_BIG_ENDIAN:
		val_hw = GUINT16_TO_BE(val_native);
		break;
	case G_LITTLE_ENDIAN:
		val_hw = GUINT16_TO_LE(val_native);
		break;
	default:
		g_assert_not_reached();
	}
	memcpy(buf, &val_hw, sizeof(val_hw));
}

/**
 * fu_common_write_uint32:
 * @buf: a writable buffer
 * @val_native: a value in host byte-order
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Writes a value to a buffer using a specified endian.
 *
 * Since: 1.0.3
 **/
void
fu_common_write_uint32(guint8 *buf, guint32 val_native, FuEndianType endian)
{
	guint32 val_hw;
	switch (endian) {
	case G_BIG_ENDIAN:
		val_hw = GUINT32_TO_BE(val_native);
		break;
	case G_LITTLE_ENDIAN:
		val_hw = GUINT32_TO_LE(val_native);
		break;
	default:
		g_assert_not_reached();
	}
	memcpy(buf, &val_hw, sizeof(val_hw));
}

/**
 * fu_common_write_uint64:
 * @buf: a writable buffer
 * @val_native: a value in host byte-order
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Writes a value to a buffer using a specified endian.
 *
 * Since: 1.5.8
 **/
void
fu_common_write_uint64(guint8 *buf, guint64 val_native, FuEndianType endian)
{
	guint64 val_hw;
	switch (endian) {
	case G_BIG_ENDIAN:
		val_hw = GUINT64_TO_BE(val_native);
		break;
	case G_LITTLE_ENDIAN:
		val_hw = GUINT64_TO_LE(val_native);
		break;
	default:
		g_assert_not_reached();
	}
	memcpy(buf, &val_hw, sizeof(val_hw));
}

/**
 * fu_common_read_uint16:
 * @buf: a readable buffer
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Read a value from a buffer using a specified endian.
 *
 * Returns: a value in host byte-order
 *
 * Since: 1.0.3
 **/
guint16
fu_common_read_uint16(const guint8 *buf, FuEndianType endian)
{
	guint16 val_hw, val_native;
	memcpy(&val_hw, buf, sizeof(val_hw));
	switch (endian) {
	case G_BIG_ENDIAN:
		val_native = GUINT16_FROM_BE(val_hw);
		break;
	case G_LITTLE_ENDIAN:
		val_native = GUINT16_FROM_LE(val_hw);
		break;
	default:
		g_assert_not_reached();
	}
	return val_native;
}

/**
 * fu_common_read_uint32:
 * @buf: a readable buffer
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Read a value from a buffer using a specified endian.
 *
 * Returns: a value in host byte-order
 *
 * Since: 1.0.3
 **/
guint32
fu_common_read_uint32(const guint8 *buf, FuEndianType endian)
{
	guint32 val_hw, val_native;
	memcpy(&val_hw, buf, sizeof(val_hw));
	switch (endian) {
	case G_BIG_ENDIAN:
		val_native = GUINT32_FROM_BE(val_hw);
		break;
	case G_LITTLE_ENDIAN:
		val_native = GUINT32_FROM_LE(val_hw);
		break;
	default:
		g_assert_not_reached();
	}
	return val_native;
}

/**
 * fu_common_read_uint64:
 * @buf: a readable buffer
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Read a value from a buffer using a specified endian.
 *
 * Returns: a value in host byte-order
 *
 * Since: 1.5.8
 **/
guint64
fu_common_read_uint64(const guint8 *buf, FuEndianType endian)
{
	guint64 val_hw, val_native;
	memcpy(&val_hw, buf, sizeof(val_hw));
	switch (endian) {
	case G_BIG_ENDIAN:
		val_native = GUINT64_FROM_BE(val_hw);
		break;
	case G_LITTLE_ENDIAN:
		val_native = GUINT64_FROM_LE(val_hw);
		break;
	default:
		g_assert_not_reached();
	}
	return val_native;
}

/**
 * fu_common_strtoull:
 * @str: a string, e.g. `0x1234`
 *
 * Converts a string value to an integer. Values are assumed base 10, unless
 * prefixed with "0x" where they are parsed as base 16.
 *
 * Returns: integer value, or 0x0 for error
 *
 * Since: 1.1.2
 **/
guint64
fu_common_strtoull(const gchar *str)
{
	guint base = 10;
	if (str == NULL)
		return 0x0;
	if (g_str_has_prefix(str, "0x")) {
		str += 2;
		base = 16;
	}
	return g_ascii_strtoull(str, NULL, base);
}

/**
 * fu_common_strtoull_full:
 * @str: a string, e.g. `0x1234`
 * @value: (out) (nullable): parsed value
 * @min: minimum acceptable value, typically 0
 * @max: maximum acceptable value, typically G_MAXUINT64
 * @error: (nullable): optional return location for an error
 *
 * Converts a string value to an integer. Values are assumed base 10, unless
 * prefixed with "0x" where they are parsed as base 16.
 *
 * Returns: %TRUE if the value was parsed correctly, or %FALSE for error
 *
 * Since: 1.7.3
 **/
gboolean
fu_common_strtoull_full(const gchar *str, guint64 *value, guint64 min, guint64 max, GError **error)
{
	gchar *endptr = NULL;
	guint64 value_tmp;
	guint base = 10;

	/* sanity check */
	if (str == NULL) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "cannot parse NULL");
		return FALSE;
	}

	/* detect hex */
	if (g_str_has_prefix(str, "0x")) {
		str += 2;
		base = 16;
	}

	/* convert */
	value_tmp = g_ascii_strtoull(str, &endptr, base);
	if ((gsize)(endptr - str) != strlen(str)) {
		g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "cannot parse %s", str);
		return FALSE;
	}

	/* overflow check */
	if (value_tmp == G_MAXUINT64) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "cannot parse %s as caused overflow",
			    str);
		return FALSE;
	}

	/* range check */
	if (value_tmp < min) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "value %" G_GUINT64_FORMAT " was below minimum %" G_GUINT64_FORMAT,
			    value_tmp,
			    min);
		return FALSE;
	}
	if (value_tmp > max) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "value %" G_GUINT64_FORMAT " was above maximum %" G_GUINT64_FORMAT,
			    value_tmp,
			    max);
		return FALSE;
	}

	/* success */
	if (value != NULL)
		*value = value_tmp;
	return TRUE;
}

/**
 * fu_common_strstrip:
 * @str: a string, e.g. ` test `
 *
 * Removes leading and trailing whitespace from a constant string.
 *
 * Returns: newly allocated string
 *
 * Since: 1.1.2
 **/
gchar *
fu_common_strstrip(const gchar *str)
{
	guint head = G_MAXUINT;
	guint tail = 0;

	g_return_val_if_fail(str != NULL, NULL);

	/* find first non-space char */
	for (guint i = 0; str[i] != '\0'; i++) {
		if (str[i] != ' ') {
			head = i;
			break;
		}
	}
	if (head == G_MAXUINT)
		return g_strdup("");

	/* find last non-space char */
	for (guint i = head; str[i] != '\0'; i++) {
		if (!g_ascii_isspace(str[i]))
			tail = i;
	}
	return g_strndup(str + head, tail - head + 1);
}

static const GError *
fu_common_error_array_find(GPtrArray *errors, FwupdError error_code)
{
	for (guint j = 0; j < errors->len; j++) {
		const GError *error = g_ptr_array_index(errors, j);
		if (g_error_matches(error, FWUPD_ERROR, error_code))
			return error;
	}
	return NULL;
}

static guint
fu_common_error_array_count(GPtrArray *errors, FwupdError error_code)
{
	guint cnt = 0;
	for (guint j = 0; j < errors->len; j++) {
		const GError *error = g_ptr_array_index(errors, j);
		if (g_error_matches(error, FWUPD_ERROR, error_code))
			cnt++;
	}
	return cnt;
}

static gboolean
fu_common_error_array_matches_any(GPtrArray *errors, FwupdError *error_codes)
{
	for (guint j = 0; j < errors->len; j++) {
		const GError *error = g_ptr_array_index(errors, j);
		gboolean matches_any = FALSE;
		for (guint i = 0; error_codes[i] != FWUPD_ERROR_LAST; i++) {
			if (g_error_matches(error, FWUPD_ERROR, error_codes[i])) {
				matches_any = TRUE;
				break;
			}
		}
		if (!matches_any)
			return FALSE;
	}
	return TRUE;
}

/**
 * fu_common_error_array_get_best:
 * @errors: (element-type GError): array of errors
 *
 * Finds the 'best' error to show the user from a array of errors, creating a
 * completely bespoke error where required.
 *
 * Returns: (transfer full): a #GError, never %NULL
 *
 * Since: 1.0.8
 **/
GError *
fu_common_error_array_get_best(GPtrArray *errors)
{
	FwupdError err_prio[] = {FWUPD_ERROR_INVALID_FILE,
				 FWUPD_ERROR_VERSION_SAME,
				 FWUPD_ERROR_VERSION_NEWER,
				 FWUPD_ERROR_NOT_SUPPORTED,
				 FWUPD_ERROR_INTERNAL,
				 FWUPD_ERROR_NOT_FOUND,
				 FWUPD_ERROR_LAST};
	FwupdError err_all_uptodate[] = {FWUPD_ERROR_VERSION_SAME,
					 FWUPD_ERROR_NOT_FOUND,
					 FWUPD_ERROR_NOT_SUPPORTED,
					 FWUPD_ERROR_LAST};
	FwupdError err_all_newer[] = {FWUPD_ERROR_VERSION_NEWER,
				      FWUPD_ERROR_VERSION_SAME,
				      FWUPD_ERROR_NOT_FOUND,
				      FWUPD_ERROR_NOT_SUPPORTED,
				      FWUPD_ERROR_LAST};

	/* are all the errors either GUID-not-matched or version-same? */
	if (fu_common_error_array_count(errors, FWUPD_ERROR_VERSION_SAME) > 1 &&
	    fu_common_error_array_matches_any(errors, err_all_uptodate)) {
		return g_error_new(FWUPD_ERROR,
				   FWUPD_ERROR_NOTHING_TO_DO,
				   "All updatable firmware is already installed");
	}

	/* are all the errors either GUID-not-matched or version same or newer? */
	if (fu_common_error_array_count(errors, FWUPD_ERROR_VERSION_NEWER) > 1 &&
	    fu_common_error_array_matches_any(errors, err_all_newer)) {
		return g_error_new(FWUPD_ERROR,
				   FWUPD_ERROR_NOTHING_TO_DO,
				   "All updatable devices already have newer versions");
	}

	/* get the most important single error */
	for (guint i = 0; err_prio[i] != FWUPD_ERROR_LAST; i++) {
		const GError *error_tmp = fu_common_error_array_find(errors, err_prio[i]);
		if (error_tmp != NULL)
			return g_error_copy(error_tmp);
	}

	/* fall back to something */
	return g_error_new(FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No supported devices found");
}

/**
 * fu_common_get_win32_basedir:
 *
 * Gets the base directory that fwupd has been launched from on Windows.
 * This is the directory containing all subdirectories (IE 'C:\Program Files (x86)\fwupd\')
 *
 * Returns: The system path, or %NULL if invalid
 *
 * Since: 1.7.4
 **/
static gchar *
fu_common_get_win32_basedir(void)
{
#ifdef _WIN32
	char drive_buf[_MAX_DRIVE];
	char dir_buf[_MAX_DIR];
	_splitpath(_pgmptr, drive_buf, dir_buf, NULL, NULL);
	return g_build_filename(drive_buf, dir_buf, "..", NULL);
#endif
	return NULL;
}

/**
 * fu_common_get_path:
 * @path_kind: a #FuPathKind e.g. %FU_PATH_KIND_DATADIR_PKG
 *
 * Gets a fwupd-specific system path. These can be overridden with various
 * environment variables, for instance %FWUPD_DATADIR.
 *
 * Returns: a system path, or %NULL if invalid
 *
 * Since: 1.0.8
 **/
gchar *
fu_common_get_path(FuPathKind path_kind)
{
	const gchar *tmp;
	g_autofree gchar *basedir = NULL;

	switch (path_kind) {
	/* /var */
	case FU_PATH_KIND_LOCALSTATEDIR:
		tmp = g_getenv("FWUPD_LOCALSTATEDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
#ifdef _WIN32
		return g_build_filename(g_getenv("USERPROFILE"),
					PACKAGE_NAME,
					FWUPD_LOCALSTATEDIR,
					NULL);
#else
		tmp = g_getenv("SNAP_USER_DATA");
		if (tmp != NULL)
			return g_build_filename(tmp, FWUPD_LOCALSTATEDIR, NULL);
		return g_build_filename(FWUPD_LOCALSTATEDIR, NULL);
#endif
	/* /proc */
	case FU_PATH_KIND_PROCFS:
		tmp = g_getenv("FWUPD_PROCFS");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/proc");
	/* /sys/firmware */
	case FU_PATH_KIND_SYSFSDIR_FW:
		tmp = g_getenv("FWUPD_SYSFSFWDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/firmware");
	/* /sys/class/tpm */
	case FU_PATH_KIND_SYSFSDIR_TPM:
		tmp = g_getenv("FWUPD_SYSFSTPMDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/class/tpm");
	/* /sys/bus/platform/drivers */
	case FU_PATH_KIND_SYSFSDIR_DRIVERS:
		tmp = g_getenv("FWUPD_SYSFSDRIVERDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/bus/platform/drivers");
	/* /sys/kernel/security */
	case FU_PATH_KIND_SYSFSDIR_SECURITY:
		tmp = g_getenv("FWUPD_SYSFSSECURITYDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/kernel/security");
	/* /sys/firmware/acpi/tables */
	case FU_PATH_KIND_ACPI_TABLES:
		tmp = g_getenv("FWUPD_ACPITABLESDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/firmware/acpi/tables");
	/* /sys/module/firmware_class/parameters/path */
	case FU_PATH_KIND_FIRMWARE_SEARCH:
		tmp = g_getenv("FWUPD_FIRMWARESEARCH");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/module/firmware_class/parameters/path");
	/* /etc */
	case FU_PATH_KIND_SYSCONFDIR:
		tmp = g_getenv("FWUPD_SYSCONFDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		tmp = g_getenv("SNAP_USER_DATA");
		if (tmp != NULL)
			return g_build_filename(tmp, FWUPD_SYSCONFDIR, NULL);
		basedir = fu_common_get_win32_basedir();
		if (basedir != NULL)
			return g_build_filename(basedir, FWUPD_SYSCONFDIR, NULL);
		return g_strdup(FWUPD_SYSCONFDIR);

	/* /usr/lib/<triplet>/fwupd-plugins-3 */
	case FU_PATH_KIND_PLUGINDIR_PKG:
		tmp = g_getenv("FWUPD_PLUGINDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		tmp = g_getenv("SNAP");
		if (tmp != NULL)
			return g_build_filename(tmp, FWUPD_PLUGINDIR, NULL);
		basedir = fu_common_get_win32_basedir();
		if (basedir != NULL)
			return g_build_filename(basedir, FWUPD_PLUGINDIR, NULL);
		return g_build_filename(FWUPD_PLUGINDIR, NULL);
	/* /usr/share/fwupd */
	case FU_PATH_KIND_DATADIR_PKG:
		tmp = g_getenv("FWUPD_DATADIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		tmp = g_getenv("SNAP");
		if (tmp != NULL)
			return g_build_filename(tmp, FWUPD_DATADIR, PACKAGE_NAME, NULL);
		basedir = fu_common_get_win32_basedir();
		if (basedir != NULL)
			return g_build_filename(basedir, FWUPD_DATADIR, PACKAGE_NAME, NULL);
		return g_build_filename(FWUPD_DATADIR, PACKAGE_NAME, NULL);
	/* /usr/share/fwupd/quirks.d */
	case FU_PATH_KIND_DATADIR_QUIRKS:
		tmp = g_getenv("FWUPD_DATADIR_QUIRKS");
		if (tmp != NULL)
			return g_strdup(tmp);
		basedir = fu_common_get_path(FU_PATH_KIND_DATADIR_PKG);
		return g_build_filename(basedir, "quirks.d", NULL);
	/* /usr/libexec/fwupd/efi */
	case FU_PATH_KIND_EFIAPPDIR:
		tmp = g_getenv("FWUPD_EFIAPPDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
#ifdef EFI_APP_LOCATION
		tmp = g_getenv("SNAP");
		if (tmp != NULL)
			return g_build_filename(tmp, EFI_APP_LOCATION, NULL);
		return g_strdup(EFI_APP_LOCATION);
#else
		return NULL;
#endif
	/* /etc/fwupd */
	case FU_PATH_KIND_SYSCONFDIR_PKG:
		tmp = g_getenv("CONFIGURATION_DIRECTORY");
		if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS))
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_SYSCONFDIR);
		return g_build_filename(basedir, PACKAGE_NAME, NULL);
	/* /var/lib/fwupd */
	case FU_PATH_KIND_LOCALSTATEDIR_PKG:
		tmp = g_getenv("STATE_DIRECTORY");
		if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS))
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR);
		return g_build_filename(basedir, "lib", PACKAGE_NAME, NULL);
	/* /var/lib/fwupd/quirks.d */
	case FU_PATH_KIND_LOCALSTATEDIR_QUIRKS:
		tmp = g_getenv("FWUPD_LOCALSTATEDIR_QUIRKS");
		if (tmp != NULL)
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR_PKG);
		return g_build_filename(basedir, "quirks.d", NULL);
	/* /var/lib/fwupd/metadata */
	case FU_PATH_KIND_LOCALSTATEDIR_METADATA:
		tmp = g_getenv("FWUPD_LOCALSTATEDIR_METADATA");
		if (tmp != NULL)
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR_PKG);
		return g_build_filename(basedir, "metadata", NULL);
	/* /var/lib/fwupd/remotes.d */
	case FU_PATH_KIND_LOCALSTATEDIR_REMOTES:
		tmp = g_getenv("FWUPD_LOCALSTATEDIR_REMOTES");
		if (tmp != NULL)
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR_PKG);
		return g_build_filename(basedir, "remotes.d", NULL);
	/* /var/cache/fwupd */
	case FU_PATH_KIND_CACHEDIR_PKG:
		tmp = g_getenv("CACHE_DIRECTORY");
		if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS))
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR);
		return g_build_filename(basedir, "cache", PACKAGE_NAME, NULL);
	/* /var/etc/fwupd */
	case FU_PATH_KIND_LOCALCONFDIR_PKG:
		tmp = g_getenv("LOCALCONF_DIRECTORY");
		if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS))
			return g_build_filename(tmp, NULL);
		basedir = fu_common_get_path(FU_PATH_KIND_LOCALSTATEDIR);
		return g_build_filename(basedir, "etc", PACKAGE_NAME, NULL);
	/* /run/lock */
	case FU_PATH_KIND_LOCKDIR:
		return g_strdup("/run/lock");
	/* /sys/class/firmware-attributes */
	case FU_PATH_KIND_SYSFSDIR_FW_ATTRIB:
		tmp = g_getenv("FWUPD_SYSFSFWATTRIBDIR");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/sys/class/firmware-attributes");
	case FU_PATH_KIND_OFFLINE_TRIGGER:
		tmp = g_getenv("FWUPD_OFFLINE_TRIGGER");
		if (tmp != NULL)
			return g_strdup(tmp);
		return g_strdup("/system-update");
	case FU_PATH_KIND_POLKIT_ACTIONS:
#ifdef POLKIT_ACTIONDIR
		return g_strdup(POLKIT_ACTIONDIR);
#else
		return NULL;
#endif
	/* C:\Program Files (x86)\fwupd\ */
	case FU_PATH_KIND_WIN32_BASEDIR:
		return fu_common_get_win32_basedir();
	/* this shouldn't happen */
	default:
		g_warning("cannot build path for unknown kind %u", path_kind);
	}

	return NULL;
}

/**
 * fu_common_string_replace:
 * @string: the #GString to operate on
 * @search: the text to search for
 * @replace: the text to use for substitutions
 *
 * Performs multiple search and replace operations on the given string.
 *
 * Returns: the number of replacements done, or 0 if @search is not found.
 *
 * Since: 1.2.0
 **/
guint
fu_common_string_replace(GString *string, const gchar *search, const gchar *replace)
{
	gchar *tmp;
	guint count = 0;
	gsize search_idx = 0;
	gsize replace_len;
	gsize search_len;

	g_return_val_if_fail(string != NULL, 0);
	g_return_val_if_fail(search != NULL, 0);
	g_return_val_if_fail(replace != NULL, 0);

	/* nothing to do */
	if (string->len == 0)
		return 0;

	search_len = strlen(search);
	replace_len = strlen(replace);

	do {
		tmp = g_strstr_len(string->str + search_idx, -1, search);
		if (tmp == NULL)
			break;

		/* advance the counter in case @replace contains @search */
		search_idx = (gsize)(tmp - string->str);

		/* reallocate the string if required */
		if (search_len > replace_len) {
			g_string_erase(string,
				       (gssize)search_idx,
				       (gssize)(search_len - replace_len));
			memcpy(tmp, replace, replace_len);
		} else if (search_len < replace_len) {
			g_string_insert_len(string,
					    (gssize)search_idx,
					    replace,
					    (gssize)(replace_len - search_len));
			/* we have to treat this specially as it could have
			 * been reallocated when the insertion happened */
			memcpy(string->str + search_idx, replace, replace_len);
		} else {
			/* just memcmp in the new string */
			memcpy(tmp, replace, replace_len);
		}
		search_idx += replace_len;
		count++;
	} while (TRUE);

	return count;
}

/**
 * fu_common_strwidth:
 * @text: the string to operate on
 *
 * Returns the width of the string in displayed characters on the console.
 *
 * Returns: width of text
 *
 * Since: 1.3.2
 **/
gsize
fu_common_strwidth(const gchar *text)
{
	const gchar *p = text;
	gsize width = 0;

	g_return_val_if_fail(text != NULL, 0);

	while (*p) {
		gunichar c = g_utf8_get_char(p);
		if (g_unichar_iswide(c))
			width += 2;
		else if (!g_unichar_iszerowidth(c))
			width += 1;
		p = g_utf8_next_char(p);
	}
	return width;
}

/**
 * fu_common_string_append_kv:
 * @str: a #GString
 * @idt: the indent
 * @key: a string to append
 * @value: a string to append
 *
 * Appends a key and string value to a string
 *
 * Since: 1.2.4
 */
void
fu_common_string_append_kv(GString *str, guint idt, const gchar *key, const gchar *value)
{
	const guint align = 24;
	gsize keysz;

	g_return_if_fail(idt * 2 < align);

	/* ignore */
	if (key == NULL)
		return;
	for (gsize i = 0; i < idt; i++)
		g_string_append(str, "  ");
	if (key[0] != '\0') {
		g_string_append_printf(str, "%s:", key);
		keysz = (idt * 2) + fu_common_strwidth(key) + 1;
	} else {
		keysz = idt * 2;
	}
	if (value != NULL) {
		g_auto(GStrv) split = NULL;
		split = g_strsplit(value, "\n", -1);
		for (guint i = 0; split[i] != NULL; i++) {
			if (i == 0) {
				for (gsize j = keysz; j < align; j++)
					g_string_append(str, " ");
			} else {
				g_string_append(str, "\n");
				for (gsize j = 0; j < idt; j++)
					g_string_append(str, "  ");
			}
			g_string_append(str, split[i]);
		}
	}
	g_string_append(str, "\n");
}

/**
 * fu_common_string_append_ku:
 * @str: a #GString
 * @idt: the indent
 * @key: a string to append
 * @value: guint64
 *
 * Appends a key and unsigned integer to a string
 *
 * Since: 1.2.4
 */
void
fu_common_string_append_ku(GString *str, guint idt, const gchar *key, guint64 value)
{
	g_autofree gchar *tmp = g_strdup_printf("%" G_GUINT64_FORMAT, value);
	fu_common_string_append_kv(str, idt, key, tmp);
}

/**
 * fu_common_string_append_kx:
 * @str: a #GString
 * @idt: the indent
 * @key: a string to append
 * @value: guint64
 *
 * Appends a key and hex integer to a string
 *
 * Since: 1.2.4
 */
void
fu_common_string_append_kx(GString *str, guint idt, const gchar *key, guint64 value)
{
	g_autofree gchar *tmp = g_strdup_printf("0x%x", (guint)value);
	fu_common_string_append_kv(str, idt, key, tmp);
}

/**
 * fu_common_string_append_kb:
 * @str: a #GString
 * @idt: the indent
 * @key: a string to append
 * @value: Boolean
 *
 * Appends a key and boolean value to a string
 *
 * Since: 1.2.4
 */
void
fu_common_string_append_kb(GString *str, guint idt, const gchar *key, gboolean value)
{
	fu_common_string_append_kv(str, idt, key, value ? "true" : "false");
}

/**
 * fu_common_dump_full:
 * @log_domain: (nullable): optional log domain, typically %G_LOG_DOMAIN
 * @title: (nullable): optional prefix title
 * @data: buffer to print
 * @len: the size of @data
 * @columns: break new lines after this many bytes
 * @flags: dump flags, e.g. %FU_DUMP_FLAGS_SHOW_ASCII
 *
 * Dumps a raw buffer to the screen.
 *
 * Since: 1.2.4
 **/
void
fu_common_dump_full(const gchar *log_domain,
		    const gchar *title,
		    const guint8 *data,
		    gsize len,
		    guint columns,
		    FuDumpFlags flags)
{
	g_autoptr(GString) str = g_string_new(NULL);

	/* optional */
	if (title != NULL)
		g_string_append_printf(str, "%s:", title);

	/* if more than can fit on one line then start afresh */
	if (len > columns || flags & FU_DUMP_FLAGS_SHOW_ADDRESSES) {
		g_string_append(str, "\n");
	} else {
		for (gsize i = str->len; i < 16; i++)
			g_string_append(str, " ");
	}

	/* offset line */
	if (flags & FU_DUMP_FLAGS_SHOW_ADDRESSES) {
		g_string_append(str, "       │ ");
		for (gsize i = 0; i < columns; i++) {
			g_string_append_printf(str, "%02x ", (guint)i);
			if (flags & FU_DUMP_FLAGS_SHOW_ASCII)
				g_string_append(str, "    ");
		}
		g_string_append(str, "\n───────┼");
		for (gsize i = 0; i < columns; i++) {
			g_string_append(str, "───");
			if (flags & FU_DUMP_FLAGS_SHOW_ASCII)
				g_string_append(str, "────");
		}
		g_string_append_printf(str, "\n0x%04x │ ", (guint)0);
	}

	/* print each row */
	for (gsize i = 0; i < len; i++) {
		g_string_append_printf(str, "%02x ", data[i]);

		/* optionally print ASCII char */
		if (flags & FU_DUMP_FLAGS_SHOW_ASCII) {
			if (g_ascii_isprint(data[i]))
				g_string_append_printf(str, "[%c] ", data[i]);
			else
				g_string_append(str, "[?] ");
		}

		/* new row required */
		if (i > 0 && i != len - 1 && (i + 1) % columns == 0) {
			g_string_append(str, "\n");
			if (flags & FU_DUMP_FLAGS_SHOW_ADDRESSES)
				g_string_append_printf(str, "0x%04x │ ", (guint)i + 1);
		}
	}
	g_log(log_domain, G_LOG_LEVEL_DEBUG, "%s", str->str);
}

/**
 * fu_common_dump_raw:
 * @log_domain: (nullable): optional log domain, typically %G_LOG_DOMAIN
 * @title: (nullable): optional prefix title
 * @data: buffer to print
 * @len: the size of @data
 *
 * Dumps a raw buffer to the screen.
 *
 * Since: 1.2.2
 **/
void
fu_common_dump_raw(const gchar *log_domain, const gchar *title, const guint8 *data, gsize len)
{
	FuDumpFlags flags = FU_DUMP_FLAGS_NONE;
	if (len > 64)
		flags |= FU_DUMP_FLAGS_SHOW_ADDRESSES;
	fu_common_dump_full(log_domain, title, data, len, 32, flags);
}

/**
 * fu_common_dump_bytes:
 * @log_domain: (nullable): optional log domain, typically %G_LOG_DOMAIN
 * @title: (nullable): optional prefix title
 * @bytes: data blob
 *
 * Dumps a byte buffer to the screen.
 *
 * Since: 1.2.2
 **/
void
fu_common_dump_bytes(const gchar *log_domain, const gchar *title, GBytes *bytes)
{
	gsize len = 0;
	const guint8 *data = g_bytes_get_data(bytes, &len);
	fu_common_dump_raw(log_domain, title, data, len);
}

/**
 * fu_common_bytes_align:
 * @bytes: data blob
 * @blksz: block size in bytes
 * @padval: the byte used to pad the byte buffer
 *
 * Aligns a block of memory to @blksize using the @padval value; if
 * the block is already aligned then the original @bytes is returned.
 *
 * Returns: (transfer full): a #GBytes, possibly @bytes
 *
 * Since: 1.2.4
 **/
GBytes *
fu_common_bytes_align(GBytes *bytes, gsize blksz, gchar padval)
{
	const guint8 *data;
	gsize sz;

	g_return_val_if_fail(bytes != NULL, NULL);
	g_return_val_if_fail(blksz > 0, NULL);

	/* pad */
	data = g_bytes_get_data(bytes, &sz);
	if (sz % blksz != 0) {
		gsize sz_align = ((sz / blksz) + 1) * blksz;
		guint8 *data_align = g_malloc(sz_align);
		memcpy(data_align, data, sz);
		memset(data_align + sz, padval, sz_align - sz);
		g_debug("aligning 0x%x bytes to 0x%x", (guint)sz, (guint)sz_align);
		return g_bytes_new_take(data_align, sz_align);
	}

	/* perfectly aligned */
	return g_bytes_ref(bytes);
}

/**
 * fu_common_bytes_is_empty:
 * @bytes: data blob
 *
 * Checks if a byte array are just empty (0xff) bytes.
 *
 * Returns: %TRUE if @bytes is empty
 *
 * Since: 1.2.6
 **/
gboolean
fu_common_bytes_is_empty(GBytes *bytes)
{
	gsize sz = 0;
	const guint8 *buf = g_bytes_get_data(bytes, &sz);
	for (gsize i = 0; i < sz; i++) {
		if (buf[i] != 0xff)
			return FALSE;
	}
	return TRUE;
}

/**
 * fu_common_bytes_compare_raw:
 * @buf1: a buffer
 * @bufsz1: sizeof @buf1
 * @buf2: another buffer
 * @bufsz2: sizeof @buf2
 * @error: (nullable): optional return location for an error
 *
 * Compares the buffers for equality.
 *
 * Returns: %TRUE if @buf1 and @buf2 are identical
 *
 * Since: 1.3.2
 **/
gboolean
fu_common_bytes_compare_raw(const guint8 *buf1,
			    gsize bufsz1,
			    const guint8 *buf2,
			    gsize bufsz2,
			    GError **error)
{
	g_return_val_if_fail(buf1 != NULL, FALSE);
	g_return_val_if_fail(buf2 != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* not the same length */
	if (bufsz1 != bufsz2) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "got %" G_GSIZE_FORMAT " bytes, expected "
			    "%" G_GSIZE_FORMAT,
			    bufsz1,
			    bufsz2);
		return FALSE;
	}

	/* check matches */
	for (guint i = 0x0; i < bufsz1; i++) {
		if (buf1[i] != buf2[i]) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "got 0x%02x, expected 0x%02x @ 0x%04x",
				    buf1[i],
				    buf2[i],
				    i);
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

/**
 * fu_common_bytes_compare:
 * @bytes1: a data blob
 * @bytes2: another #GBytes
 * @error: (nullable): optional return location for an error
 *
 * Compares the buffers for equality.
 *
 * Returns: %TRUE if @bytes1 and @bytes2 are identical
 *
 * Since: 1.2.6
 **/
gboolean
fu_common_bytes_compare(GBytes *bytes1, GBytes *bytes2, GError **error)
{
	const guint8 *buf1;
	const guint8 *buf2;
	gsize bufsz1;
	gsize bufsz2;

	g_return_val_if_fail(bytes1 != NULL, FALSE);
	g_return_val_if_fail(bytes2 != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	buf1 = g_bytes_get_data(bytes1, &bufsz1);
	buf2 = g_bytes_get_data(bytes2, &bufsz2);
	return fu_common_bytes_compare_raw(buf1, bufsz1, buf2, bufsz2, error);
}

/**
 * fu_common_bytes_pad:
 * @bytes: data blob
 * @sz: the desired size in bytes
 *
 * Pads a GBytes to a minimum @sz with `0xff`.
 *
 * Returns: (transfer full): a data blob
 *
 * Since: 1.3.1
 **/
GBytes *
fu_common_bytes_pad(GBytes *bytes, gsize sz)
{
	gsize bytes_sz;

	g_return_val_if_fail(bytes != NULL, NULL);
	g_return_val_if_fail(sz != 0, NULL);

	/* pad */
	bytes_sz = g_bytes_get_size(bytes);
	if (bytes_sz < sz) {
		const guint8 *data = g_bytes_get_data(bytes, NULL);
		guint8 *data_new = g_malloc(sz);
		memcpy(data_new, data, bytes_sz);
		memset(data_new + bytes_sz, 0xff, sz - bytes_sz);
		return g_bytes_new_take(data_new, sz);
	}

	/* not required */
	return g_bytes_ref(bytes);
}

/**
 * fu_common_bytes_new_offset:
 * @bytes: data blob
 * @offset: where subsection starts at
 * @length: length of subsection
 * @error: (nullable): optional return location for an error
 *
 * Creates a #GBytes which is a subsection of another #GBytes.
 *
 * Returns: (transfer full): a #GBytes, or #NULL if range is invalid
 *
 * Since: 1.5.4
 **/
GBytes *
fu_common_bytes_new_offset(GBytes *bytes, gsize offset, gsize length, GError **error)
{
	g_return_val_if_fail(bytes != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* sanity check */
	if (offset + length > g_bytes_get_size(bytes)) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "cannot create bytes @0x%02x for 0x%02x "
			    "as buffer only 0x%04x bytes in size",
			    (guint)offset,
			    (guint)length,
			    (guint)g_bytes_get_size(bytes));
		return NULL;
	}
	return g_bytes_new_from_bytes(bytes, offset, length);
}

/**
 * fu_common_realpath:
 * @filename: a filename
 * @error: (nullable): optional return location for an error
 *
 * Finds the canonicalized absolute filename for a path.
 *
 * Returns: a filename, or %NULL if invalid or not found
 *
 * Since: 1.2.6
 **/
gchar *
fu_common_realpath(const gchar *filename, GError **error)
{
	char full_tmp[PATH_MAX];

	g_return_val_if_fail(filename != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

#ifdef HAVE_REALPATH
	if (realpath(filename, full_tmp) == NULL) {
#else
	if (_fullpath(full_tmp, filename, sizeof(full_tmp)) == NULL) {
#endif
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "cannot resolve path: %s",
			    strerror(errno));
		return NULL;
	}
	if (!g_file_test(full_tmp, G_FILE_TEST_EXISTS)) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "cannot find path: %s",
			    full_tmp);
		return NULL;
	}
	return g_strdup(full_tmp);
}

/**
 * fu_common_fnmatch:
 * @pattern: a glob pattern, e.g. `*foo*`
 * @str: a string to match against the pattern, e.g. `bazfoobar`
 *
 * Matches a string against a glob pattern.
 *
 * Returns: %TRUE if the string matched
 *
 * Since: 1.3.5
 **/
gboolean
fu_common_fnmatch(const gchar *pattern, const gchar *str)
{
	g_return_val_if_fail(pattern != NULL, FALSE);
	g_return_val_if_fail(str != NULL, FALSE);
	return fu_common_fnmatch_impl(pattern, str);
}

static gint
fu_common_filename_glob_sort_cb(gconstpointer a, gconstpointer b)
{
	return g_strcmp0(*(const gchar **)a, *(const gchar **)b);
}

/**
 * fu_common_filename_glob:
 * @directory: a directory path
 * @pattern: a glob pattern, e.g. `*foo*`
 * @error: (nullable): optional return location for an error
 *
 * Returns all the filenames that match a specific glob pattern.
 * Any results are sorted. No matching files will set @error.
 *
 * Returns:  (element-type utf8) (transfer container): matching files, or %NULL
 *
 * Since: 1.5.0
 **/
GPtrArray *
fu_common_filename_glob(const gchar *directory, const gchar *pattern, GError **error)
{
	const gchar *basename;
	g_autoptr(GDir) dir = NULL;
	g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free);

	g_return_val_if_fail(directory != NULL, NULL);
	g_return_val_if_fail(pattern != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	dir = g_dir_open(directory, 0, error);
	if (dir == NULL)
		return NULL;
	while ((basename = g_dir_read_name(dir)) != NULL) {
		if (!fu_common_fnmatch(pattern, basename))
			continue;
		g_ptr_array_add(files, g_build_filename(directory, basename, NULL));
	}
	if (files->len == 0) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_FOUND,
				    "no files matched pattern");
		return NULL;
	}
	g_ptr_array_sort(files, fu_common_filename_glob_sort_cb);
	return g_steal_pointer(&files);
}

/**
 * fu_common_strnsplit:
 * @str: a string to split
 * @sz: size of @str
 * @delimiter: a string which specifies the places at which to split the string
 * @max_tokens: the maximum number of pieces to split @str into
 *
 * Splits a string into a maximum of @max_tokens pieces, using the given
 * delimiter. If @max_tokens is reached, the remainder of string is appended
 * to the last token.
 *
 * Returns: (transfer full): a newly-allocated NULL-terminated array of strings
 *
 * Since: 1.3.1
 **/
gchar **
fu_common_strnsplit(const gchar *str, gsize sz, const gchar *delimiter, gint max_tokens)
{
	if (str[sz - 1] != '\0') {
		g_autofree gchar *str2 = g_strndup(str, sz);
		return g_strsplit(str2, delimiter, max_tokens);
	}
	return g_strsplit(str, delimiter, max_tokens);
}

/**
 * fu_common_strnsplit_full:
 * @str: a string to split
 * @sz: size of @str, or -1 for unknown
 * @delimiter: a string which specifies the places at which to split the string
 * @callback: (scope call): a #FuCommonStrsplitFunc.
 * @user_data: user data
 * @error: (nullable): optional return location for an error
 *
 * Splits the string, calling the given function for each
 * of the tokens found. If any @callback returns %FALSE scanning is aborted.
 *
 * Use this function in preference to fu_common_strnsplit() when the input file is untrusted,
 * and you don't want to allocate a GStrv with billions of one byte items.
 *
 * Returns: %TRUE if no @callback returned FALSE
 *
 * Since: 1.7.0
 */
gboolean
fu_common_strnsplit_full(const gchar *str,
			 gssize sz,
			 const gchar *delimiter,
			 FuCommonStrsplitFunc callback,
			 gpointer user_data,
			 GError **error)
{
	gsize delimiter_sz;
	gsize str_sz;
	guint found_idx = 0;
	guint token_idx = 0;

	g_return_val_if_fail(str != NULL, FALSE);
	g_return_val_if_fail(delimiter != NULL && delimiter[0] != '\0', FALSE);
	g_return_val_if_fail(callback != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* make known */
	str_sz = sz != -1 ? (gsize)sz : strlen(str);
	delimiter_sz = strlen(delimiter);

	/* cannot split */
	if (delimiter_sz > str_sz) {
		g_autoptr(GString) token = g_string_new(str);
		return callback(token, token_idx, user_data, error);
	}

	/* start splittin' */
	for (gsize i = 0; i < (str_sz - delimiter_sz) + 1;) {
		if (strncmp(str + i, delimiter, delimiter_sz) == 0) {
			g_autoptr(GString) token = g_string_new(NULL);
			g_string_append_len(token, str + found_idx, i - found_idx);
			if (!callback(token, token_idx++, user_data, error))
				return FALSE;
			i += delimiter_sz;
			found_idx = i;
		} else {
			i++;
		}
	}

	/* any bits left over? */
	if (found_idx != str_sz) {
		g_autoptr(GString) token = g_string_new(NULL);
		g_string_append_len(token, str + found_idx, str_sz - found_idx);
		if (!callback(token, token_idx, user_data, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

/**
 * fu_common_strsafe:
 * @str: (nullable): a string to make safe for printing
 * @maxsz: maximum size of returned string
 *
 * Converts a string into something that can be safely printed.
 *
 * Returns: (transfer full): safe string, or %NULL if there was nothing valid
 *
 * Since: 1.5.5
 **/
gchar *
fu_common_strsafe(const gchar *str, gsize maxsz)
{
	gboolean valid = FALSE;
	g_autoptr(GString) tmp = NULL;

	/* sanity check */
	if (str == NULL || maxsz == 0)
		return NULL;

	/* replace non-printable chars with '.' */
	tmp = g_string_sized_new(maxsz);
	for (gsize i = 0; i < maxsz && str[i] != '\0'; i++) {
		if (!g_ascii_isprint(str[i])) {
			g_string_append_c(tmp, '.');
			continue;
		}
		g_string_append_c(tmp, str[i]);
		if (!g_ascii_isspace(str[i]))
			valid = TRUE;
	}

	/* if just junk, don't return 'all dots' */
	if (tmp->len == 0 || !valid)
		return NULL;
	return g_string_free(g_steal_pointer(&tmp), FALSE);
}

/**
 * fu_common_strjoin_array:
 * @separator: (nullable): string to insert between each of the strings
 * @array: (element-type utf8): a #GPtrArray
 *
 * Joins an array of strings together to form one long string, with the optional
 * separator inserted between each of them.
 *
 * If @array has no items, the return value will be an empty string.
 * If @array contains a single item, separator will not appear in the resulting
 * string.
 *
 * Returns: a string
 *
 * Since: 1.5.6
 **/
gchar *
fu_common_strjoin_array(const gchar *separator, GPtrArray *array)
{
	g_autofree const gchar **strv = NULL;

	g_return_val_if_fail(array != NULL, NULL);

	strv = g_new0(const gchar *, array->len + 1);
	for (guint i = 0; i < array->len; i++)
		strv[i] = g_ptr_array_index(array, i);
	return g_strjoinv(separator, (gchar **)strv);
}

/**
 * fu_memcpy_safe:
 * @dst: destination buffer
 * @dst_sz: maximum size of @dst, typically `sizeof(dst)`
 * @dst_offset: offset in bytes into @dst to copy to
 * @src: source buffer
 * @src_sz: maximum size of @dst, typically `sizeof(src)`
 * @src_offset: offset in bytes into @src to copy from
 * @n: number of bytes to copy from @src+@offset from
 * @error: (nullable): optional return location for an error
 *
 * Copies some memory using memcpy in a safe way. Providing the buffer sizes
 * of both the destination and the source allows us to check for buffer overflow.
 *
 * Providing the buffer offsets also allows us to check reading past the end of
 * the source buffer. For this reason the caller should NEVER add an offset to
 * @src or @dst.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if the bytes were copied, %FALSE otherwise
 *
 * Since: 1.3.1
 **/
gboolean
fu_memcpy_safe(guint8 *dst,
	       gsize dst_sz,
	       gsize dst_offset,
	       const guint8 *src,
	       gsize src_sz,
	       gsize src_offset,
	       gsize n,
	       GError **error)
{
	g_return_val_if_fail(dst != NULL, FALSE);
	g_return_val_if_fail(src != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (n == 0)
		return TRUE;

	if (n > src_sz) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_READ,
			    "attempted to read 0x%02x bytes from buffer of 0x%02x",
			    (guint)n,
			    (guint)src_sz);
		return FALSE;
	}
	if (n + src_offset > src_sz) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_READ,
			    "attempted to read 0x%02x bytes at offset 0x%02x from buffer of 0x%02x",
			    (guint)n,
			    (guint)src_offset,
			    (guint)src_sz);
		return FALSE;
	}
	if (n > dst_sz) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "attempted to write 0x%02x bytes to buffer of 0x%02x",
			    (guint)n,
			    (guint)dst_sz);
		return FALSE;
	}
	if (n + dst_offset > dst_sz) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "attempted to write 0x%02x bytes at offset 0x%02x to buffer of 0x%02x",
			    (guint)n,
			    (guint)dst_offset,
			    (guint)dst_sz);
		return FALSE;
	}

	/* phew! */
	memcpy(dst + dst_offset, src + src_offset, n);
	return TRUE;
}

/**
 * fu_memmem_safe:
 * @haystack: destination buffer
 * @haystack_sz: maximum size of @haystack, typically `sizeof(haystack)`
 * @needle: source buffer
 * @needle_sz: maximum size of @haystack, typically `sizeof(needle)`
 * @offset: (out) (nullable): offset in bytes @needle has been found in @haystack
 * @error: (nullable): optional return location for an error
 *
 * Finds a block of memory in another block of memory in a safe way.
 *
 * Returns: %TRUE if the needle was found in the haystack, %FALSE otherwise
 *
 * Since: 1.7.4
 **/
gboolean
fu_memmem_safe(const guint8 *haystack,
	       gsize haystack_sz,
	       const guint8 *needle,
	       gsize needle_sz,
	       gsize *offset,
	       GError **error)
{
#ifdef HAVE_MEMMEM
	const guint8 *tmp;
#endif
	g_return_val_if_fail(haystack != NULL, FALSE);
	g_return_val_if_fail(needle != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* nothing to find */
	if (needle_sz == 0) {
		if (offset != NULL)
			*offset = 0;
		return TRUE;
	}

	/* impossible */
	if (needle_sz > haystack_sz) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "needle of 0x%02x bytes is larger than haystack of 0x%02x bytes",
			    (guint)needle_sz,
			    (guint)haystack_sz);
		return FALSE;
	}

#ifdef HAVE_MEMMEM
	/* trust glibc to do a binary or linear search as appropriate */
	tmp = memmem(haystack, haystack_sz, needle, needle_sz);
	if (tmp != NULL) {
		if (offset != NULL)
			*offset = tmp - haystack;
		return TRUE;
	}
#else
	for (gsize i = 0; i < haystack_sz - needle_sz; i++) {
		if (memcmp(haystack + i, needle, needle_sz) == 0) {
			if (offset != NULL)
				*offset = i;
			return TRUE;
		}
	}
#endif

	/* not found */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_FOUND,
		    "needle of 0x%02x bytes was not found in haystack of 0x%02x bytes",
		    (guint)needle_sz,
		    (guint)haystack_sz);
	return FALSE;
}

/**
 * fu_memdup_safe:
 * @src: source buffer
 * @n: number of bytes to copy from @src
 * @error: (nullable): optional return location for an error
 *
 * Duplicates some memory using memdup in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * NOTE: This function intentionally limits allocation size to 1GB.
 *
 * Returns: (transfer full): block of allocated memory, or %NULL for an error.
 *
 * Since: 1.5.6
 **/
guint8 *
fu_memdup_safe(const guint8 *src, gsize n, GError **error)
{
	/* sanity check */
	if (n > 0x40000000) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_SUPPORTED,
			    "cannot allocate %uGB of memory",
			    (guint)(n / 0x40000000));
		return NULL;
	}

#if GLIB_CHECK_VERSION(2, 67, 3)
	/* linear block of memory */
	return g_memdup2(src, n);
#else
	return g_memdup(src, (guint)n);
#endif
}

/**
 * fu_common_read_uint8_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to copy from
 * @value: (out) (nullable): the parsed value
 * @error: (nullable): optional return location for an error
 *
 * Read a value from a buffer in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was set, %FALSE otherwise
 *
 * Since: 1.3.3
 **/
gboolean
fu_common_read_uint8_safe(const guint8 *buf,
			  gsize bufsz,
			  gsize offset,
			  guint8 *value,
			  GError **error)
{
	guint8 tmp;

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!fu_memcpy_safe(&tmp,
			    sizeof(tmp),
			    0x0, /* dst */
			    buf,
			    bufsz,
			    offset, /* src */
			    sizeof(tmp),
			    error))
		return FALSE;
	if (value != NULL)
		*value = tmp;
	return TRUE;
}

/**
 * fu_common_read_uint16_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to copy from
 * @value: (out) (nullable): the parsed value
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 * @error: (nullable): optional return location for an error
 *
 * Read a value from a buffer using a specified endian in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was set, %FALSE otherwise
 *
 * Since: 1.3.3
 **/
gboolean
fu_common_read_uint16_safe(const guint8 *buf,
			   gsize bufsz,
			   gsize offset,
			   guint16 *value,
			   FuEndianType endian,
			   GError **error)
{
	guint8 dst[2] = {0x0};

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!fu_memcpy_safe(dst,
			    sizeof(dst),
			    0x0, /* dst */
			    buf,
			    bufsz,
			    offset, /* src */
			    sizeof(dst),
			    error))
		return FALSE;
	if (value != NULL)
		*value = fu_common_read_uint16(dst, endian);
	return TRUE;
}

/**
 * fu_common_read_uint32_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to copy from
 * @value: (out) (nullable): the parsed value
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 * @error: (nullable): optional return location for an error
 *
 * Read a value from a buffer using a specified endian in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was set, %FALSE otherwise
 *
 * Since: 1.3.3
 **/
gboolean
fu_common_read_uint32_safe(const guint8 *buf,
			   gsize bufsz,
			   gsize offset,
			   guint32 *value,
			   FuEndianType endian,
			   GError **error)
{
	guint8 dst[4] = {0x0};

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!fu_memcpy_safe(dst,
			    sizeof(dst),
			    0x0, /* dst */
			    buf,
			    bufsz,
			    offset, /* src */
			    sizeof(dst),
			    error))
		return FALSE;
	if (value != NULL)
		*value = fu_common_read_uint32(dst, endian);
	return TRUE;
}

/**
 * fu_common_read_uint64_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to copy from
 * @value: (out) (nullable): the parsed value
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 * @error: (nullable): optional return location for an error
 *
 * Read a value from a buffer using a specified endian in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was set, %FALSE otherwise
 *
 * Since: 1.5.8
 **/
gboolean
fu_common_read_uint64_safe(const guint8 *buf,
			   gsize bufsz,
			   gsize offset,
			   guint64 *value,
			   FuEndianType endian,
			   GError **error)
{
	guint8 dst[8] = {0x0};

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!fu_memcpy_safe(dst,
			    sizeof(dst),
			    0x0, /* dst */
			    buf,
			    bufsz,
			    offset, /* src */
			    sizeof(dst),
			    error))
		return FALSE;
	if (value != NULL)
		*value = fu_common_read_uint64(dst, endian);
	return TRUE;
}

/**
 * fu_common_write_uint8_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to write to
 * @value: the value to write
 * @error: (nullable): optional return location for an error
 *
 * Write a value to a buffer in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was written, %FALSE otherwise
 *
 * Since: 1.5.8
 **/
gboolean
fu_common_write_uint8_safe(guint8 *buf, gsize bufsz, gsize offset, guint8 value, GError **error)
{
	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	return fu_memcpy_safe(buf,
			      bufsz,
			      offset, /* dst */
			      &value,
			      sizeof(value),
			      0x0, /* src */
			      sizeof(value),
			      error);
}

/**
 * fu_common_write_uint16_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to write to
 * @value: the value to write
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 * @error: (nullable): optional return location for an error
 *
 * Write a value to a buffer using a specified endian in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was written, %FALSE otherwise
 *
 * Since: 1.5.8
 **/
gboolean
fu_common_write_uint16_safe(guint8 *buf,
			    gsize bufsz,
			    gsize offset,
			    guint16 value,
			    FuEndianType endian,
			    GError **error)
{
	guint8 tmp[2] = {0x0};

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	fu_common_write_uint16(tmp, value, endian);
	return fu_memcpy_safe(buf,
			      bufsz,
			      offset, /* dst */
			      tmp,
			      sizeof(tmp),
			      0x0, /* src */
			      sizeof(tmp),
			      error);
}

/**
 * fu_common_write_uint32_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to write to
 * @value: the value to write
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 * @error: (nullable): optional return location for an error
 *
 * Write a value to a buffer using a specified endian in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was written, %FALSE otherwise
 *
 * Since: 1.5.8
 **/
gboolean
fu_common_write_uint32_safe(guint8 *buf,
			    gsize bufsz,
			    gsize offset,
			    guint32 value,
			    FuEndianType endian,
			    GError **error)
{
	guint8 tmp[4] = {0x0};

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	fu_common_write_uint32(tmp, value, endian);
	return fu_memcpy_safe(buf,
			      bufsz,
			      offset, /* dst */
			      tmp,
			      sizeof(tmp),
			      0x0, /* src */
			      sizeof(tmp),
			      error);
}

/**
 * fu_common_write_uint64_safe:
 * @buf: source buffer
 * @bufsz: maximum size of @buf, typically `sizeof(buf)`
 * @offset: offset in bytes into @buf to write to
 * @value: the value to write
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 * @error: (nullable): optional return location for an error
 *
 * Write a value to a buffer using a specified endian in a safe way.
 *
 * You don't need to use this function in "obviously correct" cases, nor should
 * you use it when performance is a concern. Only us it when you're not sure if
 * malicious data from a device or firmware could cause memory corruption.
 *
 * Returns: %TRUE if @value was written, %FALSE otherwise
 *
 * Since: 1.5.8
 **/
gboolean
fu_common_write_uint64_safe(guint8 *buf,
			    gsize bufsz,
			    gsize offset,
			    guint64 value,
			    FuEndianType endian,
			    GError **error)
{
	guint8 tmp[8] = {0x0};

	g_return_val_if_fail(buf != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	fu_common_write_uint64(tmp, value, endian);
	return fu_memcpy_safe(buf,
			      bufsz,
			      offset, /* dst */
			      tmp,
			      sizeof(tmp),
			      0x0, /* src */
			      sizeof(tmp),
			      error);
}

/**
 * fu_byte_array_append_uint8:
 * @array: a #GByteArray
 * @data: value
 *
 * Adds a 8 bit integer to a byte array.
 *
 * Since: 1.3.1
 **/
void
fu_byte_array_append_uint8(GByteArray *array, guint8 data)
{
	g_byte_array_append(array, &data, sizeof(data));
}

/**
 * fu_byte_array_append_uint16:
 * @array: a #GByteArray
 * @data: value
 * @endian: endian type, e.g. #G_LITTLE_ENDIAN
 *
 * Adds a 16 bit integer to a byte array.
 *
 * Since: 1.3.1
 **/
void
fu_byte_array_append_uint16(GByteArray *array, guint16 data, FuEndianType endian)
{
	guint8 buf[2];
	fu_common_write_uint16(buf, data, endian);
	g_byte_array_append(array, buf, sizeof(buf));
}

/**
 * fu_byte_array_append_uint32:
 * @array: a #GByteArray
 * @data: value
 * @endian: endian type, e.g. #G_LITTLE_ENDIAN
 *
 * Adds a 32 bit integer to a byte array.
 *
 * Since: 1.3.1
 **/
void
fu_byte_array_append_uint32(GByteArray *array, guint32 data, FuEndianType endian)
{
	guint8 buf[4];
	fu_common_write_uint32(buf, data, endian);
	g_byte_array_append(array, buf, sizeof(buf));
}

/**
 * fu_byte_array_append_uint64:
 * @array: a #GByteArray
 * @data: value
 * @endian: endian type, e.g. #G_LITTLE_ENDIAN
 *
 * Adds a 64 bit integer to a byte array.
 *
 * Since: 1.5.8
 **/
void
fu_byte_array_append_uint64(GByteArray *array, guint64 data, FuEndianType endian)
{
	guint8 buf[8];
	fu_common_write_uint64(buf, data, endian);
	g_byte_array_append(array, buf, sizeof(buf));
}

/**
 * fu_byte_array_append_bytes:
 * @array: a #GByteArray
 * @bytes: data blob
 *
 * Adds the contents of a GBytes to a byte array.
 *
 * Since: 1.5.8
 **/
void
fu_byte_array_append_bytes(GByteArray *array, GBytes *bytes)
{
	g_byte_array_append(array, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes));
}

/**
 * fu_byte_array_set_size_full:
 * @array: a #GByteArray
 * @length:  the new size of the GByteArray
 * @data: the byte used to pad the array
 *
 * Sets the size of the GByteArray, expanding with @data as required.
 *
 * Since: 1.6.0
 **/
void
fu_byte_array_set_size_full(GByteArray *array, guint length, guint8 data)
{
	guint oldlength = array->len;
	g_byte_array_set_size(array, length);
	if (length > oldlength)
		memset(array->data + oldlength, data, length - oldlength);
}

/**
 * fu_byte_array_set_size:
 * @array: a #GByteArray
 * @length: the new size of the GByteArray
 *
 * Sets the size of the GByteArray, expanding it with NULs if necessary.
 *
 * Since: 1.5.0
 **/
void
fu_byte_array_set_size(GByteArray *array, guint length)
{
	return fu_byte_array_set_size_full(array, length, 0x0);
}

/**
 * fu_byte_array_align_up:
 * @array: a #GByteArray
 * @alignment: align to this power of 2
 * @data: the byte used to pad the array
 *
 * Align a byte array length to a power of 2 boundary, where @alignment is the
 * bit position to align to. If @alignment is zero then @array is unchanged.
 *
 * Since: 1.6.0
 **/
void
fu_byte_array_align_up(GByteArray *array, guint8 alignment, guint8 data)
{
	fu_byte_array_set_size_full(array, fu_common_align_up(array->len, alignment), data);
}

/**
 * fu_common_kernel_locked_down:
 *
 * Determines if kernel lockdown in effect
 *
 * Since: 1.3.8
 **/
gboolean
fu_common_kernel_locked_down(void)
{
#ifdef __linux__
	gsize len = 0;
	g_autofree gchar *dir = fu_common_get_path(FU_PATH_KIND_SYSFSDIR_SECURITY);
	g_autofree gchar *fname = g_build_filename(dir, "lockdown", NULL);
	g_autofree gchar *data = NULL;
	g_auto(GStrv) options = NULL;

	if (!g_file_test(fname, G_FILE_TEST_EXISTS))
		return FALSE;
	if (!g_file_get_contents(fname, &data, &len, NULL))
		return FALSE;
	if (len < 1)
		return FALSE;
	options = g_strsplit(data, " ", -1);
	for (guint i = 0; options[i] != NULL; i++) {
		if (g_strcmp0(options[i], "[none]") == 0)
			return FALSE;
	}
	return TRUE;
#else
	return FALSE;
#endif
}

/**
 * fu_common_check_kernel_version :
 * @minimum_kernel: (not nullable): The minimum kernel version to check against
 * @error: (nullable): optional return location for an error
 *
 * Determines if the system is running at least a certain required kernel version
 *
 * Since: 1.6.2
 **/
gboolean
fu_common_check_kernel_version(const gchar *minimum_kernel, GError **error)
{
#ifdef HAVE_UTSNAME_H
	struct utsname name_tmp;

	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
	g_return_val_if_fail(minimum_kernel != NULL, FALSE);

	memset(&name_tmp, 0, sizeof(struct utsname));
	if (uname(&name_tmp) < 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "failed to read kernel version");
		return FALSE;
	}
	if (fu_common_vercmp_full(name_tmp.release, minimum_kernel, FWUPD_VERSION_FORMAT_TRIPLET) <
	    0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "kernel %s doesn't meet minimum %s",
			    name_tmp.release,
			    minimum_kernel);
		return FALSE;
	}

	return TRUE;
#else
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "platform doesn't support checking for minimum Linux kernel");
	return FALSE;
#endif
}

/**
 * fu_common_cpuid:
 * @leaf: the CPUID level, now called the 'leaf' by Intel
 * @eax: (out) (nullable): EAX register
 * @ebx: (out) (nullable): EBX register
 * @ecx: (out) (nullable): ECX register
 * @edx: (out) (nullable): EDX register
 * @error: (nullable): optional return location for an error
 *
 * Calls CPUID and returns the registers for the given leaf.
 *
 * Returns: %TRUE if the registers are set.
 *
 * Since: 1.5.0
 **/
gboolean
fu_common_cpuid(guint32 leaf,
		guint32 *eax,
		guint32 *ebx,
		guint32 *ecx,
		guint32 *edx,
		GError **error)
{
#ifdef HAVE_CPUID_H
	guint eax_tmp = 0;
	guint ebx_tmp = 0;
	guint ecx_tmp = 0;
	guint edx_tmp = 0;

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

	/* get vendor */
	__get_cpuid_count(leaf, 0x0, &eax_tmp, &ebx_tmp, &ecx_tmp, &edx_tmp);
	if (eax != NULL)
		*eax = eax_tmp;
	if (ebx != NULL)
		*ebx = ebx_tmp;
	if (ecx != NULL)
		*ecx = ecx_tmp;
	if (edx != NULL)
		*edx = edx_tmp;
	return TRUE;
#else
	g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "no <cpuid.h> support");
	return FALSE;
#endif
}

/**
 * fu_common_get_cpu_vendor:
 *
 * Uses CPUID to discover the CPU vendor.
 *
 * Returns: a CPU vendor, e.g. %FU_CPU_VENDOR_AMD if the vendor was AMD.
 *
 * Since: 1.5.5
 **/
FuCpuVendor
fu_common_get_cpu_vendor(void)
{
#ifdef HAVE_CPUID_H
	guint ebx = 0;
	guint ecx = 0;
	guint edx = 0;

	if (fu_common_cpuid(0x0, NULL, &ebx, &ecx, &edx, NULL)) {
		if (ebx == signature_INTEL_ebx && edx == signature_INTEL_edx &&
		    ecx == signature_INTEL_ecx) {
			return FU_CPU_VENDOR_INTEL;
		}
		if (ebx == signature_AMD_ebx && edx == signature_AMD_edx &&
		    ecx == signature_AMD_ecx) {
			return FU_CPU_VENDOR_AMD;
		}
	}
#endif

	/* failed */
	return FU_CPU_VENDOR_UNKNOWN;
}

/**
 * fu_common_is_live_media:
 *
 * Checks if the user is running from a live media using various heuristics.
 *
 * Returns: %TRUE if live
 *
 * Since: 1.4.6
 **/
gboolean
fu_common_is_live_media(void)
{
	gsize bufsz = 0;
	g_autofree gchar *buf = NULL;
	g_auto(GStrv) tokens = NULL;
	const gchar *args[] = {
	    "rd.live.image",
	    "boot=live",
	    NULL, /* last entry */
	};
	if (g_file_test("/cdrom/.disk/info", G_FILE_TEST_EXISTS))
		return TRUE;
	if (!g_file_get_contents("/proc/cmdline", &buf, &bufsz, NULL))
		return FALSE;
	if (bufsz == 0)
		return FALSE;
	tokens = fu_common_strnsplit(buf, bufsz - 1, " ", -1);
	for (guint i = 0; args[i] != NULL; i++) {
		if (g_strv_contains((const gchar *const *)tokens, args[i]))
			return TRUE;
	}
	return FALSE;
}

/**
 * fu_common_get_memory_size:
 *
 * Returns the size of physical memory.
 *
 * Returns: bytes
 *
 * Since: 1.5.6
 **/
guint64
fu_common_get_memory_size(void)
{
	return fu_common_get_memory_size_impl();
}

const gchar *
fu_common_convert_to_gpt_type(const gchar *type)
{
	struct {
		const gchar *gpt;
		const gchar *mbrs[4];
	} typeguids[] = {{"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", /* esp */
			  {"0xef", "efi", NULL}},
			 {"ebd0a0a2-b9e5-4433-87c0-68b6b72699c7", /* fat32 */
			  {"0x0b", "fat32", "fat32lba", NULL}},
			 {NULL, {NULL}}};
	for (guint i = 0; typeguids[i].gpt != NULL; i++) {
		for (guint j = 0; typeguids[i].mbrs[j] != NULL; j++) {
			if (g_strcmp0(type, typeguids[i].mbrs[j]) == 0)
				return typeguids[i].gpt;
		}
	}
	return type;
}

/**
 * fu_common_check_full_disk_encryption:
 * @error: (nullable): optional return location for an error
 *
 * Checks that all FDE volumes are not going to be affected by a firmware update. If unsure,
 * return with failure and let the user decide.
 *
 * Returns: %TRUE for success
 *
 * Since: 1.7.1
 **/
gboolean
fu_common_check_full_disk_encryption(GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;

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

	devices = fu_common_get_block_devices(error);
	if (devices == NULL)
		return FALSE;
	for (guint i = 0; i < devices->len; i++) {
		GDBusProxy *proxy = g_ptr_array_index(devices, i);
		g_autoptr(GVariant) id_type = g_dbus_proxy_get_cached_property(proxy, "IdType");
		g_autoptr(GVariant) device = g_dbus_proxy_get_cached_property(proxy, "Device");
		if (id_type == NULL || device == NULL)
			continue;
		if (g_strcmp0(g_variant_get_string(id_type, NULL), "BitLocker") == 0) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_WOULD_BLOCK,
				    "%s device %s is encrypted",
				    g_variant_get_string(id_type, NULL),
				    g_variant_get_bytestring(device));
			return FALSE;
		}
	}
	return TRUE;
}

/**
 * fu_common_get_volumes_by_kind:
 * @kind: a volume kind, typically a GUID
 * @error: (nullable): optional return location for an error
 *
 * Finds all volumes of a specific partition type
 *
 * Returns: (transfer container) (element-type FuVolume): a #GPtrArray, or %NULL if the kind was not
 *found
 *
 * Since: 1.4.6
 **/
GPtrArray *
fu_common_get_volumes_by_kind(const gchar *kind, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(GPtrArray) volumes = NULL;

	g_return_val_if_fail(kind != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	devices = fu_common_get_block_devices(error);
	if (devices == NULL)
		return NULL;
	volumes = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	for (guint i = 0; i < devices->len; i++) {
		GDBusProxy *proxy_blk = g_ptr_array_index(devices, i);
		const gchar *type_str;
		g_autoptr(FuVolume) vol = NULL;
		g_autoptr(GDBusProxy) proxy_part = NULL;
		g_autoptr(GDBusProxy) proxy_fs = NULL;
		g_autoptr(GVariant) val = NULL;

		proxy_part = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk),
						   G_DBUS_PROXY_FLAGS_NONE,
						   NULL,
						   UDISKS_DBUS_SERVICE,
						   g_dbus_proxy_get_object_path(proxy_blk),
						   UDISKS_DBUS_INTERFACE_PARTITION,
						   NULL,
						   error);
		if (proxy_part == NULL) {
			g_prefix_error(error,
				       "failed to initialize d-bus proxy %s: ",
				       g_dbus_proxy_get_object_path(proxy_blk));
			return NULL;
		}
		val = g_dbus_proxy_get_cached_property(proxy_part, "Type");
		if (val == NULL)
			continue;

		g_variant_get(val, "&s", &type_str);
		proxy_fs = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk),
						 G_DBUS_PROXY_FLAGS_NONE,
						 NULL,
						 UDISKS_DBUS_SERVICE,
						 g_dbus_proxy_get_object_path(proxy_blk),
						 UDISKS_DBUS_INTERFACE_FILESYSTEM,
						 NULL,
						 error);
		if (proxy_fs == NULL) {
			g_prefix_error(error,
				       "failed to initialize d-bus proxy %s: ",
				       g_dbus_proxy_get_object_path(proxy_blk));
			return NULL;
		}
		vol = g_object_new(FU_TYPE_VOLUME,
				   "proxy-block",
				   proxy_blk,
				   "proxy-filesystem",
				   proxy_fs,
				   NULL);

		/* convert reported type to GPT type */
		type_str = fu_common_convert_to_gpt_type(type_str);
		if (g_getenv("FWUPD_VERBOSE") != NULL) {
			g_autofree gchar *id_type = fu_volume_get_id_type(vol);
			g_debug("device %s, type: %s, internal: %d, fs: %s",
				g_dbus_proxy_get_object_path(proxy_blk),
				type_str,
				fu_volume_is_internal(vol),
				id_type);
		}
		if (g_strcmp0(type_str, kind) != 0)
			continue;
		g_ptr_array_add(volumes, g_steal_pointer(&vol));
	}
	if (volumes->len == 0) {
		g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "no volumes of type %s", kind);
		return NULL;
	}
	return g_steal_pointer(&volumes);
}

/**
 * fu_common_get_volume_by_device:
 * @device: a device string, typically starting with `/dev/`
 * @error: (nullable): optional return location for an error
 *
 * Finds the first volume from the specified device.
 *
 * Returns: (transfer full): a volume, or %NULL if the device was not found
 *
 * Since: 1.5.1
 **/
FuVolume *
fu_common_get_volume_by_device(const gchar *device, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;

	g_return_val_if_fail(device != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* find matching block device */
	devices = fu_common_get_block_devices(error);
	if (devices == NULL)
		return NULL;
	for (guint i = 0; i < devices->len; i++) {
		GDBusProxy *proxy_blk = g_ptr_array_index(devices, i);
		g_autoptr(GVariant) val = NULL;
		val = g_dbus_proxy_get_cached_property(proxy_blk, "Device");
		if (val == NULL)
			continue;
		if (g_strcmp0(g_variant_get_bytestring(val), device) == 0) {
			g_autoptr(GDBusProxy) proxy_fs = NULL;
			g_autoptr(GError) error_local = NULL;
			proxy_fs = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk),
							 G_DBUS_PROXY_FLAGS_NONE,
							 NULL,
							 UDISKS_DBUS_SERVICE,
							 g_dbus_proxy_get_object_path(proxy_blk),
							 UDISKS_DBUS_INTERFACE_FILESYSTEM,
							 NULL,
							 &error_local);
			if (proxy_fs == NULL)
				g_debug("ignoring: %s", error_local->message);
			return g_object_new(FU_TYPE_VOLUME,
					    "proxy-block",
					    proxy_blk,
					    "proxy-filesystem",
					    proxy_fs,
					    NULL);
		}
	}

	/* failed */
	g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "no volumes for device %s", device);
	return NULL;
}

/**
 * fu_common_get_volume_by_devnum:
 * @devnum: a device number
 * @error: (nullable): optional return location for an error
 *
 * Finds the first volume from the specified device.
 *
 * Returns: (transfer full): a volume, or %NULL if the device was not found
 *
 * Since: 1.5.1
 **/
FuVolume *
fu_common_get_volume_by_devnum(guint32 devnum, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;

	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* find matching block device */
	devices = fu_common_get_block_devices(error);
	if (devices == NULL)
		return NULL;
	for (guint i = 0; i < devices->len; i++) {
		GDBusProxy *proxy_blk = g_ptr_array_index(devices, i);
		g_autoptr(GVariant) val = NULL;
		val = g_dbus_proxy_get_cached_property(proxy_blk, "DeviceNumber");
		if (val == NULL)
			continue;
		if (devnum == g_variant_get_uint64(val)) {
			return g_object_new(FU_TYPE_VOLUME, "proxy-block", proxy_blk, NULL);
		}
	}

	/* failed */
	g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "no volumes for devnum %u", devnum);
	return NULL;
}

/**
 * fu_common_get_esp_default:
 * @error: (nullable): optional return location for an error
 *
 * Gets the platform default ESP
 *
 * Returns: (transfer full): a volume, or %NULL if the ESP was not found
 *
 * Since: 1.4.6
 **/
FuVolume *
fu_common_get_esp_default(GError **error)
{
	const gchar *path_tmp;
	gboolean has_internal = FALSE;
	g_autoptr(GPtrArray) volumes_fstab = g_ptr_array_new();
	g_autoptr(GPtrArray) volumes_mtab = g_ptr_array_new();
	g_autoptr(GPtrArray) volumes_vfat = g_ptr_array_new();
	g_autoptr(GPtrArray) volumes = NULL;
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* for the test suite use local directory for ESP */
	path_tmp = g_getenv("FWUPD_UEFI_ESP_PATH");
	if (path_tmp != NULL)
		return fu_volume_new_from_mount_path(path_tmp);

	volumes = fu_common_get_volumes_by_kind(FU_VOLUME_KIND_ESP, &error_local);
	if (volumes == NULL) {
		g_debug("%s, falling back to %s", error_local->message, FU_VOLUME_KIND_BDP);
		volumes = fu_common_get_volumes_by_kind(FU_VOLUME_KIND_BDP, error);
		if (volumes == NULL) {
			g_prefix_error(error, "%s: ", error_local->message);
			return NULL;
		}
	}

	/* are there _any_ internal vfat partitions?
	 * remember HintSystem is just that -- a hint! */
	for (guint i = 0; i < volumes->len; i++) {
		FuVolume *vol = g_ptr_array_index(volumes, i);
		g_autofree gchar *type = fu_volume_get_id_type(vol);
		if (g_strcmp0(type, "vfat") == 0 && fu_volume_is_internal(vol)) {
			has_internal = TRUE;
			break;
		}
	}

	/* filter to vfat partitions */
	for (guint i = 0; i < volumes->len; i++) {
		FuVolume *vol = g_ptr_array_index(volumes, i);
		g_autofree gchar *type = fu_volume_get_id_type(vol);
		if (type == NULL)
			continue;
		if (has_internal && !fu_volume_is_internal(vol))
			continue;
		if (g_strcmp0(type, "vfat") == 0)
			g_ptr_array_add(volumes_vfat, vol);
	}
	if (volumes_vfat->len == 0) {
		g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, "No ESP found");
		return NULL;
	}
	for (guint i = 0; i < volumes_vfat->len; i++) {
		FuVolume *vol = g_ptr_array_index(volumes_vfat, i);
		g_ptr_array_add(fu_volume_is_mounted(vol) ? volumes_mtab : volumes_fstab, vol);
	}
	if (volumes_mtab->len == 1) {
		FuVolume *vol = g_ptr_array_index(volumes_mtab, 0);
		return g_object_ref(vol);
	}
	if (volumes_mtab->len == 0 && volumes_fstab->len == 1) {
		FuVolume *vol = g_ptr_array_index(volumes_fstab, 0);
		return g_object_ref(vol);
	}
	g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, "More than one available ESP");
	return NULL;
}

/**
 * fu_common_get_esp_for_path:
 * @esp_path: a path to the ESP
 * @error: (nullable): optional return location for an error
 *
 * Gets the platform ESP using a UNIX or UDisks path
 *
 * Returns: (transfer full): a #volume, or %NULL if the ESP was not found
 *
 * Since: 1.4.6
 **/
FuVolume *
fu_common_get_esp_for_path(const gchar *esp_path, GError **error)
{
	g_autofree gchar *basename = NULL;
	g_autoptr(GPtrArray) volumes = NULL;
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail(esp_path != NULL, NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	volumes = fu_common_get_volumes_by_kind(FU_VOLUME_KIND_ESP, &error_local);
	if (volumes == NULL) {
		/* check if it's a valid directory already */
		if (g_file_test(esp_path, G_FILE_TEST_IS_DIR))
			return fu_volume_new_from_mount_path(esp_path);
		g_propagate_error(error, g_steal_pointer(&error_local));
		return NULL;
	}
	basename = g_path_get_basename(esp_path);
	for (guint i = 0; i < volumes->len; i++) {
		FuVolume *vol = g_ptr_array_index(volumes, i);
		g_autofree gchar *vol_basename =
		    g_path_get_basename(fu_volume_get_mount_point(vol));
		if (g_strcmp0(basename, vol_basename) == 0)
			return g_object_ref(vol);
	}
	g_set_error(error,
		    G_IO_ERROR,
		    G_IO_ERROR_INVALID_FILENAME,
		    "No ESP with path %s",
		    esp_path);
	return NULL;
}

/**
 * fu_common_crc8_full:
 * @buf: memory buffer
 * @bufsz: size of @buf
 * @crc_init: initial CRC value, typically 0x00
 * @polynomial: CRC polynomial, e.g. 0x07 for CCITT
 *
 * Returns the cyclic redundancy check value for the given memory buffer.
 *
 * Returns: CRC value
 *
 * Since: 1.7.1
 **/
guint8
fu_common_crc8_full(const guint8 *buf, gsize bufsz, guint8 crc_init, guint8 polynomial)
{
	guint32 crc = crc_init;
	for (gsize j = bufsz; j > 0; j--) {
		crc ^= (*(buf++) << 8);
		for (guint32 i = 8; i; i--) {
			if (crc & 0x8000)
				crc ^= ((polynomial | 0x100) << 7);
			crc <<= 1;
		}
	}
	return ~((guint8)(crc >> 8));
}

/**
 * fu_common_crc8:
 * @buf: memory buffer
 * @bufsz: size of @buf
 *
 * Returns the cyclic redundancy check value for the given memory buffer.
 *
 * Returns: CRC value
 *
 * Since: 1.5.0
 **/
guint8
fu_common_crc8(const guint8 *buf, gsize bufsz)
{
	return fu_common_crc8_full(buf, bufsz, 0x00, 0x07);
}

/**
 * fu_common_crc16_full:
 * @buf: memory buffer
 * @bufsz: size of @buf
 * @crc: initial CRC value, typically 0xFFFF
 * @polynomial: CRC polynomial, typically 0xA001 for IBM or 0x1021 for CCITT
 *
 * Returns the cyclic redundancy check value for the given memory buffer.
 *
 * Returns: CRC value
 *
 * Since: 1.6.2
 **/
guint16
fu_common_crc16_full(const guint8 *buf, gsize bufsz, guint16 crc, guint16 polynomial)
{
	for (gsize len = bufsz; len > 0; len--) {
		crc = (guint16)(crc ^ (*buf++));
		for (guint8 i = 0; i < 8; i++) {
			if (crc & 0x1) {
				crc = (crc >> 1) ^ polynomial;
			} else {
				crc >>= 1;
			}
		}
	}
	return ~crc;
}

/**
 * fu_common_crc16:
 * @buf: memory buffer
 * @bufsz: size of @buf
 *
 * Returns the CRC-16-IBM cyclic redundancy value for the given memory buffer.
 *
 * Returns: CRC value
 *
 * Since: 1.5.0
 **/
guint16
fu_common_crc16(const guint8 *buf, gsize bufsz)
{
	return fu_common_crc16_full(buf, bufsz, 0xFFFF, 0xA001);
}

/**
 * fu_common_crc32_full:
 * @buf: memory buffer
 * @bufsz: size of @buf
 * @crc: initial CRC value, typically 0xFFFFFFFF
 * @polynomial: CRC polynomial, typically 0xEDB88320
 *
 * Returns the cyclic redundancy check value for the given memory buffer.
 *
 * Returns: CRC value
 *
 * Since: 1.5.0
 **/
guint32
fu_common_crc32_full(const guint8 *buf, gsize bufsz, guint32 crc, guint32 polynomial)
{
	for (guint32 idx = 0; idx < bufsz; idx++) {
		guint8 data = *buf++;
		crc = crc ^ data;
		for (guint32 bit = 0; bit < 8; bit++) {
			guint32 mask = -(crc & 1);
			crc = (crc >> 1) ^ (polynomial & mask);
		}
	}
	return ~crc;
}

/**
 * fu_common_crc32:
 * @buf: memory buffer
 * @bufsz: size of @buf
 *
 * Returns the cyclic redundancy check value for the given memory buffer.
 *
 * Returns: CRC value
 *
 * Since: 1.5.0
 **/
guint32
fu_common_crc32(const guint8 *buf, gsize bufsz)
{
	return fu_common_crc32_full(buf, bufsz, 0xFFFFFFFF, 0xEDB88320);
}

/**
 * fu_common_sum8:
 * @buf: memory buffer
 * @bufsz: size of @buf
 *
 * Returns the arithmetic sum of all bytes in @buf.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint8
fu_common_sum8(const guint8 *buf, gsize bufsz)
{
	guint8 checksum = 0;
	g_return_val_if_fail(buf != NULL, G_MAXUINT8);
	for (gsize i = 0; i < bufsz; i++)
		checksum += buf[i];
	return checksum;
}

/**
 * fu_common_sum8_bytes:
 * @blob: a #GBytes
 *
 * Returns the arithmetic sum of all bytes in @blob.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint8
fu_common_sum8_bytes(GBytes *blob)
{
	g_return_val_if_fail(blob != NULL, G_MAXUINT8);
	return fu_common_sum8(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob));
}

/**
 * fu_common_sum16:
 * @buf: memory buffer
 * @bufsz: size of @buf
 *
 * Returns the arithmetic sum of all bytes in @buf, adding them one byte at a time.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint16
fu_common_sum16(const guint8 *buf, gsize bufsz)
{
	guint16 checksum = 0;
	g_return_val_if_fail(buf != NULL, G_MAXUINT16);
	for (gsize i = 0; i < bufsz; i++)
		checksum += buf[i];
	return checksum;
}

/**
 * fu_common_sum16_bytes:
 * @blob: a #GBytes
 *
 * Returns the arithmetic sum of all bytes in @blob, adding them one byte at a time.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint16
fu_common_sum16_bytes(GBytes *blob)
{
	g_return_val_if_fail(blob != NULL, G_MAXUINT16);
	return fu_common_sum16(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob));
}

/**
 * fu_common_sum16w:
 * @buf: memory buffer
 * @bufsz: size of @buf
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Returns the arithmetic sum of all bytes in @buf, adding them one word at a time.
 * The caller must ensure that @bufsz is a multiple of 2.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint16
fu_common_sum16w(const guint8 *buf, gsize bufsz, FuEndianType endian)
{
	guint16 checksum = 0;
	g_return_val_if_fail(buf != NULL, G_MAXUINT16);
	g_return_val_if_fail(bufsz % 2 == 0, G_MAXUINT16);
	for (gsize i = 0; i < bufsz; i += 2)
		checksum += fu_common_read_uint16(&buf[i], endian);
	return checksum;
}

/**
 * fu_common_sum16w_bytes:
 * @blob: a #GBytes
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Returns the arithmetic sum of all bytes in @blob, adding them one word at a time.
 * The caller must ensure that the size of @blob is a multiple of 2.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint16
fu_common_sum16w_bytes(GBytes *blob, FuEndianType endian)
{
	g_return_val_if_fail(blob != NULL, G_MAXUINT16);
	return fu_common_sum16w(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), endian);
}

/**
 * fu_common_sum32:
 * @buf: memory buffer
 * @bufsz: size of @buf
 *
 * Returns the arithmetic sum of all bytes in @buf, adding them one byte at a time.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint32
fu_common_sum32(const guint8 *buf, gsize bufsz)
{
	guint32 checksum = 0;
	g_return_val_if_fail(buf != NULL, G_MAXUINT32);
	for (gsize i = 0; i < bufsz; i++)
		checksum += buf[i];
	return checksum;
}

/**
 * fu_common_sum32_bytes:
 * @blob: a #GBytes
 *
 * Returns the arithmetic sum of all bytes in @blob, adding them one byte at a time.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint32
fu_common_sum32_bytes(GBytes *blob)
{
	g_return_val_if_fail(blob != NULL, G_MAXUINT32);
	return fu_common_sum32(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob));
}

/**
 * fu_common_sum32w:
 * @buf: memory buffer
 * @bufsz: size of @buf
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Returns the arithmetic sum of all bytes in @buf, adding them one dword at a time.
 * The caller must ensure that @bufsz is a multiple of 4.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint32
fu_common_sum32w(const guint8 *buf, gsize bufsz, FuEndianType endian)
{
	guint32 checksum = 0;
	g_return_val_if_fail(buf != NULL, G_MAXUINT32);
	g_return_val_if_fail(bufsz % 4 == 0, G_MAXUINT32);
	for (gsize i = 0; i < bufsz; i += 4)
		checksum += fu_common_read_uint32(&buf[i], endian);
	return checksum;
}

/**
 * fu_common_sum32w_bytes:
 * @blob: a #GBytes
 * @endian: an endian type, e.g. %G_LITTLE_ENDIAN
 *
 * Returns the arithmetic sum of all bytes in @blob, adding them one dword at a time.
 * The caller must ensure that the size of @blob is a multiple of 4.
 *
 * Returns: sum value
 *
 * Since: 1.7.3
 **/
guint32
fu_common_sum32w_bytes(GBytes *blob, FuEndianType endian)
{
	g_return_val_if_fail(blob != NULL, G_MAXUINT32);
	return fu_common_sum32w(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), endian);
}

/**
 * fu_common_uri_get_scheme:
 * @uri: valid URI, e.g. `https://foo.bar/baz`
 *
 * Returns the USI scheme for the given URI.
 *
 * Returns: scheme value, or %NULL if invalid, e.g. `https`
 *
 * Since: 1.5.6
 **/
gchar *
fu_common_uri_get_scheme(const gchar *uri)
{
	gchar *tmp;

	g_return_val_if_fail(uri != NULL, NULL);

	tmp = g_strstr_len(uri, -1, ":");
	if (tmp == NULL || tmp[0] == '\0')
		return NULL;
	return g_utf8_strdown(uri, tmp - uri);
}

/**
 * fu_common_align_up:
 * @value: value to align
 * @alignment: align to this power of 2, where 0x1F is the maximum value of 2GB
 *
 * Align a value to a power of 2 boundary, where @alignment is the bit position
 * to align to. If @alignment is zero then @value is always returned unchanged.
 *
 * Returns: aligned value, which will be the same as @value if already aligned,
 * 		or %G_MAXSIZE if the value would overflow
 *
 * Since: 1.6.0
 **/
gsize
fu_common_align_up(gsize value, guint8 alignment)
{
	gsize value_new;
	guint32 mask = 1 << alignment;

	g_return_val_if_fail(alignment <= FU_FIRMWARE_ALIGNMENT_2G, G_MAXSIZE);

	/* no alignment required */
	if ((value & (mask - 1)) == 0)
		return value;

	/* increment up to the next alignment value */
	value_new = value + mask;
	value_new &= ~(mask - 1);

	/* overflow */
	if (value_new < value)
		return G_MAXSIZE;

	/* success */
	return value_new;
}

/**
 * fu_battery_state_to_string:
 * @battery_state: a battery state, e.g. %FU_BATTERY_STATE_FULLY_CHARGED
 *
 * Converts an enumerated type to a string.
 *
 * Returns: a string, or %NULL for invalid
 *
 * Since: 1.6.0
 **/
const gchar *
fu_battery_state_to_string(FuBatteryState battery_state)
{
	if (battery_state == FU_BATTERY_STATE_UNKNOWN)
		return "unknown";
	if (battery_state == FU_BATTERY_STATE_CHARGING)
		return "charging";
	if (battery_state == FU_BATTERY_STATE_DISCHARGING)
		return "discharging";
	if (battery_state == FU_BATTERY_STATE_EMPTY)
		return "empty";
	if (battery_state == FU_BATTERY_STATE_FULLY_CHARGED)
		return "fully-charged";
	return NULL;
}

/**
 * fu_lid_state_to_string:
 * @lid_state: a battery state, e.g. %FU_LID_STATE_CLOSED
 *
 * Converts an enumerated type to a string.
 *
 * Returns: a string, or %NULL for invalid
 *
 * Since: 1.7.4
 **/
const gchar *
fu_lid_state_to_string(FuLidState lid_state)
{
	if (lid_state == FU_LID_STATE_UNKNOWN)
		return "unknown";
	if (lid_state == FU_LID_STATE_OPEN)
		return "open";
	if (lid_state == FU_LID_STATE_CLOSED)
		return "closed";
	return NULL;
}

/**
 * fu_bytes_get_data_safe:
 * @bytes: data blob
 * @bufsz: (out) (optional): location to return size of byte data
 * @error: (nullable): optional return location for an error
 *
 * Get the byte data in the #GBytes. This data should not be modified.
 * This function will always return the same pointer for a given #GBytes.
 *
 * If the size of @bytes is zero, then %NULL is returned and the @error is set,
 * which differs in behavior to that of g_bytes_get_data().
 *
 * This may be useful when calling g_mapped_file_new() on a zero-length file.
 *
 * Returns: a pointer to the byte data, or %NULL.
 *
 * Since: 1.6.0
 **/
const guint8 *
fu_bytes_get_data_safe(GBytes *bytes, gsize *bufsz, GError **error)
{
	const guint8 *buf = g_bytes_get_data(bytes, bufsz);
	if (buf == NULL) {
		g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid data");
		return NULL;
	}
	return buf;
}

/**
 * fu_xmlb_builder_insert_kv:
 * @bn: #XbBuilderNode
 * @key: string key
 * @value: string value
 *
 * Convenience function to add an XML node with a string value. If @value is %NULL
 * then no member is added.
 *
 * Since: 1.6.0
 **/
void
fu_xmlb_builder_insert_kv(XbBuilderNode *bn, const gchar *key, const gchar *value)
{
	if (value == NULL)
		return;
	xb_builder_node_insert_text(bn, key, value, NULL);
}

/**
 * fu_xmlb_builder_insert_kx:
 * @bn: #XbBuilderNode
 * @key: string key
 * @value: integer value
 *
 * Convenience function to add an XML node with an integer value. If @value is 0
 * then no member is added.
 *
 * Since: 1.6.0
 **/
void
fu_xmlb_builder_insert_kx(XbBuilderNode *bn, const gchar *key, guint64 value)
{
	g_autofree gchar *value_hex = NULL;
	if (value == 0)
		return;
	value_hex = g_strdup_printf("0x%x", (guint)value);
	xb_builder_node_insert_text(bn, key, value_hex, NULL);
}

/**
 * fu_xmlb_builder_insert_kb:
 * @bn: #XbBuilderNode
 * @key: string key
 * @value: boolean value
 *
 * Convenience function to add an XML node with a boolean value.
 *
 * Since: 1.6.0
 **/
void
fu_xmlb_builder_insert_kb(XbBuilderNode *bn, const gchar *key, gboolean value)
{
	xb_builder_node_insert_text(bn, key, value ? "true" : "false", NULL);
}

/**
 * fu_common_get_firmware_search_path:
 * @error: (nullable): optional return location for an error
 *
 * Reads the FU_PATH_KIND_FIRMWARE_SEARCH and
 * returns its contents
 *
 * Returns: a pointer to a gchar array
 *
 * Since: 1.6.2
 **/
gchar *
fu_common_get_firmware_search_path(GError **error)
{
	gsize sz = 0;
	g_autofree gchar *sys_fw_search_path = NULL;
	g_autofree gchar *contents = NULL;

	sys_fw_search_path = fu_common_get_path(FU_PATH_KIND_FIRMWARE_SEARCH);
	if (!g_file_get_contents(sys_fw_search_path, &contents, &sz, error))
		return NULL;

	/* remove newline character */
	if (contents != NULL && sz > 0 && contents[sz - 1] == '\n')
		contents[sz - 1] = 0;

	g_debug("read firmware search path (%" G_GSIZE_FORMAT "): %s", sz, contents);

	return g_steal_pointer(&contents);
}

/**
 * fu_common_set_firmware_search_path:
 * @path: NUL-terminated string
 * @error: (nullable): optional return location for an error
 *
 * Writes path to the FU_PATH_KIND_FIRMWARE_SEARCH
 *
 * Returns: %TRUE if successful
 *
 * Since: 1.6.2
 **/
gboolean
fu_common_set_firmware_search_path(const gchar *path, GError **error)
{
#if GLIB_CHECK_VERSION(2, 66, 0)
	g_autofree gchar *sys_fw_search_path_prm = NULL;

	g_return_val_if_fail(path != NULL, FALSE);
	g_return_val_if_fail(strlen(path) < PATH_MAX, FALSE);

	sys_fw_search_path_prm = fu_common_get_path(FU_PATH_KIND_FIRMWARE_SEARCH);

	g_debug("writing firmware search path (%" G_GSIZE_FORMAT "): %s", strlen(path), path);

	return g_file_set_contents_full(sys_fw_search_path_prm,
					path,
					strlen(path),
					G_FILE_SET_CONTENTS_NONE,
					0644,
					error);
#else
	FILE *fd;
	gsize res;
	g_autofree gchar *sys_fw_search_path_prm = NULL;

	g_return_val_if_fail(path != NULL, FALSE);
	g_return_val_if_fail(strlen(path) < PATH_MAX, FALSE);

	sys_fw_search_path_prm = fu_common_get_path(FU_PATH_KIND_FIRMWARE_SEARCH);
	/* g_file_set_contents will try to create backup files in sysfs, so use fopen here */
	fd = fopen(sys_fw_search_path_prm, "w");
	if (fd == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_PERMISSION_DENIED,
			    "Failed to open %s: %s",
			    sys_fw_search_path_prm,
			    g_strerror(errno));
		return FALSE;
	}

	g_debug("writing firmware search path (%" G_GSIZE_FORMAT "): %s", strlen(path), path);

	res = fwrite(path, sizeof(gchar), strlen(path), fd);

	fclose(fd);

	if (res != strlen(path)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "Failed to write firmware search path: %s",
			    g_strerror(errno));
		return FALSE;
	}

	return TRUE;
#endif
}

/**
 * fu_common_reset_firmware_search_path:
 * @error: (nullable): optional return location for an error
 *
 * Resets the FU_PATH_KIND_FIRMWARE_SEARCH to an empty string
 *
 * Returns: %TRUE if successful
 *
 * Since: 1.6.2
 **/
gboolean
fu_common_reset_firmware_search_path(GError **error)
{
	const gchar *contents = " ";

	return fu_common_set_firmware_search_path(contents, error);
}
