Utility functions for download firmware

Added set of utility functions to be used within the classes for
managing the PSU firmware updates. Here is a breakdown of the key
functions:

- getDevicePath():
  Construct the PSU device path using I2C bus and address.

- getClassInstance():
  Determines the appropriate updater class to use based on PSU model
  number.

- getFWFilenamePath():
  Searches a directory for a firmware file matching a specified prefix
  and file extension (.bin or .hex).

- calculateCRC8():
  Computes the CRC-8 checksum for transferred data.

- delay():
  Pauses execution for a specified number of milliseconds.

- bigEndianToLittleEndian():
  Converts a 32-bit value from big-endian to little-endian.

- validateFWFile():
  Checks if a firmware file exists and is non-empty.

- openFirmwareFile():
  Opens a firmware file in binary mode, returning a file stream if
  successful.

- readFirmwareBytes():
  Reads specified number of data bytes from a firmware file into a
  buffer. Return data read or null to the caller.

- usePsuJsonFile():
  Wrapper to check the existence of the PSU JSON file.

- Class accessors to private data:
  getPsuInventoryPath(): Accessor for PSU inventory path.
  getDevPath(): Accessor for device path.
  getDevName(): Accessor for device name.
  getImageDir(): Accessor for image directory.
  getI2C(): I2C interface accessor.

Tested every function manually:

- getDevicePath() (using busctl):
  - Validate I2C bus and address values through psuInventoryPath
    validate expected result
  - Modified psuInventoryPath to invalid path
    validate returned invalid path.

- getClassInstance():
  - Validate with matching model number the function instantiate
    appropriate class.
  - Validate the default class instantiated.

- getFWFilenamePath():
  - Validate return of the correct file name in the PSU FW directory
  - Validate null returns when FW files don't exist

- calculaterCRC8():
  - Validate single byte 0x0 result 0x0, single byte 0x01 result 0x07

- delay():
  - Verified the task suspend execution.

- bigEndianToLittleEndian():
  - Verified input 0x12345678 resulted in 0x78563412

- validateFWFile():
  - Validate the existence of the file otherwise an error is logged
  - Validate the file size is greater than 0 otherwise an error is
    logged.

- openFirmwareFile():
  - Validate ifstream object is returned and was able to read from.
  - Validate error logged if the file name is null
  - validate error logged when unable to open the file

- readFirmwareBytes():
  - Validate data read from FW file
  - Validate number of bytes read.

- usePsuJsonFile():
  - Added JSON file to simulator and validated true return.

