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/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);
+}