/*
 * Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2015 Peter Jones <pjones@redhat.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <fwupdplugin.h>

#include <efivar.h>

#include "fu-uefi-common.h"
#include "fu-uefi-device.h"

static const gchar *
fu_uefi_bootmgr_get_suffix(GError **error)
{
	guint64 firmware_bits;
	struct {
		guint64 bits;
		const gchar *arch;
	} suffixes[] = {
#if defined(__x86_64__)
		{64, "x64"},
#elif defined(__aarch64__)
		{64, "aa64"},
#endif
#if defined(__x86_64__) || defined(__i386__) || defined(__i686__)
		{32, "ia32"},
#endif
		{0, NULL}
	};
	g_autofree gchar *sysfsfwdir = fu_common_get_path(FU_PATH_KIND_SYSFSDIR_FW);
	g_autofree gchar *sysfsefidir = g_build_filename(sysfsfwdir, "efi", NULL);
	firmware_bits = fu_uefi_read_file_as_uint64(sysfsefidir, "fw_platform_size");
	if (firmware_bits == 0) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_FOUND,
			    "%s/fw_platform_size cannot be found",
			    sysfsefidir);
		return NULL;
	}
	for (guint i = 0; suffixes[i].arch != NULL; i++) {
		if (firmware_bits != suffixes[i].bits)
			continue;
		return suffixes[i].arch;
	}

	/* this should exist */
	g_set_error(error,
		    G_IO_ERROR,
		    G_IO_ERROR_NOT_FOUND,
		    "%s/fw_platform_size has unknown value %" G_GUINT64_FORMAT,
		    sysfsefidir,
		    firmware_bits);
	return NULL;
}

gchar *
fu_uefi_get_esp_app_path(FuDevice *device, const gchar *esp_path, const gchar *cmd, GError **error)
{
	const gchar *suffix = fu_uefi_bootmgr_get_suffix(error);
	g_autofree gchar *base = NULL;
	if (suffix == NULL)
		return NULL;
	base = fu_uefi_get_esp_path_for_os(device, esp_path);
	return g_strdup_printf("%s/%s%s.efi", base, cmd, suffix);
}

gchar *
fu_uefi_get_built_app_path(GError **error)
{
	const gchar *extension = "";
	const gchar *suffix;
	g_autofree gchar *source_path = NULL;
	g_autofree gchar *prefix = NULL;
	if (fu_efivar_secure_boot_enabled())
		extension = ".signed";
	suffix = fu_uefi_bootmgr_get_suffix(error);
	if (suffix == NULL)
		return NULL;
	prefix = fu_common_get_path(FU_PATH_KIND_EFIAPPDIR);
	source_path = g_strdup_printf("%s/fwupd%s.efi%s", prefix, suffix, extension);
	if (!g_file_test(source_path, G_FILE_TEST_EXISTS)) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_FOUND,
			    "%s cannot be found",
			    source_path);
		return NULL;
	}
	return g_steal_pointer(&source_path);
}

gboolean
fu_uefi_get_framebuffer_size(guint32 *width, guint32 *height, GError **error)
{
	guint32 height_tmp;
	guint32 width_tmp;
	g_autofree gchar *sysfsdriverdir = NULL;
	g_autofree gchar *fbdir = NULL;

	sysfsdriverdir = fu_common_get_path(FU_PATH_KIND_SYSFSDIR_DRIVERS);
	fbdir = g_build_filename(sysfsdriverdir, "efi-framebuffer", "efi-framebuffer.0", NULL);
	if (!g_file_test(fbdir, G_FILE_TEST_EXISTS)) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "EFI framebuffer not found");
		return FALSE;
	}
	height_tmp = fu_uefi_read_file_as_uint64(fbdir, "height");
	width_tmp = fu_uefi_read_file_as_uint64(fbdir, "width");
	if (width_tmp == 0 || height_tmp == 0) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "EFI framebuffer has invalid size "
			    "%" G_GUINT32_FORMAT "x%" G_GUINT32_FORMAT,
			    width_tmp,
			    height_tmp);
		return FALSE;
	}
	if (width != NULL)
		*width = width_tmp;
	if (height != NULL)
		*height = height_tmp;
	return TRUE;
}

