PEL: SRC FRU callout structure

This class represents a single FRU callout in the SRC section of a PEL.
When there are multiple callouts, there will be multiple instances of
this class created.  Technically, the callout isn't always a FRU, such
as it could be a maintenance procedure name, but the spec still refers
to this section as the FRU callout section.

The callout priority and location code are in this structure.

There can also be up to one each of three types of substructures
in a single callout:
* FRU Identity  (must be first if present)
* Power Controlling Enclosure (PCE)
* Manufacturing Replaceable Unit (MRU)

This commit just provides support for creating this object from a
flattened PEL, such as one that comes down from the host.  A future
commit will add support for creating a callout for BMC created event
logs.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I49535285e3cbaa15dfe031648cfaf262380a1cf7
diff --git a/extensions/openpower-pels/callout.cpp b/extensions/openpower-pels/callout.cpp
new file mode 100644
index 0000000..53fe85d
--- /dev/null
+++ b/extensions/openpower-pels/callout.cpp
@@ -0,0 +1,102 @@
+#include "callout.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+namespace openpower
+{
+namespace pels
+{
+namespace src
+{
+
+using namespace phosphor::logging;
+
+Callout::Callout(Stream& pel)
+{
+    pel >> _size >> _flags >> _priority >> _locationCodeSize;
+
+    if (_locationCodeSize)
+    {
+        _locationCode.resize(_locationCodeSize);
+        pel >> _locationCode;
+    }
+
+    size_t currentSize = 4 + _locationCodeSize;
+
+    // Read in the substructures until the end of this structure.
+    // Any stream overflows will throw an exception up to the SRC constructor
+    while (_size > currentSize)
+    {
+        // Peek the type
+        uint16_t type = 0;
+        pel >> type;
+        pel.offset(pel.offset() - 2);
+
+        switch (type)
+        {
+            case FRUIdentity::substructureType:
+            {
+                _fruIdentity = std::make_unique<FRUIdentity>(pel);
+                currentSize += _fruIdentity->flattenedSize();
+                break;
+            }
+            case PCEIdentity::substructureType:
+            {
+                _pceIdentity = std::make_unique<PCEIdentity>(pel);
+                currentSize += _pceIdentity->flattenedSize();
+                break;
+            }
+            case MRU::substructureType:
+            {
+                _mru = std::make_unique<MRU>(pel);
+                currentSize += _mru->flattenedSize();
+                break;
+            }
+            default:
+                log<level::ERR>("Invalid Callout subsection type",
+                                entry("CALLOUT_TYPE=0x%X", type));
+                throw std::runtime_error("Invalid Callout subsection type");
+                break;
+        }
+    }
+}
+
+size_t Callout::flattenedSize()
+{
+    size_t size = sizeof(_size) + sizeof(_flags) + sizeof(_priority) +
+                  sizeof(_locationCodeSize) + _locationCodeSize;
+
+    size += _fruIdentity ? _fruIdentity->flattenedSize() : 0;
+    size += _pceIdentity ? _pceIdentity->flattenedSize() : 0;
+    size += _mru ? _mru->flattenedSize() : 0;
+
+    return size;
+}
+
+void Callout::flatten(Stream& pel)
+{
+    pel << _size << _flags << _priority << _locationCodeSize;
+
+    if (_locationCodeSize)
+    {
+        pel << _locationCode;
+    }
+
+    if (_fruIdentity)
+    {
+        _fruIdentity->flatten(pel);
+    }
+
+    if (_pceIdentity)
+    {
+        _pceIdentity->flatten(pel);
+    }
+    if (_mru)
+    {
+        _mru->flatten(pel);
+    }
+}
+
+} // namespace src
+} // namespace pels
+} // namespace openpower
diff --git a/extensions/openpower-pels/callout.hpp b/extensions/openpower-pels/callout.hpp
new file mode 100644
index 0000000..ea28b7b
--- /dev/null
+++ b/extensions/openpower-pels/callout.hpp
@@ -0,0 +1,173 @@
+#pragma once
+
+#include "fru_identity.hpp"
+#include "mru.hpp"
+#include "pce_identity.hpp"
+#include "stream.hpp"
+
+namespace openpower
+{
+namespace pels
+{
+namespace src
+{
+
+/**
+ * @class Callout
+ *
+ * Represents a single FRU callout in the SRC's FRU callout
+ * subsection.
+ *
+ * The 'Callouts' class holds a list of these objects.
+ *
+ * The callout priority and location code are in this structure.
+ *
+ * There can also be up to one each of three types of substructures
+ * in a single callout:
+ *  * FRU Identity  (must be first if present)
+ *  * Power Controlling Enclosure (PCE)
+ *  * Manufacturing Replaceable Unit (MRU)
+ *
+ * These substructures have their own objects managed by unique_ptrs
+ * which will only be allocated if those substructures exist.
+ */
+class Callout
+{
+  public:
+    Callout() = delete;
+    ~Callout() = default;
+    Callout(const Callout&) = delete;
+    Callout& operator=(const Callout&) = delete;
+    Callout(Callout&&) = delete;
+    Callout& operator=(Callout&&) = delete;
+
+    /**
+     * @brief Constructor
+     *
+     * Fills in this class's data fields from the stream.
+     *
+     * @param[in] pel - the PEL data stream
+     */
+    explicit Callout(Stream& pel);
+
+    /**
+     * @brief Returns the size of this object when flattened into a PEL
+     *
+     * @return size_t - The size of the section
+     */
+    size_t flattenedSize();
+
+    /**
+     * @brief Flatten the object into the stream
+     *
+     * @param[in] stream - The stream to write to
+     */
+    void flatten(Stream& pel);
+
+    /**
+     * @brief Returns the priority field of a callout
+     *
+     * @return uint8_t - The priority
+     */
+    uint8_t priority() const
+    {
+        return _priority;
+    }
+
+    /**
+     * @brief Returns the location code of the callout
+     *
+     * @return std::string - The location code
+     */
+    std::string locationCode() const
+    {
+        std::string lc;
+        if (!_locationCode.empty())
+        {
+            // NULL terminated
+            lc = static_cast<const char*>(_locationCode.data());
+        }
+        return lc;
+    }
+
+    /**
+     * @brief Returns the FRU identity substructure
+     *
+     * @return const std::unique_ptr<FRUIdentity>&
+     */
+    const std::unique_ptr<FRUIdentity>& fruIdentity() const
+    {
+        return _fruIdentity;
+    }
+
+    /**
+     * @brief Returns the PCE identity substructure
+     *
+     * @return const std::unique_ptr<PCEIdentity>&
+     */
+    const std::unique_ptr<PCEIdentity>& pceIdentity() const
+    {
+        return _pceIdentity;
+    }
+
+    /**
+     * @brief Returns the MRU identity substructure
+     *
+     * @return const std::unique_ptr<FRUIdentity>&
+     */
+    const std::unique_ptr<MRU>& mru() const
+    {
+        return _mru;
+    }
+
+  private:
+    /**
+     * @brief The size of this structure in the PEL
+     */
+    uint8_t _size;
+
+    /**
+     * @brief The flags byte of this structure
+     */
+    uint8_t _flags;
+
+    /**
+     * @brief The replacement priority
+     */
+    uint8_t _priority;
+
+    /**
+     * @brief The length of the location code field.
+     *
+     * Includes the NULL termination, and must be a
+     * multiple of 4 (padded with zeros)
+     */
+    uint8_t _locationCodeSize;
+
+    /**
+     * @brief NULL terminated location code
+     *
+     * Includes the NULL termination, and must be a
+     * multiple of 4 (padded with zeros)
+     */
+    std::vector<char> _locationCode;
+
+    /**
+     * @brief FRU (Field Replaceable Unit) Identity substructure
+     */
+    std::unique_ptr<FRUIdentity> _fruIdentity;
+
+    /**
+     * @brief PCE (Power Controlling Enclosure) Identity substructure
+     */
+    std::unique_ptr<PCEIdentity> _pceIdentity;
+
+    /**
+     * @brief MRU (Manufacturing Replaceable Unit) substructure
+     */
+    std::unique_ptr<MRU> _mru;
+};
+
+} // namespace src
+} // namespace pels
+} // namespace openpower
diff --git a/extensions/openpower-pels/openpower-pels.mk b/extensions/openpower-pels/openpower-pels.mk
index fdc998d..9308159 100644
--- a/extensions/openpower-pels/openpower-pels.mk
+++ b/extensions/openpower-pels/openpower-pels.mk
@@ -1,6 +1,7 @@
 phosphor_log_manager_SOURCES += \
 	extensions/openpower-pels/ascii_string.cpp \
 	extensions/openpower-pels/bcd_time.cpp \
