/*
 * qemu_firmware.c: QEMU firmware
 *
 * Copyright (C) 2019 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "qemu_firmware.h"
#include "qemu_interop_config.h"
#include "configmake.h"
#include "qemu_capabilities.h"
#include "qemu_domain.h"
#include "qemu_process.h"
#include "domain_validate.h"
#include "virarch.h"
#include "virjson.h"
#include "virlog.h"
#include "viralloc.h"
#include "virenum.h"
#include "virstring.h"

#define VIR_FROM_THIS VIR_FROM_QEMU

VIR_LOG_INIT("qemu.qemu_firmware");


typedef enum {
    QEMU_FIRMWARE_OS_INTERFACE_NONE = 0,
    QEMU_FIRMWARE_OS_INTERFACE_BIOS,
    QEMU_FIRMWARE_OS_INTERFACE_OPENFIRMWARE,
    QEMU_FIRMWARE_OS_INTERFACE_UBOOT,
    QEMU_FIRMWARE_OS_INTERFACE_UEFI,

    QEMU_FIRMWARE_OS_INTERFACE_LAST
} qemuFirmwareOSInterface;

VIR_ENUM_DECL(qemuFirmwareOSInterface);
VIR_ENUM_IMPL(qemuFirmwareOSInterface,
              QEMU_FIRMWARE_OS_INTERFACE_LAST,
              "",
              "bios",
              "openfirmware",
              "uboot",
              "uefi",
);


typedef enum {
    QEMU_FIRMWARE_FLASH_MODE_SPLIT,
    QEMU_FIRMWARE_FLASH_MODE_COMBINED,
    QEMU_FIRMWARE_FLASH_MODE_STATELESS,

    QEMU_FIRMWARE_FLASH_MODE_LAST,
} qemuFirmwareFlashMode;

VIR_ENUM_DECL(qemuFirmwareFlashMode);
VIR_ENUM_IMPL(qemuFirmwareFlashMode,
              QEMU_FIRMWARE_FLASH_MODE_LAST,
              "split",
              "combined",
              "stateless",
);

typedef struct _qemuFirmwareFlashFile qemuFirmwareFlashFile;
struct _qemuFirmwareFlashFile {
    char *filename;
    char *format;
};


typedef struct _qemuFirmwareMappingFlash qemuFirmwareMappingFlash;
struct _qemuFirmwareMappingFlash {
    qemuFirmwareFlashMode mode;
    qemuFirmwareFlashFile executable;
    qemuFirmwareFlashFile nvram_template;
};


typedef struct _qemuFirmwareMappingKernel qemuFirmwareMappingKernel;
struct _qemuFirmwareMappingKernel {
    char *filename;
};


typedef struct _qemuFirmwareMappingMemory qemuFirmwareMappingMemory;
struct _qemuFirmwareMappingMemory {
    char *filename;
};


typedef enum {
    QEMU_FIRMWARE_DEVICE_NONE = 0,
    QEMU_FIRMWARE_DEVICE_FLASH,
    QEMU_FIRMWARE_DEVICE_KERNEL,
    QEMU_FIRMWARE_DEVICE_MEMORY,

    QEMU_FIRMWARE_DEVICE_LAST
} qemuFirmwareDevice;

VIR_ENUM_DECL(qemuFirmwareDevice);
VIR_ENUM_IMPL(qemuFirmwareDevice,
              QEMU_FIRMWARE_DEVICE_LAST,
              "",
              "flash",
              "kernel",
              "memory",
);


typedef struct _qemuFirmwareMapping qemuFirmwareMapping;
struct _qemuFirmwareMapping {
    qemuFirmwareDevice device;

    union {
        qemuFirmwareMappingFlash flash;
        qemuFirmwareMappingKernel kernel;
        qemuFirmwareMappingMemory memory;
    } data;
};


typedef struct _qemuFirmwareTarget qemuFirmwareTarget;
struct _qemuFirmwareTarget {
    virArch architecture;
    size_t nmachines;
    char **machines;
};


typedef enum {
    QEMU_FIRMWARE_FEATURE_NONE = 0,
    QEMU_FIRMWARE_FEATURE_ACPI_S3,
    QEMU_FIRMWARE_FEATURE_ACPI_S4,
    QEMU_FIRMWARE_FEATURE_AMD_SEV,
    QEMU_FIRMWARE_FEATURE_AMD_SEV_ES,
    QEMU_FIRMWARE_FEATURE_AMD_SEV_SNP,
    QEMU_FIRMWARE_FEATURE_INTEL_TDX,
    QEMU_FIRMWARE_FEATURE_ENROLLED_KEYS,
    QEMU_FIRMWARE_FEATURE_REQUIRES_SMM,
    QEMU_FIRMWARE_FEATURE_SECURE_BOOT,
    QEMU_FIRMWARE_FEATURE_VERBOSE_DYNAMIC,
    QEMU_FIRMWARE_FEATURE_VERBOSE_STATIC,

    QEMU_FIRMWARE_FEATURE_LAST
} qemuFirmwareFeature;

VIR_ENUM_DECL(qemuFirmwareFeature);
VIR_ENUM_IMPL(qemuFirmwareFeature,
              QEMU_FIRMWARE_FEATURE_LAST,
              "",
              "acpi-s3",
              "acpi-s4",
              "amd-sev",
              "amd-sev-es",
              "amd-sev-snp",
              "intel-tdx",
              "enrolled-keys",
              "requires-smm",
              "secure-boot",
              "verbose-dynamic",
              "verbose-static"
);


struct _qemuFirmware {
    /* Description intentionally not parsed. */

    size_t ninterfaces;
    qemuFirmwareOSInterface *interfaces;

    qemuFirmwareMapping mapping;

    size_t ntargets;
    qemuFirmwareTarget **targets;

    size_t nfeatures;
    qemuFirmwareFeature *features;

    /* Tags intentionally not parsed. */
};


static void
qemuFirmwareOSInterfaceFree(qemuFirmwareOSInterface *interfaces)
{
    g_free(interfaces);
}


G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuFirmwareOSInterface, qemuFirmwareOSInterfaceFree);


static void
qemuFirmwareFlashFileFreeContent(qemuFirmwareFlashFile *flash)
{
    g_free(flash->filename);
    g_free(flash->format);
}


static void
qemuFirmwareMappingFlashFreeContent(qemuFirmwareMappingFlash *flash)
{
    qemuFirmwareFlashFileFreeContent(&flash->executable);
    qemuFirmwareFlashFileFreeContent(&flash->nvram_template);
}


static void
qemuFirmwareMappingKernelFreeContent(qemuFirmwareMappingKernel *kernel)
{
    g_free(kernel->filename);
}


static void
qemuFirmwareMappingMemoryFreeContent(qemuFirmwareMappingMemory *memory)
{
    g_free(memory->filename);
}


static void
qemuFirmwareMappingFreeContent(qemuFirmwareMapping *mapping)
{
    switch (mapping->device) {
    case QEMU_FIRMWARE_DEVICE_FLASH:
        qemuFirmwareMappingFlashFreeContent(&mapping->data.flash);
        break;
    case QEMU_FIRMWARE_DEVICE_KERNEL:
        qemuFirmwareMappingKernelFreeContent(&mapping->data.kernel);
        break;
    case QEMU_FIRMWARE_DEVICE_MEMORY:
        qemuFirmwareMappingMemoryFreeContent(&mapping->data.memory);
        break;
    case QEMU_FIRMWARE_DEVICE_NONE:
    case QEMU_FIRMWARE_DEVICE_LAST:
        break;
    }
}


static void
qemuFirmwareTargetFree(qemuFirmwareTarget *target)
{
    if (!target)
        return;

    g_strfreev(target->machines);

    g_free(target);
}


G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuFirmwareTarget, qemuFirmwareTargetFree);


static void
qemuFirmwareFeatureFree(qemuFirmwareFeature *features)
{
    g_free(features);
}


G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuFirmwareFeature, qemuFirmwareFeatureFree);


void
qemuFirmwareFree(qemuFirmware *fw)
{
    size_t i;

    if (!fw)
        return;

    qemuFirmwareOSInterfaceFree(fw->interfaces);
    qemuFirmwareMappingFreeContent(&fw->mapping);
    for (i = 0; i < fw->ntargets; i++)
        qemuFirmwareTargetFree(fw->targets[i]);
    g_free(fw->targets);
    qemuFirmwareFeatureFree(fw->features);

    g_free(fw);
}


