FRU Parsing for platform specific config files

This patch adds the parsing for the platform specific config
files need to build the PLDM FRU records. The BMC FRU info is
populated in the D-Bus inventory namespace and these config JSON's
provide the necessary mapping to translate D-Bus properties into
PLDM record information.

Change-Id: I600cfb7c6dcf529465b2618a2e040aa1e66c1607
Signed-off-by: Tom Joseph <tomjoseph@in.ibm.com>
diff --git a/libpldmresponder/fru_parser.cpp b/libpldmresponder/fru_parser.cpp
new file mode 100644
index 0000000..b8bb01b
--- /dev/null
+++ b/libpldmresponder/fru_parser.cpp
@@ -0,0 +1,135 @@
+#include "fru_parser.hpp"
+
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <nlohmann/json.hpp>
+#include <xyz/openbmc_project/Common/error.hpp>
+
+namespace pldm
+{
+
+namespace responder
+{
+
+namespace fru_parser
+{
+
+using Json = nlohmann::json;
+using InternalFailure =
+    sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;
+
+const Json emptyJson{};
+const std::vector<Json> emptyJsonList{};
+const std::vector<std::string> emptyStringVec{};
+
+constexpr auto fruMasterJson = "FRU_Master.json";
+
+FruParser::FruParser(const std::string& dirPath)
+{
+    fs::path dir(dirPath);
+    if (!fs::exists(dir) || fs::is_empty(dir))
+    {
+        std::cerr << "FRU config directory does not exist or empty, DIR="
+                  << dirPath;
+        throw InternalFailure();
+    }
+
+    fs::path masterFilePath = dir / fruMasterJson;
+    if (!fs::exists(masterFilePath))
+    {
+        std::cerr << "FRU D-Bus lookup JSON does not exist, PATH="
+                  << masterFilePath;
+        throw InternalFailure();
+    }
+
+    setupDBusLookup(masterFilePath);
+    setupFruRecordMap(dirPath);
+}
+
+void FruParser::setupDBusLookup(const fs::path& filePath)
+{
+    std::ifstream jsonFile(filePath);
+
+    auto data = Json::parse(jsonFile, nullptr, false);
+    if (data.is_discarded())
+    {
+        std::cerr << "Parsing FRU master config file failed, FILE=" << filePath;
+        throw InternalFailure();
+    }
+
+    Service service = data.value("service", "");
+    RootPath rootPath = data.value("root_path", "");
+    Interfaces interfaces = data.value("interfaces", emptyStringVec);
+    lookupInfo.emplace(std::make_tuple(std::move(service), std::move(rootPath),
+                                       std::move(interfaces)));
+}
+
+void FruParser::setupFruRecordMap(const std::string& dirPath)
+{
+    for (auto& file : fs::directory_iterator(dirPath))
+    {
+        auto fileName = file.path().filename().string();
+        if (fruMasterJson == fileName)
+        {
+            continue;
+        }
+
+        std::ifstream jsonFile(file.path());
+        auto data = Json::parse(jsonFile, nullptr, false);
+        if (data.is_discarded())
+        {
+
+            std::cerr << "Parsing FRU master config file failed, FILE="
+                      << file.path();
+            throw InternalFailure();
+        }
+
+        auto record = data.value("record_details", emptyJson);
+        auto recordType =
+            static_cast<uint8_t>(record.value("fru_record_type", 0));
+        auto encType =
+            static_cast<uint8_t>(record.value("fru_encoding_type", 0));
+        auto dbusIntfName = record.value("dbus_interface_name", "");
+        auto entries = data.value("fru_fields", emptyJsonList);
+        std::vector<FieldInfo> fieldInfo;
+
+        for (const auto& entry : entries)
+        {
+            auto fieldType =
+                static_cast<uint8_t>(entry.value("fru_field_type", 0));
+            auto dbus = entry.value("dbus", emptyJson);
+            auto interface = dbus.value("interface", "");
+            auto property = dbus.value("property_name", "");
+            auto propType = dbus.value("property_type", "");
+            fieldInfo.emplace_back(
+                std::make_tuple(std::move(interface), std::move(property),
+                                std::move(propType), std::move(fieldType)));
+        }
+
+        FruRecordInfo fruInfo;
+        fruInfo = std::make_tuple(recordType, encType, std::move(fieldInfo));
+
+        auto search = recordMap.find(dbusIntfName);
+
+        // PLDM FRU can have multiple records for the same FRU like General FRU
+        // record and multiple OEM FRU records. If the FRU item interface name
+        // is already in the map, that indicates a record info is already added
+        // for the FRU, so append the new record info to the same data.
+        if (search != recordMap.end())
+        {
+            search->second.emplace_back(std::move(fruInfo));
+        }
+        else
+        {
+            FruRecordInfos recordInfos{fruInfo};
+            recordMap.emplace(dbusIntfName, recordInfos);
+        }
+    }
+}
+
+} // namespace fru_parser
+
+} // namespace responder
+
+} // namespace pldm
diff --git a/libpldmresponder/fru_parser.hpp b/libpldmresponder/fru_parser.hpp
new file mode 100644
index 0000000..f49c04b
--- /dev/null
+++ b/libpldmresponder/fru_parser.hpp
@@ -0,0 +1,123 @@
+#pragma once
+
+#include <filesystem>
+#include <map>
+#include <string>
+#include <tuple>
+#include <vector>
+
+namespace pldm
+{
+
+namespace responder
+{
+
+namespace dbus
+{
+
+using Service = std::string;
+using RootPath = std::string;
+using Interface = std::string;
+using Interfaces = std::vector<Interface>;
+using Property = std::string;
+using PropertyType = std::string;
+
+} // namespace dbus
+
+namespace fru
+{
+
+using RecordType = uint8_t;
+using EncodingType = uint8_t;
+using FieldType = uint8_t;
+
+} // namespace fru
+
+namespace fru_parser
+{
+
+namespace fs = std::filesystem;
+using namespace dbus;
+using namespace fru;
+
+// DBusLookupInfo contains info to lookup in the D-Bus inventory, D-Bus
+// inventory service bus name, root path of the inventory D-Bus objects and list
+//  of xyz.openbmc_project.Inventory.Item.* interface names.
+using DBusLookupInfo = std::tuple<Service, RootPath, Interfaces>;
+using FieldInfo = std::tuple<Interface, Property, PropertyType, FieldType>;
+
+using FruRecordInfo =
+    std::tuple<RecordType, EncodingType, std::vector<FieldInfo>>;
+using FruRecordInfos = std::vector<FruRecordInfo>;
+
+using ItemIntfName = std::string;
+using FruRecordMap = std::map<ItemIntfName, FruRecordInfos>;
+
+/** @class FruParser
+ *
+ *  @brief Parses the PLDM FRU configuration files to populate the data
+ *         structure, containing the information needed to map the D-Bus
+ *         inventory information into PLDM FRU Record.
+ */
+class FruParser
+{
+
+  public:
+    FruParser() = delete;
+    explicit FruParser(const std::string& dirPath);
+    virtual ~FruParser() = default;
+    FruParser(const FruParser&) = default;
+    FruParser& operator=(const FruParser&) = default;
+    FruParser(FruParser&&) = default;
+    FruParser& operator=(FruParser&&) = default;
+
+    /** @brief Provides the service, root D-Bus path and the interfaces that is
+     *         needed to build FRU record data table
+     *
+     *  @return service and inventory interfaces needed to build the FRU records
+     */
+    const DBusLookupInfo& inventoryLookup() const
+    {
+        return lookupInfo.value();
+    }
+
+    /** @brief Get the information need to create PLDM FRU records for a
+     * inventory item type. The parameter to this API is the inventory item
+     * type, for example xyz.openbmc_project.Inventory.Item.Cpu for CPU's
+     *
+     *  @param[in] intf - name of the item interface
+     *
+     *  @return return the info create the PLDM FRU records from inventory D-Bus
+     *          objects
+     */
+    const FruRecordInfos& getRecordInfo(const Interface& intf) const
+    {
+        return recordMap.at(intf);
+    }
+
+  private:
+    /** @brief Parse the FRU_Master.json file and populate the D-Bus lookup
+     *         information which provides the service, root D-Bus path and the
+     *         item interfaces.
+     *
+     *  @param[in] filePath - file path to FRU_Master.json
+     */
+    void setupDBusLookup(const fs::path& filePath);
+
+    /** @brief Parse the FRU Configuration JSON file in the directory path
+     *         except the FRU_Master.json and build the FRU record information
+     *
+     *  @param[in] dirPath - directory path where all the FRU configuration JSON
+     *                       files exist
+     */
+    void setupFruRecordMap(const std::string& dirPath);
+
+    std::optional<DBusLookupInfo> lookupInfo;
+    FruRecordMap recordMap;
+};
+
+} // namespace fru_parser
+
+} // namespace responder
+
+} // namespace pldm
diff --git a/libpldmresponder/meson.build b/libpldmresponder/meson.build
index 2a2705e..92a13be 100644
--- a/libpldmresponder/meson.build
+++ b/libpldmresponder/meson.build
@@ -13,6 +13,7 @@
   'pdr.cpp',
   'effecters.cpp',
   'platform.cpp',
