pci_handler: Add pci_handler

Taken most of the logic from phoshpor-ipmi-flash:
https://github.com/openbmc/phosphor-ipmi-flash/blob/master/tools/io.cpp

Tested: Unit Test + Tested this on a real machine

Signed-off-by: Brandon Kim <brandonkim@google.com>
Change-Id: I546d1dff8764e31c9f2fa6c7465ba23a24f6fdea
diff --git a/include/pci_handler.hpp b/include/pci_handler.hpp
new file mode 100644
index 0000000..98a8cdc
--- /dev/null
+++ b/include/pci_handler.hpp
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/mmap.hpp>
+
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <vector>
+
+namespace bios_bmc_smm_error_logger
+{
+
+/**
+ * Each data transport mechanism must implement the DataInterface.
+ */
+class DataInterface
+{
+  public:
+    virtual ~DataInterface() = default;
+
+    /**
+     * Read bytes from shared buffer (blocking call).
+     *
+     * @param[in] offset - offset to read from
+     * @param[in] length - number of bytes to read
+     * @return the bytes read
+     */
+    virtual std::vector<uint8_t> read(const uint32_t offset,
+                                      const uint32_t length) = 0;
+
+    /**
+     * Write bytes to shared buffer.
+     *
+     * @param[in] offset - offset to write to
+     * @param[in] bytes - byte vector of data.
+     * @return return the byte length written
+     */
+    virtual uint32_t write(const uint32_t offset,
+                           const std::span<const uint8_t> bytes) = 0;
+
+    /**
+     * Getter for Memory Region Size
+     *
+     * @return return Memory Region size allocated
+     */
+    virtual uint32_t getMemoryRegionSize() = 0;
+};
+
+/**
+ * Data handler for reading and writing data via the PCI bridge.
+ *
+ */
+class PciDataHandler : public DataInterface
+{
+  public:
+    explicit PciDataHandler(uint32_t regionAddress, size_t regionSize,
+                            std::unique_ptr<stdplus::fd::Fd> fd);
+
+    std::vector<uint8_t> read(uint32_t offset, uint32_t length) override;
+    uint32_t write(const uint32_t offset,
+                   const std::span<const uint8_t> bytes) override;
+    uint32_t getMemoryRegionSize() override;
+
+  private:
+    uint32_t regionAddress;
+    uint32_t regionSize;
+
+    std::unique_ptr<stdplus::fd::Fd> fd;
+    stdplus::fd::MMap mmap;
+};
+
+} // namespace bios_bmc_smm_error_logger
diff --git a/meson.build b/meson.build
index 2366091..85d7272 100644
--- a/meson.build
+++ b/meson.build
@@ -9,7 +9,13 @@
     'werror=true',
   ])
 
+root_inc = include_directories('.')
+bios_bmc_smm_error_logger_inc = include_directories('include')
+
 subdir('src')
+if not get_option('tests').disabled()
+  subdir('test')
+endif
 
 # installation of systemd service files
 subdir('service_files')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..0fc2767
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1 @@
+option('tests', type: 'feature', description: 'Build tests')
diff --git a/src/meson.build b/src/meson.build
index 1a4f80a..c7f76bd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,10 +1,35 @@
+fmt_dep = dependency('fmt', required: false)
+if not fmt_dep.found()
+  fmt_opts = import('cmake').subproject_options()
+  fmt_opts.add_cmake_defines({
+    'CMAKE_POSITION_INDEPENDENT_CODE': 'ON',
+    'MASTER_PROJECT': 'OFF',
+  })
+  fmt_proj = import('cmake').subproject(
+    'fmt',
+    options: fmt_opts,
+    required: false)
+  assert(fmt_proj.found(), 'fmtlib is required')
+  fmt_dep = fmt_proj.dependency('fmt')
+endif
+
 bios_bmc_smm_error_logger_pre = declare_dependency(
+  include_directories: [root_inc, bios_bmc_smm_error_logger_inc],
   dependencies: [
+    fmt_dep,
     dependency('threads'),
+    dependency('stdplus'),
   ])
 
