PEL: Read FFDC files and create PEL sections

Fill in the makeFFDCuserDataSection() function to read the file
descriptor passed into it and create the UserData PEL section with the
data.

If the data is in the CBOR format, add in the number of bytes required
to pad the section to a 4 byte boundary to the end of the data, so that
the parsing code can later remove that number and the padding before
parsing the CBOR (it would fail trying to parse pad bytes as CBOR).

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I2c5ba3e6ae14f99da930c01d1139bbbd31a00996
diff --git a/extensions/openpower-pels/pel.cpp b/extensions/openpower-pels/pel.cpp
index d6c404d..c741727 100644
--- a/extensions/openpower-pels/pel.cpp
+++ b/extensions/openpower-pels/pel.cpp
@@ -27,6 +27,9 @@
 #include "stream.hpp"
 #include "user_data_formats.hpp"
 
+#include <sys/stat.h>
+#include <unistd.h>
+
 #include <iostream>
 #include <phosphor-logging/log.hpp>
 
@@ -474,10 +477,116 @@
     return makeJSONUserDataSection(json);
 }
 
+std::vector<uint8_t> readFD(int fd)
+{
+    std::vector<uint8_t> data;
+
+    // Get the size
+    struct stat s;
+    int r = fstat(fd, &s);
+    if (r != 0)
+    {
+        auto e = errno;
+        log<level::ERR>("Could not get FFDC file size from FD",
+                        entry("ERRNO=%d", e));
+        return data;
+    }
+
+    if (0 == s.st_size)
+    {
+        log<level::ERR>("FFDC file is empty");
+        return data;
+    }
+
+    data.resize(s.st_size);
+
+    // Make sure its at the beginning, as maybe another
+    // extension already used it.
+    r = lseek(fd, 0, SEEK_SET);
+    if (r == -1)
+    {
+        auto e = errno;
+        log<level::ERR>("Could not seek to beginning of FFDC file",
+                        entry("ERRNO=%d", e));
+        return data;
+    }
+
+    r = read(fd, data.data(), s.st_size);
+    if (r == -1)
+    {
+        auto e = errno;
+        log<level::ERR>("Could not read FFDC file", entry("ERRNO=%d", e));
+    }
+    else if (r != s.st_size)
+    {
+        log<level::WARNING>("Could not read full FFDC file",
+                            entry("FILE_SIZE=%d", s.st_size),
+                            entry("SIZE_READ=%d", r));
+    }
+
+    return data;
+}
+
 std::unique_ptr<UserData> makeFFDCuserDataSection(uint16_t componentID,
                                                   const PelFFDCfile& file)
 {
-    return std::unique_ptr<UserData>();
+    auto data = readFD(file.fd);
+
+    if (data.empty())
+    {
+        return std::unique_ptr<UserData>();
+    }
+
+    // The data needs 4 Byte alignment, and save amount padded for the
+    // CBOR case.
+    uint32_t pad = 0;
+    while (data.size() % 4)
+    {
+        data.push_back(0);
+        pad++;
+    }
+
+    // For JSON, CBOR, and Text use our component ID, subType, and version,
+    // otherwise use the supplied ones.
+    uint16_t compID = static_cast<uint16_t>(ComponentID::phosphorLogging);
+    uint8_t subType{};
+    uint8_t version{};
+
+    switch (file.format)
+    {
+        case UserDataFormat::json:
+            subType = static_cast<uint8_t>(UserDataFormat::json);
+            version = static_cast<uint8_t>(UserDataFormatVersion::json);
+            break;
+        case UserDataFormat::cbor:
+            subType = static_cast<uint8_t>(UserDataFormat::cbor);
+            version = static_cast<uint8_t>(UserDataFormatVersion::cbor);
+
+            // The CBOR parser will fail on the extra pad bytes since they
+            // aren't CBOR.  Add the amount we padded to the end and other
+            // code will remove it all before parsing.
+            {
+                data.resize(data.size() + 4);
+                Stream stream{data};
+                stream.offset(data.size() - 4);
+                stream << pad;
+            }
+
+            break;
+        case UserDataFormat::text:
+            subType = static_cast<uint8_t>(UserDataFormat::text);
+            version = static_cast<uint8_t>(UserDataFormatVersion::text);
+            break;
+        case UserDataFormat::custom:
+        default:
+            // Use the passed in values
+            compID = componentID;
+            subType = file.subType;
+            version = file.version;
+            break;
+    }
+
+    return std::make_unique<UserData>(compID, subType, version, data);
 }
 
 } // namespace util
