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

#define G_LOG_DOMAIN "FuCommon"

#include "config.h"

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

#include "fu-common.h"
#include "fu-kernel.h"
#include "fu-path.h"
#include "fu-string.h"
#include "fu-version-common.h"

/**
 * fu_kernel_locked_down:
 *
 * Determines if kernel lockdown in effect
 *
 * Since: 1.8.2
 **/
gboolean
fu_kernel_locked_down(void)
{
#ifdef __linux__
	gsize len = 0;
	g_autofree gchar *dir = fu_path_from_kind(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_kernel_check_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.8.2
 **/
gboolean
fu_kernel_check_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_version_compare(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_kernel_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.8.2
 **/
gchar *
fu_kernel_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_path_from_kind(FU_PATH_KIND_FIRMWARE_SEARCH);
	if (!g_file_get_contents(sys_fw_search_path, &contents, &sz, error))
		return NULL;

	/* sanity check */
	if (contents == NULL || sz == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "failed to get firmware search path from %s",
			    sys_fw_search_path);
		return NULL;
	}

	/* remove newline character */
	if (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_kernel_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.8.2
 **/
gboolean
fu_kernel_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_path_from_kind(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_path_from_kind(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_kernel_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.8.2
 **/
gboolean
fu_kernel_reset_firmware_search_path(GError **error)
{
	const gchar *contents = " ";

	return fu_kernel_set_firmware_search_path(contents, error);
}

/**
 * fu_kernel_parse_cmdline:
 * @buf: (not nullable): cmdline to parse
 * @bufsz: size of @bufsz
 *
 * Parses all the kernel key/values into a hash table, respecting double quotes when required.
 *
 * Returns: (transfer container) (element-type utf8 utf8): keys from the cmdline
 *
 * Since: 1.9.1
 **/
GHashTable *
fu_kernel_parse_cmdline(const gchar *buf, gsize bufsz)
{
	gboolean is_escape = FALSE;
	g_autoptr(GHashTable) hash = NULL;
	g_autoptr(GString) acc = g_string_new(NULL);

	g_return_val_if_fail(buf != NULL, NULL);

	hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
	if (bufsz == 0)
		return g_steal_pointer(&hash);
	for (gsize i = 0; i < bufsz; i++) {
		if (!is_escape && (buf[i] == ' ' || buf[i] == '\n') && acc->len > 0) {
			g_auto(GStrv) kv = g_strsplit(acc->str, "=", 2);
			g_hash_table_insert(hash, g_strdup(kv[0]), g_strdup(kv[1]));
			g_string_set_size(acc, 0);
			continue;
		}
		if (buf[i] == '"') {
			is_escape = !is_escape;
			continue;
		}
		g_string_append_c(acc, buf[i]);
	}
	if (acc->len > 0) {
		g_auto(GStrv) kv = g_strsplit(acc->str, "=", 2);
		g_hash_table_insert(hash, g_strdup(kv[0]), g_strdup(kv[1]));
	}

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

/**
 * fu_kernel_get_cmdline:
 * @error: (nullable): optional return location for an error
 *
 * Loads all the kernel /proc/cmdline key/values into a hash table.
 *
 * Returns: (transfer container) (element-type utf8 utf8): keys from the kernel command line
 *
 * Since: 1.8.5
 **/
GHashTable *
fu_kernel_get_cmdline(GError **error)
{
#ifdef __linux__
	gsize bufsz = 0;
	g_autofree gchar *buf = NULL;

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

	if (!g_file_get_contents("/proc/cmdline", &buf, &bufsz, error))
		return NULL;
	return fu_kernel_parse_cmdline(buf, bufsz);
#else
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "platform does not support getting the kernel cmdline");
	return NULL;
#endif
}