+  'fru_parser.cpp'
 ]
 
 if get_option('oem-ibm').enabled()
diff --git a/test/fru_jsons/good/Board_General.json b/test/fru_jsons/good/Board_General.json
new file mode 100644
index 0000000..d4efdab
--- /dev/null
+++ b/test/fru_jsons/good/Board_General.json
@@ -0,0 +1,29 @@
+{
+   "record_details":
+   {
+        "fru_record_type" : 1,
+        "fru_encoding_type": 1,
+        "dbus_interface_name": "xyz.openbmc_project.Inventory.Item.Board"
+   },
+   "fru_fields":[
+      {
+         "fru_field_type" : 3,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "PartNumber",
+               "property_type" : "string"
+            }
+      },
+      {
+         "fru_field_type" : 4,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "SerialNumber",
+               "property_type" : "string"
+            }
+      }
+    ]
+}
+
diff --git a/test/fru_jsons/good/Board_VINI.json b/test/fru_jsons/good/Board_VINI.json
new file mode 100644
index 0000000..1ef87ef
--- /dev/null
+++ b/test/fru_jsons/good/Board_VINI.json
@@ -0,0 +1,30 @@
+{
+   "record_details":
+   {
+        "fru_record_type" : 254,
+        "fru_encoding_type": 1,
+        "dbus_interface_name": "xyz.openbmc_project.Inventory.Item.Board"
+   },
+   "fru_fields":[
+      {
+         "fru_field_type" : 2,
+         "dbus":
+            {
+               "interface" : "com.ibm.ipzvpd.VINI",
+               "property_name" : "RT",
+               "property_type" : "string"
+            }
+      },
+      {
+         "fru_field_type" : 3,
+         "dbus":
+            {
+               "interface" : "com.ibm.ipzvpd.VINI",
+               "property_name" : "B3",
+               "property_type" : "bytearray"
+            }
+      }
+
+    ]
+}
+
diff --git a/test/fru_jsons/good/Cpu_General.json b/test/fru_jsons/good/Cpu_General.json
new file mode 100644
index 0000000..01a378a
--- /dev/null
+++ b/test/fru_jsons/good/Cpu_General.json
@@ -0,0 +1,28 @@
+{
+   "record_details":
+   {
+        "fru_record_type" : 1,
+        "fru_encoding_type": 1,
+        "dbus_interface_name": "xyz.openbmc_project.Inventory.Item.Cpu"
+   },
+   "fru_fields":[
+      {
+         "fru_field_type" : 3,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "PartNumber",
+               "property_type" : "string"
+            }
+      },
+      {
+         "fru_field_type" : 4,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "SerialNumber",
+               "property_type" : "string"
+            }
+      }
+    ]
+}
diff --git a/test/fru_jsons/good/FRU_Master.json b/test/fru_jsons/good/FRU_Master.json
new file mode 100644
index 0000000..9c631fb
--- /dev/null
+++ b/test/fru_jsons/good/FRU_Master.json
@@ -0,0 +1,8 @@
+{
+    "service":"xyz.openbmc_project.Inventory.Manager",
+    "root_path":"/xyz/openbmc_project/inventory/system/",
+    "interfaces":[
+        "xyz.openbmc_project.Inventory.Item.Board",
+        "xyz.openbmc_project.Inventory.Item.Cpu"
+    ]
+}
diff --git a/test/fru_jsons/malformed1/Board_General.json b/test/fru_jsons/malformed1/Board_General.json
new file mode 100644
index 0000000..aa6dc4b
--- /dev/null
+++ b/test/fru_jsons/malformed1/Board_General.json
@@ -0,0 +1,28 @@
+{
+   "record_details":
+   {
+        "fru_record_type" : 1,
+        "encoding_type": 1
+   },
+   "fields":[
+      {
+         "field_type" : 3,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "PartNumber",
+               "property_type" : "string"
+            }
+      },
+      {
+         "field_type" : 4,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "SerialNumber",
+               "property_type" : "string"
+            }
+      }
+    ]
+}
+
diff --git a/test/fru_jsons/malformed2/FRU_Master.json b/test/fru_jsons/malformed2/FRU_Master.json
new file mode 100644
index 0000000..cb23da7
--- /dev/null
+++ b/test/fru_jsons/malformed2/FRU_Master.json
@@ -0,0 +1,8 @@
+{
+    "service":"xyz.openbmc_project.Inventory.Manager"
+    "root_path":"/xyz/openbmc_project/inventory/system/",
+    "interfaces":[
+        "xyz.openbmc_project.Inventory.Item.Board",
+        "xyz.openbmc_project.Inventory.Item.Cpu"
+    ]
+}
diff --git a/test/fru_jsons/malformed3/Cpu_General.json b/test/fru_jsons/malformed3/Cpu_General.json
new file mode 100644
index 0000000..99ebd39
--- /dev/null
+++ b/test/fru_jsons/malformed3/Cpu_General.json
@@ -0,0 +1,28 @@
+{
+   "record_details":
+   {
+        "fru_record_type" : 1
+        "fru_encoding_type": 1,
+        "dbus_interface_name": "xyz.openbmc_project.Inventory.Item.Cpu"
+   },
+   "fru_fields":[
+      {
+         "fru_field_type" : 3,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "PartNumber",
+               "property_type" : "string"
+            }
+      },
+      {
+         "fru_field_type" : 4,
+         "dbus":
+            {
+               "interface" : "xyz.openbmc_project.Inventory.Decorator.Asset",
+               "property_name" : "SerialNumber",
+               "property_type" : "string"
+            }
+      }
+    ]
+}
diff --git a/test/fru_jsons/malformed3/FRU_Master.json b/test/fru_jsons/malformed3/FRU_Master.json
new file mode 100644
index 0000000..9c631fb
--- /dev/null
+++ b/test/fru_jsons/malformed3/FRU_Master.json
@@ -0,0 +1,8 @@
+{
+    "service":"xyz.openbmc_project.Inventory.Manager",
+    "root_path":"/xyz/openbmc_project/inventory/system/",
+    "interfaces":[
+        "xyz.openbmc_project.Inventory.Item.Board",
+        "xyz.openbmc_project.Inventory.Item.Cpu"
+    ]
+}
diff --git a/test/libpldmresponder_fru_test.cpp b/test/libpldmresponder_fru_test.cpp
new file mode 100644
index 0000000..6a0dbbb
--- /dev/null
+++ b/test/libpldmresponder_fru_test.cpp
@@ -0,0 +1,50 @@
+#include "libpldmresponder/fru_parser.hpp"
+
+#include <gtest/gtest.h>
+
+TEST(FruParser, allScenarios)
+{
+    using namespace pldm::responder::fru_parser;
+
+    // Empty directory condition
+    ASSERT_THROW(FruParser("./fru_json"), std::exception);
+    // No master FRU JSON
+    ASSERT_THROW(FruParser("./fru_jsons/malformed1"), std::exception);
+    // Malformed master FRU JSON
+    ASSERT_THROW(FruParser("./fru_jsons/malformed2"), std::exception);
+    // Malformed FRU JSON
+    ASSERT_THROW(FruParser("./fru_jsons/malformed3"), std::exception);
+
+    FruParser parser{"./fru_jsons/good"};
+
+    // Get an item with a single PLDM FRU record
+    FruRecordInfos cpu{{1,
+                        1,
+                        {{"xyz.openbmc_project.Inventory.Decorator.Asset",
+                          "PartNumber", "string", 3},
+                         {"xyz.openbmc_project.Inventory.Decorator.Asset",
+                          "SerialNumber", "string", 4}}}};
+    auto cpuInfos =
+        parser.getRecordInfo("xyz.openbmc_project.Inventory.Item.Cpu");
+    ASSERT_EQ(cpuInfos.size(), 1);
+    ASSERT_EQ(cpu == cpuInfos, true);
+
+    // Get an item type with 2 PLDM FRU records
+    auto boardInfos =
+        parser.getRecordInfo("xyz.openbmc_project.Inventory.Item.Board");
+    ASSERT_EQ(boardInfos.size(), 2);
+
+    // D-Bus lookup info for FRU information
+    DBusLookupInfo lookupInfo{"xyz.openbmc_project.Inventory.Manager",
+                              "/xyz/openbmc_project/inventory/system/",
+                              {"xyz.openbmc_project.Inventory.Item.Board",
+                               "xyz.openbmc_project.Inventory.Item.Cpu"}};
+
+    auto dbusInfo = parser.inventoryLookup();
+    ASSERT_EQ(dbusInfo == lookupInfo, true);
+
+    // Search for an invalid item type
+    ASSERT_THROW(
+        parser.getRecordInfo("xyz.openbmc_project.Inventory.Item.DIMM"),
+        std::exception);
+}
diff --git a/test/meson.build b/test/meson.build
index 567ac34..c32451b 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -31,6 +31,7 @@
   'pldmd_instanceid_test',
   'pldmd_registration_test',
   'pldm_utils_test',
+  'libpldmresponder_fru_test'
 ]
 
 if get_option('oem-ibm').enabled()