// SPDX-License-Identifier: Apache-2.0

/**@file functions.cpp*/

#include "config.h"

#include "functions.hpp"

#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
#include <sdbusplus/bus/match.hpp>
#include <sdbusplus/exception.hpp>
#include <sdbusplus/message.hpp>
#include <sdeventplus/event.hpp>

#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <variant>
#include <vector>

namespace functions
{
namespace process_hostfirmware
{

using namespace phosphor::logging;
using InterfacesPropertiesMap =
    std::map<std::string,
             std::map<std::string, std::variant<std::vector<std::string>>>>;
using ManagedObjectType =
    std::map<sdbusplus::message::object_path, InterfacesPropertiesMap>;

/**
 * @brief GetObject function to find the service given an object path.
 *        It is used to determine if a service is running, so there is no need
 *        to specify interfaces as a parameter to constrain the search.
 */
std::string getObject(sdbusplus::bus::bus& bus, const std::string& path)
{
    auto method = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
                                      MAPPER_BUSNAME, "GetObject");
    method.append(path);
    std::vector<std::string> interfaces;
    method.append(interfaces);

    std::vector<std::pair<std::string, std::vector<std::string>>> response;

    try
    {
        auto reply = bus.call(method);
        reply.read(response);
        if (response.empty())
        {
            return std::string{};
        }
    }
    catch (const sdbusplus::exception::exception& e)
    {
        return std::string{};
    }
    return response[0].first;
}

/**
 * @brief Returns the managed objects for a given service
 */
ManagedObjectType getManagedObjects(sdbusplus::bus::bus& bus,
                                    const std::string& service)
{
    auto method = bus.new_method_call(service.c_str(), "/",
                                      "org.freedesktop.DBus.ObjectManager",
                                      "GetManagedObjects");

    ManagedObjectType objects;

    try
    {
        auto reply = bus.call(method);
        reply.read(objects);
    }
    catch (const sdbusplus::exception::exception& e)
    {
        return ManagedObjectType{};
    }
    return objects;
}

/**
 * @brief Issue callbacks safely
 *
 * std::function can be empty, so this wrapper method checks for that prior to
 * calling it to avoid std::bad_function_call
 *
 * @tparam Sig the types of the std::function arguments
 * @tparam Args the deduced argument types
 * @param[in] callback the callback being wrapped
 * @param[in] args the callback arguments
 */
template <typename... Sig, typename... Args>
void makeCallback(const std::function<void(Sig...)>& callback, Args&&... args)
{
    if (callback)
    {
        callback(std::forward<Args>(args)...);
    }
}

/**
 * @brief Get file extensions for IBMCompatibleSystem
 *
 * IBM host firmware can be deployed as blobs (files) in a filesystem.  Host
 * firmware blobs for different values of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem are packaged with
 * different filename extensions.  getExtensionsForIbmCompatibleSystem
 * maintains the mapping from a given value of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem to an array of
 * filename extensions.
 *
 * If a mapping is found getExtensionsForIbmCompatibleSystem returns true and
 * the extensions parameter is reset with the map entry.  If no mapping is
 * found getExtensionsForIbmCompatibleSystem returns false and extensions is
 * unmodified.
 *
 * @param[in] extensionMap a map of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem to host firmware blob
 * file extensions.
 * @param[in] ibmCompatibleSystem The names property of an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem
 * @param[out] extentions the host firmware blob file extensions
 * @return true if an entry was found, otherwise false
 */
bool getExtensionsForIbmCompatibleSystem(
    const std::map<std::string, std::vector<std::string>>& extensionMap,
    const std::vector<std::string>& ibmCompatibleSystem,
    std::vector<std::string>& extensions)
{
    for (const auto& system : ibmCompatibleSystem)
    {
        auto extensionMapIterator = extensionMap.find(system);
        if (extensionMapIterator != extensionMap.end())
        {
            extensions = extensionMapIterator->second;
            return true;
        }
    }

    return false;
}

/**
 * @brief Write host firmware well-known name
 *
 * A wrapper around std::filesystem::create_symlink that avoids EEXIST by
 * deleting any pre-existing file.
 *
 * @param[in] linkTarget The link target argument to
 * std::filesystem::create_symlink
 * @param[in] linkPath The link path argument to std::filesystem::create_symlink
 * @param[in] errorCallback A callback made in the event of filesystem errors.
 */
void writeLink(const std::filesystem::path& linkTarget,
               const std::filesystem::path& linkPath,
               const ErrorCallbackType& errorCallback)
{
    std::error_code ec;

    // remove files with the same name as the symlink to be created,
    // otherwise symlink will fail with EEXIST.
    if (!std::filesystem::remove(linkPath, ec))
    {
        if (ec)
        {
            makeCallback(errorCallback, linkPath, ec);
            return;
        }
    }

    std::filesystem::create_symlink(linkTarget, linkPath, ec);
    if (ec)
    {
        makeCallback(errorCallback, linkPath, ec);
        return;
    }
}

/**
 * @brief Find host firmware blob files that need well-known names
 *
 * The IBM host firmware runtime looks for data and/or additional code while
 * bootstraping in files with well-known names.  findLinks uses the provided
 * extensions argument to find host firmware blob files that require a
 * well-known name.  When a blob is found, issue the provided callback
 * (typically a function that will write a symlink).
 *
 * @param[in] hostFirmwareDirectory The directory in which findLinks should
 * look for host firmware blob files that need well-known names.
 * @param[in] extentions The extensions of the firmware blob files denote a
 * host firmware blob file requires a well-known name.
 * @param[in] errorCallback A callback made in the event of filesystem errors.
 * @param[in] linkCallback A callback made when host firmware blob files
 * needing a well known name are found.
 */
void findLinks(const std::filesystem::path& hostFirmwareDirectory,
               const std::vector<std::string>& extensions,
               const ErrorCallbackType& errorCallback,
               const LinkCallbackType& linkCallback)
{
    std::error_code ec;
    std::filesystem::directory_iterator directoryIterator(hostFirmwareDirectory,
                                                          ec);
    if (ec)
    {
        makeCallback(errorCallback, hostFirmwareDirectory, ec);
        return;
    }

    // Create a symlink for pnor.toc
    static const auto tocLid = "81e00994.lid";
    auto tocLidPath = hostFirmwareDirectory / tocLid;
    if (std::filesystem::exists(tocLidPath))
    {
        static const auto tocName = "pnor.toc";
        auto tocLinkPath = hostFirmwareDirectory / tocName;
        makeCallback(linkCallback, tocLid, tocLinkPath, errorCallback);
    }

    for (; directoryIterator != std::filesystem::end(directoryIterator);
         directoryIterator.increment(ec))
    {
        const auto& file = directoryIterator->path();
        if (ec)
        {
            makeCallback(errorCallback, file, ec);
            // quit here if the increment call failed otherwise the loop may
            // never finish
            break;
        }

        if (std::find(extensions.begin(), extensions.end(), file.extension()) ==
            extensions.end())
        {
            // this file doesn't have an extension or doesn't match any of the
            // provided extensions.
            continue;
        }

        auto linkPath(file.parent_path().append(
            static_cast<const std::string&>(file.stem())));

        makeCallback(linkCallback, file.filename(), linkPath, errorCallback);
    }
}

/**
 * @brief Parse the elements json file and construct a string with the data to
 *        be used to update the bios attribute table.
 *
 * @param[in] elementsJsonFilePath - The path to the host firmware json file.
 * @param[in] extensions - The extensions of the firmware blob files.
 */
std::string getBiosAttrStr(const std::filesystem::path& elementsJsonFilePath,
                           const std::vector<std::string>& extensions)
{
    std::string biosAttrStr{};

    std::ifstream jsonFile(elementsJsonFilePath.c_str());
    if (!jsonFile)
    {
        return {};
    }

    std::map<std::string, std::string> attr;
    auto data = nlohmann::json::parse(jsonFile, nullptr, false);
    if (data.is_discarded())
    {
        log<level::ERR>("Error parsing JSON file",
                        entry("FILE=%s", elementsJsonFilePath.c_str()));
        return {};
    }

    // .get requires a non-const iterator
    for (auto& iter : data["lids"])
    {
        std::string name{};
        std::string lid{};

        try
        {
            name = iter["element_name"].get<std::string>();
            lid = iter["short_lid_name"].get<std::string>();
        }
        catch (const std::exception& e)
        {
            // Possibly the element or lid name field was not found
            log<level::ERR>("Error reading JSON field",
                            entry("FILE=%s", elementsJsonFilePath.c_str()),
                            entry("ERROR=%s", e.what()));
            continue;
        }

        // The elements with the ipl extension have higher priority. Therefore
        // Use operator[] to overwrite value if an entry for it already exists.
        // Ex: if the JSON contains an entry A.P10 followed by A.P10.iplTime,
        // the lid value for the latter one will be overwrite the value of the
        // first one.
        constexpr auto iplExtension = ".iplTime";
        std::filesystem::path path(name);
        if (path.extension() == iplExtension)
        {
            // Some elements have an additional extension, ex: .P10.iplTime
            // Strip off the ipl extension with stem(), then check if there is
            // an additional extension with extension().
            if (!path.stem().extension().empty())
            {
                // Check if the extension matches the extensions for this system
                if (std::find(extensions.begin(), extensions.end(),
                              path.stem().extension()) == extensions.end())
                {
                    continue;
                }
            }
            // Get the element name without extensions by calling stem() twice
            // since stem() returns the base name if no periods are found.
            // Therefore both "element.P10" and "element.P10.iplTime" would
            // become "element".
            attr[path.stem().stem()] = lid;
            continue;
        }

        // Process all other extensions. The extension should match the list of
        // supported extensions for this system. Use .insert() to only add
        // entries that do not exist, so to not overwrite the values that may
        // had been added that had the ipl extension.
        if (std::find(extensions.begin(), extensions.end(), path.extension()) !=
            extensions.end())
        {
            attr.insert({path.stem(), lid});
        }
    }
    for (const auto& a : attr)
    {
        // Build the bios attribute string with format:
        // "element1=lid1,element2=lid2,elementN=lidN,"
        biosAttrStr += a.first + "=" + a.second + ",";

        // Create symlinks from the hostfw elements to their corresponding
        // lid files if they don't exist
        auto elementFilePath =
            std::filesystem::path("/media/hostfw/running") / a.first;
        if (!std::filesystem::exists(elementFilePath))
        {
            std::error_code ec;
            auto lidName = a.second + ".lid";
            std::filesystem::create_symlink(lidName, elementFilePath, ec);
            if (ec)
            {
                log<level::ERR>("Error creating symlink",
                                entry("TARGET=%s", lidName.c_str()),
                                entry("LINK=%s", elementFilePath.c_str()));
            }
        }
    }

    // Delete the last comma of the bios attribute string
    if (biosAttrStr.back() == ',')
    {
        return biosAttrStr.substr(0, biosAttrStr.length() - 1);
    }

    return biosAttrStr;
}

/**
 * @brief Set the bios attribute table with details of the host firmware data
 * for this system.
 *
 * @param[in] elementsJsonFilePath - The path to the host firmware json file.
 * @param[in] extentions - The extensions of the firmware blob files.
 */
void setBiosAttr(const std::filesystem::path& elementsJsonFilePath,
                 const std::vector<std::string>& extensions)
{
    auto biosAttrStr = getBiosAttrStr(elementsJsonFilePath, extensions);

    constexpr auto biosConfigPath = "/xyz/openbmc_project/bios_config/manager";
    constexpr auto biosConfigIntf = "xyz.openbmc_project.BIOSConfig.Manager";
    constexpr auto dbusAttrName = "hb_lid_ids";
    constexpr auto dbusAttrType =
        "xyz.openbmc_project.BIOSConfig.Manager.AttributeType.String";

    using PendingAttributesType = std::vector<std::pair<
        std::string, std::tuple<std::string, std::variant<std::string>>>>;
    PendingAttributesType pendingAttributes;
    pendingAttributes.emplace_back(std::make_pair(
        dbusAttrName, std::make_tuple(dbusAttrType, biosAttrStr)));

    auto bus = sdbusplus::bus::new_default();
    auto method = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
                                      MAPPER_INTERFACE, "GetObject");
    method.append(biosConfigPath, std::vector<std::string>({biosConfigIntf}));
    std::vector<std::pair<std::string, std::vector<std::string>>> response;
    try
    {
        auto reply = bus.call(method);
        reply.read(response);
        if (response.empty())
        {
            log<level::ERR>("Error reading mapper response",
                            entry("PATH=%s", biosConfigPath),
                            entry("INTERFACE=%s", biosConfigIntf));
            return;
        }
        auto method = bus.new_method_call((response.begin()->first).c_str(),
                                          biosConfigPath,
                                          SYSTEMD_PROPERTY_INTERFACE, "Set");
        method.append(biosConfigIntf, "PendingAttributes",
                      std::variant<PendingAttributesType>(pendingAttributes));
        bus.call(method);
    }
    catch (const sdbusplus::exception::exception& e)
    {
        log<level::ERR>("Error setting the bios attribute",
                        entry("ERROR=%s", e.what()),
                        entry("ATTRIBUTE=%s", dbusAttrName));
        return;
    }
}

