/*
 * Copyright 2021 Mario Limonciello <mario.limonciello@amd.com>
 * Copyright 2021 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

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

struct _FuUefiGrubDevice {
	FuUefiCapsuleDevice parent_instance;
};

G_DEFINE_TYPE(FuUefiGrubDevice, fu_uefi_grub_device, FU_TYPE_UEFI_CAPSULE_DEVICE)

static gboolean
fu_uefi_grub_device_mkconfig(FuUefiCapsuleDevice *self, const gchar *app_dst, GError **error)
{
	FuContext *ctx = fu_device_get_context(FU_DEVICE(self));
	FuPathStore *pstore = fu_context_get_path_store(ctx);
	FuVolume *esp = fu_uefi_capsule_device_get_esp(self);
	g_autofree gchar *fn_grub_cfg = NULL;
	const gchar *argv_mkconfig[] = {"", "-o", "grub.cfg", NULL};
	const gchar *argv_reboot[] = {"", "fwupd", NULL};
	gboolean exists_mkconfig = FALSE;
	g_autofree gchar *esp_path = NULL;
	g_autofree gchar *grub_mkconfig = NULL;
	g_autofree gchar *grub_reboot = NULL;
	g_autofree gchar *grub_target = NULL;
	g_autofree gchar *output = NULL;
	g_autoptr(GString) str = g_string_new(NULL);

	/* sanity check */
	if (esp == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "could not find ESP volume");
		return FALSE;
	}
	esp_path = fu_volume_get_mount_point(esp);
	if (esp_path == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "could not determine ESP mount point");
		return FALSE;
	}

	/* find grub.conf */
	fn_grub_cfg = fu_context_build_filename(ctx,
						error,
						FU_PATH_KIND_HOSTFS_BOOT,
						"grub",
						"grub.cfg",
						NULL);
	if (fn_grub_cfg == NULL)
		return FALSE;
	if (!fu_device_query_file_exists(FU_DEVICE(self), fn_grub_cfg, &exists_mkconfig, error))
		return FALSE;

	/* try harder */
	if (!exists_mkconfig) {
		g_free(fn_grub_cfg);
		fn_grub_cfg = fu_context_build_filename(ctx,
							error,
							FU_PATH_KIND_HOSTFS_BOOT,
							"grub2",
							"grub.cfg",
							NULL);
		if (fn_grub_cfg == NULL)
			return FALSE;
	}
	if (!fu_device_query_file_exists(FU_DEVICE(self), fn_grub_cfg, &exists_mkconfig, error))
		return FALSE;
	if (!exists_mkconfig) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "could not find grub.conf");
		return FALSE;
	}

	/* find grub-mkconfig */
	grub_mkconfig = fu_path_store_find_program(pstore, "grub-mkconfig", NULL);
	if (grub_mkconfig == NULL)
		grub_mkconfig = fu_path_store_find_program(pstore, "grub2-mkconfig", NULL);
	if (grub_mkconfig == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "could not find grub-mkconfig");
		return FALSE;
	}

	/* find grub-reboot */
	grub_reboot = fu_path_store_find_program(pstore, "grub-reboot", NULL);
	if (grub_reboot == NULL)
		grub_reboot = fu_path_store_find_program(pstore, "grub2-reboot", NULL);
	if (grub_reboot == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "could not find grub-reboot");
		return FALSE;
	}

	/* replace ESP info in conf with what we detected */
	g_string_append_printf(str, "EFI_PATH=%s\n", app_dst);
	g_string_replace(str, esp_path, "", 0);
	g_string_append_printf(str, "ESP=%s\n", esp_path);
	grub_target = fu_context_build_filename(ctx,
						error,
						FU_PATH_KIND_LOCALSTATEDIR_PKG,
						"uefi_capsule.conf",
						NULL);
	if (grub_target == NULL)
		return FALSE;
	if (!fu_path_mkdir_parent(grub_target, error))
		return FALSE;
	if (!g_file_set_contents(grub_target, str->str, -1, error)) {
		fwupd_error_convert(error);
		return FALSE;
	}

	/* refresh GRUB configuration: nocheck:blocked */
	argv_mkconfig[0] = grub_mkconfig;
	argv_mkconfig[2] = fn_grub_cfg;
	if (!g_spawn_sync(NULL,
			  (gchar **)argv_mkconfig,
			  NULL,
			  G_SPAWN_DEFAULT,
			  NULL,
			  NULL,
			  &output,
			  NULL,
			  NULL,
			  error)) {
		fwupd_error_convert(error);
		return FALSE;
	}
	g_debug("%s", output);

	/* skip for self tests */
	if (g_getenv("FWUPD_UEFI_TEST") != NULL)
		return TRUE;

	/* make fwupd default */
	argv_reboot[0] = grub_reboot;
	if (!g_spawn_sync(NULL,
			  (gchar **)argv_reboot,
			  NULL,
			  G_SPAWN_DEFAULT,
			  NULL,
			  NULL,
			  NULL,
			  NULL,
			  NULL,
			  error)) {
		fwupd_error_convert(error);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_uefi_grub_device_write_firmware(FuDevice *device,
				   FuFirmware *firmware,
				   FuProgress *progress,
				   FwupdInstallFlags flags,
				   GError **error)
{
	FuContext *ctx = fu_device_get_context(device);
	FuEfivars *efivars = fu_context_get_efivars(ctx);
	FuPathStore *pstore = fu_context_get_path_store(ctx);
	FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device);
	const gchar *fw_class = fu_uefi_capsule_device_get_guid(self);
	g_autofree gchar *basename = NULL;
	g_autofree gchar *fn = NULL;
	g_autofree gchar *app_basename = NULL;
	g_autofree gchar *app_src = NULL;
	g_autofree gchar *app_dst = NULL;
	g_autofree gchar *varname = fu_uefi_capsule_device_build_varname(self);
	g_autoptr(GBytes) fixed_fw = NULL;
	g_autoptr(GBytes) fw = NULL;

	/* ensure we have the existing state */
	if (fw_class == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "cannot update device info with no GUID");
		return FALSE;
	}

	/* get default image */
	fw = fu_firmware_get_bytes(firmware, error);
	if (fw == NULL)
		return FALSE;

	/* save the blob to the ESP */
	basename = g_strdup_printf("fwupd-%s.cap", fw_class);
	fn = fu_uefi_capsule_device_build_efi_path(FU_UEFI_CAPSULE_DEVICE(device),
						   error,
						   "fw",
						   basename,
						   NULL);
	if (fn == NULL)
		return FALSE;
	if (!fu_path_mkdir_parent(fn, error))
		return FALSE;
	fixed_fw = fu_uefi_capsule_device_fixup_firmware(self, fw, error);
	if (fixed_fw == NULL)
		return FALSE;
	if (!fu_bytes_set_contents(fn, fixed_fw, error))
		return FALSE;

	/* enable debugging in the EFI binary */
	if (!fu_uefi_capsule_device_perhaps_enable_debugging(self, error))
		return FALSE;

	/* delete the old log to save space */
	if (fu_efivars_exists(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG")) {
		if (!fu_efivars_delete(efivars,
				       FU_EFIVARS_GUID_FWUPDATE,
				       "FWUPDATE_DEBUG_LOG",
				       error))
			return FALSE;
	}

	/* set the blob header shared with fwupd.efi */
	if (!fu_uefi_capsule_device_write_update_info(self, fn, varname, fw_class, error))
		return FALSE;

	/* if secure boot was turned on this might need to be installed separately */
	app_basename = fu_uefi_capsule_build_app_basename(pstore, "fwupd", error);
	if (app_basename == NULL)
		return FALSE;
	app_src = fu_uefi_get_built_app_path(pstore, efivars, app_basename, error);
	if (app_src == NULL)
		return FALSE;
	app_dst = fu_uefi_capsule_device_build_efi_path(FU_UEFI_CAPSULE_DEVICE(device),
							error,
							app_basename,
							NULL);
	if (app_dst == NULL)
		return FALSE;

	/* test if correct asset in place */
	if (!fu_uefi_esp_target_verify(app_src, app_dst)) {
		if (!fu_uefi_esp_target_copy(app_src, app_dst, error))
			return FALSE;
	}

	/* we are using GRUB instead of NVRAM variables */
	return fu_uefi_grub_device_mkconfig(self, app_dst, error);
}