static int
qemuFirmwareInterfaceParse(const char *path,
                           virJSONValue *doc,
                           qemuFirmware *fw)
{
    virJSONValue *interfacesJSON;
    g_autoptr(qemuFirmwareOSInterface) interfaces = NULL;
    g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
    size_t ninterfaces;
    size_t i;

    if (!(interfacesJSON = virJSONValueObjectGetArray(doc, "interface-types"))) {
        VIR_DEBUG("failed to get interface-types from '%s'", path);
        return -1;
    }

    ninterfaces = virJSONValueArraySize(interfacesJSON);

    interfaces = g_new0(qemuFirmwareOSInterface, ninterfaces);

    for (i = 0; i < ninterfaces; i++) {
        virJSONValue *item = virJSONValueArrayGet(interfacesJSON, i);
        const char *tmpStr = virJSONValueGetString(item);
        int tmp;

        if ((tmp = qemuFirmwareOSInterfaceTypeFromString(tmpStr)) <= 0) {
            VIR_DEBUG("unknown interface type: '%s'", tmpStr);
            return -1;
        }

        virBufferAsprintf(&buf, " %s", tmpStr);
        interfaces[i] = tmp;
    }

    VIR_DEBUG("firmware description path '%s' supported interfaces: %s",
              path, NULLSTR_MINUS(virBufferCurrentContent(&buf)));

    fw->interfaces = g_steal_pointer(&interfaces);
    fw->ninterfaces = ninterfaces;
    return 0;
}


static int
qemuFirmwareFlashFileParse(const char *path,
                           virJSONValue *doc,
                           qemuFirmwareFlashFile *flash)
{
    const char *filename;
    const char *format;

    if (!(filename = virJSONValueObjectGetString(doc, "filename"))) {
        VIR_DEBUG("missing 'filename' in '%s'", path);
        return -1;
    }

    flash->filename = g_strdup(filename);

    if (!(format = virJSONValueObjectGetString(doc, "format"))) {
        VIR_DEBUG("missing 'format' in '%s'", path);
        return -1;
    }

    flash->format = g_strdup(format);

    return 0;
}


static int
qemuFirmwareMappingFlashParse(const char *path,
                              virJSONValue *doc,
                              qemuFirmwareMappingFlash *flash)
{
    virJSONValue *mode;
    virJSONValue *executable;
    virJSONValue *nvram_template;

    if (!(mode = virJSONValueObjectGet(doc, "mode"))) {
        /* Historical default */
        flash->mode = QEMU_FIRMWARE_FLASH_MODE_SPLIT;
    } else {
        const char *modestr = virJSONValueGetString(mode);
        int modeval;
        if (!modestr) {
            VIR_DEBUG("Firmware flash mode value was malformed");
            return -1;
        }
        modeval = qemuFirmwareFlashModeTypeFromString(modestr);
        if (modeval < 0) {
            VIR_DEBUG("Firmware flash mode value '%s' unexpected", modestr);
            return -1;
        }
        flash->mode = modeval;
    }

    if (!(executable = virJSONValueObjectGet(doc, "executable"))) {
        VIR_DEBUG("missing 'executable' in '%s'", path);
        return -1;
    }

    if (qemuFirmwareFlashFileParse(path, executable, &flash->executable) < 0)
        return -1;

    if (flash->mode == QEMU_FIRMWARE_FLASH_MODE_SPLIT) {
        if (!(nvram_template = virJSONValueObjectGet(doc, "nvram-template"))) {
            VIR_DEBUG("missing 'nvram-template' in '%s'", path);
            return -1;
        }

        if (qemuFirmwareFlashFileParse(path, nvram_template, &flash->nvram_template) < 0)
            return -1;
    }

    return 0;
}


static int
qemuFirmwareMappingKernelParse(const char *path,
                               virJSONValue *doc,
                               qemuFirmwareMappingKernel *kernel)
{
    const char *filename;

    if (!(filename = virJSONValueObjectGetString(doc, "filename"))) {
        VIR_DEBUG("missing 'filename' in '%s'", path);
        return -1;
    }

    kernel->filename = g_strdup(filename);

    return 0;
}


static int
qemuFirmwareMappingMemoryParse(const char *path,
                               virJSONValue *doc,
                               qemuFirmwareMappingMemory *memory)
{
    const char *filename;

    if (!(filename = virJSONValueObjectGetString(doc, "filename"))) {
        VIR_DEBUG("missing 'filename' in '%s'", path);
        return -1;
    }

    memory->filename = g_strdup(filename);

    return 0;
}


static int
qemuFirmwareMappingParse(const char *path,
                         virJSONValue *doc,
                         qemuFirmware *fw)
{
    virJSONValue *mapping;
    const char *deviceStr;
    int tmp;

    if (!(mapping = virJSONValueObjectGet(doc, "mapping"))) {
        VIR_DEBUG("missing mapping in '%s'", path);
        return -1;
    }

    if (!(deviceStr = virJSONValueObjectGetString(mapping, "device"))) {
        VIR_DEBUG("missing device type in '%s'", path);
        return -1;
    }

    if ((tmp = qemuFirmwareDeviceTypeFromString(deviceStr)) <= 0) {
        VIR_DEBUG("unknown device type in '%s'", path);
        return -1;
    }

    fw->mapping.device = tmp;

    switch (fw->mapping.device) {
    case QEMU_FIRMWARE_DEVICE_FLASH:
        if (qemuFirmwareMappingFlashParse(path, mapping, &fw->mapping.data.flash) < 0)
            return -1;
        break;
    case QEMU_FIRMWARE_DEVICE_KERNEL:
        if (qemuFirmwareMappingKernelParse(path, mapping, &fw->mapping.data.kernel) < 0)
            return -1;
        break;
    case QEMU_FIRMWARE_DEVICE_MEMORY:
        if (qemuFirmwareMappingMemoryParse(path, mapping, &fw->mapping.data.memory) < 0)
            return -1;
        break;

    case QEMU_FIRMWARE_DEVICE_NONE:
    case QEMU_FIRMWARE_DEVICE_LAST:
        break;
    }

    return 0;
}


static int
qemuFirmwareTargetParse(const char *path,
                        virJSONValue *doc,
                        qemuFirmware *fw)
{
    virJSONValue *targetsJSON;
    qemuFirmwareTarget **targets = NULL;
    size_t ntargets;
    size_t i;
    int ret = -1;

    if (!(targetsJSON = virJSONValueObjectGetArray(doc, "targets"))) {
        VIR_DEBUG("failed to get targets from '%s'", path);
        return -1;
    }

    ntargets = virJSONValueArraySize(targetsJSON);

    targets = g_new0(qemuFirmwareTarget *, ntargets);

    for (i = 0; i < ntargets; i++) {
        virJSONValue *item = virJSONValueArrayGet(targetsJSON, i);
        virJSONValue *machines;
        g_autoptr(qemuFirmwareTarget) t = NULL;
        const char *architectureStr = NULL;
        size_t nmachines;
        size_t j;

        t = g_new0(qemuFirmwareTarget, 1);

        if (!(architectureStr = virJSONValueObjectGetString(item, "architecture"))) {
            VIR_DEBUG("missing 'architecture' in '%s'", path);
            goto cleanup;
        }

        if ((t->architecture = virQEMUCapsArchFromString(architectureStr)) == VIR_ARCH_NONE) {
            VIR_DEBUG("unknown architecture '%s'", architectureStr);
            goto cleanup;
        }

        if (!(machines = virJSONValueObjectGetArray(item, "machines"))) {
            VIR_DEBUG("missing 'machines' in '%s'", path);
            goto cleanup;
        }

        nmachines = virJSONValueArraySize(machines);

        t->machines = g_new0(char *, nmachines + 1);

        for (j = 0; j < nmachines; j++) {
            virJSONValue *machine = virJSONValueArrayGet(machines, j);
            g_autofree char *machineStr = NULL;

            machineStr = g_strdup(virJSONValueGetString(machine));

            VIR_APPEND_ELEMENT_INPLACE(t->machines, t->nmachines, machineStr);
        }

        targets[i] = g_steal_pointer(&t);
    }

    fw->targets = g_steal_pointer(&targets);
    fw->ntargets = ntargets;
    ntargets = 0;
    ret = 0;

 cleanup:
    for (i = 0; i < ntargets; i++)
        qemuFirmwareTargetFree(targets[i]);
    VIR_FREE(targets);
    return ret;
}


