bugfix: frudevice: handle multi-record area

The multi-record area doesn't start with a leading header like the other
areas, so it needs to be handled differently.  It is comprised of
individual records each with their own 5 byte headers.  This processes
the multi-record area separately if it's detected.

Tested: Verified this properly handles the multi-record area by
independent FRU content validation from the returned IPMI response.
This is what was used to detect the bug in the first place.
Tested: Verified FRUs without the area are still processed fine.

Signed-off-by: Patrick Venture <venture@google.com>
Change-Id: Ibd119a87d79ed71463e1dd62e1e1dfd0322e2268
diff --git a/src/FruDevice.cpp b/src/FruDevice.cpp
index 7a46311..4b6d7c2 100644
--- a/src/FruDevice.cpp
+++ b/src/FruDevice.cpp
@@ -32,6 +32,7 @@
 #include <future>
 #include <iomanip>
 #include <iostream>
+#include <limits>
 #include <nlohmann/json.hpp>
 #include <regex>
 #include <sdbusplus/asio/connection.hpp>
@@ -113,6 +114,7 @@
     std::vector<char> device;
     device.insert(device.end(), blockData.begin(), blockData.begin() + 8);
 
+    bool hasMultiRecords = false;
     int fruLength = 0;
     for (size_t jj = 1; jj <= FRU_AREAS.size(); jj++)
     {
@@ -124,6 +126,14 @@
             continue;
         }
 
+        // MultiRecords are different.  jj is not tracking section, it's walking
+        // the common header.
+        if (std::string(FRU_AREAS[jj - 1]) == std::string("MULTIRECORD"))
+        {
+            hasMultiRecords = true;
+            break;
+        }
+
         areaOffset *= 8;
 
         if (readBlock(flag, file, static_cast<uint16_t>(areaOffset), 0x2,
@@ -139,6 +149,43 @@
         fruLength = (areaOffset > fruLength) ? areaOffset : fruLength;
     }
 
+    if (hasMultiRecords)
+    {
+        // device[area count] is the index to the last area because the 0th
+        // entry is not an offset in the common header.
+        int areaOffset = device[FRU_AREAS.size()];
+        areaOffset *= 8;
+
+        // the multi-area record header is 5 bytes long.
+        constexpr int multiRecordHeaderSize = 5;
+        constexpr int multiRecordEndOfList = 0x80;
+
+        // Sanity hard-limit to 64KB.
+        while (areaOffset < std::numeric_limits<uint16_t>::max())
+        {
+            // 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, static_cast<uint16_t>(areaOffset), 0x3,
+                          blockData.data()) < 0)
+            {
+                std::cerr << "failed to read " << errorHelp << "\n";
+                return {};
+            }
+
+            // Ok, let's check the record length, which is in bytes (unsigned,
+            // up to 255, so blockData should hold uint8_t not char)
+            int recordLength = blockData[2];
+            areaOffset += (recordLength + multiRecordHeaderSize);
+            fruLength = (areaOffset > fruLength) ? areaOffset : fruLength;
+
+            // If this is the end of the list bail.
+            if ((blockData[1] & multiRecordEndOfList))
+            {
+                break;
+            }
+        }
+    }
+
     // You already copied these first 8 bytes (the ipmi fru header size)
     fruLength -= 8;