Add a IPMI blob handler for SMBIOS tables

smbios-mdr has dependencies on intel-ipmi-oem which makes BIOS sending
SMBIOS tables to BMC through VGA shared memory. For platforms without
intel-ipmi-oem, implement a IPMI blob hanler so that BIOS can send
SMBIOS tables through IPMI blob interfaces.

Test:
Unit tests for the IPMI blob handler.
Manual test that transfers SMBIOS tables to BMC through IPMI blob
interfaces on a platform host.

Signed-off-by: Jie Yang <jjy@google.com>
Change-Id: I9bc1ae7e9bfaa793e47e38fa19049f0f69355189
diff --git a/src/smbios-ipmi-blobs/handler.cpp b/src/smbios-ipmi-blobs/handler.cpp
new file mode 100644
index 0000000..51d9d95
--- /dev/null
+++ b/src/smbios-ipmi-blobs/handler.cpp
@@ -0,0 +1,280 @@
+#include "handler.hpp"
+
+#include "mdrv2.hpp"
+#include "smbios_mdrv2.hpp"
+
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <ipmid/api.hpp>
+#include <phosphor-logging/log.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <ctime>
+#include <fstream>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace blobs
+{
+
+namespace internal
+{
+
+constexpr const char* mdrV2Service = "xyz.openbmc_project.Smbios.MDR_V2";
+constexpr const char* mdrV2Interface = "xyz.openbmc_project.Smbios.MDR_V2";
+
+bool syncSmbiosData()
+{
+    bool status = false;
+    sdbusplus::bus::bus bus =
+        sdbusplus::bus::bus(ipmid_get_sd_bus_connection());
+    sdbusplus::message::message method =
+        bus.new_method_call(mdrV2Service, phosphor::smbios::mdrV2Path,
+                            mdrV2Interface, "AgentSynchronizeData");
+
+    try
+    {
+        sdbusplus::message::message reply = bus.call(method);
+        reply.read(status);
+    }
+    catch (sdbusplus::exception_t& e)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Error Sync data with service",
+            phosphor::logging::entry("ERROR=%s", e.what()),
+            phosphor::logging::entry("SERVICE=%s", mdrV2Service),
+            phosphor::logging::entry("PATH=%s", phosphor::smbios::mdrV2Path));
+        return false;
+    }
+
+    if (!status)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Sync data with service failure");
+        return false;
+    }
+
+    return true;
+}
+
+} // namespace internal
+
+bool SmbiosBlobHandler::canHandleBlob(const std::string& path)
+{
+    return path == blobId;
+}
+
+std::vector<std::string> SmbiosBlobHandler::getBlobIds()
+{
+    return std::vector<std::string>(1, blobId);
+}
+
+bool SmbiosBlobHandler::deleteBlob(const std::string& path)
+{
+    return false;
+}
+
+bool SmbiosBlobHandler::stat(const std::string& path, struct BlobMeta* meta)
+{
+    if (!blobPtr || blobPtr->blobId != path)
+    {
+        return false;
+    }
+
+    meta->size = blobPtr->buffer.size();
+    meta->blobState = blobPtr->state;
+    return true;
+}
+
+bool SmbiosBlobHandler::open(uint16_t session, uint16_t flags,
+                             const std::string& path)
+{
+    if (flags & blobs::OpenFlags::read)
+    {
+        /* Disable the read operation. */
+        return false;
+    }
+
+    /* The handler only allows one session. If an open blob exists, return
+     * false directly.
+     */
+    if (blobPtr)
+    {
+        return false;
+    }
+    blobPtr = std::make_unique<SmbiosBlob>(session, path, flags);
+    return true;
+}
+
+std::vector<uint8_t> SmbiosBlobHandler::read(uint16_t session, uint32_t offset,
+                                             uint32_t requestedSize)
+{
+    /* SMBIOS blob handler does not support read. */
+    return std::vector<uint8_t>();
+}
+
+bool SmbiosBlobHandler::write(uint16_t session, uint32_t offset,
+                              const std::vector<uint8_t>& data)
+{
+    if (!blobPtr || blobPtr->sessionId != session)
+    {
+        return false;
+    }
+
+    if (!(blobPtr->state & blobs::StateFlags::open_write))
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "No open blob to write");
+        return false;
+    }
+
+    /* Is the offset beyond the array? */
+    if (offset >= maxBufferSize)
+    {
+        return false;
+    }
+
+    /* Determine whether all their bytes will fit. */
+    uint32_t remain = maxBufferSize - offset;
+    if (data.size() > remain)
+    {
+        return false;
+    }
+
+    /* Resize the buffer if what we're writing will go over the size */
+    uint32_t newBufferSize = data.size() + offset;
+    if (newBufferSize > blobPtr->buffer.size())
+    {
+        blobPtr->buffer.resize(newBufferSize);
+    }
+
+    std::memcpy(blobPtr->buffer.data() + offset, data.data(), data.size());
+    return true;
+}
+
+bool SmbiosBlobHandler::writeMeta(uint16_t session, uint32_t offset,
+                                  const std::vector<uint8_t>& data)
+{
+    return false;
+}
+
+bool SmbiosBlobHandler::commit(uint16_t session,
+                               const std::vector<uint8_t>& data)
+{
+    if (!data.empty())
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Unexpected data provided to commit call");
+        return false;
+    }
+
+    if (!blobPtr || blobPtr->sessionId != session)
+    {
+        return false;
+    }
+
+    /* If a blob is committing or commited, return true directly. But if last
+     * commit fails, may try to commit again.
+     */
+    if (blobPtr->state &
+        (blobs::StateFlags::committing | blobs::StateFlags::committed))
+    {
+        return true;
+    }
+
+    /* Clear the commit_error bit. */
+    blobPtr->state &= ~blobs::StateFlags::commit_error;
+
+    MDRSMBIOSHeader mdrHdr;
+    mdrHdr.mdrType = mdrTypeII;
+    mdrHdr.timestamp = std::time(nullptr);
+    mdrHdr.dataSize = blobPtr->buffer.size();
+    if (access(smbiosPath, F_OK) == -1)
+    {
+        int flag = mkdir(smbiosPath, S_IRWXU);
+        if (flag != 0)
+        {
+            phosphor::logging::log<phosphor::logging::level::ERR>(
+                "create folder failed for writting smbios file");
+            blobPtr->state |= blobs::StateFlags::commit_error;
+            return false;
+        }
+    }
+
+    std::ofstream smbiosFile(mdrType2File,
+                             std::ios_base::binary | std::ios_base::trunc);
+    if (!smbiosFile.good())
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Write data from flash error - Open SMBIOS table file failure");
+        blobPtr->state |= blobs::StateFlags::commit_error;
+        return false;
+    }
+
+    smbiosFile.exceptions(std::ofstream::badbit | std::ofstream::failbit);
+    try
+    {
+        smbiosFile.write(reinterpret_cast<char*>(&mdrHdr),
+                         sizeof(MDRSMBIOSHeader));
+        smbiosFile.write(reinterpret_cast<char*>(blobPtr->buffer.data()),
+                         mdrHdr.dataSize);
+        blobPtr->state |= blobs::StateFlags::committing;
+    }
+    catch (std::ofstream::failure& e)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Write data from flash error - write data error",
+            phosphor::logging::entry("ERROR=%s", e.what()));
+        blobPtr->state |= blobs::StateFlags::commit_error;
+        return false;
+    }
+
+    if (!internal::syncSmbiosData())
+    {
+        blobPtr->state &= ~blobs::StateFlags::committing;
+        blobPtr->state |= blobs::StateFlags::commit_error;
+        return false;
+    }
+
+    // Unset committing state and set committed state
+    blobPtr->state &= ~blobs::StateFlags::committing;
+    blobPtr->state |= blobs::StateFlags::committed;
+
+    return true;
+}
+
+bool SmbiosBlobHandler::close(uint16_t session)
+{
+    if (!blobPtr || blobPtr->sessionId != session)
+    {
+        return false;
+    }
+
+    blobPtr = nullptr;
+    return true;
+}
+
+bool SmbiosBlobHandler::stat(uint16_t session, struct BlobMeta* meta)
+{
+    if (!blobPtr || blobPtr->sessionId != session)
+    {
+        return false;
+    }
+
+    meta->size = blobPtr->buffer.size();
+    meta->blobState = blobPtr->state;
+    return true;
+}
+
+bool SmbiosBlobHandler::expire(uint16_t session)
+{
+    return close(session);
+}
+
+} // namespace blobs
diff --git a/src/smbios-ipmi-blobs/handler.hpp b/src/smbios-ipmi-blobs/handler.hpp
new file mode 100644
index 0000000..d97618a
--- /dev/null
+++ b/src/smbios-ipmi-blobs/handler.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace blobs
+{
+
+class SmbiosBlobHandler : public GenericBlobInterface
+{
+  public:
+    SmbiosBlobHandler() = default;
+    ~SmbiosBlobHandler() = default;
+    SmbiosBlobHandler(const SmbiosBlobHandler&) = delete;
+    SmbiosBlobHandler& operator=(const SmbiosBlobHandler&) = delete;
+    SmbiosBlobHandler(SmbiosBlobHandler&&) = default;
+    SmbiosBlobHandler& operator=(SmbiosBlobHandler&&) = default;
+
+    struct SmbiosBlob
+    {
+        SmbiosBlob(uint16_t id, const std::string& path, uint16_t flags) :
+            sessionId(id), blobId(path), state(0)
+        {
+            if (flags & blobs::OpenFlags::write)
+            {
+                state |= blobs::StateFlags::open_write;
+            }
+
+            /* Pre-allocate the buffer.capacity() with maxBufferSize */
+            buffer.reserve(maxBufferSize);
+        }
+
+        /* The blob handler session id. */
+        uint16_t sessionId;
+
+        /* The identifier for the blob */
+        std::string blobId;
+
+        /* The current state. */
+        uint16_t state;
+
+        /* The staging buffer. */
+        std::vector<uint8_t> buffer;
+    };
+
+    bool canHandleBlob(const std::string& path) override;
+    std::vector<std::string> getBlobIds() override;
+    bool deleteBlob(const std::string& path) override;
+    bool stat(const std::string& path, struct BlobMeta* meta) override;
+    bool open(uint16_t session, uint16_t flags,
+              const std::string& path) override;
+    std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+                              uint32_t requestedSize) override;
+    bool write(uint16_t session, uint32_t offset,
+               const std::vector<uint8_t>& data) override;
+    bool writeMeta(uint16_t session, uint32_t offset,
+                   const std::vector<uint8_t>& data) override;
+    bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+    bool close(uint16_t session) override;
+    bool stat(uint16_t session, struct BlobMeta* meta) override;
+    bool expire(uint16_t session) override;
+
+  private:
+    static constexpr char blobId[] = "/smbios";
+
+    /* SMBIOS table storage size */
+    static constexpr uint32_t maxBufferSize = 64 * 1024;
+
+    /* The handler only allows one open blob. */
+    std::unique_ptr<SmbiosBlob> blobPtr = nullptr;
+};
+
+} // namespace blobs
diff --git a/src/smbios-ipmi-blobs/main.cpp b/src/smbios-ipmi-blobs/main.cpp
new file mode 100644
index 0000000..5f031dd
--- /dev/null
+++ b/src/smbios-ipmi-blobs/main.cpp
@@ -0,0 +1,10 @@
+#include "handler.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+    return std::make_unique<blobs::SmbiosBlobHandler>();
+}
diff --git a/src/smbios-ipmi-blobs/test/handler_open_unittest.cpp b/src/smbios-ipmi-blobs/test/handler_open_unittest.cpp
new file mode 100644
index 0000000..22223cc
--- /dev/null
+++ b/src/smbios-ipmi-blobs/test/handler_open_unittest.cpp
@@ -0,0 +1,32 @@
+#include "handler_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+
+namespace blobs
+{
+
+class SmbiosBlobHandlerOpenTest : public SmbiosBlobHandlerTest
+{};
+
+TEST_F(SmbiosBlobHandlerOpenTest, OpenWithBadFlagsFails)
+{
+    // SMBIOS blob handler disables read flag
+
+    EXPECT_FALSE(handler.open(session, blobs::OpenFlags::read, expectedBlobId));
+}
+
+TEST_F(SmbiosBlobHandlerOpenTest, OpenEverythingSucceeds)
+{
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+}
+
+TEST_F(SmbiosBlobHandlerOpenTest, CannotOpenSameSessionTwice)
+{
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_FALSE(
+        handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+}
+
+} // namespace blobs
diff --git a/src/smbios-ipmi-blobs/test/handler_readwrite_unittest.cpp b/src/smbios-ipmi-blobs/test/handler_readwrite_unittest.cpp
new file mode 100644
index 0000000..5cd3abc
--- /dev/null
+++ b/src/smbios-ipmi-blobs/test/handler_readwrite_unittest.cpp
@@ -0,0 +1,102 @@
+#include "handler_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using ::testing::IsEmpty;
+
+namespace blobs
+{
+
+class SmbiosBlobHandlerReadWriteTest : public SmbiosBlobHandlerTest
+{};
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, InvalidSessionWriteIsRejected)
+{
+    // Verify the handler checks for a valid session.
+
+    std::vector<uint8_t> data = {0x1, 0x2};
+    EXPECT_FALSE(handler.write(session, 0, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, NoWriteFlagRejected)
+{
+    // Verify the handler checks the write flag;
+
+    EXPECT_TRUE(handler.open(session, 0, expectedBlobId));
+
+    std::vector<uint8_t> data = {0x1, 0x2};
+    EXPECT_FALSE(handler.write(session, 0, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, WritingTooMuchByOneByteFails)
+{
+    int bytes = handlerMaxBufferSize + 1;
+    std::vector<uint8_t> data(bytes, 0x11);
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_FALSE(handler.write(session, 0, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, WritingTooMuchByOffsetOfOne)
+{
+    std::vector<uint8_t> data(handlerMaxBufferSize, 0x11);
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_FALSE(handler.write(session, 1, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+    std::vector<uint8_t> data = {0x01, 0x02};
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_FALSE(handler.write(session, handlerMaxBufferSize - 1, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+    std::vector<uint8_t> data = {0x01};
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_FALSE(handler.write(session, handlerMaxBufferSize, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+    std::vector<uint8_t> data(handlerMaxBufferSize, 0x01);
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_TRUE(handler.write(session, 0, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+    std::vector<uint8_t> data = {0x01};
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_TRUE(handler.write(session, handlerMaxBufferSize - 1, data));
+}
+
+TEST_F(SmbiosBlobHandlerReadWriteTest, ReadAlwaysReturnsEmpty)
+{
+    const uint32_t testOffset = 0;
+    const std::vector<uint8_t> testData = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_TRUE(handler.write(session, testOffset, testData));
+
+    EXPECT_THAT(handler.read(session, testOffset, testData.size()), IsEmpty());
+
+    for (size_t i = 0; i < testData.size(); ++i)
+    {
+        EXPECT_THAT(handler.read(session, i, 1), IsEmpty());
+    }
+}
+
+} // namespace blobs
diff --git a/src/smbios-ipmi-blobs/test/handler_statclose_unittest.cpp b/src/smbios-ipmi-blobs/test/handler_statclose_unittest.cpp
new file mode 100644
index 0000000..b8d32cd
--- /dev/null
+++ b/src/smbios-ipmi-blobs/test/handler_statclose_unittest.cpp
@@ -0,0 +1,78 @@
+#include "handler_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace blobs
+{
+
+class SmbiosBlobHandlerStatCloseTest : public SmbiosBlobHandlerTest
+{
+  protected:
+    blobs::BlobMeta meta;
+
+    // Initialize expected_meta_ with empty members
+    blobs::BlobMeta expected_meta_session = {};
+    blobs::BlobMeta expected_meta_path = {};
+};
+
+TEST_F(SmbiosBlobHandlerStatCloseTest, InvalidSessionStatIsRejected)
+{
+    EXPECT_FALSE(handler.stat(session, &meta));
+}
+
+TEST_F(SmbiosBlobHandlerStatCloseTest, SessionStatAlwaysInitialReadAndWrite)
+{
+    // Verify the session stat returns the information for a session.
+
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+
+    EXPECT_TRUE(handler.stat(session, &meta));
+    expected_meta_session.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta, expected_meta_session);
+
+    EXPECT_TRUE(handler.stat(expectedBlobId, &meta));
+    expected_meta_path.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta, expected_meta_path);
+}
+
+TEST_F(SmbiosBlobHandlerStatCloseTest, AfterWriteMetadataLengthMatches)
+{
+    // Verify that after writes, the length returned matches.
+
+    std::vector<uint8_t> data = {0x01};
+    EXPECT_TRUE(handler.open(session, blobs::OpenFlags::write, expectedBlobId));
+    EXPECT_TRUE(handler.write(session, handlerMaxBufferSize - 1, data));
+
+    // We wrote one byte to the last index, making the length the buffer size.
+    EXPECT_TRUE(handler.stat(session, &meta));
+    expected_meta_session.size = handlerMaxBufferSize;
+    expected_meta_session.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta, expected_meta_session);
+
+    EXPECT_TRUE(handler.stat(expectedBlobId, &meta));
+    expected_meta_path.size = handlerMaxBufferSize;
+    expected_meta_path.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta, expected_meta_path);
+}
+
+TEST_F(SmbiosBlobHandlerStatCloseTest, CloseWithInvalidSessionFails)
+{
+    // Verify you cannot close an invalid session.
+
+    EXPECT_FALSE(handler.close(session));
+}
+
+TEST_F(SmbiosBlobHandlerStatCloseTest, CloseWithValidSessionSuccess)
+{
+    // Verify you can close a valid session.
+
+    EXPECT_TRUE(handler.open(session, 0, expectedBlobId));
+
+    EXPECT_TRUE(handler.close(session));
+}
+} // namespace blobs
diff --git a/src/smbios-ipmi-blobs/test/handler_unittest.cpp b/src/smbios-ipmi-blobs/test/handler_unittest.cpp
new file mode 100644
index 0000000..9395e23
--- /dev/null
+++ b/src/smbios-ipmi-blobs/test/handler_unittest.cpp
@@ -0,0 +1,38 @@
+#include "handler_unittest.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace blobs
+{
+
+class SmbiosBlobHandlerBasicTest : public SmbiosBlobHandlerTest
+{};
+
+TEST_F(SmbiosBlobHandlerBasicTest, CanHandleBlobChecksNameInvalid)
+{
+    // Verify canHandleBlob checks and returns false on an invalid name.
+
+    EXPECT_FALSE(handler.canHandleBlob("asdf"));
+    EXPECT_FALSE(handler.canHandleBlob("smbios"));
+    EXPECT_FALSE(handler.canHandleBlob("/smbios0"));
+    EXPECT_FALSE(handler.canHandleBlob("/smbios/0"));
+}
+
+TEST_F(SmbiosBlobHandlerBasicTest, CanHandleBlobChecksNameVaild)
+{
+    // Verify canHandleBlob checks and returns true on the valid name.
+
+    EXPECT_TRUE(handler.canHandleBlob(expectedBlobId));
+}
+
+TEST_F(SmbiosBlobHandlerBasicTest, GetblobIdsAsExpected)
+{
+    // Verify getBlobIds returns the expected blob list.
+
+    EXPECT_EQ(handler.getBlobIds(), expectedBlobIdList);
+}
+
+} // namespace blobs
diff --git a/src/smbios-ipmi-blobs/test/handler_unittest.hpp b/src/smbios-ipmi-blobs/test/handler_unittest.hpp
new file mode 100644
index 0000000..0b02a79
--- /dev/null
+++ b/src/smbios-ipmi-blobs/test/handler_unittest.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "handler.hpp"
+
+#include <ipmid/api.h>
+#include <systemd/sd-bus.h>
+
+#include <gtest/gtest.h>
+
+sd_bus* ipmid_get_sd_bus_connection()
+{
+    return nullptr;
+}
+
+namespace blobs
+{
+
+class SmbiosBlobHandlerTest : public ::testing::Test
+{
+  protected:
+    SmbiosBlobHandlerTest() = default;
+
+    SmbiosBlobHandler handler;
+
+    const uint16_t session = 0;
+    const std::string expectedBlobId = "/smbios";
+    const std::vector<std::string> expectedBlobIdList = {"/smbios"};
+    const uint32_t handlerMaxBufferSize = 64 * 1024;
+};
+} // namespace blobs