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

#define G_LOG_DOMAIN "FuCommon"

#include "config.h"

#ifdef HAVE_CPUID_H
#include <cpuid.h>
#endif
#include <errno.h>
#ifdef HAVE_GETRANDOM
#include <sys/random.h>
#endif
#ifdef HAVE_ARC4RANDOM
#include <stdlib.h>
#endif
#ifdef _WIN32
/* clang-format off */
#include <windows.h>
#include <wincrypt.h>
/* clang-format on */
#endif

#include "fu-common-private.h"
#include "fu-firmware.h"
#include "fu-path-store.h"
#include "fu-string.h"

/**
 * fu_common_from_bcd_u16:
 * @value: the BCD-encoded value
 *
 * Converts a decimal value from BCD.
 *
 * Returns: integer
 *
 * Since: 2.1.5
 **/
guint16
fu_common_from_bcd_u16(guint16 value)
{
	return (((value >> 12) & 0xF) * 1000) + (((value >> 8) & 0xF) * 100) +
	       (((value >> 4) & 0xF) * 10) + (value & 0xF);
}

/**
 * fu_common_to_bcd_u16:
 * @value: the decimal value
 *
 * Converts a decimal value to BCD.
 *
 * Returns: integer
 *
 * Since: 2.1.5
 **/
guint16
fu_common_to_bcd_u16(guint16 value)
{
	return ((value / 1000) << 12) | (((value / 100) % 10) << 8) | (((value / 10) % 10) << 4) |
	       (value % 10);
}

/**
 * fu_common_from_bcd_u8:
 * @value: the BCD-encoded value
 *
 * Converts a decimal value from BCD.
 *
 * Returns: integer
 *
 * Since: 2.1.5
 **/
guint8
fu_common_from_bcd_u8(guint8 value)
{
	return (((value >> 4) & 0xF) * 10) + (value & 0xF);
}

/**
 * fu_common_to_bcd_u8:
 * @value: the decimal value
 *
 * Converts a decimal value to BCD.
 *
 * Returns: integer
 *
 * Since: 2.1.5
 **/
guint8
fu_common_to_bcd_u8(guint8 value)
{
	return (((value / 10) % 10) << 4) | (value % 10);
}

/**
 * fu_size_checked_add:
 * @a: The #gsize left operand
 * @b: The #gsize right operand
 *
 * Performs a checked addition of a and b, ensuring the result does not overflow.
 *
 * Returns: @a+@b, or %G_MAXSIZE on overflow
 *
 * Since: 2.0.19
 **/
gsize
fu_size_checked_add(gsize a, gsize b)
{
	gsize tmp = 0;
	if (!g_size_checked_add(&tmp, a, b))
		return G_MAXSIZE;
	return tmp;
}

/**
 * fu_size_checked_inc:
 * @value: (inout): the starting and ending value
 * @len: the value to add
 * @error: (nullable): optional return location for an error
 *
 * Performs a checked increment of @value and @len, ensuring the result does not overflow.
 *
 * NOTE: If the operation does overflow then @value is *not* modified.
 *
 * Returns: boolean success
 *
 * Since: 2.1.2
 **/
gboolean
fu_size_checked_inc(gsize *value, gsize len, GError **error)
{
	gsize value_new;

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

	value_new = *value;
	if (!g_size_checked_add(&value_new, *value, len)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "0x%" G_GSIZE_MODIFIER "x + 0x%" G_GSIZE_MODIFIER "x would overflow",
			    *value,
			    len);
		return FALSE;
	}

	/* success */
	*value = value_new;
	return TRUE;
}

/**
 * fu_size_checked_inc_product:
 * @value: (inout): the starting and ending value
 * @a: the LHS product value
 * @b: the RHS product value
 * @error: (nullable): optional return location for an error
 *
 * Performs a checked increment of @value and @a x @b, ensuring the result does not overflow.
 *
 * NOTE: If the operation does overflow then @value is *not* modified.
 *
 * Returns: boolean success
 *
 * Since: 2.1.2
 **/
gboolean
fu_size_checked_inc_product(gsize *value, gsize a, gsize b, GError **error)
{
	gsize value_tmp = 0;

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

	if (!g_size_checked_mul(&value_tmp, a, b)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "0x%" G_GSIZE_MODIFIER "x * 0x%" G_GSIZE_MODIFIER "x would overflow",
			    a,
			    b);
		return FALSE;
	}

	/* success */
	return fu_size_checked_inc(value, value_tmp, error);
}

/**
 * fu_size_from_uint64:
 * @value: a 64-bit unsigned integer
 * @out: (out): output location for the converted value
 * @error: (nullable): optional return location for an error
 *
 * Safely converts a 64-bit unsigned integer to a #gsize value, ensuring the value
 * fits within the platform's #gsize range. On 32-bit platforms where #gsize is 32 bits,
 * this prevents silent truncation that could lead to security vulnerabilities.
 *
 * Returns: %TRUE if the conversion was successful, %FALSE if the value exceeds #G_MAXSIZE
 *
 * Since: 2.1.2
 **/