Change-Id: I0b8b24ae7d37724dab608d2c4977c1b42d4e1632
Signed-off-by: Faisal Awada <faisal@us.ibm.com>
diff --git a/tools/power-utils/main.cpp b/tools/power-utils/main.cpp
index eb81db4..f240ece 100644
--- a/tools/power-utils/main.cpp
+++ b/tools/power-utils/main.cpp
@@ -71,7 +71,7 @@
     if (!updateArguments.empty())
     {
         assert(updateArguments.size() == 2);
-        if (updater::update(updateArguments[0], updateArguments[1]))
+        if (updater::update(bus, updateArguments[0], updateArguments[1]))
         {
             ret = "Update successful";
         }
diff --git a/tools/power-utils/test/meson.build b/tools/power-utils/test/meson.build
index 40b2ca6..e48f799 100644
--- a/tools/power-utils/test/meson.build
+++ b/tools/power-utils/test/meson.build
@@ -26,6 +26,7 @@
         'test_updater',
         'test_updater.cpp',
         '../updater.cpp',
+        '../version.cpp',
         dependencies: [
             gtest,
             gmock,
diff --git a/tools/power-utils/test/test_updater.cpp b/tools/power-utils/test/test_updater.cpp
index 6ad3f73..9c34926 100644
--- a/tools/power-utils/test/test_updater.cpp
+++ b/tools/power-utils/test/test_updater.cpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 #include "../updater.hpp"
+#include "../version.hpp"
 #include "mocked_i2c_interface.hpp"
 
 #include <filesystem>
diff --git a/tools/power-utils/updater.cpp b/tools/power-utils/updater.cpp
index 5c4f7f5..951b78c 100644
--- a/tools/power-utils/updater.cpp
+++ b/tools/power-utils/updater.cpp
@@ -21,11 +21,14 @@
 #include "types.hpp"
 #include "utility.hpp"
 
+#include <sys/stat.h>
+
 #include <phosphor-logging/log.hpp>
 
 #include <chrono>
 #include <fstream>
 #include <thread>
+#include <vector>
 
 using namespace phosphor::logging;
 namespace util = phosphor::power::util;
@@ -36,6 +39,10 @@
 namespace internal
 {
 
+// Define the CRC-8 polynomial (CRC-8-CCITT)
+constexpr uint8_t CRC8_POLYNOMIAL = 0x07;
+constexpr uint8_t CRC8_INITIAL = 0x00;
+
 /* Get the device name from the device path */
 std::string getDeviceName(std::string devPath)
 {
@@ -46,27 +53,59 @@
     return fs::path(devPath).stem().string();
 }
 
-std::string getDevicePath(const std::string& psuInventoryPath)
+// Construct the device path using the I2C bus and address or read inventory
+// path
+std::string getDevicePath(sdbusplus::bus_t& bus,
+                          const std::string& psuInventoryPath)
 {
-    auto data = util::loadJSONFromFile(PSU_JSON_PATH);
-
-    if (data == nullptr)
+    try
     {
+        if (internal::usePsuJsonFile())
+        {
+            auto data = util::loadJSONFromFile(PSU_JSON_PATH);
+            if (data == nullptr)
+            {
+                return {};
+            }
+            auto devicePath = data["psuDevices"][psuInventoryPath];
+            if (devicePath.empty())
+            {
+                log<level::WARNING>("Unable to find psu devices or path");
+            }
+            return devicePath;
+        }
+        else
+        {
+            using namespace version;
+            const auto& [i2cbus, i2caddr] =
+                version::utils::getPsuI2c(bus, psuInventoryPath);
+            const auto DevicePath = "/sys/bus/i2c/devices/";
+            std::ostringstream ss;
+            ss << std::hex << std::setw(4) << std::setfill('0') << i2caddr;
+            std::string addrStr = ss.str();
+            std::string busStr = std::to_string(i2cbus);
+            std::string devPath = DevicePath + busStr + "-" + addrStr;
+            return devPath;
+        }
+    }
+    catch (const std::exception& e)
+    {
+        log<level::ERR>(
+            std::format("Error in getDevicePath: {}", e.what()).c_str());
         return {};
     }
-
-    auto devicePath = data["psuDevices"][psuInventoryPath];
-    if (devicePath.empty())
+    catch (...)
     {
-        log<level::WARNING>("Unable to find psu devices or path");
+        log<level::ERR>("Unknown error occurred in getDevicePath");
+        return {};
     }
-    return devicePath;
 }
 
+// Parse the device name to get I2C bus and address
 std::pair<uint8_t, uint8_t> parseDeviceName(const std::string& devName)
 {
-    // Get I2C bus id and device address, e.g. 3-0068
-    // is parsed to bus id 3, device address 0x68
+    // Get I2C bus and device address, e.g. 3-0068
+    // is parsed to bus 3, device address 0x68
     auto pos = devName.find('-');
     assert(pos != std::string::npos);
     uint8_t busId = std::stoi(devName.substr(0, pos));
@@ -74,28 +113,176 @@
     return {busId, devAddr};
 }
 
+// Get the appropriate Updater class instance based PSU model number
+std::unique_ptr<updater::Updater> getClassInstance(
+    const std::string& model, const std::string& psuInventoryPath,
+    const std::string& devPath, const std::string& imageDir)
+{
+    if (model == "XXXX")
+    {
+        // XXXX updater class
+    }
+    return std::make_unique<Updater>(psuInventoryPath, devPath, imageDir);
+}
+
+// Function to locate FW file with model and extension bin or hex
+const std::string getFWFilenamePath(const std::string& directory)
+{
+    namespace fs = std::filesystem;
+    // Get the last part of the directory name (model number)
+    std::string model = fs::path(directory).filename().string();
+    for (const auto& entry : fs::directory_iterator(directory))
+    {
+        if (entry.is_regular_file())
+        {
+            std::string filename = entry.path().filename().string();
+
+            if ((filename.rfind(model, 0) == 0) &&
+                (filename.ends_with(".bin") || filename.ends_with(".hex")))
+            {
+                return directory + "/" + filename;
+            }
+        }
+    }
+    return "";
+}
+
+// Compute CRC-8 checksum for a vector of bytes
+uint8_t calculateCRC8(const std::vector<uint8_t>& data)
+{
+    uint8_t crc = CRC8_INITIAL;
+
+    for (const auto& byte : data)
+    {
+        crc ^= byte;
+        for (int i = 0; i < 8; ++i)
+        {
+            if (crc & 0x80)
+                crc = (crc << 1) ^ CRC8_POLYNOMIAL;
+            else
+                crc <<= 1;
+        }
+    }
+    return crc;
+}
+
+// Delay execution for a specified number of milliseconds
+void delay(const int& milliseconds)
+{
+    std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
+}
+
+// Convert big endian (32 bit integer) to a vector of little endian.
+std::vector<uint8_t> bigEndianToLittleEndian(const uint32_t bigEndianValue)
+{
+    std::vector<uint8_t> littleEndianBytes(4);
+
+    littleEndianBytes[3] = (bigEndianValue >> 24) & 0xFF;
+    littleEndianBytes[2] = (bigEndianValue >> 16) & 0xFF;
+    littleEndianBytes[1] = (bigEndianValue >> 8) & 0xFF;
+    littleEndianBytes[0] = bigEndianValue & 0xFF;
+    return littleEndianBytes;
+}
+
+// Validate the existence and size of a firmware file.
+bool validateFWFile(const std::string& fileName)
+{
+    // Ensure the file exists and get the file size.
+    if (!std::filesystem::exists(fileName))
+    {
+        log<level::ERR>(
+            std::format("Firmware file not found: {}", fileName).c_str());
+        return false;
+    }
+
+    // Check the file size
+    auto fileSize = std::filesystem::file_size(fileName);
+    if (fileSize == 0)
+    {
+        log<level::ERR>("Firmware file is empty");
+        return false;
+    }
+    return true;
+}
+
+// Open a firmware file for reading in binary mode.
+std::unique_ptr<std::ifstream> openFirmwareFile(const std::string& fileName)
+{
+    if (fileName.empty())
+    {
+        log<level::ERR>("Firmware file path is not provided");
+        return nullptr;
+    }
+    auto inputFile =
+        std::make_unique<std::ifstream>(fileName, std::ios::binary);
+    if (!inputFile->is_open())
+    {
+        log<level::ERR>(
+            std::format("Failed to open firmware file: {}", fileName).c_str());
+        return nullptr;
+    }
+    return inputFile;
+}
+
+// Read firmware bytes from input stream.
+std::vector<uint8_t> readFirmwareBytes(std::ifstream& inputFile,
+                                       const size_t numberOfBytesToRead)
+{
+    std::vector<uint8_t> readDataBytes(numberOfBytesToRead, 0xFF);
+    try
+    {
+        // Enable exceptions for failbit and badbit
+        inputFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
+        inputFile.read(reinterpret_cast<char*>(readDataBytes.data()),
+                       numberOfBytesToRead);
+        size_t bytesRead = inputFile.gcount();
+        if (bytesRead != numberOfBytesToRead)
+        {
+            readDataBytes.resize(bytesRead);
+        }
+    }
+    catch (const std::ios_base::failure& e)
+    {
+        log<level::ERR>(
+            std::format("Error reading firmware: {}", e.what()).c_str());
+        readDataBytes.clear();
+    }
+    return readDataBytes;
+}
+
+// Wrapper to check existence of PSU JSON file.
+bool usePsuJsonFile()
+{
+    return version::utils::checkFileExists(PSU_JSON_PATH);
+}
 } // namespace internal
 
-bool update(const std::string& psuInventoryPath, const std::string& imageDir)
+bool update(sdbusplus::bus_t& bus, const std::string& psuInventoryPath,
+            const std::string& imageDir)
 {
-    auto devPath = internal::getDevicePath(psuInventoryPath);
+    auto devPath = internal::getDevicePath(bus, psuInventoryPath);
+
     if (devPath.empty())
     {
         return false;
     }
 
-    Updater updater(psuInventoryPath, devPath, imageDir);
-    if (!updater.isReadyToUpdate())
+    std::filesystem::path fsPath(imageDir);
+
+    std::unique_ptr<updater::Updater> updaterPtr = internal::getClassInstance(
+        fsPath.filename().string(), psuInventoryPath, devPath, imageDir);
+
+    if (!updaterPtr->isReadyToUpdate())
     {
         log<level::ERR>("PSU not ready to update",
                         entry("PSU=%s", psuInventoryPath.c_str()));
         return false;
     }
 
-    updater.bindUnbind(false);
-    updater.createI2CDevice();
-    int ret = updater.doUpdate();
-    updater.bindUnbind(true);
+    updaterPtr->bindUnbind(false);
+    updaterPtr->createI2CDevice();
+    int ret = updaterPtr->doUpdate();
+    updaterPtr->bindUnbind(true);
     return ret == 0;
 }
 
@@ -210,7 +397,7 @@
         // directly read the debugfs to get the status.
         try
         {
-            auto path = internal::getDevicePath(p);
+            auto path = internal::getDevicePath(bus, p);
             PMBus pmbus(path);
             uint16_t statusWord = pmbus.read(STATUS_WORD, Type::Debug);
             auto status0Vout = pmbus.insertPageNum(STATUS_VOUT, 0);
@@ -273,5 +460,4 @@
     auto [id, addr] = internal::parseDeviceName(devName);
     i2c = i2c::create(id, addr);
 }
-
 } // namespace updater
diff --git a/tools/power-utils/updater.hpp b/tools/power-utils/updater.hpp
index 458f651..1732f86 100644
--- a/tools/power-utils/updater.hpp
+++ b/tools/power-utils/updater.hpp
@@ -16,11 +16,15 @@
 #pragma once
 
 #include "i2c_interface.hpp"
+#include "version.hpp"
 
 #include <sdbusplus/bus.hpp>
 
+#include <chrono>
 #include <filesystem>
+#include <memory>
 #include <string>
+#include <thread>
 
 class TestUpdater;
 
@@ -32,12 +36,14 @@
 /**
  * Update PSU firmware
  *
+ * @param[in] bus - The sdbusplus DBus bus connection
  * @param[in] psuInventoryPath - The inventory path of the PSU
  * @param[in] imageDir - The directory containing the PSU image
  *
  * @return true if successful, otherwise false
  */
-bool update(const std::string& psuInventoryPath, const std::string& imageDir);
+bool update(sdbusplus::bus_t& bus, const std::string& psuInventoryPath,
+            const std::string& imageDir);
 
 class Updater
 {
@@ -60,7 +66,7 @@
             const std::string& imageDir);
 
     /** @brief Destructor */
-    ~Updater() = default;
+    virtual ~Updater() = default;
 
     /** @brief Bind or unbind the driver
      *
@@ -84,7 +90,7 @@
      *
      * @return 0 if success, otherwise non-zero
      */
-    int doUpdate();
+    virtual int doUpdate();
 
     /** @brief Create I2C device
      *
@@ -93,6 +99,37 @@
      */
     void createI2CDevice();
 
+  protected:
+    /** @brief Accessor for PSU inventory path */
+    const std::string& getPsuInventoryPath() const
+    {
+        return psuInventoryPath;
+    }
+
+    /** @brief Accessor for device path */
+    const std::string& getDevPath() const
+    {
+        return devPath;
+    }
+
+    /** @brief Accessor for device name */
+    const std::string& getDevName() const
+    {
+        return devName;
+    }
+
+    /** @brief Accessor for image directory */
+    const std::string& getImageDir() const
+    {
+        return imageDir;
+    }
+
+    /** @brief I2C interface accessor */
+    i2c::I2CInterface* getI2C()
+    {
+        return i2c.get();
+    }
+
   private:
     /** @brief The sdbusplus DBus bus connection */
     sdbusplus::bus_t bus;
@@ -128,4 +165,122 @@
     std::unique_ptr<i2c::I2CInterface> i2c;
 };
 