static int
qemuFirmwareFeatureParse(const char *path,
                         virJSONValue *doc,
                         qemuFirmware *fw)
{
    virJSONValue *featuresJSON;
    g_autoptr(qemuFirmwareFeature) features = NULL;
    size_t nfeatures;
    size_t nparsed = 0;
    size_t i;

    if (!(featuresJSON = virJSONValueObjectGetArray(doc, "features"))) {
        VIR_DEBUG("failed to get features from '%s'", path);
        return -1;
    }

    nfeatures = virJSONValueArraySize(featuresJSON);

    features = g_new0(qemuFirmwareFeature, nfeatures);

    for (i = 0; i < nfeatures; i++) {
        virJSONValue *item = virJSONValueArrayGet(featuresJSON, i);
        const char *tmpStr = virJSONValueGetString(item);
        int tmp;

        if ((tmp = qemuFirmwareFeatureTypeFromString(tmpStr)) <= 0) {
            VIR_DEBUG("ignoring unknown QEMU firmware feature '%s'", tmpStr);
            continue;
        }

        features[nparsed] = tmp;
        nparsed++;
    }

    fw->features = g_steal_pointer(&features);
    fw->nfeatures = nparsed;
    return 0;
}


/* 1MiB should be enough for everybody (TM) */
#define DOCUMENT_SIZE (1024 * 1024)

qemuFirmware *
qemuFirmwareParse(const char *path)
{
    g_autofree char *cont = NULL;
    g_autoptr(virJSONValue) doc = NULL;
    g_autoptr(qemuFirmware) fw = NULL;

    if (virFileReadAll(path, DOCUMENT_SIZE, &cont) < 0)
        return NULL;

    if (!(doc = virJSONValueFromString(cont))) {
        VIR_DEBUG("unable to parse json file '%s'", path);
        return NULL;
    }

    fw = g_new0(qemuFirmware, 1);

    if (qemuFirmwareInterfaceParse(path, doc, fw) < 0)
        return NULL;

    if (qemuFirmwareMappingParse(path, doc, fw) < 0)
        return NULL;

    if (qemuFirmwareTargetParse(path, doc, fw) < 0)
        return NULL;

    if (qemuFirmwareFeatureParse(path, doc, fw) < 0)
        return NULL;

    return g_steal_pointer(&fw);
}


static int
qemuFirmwareInterfaceFormat(virJSONValue *doc,
                            qemuFirmware *fw)
{
    g_autoptr(virJSONValue) interfacesJSON = NULL;
    size_t i;

    interfacesJSON = virJSONValueNewArray();

    for (i = 0; i < fw->ninterfaces; i++) {
        if (virJSONValueArrayAppendString(interfacesJSON,
                                          qemuFirmwareOSInterfaceTypeToString(fw->interfaces[i])) < 0)
            return -1;
    }

    if (virJSONValueObjectAppend(doc,
                                 "interface-types",
                                 &interfacesJSON) < 0)
        return -1;

    return 0;
}


static virJSONValue *
qemuFirmwareFlashFileFormat(qemuFirmwareFlashFile flash)
{
    g_autoptr(virJSONValue) json = virJSONValueNewObject();
    virJSONValue *ret;

    if (virJSONValueObjectAppendString(json,
                                       "filename",
                                       flash.filename) < 0)
        return NULL;

    if (virJSONValueObjectAppendString(json,
                                       "format",
                                       flash.format) < 0)
        return NULL;

    ret = g_steal_pointer(&json);
    return ret;
}


static int
qemuFirmwareMappingFlashFormat(virJSONValue *mapping,
                               qemuFirmwareMappingFlash *flash)
{
    g_autoptr(virJSONValue) executable = NULL;
    g_autoptr(virJSONValue) nvram_template = NULL;

    if (virJSONValueObjectAppendString(mapping,
                                       "mode",
                                       qemuFirmwareFlashModeTypeToString(flash->mode)) < 0)
        return -1;

    if (!(executable = qemuFirmwareFlashFileFormat(flash->executable)))
        return -1;

    if (virJSONValueObjectAppend(mapping,
                                 "executable",
                                 &executable) < 0)
        return -1;

    if (flash->mode == QEMU_FIRMWARE_FLASH_MODE_SPLIT) {
        if (!(nvram_template = qemuFirmwareFlashFileFormat(flash->nvram_template)))
            return -1;

        if (virJSONValueObjectAppend(mapping,
                                     "nvram-template",
                                     &nvram_template) < 0)
            return -1;
    }

    return 0;
}


static int
qemuFirmwareMappingKernelFormat(virJSONValue *mapping,
                                qemuFirmwareMappingKernel *kernel)
{
    if (virJSONValueObjectAppendString(mapping,
                                       "filename",
                                       kernel->filename) < 0)
        return -1;

    return 0;
}


static int
qemuFirmwareMappingMemoryFormat(virJSONValue *mapping,
                                qemuFirmwareMappingMemory *memory)
{
    if (virJSONValueObjectAppendString(mapping,
                                       "filename",
                                       memory->filename) < 0)
        return -1;

    return 0;
}


static int
qemuFirmwareMappingFormat(virJSONValue *doc,
                          qemuFirmware *fw)
{
    g_autoptr(virJSONValue) mapping = virJSONValueNewObject();

    if (virJSONValueObjectAppendString(mapping,
                                       "device",
                                       qemuFirmwareDeviceTypeToString(fw->mapping.device)) < 0)
        return -1;

    switch (fw->mapping.device) {
    case QEMU_FIRMWARE_DEVICE_FLASH:
        if (qemuFirmwareMappingFlashFormat(mapping, &fw->mapping.data.flash) < 0)
            return -1;
        break;
    case QEMU_FIRMWARE_DEVICE_KERNEL:
        if (qemuFirmwareMappingKernelFormat(mapping, &fw->mapping.data.kernel) < 0)
            return -1;
        break;
    case QEMU_FIRMWARE_DEVICE_MEMORY:
        if (qemuFirmwareMappingMemoryFormat(mapping, &fw->mapping.data.memory) < 0)
            return -1;
        break;

    case QEMU_FIRMWARE_DEVICE_NONE:
    case QEMU_FIRMWARE_DEVICE_LAST:
        break;
    }

    if (virJSONValueObjectAppend(doc, "mapping", &mapping) < 0)
        return -1;

    return 0;
}


static int
qemuFirmwareTargetFormat(virJSONValue *doc,
                         qemuFirmware *fw)
{
    g_autoptr(virJSONValue) targetsJSON = NULL;
    size_t i;

    targetsJSON = virJSONValueNewArray();

    for (i = 0; i < fw->ntargets; i++) {
        qemuFirmwareTarget *t = fw->targets[i];
        g_autoptr(virJSONValue) target = virJSONValueNewObject();
        g_autoptr(virJSONValue) machines = NULL;
        size_t j;

        if (virJSONValueObjectAppendString(target,
                                           "architecture",
                                           virQEMUCapsArchToString(t->architecture)) < 0)
            return -1;

        machines = virJSONValueNewArray();

        for (j = 0; j < t->nmachines; j++) {
            if (virJSONValueArrayAppendString(machines,
                                              t->machines[j]) < 0)
                return -1;
        }

        if (virJSONValueObjectAppend(target, "machines", &machines) < 0)
            return -1;

        if (virJSONValueArrayAppend(targetsJSON, &target) < 0)
            return -1;
    }

    if (virJSONValueObjectAppend(doc, "targets", &targetsJSON) < 0)
        return -1;

    return 0;
}


static int
qemuFirmwareFeatureFormat(virJSONValue *doc,
                          qemuFirmware *fw)
{
    g_autoptr(virJSONValue) featuresJSON = NULL;
    size_t i;

    featuresJSON = virJSONValueNewArray();

    for (i = 0; i < fw->nfeatures; i++) {
        if (virJSONValueArrayAppendString(featuresJSON,
                                          qemuFirmwareFeatureTypeToString(fw->features[i])) < 0)
            return -1;
    }

    if (virJSONValueObjectAppend(doc,
                                 "features",
                                 &featuresJSON) < 0)
        return -1;

    return 0;
}


char *
qemuFirmwareFormat(qemuFirmware *fw)
{
    g_autoptr(virJSONValue) doc = virJSONValueNewObject();

    if (!fw)
        return NULL;

    if (qemuFirmwareInterfaceFormat(doc, fw) < 0)
        return NULL;

    if (qemuFirmwareMappingFormat(doc, fw) < 0)
        return NULL;

    if (qemuFirmwareTargetFormat(doc, fw) < 0)
        return NULL;

    if (qemuFirmwareFeatureFormat(doc, fw) < 0)
        return NULL;

    return virJSONValueToString(doc, true);
}


int
qemuFirmwareFetchConfigs(char ***firmwares,
                         bool privileged)
{
    return qemuInteropFetchConfigs("firmware", firmwares, privileged);
}


