updater: add process-host-firmware subcommand

Add general support for subcommands.  Do this by bringing in the the
CLI11 library, and by skipping over dbus service initialization if a
subcommand is specified on the command line.

Implement the first subcommand: process-host-firmware.  If no subcommand
is specified on the command line, openpower-update-manager continues to
launch as a dbus service.

Introduce functions.cpp/functions.hpp.  functions.cpp is intended to be
a module in which process-hostfirmware and future openpower-item-updater
subcommands can be implemented.  functions.hpp should be used to export
declarations to unit tests or item_updater_main.cpp.

The IBM POWER host firmware runtime looks for data and/or additional
code while bootstraping in locations with well-known names.
Additionally, the host firmware runtime relies on an external party to
map platform specific data and/or code to the well known names.  The
process-host-firmware command maintains that mapping on behalf of the
host firmware and ensures the correct blob files exist in the well-known
locations prior to starting the host firmware runtime.

The process-host-firmware subcommand registers for callbacks from entity
manager when entity manager adds new DBus interfaces.  Interfaces other
than IBMCompatibleSystem are ignored.  Entity manager is started with
dbus activation if it is not already running.  If IBMCompatibleSystem is
found and a set of host firmware blob filename extensions are mapped,
they are used to write "well-known" names into the filesystem for use by
the host firmware runtime.

Once the well-known names are written to the filesystem the program will
exit.

If entity manager does not publish an IBMCompatibleSystem interface the
command will wait indefinitely for it to appear.  For this reason it is
not recommended to run the process-host-fimrware subcommand if
IBMCompatibleSystem is not implemented.

If IBMCompatibleSystem is implemented but no host firmware blob filename
extensions are mapped, the program will exit without doing anything.

Testcases are provided.

Change-Id: Icb066b0f3e164520cae312e3b03432a6ad67e0df
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/Makefile.am b/Makefile.am
index 37adaf1..a2f65be 100755
--- a/Makefile.am
+++ b/Makefile.am
@@ -10,6 +10,7 @@
 
 openpower_update_manager_SOURCES = \
 	activation.cpp \
+	functions.cpp \
 	version.cpp \
 	item_updater.cpp \
 	item_updater_main.cpp \
diff --git a/configure.ac b/configure.ac
index cd2a88f..3b6479c 100755
--- a/configure.ac
+++ b/configure.ac
@@ -18,6 +18,9 @@
 PKG_CHECK_MODULES([SDEVENTPLUS], [sdeventplus])
 PKG_CHECK_MODULES([PHOSPHOR_LOGGING], [phosphor-logging])
 
+# Check for headers
+AC_CHECK_HEADERS([CLI/CLI.hpp], [], [AC_MSG_ERROR([CLI11 is required and was not found])])
+
 # Checks for library functions
 LT_INIT # Required for systemd linking
 