gboolean
fu_uefi_get_bitmap_size(const guint8 *buf,
			gsize bufsz,
			guint32 *width,
			guint32 *height,
			GError **error)
{
	guint32 ui32;

	g_return_val_if_fail(buf != NULL, FALSE);

	/* check header */
	if (bufsz < 26 || memcmp(buf, "BM", 2) != 0) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "invalid BMP header signature");
		return FALSE;
	}

	/* starting address */
	if (!fu_common_read_uint32_safe(buf, bufsz, 10, &ui32, G_LITTLE_ENDIAN, error))
		return FALSE;
	if (ui32 < 26) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "BMP header invalid @ %" G_GUINT32_FORMAT "x",
			    ui32);
		return FALSE;
	}

	/* BITMAPINFOHEADER header */
	if (!fu_common_read_uint32_safe(buf, bufsz, 14, &ui32, G_LITTLE_ENDIAN, error))
		return FALSE;
	if (ui32 < 26 - 14) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "BITMAPINFOHEADER invalid @ %" G_GUINT32_FORMAT "x",
			    ui32);
		return FALSE;
	}

	/* dimensions */
	if (width != NULL) {
		if (!fu_common_read_uint32_safe(buf, bufsz, 18, width, G_LITTLE_ENDIAN, error))
			return FALSE;
	}
	if (height != NULL) {
		if (!fu_common_read_uint32_safe(buf, bufsz, 22, height, G_LITTLE_ENDIAN, error))
			return FALSE;
	}
	return TRUE;
}

gchar *
fu_uefi_get_esp_path_for_os(FuDevice *device, const gchar *base)
{
#ifndef EFI_OS_DIR
	const gchar *os_release_id = NULL;
	const gchar *id_like = NULL;
	g_autofree gchar *esp_path = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GHashTable) os_release = fwupd_get_os_release(&error_local);
	/* try to lookup /etc/os-release ID key */
	if (os_release != NULL) {
		os_release_id = g_hash_table_lookup(os_release, "ID");
	} else {
		g_debug("failed to get ID: %s", error_local->message);
	}
	if (os_release_id == NULL)
		os_release_id = "unknown";
	/* if ID key points at something existing return it */
	esp_path = g_build_filename(base, "EFI", os_release_id, NULL);
	if (g_file_test(esp_path, G_FILE_TEST_IS_DIR) || os_release == NULL)
		return g_steal_pointer(&esp_path);
	/* if ID key doesn't exist, try ID_LIKE */
	id_like = g_hash_table_lookup(os_release, "ID_LIKE");
	if (id_like != NULL) {
		g_auto(GStrv) split = g_strsplit(id_like, " ", -1);
		for (guint i = 0; split[i] != NULL; i++) {
			g_autofree gchar *id_like_path =
			    g_build_filename(base, "EFI", split[i], NULL);
			if (g_file_test(id_like_path, G_FILE_TEST_IS_DIR)) {
				g_debug("Using ID_LIKE key from os-release");
				return g_steal_pointer(&id_like_path);
			}
		}
	}
	/* try to fallback to use UEFI removable path if ID_LIKE path doesn't exist */
	if (fu_device_has_private_flag(device, FU_UEFI_DEVICE_FLAG_FALLBACK_TO_REMOVABLE_PATH)) {
		esp_path = g_build_filename(base, "EFI", "boot", NULL);
		if (!g_file_test(esp_path, G_FILE_TEST_IS_DIR))
			g_debug("failed to fallback due to missing %s", esp_path);
	}
	return g_steal_pointer(&esp_path);
#else
	return g_build_filename(base, "EFI", EFI_OS_DIR, NULL);
#endif
}

guint64
fu_uefi_read_file_as_uint64(const gchar *path, const gchar *attr_name)
{
	g_autofree gchar *data = NULL;
	g_autofree gchar *fn = g_build_filename(path, attr_name, NULL);
	if (!g_file_get_contents(fn, &data, NULL, NULL))
		return 0x0;
	return fu_common_strtoull(data);
}

gboolean
fu_uefi_cmp_asset(const gchar *source, const gchar *target)
{
	gsize len = 0;
	g_autofree gchar *source_csum = NULL;
	g_autofree gchar *source_data = NULL;
	g_autofree gchar *target_csum = NULL;
	g_autofree gchar *target_data = NULL;

	/* nothing in target yet */
	if (!g_file_test(target, G_FILE_TEST_EXISTS))
		return FALSE;

	/* test if the file needs to be updated */
	if (!g_file_get_contents(source, &source_data, &len, NULL))
		return FALSE;
	source_csum = g_compute_checksum_for_data(G_CHECKSUM_SHA256, (guchar *)source_data, len);
	if (!g_file_get_contents(target, &target_data, &len, NULL))
		return FALSE;
	target_csum = g_compute_checksum_for_data(G_CHECKSUM_SHA256, (guchar *)target_data, len);
	return g_strcmp0(target_csum, source_csum) == 0;
}

gboolean
fu_uefi_copy_asset(const gchar *source, const gchar *target, GError **error)
{
	g_autoptr(GFile) source_file = g_file_new_for_path(source);
	g_autoptr(GFile) target_file = g_file_new_for_path(target);

	if (!g_file_copy(source_file,
			 target_file,
			 G_FILE_COPY_OVERWRITE,
			 NULL,
			 NULL,
			 NULL,
			 error)) {
		g_prefix_error(error, "Failed to copy %s to %s: ", source, target);
		return FALSE;
	}

	return TRUE;
}