static bool
qemuFirmwareMatchesMachineArch(const qemuFirmware *fw,
                               const char *machine,
                               virArch arch)
{
    size_t i;

    for (i = 0; i < fw->ntargets; i++) {
        size_t j;

        if (arch != fw->targets[i]->architecture)
            continue;

        for (j = 0; j < fw->targets[i]->nmachines; j++) {
            if (g_pattern_match_simple(fw->targets[i]->machines[j], machine))
                return true;
        }
    }

    return false;
}


/**
 * qemuFirmwareMatchesPaths:
 * @fw: firmware definition
 * @loader: loader definition
 * @kernelPath: path to kernel image
 *
 * Checks whether @fw is compatible with the information provided as
 * part of the domain definition.
 *
 * Returns: true if @fw is compatible with @loader and @kernelPath,
 *          false otherwise
 */
static bool
qemuFirmwareMatchesPaths(const qemuFirmware *fw,
                         const virDomainLoaderDef *loader,
                         const char *kernelPath)
{
    const qemuFirmwareMappingFlash *flash = &fw->mapping.data.flash;
    const qemuFirmwareMappingKernel *kernel = &fw->mapping.data.kernel;
    const qemuFirmwareMappingMemory *memory = &fw->mapping.data.memory;

    switch (fw->mapping.device) {
    case QEMU_FIRMWARE_DEVICE_FLASH:
        if (loader && loader->path &&
            STRNEQ(loader->path, flash->executable.filename))
            return false;
        if (loader && loader->nvramTemplate) {
            if (flash->mode != QEMU_FIRMWARE_FLASH_MODE_SPLIT)
                return false;
            if (STRNEQ(loader->nvramTemplate, flash->nvram_template.filename))
                return false;
        }
        break;
    case QEMU_FIRMWARE_DEVICE_MEMORY:
        if (loader && loader->path &&
            STRNEQ(loader->path, memory->filename))
            return false;
        break;
    case QEMU_FIRMWARE_DEVICE_KERNEL:
        if (kernelPath &&
            STRNEQ(kernelPath, kernel->filename))
            return false;
        break;
    case QEMU_FIRMWARE_DEVICE_NONE:
    case QEMU_FIRMWARE_DEVICE_LAST:
        return false;
    }

    return true;
}


static qemuFirmwareOSInterface
qemuFirmwareOSInterfaceTypeFromOsDefFirmware(virDomainOsDefFirmware fw)
{
    switch (fw) {
    case VIR_DOMAIN_OS_DEF_FIRMWARE_BIOS:
        return QEMU_FIRMWARE_OS_INTERFACE_BIOS;
    case VIR_DOMAIN_OS_DEF_FIRMWARE_EFI:
        return QEMU_FIRMWARE_OS_INTERFACE_UEFI;
    case VIR_DOMAIN_OS_DEF_FIRMWARE_NONE:
    case VIR_DOMAIN_OS_DEF_FIRMWARE_LAST:
        break;
    }

    return QEMU_FIRMWARE_OS_INTERFACE_NONE;
}


static virDomainOsDefFirmware
qemuFirmwareOSInterfaceTypeToOsDefFirmware(qemuFirmwareOSInterface interface)
{
    switch (interface) {
    case QEMU_FIRMWARE_OS_INTERFACE_BIOS:
        return VIR_DOMAIN_OS_DEF_FIRMWARE_BIOS;
    case QEMU_FIRMWARE_OS_INTERFACE_UEFI:
        return VIR_DOMAIN_OS_DEF_FIRMWARE_EFI;
    case QEMU_FIRMWARE_OS_INTERFACE_UBOOT:
    case QEMU_FIRMWARE_OS_INTERFACE_OPENFIRMWARE:
    case QEMU_FIRMWARE_OS_INTERFACE_NONE:
    case QEMU_FIRMWARE_OS_INTERFACE_LAST:
        break;
    }

    return VIR_DOMAIN_OS_DEF_FIRMWARE_NONE;
}


static qemuFirmwareOSInterface
qemuFirmwareOSInterfaceTypeFromOsDefLoaderType(virDomainLoader type)
{
    switch (type) {
    case VIR_DOMAIN_LOADER_TYPE_ROM:
        return QEMU_FIRMWARE_OS_INTERFACE_BIOS;
    case VIR_DOMAIN_LOADER_TYPE_PFLASH:
        return QEMU_FIRMWARE_OS_INTERFACE_UEFI;
    case VIR_DOMAIN_LOADER_TYPE_NONE:
    case VIR_DOMAIN_LOADER_TYPE_LAST:
        break;
    }

    return QEMU_FIRMWARE_OS_INTERFACE_NONE;
}


/**
 * qemuFirmwareEnsureNVRAM:
 * @def: domain definition
 * @driver: QEMU driver
 * @abiUpdate: whether a new domain is being defined
 *
 * Make sure that a source for the NVRAM file exists, possibly by
 * creating it. This might involve automatically generating the
 * corresponding path.
 */
static void
qemuFirmwareEnsureNVRAM(virDomainDef *def,
                        virQEMUDriver *driver,
                        bool abiUpdate)
{
    g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver);
    virDomainLoaderDef *loader = def->os.loader;
    const char *ext = NULL;

    if (!loader)
        return;

    if (loader->type != VIR_DOMAIN_LOADER_TYPE_PFLASH)
        return;

    if (loader->readonly != VIR_TRISTATE_BOOL_YES)
        return;

    if (loader->stateless == VIR_TRISTATE_BOOL_YES)
        return;

    /* If the NVRAM format hasn't been set yet, inherit the same as
     * the loader */
    if (loader->nvram && !loader->nvram->format)
        loader->nvram->format = loader->format;

    if (loader->nvram) {
        /* Nothing to do if a proper NVRAM backend is already configured */
        if (!virStorageSourceIsEmpty(loader->nvram))
            return;

        /* otherwise we want to reset and re-populate the definition */
        virObjectUnref(loader->nvram);
    }

    loader->nvram = virStorageSourceNew();
    loader->nvram->type = VIR_STORAGE_TYPE_FILE;

    /* The nvram template format should be always present but as a failsafe,
     * duplicate the loader format if it is not available. */
    if (loader->nvramTemplateFormat > VIR_STORAGE_FILE_NONE)
        loader->nvram->format = loader->nvramTemplateFormat;
    else
        loader->nvram->format = loader->format;

    if (loader->nvram->format == VIR_STORAGE_FILE_RAW) {
        /* The extension used by raw edk2 builds has historically
         * been .fd, but more recent aarch64 builds have started
         * using the .raw extension instead.
         *
         * If we're defining a new domain, we should try to match the
         * extension for the file backing its NVRAM store with the
         * one used by the template to keep things nice and
         * consistent.
         *
         * If we're loading an existing domain, however, we need to
         * stick with the .fd extension to ensure compatibility */
        if (abiUpdate &&
            loader->nvramTemplate &&
            virStringHasSuffix(loader->nvramTemplate, ".raw"))
            ext = ".raw";
        else
            ext = ".fd";
    }
    if (loader->nvram->format == VIR_STORAGE_FILE_QCOW2)
        ext = ".qcow2";

    loader->nvram->path = g_strdup_printf("%s/%s_VARS%s",
                                          cfg->nvramDir, def->name,
                                          NULLSTR_EMPTY(ext));
}



/**
 * qemuFirmwareSetOsFeatures:
 * @def: domain definition
 * @secureBoot: whether the 'secure-boot' feature is enabled
 * @enrolledKeys: whether the 'enrolled-keys' feature is enabled
 *
 * Set firmware features for @def to match those declared by the JSON
 * descriptor that was found to match autoselection requirements.
 */
static void
qemuFirmwareSetOsFeatures(virDomainDef *def,
                          bool secureBoot,
                          bool enrolledKeys)
{
    int *features = def->os.firmwareFeatures;
    virDomainLoaderDef *loader = def->os.loader;

    if (!features) {
        features = g_new0(int, VIR_DOMAIN_OS_DEF_FIRMWARE_FEATURE_LAST);
        def->os.firmwareFeatures = features;
    }

    features[VIR_DOMAIN_OS_DEF_FIRMWARE_FEATURE_SECURE_BOOT] = virTristateBoolFromBool(secureBoot);
    features[VIR_DOMAIN_OS_DEF_FIRMWARE_FEATURE_ENROLLED_KEYS] = virTristateBoolFromBool(enrolledKeys);

    /* If the NVRAM template is blank at this point and we're not dealing
     * with a stateless firmware image, then it means that the NVRAM file
     * is not local. In this scenario we can't really make any assumptions
     * about its contents, so it's preferable to leave the state of the
     * enrolled-keys feature unspecified */
    if (loader &&
        loader->type == VIR_DOMAIN_LOADER_TYPE_PFLASH &&
        loader->stateless != VIR_TRISTATE_BOOL_YES &&
        !loader->nvramTemplate) {
        features[VIR_DOMAIN_OS_DEF_FIRMWARE_FEATURE_ENROLLED_KEYS] = VIR_TRISTATE_BOOL_ABSENT;
    }
}