+	extensions/openpower-pels/callout.cpp \
 	extensions/openpower-pels/data_interface.cpp \
 	extensions/openpower-pels/entry_points.cpp \
 	extensions/openpower-pels/failing_mtms.cpp \
diff --git a/test/openpower-pels/Makefile.include b/test/openpower-pels/Makefile.include
index e54bce8..ccd5d26 100644
--- a/test/openpower-pels/Makefile.include
+++ b/test/openpower-pels/Makefile.include
@@ -20,6 +20,7 @@
 	repository_test \
 	section_header_test \
 	severity_test \
+	src_callout_test \
 	stream_test \
 	user_data_test \
 	user_header_test
@@ -227,3 +228,17 @@
 	$(test_ldadd) \
 	$(top_builddir)/extensions/openpower-pels/mru.o
 mru_test_LDFLAGS = $(test_ldflags)
+
+src_callout_test_SOURCES = \
+	%reldir%/src_callout_test.cpp \
+	%reldir%/pel_utils.cpp
+src_callout_test_CPPFLAGS = $(test_cppflags)
+src_callout_test_CXXFLAGS = $(test_cxxflags)
+src_callout_test_LDADD = \
+	$(test_ldadd) \
+	$(top_builddir)/extensions/openpower-pels/callout.o \
+	$(top_builddir)/extensions/openpower-pels/fru_identity.o \
+	$(top_builddir)/extensions/openpower-pels/mru.o \
+	$(top_builddir)/extensions/openpower-pels/mtms.o \
+	$(top_builddir)/extensions/openpower-pels/pce_identity.o
+src_callout_test_LDFLAGS = $(test_ldflags)
diff --git a/test/openpower-pels/pel_utils.cpp b/test/openpower-pels/pel_utils.cpp
index 3e0ac01..3610dd7 100644
--- a/test/openpower-pels/pel_utils.cpp
+++ b/test/openpower-pels/pel_utils.cpp
@@ -51,6 +51,33 @@
     // Add more as the code supports more
 };
 