diff --git a/test/openpower-pels/pel_test.cpp b/test/openpower-pels/pel_test.cpp
index c5a1571..c6a29b6 100644
--- a/test/openpower-pels/pel_test.cpp
+++ b/test/openpower-pels/pel_test.cpp
@@ -33,6 +33,27 @@
 {
 };
 
+fs::path makeTempDir()
+{
+    char path[] = "/tmp/tempdirXXXXXX";
+    std::filesystem::path dir = mkdtemp(path);
+    return dir;
+}
+
+int writeFileAndGetFD(const fs::path& dir, const std::vector<uint8_t>& data)
+{
+    static size_t count = 0;
+    fs::path path = dir / (std::string{"file"} + std::to_string(count));
+    std::ofstream stream{path};
+    count++;
+
+    stream.write(reinterpret_cast<const char*>(data.data()), data.size());
+    stream.close();
+
+    FILE* fp = fopen(path.c_str(), "r");
+    return fileno(fp);
+}
+
 TEST_F(PELTest, FlattenTest)
 {
     auto data = pelDataFactory(TestPELType::pelSimple);
@@ -441,3 +462,308 @@
         }
     }
 }
+
+PelFFDCfile getJSONFFDC(const fs::path& dir)
+{
+    PelFFDCfile ffdc;
+    ffdc.format = UserDataFormat::json;
+    ffdc.subType = 5;
+    ffdc.version = 42;
+
+    auto inputJSON = R"({
+        "key1": "value1",
+        "key2": 42,
+        "key3" : [1, 2, 3, 4, 5],
+        "key4": {"key5": "value5"}
+    })"_json;
+
+    // Write the JSON to a file and get its descriptor.
+    auto s = inputJSON.dump();
+    std::vector<uint8_t> data{s.begin(), s.end()};
+    ffdc.fd = writeFileAndGetFD(dir, data);
+
+    return ffdc;
+}
+
+TEST_F(PELTest, MakeJSONFileUDSectionTest)
+{
+    auto dir = makeTempDir();
+
+    {
+        auto ffdc = getJSONFFDC(dir);
+
+        auto ud = util::makeFFDCuserDataSection(0x2002, ffdc);
+        close(ffdc.fd);
+        ASSERT_TRUE(ud);
+        ASSERT_TRUE(ud->valid());
+        EXPECT_EQ(ud->header().id, 0x5544);
+
+        EXPECT_EQ(ud->header().version,
+                  static_cast<uint8_t>(UserDataFormatVersion::json));
+        EXPECT_EQ(ud->header().subType,
+                  static_cast<uint8_t>(UserDataFormat::json));
+        EXPECT_EQ(ud->header().componentID,
+                  static_cast<uint16_t>(ComponentID::phosphorLogging));
+
+        // Pull the JSON back out of the the UserData section
+        const auto& d = ud->data();
+        std::string js{d.begin(), d.end()};
+        auto json = nlohmann::json::parse(js);
+
+        EXPECT_EQ("value1", json["key1"].get<std::string>());
+        EXPECT_EQ(42, json["key2"].get<int>());
+
+        std::vector<int> key3Values{1, 2, 3, 4, 5};
+        EXPECT_EQ(key3Values, json["key3"].get<std::vector<int>>());
+
+        std::map<std::string, std::string> key4Values{{"key5", "value5"}};
+        auto actual = json["key4"].get<std::map<std::string, std::string>>();
+        EXPECT_EQ(key4Values, actual);
+    }
+
+    {
+        // A bad FD
+        PelFFDCfile ffdc;
+        ffdc.format = UserDataFormat::json;
+        ffdc.subType = 5;
+        ffdc.version = 42;
+        ffdc.fd = 10000;
+
+        // The section shouldn't get made
+        auto ud = util::makeFFDCuserDataSection(0x2002, ffdc);
+        ASSERT_FALSE(ud);
+    }
+
+    fs::remove_all(dir);
+}
+
+PelFFDCfile getCBORFFDC(const fs::path& dir)
+{
+    PelFFDCfile ffdc;
+    ffdc.format = UserDataFormat::cbor;
+    ffdc.subType = 5;
+    ffdc.version = 42;
+
+    auto inputJSON = R"({
+        "key1": "value1",
+        "key2": 42,
+        "key3" : [1, 2, 3, 4, 5],
+        "key4": {"key5": "value5"}
+    })"_json;
+
+    // Convert the JSON to CBOR and write it to a file
+    auto data = nlohmann::json::to_cbor(inputJSON);
+    ffdc.fd = writeFileAndGetFD(dir, data);
+
+    return ffdc;
+}
+
+TEST_F(PELTest, MakeCBORFileUDSectionTest)
+{
+    auto dir = makeTempDir();
+
+    auto ffdc = getCBORFFDC(dir);
+    auto ud = util::makeFFDCuserDataSection(0x2002, ffdc);
+    close(ffdc.fd);
+    ASSERT_TRUE(ud);
+    ASSERT_TRUE(ud->valid());
+    EXPECT_EQ(ud->header().id, 0x5544);
+
+    EXPECT_EQ(ud->header().version,
+              static_cast<uint8_t>(UserDataFormatVersion::cbor));
+    EXPECT_EQ(ud->header().subType, static_cast<uint8_t>(UserDataFormat::cbor));
+    EXPECT_EQ(ud->header().componentID,
+              static_cast<uint16_t>(ComponentID::phosphorLogging));
+
+    // Pull the CBOR back out of the PEL section
+    // The number of pad bytes to make the section be 4B aligned
+    // was added at the end, read it and then remove it and the
+    // padding before parsing it.
+    auto data = ud->data();
+    Stream stream{data};
+    stream.offset(data.size() - 4);
+    uint32_t pad;
+    stream >> pad;
+
+    data.resize(data.size() - 4 - pad);
+
+    auto json = nlohmann::json::from_cbor(data);
+
+    EXPECT_EQ("value1", json["key1"].get<std::string>());
+    EXPECT_EQ(42, json["key2"].get<int>());
+
+    std::vector<int> key3Values{1, 2, 3, 4, 5};
+    EXPECT_EQ(key3Values, json["key3"].get<std::vector<int>>());
+
+    std::map<std::string, std::string> key4Values{{"key5", "value5"}};
+    auto actual = json["key4"].get<std::map<std::string, std::string>>();
+    EXPECT_EQ(key4Values, actual);
+
+    fs::remove_all(dir);
+}
+
+PelFFDCfile getTextFFDC(const fs::path& dir)
+{
+    PelFFDCfile ffdc;
+    ffdc.format = UserDataFormat::text;
+    ffdc.subType = 5;
+    ffdc.version = 42;
+
+    std::string text{"this is some text that will be used for FFDC"};
+    std::vector<uint8_t> data{text.begin(), text.end()};
+
+    ffdc.fd = writeFileAndGetFD(dir, data);
+
+    return ffdc;
+}
+
+TEST_F(PELTest, MakeTextFileUDSectionTest)
+{
+    auto dir = makeTempDir();
+
+    auto ffdc = getTextFFDC(dir);
+    auto ud = util::makeFFDCuserDataSection(0x2002, ffdc);
+    close(ffdc.fd);
+    ASSERT_TRUE(ud);
+    ASSERT_TRUE(ud->valid());
+    EXPECT_EQ(ud->header().id, 0x5544);
+
+    EXPECT_EQ(ud->header().version,
+              static_cast<uint8_t>(UserDataFormatVersion::text));
+    EXPECT_EQ(ud->header().subType, static_cast<uint8_t>(UserDataFormat::text));
+    EXPECT_EQ(ud->header().componentID,
+              static_cast<uint16_t>(ComponentID::phosphorLogging));
+
+    // Get the text back out
+    std::string text{ud->data().begin(), ud->data().end()};
+    EXPECT_EQ(text, "this is some text that will be used for FFDC");
+
+    fs::remove_all(dir);
+}
+
+PelFFDCfile getCustomFFDC(const fs::path& dir, const std::vector<uint8_t>& data)
+{
+    PelFFDCfile ffdc;
+    ffdc.format = UserDataFormat::custom;
+    ffdc.subType = 5;
+    ffdc.version = 42;
+
+    ffdc.fd = writeFileAndGetFD(dir, data);
+
+    return ffdc;
+}
+
+TEST_F(PELTest, MakeCustomFileUDSectionTest)
+{
+    auto dir = makeTempDir();
+
+    {
+        std::vector<uint8_t> data{1, 2, 3, 4, 5, 6, 7, 8};
+
+        auto ffdc = getCustomFFDC(dir, data);
+        auto ud = util::makeFFDCuserDataSection(0x2002, ffdc);
+        close(ffdc.fd);
+        ASSERT_TRUE(ud);
+        ASSERT_TRUE(ud->valid());
+        EXPECT_EQ(ud->header().size, 8 + 8); // data size + header size
+        EXPECT_EQ(ud->header().id, 0x5544);
+
+        EXPECT_EQ(ud->header().version, 42);
+        EXPECT_EQ(ud->header().subType, 5);
+        EXPECT_EQ(ud->header().componentID, 0x2002);
+
+        // Get the data back out
+        std::vector<uint8_t> newData{ud->data().begin(), ud->data().end()};
+        EXPECT_EQ(data, newData);
+    }
+
+    // Do the same thing again, but make it be non 4B aligned
+    // so the data gets padded.
+    {
+        std::vector<uint8_t> data{1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        auto ffdc = getCustomFFDC(dir, data);
+        auto ud = util::makeFFDCuserDataSection(0x2002, ffdc);
+        close(ffdc.fd);
+        ASSERT_TRUE(ud);
+        ASSERT_TRUE(ud->valid());
+        EXPECT_EQ(ud->header().size, 12 + 8); // data size + header size
+        EXPECT_EQ(ud->header().id, 0x5544);
+
+        EXPECT_EQ(ud->header().version, 42);
+        EXPECT_EQ(ud->header().subType, 5);
+        EXPECT_EQ(ud->header().componentID, 0x2002);
+
+        // Get the data back out
+        std::vector<uint8_t> newData{ud->data().begin(), ud->data().end()};
+
+        // pad the original to 12B so we can compare
+        data.push_back(0);
+        data.push_back(0);
+        data.push_back(0);
+
+        EXPECT_EQ(data, newData);
+    }
+
+    fs::remove_all(dir);
+}
+
+// Test Adding FFDC from files to a PEL
+TEST_F(PELTest, CreateWithFFDCTest)
+{
+    auto dir = makeTempDir();
+    message::Entry regEntry;
+    uint64_t timestamp = 5;
+
+    regEntry.name = "test";
+    regEntry.subsystem = 5;
+    regEntry.actionFlags = 0xC000;
+    regEntry.src.type = 0xBD;
+    regEntry.src.reasonCode = 0x1234;
+
+    std::vector<std::string> additionalData{"KEY1=VALUE1"};
+    AdditionalData ad{additionalData};
+    NiceMock<MockDataInterface> dataIface;
+    PelFFDC ffdc;
+
+    std::vector<uint8_t> customData{1, 2, 3, 4, 5, 6, 7, 8};
+
+    // This will be trimmed when added
+    std::vector<uint8_t> hugeCustomData(17000, 0x42);
+
+    ffdc.emplace_back(std::move(getJSONFFDC(dir)));
+    ffdc.emplace_back(std::move(getCBORFFDC(dir)));
+    ffdc.emplace_back(std::move(getTextFFDC(dir)));
+    ffdc.emplace_back(std::move(getCustomFFDC(dir, customData)));
+    ffdc.emplace_back(std::move(getCustomFFDC(dir, hugeCustomData)));
+
+    PEL pel{regEntry, 42,   timestamp, phosphor::logging::Entry::Level::Error,
+            ad,       ffdc, dataIface};
+
+    EXPECT_TRUE(pel.valid());
+
+    // Clipped to the max
+    EXPECT_EQ(pel.size(), 16384);
+
+    // Check for the FFDC sections
+    size_t udCount = 0;
+    Section* ud = nullptr;
+
+    for (const auto& section : pel.optionalSections())
+    {
+        if (section->header().id == static_cast<uint16_t>(SectionID::userData))
+        {
+            udCount++;
+            ud = section.get();
+        }
+    }
+
+    EXPECT_EQ(udCount, 7); // AD section, sysInfo, 5 ffdc sections
+
+    // Check the last section was trimmed to
+    // something a bit less that 17000.
+    EXPECT_GT(ud->header().size, 14000);
+    EXPECT_LT(ud->header().size, 16000);
+
+    fs::remove_all(dir);
+}