#define VIR_QEMU_FIRMWARE_AMD_SEV_ES_POLICY (1 << 2)


static bool
qemuFirmwareMatchDomain(const virDomainDef *def,
                        const qemuFirmware *fw,
                        const char *path)
{
    const virDomainLoaderDef *loader = def->os.loader;
    size_t i;
    qemuFirmwareOSInterface want;
    bool supportsS3 = false;
    bool supportsS4 = false;
    bool requiresSMM = false;
    bool supportsSEV = false;
    bool supportsSEVES = false;
    bool supportsSEVSNP = false;
    bool supportsTDX = false;
    bool supportsSecureBoot = false;
    bool hasEnrolledKeys = false;
    int reqSecureBoot;
    int reqEnrolledKeys;

    want = qemuFirmwareOSInterfaceTypeFromOsDefFirmware(def->os.firmware);

    if (want == QEMU_FIRMWARE_OS_INTERFACE_NONE && loader) {
        want = qemuFirmwareOSInterfaceTypeFromOsDefLoaderType(loader->type);
    }

    for (i = 0; i < fw->ninterfaces; i++) {
        if (fw->interfaces[i] == want)
            break;
    }

    if (i == fw->ninterfaces) {
        VIR_DEBUG("No matching interface in '%s'", path);
        return false;
    }

    if (!qemuFirmwareMatchesPaths(fw, def->os.loader, def->os.kernel)) {
        VIR_DEBUG("No matching path in '%s'", path);
        return false;
    }

    if (!qemuFirmwareMatchesMachineArch(fw, def->os.machine, def->os.arch)) {
        VIR_DEBUG("No matching machine type in '%s'", path);
        return false;
    }

    for (i = 0; i < fw->nfeatures; i++) {
        switch (fw->features[i]) {
        case QEMU_FIRMWARE_FEATURE_ACPI_S3:
            supportsS3 = true;
            break;
        case QEMU_FIRMWARE_FEATURE_ACPI_S4:
            supportsS4 = true;
            break;
        case QEMU_FIRMWARE_FEATURE_AMD_SEV:
            supportsSEV = true;
            break;

        case QEMU_FIRMWARE_FEATURE_AMD_SEV_ES:
            supportsSEVES = true;
            break;

        case QEMU_FIRMWARE_FEATURE_AMD_SEV_SNP:
            supportsSEVSNP = true;
            break;

        case QEMU_FIRMWARE_FEATURE_INTEL_TDX:
            supportsTDX = true;
            break;

        case QEMU_FIRMWARE_FEATURE_REQUIRES_SMM:
            requiresSMM = true;
            break;

        case QEMU_FIRMWARE_FEATURE_SECURE_BOOT:
            supportsSecureBoot = true;
            break;

        case QEMU_FIRMWARE_FEATURE_ENROLLED_KEYS:
            hasEnrolledKeys = true;
            break;

        case QEMU_FIRMWARE_FEATURE_VERBOSE_DYNAMIC:
        case QEMU_FIRMWARE_FEATURE_VERBOSE_STATIC:
        case QEMU_FIRMWARE_FEATURE_NONE:
        case QEMU_FIRMWARE_FEATURE_LAST:
            break;
        }
    }

    if (def->pm.s3 == VIR_TRISTATE_BOOL_YES &&
        !supportsS3) {
        VIR_DEBUG("Domain requires S3, firmware '%s' doesn't support it", path);
        return false;
    }

    if (def->pm.s4 == VIR_TRISTATE_BOOL_YES &&
        !supportsS4) {
        VIR_DEBUG("Domain requires S4, firmware '%s' doesn't support it", path);
        return false;
    }

    if (def->os.firmwareFeatures) {
        reqSecureBoot = def->os.firmwareFeatures[VIR_DOMAIN_OS_DEF_FIRMWARE_FEATURE_SECURE_BOOT];
        if (reqSecureBoot == VIR_TRISTATE_BOOL_YES && !supportsSecureBoot) {
            VIR_DEBUG("User requested Secure Boot, firmware '%s' doesn't support it",
                      path);
            return false;
        }
        if (reqSecureBoot == VIR_TRISTATE_BOOL_NO && supportsSecureBoot) {
            VIR_DEBUG("User refused Secure Boot, firmware '%s' supports it", path);
            return false;
        }

        reqEnrolledKeys = def->os.firmwareFeatures[VIR_DOMAIN_OS_DEF_FIRMWARE_FEATURE_ENROLLED_KEYS];
        if (reqEnrolledKeys == VIR_TRISTATE_BOOL_YES && !hasEnrolledKeys) {
            VIR_DEBUG("User requested Enrolled keys, firmware '%s' doesn't have them",
                      path);
            return false;
        }
        if (reqEnrolledKeys == VIR_TRISTATE_BOOL_NO && hasEnrolledKeys) {
            VIR_DEBUG("User refused Enrolled keys, firmware '%s' has them", path);
            return false;
        }
    }

    if (requiresSMM) {
        if (def->features[VIR_DOMAIN_FEATURE_SMM] == VIR_TRISTATE_SWITCH_OFF) {
            VIR_DEBUG("Domain explicitly disables SMM, "
                      "but firmware '%s' requires it to be enabled", path);
            return false;
        }
        if (loader && loader->secure == VIR_TRISTATE_BOOL_NO) {
            VIR_DEBUG("Domain doesn't restrict pflash programming to SMM, "
                      "but firmware '%s' requires use of SMM", path);
            return false;
        }
    } else {
        if (loader && loader->secure == VIR_TRISTATE_BOOL_YES) {
            VIR_DEBUG("Domain restricts pflash programming to SMM, "
                      "but firmware '%s' doesn't support SMM", path);
            return false;
        }
    }

    if (fw->mapping.device == QEMU_FIRMWARE_DEVICE_FLASH) {
        const qemuFirmwareMappingFlash *flash = &fw->mapping.data.flash;

        if (loader && loader->type &&
            loader->type != VIR_DOMAIN_LOADER_TYPE_PFLASH) {
            VIR_DEBUG("Discarding flash loader");
            return false;
        }

        if (loader && loader->stateless == VIR_TRISTATE_BOOL_YES) {
            if (flash->mode != QEMU_FIRMWARE_FLASH_MODE_STATELESS) {
                VIR_DEBUG("Discarding loader without stateless flash");
                return false;
            }
        } else {
            if (flash->mode != QEMU_FIRMWARE_FLASH_MODE_SPLIT) {
                VIR_DEBUG("Discarding loader without split flash");
                return false;
            }
        }

        if (loader &&
            loader->readonly == VIR_TRISTATE_BOOL_NO &&
            flash->mode != QEMU_FIRMWARE_FLASH_MODE_COMBINED) {
            VIR_DEBUG("Discarding readonly loader");
            return false;
        }

        if (STRNEQ(flash->executable.format, "raw") &&
            STRNEQ(flash->executable.format, "qcow2")) {
            VIR_DEBUG("Discarding loader with unsupported flash format '%s'",
                      flash->executable.format);
            return false;
        }
        if (loader && loader->format &&
            STRNEQ(flash->executable.format, virStorageFileFormatTypeToString(loader->format))) {
            VIR_DEBUG("Discarding loader with mismatching flash format '%s' != '%s'",
                      flash->executable.format,
                      virStorageFileFormatTypeToString(loader->format));
            return false;
        }
        if (flash->mode == QEMU_FIRMWARE_FLASH_MODE_SPLIT) {
            if (STRNEQ(flash->nvram_template.format, "raw") &&
                STRNEQ(flash->nvram_template.format, "qcow2")) {
                VIR_DEBUG("Discarding loader with unsupported nvram template format '%s'",
                          flash->nvram_template.format);
                return false;
            }
        }
    } else if (fw->mapping.device == QEMU_FIRMWARE_DEVICE_MEMORY) {
        if (loader && loader->type &&
            loader->type != VIR_DOMAIN_LOADER_TYPE_ROM) {
            VIR_DEBUG("Discarding rom loader");
            return false;
        }

        if (loader && loader->readonly == VIR_TRISTATE_BOOL_NO) {
            VIR_DEBUG("Discarding readonly loader");
            return false;
        }
    }

    if (def->sec) {
        switch (def->sec->sectype) {
        case VIR_DOMAIN_LAUNCH_SECURITY_SEV:
            if (!supportsSEV) {
                VIR_DEBUG("Domain requires SEV, firmware '%s' doesn't support it",
                          path);
                return false;
            }

            if (def->sec->data.sev.policy & VIR_QEMU_FIRMWARE_AMD_SEV_ES_POLICY &&
                !supportsSEVES) {
                VIR_DEBUG("Domain requires SEV-ES, firmware '%s' doesn't support it",
                          path);
                return false;
            }
            break;

        case VIR_DOMAIN_LAUNCH_SECURITY_SEV_SNP:
            if (!supportsSEVSNP) {
                VIR_DEBUG("Domain requires SEV-SNP firmware '%s' doesn't support it",
                          path);
                return false;
            }
            break;

        case VIR_DOMAIN_LAUNCH_SECURITY_TDX:
            if (!supportsTDX) {
                VIR_DEBUG("Domain requires TDX, firmware '%s' doesn't support it",
                          path);
                return false;
            }
            break;

        case VIR_DOMAIN_LAUNCH_SECURITY_PV:
            break;

        case VIR_DOMAIN_LAUNCH_SECURITY_NONE:
        case VIR_DOMAIN_LAUNCH_SECURITY_LAST:
            virReportEnumRangeError(virDomainLaunchSecurity, def->sec->sectype);
            return -1;
        }
    }

    VIR_DEBUG("Firmware '%s' matches domain requirements", path);
    return true;
}