/**
 * @brief Make callbacks on
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem instances.
 *
 * Look for an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem in the provided
 * argument and if found, issue the provided callback.
 *
 * @param[in] interfacesAndProperties the interfaces in which to look for an
 * instance of xyz.openbmc_project.Configuration.IBMCompatibleSystem
 * @param[in] callback the user callback to make if
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem is found in
 * interfacesAndProperties
 * @return true if interfacesAndProperties contained an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem, false otherwise
 */
bool maybeCall(const std::map<std::string,
                              std::map<std::string,
                                       std::variant<std::vector<std::string>>>>&
                   interfacesAndProperties,
               const MaybeCallCallbackType& callback)
{
    using namespace std::string_literals;

    static const auto interfaceName =
        "xyz.openbmc_project.Configuration.IBMCompatibleSystem"s;
    auto interfaceIterator = interfacesAndProperties.find(interfaceName);
    if (interfaceIterator == interfacesAndProperties.cend())
    {
        // IBMCompatibleSystem interface not found, so instruct the caller to
        // keep waiting or try again later.
        return false;
    }
    auto propertyIterator = interfaceIterator->second.find("Names"s);
    if (propertyIterator == interfaceIterator->second.cend())
    {
        // The interface exists but the property doesn't.  This is a bug in the
        // IBMCompatibleSystem implementation.  The caller should not try
        // again.
        std::cerr << "Names property not implemented on " << interfaceName
                  << "\n";
        return true;
    }

    const auto& ibmCompatibleSystem =
        std::get<std::vector<std::string>>(propertyIterator->second);
    if (callback)
    {
        callback(ibmCompatibleSystem);
    }

    // IBMCompatibleSystem found and callback issued.
    return true;
}

