[cper-lib] Implement CperDecoder and refactor safe accessors

Introduces a `CperDecoder` library to safely parse UEFI CPER records and
refactors the codebase to centralize safe memory access patterns. This ensures
full compatibility with UBSan and strict alignment toolchains.

Key Changes:
- CperDecoder: Added a new `uefi::cper::CperDecoder` class that provides a
robust interface for parsing CPER records. It performs signature validation,
boundary checks for section descriptors, and provides safe access to section
bodies via std::span.
- Centralized getField: Moved the `getField<T>` helper to `include/cper.hpp`.
This allows the decoder and unit tests to share a single implementation for
safely reading members of unaligned packed structs using memcpy.
- Hardened gmtime_r: Added explicit return value checks for `gmtime_r` calls,
throwing `std::runtime_error` on failure to ensure reliability when processing
timestamps.
- Refactored Tests: Updated `cper_encoder_test.cpp` to use the new
`CperDecoder` for its assertions. This simplifies the test logic and verifies
the decoder's functionality through round-trip testing.
- New Test Coverage: Added unit tests for the decoder to verify handling of
truncated headers, invalid signatures, and malformed section counts.

Tested:
- All 9 unit tests passed via Meson inside the gbmc_dev container.
- All tests passed via Bazel/Blaze using the Google cc_toolchain.
- Built an experimental cli tool on google3 using the decoder on AsicCper logs
from sdhb and compared the output to the CFA