static int
qemuFirmwareEnableFeaturesModern(virDomainDef *def,
                                 const qemuFirmware *fw)
{
    const qemuFirmwareMappingFlash *flash = &fw->mapping.data.flash;
    const qemuFirmwareMappingKernel *kernel = &fw->mapping.data.kernel;
    const qemuFirmwareMappingMemory *memory = &fw->mapping.data.memory;
    virDomainLoaderDef *loader = NULL;
    virStorageFileFormat format;
    bool hasSecureBoot = false;
    bool hasEnrolledKeys = false;
    size_t i;

    switch (fw->mapping.device) {
    case QEMU_FIRMWARE_DEVICE_FLASH:
        if ((format = virStorageFileFormatTypeFromString(flash->executable.format)) < 0)
            return -1;

        if (!def->os.loader)
            def->os.loader = virDomainLoaderDefNew();
        loader = def->os.loader;

        loader->type = VIR_DOMAIN_LOADER_TYPE_PFLASH;
        loader->readonly = VIR_TRISTATE_BOOL_YES;
        loader->format = format;

        VIR_FREE(loader->path);
        loader->path = g_strdup(flash->executable.filename);

        if (flash->mode == QEMU_FIRMWARE_FLASH_MODE_SPLIT) {
            /* Only fill in nvramTemplate if the NVRAM location is already
             * known to be a local path or hasn't been provided, in which
             * case a local path will be generated by libvirt later.
             *
             * We can't create or reset non-local NVRAM files, so filling
             * in nvramTemplate for those would be misleading */
            VIR_FREE(loader->nvramTemplate);
            loader->nvramTemplateFormat = VIR_STORAGE_FILE_NONE;

            if (!loader->nvram || virStorageSourceIsLocalStorage(loader->nvram)) {
                /* validation when parsing the JSON files ensures that we get
                 * only 'raw' and 'qcow2' here. Fall back to sharing format with loader */
                if (flash->nvram_template.format)
                    loader->nvramTemplateFormat = virStorageFileFormatTypeFromString(flash->nvram_template.format);
                else
                    loader->nvramTemplateFormat = loader->format;

                loader->nvramTemplate = g_strdup(flash->nvram_template.filename);
            }
        }

        VIR_DEBUG("decided on firmware '%s' template '%s'",
                  loader->path, NULLSTR(loader->nvramTemplate));
        break;

    case QEMU_FIRMWARE_DEVICE_KERNEL:
        VIR_FREE(def->os.kernel);
        def->os.kernel = g_strdup(kernel->filename);

        VIR_DEBUG("decided on kernel '%s'",
                  def->os.kernel);
        break;

    case QEMU_FIRMWARE_DEVICE_MEMORY:
        if (!def->os.loader)
            def->os.loader = virDomainLoaderDefNew();
        loader = def->os.loader;

        loader->type = VIR_DOMAIN_LOADER_TYPE_ROM;

        VIR_FREE(loader->path);
        loader->path = g_strdup(memory->filename);

        VIR_DEBUG("decided on loader '%s'",
                  loader->path);
        break;

    case QEMU_FIRMWARE_DEVICE_NONE:
    case QEMU_FIRMWARE_DEVICE_LAST:
        break;
    }

    for (i = 0; i < fw->nfeatures; i++) {
        switch (fw->features[i]) {
        case QEMU_FIRMWARE_FEATURE_REQUIRES_SMM:
            VIR_DEBUG("Enabling SMM feature");
            def->features[VIR_DOMAIN_FEATURE_SMM] = VIR_TRISTATE_SWITCH_ON;

            VIR_DEBUG("Enabling secure loader");
            def->os.loader->secure = VIR_TRISTATE_BOOL_YES;
            break;

        case QEMU_FIRMWARE_FEATURE_SECURE_BOOT:
            hasSecureBoot = true;
            break;

        case QEMU_FIRMWARE_FEATURE_ENROLLED_KEYS:
            hasEnrolledKeys = true;
            break;

        case QEMU_FIRMWARE_FEATURE_ACPI_S3:
        case QEMU_FIRMWARE_FEATURE_ACPI_S4:
        case QEMU_FIRMWARE_FEATURE_AMD_SEV:
        case QEMU_FIRMWARE_FEATURE_AMD_SEV_ES:
        case QEMU_FIRMWARE_FEATURE_AMD_SEV_SNP:
        case QEMU_FIRMWARE_FEATURE_INTEL_TDX:
        case QEMU_FIRMWARE_FEATURE_VERBOSE_DYNAMIC:
        case QEMU_FIRMWARE_FEATURE_VERBOSE_STATIC:
        case QEMU_FIRMWARE_FEATURE_NONE:
        case QEMU_FIRMWARE_FEATURE_LAST:
            break;
        }
    }

    if (!def->os.firmware) {
        /* If a firmware type for autoselection was not already present,
         * pick the first reasonable one from the descriptor list */
        for (i = 0; i < fw->ninterfaces; i++) {
            def->os.firmware = qemuFirmwareOSInterfaceTypeToOsDefFirmware(fw->interfaces[i]);
            if (def->os.firmware)
                break;
        }
    }
    if (def->os.firmware) {
        qemuFirmwareSetOsFeatures(def, hasSecureBoot, hasEnrolledKeys);
    }

    return 0;
}


static void
qemuFirmwareSanityCheck(const qemuFirmware *fw,
                        const char *filename)
{
    size_t i;
    bool requiresSMM = false;
    bool supportsSecureBoot = false;
    bool hasEnrolledKeys = false;

    for (i = 0; i < fw->nfeatures; i++) {
        switch (fw->features[i]) {
        case QEMU_FIRMWARE_FEATURE_REQUIRES_SMM:
            requiresSMM = true;
            break;
        case QEMU_FIRMWARE_FEATURE_SECURE_BOOT:
            supportsSecureBoot = true;
            break;
        case QEMU_FIRMWARE_FEATURE_ENROLLED_KEYS:
            hasEnrolledKeys = true;
            break;
        case QEMU_FIRMWARE_FEATURE_NONE:
        case QEMU_FIRMWARE_FEATURE_ACPI_S3:
        case QEMU_FIRMWARE_FEATURE_ACPI_S4:
        case QEMU_FIRMWARE_FEATURE_AMD_SEV:
        case QEMU_FIRMWARE_FEATURE_AMD_SEV_ES:
        case QEMU_FIRMWARE_FEATURE_AMD_SEV_SNP:
        case QEMU_FIRMWARE_FEATURE_INTEL_TDX:
        case QEMU_FIRMWARE_FEATURE_VERBOSE_DYNAMIC:
        case QEMU_FIRMWARE_FEATURE_VERBOSE_STATIC:
        case QEMU_FIRMWARE_FEATURE_LAST:
            break;
        }
    }

    if ((supportsSecureBoot != requiresSMM) ||
        (hasEnrolledKeys && !supportsSecureBoot)) {
        VIR_WARN("Firmware description '%s' has invalid set of features: "
                 "%s = %d, %s = %d, %s = %d",
                 filename,
                 qemuFirmwareFeatureTypeToString(QEMU_FIRMWARE_FEATURE_REQUIRES_SMM),
                 requiresSMM,
                 qemuFirmwareFeatureTypeToString(QEMU_FIRMWARE_FEATURE_SECURE_BOOT),
                 supportsSecureBoot,
                 qemuFirmwareFeatureTypeToString(QEMU_FIRMWARE_FEATURE_ENROLLED_KEYS),
                 hasEnrolledKeys);
    }
}