/**
 * @brief Make callbacks on
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem instances.
 *
 * Look for an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem in the provided
 * argument and if found, issue the provided callback.
 *
 * @param[in] message the DBus message in which to look for an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem
 * @param[in] callback the user callback to make if
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem is found in
 * message
 * @return true if message contained an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem, false otherwise
 */
bool maybeCallMessage(sdbusplus::message::message& message,
                      const MaybeCallCallbackType& callback)
{
    std::map<std::string,
             std::map<std::string, std::variant<std::vector<std::string>>>>
        interfacesAndProperties;
    sdbusplus::message::object_path _;
    message.read(_, interfacesAndProperties);
    return maybeCall(interfacesAndProperties, callback);
}

/**
 * @brief Determine system support for host firmware well-known names.
 *
 * Using the provided extensionMap and
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem, determine if
 * well-known names for host firmare blob files are necessary and if so, create
 * them.
 *
 * @param[in] extensionMap a map of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem to host firmware blob
 * file extensions.
 * @param[in] hostFirmwareDirectory The directory in which findLinks should
 * look for host firmware blob files that need well-known names.
 * @param[in] ibmCompatibleSystem The names property of an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem
 * @param[in] errorCallback A callback made in the event of filesystem errors.
 */
void maybeMakeLinks(
    const std::map<std::string, std::vector<std::string>>& extensionMap,
    const std::filesystem::path& hostFirmwareDirectory,
    const std::vector<std::string>& ibmCompatibleSystem,
    const ErrorCallbackType& errorCallback)
{
    std::vector<std::string> extensions;
    if (getExtensionsForIbmCompatibleSystem(extensionMap, ibmCompatibleSystem,
                                            extensions))
    {
        findLinks(hostFirmwareDirectory, extensions, errorCallback, writeLink);
    }
}