+std::vector<uint8_t> srcFRUIdentityCallout{
+    'I', 'D', 0x1C, 0x1D,                     // type, size, flags
+    '1', '2', '3',  '4',                      // PN
+    '5', '6', '7',  0x00, 'A', 'A', 'A', 'A', // CCIN
+    '1', '2', '3',  '4',  '5', '6', '7', '8', // SN
+    '9', 'A', 'B',  'C'};
+
+std::vector<uint8_t> srcPCEIdentityCallout{
+    'P', 'E', 0x24, 0x00,                      // type, size, flags
+    'T', 'T', 'T',  'T',  '-', 'M', 'M',  'M', // MTM
+    '1', '2', '3',  '4',  '5', '6', '7',       // SN
+    '8', '9', 'A',  'B',  'C', 'P', 'C',  'E', // Name + null padded
+    'N', 'A', 'M',  'E',  '1', '2', 0x00, 0x00, 0x00};
+
+std::vector<uint8_t> srcMRUCallout{
+    'M',  'R',  0x28, 0x04, // ID, size, flags
+    0x00, 0x00, 0x00, 0x00, // Reserved
+    0x00, 0x00, 0x00, 'H',  // priority 0
+    0x01, 0x01, 0x01, 0x01, // MRU ID 0
+    0x00, 0x00, 0x00, 'M',  // priority 1
+    0x02, 0x02, 0x02, 0x02, // MRU ID 1
+    0x00, 0x00, 0x00, 'L',  // priority 2
+    0x03, 0x03, 0x03, 0x03, // MRU ID 2
+    0x00, 0x00, 0x00, 'H',  // priority 3
+    0x04, 0x04, 0x04, 0x04, // MRU ID 3
+};
+
 std::unique_ptr<std::vector<uint8_t>> pelDataFactory(TestPelType type)
 {
     std::unique_ptr<std::vector<uint8_t>> data;
@@ -74,6 +101,22 @@
     return data;
 }
 
