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

#include "config.h"

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

struct _FuUefiDbDevice {
	FuUefiDevice parent_instance;
};

G_DEFINE_TYPE(FuUefiDbDevice, fu_uefi_db_device, FU_TYPE_UEFI_DEVICE)

#define FU_UEFI_DB_DEVICE_DEFAULT_REQUIRED_FREE (16 * FU_KB) /* bytes */

static gboolean
fu_uefi_db_device_probe(FuDevice *device, GError **error)
{
	FuContext *ctx = fu_device_get_context(device);
	g_autoptr(FuFirmware) siglist = NULL;
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);
	g_autoptr(GPtrArray) default_sigs = NULL;
	g_autoptr(GPtrArray) sigs = NULL;

	/* FuUefiDevice->probe */
	if (!FU_DEVICE_CLASS(fu_uefi_db_device_parent_class)->probe(device, error))
		return FALSE;

	/* for specific vendors we care about the default value */
	if (fu_context_has_hwid_flag(ctx, "use-db-default-ids")) {
		g_autoptr(FuFirmware) firmware_default = NULL;
		g_autoptr(GError) error_local = NULL;
		firmware_default = fu_uefi_device_read_default(FU_UEFI_DEVICE(device),
							       FU_EFIVARS_GUID_EFI_GLOBAL,
							       "db",
							       &error_local);
		if (firmware_default == NULL) {
			g_debug("failed to get dbDefault, ignoring: %s", error_local->message);
		} else {
			default_sigs = fu_firmware_get_images(firmware_default);
		}
	}

	/* add each subdevice */
	siglist = fu_device_read_firmware(device,
					  progress,
					  FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM,
					  error);
	if (siglist == NULL) {
		g_prefix_error_literal(error, "failed to parse db: ");
		return FALSE;
	}
	sigs = fu_efi_signature_list_get_newest(FU_EFI_SIGNATURE_LIST(siglist));
	for (guint i = 0; i < sigs->len; i++) {
		FuEfiSignature *sig = g_ptr_array_index(sigs, i);
		g_autoptr(FuEfiX509Device) x509_device = NULL;
		if (fu_efi_signature_get_kind(sig) != FU_EFI_SIGNATURE_KIND_X509)
			continue;
		x509_device = fu_efi_x509_device_new(ctx, FU_EFI_X509_SIGNATURE(sig));
		fu_device_add_flag(FU_DEVICE(x509_device), FWUPD_DEVICE_FLAG_AFFECTS_FDE);
		fu_device_add_private_flag(FU_DEVICE(x509_device),
					   FU_DEVICE_PRIVATE_FLAG_REQUIRES_UNLOCK_SECUREBOOT);
		fu_device_set_physical_id(FU_DEVICE(x509_device), "db");
		fu_device_set_proxy(FU_DEVICE(x509_device), device);
		if (!fu_device_probe(FU_DEVICE(x509_device), error))
			return FALSE;
		for (guint j = 0; default_sigs != NULL && j < default_sigs->len; j++) {
			FuFirmware *default_sig = g_ptr_array_index(default_sigs, j);
			if (fu_firmware_get_id(default_sig) == NULL)
				continue;
			if (g_strcmp0(fu_firmware_get_id(default_sig),
				      fu_firmware_get_id(FU_FIRMWARE(sig))) == 0)
				continue;
			fu_device_add_instance_strup(FU_DEVICE(x509_device),
						     "CRTD",
						     fu_firmware_get_id(default_sig));
			if (!fu_device_build_instance_id(FU_DEVICE(x509_device),
							 error,
							 "UEFI",
							 "CRT",
							 "CRTD",
							 NULL))
				return FALSE;
		}
		fu_device_add_child(device, FU_DEVICE(x509_device));
	}

	/* success */
	return TRUE;
}

static gboolean
fu_uefi_db_device_write_firmware(FuDevice *device,
				 FuFirmware *firmware,
				 FuProgress *progress,
				 FwupdInstallFlags flags,
				 GError **error)
{
	g_autoptr(GBytes) fw = NULL;

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

	/* write entire chunk to efivarsfs */
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
	if (!fu_uefi_device_set_efivar_bytes(
		FU_UEFI_DEVICE(device),
		FU_EFIVARS_GUID_SECURITY_DATABASE,
		fu_device_get_physical_id(device),
		fw,
		FU_EFI_VARIABLE_ATTR_APPEND_WRITE |
		    FU_EFI_VARIABLE_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS |
		    FU_EFI_VARIABLE_ATTR_RUNTIME_ACCESS | FU_EFI_VARIABLE_ATTR_BOOTSERVICE_ACCESS |
		    FU_EFI_VARIABLE_ATTR_NON_VOLATILE,
		error)) {
		return FALSE;
	}

	/* success! */
	return TRUE;
}

static void
fu_uefi_db_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs)
{
	GPtrArray *children = fu_device_get_children(device);
	gboolean seen_old = FALSE;
	gboolean seen_new = FALSE;
	g_autoptr(FwupdSecurityAttr) attr = NULL;

	/* create attr */
	attr = fu_device_security_attr_new(device, FWUPD_SECURITY_ATTR_ID_UEFI_DB);
	fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID);
	fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE);
	fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW);
	fu_security_attrs_append(attrs, attr);

	/* look for both versions of the Microsoft UEFI CA */
	for (guint i = 0; i < children->len; i++) {
		FuDevice *child = g_ptr_array_index(children, i);
		if (fu_device_has_instance_id(child,
					      "UEFI\\CRT_7CD7437C555F89E7C2B50E21937E420C4E583E80",
					      FU_DEVICE_INSTANCE_FLAG_VISIBLE)) {
			seen_new = TRUE;
		}
		if (fu_device_has_instance_id(child,
					      "UEFI\\CRT_E30CF09DABEAB32A6E3B07A7135245DE05FFB658",
					      FU_DEVICE_INSTANCE_FLAG_VISIBLE)) {
			seen_old = TRUE;
		}
	}

	if (!seen_new && !seen_old) {
		/* user is using a custom UEFI db, just ignore this HSI attribute */
		fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS);
		fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND);
	} else if (seen_new) {
		fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS);
	} else {
		fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID);
	}
}

static void
fu_uefi_db_device_set_progress(FuDevice *device, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
	fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload");
}

static void
fu_uefi_db_device_init(FuUefiDbDevice *self)
{
	fu_device_set_physical_id(FU_DEVICE(self), "db");
	fu_device_set_name(FU_DEVICE(self), "UEFI Signature Database");
	fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD);
	fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EFI_SIGNATURE_LIST);
	fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_APPLICATION_CERTIFICATE);
	fu_device_set_required_free(FU_DEVICE(self), FU_UEFI_DB_DEVICE_DEFAULT_REQUIRED_FREE);
}

static void
fu_uefi_db_device_class_init(FuUefiDbDeviceClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->probe = fu_uefi_db_device_probe;
	device_class->write_firmware = fu_uefi_db_device_write_firmware;
	device_class->add_security_attrs = fu_uefi_db_device_add_security_attrs;
	device_class->set_progress = fu_uefi_db_device_set_progress;
	fu_device_register_private_flag(device_class, FU_UEFI_DEVICE_PRIVATE_FLAG_IS_EXTERNAL);
}
