Add support for file descriptor PDR

Tested:
Unit tested and also tested on BMC

Google-Bug-Id: 423027264
Change-Id: Ib6c53c78533dfb40d1907bd71a5cf168aede6deb
Signed-off-by: Kasun Athukorala <kasunath@google.com>
diff --git a/libpldmresponder/meson.build b/libpldmresponder/meson.build
index acbb400..25cb1f6 100644
--- a/libpldmresponder/meson.build
+++ b/libpldmresponder/meson.build
@@ -19,6 +19,7 @@
     'bios_config.cpp',
     'pdr_utils.cpp',
     'pdr.cpp',
+    'pdr_file.cpp',
     'platform.cpp',
     'platform_config.cpp',
     'fru_parser.cpp',
diff --git a/libpldmresponder/pdr_file.cpp b/libpldmresponder/pdr_file.cpp
new file mode 100644
index 0000000..dc22805
--- /dev/null
+++ b/libpldmresponder/pdr_file.cpp
@@ -0,0 +1,166 @@
+#include "pdr_file.hpp"
+
+#include <libpldm/entity.h>
+
+#include <phosphor-logging/lg2.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+namespace pldm
+{
+namespace responder
+{
+namespace pdr_file
+{
+
+using Json = nlohmann::json;
+
+void generateFileDescriptorPDR(
+    const Json& json, std::map<uint16_t, FileInfo>& fileIdentifierToFileInfo,
+    std::map<uint32_t, FileInfo>& recordHandleToFileInfo,
+    pdr_utils::RepoInterface& repo)
+{
+    static const std::vector<Json> emptyList{};
+    auto entries = json.value("entries", emptyList);
+    for (const auto& e : entries)
+    {
+        auto entityTypeIt = e.find("EntityType");
+        if (entityTypeIt == e.end() || !entityTypeIt->is_number())
+        {
+            error("EntityType is missing or it is not a number.");
+            continue;
+        }
+
+        uint16_t entityType = static_cast<uint16_t>(*entityTypeIt);
+        if ((entityType != PLDM_ENTITY_DEVICE_FILE) &&
+            (entityType != PLDM_ENTITY_DEVICE_FILE_DIRECTORY))
+        {
+            error(
+                "Invalid EntityType '{ENTITY_TYPE}' for file descriptor PDR. Expected 9 or 10.",
+                "ENTITY_TYPE", entityType);
+            continue;
+        }
+
+        auto pathIt = e.find("Path");
+        if (pathIt == e.end() || !pathIt->is_string())
+        {
+            error("'Path' is missing or it is not a string.");
+            continue;
+        }
+
+        uint16_t container_id = e.value("container", 0);
+
+        auto files = e.value("Files", emptyList);
+        for (const auto& file : files)
+        {
+            auto fileIdIt = file.find("FileIdentifier");
+            if (fileIdIt == file.end() || !fileIdIt->is_number())
+            {
+                error("FileIdentifier is missing or it is not a number.");
+                continue;
+            }
+
+            auto fileNameIt = file.find("FileName");
+            if (fileNameIt == file.end() || !fileNameIt->is_string())
+            {
+                error("FileName is missing or it is not a string.");
+                continue;
+            }
+
+            auto fileSizeIt = file.find("FileSize");
+            if (fileSizeIt == file.end() || !fileSizeIt->is_number())
+            {
+                error("FileSize is missing or it is not a string.");
+                continue;
+            }
+
+            const std::string& fileName = fileNameIt.value();
+            int fileNameLengthWithNull = fileName.length() + 1;
+
+            // Calculate the total length of the PDR including the common PDR
+            // header size. For now this skips the
+            // OemFileClassificationNameLength and OemFileClassificationName
+            // since they are not included.
+            size_t pdrSize = PLDM_PDR_FILE_DESCRIPTOR_PDR_MIN_LENGTH +
+                             fileNameLengthWithNull;
+            pldm_platform_file_descriptor_pdr pdr{};
+
+            // This will be assigned automatically by PDR repository.
+            pdr.hdr.record_handle = 0;
+            pdr.hdr.version = 1;
+            pdr.hdr.type = PLDM_FILE_DESCRIPTOR_PDR;
+            pdr.hdr.record_change_num = 0;
+            pdr.hdr.length = pdrSize - sizeof(pldm_pdr_hdr);
+
+            pdr.terminus_handle = TERMINUS_HANDLE;
+            pdr.file_identifier = *fileIdIt;
+            pdr.container.entity_type = entityType;
+            pdr.container.entity_instance_num = file.value("InstanceNumber", 0);
+            pdr.container.entity_container_id = container_id;
+            pdr.superior_directory_file_identifier =
+                file.value("SuperiorDirectoryFileIdentifier", 0);
+            pdr.file_classification = file.value("FileClassification", 0);
+            pdr.oem_file_classification = 0;
+            pdr.file_capabilities.value = file.value("FileCapabilities", 0);
+            pdr.file_maximum_size = fileSizeIt.value();
+            pdr.file_maximum_file_descriptor_count =
+                file.value("MaxFileDescriptors", 0);
+            pdr.file_name.ptr =
+                reinterpret_cast<const uint8_t*>(fileName.c_str());
+            pdr.file_name.length = fileNameLengthWithNull;
+            pdr.oem_file_classification_name.ptr = nullptr;
+            pdr.oem_file_classification_name.length = 0;
+
+            std::vector<uint8_t> entry{};
+            entry.resize(pdrSize);
+
+            size_t pdrSizeCopy = pdrSize;
+            int ret = encode_pldm_platform_file_descriptor_pdr(
+                &pdr, entry.data(), &pdrSizeCopy);
+            if (ret != 0)
+            {
+                error("Failed to encode file descriptor PDR for '{FILE_NAME}'",
+                      "FILE_NAME", fileName);
+                continue;
+            }
+
+            FileInfo fileInfo{};
+
+            // Try opening the file here. If the open fail, we will not save the
+            // PDR. If we do open the file later on, PDR information and the
+            // actual file might not be in sync.
+            fileInfo.filePath = pathIt->get<std::string>() + "/" + fileName;
+            FILE* fp = fopen(fileInfo.filePath.c_str(), "rb");
+            if (fp == nullptr)
+            {
+                error("Failed to open file '{FILE_PATH}'", "FILE_PATH",
+                      fileInfo.filePath);
+                continue;
+            }
+
+            // This file pointer will be kept open through out the lifetime of
+            // the program
+            // TODO: If the file changes during runtime, we need to load it
+            // again. At that time we will close this and open a file pointer to
+            // the new file.
+            fileInfo.fp = fp;
+            fileInfo.fileIdentifier = *fileIdIt;
+            fileInfo.fileSize = fileSizeIt.value();
+
+            pdr_utils::PdrEntry pdrEntry{};
+            pdrEntry.data = entry.data();
+            pdrEntry.size = pdrSize;
+            pdr_utils::RecordHandle recordHandle = repo.addRecord(pdrEntry);
+
+            fileInfo.recordHandle = static_cast<uint32_t>(recordHandle);
+
+            fileIdentifierToFileInfo.try_emplace(*fileIdIt, fileInfo);
+            recordHandleToFileInfo.try_emplace(
+                static_cast<uint32_t>(recordHandle), fileInfo);
+        }
+    }
+}
+
+} // namespace pdr_file
+} // namespace responder
+} // namespace pldm
diff --git a/libpldmresponder/pdr_file.hpp b/libpldmresponder/pdr_file.hpp
new file mode 100644
index 0000000..fd722f6
--- /dev/null
+++ b/libpldmresponder/pdr_file.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "pdr_utils.hpp"
+
+#include <libpldm/platform.h>
+
+#include <nlohmann/json.hpp>
+
+namespace pldm
+{
+namespace responder
+{
+namespace pdr_file
+{
+
+/**
+ * Data structue to hold the complete file path, C style file pointer, 16 bit
+ * file indetifier and record handle
+ */
+struct FileInfo
+{
+    std::string filePath = "";
+    uint32_t fileSize;
+    FILE* fp = nullptr;
+    uint16_t fileIdentifier = 0;
+    uint32_t recordHandle = 0;
+};
+
+/**
+ * @brief Parse PDR JSON file and generate file descriptor PDR structure
+ *
+ *  @param[in] json - the JSON Object with the file descriptor PDR
+ *  @param[out] fileIdentifierToFileInfo - store FileInfo details based on the
+ * fileIdentifier
+ *  @param[out] recordHandleToFileInfo - store FileInfo details based on the
+ * recordHandle
+ *  @param[out] repo - pdr::RepoInterface
+ */
+void generateFileDescriptorPDR(
+    const nlohmann::json& json,
+    std::map<uint16_t, FileInfo>& fileIdentifierToFileInfo,
+    std::map<uint32_t, FileInfo>& recordHandleToFileInfo,
+    pdr_utils::RepoInterface& repo);
+
+} // namespace pdr_file
+} // namespace responder
+} // namespace pldm
diff --git a/libpldmresponder/platform.cpp b/libpldmresponder/platform.cpp
index b034acb..66f205d 100644
--- a/libpldmresponder/platform.cpp
+++ b/libpldmresponder/platform.cpp
@@ -116,6 +116,14 @@
                     auto json = readJson(dirEntry.path().string());
                     if (!json.empty())
                     {
+                        auto filePdrs = json.value("fileDescriptorPDRs", empty);
+                        for (const auto& filePdrEntries : filePdrs)
+                        {
+                            generateFileDescriptorPDR(
+                                filePdrEntries, fileIdentifierToFileInfo,
+                                recordHandleToFileInfo, repo);
+                        }
+
                         auto effecterPDRs = json.value("effecterPDRs", empty);
                         for (const auto& effecter : effecterPDRs)
                         {
diff --git a/libpldmresponder/platform.hpp b/libpldmresponder/platform.hpp
index 7c835d4..0defc8a 100644
--- a/libpldmresponder/platform.hpp
+++ b/libpldmresponder/platform.hpp
@@ -9,6 +9,7 @@
 #include "libpldmresponder/pdr_utils.hpp"
 #include "libpldmresponder/platform_config.hpp"
 #include "oem_handler.hpp"
+#include "pdr_file.hpp"
 #include "pldmd/handler.hpp"
 
 #include <libpldm/pdr.h>
@@ -546,6 +547,8 @@
     bool pdrCreated;
     std::vector<fs::path> pdrJsonsDir;
     std::unique_ptr<sdeventplus::source::Defer> deferredGetPDREvent;
+    std::map<uint16_t, pdr_file::FileInfo> fileIdentifierToFileInfo;
+    std::map<uint32_t, pdr_file::FileInfo> recordHandleToFileInfo;
 };
 
 /** @brief Function to check if a sensor falls in OEM range
diff --git a/libpldmresponder/test/libpldmresponder_pdr_file_test.cpp b/libpldmresponder/test/libpldmresponder_pdr_file_test.cpp
new file mode 100644
index 0000000..51b4f38
--- /dev/null
+++ b/libpldmresponder/test/libpldmresponder_pdr_file_test.cpp
@@ -0,0 +1,336 @@
+#include "libpldmresponder/pdr_file.hpp"
+#include "libpldmresponder/pdr_utils.hpp"
+
+#include <libpldm/platform.h>
+
+#include <nlohmann/json.hpp>
+
+#include <filesystem>
+#include <fstream>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace pldm::responder::pdr_file;
+using namespace pldm::responder::pdr_utils;
+using ::testing::_;
+using ::testing::Return;
+using Json = nlohmann::json;
+namespace fs = std::filesystem;
+
+class MockRepo : public RepoInterface
+{
+  public:
+    MockRepo() : RepoInterface(nullptr) {}
+
+    MOCK_METHOD(pldm_pdr*, getPdr, (), (const, override));
+    MOCK_METHOD(RecordHandle, addRecord, (const PdrEntry&), (override));
+    MOCK_METHOD(const pldm_pdr_record*, getFirstRecord, (PdrEntry&),
+                (override));
+    MOCK_METHOD(const pldm_pdr_record*, getNextRecord,
+                (const pldm_pdr_record*, PdrEntry&), (override));
+    MOCK_METHOD(uint32_t, getRecordHandle, (const pldm_pdr_record*),
+                (const, override));
+    MOCK_METHOD(uint32_t, getRecordCount, (), (override));
+    MOCK_METHOD(bool, empty, (), (override));
+    MOCK_METHOD(uint32_t, getRepositorySize, (), (override));
+    MOCK_METHOD(uint32_t, getLargestRecordSize, (), (override));
+};
+
+class GenerateFileDescriptorPDRTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        // Create a temporary directory for test files.
+        char tmppldm[] = "/tmp/pldm_file_pdr_test.XXXXXX";
+        dir = fs::path(mkdtemp(tmppldm));
+
+        // Create a dummy file for the success case.
+        testFilePath = dir / "test_dir";
+        fs::create_directory(testFilePath);
+        std::ofstream(testFilePath / "test_file.bin").close();
+    }
+
+    void TearDown() override
+    {
+        for (const auto& pair : fileIdentifierToFileInfo)
+        {
+            if (pair.second.fp)
+            {
+                fclose(pair.second.fp);
+            }
+        }
+        fs::remove_all(dir);
+    }
+
+    fs::path dir;
+    fs::path testFilePath;
+    MockRepo repo;
+    std::map<uint16_t, FileInfo> fileIdentifierToFileInfo;
+    std::map<uint32_t, FileInfo> recordHandleToFileInfo;
+};
+
+TEST_F(GenerateFileDescriptorPDRTest, SuccessCase)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "placeholder",
+                "Files": [
+                    {
+                        "FileIdentifier": 1,
+                        "FileName": "test_file.bin",
+                        "FileSize": 1234
+                    }
+                ]
+            }
+        ]
+    })"_json;
+
+    auto modifiedJson = json;
+    modifiedJson["entries"][0]["Path"] = testFilePath.string();
+
+    EXPECT_CALL(repo, addRecord(_)).WillOnce(Return(1));
+
+    generateFileDescriptorPDR(modifiedJson, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+
+    EXPECT_EQ(fileIdentifierToFileInfo.size(), 1);
+    EXPECT_EQ(recordHandleToFileInfo.size(), 1);
+    EXPECT_TRUE(fileIdentifierToFileInfo.contains(1));
+    EXPECT_TRUE(recordHandleToFileInfo.contains(1));
+
+    const auto& fileInfo = fileIdentifierToFileInfo.at(1);
+    EXPECT_EQ(fileInfo.fileIdentifier, 1);
+    EXPECT_EQ(fileInfo.fileSize, 1234);
+    EXPECT_EQ(fileInfo.recordHandle, 1);
+    EXPECT_EQ(fileInfo.filePath, testFilePath / "test_file.bin");
+    EXPECT_NE(fileInfo.fp, nullptr);
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureMissingEntityType)
+{
+    const Json json = R"({ "entries": [{ "Path": "p" }]})"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureEntityTypeNotNumber)
+{
+    const Json json =
+        R"({ "entries": [{ "EntityType": "abc", "Path": "p" }]})"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureInvalidEntityType)
+{
+    const Json json =
+        R"({ "entries": [{ "EntityType": 8, "Path": "p" }]})"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureMissingPath)
+{
+    const Json json = R"({ "entries": [{ "EntityType": 9 }]})"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailurePathNotString)
+{
+    const Json json =
+        R"({ "entries": [{ "EntityType": 9, "Path": 123 }]})"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureMissingFileIdentifier)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": [ { "FileName": "f", "FileSize": 0 } ]
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureFileIdentifierNotNumber)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": [ { "FileIdentifier": "abc", "FileName": "f", "FileSize": 0 } ]
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureMissingFileName)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": [ { "FileIdentifier": 1, "FileSize": 0 } ]
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureFileNameNotString)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": [ { "FileIdentifier": 1, "FileName": 123, "FileSize": 0 } ]
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureMissingFileSize)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": [ { "FileIdentifier": 1, "FileName": "f" } ]
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureFileSizeNotNumber)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": [ { "FileIdentifier": 1, "FileName": "f", "FileSize": "abc" } ]
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureFileNameTooLong)
+{
+    std::string longName(255, 'a');
+    Json json = {{"entries",
+                  {{{"EntityType", 9},
+                    {"Path", "p"},
+                    {"Files",
+                     {{{"FileIdentifier", 1},
+                       {"FileName", longName},
+                       {"FileSize", 0}}}}}}}};
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, FailureCannotOpenFile)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "nonexistent_dir",
+                "Files": [
+                    {
+                        "FileIdentifier": 1,
+                        "FileName": "nonexistent_file.bin",
+                        "FileSize": 0
+                    }
+                ]
+            }
+        ]
+    })"_json;
+
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, CornerCaseEmptyEntries)
+{
+    const Json json = R"({ "entries": [] })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
+
+TEST_F(GenerateFileDescriptorPDRTest, CornerCaseEmptyFiles)
+{
+    const Json json = R"(
+    {
+        "entries": [
+            {
+                "EntityType": 9,
+                "Path": "p",
+                "Files": []
+            }
+        ]
+    })"_json;
+    generateFileDescriptorPDR(json, fileIdentifierToFileInfo,
+                              recordHandleToFileInfo, repo);
+    EXPECT_TRUE(fileIdentifierToFileInfo.empty());
+    EXPECT_TRUE(recordHandleToFileInfo.empty());
+}
diff --git a/libpldmresponder/test/meson.build b/libpldmresponder/test/meson.build
index fa581cd..9cb5ca9 100644
--- a/libpldmresponder/test/meson.build
+++ b/libpldmresponder/test/meson.build
@@ -9,6 +9,7 @@
     'libpldmresponder_fru_test',
     'libpldmresponder_platform_test',
     'libpldmresponder_pdr_effecter_test',
+    'libpldmresponder_pdr_file_test',
     'libpldmresponder_pdr_sensor_test',
 ]