FruUtils: Add support for finding FRU at an offset

This change adds findFRUHeader which can detect non-IPMI headers to
identify the offset for IPMI FRU data. An implementation for "$TYAN$"
has been added. The new findFRUHeader is now used by readFRUContents
to generically read IPMI FRU both at offset 0 and at a offset as
determined by findFRUHeader.

Signed-off-by: Oskar Senft <osk@google.com>
Change-Id: I4325aac7737db59aaa11359ad7a0d575957f16e7
diff --git a/include/FruUtils.hpp b/include/FruUtils.hpp
index 74f80e4..906b77e 100644
--- a/include/FruUtils.hpp
+++ b/include/FruUtils.hpp
@@ -126,6 +126,23 @@
 
 ssize_t getFieldLength(uint8_t fruFieldTypeLenValue);
 
+/// \brief Find a FRU header.
+/// \param flag the flag required for raw i2c
+/// \param file the open file handle
+/// \param address the i2c device address
+/// \param readBlock a read method
+/// \param errorHelp and a helper string for failures
+/// \param blockData buffer to return the last read block
+/// \param baseOffset the offset to start the search at;
+///        set to 0 to perform search;
+///        returns the offset at which a header was found
+/// \return whether a header was found
+bool findFRUHeader(int flag, int file, uint16_t address,
+                   const ReadBlockFunc& readBlock,
+                   const std::string& errorHelp,
+                   std::array<uint8_t, I2C_SMBUS_BLOCK_MAX>& blockData,
+                   uint16_t& baseOffset);
+
 /// \brief Read and validate FRU contents.
 /// \param flag the flag required for raw i2c
 /// \param file the open file handle
diff --git a/src/FruUtils.cpp b/src/FruUtils.cpp
index b8b1905..940f8c8 100644
--- a/src/FruUtils.cpp
+++ b/src/FruUtils.cpp
@@ -586,26 +586,61 @@
     return true;
 }
 
