Allow decoding MAC addresses in some FRUs

Some motherboards store their mac address in a zlib compressed chunk at
a known location in the FRU.  Decode that section, and pull the mac
address into the appropriate field.

This requires some refactoring so that the indexing can now have the
indexes passed through the various parse functions.

To use this functionality requires the use of libxml and zlib, which are
added as new dependencies.

Change-Id: Icb5c2e46e2a08ca83b3559892169ee2b3f319b2e
Signed-off-by: Ed Tanous <etanous@nvidia.com>
diff --git a/src/fru_device/fru_utils.cpp b/src/fru_device/fru_utils.cpp
index c9669de..efb4ec1 100644
--- a/src/fru_device/fru_utils.cpp
+++ b/src/fru_device/fru_utils.cpp
@@ -3,6 +3,8 @@
 
 #include "fru_utils.hpp"
 
+#include "gzip_utils.hpp"
+
 #include <phosphor-logging/lg2.hpp>
 
 #include <array>
@@ -717,27 +719,66 @@
     return true;
 }
 
-bool findFRUHeader(FRUReader& reader, const std::string& errorHelp,
-                   std::array<uint8_t, I2C_SMBUS_BLOCK_MAX>& blockData,
-                   off_t& baseOffset)
+std::string parseMacFromGzipXmlHeader(FRUReader& reader, off_t offset)
 {
-    if (reader.read(baseOffset, 0x8, blockData.data()) < 0)
+    // gzip starts at offset 512. Read that from the FRU
+    // in this case, 32k bytes is enough to hold the whole manifest
+    constexpr size_t totalReadSize = 32UL * 1024UL;
+
+    std::vector<uint8_t> headerData(totalReadSize, 0U);
+
+    int rc = reader.read(offset, totalReadSize, headerData.data());
+    if (rc <= 0)
+    {
+        return {};
+    }
+
+    std::optional<std::string> xml = gzipInflate(headerData);
+    if (!xml)
+    {
+        return {};
+    }
+    std::vector<std::string> node = getNodeFromXml(
+        *xml, "/GSSKU/BoardInfo/Main/NIC/*[Mode = 'Dedicated']/MacAddr0");
+    if (node.empty())
+    {
+        lg2::debug("No mac address found in gzip xml header");
+        return {};
+    }
+    if (node.size() > 1)
+    {
+        lg2::warning("Multiple mac addresses found in gzip xml header");
+    }
+    return node[0];
+}
+
+std::optional<FruSections> findFRUHeader(
+    FRUReader& reader, const std::string& errorHelp, off_t startingOffset)
+{
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData = {};
+    if (reader.read(startingOffset, 0x8, blockData.data()) < 0)
     {
         lg2::error("failed to read {ERR} base offset {OFFSET}", "ERR",
-                   errorHelp, "OFFSET", baseOffset);
-        return false;
+                   errorHelp, "OFFSET", startingOffset);
+        return std::nullopt;
     }
 
     // check the header checksum
     if (validateHeader(blockData))
     {
-        return true;
+        FruSections fru = {};
+        static_assert(fru.ipmiFruBlock.size() == blockData.size(),
+                      "size mismatch in block data");
+        std::memcpy(fru.ipmiFruBlock.data(), blockData.data(),
+                    I2C_SMBUS_BLOCK_MAX);
+        fru.IpmiFruOffset = startingOffset;
+        return fru;
     }
 
     // only continue the search if we just looked at 0x0.
-    if (baseOffset != 0)
+    if (startingOffset != 0)
     {
-        return false;
+        return std::nullopt;
     }
 
     // now check for special cases where the IPMI data is at an offset
@@ -748,8 +789,8 @@
         std::equal(tyanHeader.begin(), tyanHeader.end(), blockData.begin()))
     {
         // look for the FRU header at offset 0x6000
-        baseOffset = 0x6000;
-        return findFRUHeader(reader, errorHelp, blockData, baseOffset);
+        off_t tyanOffset = 0x6000;
+        return findFRUHeader(reader, errorHelp, tyanOffset);
     }
 
     // check if blockData starts with gigabyteHeader
@@ -760,27 +801,39 @@
                    blockData.begin()))
     {
         // look for the FRU header at offset 0x4000
-        baseOffset = 0x4000;
-        return findFRUHeader(reader, errorHelp, blockData, baseOffset);
+        off_t gbOffset = 0x4000;
+        auto sections = findFRUHeader(reader, errorHelp, gbOffset);
+        if (sections)
+        {
+            lg2::debug("succeeded on GB parse");
+            // GB xml header is at 512 bytes
+            sections->GigabyteXmlOffset = 512;
+        }
+        else
+        {
+            lg2::error("Failed on GB parse");
+        }
+        return sections;
     }
 
     lg2::debug("Illegal header {HEADER} base offset {OFFSET}", "HEADER",
-               errorHelp, "OFFSET", baseOffset);
+               errorHelp, "OFFSET", startingOffset);
 
-    return false;
+    return std::nullopt;
 }
 
 std::pair<std::vector<uint8_t>, bool> readFRUContents(
     FRUReader& reader, const std::string& errorHelp)
 {
     std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
-    off_t baseOffset = 0x0;
-
-    if (!findFRUHeader(reader, errorHelp, blockData, baseOffset))
+    std::optional<FruSections> sections = findFRUHeader(reader, errorHelp, 0);
+    if (!sections)
     {
         return {{}, false};
     }
-
+    const off_t baseOffset = sections->IpmiFruOffset;
+    std::memcpy(blockData.data(), sections->ipmiFruBlock.data(),
+                blockData.size());
     std::vector<uint8_t> device;
     device.insert(device.end(), blockData.begin(),
                   std::next(blockData.begin(), 8));
@@ -897,6 +950,20 @@
         fruLength -= std::min(requestLength, fruLength);
     }
 
+    if (sections->GigabyteXmlOffset != 0)
+    {
+        std::string macAddress =
+            parseMacFromGzipXmlHeader(reader, sections->GigabyteXmlOffset);
+        if (!macAddress.empty())
+        {
+            // launder the mac address as we expect into
+            // BOARD_INFO_AM2 to allow the rest of the
+            // system to use it
+            std::string mac = std::format("MAC: {}", macAddress);
+            updateAddProperty(mac, "BOARD_INFO_AM2", device);
+        }
+    }
+
     return {device, true};
 }