+bios_bmc_smm_error_logger_lib = static_library(
+  'bios_bmc_smm_error_logger',
+  'pci_handler.cpp',
+  implicit_include_directories: false,
+  dependencies: bios_bmc_smm_error_logger_pre)
+
 bios_bmc_smm_error_logger_dep = declare_dependency(
-  dependencies: bios_bmc_smm_error_logger_pre,)
+  link_with: bios_bmc_smm_error_logger_lib,
+  dependencies: bios_bmc_smm_error_logger_pre)
 
 executable(
   'bios-bmc-smm-error-logger',
diff --git a/src/pci_handler.cpp b/src/pci_handler.cpp
new file mode 100644
index 0000000..b3e25bd
--- /dev/null
+++ b/src/pci_handler.cpp
@@ -0,0 +1,73 @@
+#include "pci_handler.hpp"
+
+#include <fcntl.h>
+#include <fmt/format.h>
+
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/mmap.hpp>
+
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <span>
+#include <vector>
+
+namespace bios_bmc_smm_error_logger
+{
+
+PciDataHandler::PciDataHandler(uint32_t regionAddress, size_t regionSize,
+                               std::unique_ptr<stdplus::fd::Fd> fd) :
+    regionAddress(regionAddress),
+    regionSize(regionSize), fd(std::move(fd)),
+    mmap(stdplus::fd::MMap(
+        *this->fd, regionSize, stdplus::fd::ProtFlags{PROT_READ | PROT_WRITE},
+        stdplus::fd::MMapFlags{stdplus::fd::MMapAccess::Shared}, regionAddress))
+{}
+
+std::vector<uint8_t> PciDataHandler::read(const uint32_t offset,
+                                          const uint32_t length)
+{
+    if (offset > regionSize || length == 0)
+    {
+        fmt::print(stderr,
+                   "[read] Offset [{}] was bigger than regionSize [{}] "
+                   "OR length [{}] was equal to 0\n",
+                   offset, regionSize, length);
+        return {};
+    }
+
+    // Read up to regionSize in case the offset + length overflowed
+    uint32_t finalLength =
+        (offset + length < regionSize) ? length : regionSize - offset;
+    std::vector<uint8_t> results(finalLength);
+
+    std::memcpy(results.data(), mmap.get().data() + offset, finalLength);
+    return results;
+}
+
+uint32_t PciDataHandler::write(const uint32_t offset,
+                               const std::span<const uint8_t> bytes)
+{
+    const size_t length = bytes.size();
+    if (offset > regionSize || length == 0)
+    {
+        fmt::print(stderr,
+                   "[write] Offset [{}] was bigger than regionSize [{}] "
+                   "OR length [{}] was equal to 0\n",
+                   offset, regionSize, length);
+        return 0;
+    }
+
+    // Write up to regionSize in case the offset + length overflowed
+    uint16_t finalLength =
+        (offset + length < regionSize) ? length : regionSize - offset;
+    std::memcpy(mmap.get().data() + offset, bytes.data(), finalLength);
+    return finalLength;
+}
+
+uint32_t PciDataHandler::getMemoryRegionSize()
+{
+    return regionSize;
+}
+
+} // namespace bios_bmc_smm_error_logger
diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap
new file mode 100644
index 0000000..6847ae5
--- /dev/null
+++ b/subprojects/fmt.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/fmtlib/fmt
+revision = HEAD
diff --git a/subprojects/stdplus.wrap b/subprojects/stdplus.wrap
new file mode 100644
index 0000000..08f32fa
--- /dev/null
+++ b/subprojects/stdplus.wrap
@@ -0,0 +1,6 @@
+[wrap-git]
+url = https://github.com/openbmc/stdplus
+revision = HEAD
+
+[provide]
+stdplus = stdplus_dep
\ No newline at end of file
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..93ddce9
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,28 @@
+gtest = dependency('gtest', main: true, disabler: true, required: false)
+gmock = dependency('gmock', disabler: true, required: false)
+if not gtest.found() or not gmock.found()
+  gtest_opt = import('cmake').subproject_options()
+  gtest_opt.append_compile_args('c++', ['-DCMAKE_CXX_FLAGS=-Wno-pedantic'])
+  gtest_proj = cmake.subproject('googletest', options: gtest_opt, required: false)
+  fmt_depj.dependency('fmt')
+
+  if gtest_proj.found()
+    gtest = declare_dependency(
+      dependencies: [
+        dependency('threads'),
+        gtest_proj.dependency('gtest'),
+        gtest_proj.dependency('gtest_main'),
+      ])
+    gmock = gtest_proj.dependency('gmock')
+  endif
+endif
+
+gtests = [
+  'pci_handler',
+]
+foreach t : gtests
+  test(t, executable(t.underscorify(), t + '_test.cpp',
+                     build_by_default: false,
+                     implicit_include_directories: false,
+                     dependencies: [bios_bmc_smm_error_logger_dep, gtest, gmock]))
+endforeach
diff --git a/test/pci_handler_test.cpp b/test/pci_handler_test.cpp
new file mode 100644
index 0000000..817fc8f
--- /dev/null
+++ b/test/pci_handler_test.cpp
@@ -0,0 +1,144 @@
+#include "pci_handler.hpp"
+
+#include <stdplus/fd/gmock.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/mmap.hpp>
+
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace bios_bmc_smm_error_logger
+{
+namespace
+{
+
+using ::testing::_;
+using ::testing::ElementsAreArray;
+using ::testing::Return;
+
+class PciHandlerTest : public ::testing::Test
+{
+  protected:
+    PciHandlerTest() :
+        testMapped({std::byte(0), std::byte(11), std::byte(22), std::byte(33),
+                    std::byte(44), std::byte(55), std::byte(66),
+                    std::byte(77)}),
+        fdMock(std::make_unique<stdplus::fd::FdMock>()), fdMockPtr(fdMock.get())
+    {
+        // Verify that the constructor is called as expected
+        EXPECT_CALL(*fdMockPtr, mmap(_, _, _, _))
+            .WillOnce(Return(std::span<std::byte>(testMapped)));
+        pciDataHandler = std::make_unique<PciDataHandler>(
+            testRegionAddress, testRegionSize, std::move(fdMock));
+    }
+    ~PciHandlerTest() override
+    {
+        // Verify that the destructor is called as expected
+        EXPECT_CALL(*fdMockPtr, munmap(_)).Times(1);
+    }
+    static constexpr uint32_t testRegionAddress = 0xF0848000;
+    // Smaller region size for easier boundary testing
+    static constexpr size_t testRegionSize = 8;
+    std::vector<std::byte> testMapped;
+    std::unique_ptr<stdplus::fd::FdMock> fdMock;
+    stdplus::fd::FdMock* fdMockPtr;
+
+    std::unique_ptr<PciDataHandler> pciDataHandler;
+};
+
+TEST_F(PciHandlerTest, GetMemoryRegionSizeSanity)
+{
+    EXPECT_EQ(pciDataHandler->getMemoryRegionSize(), testRegionSize);
+}
+
+TEST_F(PciHandlerTest, BoundaryChecksReadFail)
+{
+    std::vector<uint8_t> emptyVector;
+    // Zero size
+    EXPECT_THAT(pciDataHandler->read(0, 0), ElementsAreArray(emptyVector));
+
+    const int offsetTooBig = testRegionSize + 1;
+    EXPECT_THAT(pciDataHandler->read(offsetTooBig, 1),
+                ElementsAreArray(emptyVector));
+}
+
+TEST_F(PciHandlerTest, BoundaryChecksWriteFail)
+{
+    std::vector<uint8_t> emptyVector;
+    // Zero size
+    EXPECT_EQ(pciDataHandler->write(0, emptyVector), 0);
+
+    const int offsetTooBig = testRegionSize + 1;
+    std::vector<uint8_t> testVector(testRegionSize - 1);
+    EXPECT_EQ(pciDataHandler->write(offsetTooBig, testVector), 0);
+}
+
+TEST_F(PciHandlerTest, ReadPasses)
+{
+    // Normal read from 0
+    uint32_t testOffset = 0;
+    uint32_t testSize = 2;
+    std::vector<uint8_t> expectedVector{0, 11};
+    EXPECT_THAT(pciDataHandler->read(testOffset, testSize),
+                ElementsAreArray(expectedVector));
+
+    // Read to buffer boundary from non 0 offset
+    testOffset = 3;
+    testSize = testRegionSize - testOffset;
+    expectedVector.clear();
+    expectedVector = {33, 44, 55, 66, 77};
+    EXPECT_THAT(pciDataHandler->read(testOffset, testSize),
+                ElementsAreArray(expectedVector));
+
+    // Read over buffer boundary (which will read until the end)
+    testOffset = 4;
+    testSize = testRegionSize - testOffset + 1;
+    expectedVector.clear();
+    expectedVector = {44, 55, 66, 77};
+    EXPECT_THAT(pciDataHandler->read(testOffset, testSize),
+                ElementsAreArray(expectedVector));
+}
+
+TEST_F(PciHandlerTest, WritePasses)
+{
+    std::vector<std::byte> expectedMapped{
+        std::byte(0),  std::byte(11), std::byte(22), std::byte(33),
+        std::byte(44), std::byte(55), std::byte(66), std::byte(77)};
+
+    // Normal write from 0
+    uint32_t testOffset = 0;
+    std::vector<uint8_t> writeVector{99, 88};
+    expectedMapped[0] = std::byte(99);
+    expectedMapped[1] = std::byte(88);
+
+    EXPECT_EQ(pciDataHandler->write(testOffset, writeVector),
+              writeVector.size());
+    EXPECT_THAT(testMapped, ElementsAreArray(expectedMapped));
+
+    // Write to buffer boundary from non 0 offset
+    testOffset = 4;
+    writeVector = {55, 44, 33, 22};
+    expectedMapped[4] = std::byte(55);
+    expectedMapped[5] = std::byte(44);
+    expectedMapped[6] = std::byte(33);
+    expectedMapped[7] = std::byte(22);
+    EXPECT_EQ(pciDataHandler->write(testOffset, writeVector),
+              writeVector.size());
+    EXPECT_THAT(testMapped, ElementsAreArray(expectedMapped));
+
+    // Read over buffer boundary (which will read until the end)
+    testOffset = 7;
+    writeVector = {12, 23, 45};
+    expectedMapped[7] = std::byte(12);
+    EXPECT_EQ(pciDataHandler->write(testOffset, writeVector),
+              testRegionSize - testOffset);
+    EXPECT_THAT(testMapped, ElementsAreArray(expectedMapped));
+}
+
+} // namespace
+} // namespace bios_bmc_smm_error_logger