Google-Bug-Id: 505395424
Change-Id: I02a385a591f7ccee8fc9e3e28ee4e79755170656
Signed-off-by: Aryk Ledet <arykledet@google.com>
diff --git a/include/cper.hpp b/include/cper.hpp
index 500cf6e..4f75521 100644
--- a/include/cper.hpp
+++ b/include/cper.hpp
@@ -11,6 +11,7 @@
 // https://uefi.org/specs/UEFI/2.10/Apx_N_Common_Platform_Error_Record.html#common-platform-error-record-cper
 namespace uefi::cper
 {
+
 constexpr size_t kRecordSignatureSize = 0x4;
 constexpr std::array<uint8_t, kRecordSignatureSize> kRecordSignature = {
     'C', 'P', 'E', 'R'};
diff --git a/include/cper_decoder.hpp b/include/cper_decoder.hpp
new file mode 100644
index 0000000..498c876
--- /dev/null
+++ b/include/cper_decoder.hpp
@@ -0,0 +1,131 @@
+#pragma once
+
+#include "cper.hpp"
+
+#include <absl/base/internal/endian.h>
+
+#include <span>
+#include <stdexcept>
+#include <vector>
+
+namespace uefi::cper
+{
+
+template <typename T>
+inline T littleEndianLoad(const void* p)
+{
+    if constexpr (sizeof(T) == 1)
+        return *static_cast<const uint8_t*>(p);
+    if constexpr (sizeof(T) == 2)
+        return absl::little_endian::Load16(p);
+    if constexpr (sizeof(T) == 4)
+        return absl::little_endian::Load32(p);
+    if constexpr (sizeof(T) == 8)
+        return absl::little_endian::Load64(p);
+}
+
+class CperDecoder
+{
+  public:
+    /**
+     * @brief Construct a new CperDecoder object
+     *
+     * @param[in] data - The CPER record data to decode.
+     *
+     * @throws std::invalid_argument If the data is too small to contain a
+     * RecordHeader or if the CPER signature is missing.
+     */
+    explicit CperDecoder(std::span<const uint8_t> data) : data_(data)
+    {
+        if (data_.size() < kRecordHeaderSizeBytes)
+        {
+            throw std::invalid_argument("Data too small for RecordHeader");
+        }
+
+        const uint8_t* headerPtr = data_.data();
+
+        // Manual copy for the signature array since Load helpers usually target
+        // integers
+        std::array<uint8_t, kRecordSignatureSize> signature;
+        std::memcpy(signature.data(),
+                    headerPtr + offsetof(RecordHeader, signature),
+                    kRecordSignatureSize);
+
+        if (memcmp(signature.data(), kRecordSignature.data(),
+                   kRecordSignatureSize) != 0)
+        {
+            throw std::invalid_argument("Invalid CPER signature");
+        }
+    }
+
+    /**
+     * @brief Get the RecordHeader from the CPER record.
+     *
+     * @return The RecordHeader struct.
+     */
+    RecordHeader getHeader() const
+    {
+        RecordHeader header;
+        memcpy(&header, data_.data(), kRecordHeaderSizeBytes);
+        return header;
+    }
+
+    struct Section
+    {
+        SectionDescriptor descriptor;
+        std::span<const uint8_t> body;
+    };
+
+    /**
+     * @brief Get the sections from the CPER record.
+     *
+     * @return A vector of Section structs containing the descriptor and body.
+     *
+     * @throws std::runtime_error If the record is malformed (e.g. section
+     * offset or length out of bounds).
+     */
+    std::vector<Section> getSections() const
+    {
+        const uint8_t* headerPtr = data_.data();
+        const uint16_t sectionCount = littleEndianLoad<uint16_t>(
+            headerPtr + offsetof(RecordHeader, sectionCount));
+
+        std::vector<Section> sections;
+        sections.reserve(sectionCount);
+
+        for (uint16_t i = 0; i < sectionCount; ++i)
+        {
+            const uint64_t descriptorOffset =
+                kRecordHeaderSizeBytes + (i * kSectionDescriptorSizeBytes);
+
+            if (descriptorOffset + kSectionDescriptorSizeBytes > data_.size())
+            {
+                throw std::runtime_error("Section descriptor out of bounds");
+            }
+
+            const uint8_t* descriptorPtr = data_.data() + descriptorOffset;
+            SectionDescriptor descriptor;
+            memcpy(&descriptor, descriptorPtr, kSectionDescriptorSizeBytes);
+
+            const uint32_t bodyOffset = littleEndianLoad<uint32_t>(
+                descriptorPtr + offsetof(SectionDescriptor, sectionOffset));
+            const uint32_t bodyLength = littleEndianLoad<uint32_t>(
+                descriptorPtr + offsetof(SectionDescriptor, sectionLength));
+
+            if (bodyOffset + bodyLength > data_.size())
+            {
+                throw std::runtime_error("Section body out of bounds");
+            }
+
+            sections.push_back(
+                {descriptor, data_.subspan(bodyOffset, bodyLength)});
+        }
+
+        return sections;
+    }
+
+  private:
+    std::span<const uint8_t> data_;
+};
+
+} // namespace uefi::cper
diff --git a/meson.build b/meson.build
index 1003aba..a951bdf 100644
--- a/meson.build
+++ b/meson.build
@@ -8,8 +8,11 @@
 
 cpp_compiler = meson.get_compiler('cpp')
 
+absl_endian_dep = dependency('absl_endian', required : true)
+
 cper_lib_headers = files(
     'include/cper_encoder.hpp',
+    'include/cper_decoder.hpp',
     'include/cper.hpp',
     'include/guid.hpp'
 )
diff --git a/test/cper_decoder_test.cpp b/test/cper_decoder_test.cpp
new file mode 100644
index 0000000..a2efa99
--- /dev/null
+++ b/test/cper_decoder_test.cpp
@@ -0,0 +1,38 @@
+#include "cper_decoder.hpp"
+#include "mock_cper_encoder.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace uefi::cper
+{
+
+TEST(CperDecoderTest, DecoderInvalidData)
+{
+    // Too small for header
+    std::vector<uint8_t> smallData(kRecordHeaderSizeBytes - 1, 0);
+    EXPECT_THROW(CperDecoder{smallData}, std::invalid_argument);
+
+    // Invalid signature
+    std::vector<uint8_t> invalidSignature(kRecordHeaderSizeBytes, 0);
+    EXPECT_THROW(CperDecoder{invalidSignature}, std::invalid_argument);
+}
+
+TEST(CperDecoderTest, DecoderMalformedSections)
+{
+    MockCperEncoder<uint8_t> mockEncoder;
+    const std::array<uint8_t, 10> sectionBody = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+    mockEncoder.addSection(sectionBody, kMockSectionFlag, kMockSectionType);
+    std::vector<uint8_t> cperArray = mockEncoder.serializeToByteArray();
+
+    // Corrupt section count in header to be 2 instead of 1
+    void* headerPtr = cperArray.data();
+    uint16_t malformedCount = 2;
+    memcpy(static_cast<char*>(headerPtr) + offsetof(RecordHeader, sectionCount),
+           &malformedCount, sizeof(malformedCount));
+
+    CperDecoder decoder(cperArray);
+    EXPECT_THROW(decoder.getSections(), std::runtime_error);
+}
+
+} // namespace uefi::cper
diff --git a/test/cper_encoder_test.cpp b/test/cper_encoder_test.cpp
index 8a8e7f4..1de0fac 100644
--- a/test/cper_encoder_test.cpp
+++ b/test/cper_encoder_test.cpp
@@ -1,8 +1,8 @@
+#include "cper_decoder.hpp"
 #include "cper_encoder.hpp"
 #include "mock_cper_encoder.hpp"
 
 #include <chrono>
-#include <concepts>
 #include <cstring>
 
 #include <gmock/gmock.h>
@@ -34,14 +34,6 @@
     void SetUp() override
     {}
 
-    template <typename T, typename S>
-    T getField(const S* s, size_t offset)
-    {
-        T val;
-        std::memcpy(&val, reinterpret_cast<const char*>(s) + offset, sizeof(T));
-        return val;
-    }
-
     bool isArrayEq(const void* arr1, const void* arr2, const uint64_t arr1Size,
                    const uint64_t arr2Size)
     {
@@ -52,136 +44,165 @@
         return memcmp(arr1, arr2, arr1Size) == 0;
     }
 
-    void expectValidHeader(const void* data, const uint64_t size,
+    void expectValidHeader(const RecordHeader& header,
                            const uint16_t numSections, const uint64_t cperSize,
                            const uint64_t timestamp)
     {
-        ASSERT_GE(size, sizeof(RecordHeader));
-        const RecordHeader* header =
-            reinterpret_cast<const RecordHeader*>(data);
+        const void* headerPtr = &header;
 
-        const auto signature =
-            getField<std::array<uint8_t, kRecordSignatureSize>>(
-                header, offsetof(RecordHeader, signature));
+        std::array<uint8_t, kRecordSignatureSize> signature;
+        std::memcpy(signature.data(),
+                    static_cast<const char*>(headerPtr) +
+                        offsetof(RecordHeader, signature),
+                    kRecordSignatureSize);
+
         EXPECT_EQ(memcmp(signature.data(), kRecordSignature.data(),
                          kRecordSignatureSize),
                   0);
-        EXPECT_EQ(getField<uint16_t>(header, offsetof(RecordHeader, revision)),
-                  kRecordRevision);
         EXPECT_EQ(
-            getField<uint32_t>(header, offsetof(RecordHeader, signatureEnd)),
+            littleEndianLoad<uint16_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, revision)),
+            kRecordRevision);
+        EXPECT_EQ(
+            littleEndianLoad<uint32_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, signatureEnd)),
             kRecordSignatureEnd);
         EXPECT_EQ(
-            getField<uint16_t>(header, offsetof(RecordHeader, sectionCount)),
+            littleEndianLoad<uint16_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, sectionCount)),
             numSections);
         EXPECT_EQ(
-            getField<uint32_t>(header, offsetof(RecordHeader, errorSeverity)),
+            littleEndianLoad<uint32_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, errorSeverity)),
             kMockRecordSeverity);
         EXPECT_EQ(
-            getField<uint32_t>(header, offsetof(RecordHeader, validationBits)),
+            littleEndianLoad<uint32_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, validationBits)),
             kMockValidationBits);
         EXPECT_EQ(
-            getField<uint32_t>(header, offsetof(RecordHeader, recordLength)),
+            littleEndianLoad<uint32_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, recordLength)),
             cperSize);
