buffer: Implement "wraparoundRead"

This is a helper function for reading the circular buffer

Tested: Unit tested

Signed-off-by: Brandon Kim <brandonkim@google.com>
Change-Id: I0f7c0e3c0195c82fe932b36babbce0ab8c8405ed
diff --git a/include/buffer.hpp b/include/buffer.hpp
index 6c42401..ad845c4 100644
--- a/include/buffer.hpp
+++ b/include/buffer.hpp
@@ -86,6 +86,19 @@
      * @param[in] newReadPtr - read pointer to update to
      */
     virtual void updateReadPtr(const uint32_t newReadPtr) = 0;
+
+    /**
+     * Wrapper for the dataInterface->read, performs wraparound read
+     *
+     * @param[in] offset - offset to read from
+     * @param[in] length - bytes to read
+     * @param[in] additionalBoundaryCheck - bytes to add to the boundary check
+     * for added restriction
+     * @return the bytes read
+     */
+    virtual std::vector<uint8_t>
+        wraparoundRead(const uint32_t offset, const uint32_t length,
+                       const uint32_t additionalBoundaryCheck) = 0;
 };
 
 /**
@@ -104,8 +117,17 @@
     void readBufferHeader() override;
     struct CircularBufferHeader getCachedBufferHeader() const override;
     void updateReadPtr(const uint32_t newReadPtr) override;
+    std::vector<uint8_t>
+        wraparoundRead(const uint32_t offset, const uint32_t length,
+                       const uint32_t additionalBoundaryCheck = 0) override;
 
   private:
+    /** @brief The Error log queue starts after the UE region, which is where
+     * the read and write pointers are offset from relatively
+     *  @return relative offset for read and write pointers
+     */
+    size_t getQueueOffset();
+
     std::unique_ptr<DataInterface> dataInterface;
     struct CircularBufferHeader cachedBufferHeader = {};
 };
diff --git a/src/buffer.cpp b/src/buffer.cpp
index c07bba6..0b6f460 100644
--- a/src/buffer.cpp
+++ b/src/buffer.cpp
@@ -109,4 +109,55 @@
     cachedBufferHeader.bmcReadPtr = truncatedReadPtr;
 }
 
+size_t BufferImpl::getQueueOffset()
+{
+    return sizeof(struct CircularBufferHeader) +
+           boost::endian::little_to_native(cachedBufferHeader.ueRegionSize);
+}
+
+std::vector<uint8_t>
+    BufferImpl::wraparoundRead(const uint32_t offset, const uint32_t length,
+                               const uint32_t additionalBoundaryCheck)
+{
+    const size_t memoryRegionSize = dataInterface->getMemoryRegionSize();
+
+    size_t queueOffset = getQueueOffset();
+    if (queueOffset + length + additionalBoundaryCheck > memoryRegionSize)
+    {
+        throw std::runtime_error(fmt::format(
+            "[wraparoundRead] queueOffset '{}' + length '{}' "
+            "+ additionalBoundaryCheck '{}' + was bigger "
+            "than memoryRegionSize '{}'",
+            queueOffset, length, additionalBoundaryCheck, memoryRegionSize));
+    }
+
+    // Do a first read up to the end of the buffer (dataInerface->read should
+    // only read up to the end of the buffer)
+    std::vector<uint8_t> bytesRead = dataInterface->read(offset, length);
+    size_t updatedReadOffset = offset + bytesRead.size();
+    size_t bytesRemaining = length - bytesRead.size();
+
+    // If there are any more bytes to be read beyond the buffer, wrap around and
+    // read from the beginning of the buffer (offset by the queueOffset)
+    if (bytesRemaining > 0)
+    {
+        std::vector<uint8_t> wrappedBytesRead =
+            dataInterface->read(queueOffset, bytesRemaining);
+        bytesRemaining -= wrappedBytesRead.size();
+        if (bytesRemaining != 0)
+        {
+            throw std::runtime_error(fmt::format(
+                "[wraparoundRead] Buffer wrapped around but was not able to read "
+                "all of the requested info. Bytes remaining to read '{}' of '{}'",
+                bytesRemaining, length));
+        }
+        bytesRead.insert(bytesRead.end(), wrappedBytesRead.begin(),
+                         wrappedBytesRead.end());
+        updatedReadOffset = queueOffset + wrappedBytesRead.size();
+    }
+    updateReadPtr(updatedReadOffset);
+
+    return bytesRead;
+}
+
 } // namespace bios_bmc_smm_error_logger