/**
 * @brief Determine system support for updating the bios attribute table.
 *
 * Using the provided extensionMap and
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem, determine if the bios
 * attribute table needs to be updated.
 *
 * @param[in] extensionMap a map of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem to host firmware blob
 * file extensions.
 * @param[in] elementsJsonFilePath The file path to the json file
 * @param[in] ibmCompatibleSystem The names property of an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem
 */
void maybeSetBiosAttr(
    const std::map<std::string, std::vector<std::string>>& extensionMap,
    const std::filesystem::path& elementsJsonFilePath,
    const std::vector<std::string>& ibmCompatibleSystem)
{
    std::vector<std::string> extensions;
    if (getExtensionsForIbmCompatibleSystem(extensionMap, ibmCompatibleSystem,
                                            extensions))
    {
        setBiosAttr(elementsJsonFilePath, extensions);
    }
}

/**
 * @brief process host firmware
 *
 * Allocate a callback context and register for DBus.ObjectManager Interfaces
 * added signals from entity manager.
 *
 * Check the current entity manager object tree for a
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem instance (entity
 * manager will be dbus activated if it is not running).  If one is found,
 * determine if symlinks need to be created and create them.  Instruct the
 * program event loop to exit.
 *
 * If no instance of xyz.openbmc_project.Configuration.IBMCompatibleSystem is
 * found return the callback context to main, where the program will sleep
 * until the callback is invoked one or more times and instructs the program
 * event loop to exit when
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem is added.
 *
 * @param[in] bus a DBus client connection
 * @param[in] extensionMap a map of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem to host firmware blob
 * file extensions.
 * @param[in] hostFirmwareDirectory The directory in which processHostFirmware
 * should look for blob files.
 * @param[in] errorCallback A callback made in the event of filesystem errors.
 * @param[in] loop a program event loop
 * @return nullptr if an instance of
 * xyz.openbmc_project.Configuration.IBMCompatibleSystem is found, otherwise a
 * pointer to an sdbusplus match object.
 */