static ssize_t
qemuFirmwareFetchParsedConfigs(bool privileged,
                               qemuFirmware ***firmwaresRet,
                               char ***pathsRet)
{
    g_auto(GStrv) possiblePaths = NULL;
    char **currentPath = NULL;
    qemuFirmware **firmwares = NULL;
    char **paths = NULL;
    size_t nfirmwares = 0;
    size_t npaths = 0;

    if (qemuFirmwareFetchConfigs(&possiblePaths, privileged) < 0)
        return -1;

    if (!possiblePaths)
        return 0;

    for (currentPath = possiblePaths; *currentPath; currentPath++) {
        qemuFirmware *firmware = qemuFirmwareParse(*currentPath);

        if (!firmware)
            continue;

        VIR_APPEND_ELEMENT(firmwares, nfirmwares, firmware);

        if (pathsRet) {
            char *path = g_strdup(*currentPath);
            VIR_APPEND_ELEMENT(paths, npaths, path);
        }
    }

    *firmwaresRet = firmwares;
    if (pathsRet) {
        char *terminator = NULL;
        VIR_APPEND_ELEMENT(paths, npaths, terminator);
        *pathsRet = paths;
    }

    return nfirmwares;
}


/**
 * qemuFirmwareFillDomainLegacy:
 * @driver: QEMU driver
 * @def: domain definition
 *
 * Go through the legacy list of CODE:VARS pairs looking for a
 * suitable NVRAM template for the user-provided firmware path.
 *
 * Should only be used as a fallback in case looking at the firmware
 * descriptors yielded no results.
 *
 * Returns: 0 on success,
 *          1 if a matching firmware could not be found,
 *          -1 on error.
 */
static int
qemuFirmwareFillDomainLegacy(virQEMUDriver *driver,
                             virDomainDef *def)
{
    g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver);
    virDomainLoaderDef *loader = def->os.loader;
    size_t i;

    if (!loader)
        return 1;

    if (loader->type != VIR_DOMAIN_LOADER_TYPE_PFLASH) {
        VIR_DEBUG("Ignoring legacy entries for '%s' loader",
                  virDomainLoaderTypeToString(loader->type));
        return 1;
    }

    if (loader->readonly == VIR_TRISTATE_BOOL_NO) {
        VIR_DEBUG("Ignoring legacy entries for read-write loader");
        return 1;
    }

    if (loader->stateless == VIR_TRISTATE_BOOL_YES) {
        VIR_DEBUG("Ignoring legacy entries for stateless loader");
        return 1;
    }

    if (loader->format &&
        loader->format != VIR_STORAGE_FILE_RAW) {
        VIR_DEBUG("Ignoring legacy entries for loader with flash format '%s'",
                  virStorageFileFormatTypeToString(loader->format));
        return 1;
    }

    for (i = 0; i < cfg->nfirmwares; i++) {
        virFirmware *fw = cfg->firmwares[i];

        if (STRNEQ(fw->name, loader->path)) {
            VIR_DEBUG("Not matching loader path '%s' for user provided path '%s'",
                      fw->name, loader->path);
            continue;
        }

        loader->type = VIR_DOMAIN_LOADER_TYPE_PFLASH;
        loader->readonly = VIR_TRISTATE_BOOL_YES;
        loader->format = VIR_STORAGE_FILE_RAW;

        /* Only use the default template path if one hasn't been
         * provided by the user. Assume that the template is in 'raw' format.
         *
         * In addition to fully-custom templates, which are a valid
         * use case, we could simply be in a situation where
         * qemu.conf contains
         *
         *   nvram = [
         *     "/path/to/OVMF_CODE.secboot.fd:/path/to/OVMF_VARS.fd",
         *     "/path/to/OVMF_CODE.secboot.fd:/path/to/OVMF_VARS.secboot.fd"
         *   ]
         *
         * and the domain has been configured as
         *
         *   <os>
         *     <loader readonly='yes' type='pflash'>/path/to/OVMF_CODE.secboot.fd</loader>
         *     <nvram template='/path/to/OVMF/OVMF_VARS.secboot.fd'>
         *   </os>
         *
         * In this case, the global default is to have Secure Boot
         * disabled, but the domain configuration explicitly enables
         * it, and we shouldn't overrule this choice */
        if (!loader->nvramTemplate) {
            loader->nvramTemplate = g_strdup(cfg->firmwares[i]->nvram);
            loader->nvramTemplateFormat = VIR_STORAGE_FILE_RAW;
        }

        if (loader->nvramTemplateFormat == VIR_STORAGE_FILE_NONE)
            loader->nvramTemplateFormat = VIR_STORAGE_FILE_RAW;

        VIR_DEBUG("decided on firmware '%s' template '%s'",
                  loader->path, NULLSTR(loader->nvramTemplate));

        return 0;
    }

    return 1;
}


/**
 * qemuFirmwareFillDomainModern:
 * @driver: QEMU driver
 * @def: domain definition
 *
 * Look at the firmware descriptors available on the system and try
 * to find one that matches the user's requested configuration. If
 * successful, @def will be updated so that it explicitly points to
 * the corresponding paths.
 *
 * Returns: 0 on success,
 *          1 if a matching firmware could not be found,
 *          -1 on error.
 */
static int
qemuFirmwareFillDomainModern(virQEMUDriver *driver,
                             virDomainDef *def)
{
    g_auto(GStrv) paths = NULL;
    qemuFirmware **firmwares = NULL;
    ssize_t nfirmwares = 0;
    const qemuFirmware *theone = NULL;
    size_t i;
    int ret = -1;

    if ((nfirmwares = qemuFirmwareFetchParsedConfigs(driver->privileged,
                                                     &firmwares, &paths)) < 0)
        return -1;

    for (i = 0; i < nfirmwares; i++) {
        if (qemuFirmwareMatchDomain(def, firmwares[i], paths[i])) {
            theone = firmwares[i];
            VIR_DEBUG("Found matching firmware (description path '%s')",
                      paths[i]);
            break;
        }
    }

    if (!theone) {
        ret = 1;
        goto cleanup;
    }

    /* Firstly, let's do some sanity checks. If either of these
     * fail we can still start the domain successfully, but it's
     * likely that admin/FW manufacturer messed up. */
    qemuFirmwareSanityCheck(theone, paths[i]);

    if (qemuFirmwareEnableFeaturesModern(def, theone) < 0)
        goto cleanup;

    ret = 0;

 cleanup:
    for (i = 0; i < nfirmwares; i++)
        qemuFirmwareFree(firmwares[i]);
    VIR_FREE(firmwares);
    return ret;
}


/**
 * qemuFirmwareFillDomain:
 * @driver: QEMU driver
 * @def: domain definition
 * @abiUpdate: whether a new domain is being defined
 *
 * Perform firmware selection.
 *
 * When firmware autoselection is used, this means looking at the
 * firmware descriptors available on the system and finding one that
 * matches the user's requested parameters; when manual firmware
 * selection is used, the path to the firmware itself is usually
 * already provided, but other information such as the path to the
 * NVRAM template might be missing.
 *
 * The idea is that calling this function a first time (at PostParse
 * time) will convert whatever partial configuration the user might
 * have provided into a fully specified firmware configuration, such
 * as that calling it a second time (at domain start time) will
 * result in an early successful exit. The same thing should happen
 * if the input configuration wasn't missing any information in the
 * first place.
 *
 * Returns: 0 on success,
 *          -1 on error.
 */