diff --git a/functions.cpp b/functions.cpp
new file mode 100644
index 0000000..1780c87
--- /dev/null
+++ b/functions.cpp
@@ -0,0 +1,415 @@
+// SPDX-License-Identifier: Apache-2.0
+
+/**@file functions.cpp*/
+
+#include "functions.hpp"
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdeventplus/event.hpp>
+
+#include <filesystem>
+#include <functional>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace functions
+{
+namespace process_hostfirmware
+{
+
+/**
+ * @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;
+    }
+
+    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 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 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");
+    auto reply = bus.call(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;
+    reply.read(objects);
+
+    // 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;
+}
+} // namespace process_hostfirmware
+} // namespace functions
diff --git a/functions.hpp b/functions.hpp
new file mode 100644
index 0000000..f7511aa
--- /dev/null
+++ b/functions.hpp
@@ -0,0 +1,49 @@
+#include <filesystem>
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace sdbusplus
+{
+namespace bus
+{
+class bus;
+} // namespace bus
+} // namespace sdbusplus
+
+namespace sdeventplus
+{
+class Event;
+} // namespace sdeventplus
+
+namespace functions
+{
+namespace process_hostfirmware
+{
+using ErrorCallbackType =
+    std::function<void(const std::filesystem::path&, std::error_code&)>;
+using LinkCallbackType =
+    std::function<void(const std::filesystem::path&,
+                       const std::filesystem::path&, const ErrorCallbackType&)>;
+using MaybeCallCallbackType =
+    std::function<void(const std::vector<std::string>&)>;
+bool getExtensionsForIbmCompatibleSystem(
+    const std::map<std::string, std::vector<std::string>>&,
+    const std::vector<std::string>&, std::vector<std::string>&);
+void writeLink(const std::filesystem::path&, const std::filesystem::path&,
+               const ErrorCallbackType&);
+void findLinks(const std::filesystem::path&, const std::vector<std::string>&,
+               const ErrorCallbackType&, const LinkCallbackType&);
+bool maybeCall(
+    const std::map<
+        std::string,
+        std::map<std::string, std::variant<std::vector<std::string>>>>&,
+    const MaybeCallCallbackType&);
+std::shared_ptr<void> processHostFirmware(
+    sdbusplus::bus::bus&, std::map<std::string, std::vector<std::string>>,
+    std::filesystem::path, ErrorCallbackType, sdeventplus::Event&);
+} // namespace process_hostfirmware
+} // namespace functions
diff --git a/item_updater_main.cpp b/item_updater_main.cpp
index 85dc2b3..ea8c435 100644
--- a/item_updater_main.cpp
+++ b/item_updater_main.cpp
@@ -8,13 +8,19 @@
 #else
 #include "static/item_updater_static.hpp"
 #endif
+#include "functions.hpp"
 
+#include <CLI/CLI.hpp>
 #include <phosphor-logging/log.hpp>
 #include <sdbusplus/bus.hpp>
 #include <sdbusplus/server/manager.hpp>
 #include <sdeventplus/event.hpp>
 
+#include <map>
+#include <memory>
+#include <string>
 #include <system_error>
+#include <vector>
 
 namespace openpower
 {
@@ -42,7 +48,7 @@
 } // namespace software
 } // namespace openpower
 
-int main(int, char*[])
+int main(int argc, char* argv[])
 {
     using namespace openpower::software::updater;
     using namespace phosphor::logging;
@@ -51,7 +57,38 @@
 
     bus.attach_event(loop.get(), SD_EVENT_PRIORITY_NORMAL);
 
-    initializeService(bus);
+    CLI::App app{"OpenPOWER host firmware manager"};
+
+    // subcommandContext allows program subcommand callbacks to add loop event
+    // callbacks (e.g. reception of a dbus signal) and then return to main,
+    // without the loop event callback being destroyed until the loop event
+    // callback has a chance to run and instruct the loop to exit.
+    std::shared_ptr<void> subcommandContext;
+    static_cast<void>(
+        app.add_subcommand("process-host-firmware",
+                           "Point the host firmware at its data.")
+            ->callback([&bus, &loop, &subcommandContext]() {
+                using namespace std::string_literals;
+                std::map<std::string, std::vector<std::string>> extensionMap{{
+                    {"ibm,rainier-2u"s, {".RAINIER_2U_XML"s, ".P10"s}},
+                    {"ibm,rainier-4u"s, {".RAINIER_4U_XML"s, ".P10"s}},
+                }};
+                auto hostFirmwareDirectory = "/media/hostfw/running"s;
+                auto logCallback = [](const auto& path, auto& ec) {
+                    std::cerr << path << ": " << ec.message() << "\n";
+                };
+                subcommandContext =
+                    functions::process_hostfirmware::processHostFirmware(
+                        bus, std::move(extensionMap),
+                        std::move(hostFirmwareDirectory),
+                        std::move(logCallback), loop);
+            }));
+    CLI11_PARSE(app, argc, argv);
+
+    if (app.get_subcommands().size() == 0)
+    {
+        initializeService(bus);
+    }
 
     try
     {
diff --git a/meson.build b/meson.build
index 1fb5d35..12ff150 100644
--- a/meson.build
+++ b/meson.build
@@ -27,6 +27,8 @@
     message('Disabling lto because optimization is disabled')
 endif
 
+cxx = meson.get_compiler('cpp')
+
 extra_sources = []
 extra_unit_files = []
 extra_scripts = []
@@ -34,6 +36,10 @@
 build_vpnor = get_option('vpnor').enabled()
 build_verify_signature = get_option('verify-signature').enabled()
 
+if not cxx.has_header('CLI/CLI.hpp')
+      error('Could not find CLI.hpp')
+endif
+
 summary('building for device type', '@0@'.format(get_option('device-type')))
 summary('building vpnor', build_vpnor)
 summary('building signature verify', build_verify_signature)
@@ -155,6 +161,7 @@
     'openpower-update-manager',
     [
         'activation.cpp',
+        'functions.cpp',
         'version.cpp',
         'item_updater.cpp',
         'item_updater_main.cpp',
@@ -248,4 +255,17 @@
             include_directories: '.',
         )
     )
+    test(
+        'test_functions',
+        executable(
+            'test_functions',
+            'test/test_functions.cpp',
+            'functions.cpp',
+            dependencies: [
+                dependency('gtest', main: true),
+                dependency('sdbusplus'),
+                dependency('sdeventplus'),
+            ],
+        )
+    )
 endif
diff --git a/test/Makefile.am b/test/Makefile.am
index ed9fefe..7a711a6 100755
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -1,7 +1,7 @@
 AM_CPPFLAGS = -I$(top_srcdir)
 
 # gtest unit tests which run during a 'make check'
-check_PROGRAMS = utest
+check_PROGRAMS = utest test_functions
 
 # Run all 'check' test programs
 TESTS = $(check_PROGRAMS)
@@ -44,3 +44,23 @@
 	test_version.cpp \
 	test_item_updater_static.cpp \
 	msl_verify.cpp
+
+test_functions_CPPFLAGS = \
+	-Igtest \
+	$(GTEST_CPPFLAGS) \
+	$(AM_CPPFLAGS)
+
+test_functions_CXXFLAGS = \
+	$(PTHREAD_CFLAGS) \
+	$(SDBUSPLUS_CFLAGS) \
+	$(SDEVENTPLUS_CFLAGS)
+
+test_functions_LDFLAGS = \
+	-lgtest_main \
+	-lgtest \
+	$(SDBUSPLUS_LIBS) \
+	$(SDEVENTPLUS_LIBS)
+
+test_functions_SOURCES = \
+	test_functions.cpp \
+	../functions.cpp
diff --git a/test/test_functions.cpp b/test/test_functions.cpp
new file mode 100644
index 0000000..3b7d366
--- /dev/null
+++ b/test/test_functions.cpp
@@ -0,0 +1,466 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "../functions.hpp"
+
+#include <stdlib.h>
+
+#include <array>
+#include <cerrno>
+#include <filesystem>
+#include <fstream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using namespace std::string_literals;
+
+TEST(GetExtensionsForIbmCompatibleSystem, testSingleMatch)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-foo"s, {".EXT"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s}, extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".EXT"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testSingleNoMatchNoModify)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".EXT"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s},
+        extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_FALSE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{"foo"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMatchModify)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s},
+        extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testEmpty)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap;
+    std::vector<std::string> compatibleSystem{"system-foo"s},
+        extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_FALSE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{"foo"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testEmptyEmpty)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap;
+    std::vector<std::string> compatibleSystem, extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_FALSE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{"foo"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMatchMultiCompat)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s, "system"},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s, "system-bar"},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat2)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-foo"s, {".FOO"s}},
+        {"system-bar"s, {".BAR"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s, "system-bar"},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat3)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-bar", "system-foo"s},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".BAR"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat4)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-foo"s, {".FOO"s}},
+        {"system-bar"s, {".BAR"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-bar", "system-foo"s},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".BAR"s});
+}
+
+TEST(MaybeCall, noMatch)
+{
+    bool called = false;
+    auto callback = [&called](const auto&) { called = true; };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"foo"s, {{"bar"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_FALSE(found);
+    EXPECT_FALSE(called);
+}
+
+TEST(MaybeCall, match)
+{
+    bool called = false;
+    std::vector<std::string> sys;
+    auto callback = [&called, &sys](const auto& system) {
+        sys = system;
+        called = true;
+    };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"xyz.openbmc_project.Configuration.IBMCompatibleSystem"s,
+             {{"Names"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_TRUE(found);
+    EXPECT_TRUE(called);
+    EXPECT_EQ(sys, std::vector<std::string>{"foo"s});
+}
+
+TEST(MaybeCall, missingNames)
+{
+    bool called = false;
+    auto callback = [&called](const auto&) { called = true; };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"xyz.openbmc_project.Configuration.IBMCompatibleSystem"s, {}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_TRUE(found);
+    EXPECT_FALSE(called);
+}
+
+TEST(MaybeCall, emptyCallbackFound)
+{
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"xyz.openbmc_project.Configuration.IBMCompatibleSystem"s,
+             {{"Names"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::function<void(std::vector<std::string>)>());
+    EXPECT_TRUE(found);
+}
+
+TEST(MaybeCall, emptyCallbackNotFound)
+{
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"foo"s, {{"Names"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::function<void(std::vector<std::string>)>());
+    EXPECT_FALSE(found);
+}
+
+TEST(MaybeCall, emptyInterfaces)
+{
+    bool called = false;
+    auto callback = [&called](const auto&) { called = true; };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces;
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_FALSE(found);
+    EXPECT_FALSE(called);
+}
+
+TEST(MaybeCall, emptyInterfacesEmptyCallback)
+{
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces;
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::function<void(std::vector<std::string>)>());
+    EXPECT_FALSE(found);
+}
+
+TEST(WriteLink, testLinkNoDelete)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    bool called = false;
+    auto callback = [&called](const auto&, auto&) { called = true; };
+    std::filesystem::path linkPath = workdir / "link";
+    std::filesystem::path targetPath = workdir / "target";
+    std::ofstream link{linkPath};
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+}
+
+TEST(WriteLink, testLinkDelete)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    bool called = false;
+    auto callback = [&called](const auto&, auto&) { called = true; };
+    auto linkPath = workdir / "link";
+    auto targetPath = workdir / "target";
+    std::ofstream link{linkPath}, target{targetPath};
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+}
+
+TEST(WriteLink, testLinkFailDeleteDir)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::error_code ec;
+    std::filesystem::path callbackPath;
+    auto callback = [&ec, &callbackPath](const auto& p, auto& _ec) {
+        ec = _ec;
+        callbackPath = p;
+    };
+    auto targetPath = workdir / "target";
+    std::filesystem::create_directory(targetPath);
+    auto linkPath = workdir / "link";
+    auto filePath = targetPath / "file";
+    std::ofstream link{linkPath}, file{filePath};
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_EQ(ec.value(), ENOTEMPTY);
+    EXPECT_EQ(callbackPath, targetPath);
+}
+
+TEST(WriteLink, testLinkPathNotExist)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::error_code ec;
+    std::filesystem::path callbackPath;
+    auto callback = [&ec, &callbackPath](const auto& p, auto& _ec) {
+        ec = _ec;
+        callbackPath = p;
+    };
+    auto linkPath = workdir / "baz";
+    auto targetPath = workdir / "foo/bar/foo";
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_EQ(ec.value(), ENOENT);
+    EXPECT_EQ(callbackPath, targetPath);
+}
+
+TEST(FindLinks, testNoLinks)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    bool callbackCalled = false, errorCallbackCalled = false;
+    auto callback = [&callbackCalled](const auto&, const auto&, const auto&) {
+        callbackCalled = true;
+    };
+    auto errorCallback = [&errorCallbackCalled](const auto&, auto&) {
+        errorCallbackCalled = true;
+    };
+
+    std::vector<std::string> extensions;
+    functions::process_hostfirmware::findLinks(workdir, extensions,
+                                               errorCallback, callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(errorCallbackCalled);
+    EXPECT_FALSE(callbackCalled);
+}
+
+TEST(FindLinks, testOneFound)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::filesystem::path callbackPath, callbackLink;
+
+    bool errorCallbackCalled = false;
+    auto callback = [&callbackPath, &callbackLink](
+                        const auto& p1, const auto& p2, const auto&) {
+        callbackPath = p1;
+        callbackLink = p2;
+    };
+    auto errorCallback = [&errorCallbackCalled](const auto&, auto&) {
+        errorCallbackCalled = true;
+    };
+
+    auto filePath = workdir / "foo.foo";
+    std::ofstream file{filePath};
+    std::vector<std::string> extensions{".foo"s};
+    functions::process_hostfirmware::findLinks(workdir, extensions,
+                                               errorCallback, callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(errorCallbackCalled);
+    EXPECT_EQ(callbackLink, workdir / "foo");
+    EXPECT_EQ(callbackPath, filePath.filename());
+}
+
+TEST(FindLinks, testNoExtensions)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::filesystem::path callbackPath, callbackLink;
+
+    bool errorCallbackCalled = false, callbackCalled = false;
+    auto callback = [&callbackCalled](const auto&, const auto&, const auto&) {
+        callbackCalled = true;
+    };
+    auto errorCallback = [&errorCallbackCalled](const auto&, auto&) {
+        errorCallbackCalled = true;
+    };
+
+    auto filePath = workdir / "foo.foo";
+    std::ofstream file{filePath};
+    std::vector<std::string> extensions;
+    functions::process_hostfirmware::findLinks(workdir, extensions,
+                                               errorCallback, callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(errorCallbackCalled);
+    EXPECT_FALSE(callbackCalled);
+}
+
+TEST(FindLinks, testEnoent)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    std::error_code ec;
+    bool called = false;
+    std::filesystem::path callbackPath;
+    auto callback = [&called](const auto&, const auto&, const auto&) {
+        called = true;
+    };
+    auto errorCallback = [&ec, &callbackPath](const auto& p, auto& _ec) {
+        ec = _ec;
+        callbackPath = p;
+    };
+
+    std::vector<std::string> extensions;
+    auto dir = workdir / "baz";
+    functions::process_hostfirmware::findLinks(dir, extensions, errorCallback,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_EQ(ec.value(), ENOENT);
+    EXPECT_EQ(callbackPath, dir);
+    EXPECT_FALSE(called);
+}
+
+TEST(FindLinks, testEmptyCallback)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    bool called = false;
+    std::filesystem::path callbackPath;
+    auto errorCallback = [&called](const auto&, auto&) { called = true; };
+
+    auto filePath = workdir / "foo.foo";
+    std::ofstream file{filePath};
+
+    std::vector<std::string> extensions{".foo"s};
+    functions::process_hostfirmware::findLinks(
+        workdir, extensions, errorCallback,
+        functions::process_hostfirmware::LinkCallbackType());
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+    EXPECT_NO_THROW();
+}
+
+TEST(FindLinks, testEmptyErrorCallback)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    bool called = false;
+    auto callback = [&called](const auto&, const auto&, const auto&) {
+        called = true;
+    };
+
+    std::vector<std::string> extensions;
+    auto dir = workdir / "baz";
+    functions::process_hostfirmware::findLinks(
+        dir, extensions, functions::process_hostfirmware::ErrorCallbackType(),
+        callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+    EXPECT_NO_THROW();
+}