-        EXPECT_EQ(getField<uint64_t>(header, offsetof(RecordHeader, timestamp)),
-                  timestamp);
-
-        EXPECT_EQ(getField<guid_t>(header, offsetof(RecordHeader, platformId)),
-                  kMockPlatformId);
-        EXPECT_EQ(getField<guid_t>(header, offsetof(RecordHeader, partitionId)),
-                  kMockPartitionId);
-        EXPECT_EQ(getField<guid_t>(header, offsetof(RecordHeader, creatorId)),
-                  kMockCreatorId);
         EXPECT_EQ(
-            getField<guid_t>(header, offsetof(RecordHeader, notificationType)),
-            kMockNotificationType);
+            littleEndianLoad<uint64_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, timestamp)),
+            timestamp);
 
-        EXPECT_EQ(getField<uint64_t>(header, offsetof(RecordHeader, recordId)),
-                  kMockRecordId);
-        EXPECT_EQ(getField<uint32_t>(header, offsetof(RecordHeader, flags)),
-                  kMockHeaderFlags);
-        EXPECT_EQ(getField<uint64_t>(
-                      header, offsetof(RecordHeader, persistenceInformation)),
+        guid_t platformId;
+        std::memcpy(&platformId,
+                    static_cast<const char*>(headerPtr) +
+                        offsetof(RecordHeader, platformId),
+                    sizeof(guid_t));
+        EXPECT_EQ(platformId, kMockPlatformId);
+
+        guid_t partitionId;
+        std::memcpy(&partitionId,
+                    static_cast<const char*>(headerPtr) +
+                        offsetof(RecordHeader, partitionId),
+                    sizeof(guid_t));
+        EXPECT_EQ(partitionId, kMockPartitionId);
+
+        guid_t creatorId;
+        std::memcpy(&creatorId,
+                    static_cast<const char*>(headerPtr) +
+                        offsetof(RecordHeader, creatorId),
+                    sizeof(guid_t));
+        EXPECT_EQ(creatorId, kMockCreatorId);
+
+        guid_t notificationType;
+        std::memcpy(&notificationType,
+                    static_cast<const char*>(headerPtr) +
+                        offsetof(RecordHeader, notificationType),
+                    sizeof(guid_t));
+        EXPECT_EQ(notificationType, kMockNotificationType);
+
+        EXPECT_EQ(
+            littleEndianLoad<uint64_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, recordId)),
+            kMockRecordId);
+        EXPECT_EQ(
+            littleEndianLoad<uint32_t>(static_cast<const char*>(headerPtr) +
+                                       offsetof(RecordHeader, flags)),
+            kMockHeaderFlags);
+        EXPECT_EQ(littleEndianLoad<uint64_t>(
+                      static_cast<const char*>(headerPtr) +
+                      offsetof(RecordHeader, persistenceInformation)),
                   kMockPersistenceInformation);
 
         std::array<uint8_t, 12> expectedReserved = {0};