std::shared_ptr<void> processHostFirmware(
    sdbusplus::bus::bus& bus,
    std::map<std::string, std::vector<std::string>> extensionMap,
    std::filesystem::path hostFirmwareDirectory,
    ErrorCallbackType errorCallback, sdeventplus::Event& loop)
{
    // ownership of extensionMap, hostFirmwareDirectory and errorCallback can't
    // be transfered to the match callback because they are needed in the non
    // async part of this function below, so they need to be moved to the heap.
    auto pExtensionMap =
        std::make_shared<decltype(extensionMap)>(std::move(extensionMap));
    auto pHostFirmwareDirectory =
        std::make_shared<decltype(hostFirmwareDirectory)>(
            std::move(hostFirmwareDirectory));
    auto pErrorCallback =
        std::make_shared<decltype(errorCallback)>(std::move(errorCallback));

    // register for a callback in case the IBMCompatibleSystem interface has
    // not yet been published by entity manager.
    auto interfacesAddedMatch = std::make_shared<sdbusplus::bus::match::match>(
        bus,
        sdbusplus::bus::match::rules::interfacesAdded() +
            sdbusplus::bus::match::rules::sender(
                "xyz.openbmc_project.EntityManager"),
        [pExtensionMap, pHostFirmwareDirectory, pErrorCallback,
         &loop](auto& message) {
            // bind the extension map, host firmware directory, and error
            // callback to the maybeMakeLinks function.
            auto maybeMakeLinksWithArgsBound =
                std::bind(maybeMakeLinks, std::cref(*pExtensionMap),
                          std::cref(*pHostFirmwareDirectory),
                          std::placeholders::_1, std::cref(*pErrorCallback));

            // if the InterfacesAdded message contains an an instance of
            // xyz.openbmc_project.Configuration.IBMCompatibleSystem, check to
            // see if links are necessary on this system and if so, create
            // them.
            if (maybeCallMessage(message, maybeMakeLinksWithArgsBound))
            {
                // The IBMCompatibleSystem interface was found and the links
                // were created if applicable.  Instruct the event loop /
                // subcommand to exit.
                loop.exit(0);
            }
        });

    // now that we'll get a callback in the event of an InterfacesAdded signal
    // (potentially containing
    // xyz.openbmc_project.Configuration.IBMCompatibleSystem), activate entity
    // manager if it isn't running and enumerate its objects
    auto getManagedObjects = bus.new_method_call(
        "xyz.openbmc_project.EntityManager", "/",
        "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
    std::map<std::string,
             std::map<std::string, std::variant<std::vector<std::string>>>>
        interfacesAndProperties;
    std::map<sdbusplus::message::object_path, decltype(interfacesAndProperties)>
        objects;
    try
    {
        auto reply = bus.call(getManagedObjects);
        reply.read(objects);
    }
    catch (const sdbusplus::exception::exception& e)
    {
        // Error querying the EntityManager interface. Return the match to have
        // the callback run if/when the interface appears in D-Bus.
        return interfacesAddedMatch;
    }

    // bind the extension map, host firmware directory, and error callback to
    // the maybeMakeLinks function.
    auto maybeMakeLinksWithArgsBound =
        std::bind(maybeMakeLinks, std::cref(*pExtensionMap),
                  std::cref(*pHostFirmwareDirectory), std::placeholders::_1,
                  std::cref(*pErrorCallback));

    for (const auto& pair : objects)
    {
        std::tie(std::ignore, interfacesAndProperties) = pair;
        // if interfacesAndProperties contains an an instance of
        // xyz.openbmc_project.Configuration.IBMCompatibleSystem, check to see
        // if links are necessary on this system and if so, create them
        if (maybeCall(interfacesAndProperties, maybeMakeLinksWithArgsBound))
        {
            // The IBMCompatibleSystem interface is already on the bus and the
            // links were created if applicable.  Instruct the event loop to
            // exit.
            loop.exit(0);
            // The match object isn't needed anymore, so destroy it on return.
            return nullptr;
        }
    }

    // The IBMCompatibleSystem interface has not yet been published.  Move
    // ownership of the match callback to the caller.
    return interfacesAddedMatch;
}

/**
 * @brief Update the Bios Attribute Table
 *
 * If an instance of xyz.openbmc_project.Configuration.IBMCompatibleSystem is
 * found, update the Bios Attribute Table with the appropriate host firmware
 * data.
 *
 * @param[in] bus - D-Bus client connection.
 * @param[in] extensionMap - Map of IBMCompatibleSystem names and host firmware
 *                           file extensions.
 * @param[in] elementsJsonFilePath - The Path to the json file
 * @param[in] loop - Program event loop.
 * @return nullptr
 */
std::vector<std::shared_ptr<void>> updateBiosAttrTable(
    sdbusplus::bus::bus& bus,
    std::map<std::string, std::vector<std::string>> extensionMap,
    std::filesystem::path elementsJsonFilePath, sdeventplus::Event& loop)
{
    constexpr auto pldmPath = "/xyz/openbmc_project/pldm";
    constexpr auto entityManagerServiceName =
        "xyz.openbmc_project.EntityManager";

    auto pExtensionMap =
        std::make_shared<decltype(extensionMap)>(std::move(extensionMap));
    auto pElementsJsonFilePath =
        std::make_shared<decltype(elementsJsonFilePath)>(
            std::move(elementsJsonFilePath));

    // Entity Manager is needed to get the list of supported extensions. Add a
    // match to monitor interfaces added in case it's not running yet.
    auto maybeSetAttrWithArgsBound =
        std::bind(maybeSetBiosAttr, std::cref(*pExtensionMap),
                  std::cref(*pElementsJsonFilePath), std::placeholders::_1);

    std::vector<std::shared_ptr<void>> matches;
    matches.emplace_back(std::make_shared<sdbusplus::bus::match::match>(
        bus,
        sdbusplus::bus::match::rules::interfacesAdded() +
            sdbusplus::bus::match::rules::sender(
                "xyz.openbmc_project.EntityManager"),
        [pldmPath, pExtensionMap, pElementsJsonFilePath,
         maybeSetAttrWithArgsBound, &loop](auto& message) {
            auto bus = sdbusplus::bus::new_default();
            auto pldmObject = getObject(bus, pldmPath);
            if (pldmObject.empty())
            {
                return;
            }
            if (maybeCallMessage(message, maybeSetAttrWithArgsBound))
            {
                loop.exit(0);
            }
        }));
    matches.emplace_back(std::make_shared<sdbusplus::bus::match::match>(
        bus,
        sdbusplus::bus::match::rules::nameOwnerChanged() +
            sdbusplus::bus::match::rules::arg0namespace(
                "xyz.openbmc_project.PLDM"),
        [pExtensionMap, pElementsJsonFilePath, maybeSetAttrWithArgsBound,
         &loop](auto& message) {
            std::string name;
            std::string oldOwner;
            std::string newOwner;
            message.read(name, oldOwner, newOwner);

            if (newOwner.empty())
            {
                return;
            }

            auto bus = sdbusplus::bus::new_default();
            InterfacesPropertiesMap interfacesAndProperties;
            auto objects = getManagedObjects(bus, entityManagerServiceName);
            for (const auto& pair : objects)
            {
                std::tie(std::ignore, interfacesAndProperties) = pair;
                if (maybeCall(interfacesAndProperties,
                              maybeSetAttrWithArgsBound))
                {
                    loop.exit(0);
                }
            }
        }));

    // The BIOS attribute table can only be updated if PLDM is running because
    // PLDM is the one that exposes this property. Return if it's not running.
    auto pldmObject = getObject(bus, pldmPath);
    if (pldmObject.empty())
    {
        return matches;
    }

    InterfacesPropertiesMap interfacesAndProperties;
    auto objects = getManagedObjects(bus, entityManagerServiceName);
    for (const auto& pair : objects)
    {
        std::tie(std::ignore, interfacesAndProperties) = pair;
        if (maybeCall(interfacesAndProperties, maybeSetAttrWithArgsBound))
        {
            loop.exit(0);
            return {};
        }
    }

    return matches;
}

} // namespace process_hostfirmware
} // namespace functions