static void
fu_uefi_grub_device_report_metadata_pre(FuDevice *device, GHashTable *metadata)
{
	/* FuUefiCapsuleDevice */
	FU_DEVICE_CLASS(fu_uefi_grub_device_parent_class)->report_metadata_pre(device, metadata);
	g_hash_table_insert(metadata, g_strdup("CapsuleApplyMethod"), g_strdup("grub"));
}

static gboolean
fu_uefi_grub_device_prepare(FuDevice *device,
			    FuProgress *progress,
			    FwupdInstallFlags flags,
			    GError **error)
{
	/* FuUefiCapsuleDevice->prepare */
	if (!FU_DEVICE_CLASS(fu_uefi_grub_device_parent_class)
		 ->prepare(device, progress, flags, error))
		return FALSE;

	/* sanity checks */
	return fu_uefi_capsule_device_check_asset(FU_UEFI_CAPSULE_DEVICE(device), error);
}

static void
fu_uefi_grub_device_init(FuUefiGrubDevice *self)
{
	fu_device_set_summary(FU_DEVICE(self),
			      "UEFI System Resource Table device (updated via grub)");
}

static void
fu_uefi_grub_device_class_init(FuUefiGrubDeviceClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->write_firmware = fu_uefi_grub_device_write_firmware;
	device_class->report_metadata_pre = fu_uefi_grub_device_report_metadata_pre;
	device_class->prepare = fu_uefi_grub_device_prepare;
}