-        const auto reserved = getField<std::array<uint8_t, 12>>(
-            header, offsetof(RecordHeader, reserved));
+        std::array<uint8_t, 12> reserved;
+        std::memcpy(reserved.data(),
+                    static_cast<const char*>(headerPtr) +
+                        offsetof(RecordHeader, reserved),
+                    12);
         EXPECT_EQ(
             memcmp(reserved.data(), expectedReserved.data(), reserved.size()),
             0);
     }
 
-    template <StringOrVector CperType>
-    void expectValidSectionDescriptor(const CperType& cperArray,
-                                      const uint64_t sectionNum,
+    void expectValidSectionDescriptor(const SectionDescriptor& descriptor,
                                       const uint32_t expectedBodyOffset,
                                       const uint32_t expectedLength,
                                       const uint32_t expectedFlag,
                                       const guid_t& expectedSectionType)
     {
-        const uint64_t sectionOffset =
-            kRecordHeaderSizeBytes + (kSectionDescriptorSizeBytes * sectionNum);
-        ASSERT_GE(cperArray.size(), sectionOffset + sizeof(SectionDescriptor));
-        const SectionDescriptor* descriptor =
-            reinterpret_cast<const SectionDescriptor*>(
-                &cperArray[sectionOffset]);
+        const void* descPtr = &descriptor;
 
-        EXPECT_EQ(getField<uint32_t>(
-                      descriptor, offsetof(SectionDescriptor, sectionOffset)),
+        EXPECT_EQ(littleEndianLoad<uint32_t>(
+                      static_cast<const char*>(descPtr) +
+                      offsetof(SectionDescriptor, sectionOffset)),
                   expectedBodyOffset);
-        EXPECT_EQ(getField<uint32_t>(
-                      descriptor, offsetof(SectionDescriptor, sectionLength)),
+        EXPECT_EQ(littleEndianLoad<uint32_t>(
+                      static_cast<const char*>(descPtr) +
+                      offsetof(SectionDescriptor, sectionLength)),
                   expectedLength);
-        EXPECT_EQ(getField<uint16_t>(descriptor,
-                                     offsetof(SectionDescriptor, revision)),
-                  kMockSectionRevision);
-        EXPECT_EQ(getField<uint8_t>(
-                      descriptor, offsetof(SectionDescriptor, validationBits)),
+        EXPECT_EQ(
+            littleEndianLoad<uint16_t>(static_cast<const char*>(descPtr) +
+                                       offsetof(SectionDescriptor, revision)),
+            kMockSectionRevision);
+        EXPECT_EQ(static_cast<const uint8_t*>(
+                      descPtr)[offsetof(SectionDescriptor, validationBits)],
                   kMockSectionValidationBits);
-        EXPECT_EQ(getField<uint8_t>(descriptor,
-                                    offsetof(SectionDescriptor, reserved)),
+        EXPECT_EQ(static_cast<const uint8_t*>(
+                      descPtr)[offsetof(SectionDescriptor, reserved)],
                   0);
         EXPECT_EQ(
-            getField<uint32_t>(descriptor, offsetof(SectionDescriptor, flags)),
+            littleEndianLoad<uint32_t>(static_cast<const char*>(descPtr) +
+                                       offsetof(SectionDescriptor, flags)),
             expectedFlag);
 
-        EXPECT_EQ(getField<guid_t>(descriptor,
-                                   offsetof(SectionDescriptor, sectionType)),
-                  expectedSectionType);
-        EXPECT_EQ(
-            getField<guid_t>(descriptor, offsetof(SectionDescriptor, fruId)),
-            kMockFruId);
+        guid_t sectionType;
+        std::memcpy(&sectionType,
+                    static_cast<const char*>(descPtr) +
+                        offsetof(SectionDescriptor, sectionType),
+                    sizeof(guid_t));
+        EXPECT_EQ(sectionType, expectedSectionType);
 
-        EXPECT_EQ(getField<uint32_t>(
-                      descriptor, offsetof(SectionDescriptor, sectionSeverity)),
+        guid_t fruId;
+        std::memcpy(&fruId,
+                    static_cast<const char*>(descPtr) +
+                        offsetof(SectionDescriptor, fruId),
+                    sizeof(guid_t));
+        EXPECT_EQ(fruId, kMockFruId);
+
+        EXPECT_EQ(littleEndianLoad<uint32_t>(
+                      static_cast<const char*>(descPtr) +
+                      offsetof(SectionDescriptor, sectionSeverity)),
                   kMockSectionSeverity);
 
         std::array<uint8_t, 20> expectedFruText = {0};
         memcpy(expectedFruText.data(), kMockFruText.data(),
                kMockFruText.size());
-        const auto fruText = getField<std::array<uint8_t, 20>>(
-            descriptor, offsetof(SectionDescriptor, fruText));
+
+        std::array<uint8_t, 20> fruText;
+        std::memcpy(fruText.data(),
+                    static_cast<const char*>(descPtr) +
+                        offsetof(SectionDescriptor, fruText),
+                    20);
         EXPECT_EQ(
             memcmp(fruText.data(), expectedFruText.data(), fruText.size()), 0);
     }
 
-    template <ByteLike BodyType, StringOrVector CperType>
-    void expectValidSectionBody(
-        const CperType& cperArray,
-        const std::span<const BodyType> expectedSectionBody,
-        const uint32_t sectionOffset)
-    {
-        const uint64_t kSectionBodySize = expectedSectionBody.size();
-        ASSERT_GE(cperArray.size(), sectionOffset + kSectionBodySize);
-        CperType sectionBody(kSectionBodySize, 0);
-        memcpy(sectionBody.data(), &cperArray[sectionOffset],
-               sectionBody.size());
-
-        EXPECT_TRUE(isArrayEq(sectionBody.data(), expectedSectionBody.data(),
-                              sectionBody.size(), expectedSectionBody.size()));
-    }
-
     void expectEqTimestamp(
         const uint64_t cperTimestamp,
         const std::chrono::time_point<std::chrono::system_clock> ref,
@@ -273,23 +294,29 @@
     EXPECT_CALL(mockEncoder, mockCreateSectionDescriptor).Times(kNumSections);
     const std::vector<uint8_t> cperArray = mockEncoder.serializeToByteArray();
     ASSERT_EQ(cperArray.size(), kExpectedCperSize);
-    expectValidHeader(cperArray.data(), cperArray.size(), kNumSections,
-                      kExpectedCperSize, mockEncoder.timestamp);
+
+    CperDecoder decoder(cperArray);
+    expectValidHeader(decoder.getHeader(), kNumSections, kExpectedCperSize,
+                      mockEncoder.timestamp);
+
+    auto sections = decoder.getSections();
+    ASSERT_EQ(sections.size(), kNumSections);
 
     uint32_t expectedBodyOffset =
         kRecordHeaderSizeBytes + (kNumSections * kSectionDescriptorSizeBytes);
-    uint64_t sectionIdx = 0;
-    for (const std::string& body : sectionBodies)
+    for (size_t i = 0; i < kNumSections; ++i)
     {
-        const uint32_t kBodyLength = body.size();
-        expectValidSectionDescriptor(cperArray, sectionIdx, expectedBodyOffset,
+        const uint32_t kBodyLength = sectionBodies[i].size();
+        expectValidSectionDescriptor(sections[i].descriptor, expectedBodyOffset,
                                      kBodyLength, kMockSectionFlag,
                                      kMockSectionType);
 
-        expectValidSectionBody<char>(cperArray, body, expectedBodyOffset);
+        EXPECT_EQ(sections[i].body.size(), kBodyLength);
+        EXPECT_EQ(memcmp(sections[i].body.data(), sectionBodies[i].data(),
+                         kBodyLength),
+                  0);
 
         expectedBodyOffset += kBodyLength;
-        sectionIdx++;
     }
 
     // Serialize to a string.
@@ -297,24 +324,11 @@
     EXPECT_CALL(mockEncoder, mockCreateSectionDescriptor).Times(kNumSections);
     const std::string cperStr = mockEncoder.serializeToString();
     ASSERT_EQ(cperStr.size(), kExpectedCperSize);
-    expectValidHeader(cperStr.data(), cperStr.size(), kNumSections,
-                      kExpectedCperSize, mockEncoder.timestamp);
 
-    expectedBodyOffset =
-        kRecordHeaderSizeBytes + (kNumSections * kSectionDescriptorSizeBytes);
-    sectionIdx = 0;
-    for (const std::string& body : sectionBodies)
-    {
-        const uint32_t kBodyLength = body.size();
-        expectValidSectionDescriptor(cperStr, sectionIdx, expectedBodyOffset,
-                                     kBodyLength, kMockSectionFlag,
-                                     kMockSectionType);
-
-        expectValidSectionBody<char>(cperStr, body, expectedBodyOffset);
-
-        expectedBodyOffset += kBodyLength;
-        sectionIdx++;
-    }
+    CperDecoder strDecoder(std::span<const uint8_t>(
+        reinterpret_cast<const uint8_t*>(cperStr.data()), cperStr.size()));
+    expectValidHeader(strDecoder.getHeader(), kNumSections, kExpectedCperSize,
+                      mockEncoder.timestamp);
 }
 
 TEST_F(CperEncoderTest, EncodeOneU8SectionToString)
@@ -329,55 +343,32 @@
 
     EXPECT_CALL(mockEncoder, mockCreateRecordHeader).Times(1);
     EXPECT_CALL(mockEncoder, mockCreateSectionDescriptor).Times(kNumSections);
-    const std::string cperArray = mockEncoder.serializeToString();
+    const std::string cperStr = mockEncoder.serializeToString();
 
     const uint64_t kExpectedCperSize =
         (kSectionBodySize + kSectionDescriptorSizeBytes +
          kRecordHeaderSizeBytes);
 
-    ASSERT_EQ(cperArray.size(), kExpectedCperSize);
-    expectValidHeader(cperArray.data(), cperArray.size(), kNumSections,
-                      kExpectedCperSize, mockEncoder.timestamp);
+    ASSERT_EQ(cperStr.size(), kExpectedCperSize);
+
+    CperDecoder decoder(std::span<const uint8_t>(
+        reinterpret_cast<const uint8_t*>(cperStr.data()), cperStr.size()));
+    expectValidHeader(decoder.getHeader(), kNumSections, kExpectedCperSize,
+                      mockEncoder.timestamp);
+
+    auto sections = decoder.getSections();
+    ASSERT_EQ(sections.size(), kNumSections);
 
     const uint32_t kExpectedOffset =
         kRecordHeaderSizeBytes + (kNumSections * kSectionDescriptorSizeBytes);
-    expectValidSectionDescriptor(cperArray, 0, kExpectedOffset,
+    expectValidSectionDescriptor(sections[0].descriptor, kExpectedOffset,
                                  kSectionBodySize, kMockSectionFlag,
                                  kMockSectionType);
 
-    expectValidSectionBody<uint8_t>(cperArray, sectionBody, kExpectedOffset);
-}
-
-TEST_F(CperEncoderTest, EncodeOneCharSectionToArray)
-{
-    MockCperEncoder<char> mockEncoder;
-
-    const std::string sectionBody =
-        "Hello world, this is a simplistic example of a CPER Section Body.";
-    const uint64_t kSectionBodySize = sectionBody.size();
-    constexpr uint16_t kNumSections = 1;
-
-    mockEncoder.addSection(sectionBody, kMockSectionFlag, kMockSectionType);
-
-    EXPECT_CALL(mockEncoder, mockCreateRecordHeader).Times(1);
-    EXPECT_CALL(mockEncoder, mockCreateSectionDescriptor).Times(kNumSections);
-    const std::vector<uint8_t> cperArray = mockEncoder.serializeToByteArray();
-
-    const uint64_t kExpectedCperSize =
-        (kSectionBodySize + kSectionDescriptorSizeBytes +
-         kRecordHeaderSizeBytes);
-
-    ASSERT_EQ(cperArray.size(), kExpectedCperSize);
-    expectValidHeader(cperArray.data(), cperArray.size(), kNumSections,
-                      kExpectedCperSize, mockEncoder.timestamp);
-
-    const uint32_t kExpectedOffset =
-        kRecordHeaderSizeBytes + (kNumSections * kSectionDescriptorSizeBytes);
-    expectValidSectionDescriptor(cperArray, 0, kExpectedOffset,
-                                 kSectionBodySize, kMockSectionFlag,
-                                 kMockSectionType);
-
-    expectValidSectionBody<char>(cperArray, sectionBody, kExpectedOffset);
+    EXPECT_EQ(sections[0].body.size(), kSectionBodySize);
+    EXPECT_EQ(
+        memcmp(sections[0].body.data(), sectionBody.data(), kSectionBodySize),
+        0);
 }
 
 TEST_F(CperEncoderTest, SeqMemcopy)
@@ -416,21 +407,32 @@
 TEST_F(CperEncoderTest, SmokeTest)
 {
     RecordHeader recordHeader;
-    const auto signature = getField<std::array<uint8_t, kRecordSignatureSize>>(
-        &recordHeader, offsetof(RecordHeader, signature));
+    const void* headerPtr = &recordHeader;
+
+    std::array<uint8_t, kRecordSignatureSize> signature;
+    std::memcpy(signature.data(),
+                static_cast<const char*>(headerPtr) +
+                    offsetof(RecordHeader, signature),
+                kRecordSignatureSize);
+
     EXPECT_EQ(
         memcmp(signature.data(), kRecordSignature.data(), kRecordSignatureSize),
         0);
-    EXPECT_EQ(
-        getField<uint16_t>(&recordHeader, offsetof(RecordHeader, revision)),
-        0x0000);
-    EXPECT_EQ(
-        getField<uint32_t>(&recordHeader, offsetof(RecordHeader, signatureEnd)),
-        0xffffffff);
+    EXPECT_EQ(littleEndianLoad<uint16_t>(static_cast<const char*>(headerPtr) +
+                                         offsetof(RecordHeader, revision)),
+              0x0000);
+    EXPECT_EQ(littleEndianLoad<uint32_t>(static_cast<const char*>(headerPtr) +
+                                         offsetof(RecordHeader, signatureEnd)),
+              0xffffffff);
+
     std::array<uint8_t, 12> expectedReserved = {0};
-    const auto reserved = getField<std::array<uint8_t, 12>>(
-        &recordHeader, offsetof(RecordHeader, reserved));
+    std::array<uint8_t, 12> reserved;
+    std::memcpy(reserved.data(),
+                static_cast<const char*>(headerPtr) +
+                    offsetof(RecordHeader, reserved),
+                12);
     EXPECT_EQ(memcmp(reserved.data(), expectedReserved.data(), reserved.size()),
               0);
 }
+
 } // namespace uefi::cper
diff --git a/test/meson.build b/test/meson.build
index 1fdc537..66ea45e 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -15,12 +15,14 @@
 
 default_test_deps = [
   gmock_dep,
+  absl_endian_dep,
 ]
 
 test_cpp_args = ['-Db_sanitize=address,undefined,thread']
 
 tests = [
     'cper_encoder_test',
+    'cper_decoder_test',
 ]
 
 foreach t : tests