gboolean
fu_size_from_uint64(guint64 value, gsize *out, GError **error) /* nocheck:name */
{
	g_return_val_if_fail(out != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (value > G_MAXSIZE) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "value 0x%" G_GINT64_MODIFIER
			    "x exceeds gsize range 0x%" G_GSIZE_MODIFIER "x",
			    value,
			    (gsize)G_MAXSIZE);
		return FALSE;
	}

	/* success */
	*out = (gsize)value;
	return TRUE;
}

/**
 * fu_base64_encode:
 * @buf: (nullable): data blob
 * @bufsz: size of @data
 *
 * Encode a sequence of binary data into its Base-64 stringified representation.
 *
 * This should be used in preference to g_base64_encode() as GLib < 2.60.0 would return
 * a critical warning if @data is NULL.
 *
 * Returns: zero-terminated Base-64 encoded string representing data, or an empty string for no data
 *
 * Since: 2.1.5
 **/
gchar *
fu_base64_encode(const guint8 *buf, gsize bufsz)
{
#if !GLIB_CHECK_VERSION(2, 60, 0)
	if (buf == NULL)
		return g_strdup("");
#endif
	/* nocheck:blocked */
	return g_base64_encode((const guchar *)buf, bufsz);
}

/**
 * fu_error_map_entry_to_gerror:
 * @value: the value to look up
 * @entries: the #FuErrorMapEntry map
 * @n_entries: number of @entries
 * @error: (nullable): optional return location for an error
 *
 * Sets the #GError from the integer value and the error map.
 *
 * Any entries with a error_code of `FWUPD_ERROR_LAST` will return success.
 *
 * Returns: boolean success
 *
 * Since: 2.0.13
 **/
gboolean
fu_error_map_entry_to_gerror(guint value,
			     const FuErrorMapEntry entries[],
			     guint n_entries,
			     GError **error)
{
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	for (guint i = 0; i < n_entries; i++) {
		const FuErrorMapEntry entry = entries[i];
		if (entry.value != value)
			continue;
		if (entry.code == FWUPD_ERROR_LAST)
			return TRUE;
		g_set_error(error,
			    FWUPD_ERROR,
			    entry.code,
			    "%s [0x%x]",
			    entry.message != NULL ? entry.message
						  : fwupd_error_to_string(entry.value),
			    entry.value);
		return FALSE;
	}
	g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "generic failure [0x%x]", value);
	return FALSE;
}

/**
 * fu_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.8.2
 **/
gboolean
fu_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, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no <cpuid.h> support");
	return FALSE;
#endif
}

/**
 * fu_cpu_get_attrs:
 * @error: (nullable): optional return location for an error
 *
 * Gets attributes for the first CPU listed in `/proc/cpuinfo`.
 *
 * Returns: (element-type utf8 utf8) (transfer full): CPU attributes
 *
 * Since: 2.0.7
 **/