diff --git a/test/buffer_test.cpp b/test/buffer_test.cpp
index 4307825..290d1ca 100644
--- a/test/buffer_test.cpp
+++ b/test/buffer_test.cpp
@@ -184,5 +184,164 @@
     EXPECT_NO_THROW(bufferImpl->updateReadPtr(testNewReadPtr));
 }
 
+class BufferWraparoundReadTest : public BufferTest
+{
+  protected:
+    BufferWraparoundReadTest()
+    {
+        // Initialize the memory and the cachedBufferHeader
+        InSequence s;
+        EXPECT_CALL(*dataInterfaceMockPtr, getMemoryRegionSize())
+            .WillOnce(Return(testRegionSize));
+        const std::vector<uint8_t> emptyArray(testRegionSize, 0);
+        EXPECT_CALL(*dataInterfaceMockPtr,
+                    write(0, ElementsAreArray(emptyArray)))
+            .WillOnce(Return(testRegionSize));
+
+        uint8_t* testInitializationHeaderPtr =
+            reinterpret_cast<uint8_t*>(&testInitializationHeader);
+        EXPECT_CALL(*dataInterfaceMockPtr,
+                    write(0, ElementsAreArray(testInitializationHeaderPtr,
+                                              bufferHeaderSize)))
+            .WillOnce(Return(bufferHeaderSize));
+        EXPECT_NO_THROW(bufferImpl->initialize(testBmcInterfaceVersion,
+                                               testQueueSize, testUeRegionSize,
+                                               testMagicNumber));
+    }
+    static constexpr size_t expectedWriteSize = 2;
+    static constexpr uint8_t expectedBmcReadPtrOffset = 0x20;
+    static constexpr size_t expectedqueueOffset = 0x30 + testUeRegionSize;
+};
+
+TEST_F(BufferWraparoundReadTest, TooBigReadFail)
+{
+    InSequence s;
+    EXPECT_CALL(*dataInterfaceMockPtr, getMemoryRegionSize())
+        .WillOnce(Return(testRegionSize));
+    size_t tooBigLength = testRegionSize - expectedqueueOffset + 1;
+    EXPECT_THROW(
+        try {
+            bufferImpl->wraparoundRead(/* offset */ 0, tooBigLength);
+        } catch (const std::runtime_error& e) {
+            EXPECT_STREQ(e.what(),
+                         "[wraparoundRead] queueOffset '128' + length '385' + "
+                         "additionalBoundaryCheck '0' + was "
+                         "bigger than memoryRegionSize '512'");
+            throw;
+        },
+        std::runtime_error);
+}
+
+TEST_F(BufferWraparoundReadTest, TooBigReadWithAdditionalBoundaryCheckFail)
+{
+    InSequence s;
+    EXPECT_CALL(*dataInterfaceMockPtr, getMemoryRegionSize())
+        .WillOnce(Return(testRegionSize));
+    // Use additionalBoundaryCheck to still go over the memoryRegionSize by 1
+    size_t additionalBoundaryCheck = 10;
+    size_t tooBigLength =
+        testRegionSize - expectedqueueOffset - additionalBoundaryCheck + 1;
+    EXPECT_THROW(
+        try {
+            bufferImpl->wraparoundRead(/* offset */ 0, tooBigLength,
+                                       additionalBoundaryCheck);
+        } catch (const std::runtime_error& e) {
+            EXPECT_STREQ(e.what(),
+                         "[wraparoundRead] queueOffset '128' + length '375' + "
+                         "additionalBoundaryCheck '10' + was "
+                         "bigger than memoryRegionSize '512'");
+            throw;
+        },
+        std::runtime_error);
+}
+
+TEST_F(BufferWraparoundReadTest, NoWrapAroundReadPass)
+{
+    InSequence s;
+    EXPECT_CALL(*dataInterfaceMockPtr, getMemoryRegionSize())
+        .WillOnce(Return(testRegionSize));
+    size_t testLength = 0x10;
+    size_t testOffset = 0x50;
+
+    // Successfully read all the requested length without a wrap around
+    std::vector<std::uint8_t> testBytesRead(testLength);
+    EXPECT_CALL(*dataInterfaceMockPtr, read(testOffset, testLength))
+        .WillOnce(Return(testBytesRead));
+
+    // Call to updateReadPtr is triggered
+    const std::vector<uint8_t> expectedReadPtr{
+        static_cast<uint8_t>(testOffset + testLength), 0x0};
+    EXPECT_CALL(*dataInterfaceMockPtr, write(expectedBmcReadPtrOffset,
+                                             ElementsAreArray(expectedReadPtr)))
+        .WillOnce(Return(expectedWriteSize));
+
+    EXPECT_THAT(bufferImpl->wraparoundRead(testOffset, testLength),
+                ElementsAreArray(testBytesRead));
+}
+
+TEST_F(BufferWraparoundReadTest, WrapAroundReadFails)
+{
+    InSequence s;
+    EXPECT_CALL(*dataInterfaceMockPtr, getMemoryRegionSize())
+        .WillOnce(Return(testRegionSize));
+    size_t testBytesLeft = 3;
+    size_t testLength = 0x10;
+    size_t testOffset = testRegionSize - (testLength - testBytesLeft);
+
+    // Read 3 bytes short
+    std::vector<std::uint8_t> testBytesReadShort(testLength - testBytesLeft);
+    EXPECT_CALL(*dataInterfaceMockPtr, read(testOffset, testLength))
+        .WillOnce(Return(testBytesReadShort));
+
+    // Read 1 byte short after wraparound
+    std::vector<std::uint8_t> testBytesLeftReadShort(testBytesLeft - 1);
+    EXPECT_CALL(*dataInterfaceMockPtr, read(expectedqueueOffset, testBytesLeft))
+        .WillOnce(Return(testBytesLeftReadShort));
+
+    EXPECT_THROW(
+        try {
+            bufferImpl->wraparoundRead(testOffset, testLength);
+        } catch (const std::runtime_error& e) {
+            EXPECT_STREQ(e.what(),
+                         "[wraparoundRead] Buffer wrapped around but was not "
+                         "able to read all of the requested info. "
+                         "Bytes remaining to read '1' of '16'");
+            throw;
+        },
+        std::runtime_error);
+}
+
+TEST_F(BufferWraparoundReadTest, WrapAroundReadPasses)
+{
+    InSequence s;
+    EXPECT_CALL(*dataInterfaceMockPtr, getMemoryRegionSize())
+        .WillOnce(Return(testRegionSize));
+    size_t testBytesLeft = 3;
+    size_t testLength = 0x10;
+    size_t testOffset = testRegionSize - (testLength - testBytesLeft);
+
+    // Read 3 bytes short
+    std::vector<std::uint8_t> testBytesReadFirst{16, 15, 14, 13, 12, 11, 10,
+                                                 9,  8,  7,  6,  5,  4};
+    EXPECT_CALL(*dataInterfaceMockPtr, read(testOffset, testLength))
+        .WillOnce(Return(testBytesReadFirst));
+
+    std::vector<std::uint8_t> testBytesReadSecond{3, 2, 1};
+    EXPECT_CALL(*dataInterfaceMockPtr, read(expectedqueueOffset, testBytesLeft))
+        .WillOnce(Return(testBytesReadSecond));
+
+    // Call to updateReadPtr is triggered
+    const std::vector<uint8_t> expectedReadPtr{
+        static_cast<uint8_t>(expectedqueueOffset + testBytesLeft), 0x0};
+    EXPECT_CALL(*dataInterfaceMockPtr, write(expectedBmcReadPtrOffset,
+                                             ElementsAreArray(expectedReadPtr)))
+        .WillOnce(Return(expectedWriteSize));
+
+    std::vector<std::uint8_t> expectedBytes = {16, 15, 14, 13, 12, 11, 10, 9,
+                                               8,  7,  6,  5,  4,  3,  2,  1};
+    EXPECT_THAT(bufferImpl->wraparoundRead(testOffset, testLength),
+                ElementsAreArray(expectedBytes));
+}
+
 } // namespace
 } // namespace bios_bmc_smm_error_logger