int
qemuFirmwareFillDomain(virQEMUDriver *driver,
                       virDomainDef *def,
                       bool abiUpdate)
{
    virDomainLoaderDef *loader = def->os.loader;
    virStorageSource *nvram = loader ? loader->nvram : NULL;
    bool autoSelection = (def->os.firmware != VIR_DOMAIN_OS_DEF_FIRMWARE_NONE);
    int ret;

    /* Start by performing a thorough validation of the input.
     *
     * We need to do this here because the firmware selection logic
     * can only work correctly if the request is constructed
     * properly; at the same time, we can't rely on Validate having
     * been called ahead of time, because in some situations (such as
     * when loading the configuration of existing domains from disk)
     * that entire phase is intentionally skipped */
    if (virDomainDefOSValidate(def, NULL) < 0)
        return -1;

    if (loader &&
        loader->format &&
        loader->format != VIR_STORAGE_FILE_RAW &&
        loader->format != VIR_STORAGE_FILE_QCOW2) {
        virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
                       _("Unsupported loader format '%1$s'"),
                       virStorageFileFormatTypeToString(loader->format));
        return -1;
    }
    if (nvram &&
        nvram->format &&
        nvram->format != VIR_STORAGE_FILE_RAW &&
        nvram->format != VIR_STORAGE_FILE_QCOW2) {
        virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
                       _("Unsupported nvram format '%1$s'"),
                       virStorageFileFormatTypeToString(nvram->format));
        return -1;
    }

    /* If firmware autoselection is disabled and the loader is a ROM
     * instead of a PFLASH device, then we're using BIOS and we don't
     * need any information at all */
    if (!autoSelection &&
        (!loader || (loader && loader->type == VIR_DOMAIN_LOADER_TYPE_ROM))) {
        return 0;
    }

    /* Look for the information we need in firmware descriptors */
    if ((ret = qemuFirmwareFillDomainModern(driver, def)) < 0)
        return -1;

    if (ret == 1) {
        /* If we haven't found any match among firmware descriptors,
         * that would normally be the end of it.
         *
         * However, in order to handle legacy configurations
         * correctly, we make another attempt at locating the missing
         * information by going through the hardcoded list of
         * CODE:NVRAM pairs that might have been provided at build
         * time */
        if (!autoSelection) {
            if ((ret = qemuFirmwareFillDomainLegacy(driver, def)) < 0)
                return -1;

            /* If we've gotten this far without finding a match, it
             * means that we're dealing with a set of completely
             * custom paths. In that case, unless the user has
             * specified otherwise, we have to assume that they're in
             * raw format */
            if (ret == 1) {
                if (loader && !loader->format) {
                    loader->format = VIR_STORAGE_FILE_RAW;
                }
            }
        } else {
            virReportError(VIR_ERR_OPERATION_FAILED,
                           _("Unable to find '%1$s' firmware that is compatible with the current configuration"),
                           virDomainOsDefFirmwareTypeToString(def->os.firmware));
            return -1;
        }
    }

    /* Always ensure that the NVRAM path is present, even if we
     * haven't found a match: the configuration might simply be
     * referring to a custom firmware build */
    qemuFirmwareEnsureNVRAM(def, driver, abiUpdate);

    return 0;
}


/**
 * qemuFirmwareGetSupported:
 * @machine: machine type
 * @arch: architecture
 * @privileged: whether running as privileged user
 * @supported: returned bitmap of supported interfaces
 * @secure: true if at least one secure boot enabled FW was found
 * @fws: (optional) list of found firmwares
 * @nfws: (optional) number of members in @fws
 *
 * Parse all FW descriptors (depending whether running as @privileged this may
 * or may not include user's $HOME) and for given combination of @machine and
 * @arch extract information to be later reported in domain capabilities.
 * The @supported contains a bitmap of found interfaces (and ORed values of 1
 * << VIR_DOMAIN_OS_DEF_FIRMWARE_*). Then, @supported is true if at least one
 * FW descriptor signalizes secure boot (although, this is checked against SMM
 * rather than SECURE_BOOT because reasons).
 *
 * If @fws and @nfws are not NULL, then @fws is allocated (must be freed by
 * caller when no longer needed) and contains list of firmwares found in form
 * of virFirmware. This can be useful if caller wants to know the paths to
 * firmware images (e.g. to present them in domain capabilities XML).
 * Moreover, to allow the caller distinguish between no FW descriptors found
 * and no matching FW descriptors found (nfws == 0 in both cases), the @fws is
 * going to be allocated in case of the latter anyway (with no real content
 * though).
 *
 * Returns: 0 on success,
 *         -1 otherwise.
 */
int
qemuFirmwareGetSupported(const char *machine,
                         virArch arch,
                         bool privileged,
                         uint64_t *supported,
                         bool *secure,
                         virFirmware ***fws,
                         size_t *nfws)
{
    qemuFirmware **firmwares = NULL;
    ssize_t nfirmwares = 0;
    size_t i;

    *supported = VIR_DOMAIN_OS_DEF_FIRMWARE_NONE;
    *secure = false;

    if (fws) {
        *fws = NULL;
        *nfws = 0;
    }

    if ((nfirmwares = qemuFirmwareFetchParsedConfigs(privileged,
                                                     &firmwares, NULL)) < 0)
        return -1;

    for (i = 0; i < nfirmwares; i++) {
        qemuFirmware *fw = firmwares[i];
        const qemuFirmwareMappingFlash *flash = &fw->mapping.data.flash;
        const qemuFirmwareMappingMemory *memory = &fw->mapping.data.memory;
        const char *fwpath = NULL;
        const char *nvrampath = NULL;
        size_t j;

        if (!qemuFirmwareMatchesMachineArch(fw, machine, arch))
            continue;

        for (j = 0; j < fw->ninterfaces; j++) {
            switch (fw->interfaces[j]) {
            case QEMU_FIRMWARE_OS_INTERFACE_UEFI:
                *supported |= 1ULL << VIR_DOMAIN_OS_DEF_FIRMWARE_EFI;
                break;
            case QEMU_FIRMWARE_OS_INTERFACE_BIOS:
                *supported |= 1ULL << VIR_DOMAIN_OS_DEF_FIRMWARE_BIOS;
                break;
            case QEMU_FIRMWARE_OS_INTERFACE_NONE:
            case QEMU_FIRMWARE_OS_INTERFACE_OPENFIRMWARE:
            case QEMU_FIRMWARE_OS_INTERFACE_UBOOT:
            case QEMU_FIRMWARE_OS_INTERFACE_LAST:
            default:
                break;
            }
        }

        for (j = 0; j < fw->nfeatures; j++) {
            switch (fw->features[j]) {
            case QEMU_FIRMWARE_FEATURE_REQUIRES_SMM:
                *secure = true;
                break;
            case QEMU_FIRMWARE_FEATURE_NONE:
            case QEMU_FIRMWARE_FEATURE_ACPI_S3:
            case QEMU_FIRMWARE_FEATURE_ACPI_S4:
            case QEMU_FIRMWARE_FEATURE_AMD_SEV:
            case QEMU_FIRMWARE_FEATURE_AMD_SEV_ES:
            case QEMU_FIRMWARE_FEATURE_AMD_SEV_SNP:
            case QEMU_FIRMWARE_FEATURE_INTEL_TDX:
            case QEMU_FIRMWARE_FEATURE_ENROLLED_KEYS:
            case QEMU_FIRMWARE_FEATURE_SECURE_BOOT:
            case QEMU_FIRMWARE_FEATURE_VERBOSE_DYNAMIC:
            case QEMU_FIRMWARE_FEATURE_VERBOSE_STATIC:
            case QEMU_FIRMWARE_FEATURE_LAST:
                break;
            }
        }

        switch (fw->mapping.device) {
        case QEMU_FIRMWARE_DEVICE_FLASH:
            fwpath = flash->executable.filename;
            nvrampath = flash->nvram_template.filename;
            break;

        case QEMU_FIRMWARE_DEVICE_MEMORY:
            fwpath = memory->filename;
            break;

        case QEMU_FIRMWARE_DEVICE_KERNEL:
        case QEMU_FIRMWARE_DEVICE_NONE:
        case QEMU_FIRMWARE_DEVICE_LAST:
            break;
        }

        if (fws && fwpath) {
            g_autoptr(virFirmware) tmp = NULL;

            /* Append only unique pairs. */
            for (j = 0; j < *nfws; j++) {
                if (STREQ((*fws)[j]->name, fwpath) &&
                    STREQ_NULLABLE((*fws)[j]->nvram, nvrampath))
                    break;
            }

            if (j == *nfws) {
                tmp = g_new0(virFirmware, 1);

                tmp->name = g_strdup(fwpath);
                tmp->nvram = g_strdup(nvrampath);
                VIR_APPEND_ELEMENT(*fws, *nfws, tmp);
            }
        }
    }

    if (fws && !*fws && nfirmwares)
        VIR_REALLOC_N(*fws, 0);

    for (i = 0; i < nfirmwares; i++)
        qemuFirmwareFree(firmwares[i]);
    VIR_FREE(firmwares);
    return 0;
}