GHashTable *
fu_cpu_get_attrs(FuPathStore *pstore, GError **error)
{
	gsize bufsz = 0;
	g_autofree gchar *buf = NULL;
	g_autofree gchar *fn = NULL;
	g_autoptr(GHashTable) hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

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

	fn = fu_path_store_build_filename(pstore, error, FU_PATH_KIND_PROCFS, "cpuinfo", NULL);
	if (fn == NULL)
		return NULL;
	if (!g_file_get_contents(fn, &buf, &bufsz, error))
		return NULL;
	if (bufsz > 0) {
		g_auto(GStrv) lines = fu_strsplit(buf, bufsz, "\n", -1);
		for (guint i = 0; lines[i] != NULL; i++) {
			g_auto(GStrv) tokens = NULL;
			g_autofree gchar *key = NULL;
			if (lines[i][0] == '\0')
				break;
			tokens = g_strsplit(lines[i], ": ", 2);
			if (g_strv_length(tokens) != 2)
				continue;
			key = fu_strstrip(tokens[0]);
			if (key == NULL)
				continue;
			g_hash_table_insert(hash, g_steal_pointer(&key), g_strdup(tokens[1]));
		}
	}

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

/**
 * 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();
}

/**
 * fu_common_get_kernel_cmdline:
 * @error: (nullable): optional return location for an error
 *
 * Returns the current kernel command line options.
 *
 * Returns: options as a string, or %NULL on error
 *
 * Since: 1.5.6
 **/
gchar *
fu_common_get_kernel_cmdline(GError **error)
{
	return fu_common_get_kernel_cmdline_impl(error);
}

/**
 * fu_common_get_olson_timezone_id:
 * @pstore: a #FuPathStore
 * @error: (nullable): optional return location for an error
 *
 * Gets the system Olson timezone ID, as used in the CLDR and ICU specifications.
 *
 * Returns: timezone string, e.g. `Europe/London` or %NULL on error
 *
 * Since: 1.9.7
 **/
gchar *
fu_common_get_olson_timezone_id(FuPathStore *pstore, GError **error)
{
	g_return_val_if_fail(FU_IS_PATH_STORE(pstore), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);
	return fu_common_get_olson_timezone_id_impl(pstore, error);
}

/**
 * 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;
	gsize mask = (gsize)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_power_state_is_ac:
 * @power_state: a power state, e.g. %FU_POWER_STATE_AC_FULLY_CHARGED
 *
 * Determines if the power state can be considered "on AC power".
 *
 * Returns: %TRUE for success
 *
 * Since: 1.8.11
 **/
gboolean
fu_power_state_is_ac(FuPowerState power_state)
{
	return power_state != FU_POWER_STATE_BATTERY;
}

/**
 * fu_error_convert:
 * @entries: the #FuErrorConvertEntry map
 * @n_entries: number of @entries
 * @perror: (nullable): A #GError, perhaps with domain #GIOError
 *
 * Convert the error to a #FwupdError, if required.
 *
 * Since: 2.0.14
 **/
gboolean
fu_error_convert(const FuErrorConvertEntry entries[], guint n_entries, GError **perror)
{
	GError *error = (perror != NULL) ? *perror : NULL;

	/* sanity check */
	if (error == NULL)
		return TRUE;

	/* convert GIOError and GFileError */
	fwupd_error_convert(perror);
	if (error->domain == FWUPD_ERROR)
		return FALSE;
	for (guint i = 0; i < n_entries; i++) {
		if (g_error_matches(error, entries[i].domain, entries[i].code)) {
			error->domain = FWUPD_ERROR;
			error->code = entries[i].error;
			return FALSE;
		}
	}

#ifndef SUPPORTED_BUILD
	/* fallback */
	g_critical("GError %s:%i was not converted to FwupdError",
		   g_quark_to_string(error->domain),
		   error->code);
#endif
	error->domain = FWUPD_ERROR;
	error->code = FWUPD_ERROR_INTERNAL;
	return FALSE;
}

static gboolean
fu_common_get_random_raw(guint8 *buf, gsize bufsz, gsize *bufsz_valid, GError **error)
{
#ifdef HAVE_GETRANDOM
	gssize rc;
	rc = getrandom(buf, bufsz, 0);
	if (rc == -1) {
		if (errno == EINTR) {
			*bufsz_valid = 0;
			return TRUE;
		}
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "failed to get random data: %s",
			    fwupd_strerror(errno));
		return FALSE;
	}
	*bufsz_valid = rc;
	return TRUE;
#elif defined(HAVE_ARC4RANDOM)
	arc4random_buf(buf, bufsz);
	*bufsz_valid = bufsz;
	return TRUE;
#elif defined(_WIN32)
	HCRYPTPROV ctx = {0};

	if (!CryptAcquireContext(&ctx, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "failed to acquire context");
		return FALSE;
	}
	if (!CryptGenRandom(ctx, (DWORD)bufsz, (BYTE *)buf)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no random data");
		return FALSE;
	}
	if (!CryptReleaseContext(ctx, 0)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "failed to release context");
		return FALSE;
	}
	*bufsz_valid = bufsz;
	return TRUE;
#else
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no RNG available");
	return FALSE;
#endif
}

/**
 * fu_common_get_random:
 * @bufsz: the number of bytes to return
 * @error: (nullable): optional return location for an error
 *
 * Gets random bytes using a cryptographically secure RNG.
 *
 * Returns: (transfer full): a blob of random data, or %NULL for error
 *
 * Since: 2.1.5
 **/
GByteArray *
fu_common_get_random(guint bufsz, GError **error)
{
	g_autoptr(GByteArray) buf = g_byte_array_sized_new(bufsz);

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

	/* get a blob using cryptographically secure RNG */
	while (buf->len < bufsz) {
		guint8 tbuf[32] = {0};
		gsize tbuf_valid = 0;
		if (!fu_common_get_random_raw(tbuf, sizeof(tbuf), &tbuf_valid, error))
			return NULL;
		g_byte_array_append(buf, tbuf, tbuf_valid);
	}

	/* success */
	g_byte_array_set_size(buf, bufsz);
	return g_steal_pointer(&buf);
}

/**
 * fu_common_get_random_string:
 * @length: the exact string length of bytes to return
 * @error: (nullable): optional return location for an error
 *
 * Generates a random password using a cryptographically secure RNG.
 *
 * Returns: (transfer full): a string of alphanumeric chars, or %NULL for error
 *
 * Since: 2.1.5
 **/
gchar *
fu_common_get_random_string(guint length, GError **error)
{
	g_autoptr(GString) str = g_string_sized_new(length);

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

	/* filtering ~62:256 */
	while (str->len < length) {
		guint8 tbuf[32] = {0};
		gsize tbuf_valid = 0;
		if (!fu_common_get_random_raw(tbuf, sizeof(tbuf), &tbuf_valid, error))
			return NULL;
		for (gsize i = 0; i < tbuf_valid && str->len < length; i++) {
			const gchar tmp = (gchar)tbuf[i];
			if (g_ascii_isalnum(tmp))
				g_string_append_c(str, tmp);
		}
	}

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

/**
 * 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%lx", (gulong)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);
}