+std::vector<uint8_t> srcDataFactory(TestSRCType type)
+{
+    switch (type)
+    {
+        case TestSRCType::fruIdentityStructure:
+            return srcFRUIdentityCallout;
+
+        case TestSRCType::pceIdentityStructure:
+            return srcPCEIdentityCallout;
+
+        case TestSRCType::mruStructure:
+            return srcMRUCallout;
+    }
+    return {};
+}
+
 std::unique_ptr<std::vector<uint8_t>> readPELFile(const fs::path& path)
 {
     std::ifstream file{path};
diff --git a/test/openpower-pels/pel_utils.hpp b/test/openpower-pels/pel_utils.hpp
index e61bea2..2d40033 100644
--- a/test/openpower-pels/pel_utils.hpp
+++ b/test/openpower-pels/pel_utils.hpp
@@ -60,6 +60,16 @@
 };
 
 /**
+ * @brief Tells the SRC factory which data to create
+ */
+enum class TestSRCType
+{
+    fruIdentityStructure,
+    pceIdentityStructure,
+    mruStructure,
+};
+
+/**
  * @brief PEL data factory, for testing
  *
  * @param[in] type - the type of data to create
@@ -69,6 +79,17 @@
 std::unique_ptr<std::vector<uint8_t>> pelDataFactory(TestPelType type);
 
 /**
+ * @brief SRC data factory, for testing
+ *
+ * Provides pieces of the SRC PEL section, such as a callout.
+ *
+ * @param[in] type - the type of data to create
+ *
+ * @return std::vector<uint8_t> - The SRC data
+ */
+std::vector<uint8_t> srcDataFactory(TestSRCType type);
+
+/**
  * @brief Helper function to read raw PEL data from a file
  *
  * @param[in] path - the path to read
diff --git a/test/openpower-pels/src_callout_test.cpp b/test/openpower-pels/src_callout_test.cpp
new file mode 100644
index 0000000..0a78c94
--- /dev/null
+++ b/test/openpower-pels/src_callout_test.cpp
@@ -0,0 +1,146 @@
+#include "extensions/openpower-pels/callout.hpp"
+#include "pel_utils.hpp"
+
+#include <gtest/gtest.h>
+
+using namespace openpower::pels;
+using namespace openpower::pels::src;
+
+// Unflatten the callout section with all three substructures
+TEST(CalloutTest, TestUnflattenAllSubstructures)
+{
+    // The base data.
+    std::vector<uint8_t> data{
+        0xFF, 0x2F, 'H', 8, // size, flags, priority, LC length
+        'U',  '1',  '2', '-', 'P', '1', 0x00, 0x00 // LC
+    };
+
+    auto fruIdentity = srcDataFactory(TestSRCType::fruIdentityStructure);
+    auto pceIdentity = srcDataFactory(TestSRCType::pceIdentityStructure);
+    auto mrus = srcDataFactory(TestSRCType::mruStructure);
+
+    // Add all 3 substructures
+    data.insert(data.end(), fruIdentity.begin(), fruIdentity.end());
+    data.insert(data.end(), pceIdentity.begin(), pceIdentity.end());
+    data.insert(data.end(), mrus.begin(), mrus.end());
+
+    // The final size
+    data[0] = data.size();
+
+    Stream stream{data};
+    Callout callout{stream};
+
+    EXPECT_EQ(callout.flattenedSize(), data.size());
+    EXPECT_EQ(callout.priority(), 'H');
+    EXPECT_EQ(callout.locationCode(), "U12-P1");
+
+    // Spot check the 3 substructures
+    EXPECT_TRUE(callout.fruIdentity());
+    EXPECT_EQ(callout.fruIdentity()->getSN(), "123456789ABC");
+
+    EXPECT_TRUE(callout.pceIdentity());
+    EXPECT_EQ(callout.pceIdentity()->enclosureName(), "PCENAME12");
+
+    EXPECT_TRUE(callout.mru());
+    EXPECT_EQ(callout.mru()->mrus().size(), 4);
+    EXPECT_EQ(callout.mru()->mrus().at(3).id, 0x04040404);
+
+    // Now flatten
+    std::vector<uint8_t> newData;
+    Stream newStream{newData};
+
+    callout.flatten(newStream);
+    EXPECT_EQ(data, newData);
+}
+
+TEST(CalloutTest, TestUnflattenOneSubstructure)
+{
+    std::vector<uint8_t> data{
+        0xFF, 0x28, 'H', 0x08, // size, flags, priority, LC length
+        'U',  '1',  '2', '-',  'P', '1', 0x00, 0x00 // LC
+    };
+
+    auto fruIdentity = srcDataFactory(TestSRCType::fruIdentityStructure);
+
+    data.insert(data.end(), fruIdentity.begin(), fruIdentity.end());
+
+    // The final size
+    data[0] = data.size();
+
+    Stream stream{data};
+    Callout callout{stream};
+
+    EXPECT_EQ(callout.flattenedSize(), data.size());
+
+    // Spot check the substructure
+    EXPECT_TRUE(callout.fruIdentity());
+    EXPECT_EQ(callout.fruIdentity()->getSN(), "123456789ABC");
+
+    // Not present
+    EXPECT_FALSE(callout.pceIdentity());
+    EXPECT_FALSE(callout.mru());
+
+    // Now flatten
+    std::vector<uint8_t> newData;
+    Stream newStream{newData};
+
+    callout.flatten(newStream);
+    EXPECT_EQ(data, newData);
+}
+
+TEST(CalloutTest, TestUnflattenTwoSubstructures)
+{
+    std::vector<uint8_t> data{
+        0xFF, 0x2B, 'H', 0x08, // size, flags, priority, LC length
+        'U',  '1',  '2', '-',  'P', '1', 0x00, 0x00 // LC
+    };
+
+    auto fruIdentity = srcDataFactory(TestSRCType::fruIdentityStructure);
+    auto pceIdentity = srcDataFactory(TestSRCType::pceIdentityStructure);
+
+    data.insert(data.end(), fruIdentity.begin(), fruIdentity.end());
+    data.insert(data.end(), pceIdentity.begin(), pceIdentity.end());
+
+    // The final size
+    data[0] = data.size();
+
+    Stream stream{data};
+    Callout callout{stream};
+
+    EXPECT_EQ(callout.flattenedSize(), data.size());
+
+    // Spot check the 2 substructures
+    EXPECT_TRUE(callout.fruIdentity());
+    EXPECT_EQ(callout.fruIdentity()->getSN(), "123456789ABC");
+
+    EXPECT_TRUE(callout.pceIdentity());
+    EXPECT_EQ(callout.pceIdentity()->enclosureName(), "PCENAME12");
+
+    // Not present
+    EXPECT_FALSE(callout.mru());
+
+    // Now flatten
+    std::vector<uint8_t> newData;
+    Stream newStream{newData};
+
+    callout.flatten(newStream);
+    EXPECT_EQ(data, newData);
+}
+
+TEST(CalloutTest, TestNoLocationCode)
+{
+    std::vector<uint8_t> data{
+        0xFF, 0x2B, 'H', 0x00 // size, flags, priority, LC length
+    };
+
+    auto fruIdentity = srcDataFactory(TestSRCType::fruIdentityStructure);
+    data.insert(data.end(), fruIdentity.begin(), fruIdentity.end());
+
+    // The final size
+    data[0] = data.size();
+
+    Stream stream{data};
+    Callout callout{stream};
+
+    EXPECT_TRUE(callout.locationCode().empty());
+}