+bool findFRUHeader(int flag, int file, uint16_t address,
+                   const ReadBlockFunc& readBlock,
+                   const std::string& errorHelp,
+                   std::array<uint8_t, I2C_SMBUS_BLOCK_MAX>& blockData,
+                   uint16_t& baseOffset)
+{
+    if (readBlock(flag, file, address, baseOffset, 0x8, blockData.data()) < 0)
+    {
+        std::cerr << "failed to read " << errorHelp << " base offset "
+            << baseOffset << "\n";
+        return false;
+    }
+
+    // check the header checksum
+    if (validateHeader(blockData))
+    {
+        return true;
+    }
+
+    // only continue the search if we just looked at 0x0.
+    if (baseOffset != 0) {
+        return false;
+    }
+
+    // now check for special cases where the IPMI data is at an offset
+
+    // check if blockData starts with tyanHeader
+    const std::vector<uint8_t> tyanHeader = {'$', 'T', 'Y', 'A', 'N', '$'};
+    if (blockData.size() >= tyanHeader.size() &&
+        std::equal(tyanHeader.begin(), tyanHeader.end(), blockData.begin()))
+    {
+        // look for the FRU header at offset 0x6000
+        baseOffset = 0x6000;
+        return findFRUHeader(flag, file, address, readBlock, errorHelp,
+            blockData, baseOffset);
+    }
+
+    if (debug)
+    {
+        std::cerr << "Illegal header " << errorHelp << " base offset "
+            << baseOffset << "\n";
+    }
+
+    return false;
+}
+
 std::vector<uint8_t> readFRUContents(int flag, int file, uint16_t address,
                                      const ReadBlockFunc& readBlock,
                                      const std::string& errorHelp)
 {
     std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    uint16_t baseOffset = 0x0;
 
-    if (readBlock(flag, file, address, 0x0, 0x8, blockData.data()) < 0)
-    {
-        std::cerr << "failed to read " << errorHelp << "\n";
-        return {};
-    }
-
-    // check the header checksum
-    if (!validateHeader(blockData))
-    {
-        if (debug)
-        {
-            std::cerr << "Illegal header " << errorHelp << "\n";
-        }
-
+    if (!findFRUHeader(flag, file, address, readBlock, errorHelp,
+            blockData, baseOffset)) {
         return {};
     }
 
@@ -648,10 +683,12 @@
 
         areaOffset *= fruBlockSize;
 
-        if (readBlock(flag, file, address, static_cast<uint16_t>(areaOffset),
+        if (readBlock(flag, file, address,
+                      baseOffset + static_cast<uint16_t>(areaOffset),
                       0x2, blockData.data()) < 0)
         {
-            std::cerr << "failed to read " << errorHelp << "\n";
+            std::cerr << "failed to read " << errorHelp << " base offset "
+                << baseOffset << "\n";
             return {};
         }
 
@@ -679,10 +716,11 @@
             // In multi-area, the area offset points to the 0th record, each
             // record has 3 bytes of the header we care about.
             if (readBlock(flag, file, address,
-                          static_cast<uint16_t>(areaOffset), 0x3,
+                          baseOffset + static_cast<uint16_t>(areaOffset), 0x3,
                           blockData.data()) < 0)
             {
-                std::cerr << "failed to read " << errorHelp << "\n";
+                std::cerr << "failed to read " << errorHelp << " base offset "
+                    << baseOffset << "\n";
                 return {};
             }
 
@@ -710,11 +748,13 @@
         size_t requestLength =
             std::min(static_cast<size_t>(I2C_SMBUS_BLOCK_MAX), fruLength);
 
-        if (readBlock(flag, file, address, static_cast<uint16_t>(readOffset),
+        if (readBlock(flag, file, address,
+                      baseOffset + static_cast<uint16_t>(readOffset),
                       static_cast<uint8_t>(requestLength),
                       blockData.data()) < 0)
         {
-            std::cerr << "failed to read " << errorHelp << "\n";
+            std::cerr << "failed to read " << errorHelp << " base offset "
+                << baseOffset << "\n";
             return {};
         }
 
diff --git a/test/test_fru-utils.cpp b/test/test_fru-utils.cpp
index d2f0e36..2984094 100644
--- a/test/test_fru-utils.cpp
+++ b/test/test_fru-utils.cpp
@@ -1,6 +1,8 @@
 #include "FruUtils.hpp"
 
 #include <array>
+#include<algorithm>
+#include<iterator>
 
 #include "gtest/gtest.h"
 
@@ -142,3 +144,101 @@
 
     EXPECT_EQ(calculateChecksum(data), 255);
 }
+
+int64_t getDataTempl(
+    const std::vector<uint8_t>& data,
+    [[maybe_unused]] int flag,
+    [[maybe_unused]] int file,
+    [[maybe_unused]] uint16_t address,
+    uint16_t offset, uint8_t length, uint8_t* outBuf)
+{
+    if (offset >= data.size())
+    {
+        return 0;
+    }
+
+    uint16_t idx;
+    for (idx = offset;
+         idx < data.size() && idx < offset + length;
+         ++idx, ++outBuf)
+    {
+        *outBuf = data[idx];
+    }
+
+    return idx - offset;
+}
+
+TEST(FindFRUHeaderTest, InvalidHeader)
+{
+    const std::vector<uint8_t> data = {255, 16};
+    uint16_t offset = 0;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    auto getData = [&data](auto fl, auto fi, auto a, auto o, auto l,
+        auto* b) { return getDataTempl(data, fl, fi, a, o, l, b); };
+
+    EXPECT_FALSE(findFRUHeader(0, 0, 0, getData, "error", blockData, offset));
+}
+
+TEST(FindFRUHeaderTest, NoData)
+{
+    const std::vector<uint8_t> data = {};
+    uint16_t offset = 0;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    auto getData = [&data](auto fl, auto fi, auto a, auto o, auto l,
+        auto* b) { return getDataTempl(data, fl, fi, a, o, l, b); };
+
+    EXPECT_FALSE(findFRUHeader(0, 0, 0, getData, "error", blockData, offset));
+}
+
+TEST(FindFRUHeaderTest, ValidHeader)
+{
+    const std::vector<uint8_t> data = {
+        0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0xf5};
+    uint16_t offset = 0;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    auto getData = [&data](auto fl, auto fi, auto a, auto o, auto l,
+        auto* b) { return getDataTempl(data, fl, fi, a, o, l, b); };
+
+    EXPECT_TRUE(findFRUHeader(0, 0, 0, getData, "error", blockData, offset));
+    EXPECT_EQ(0, offset);
+}
+
+TEST(FindFRUHeaderTest, TyanInvalidHeader)
+{
+    std::vector<uint8_t> data = {'$', 'T', 'Y', 'A', 'N', '$', 0, 0};
+    data.resize(0x6000 + I2C_SMBUS_BLOCK_MAX);
+    uint16_t offset = 0;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    auto getData = [&data](auto fl, auto fi, auto a, auto o, auto l,
+        auto* b) { return getDataTempl(data, fl, fi, a, o, l, b); };
+
+    EXPECT_FALSE(findFRUHeader(0, 0, 0, getData, "error", blockData, offset));
+}
+
+TEST(FindFRUHeaderTest, TyanNoData)
+{
+    const std::vector<uint8_t> data = {'$', 'T', 'Y', 'A', 'N', '$', 0, 0};
+    uint16_t offset = 0;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    auto getData = [&data](auto fl, auto fi, auto a, auto o, auto l,
+        auto* b) { return getDataTempl(data, fl, fi, a, o, l, b); };
+
+    EXPECT_FALSE(findFRUHeader(0, 0, 0, getData, "error", blockData, offset));
+}
+
+TEST(FindFRUHeaderTest, TyanValidHeader)
+{
+    std::vector<uint8_t> data = {'$', 'T', 'Y', 'A', 'N', '$', 0, 0};
+    data.resize(0x6000);
+    constexpr std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> fruHeader = {
+        0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0xf5};
+    copy(fruHeader.begin(), fruHeader.end(), back_inserter(data));
+
+    uint16_t offset = 0;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData;
+    auto getData = [&data](auto fl, auto fi, auto a, auto o, auto l,
+        auto* b) { return getDataTempl(data, fl, fi, a, o, l, b); };
+
+    EXPECT_TRUE(findFRUHeader(0, 0, 0, getData, "error", blockData, offset));
+    EXPECT_EQ(0x6000, offset);
+}