+namespace internal
+{
+
+/**
+ * @brief Get the device name from the device path
+ *
+ * @param[in] devPath - PSU path
+ *
+ * @return device name e.g. 3-0068
+ */
+std::string getDeviceName(std::string devPath);
+
+/**
+ * @brief Function to get device path using DBus bus and PSU
+ * inventory Path
+ *
+ * @param[in] bus - The sdbusplus DBus bus connection
+ * @param[in] psuInventoryPath - PSU inventory path
+ *
+ * @return device path e.g. /sys/bus/i2c/devices/3-0068
+ */
+std::string getDevicePath(sdbusplus::bus_t& bus,
+                          const std::string& psuInventoryPath);
+
+/**
+ * @brief Parse the device name to obtain bus and device address
+ *
+ * @param[in] devName - Device name
+ *
+ * @return bus and device address
+ */
+std::pair<uint8_t, uint8_t> parseDeviceName(const std::string& devName);
+
+/**
+ * @brief Factory function to create an Updater instance based on PSU model
+ * number
+ *
+ * @param[in] model - PSU model number
+ * @param[in] psuInventoryPath - PSU inventory path
+ * @param[in] devPath - Device path
+ * @param[in] imageDir - Image directory
+ *
+ * return pointer class based on the device PSU model.
+ */
+std::unique_ptr<updater::Updater> getClassInstance(
+    const std::string& model, const std::string& psuInventoryPath,
+    const std::string& devPath, const std::string& imageDir);
+
+/**
+ * @brief Retrieve the firmware filename path in the specified directory
+ *
+ * @param[in] directory - Path to FS directory
+ *
+ * @retun filename or null
+ */
+const std::string getFWFilenamePath(const std::string& directory);
+
+/**
+ * @brief Calculate CRC-8 for a data vector
+ *
+ * @param[in] data - Firmware data block
+ *
+ * @return CRC8
+ */
+uint8_t calculateCRC8(const std::vector<uint8_t>& data);
+
+/**
+ * @brief Delay execution in milliseconds
+ *
+ * @param[in] milliseconds - Time in milliseconds
+ */
+void delay(const int& milliseconds);
+
+/**
+ * @brief Convert a big-endian value to little-endian
+ *
+ * @param[in] bigEndianValue - Uint 32 bit value
+ *
+ * @return vector of little endians.
+ */
+std::vector<uint8_t> bigEndianToLittleEndian(const uint32_t bigEndianValue);
+
+/**
+ * @brief Validate the existence and size of a firmware file.
+ *
+ * @param[in] fileName - Firmware file name
+ *
+ * @return true for success or false
+ */
+bool validateFWFile(const std::string& fileName);
+
+/**
+ * @brief Open a firmware file for reading in binary mode.
+ *
+ * @param[in] fileName - Firmware file name
+ *
+ * @return pointer to firmware file stream
+ */
+std::unique_ptr<std::ifstream> openFirmwareFile(const std::string& fileName);
+
+/**
+ * @brief Read firmware bytes from file.
+ *
+ * @param[in] inputFile - Input file stream
+ * @param[in] numberOfBytesToRead - Number of bytes to read from firmware file.
+ *
+ * @return vector of data read
+ */
+std::vector<uint8_t> readFirmwareBytes(std::ifstream& inputFile,
+                                       const size_t numberOfBytesToRead);
+
+/**
+ * @brief Wrapper to check existence of PSU JSON file
+ *
+ * @return true or false (true if using JSON file)
+ */
+bool usePsuJsonFile();
+} // namespace internal
 } // namespace updater