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/meson.build b/meson.build
index dbce245..c364a9e 100644
--- a/meson.build
+++ b/meson.build
@@ -38,6 +38,8 @@
nlohmann_json_dep = dependency('nlohmann_json', include_type: 'system')
sdbusplus = dependency('sdbusplus')
phosphor_logging_dep = dependency('phosphor-logging')
+zlib_dep = dependency('zlib', include_type: 'system')
+libxml2_dep = dependency('libxml-2.0', include_type: 'system')
if get_option('gpio-presence') or get_option('tests').allowed()
libgpio_dep = dependency(
diff --git a/src/fru_device/fru_device.cpp b/src/fru_device/fru_device.cpp
index 324afcd..2363477 100644
--- a/src/fru_device/fru_device.cpp
+++ b/src/fru_device/fru_device.cpp
@@ -958,7 +958,6 @@
return false;
}
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
std::string errorMessage = "eeprom at " + std::to_string(bus) +
" address " + std::to_string(address);
auto readFunc = [eeprom](off_t offset, size_t length, uint8_t* outbuf) {
@@ -966,10 +965,15 @@
};
FRUReader reader(std::move(readFunc));
- if (!findFRUHeader(reader, errorMessage, blockData, offset))
+ auto sections = findFRUHeader(reader, errorMessage, 0);
+ if (!sections)
{
offset = 0;
}
+ else
+ {
+ offset = sections->IpmiFruOffset;
+ }
if (lseek(eeprom, offset, SEEK_SET) < 0)
{
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};
}
diff --git a/src/fru_device/fru_utils.hpp b/src/fru_device/fru_utils.hpp
index 0491b30..d689d5c 100644
--- a/src/fru_device/fru_utils.hpp
+++ b/src/fru_device/fru_utils.hpp
@@ -126,6 +126,14 @@
ssize_t getFieldLength(uint8_t fruFieldTypeLenValue);
+struct FruSections
+{
+ off_t IpmiFruOffset = 0;
+ std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> ipmiFruBlock;
+
+ off_t GigabyteXmlOffset = 0;
+};
+
/// \brief Find a FRU header.
/// \param reader the FRUReader to read via
/// \param errorHelp and a helper string for failures
@@ -134,9 +142,8 @@
/// set to 0 to perform search;
/// returns the offset at which a header was found
/// \return whether a header was found
-bool findFRUHeader(FRUReader& reader, const std::string& errorHelp,
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX>& blockData,
- off_t& baseOffset);
+std::optional<FruSections> findFRUHeader(
+ FRUReader& reader, const std::string& errorHelp, off_t offset);
/// \brief Read and validate FRU contents.
/// \param reader the FRUReader to read via
@@ -229,4 +236,6 @@
bool updateAddProperty(const std::string& propertyValue,
const std::string& propertyName,
- std::vector<uint8_t>& data);
+ std::vector<uint8_t>& fruData);
+
+std::string parseMacFromGzipXmlHeader(FRUReader& reader, off_t offset);
diff --git a/src/fru_device/gzip_utils.cpp b/src/fru_device/gzip_utils.cpp
new file mode 100644
index 0000000..430fb91
--- /dev/null
+++ b/src/fru_device/gzip_utils.cpp
@@ -0,0 +1,111 @@
+#include "gzip_utils.hpp"
+
+#include <libxml/parser.h>
+#include <libxml/xpath.h>
+#include <unistd.h>
+#include <zlib.h>
+
+#include <optional>
+#include <span>
+#include <string>
+#include <vector>
+
+std::optional<std::string> gzipInflate(std::span<uint8_t> compressedBytes)
+{
+ std::string uncompressedBytes;
+ if (compressedBytes.empty())
+ {
+ return std::nullopt;
+ }
+
+ z_stream strm{
+
+ };
+ strm.next_in = (Bytef*)compressedBytes.data();
+ strm.avail_in = static_cast<uInt>(compressedBytes.size());
+ strm.total_out = 0;
+
+ if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK)
+ {
+ return std::nullopt;
+ }
+
+ while (strm.avail_in > 0)
+ {
+ constexpr size_t chunkSize = 1024;
+ uncompressedBytes.resize(uncompressedBytes.size() + chunkSize);
+ strm.next_out =
+ std::bit_cast<Bytef*>(uncompressedBytes.end() - chunkSize);
+ strm.avail_out = chunkSize;
+
+ // Inflate another chunk.
+ int err = inflate(&strm, Z_SYNC_FLUSH);
+ if (err == Z_STREAM_END)
+ {
+ break;
+ }
+ if (err != Z_OK)
+ {
+ return std::nullopt;
+ }
+ }
+
+ if (inflateEnd(&strm) != Z_OK)
+ {
+ return std::nullopt;
+ }
+ uncompressedBytes.resize(strm.total_out);
+
+ return {uncompressedBytes};
+}
+
+static std::vector<std::string> xpathText(xmlDocPtr doc, const char* xp)
+{
+ std::vector<std::string> val;
+ xmlXPathContextPtr ctx = xmlXPathNewContext(doc);
+ if (ctx == nullptr)
+ {
+ return val;
+ }
+ const unsigned char* xpptr = std::bit_cast<const unsigned char*>(xp);
+ xmlXPathObjectPtr obj = xmlXPathEvalExpression(xpptr, ctx);
+ if (obj != nullptr)
+ {
+ if (obj->type == XPATH_NODESET && obj->nodesetval != nullptr)
+ {
+ xmlNodeSetPtr nodeTab = obj->nodesetval;
+ size_t nodeNr = static_cast<size_t>(nodeTab->nodeNr);
+ std::span<xmlNodePtr> nodes{nodeTab->nodeTab, nodeNr};
+ for (xmlNodePtr node : nodes)
+ {
+ unsigned char* keyword = xmlNodeGetContent(node);
+ val.emplace_back(std::bit_cast<const char*>(keyword));
+ xmlFree(keyword);
+ }
+ }
+ }
+
+ xmlXPathFreeObject(obj);
+ xmlXPathFreeContext(ctx);
+ return val;
+}
+
+std::vector<std::string> getNodeFromXml(std::string_view xml,
+ const char* nodeName)
+{
+ std::vector<std::string> node;
+ if (xml.empty())
+ {
+ return node;
+ }
+ xmlDocPtr doc = xmlReadMemory(
+ xml.data(), xml.size(), nullptr, nullptr,
+ XML_PARSE_RECOVER | XML_PARSE_NONET | XML_PARSE_NOWARNING);
+ if (doc == nullptr)
+ {
+ return {};
+ }
+ node = xpathText(doc, nodeName);
+ xmlFreeDoc(doc);
+ return node;
+}
diff --git a/src/fru_device/gzip_utils.hpp b/src/fru_device/gzip_utils.hpp
new file mode 100644
index 0000000..db94a89
--- /dev/null
+++ b/src/fru_device/gzip_utils.hpp
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
+
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <optional>
+#include <span>
+#include <string>
+#include <vector>
+
+std::optional<std::string> gzipInflate(std::span<uint8_t> compressedBytes);
+
+std::vector<std::string> getNodeFromXml(std::string_view xml,
+ const char* nodeName);
diff --git a/src/fru_device/meson.build b/src/fru_device/meson.build
index 0197f77..b99d0d8 100644
--- a/src/fru_device/meson.build
+++ b/src/fru_device/meson.build
@@ -13,17 +13,20 @@
'fru-device',
'fru_device.cpp',
'../utils.cpp',
- 'fru_utils.cpp',
'fru_reader.cpp',
+ 'fru_utils.cpp',
+ 'gzip_utils.cpp',
cpp_args: cpp_args_fd,
dependencies: [
boost,
i2c,
+ libxml2_dep,
nlohmann_json_dep,
phosphor_logging_dep,
sdbusplus,
threads,
valijson,
+ zlib_dep,
],
install: true,
install_dir: installdir,
diff --git a/test/meson.build b/test/meson.build
index adb6097..5282b07 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -23,10 +23,20 @@
executable(
'test_fru_utils',
'test_fru-utils.cpp',
+ 'test_gzip-utils.cpp',
'../src/fru_device/fru_utils.cpp',
'../src/fru_device/fru_reader.cpp',
+ '../src/fru_device/gzip_utils.cpp',
cpp_args: test_boost_args,
- dependencies: [boost, gtest, gmock, phosphor_logging_dep, sdbusplus],
+ dependencies: [
+ boost,
+ gmock,
+ gtest,
+ libxml2_dep,
+ phosphor_logging_dep,
+ sdbusplus,
+ zlib_dep,
+ ],
include_directories: test_include_dir,
),
)
diff --git a/test/test_fru-utils.cpp b/test/test_fru-utils.cpp
index acd712d..f925db2 100644
--- a/test/test_fru-utils.cpp
+++ b/test/test_fru-utils.cpp
@@ -151,8 +151,8 @@
EXPECT_EQ(calculateChecksum(data), 255);
}
-int64_t getDataTempl(const std::vector<uint8_t>& data, off_t offset,
- size_t length, uint8_t* outBuf)
+int64_t getDataTempl(std::span<const uint8_t> data, off_t offset, size_t length,
+ uint8_t* outBuf)
{
if (offset >= static_cast<off_t>(data.size()))
{
@@ -311,26 +311,25 @@
{
const std::vector<uint8_t> data = {255, 16};
off_t offset = 0;
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
auto getData = [&data](auto o, auto l, auto* b) {
return getDataTempl(data, o, l, b);
};
FRUReader reader(getData);
-
- EXPECT_FALSE(findFRUHeader(reader, "error", blockData, offset));
+ auto sections = findFRUHeader(reader, "error", offset);
+ EXPECT_EQ(sections, std::nullopt);
}
TEST(FindFRUHeaderTest, NoData)
{
const std::vector<uint8_t> data = {};
off_t offset = 0;
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
auto getData = [&data](auto o, auto l, auto* b) {
return getDataTempl(data, o, l, b);
};
FRUReader reader(getData);
- EXPECT_FALSE(findFRUHeader(reader, "error", blockData, offset));
+ auto sections = findFRUHeader(reader, "error", offset);
+ EXPECT_EQ(sections, std::nullopt);
}
TEST(FindFRUHeaderTest, ValidHeader)
@@ -338,14 +337,16 @@
const std::vector<uint8_t> data = {0x01, 0x00, 0x01, 0x02,
0x03, 0x04, 0x00, 0xf5};
off_t offset = 0;
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
auto getData = [&data](auto o, auto l, auto* b) {
return getDataTempl(data, o, l, b);
};
FRUReader reader(getData);
-
- EXPECT_TRUE(findFRUHeader(reader, "error", blockData, offset));
- EXPECT_EQ(0, offset);
+ auto sections = findFRUHeader(reader, "error", offset);
+ ASSERT_NE(sections, std::nullopt);
+ if (sections)
+ {
+ EXPECT_EQ(0, sections->IpmiFruOffset);
+ }
}
TEST(FindFRUHeaderTest, TyanInvalidHeader)
@@ -353,26 +354,24 @@
std::vector<uint8_t> data = {'$', 'T', 'Y', 'A', 'N', '$', 0, 0};
data.resize(0x6000 + I2C_SMBUS_BLOCK_MAX);
off_t offset = 0;
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
auto getData = [&data](auto o, auto l, auto* b) {
return getDataTempl(data, o, l, b);
};
FRUReader reader(getData);
-
- EXPECT_FALSE(findFRUHeader(reader, "error", blockData, offset));
+ auto sections = findFRUHeader(reader, "error", offset);
+ EXPECT_EQ(sections, std::nullopt);
}
TEST(FindFRUHeaderTest, TyanNoData)
{
const std::vector<uint8_t> data = {'$', 'T', 'Y', 'A', 'N', '$', 0, 0};
off_t offset = 0;
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
auto getData = [&data](auto o, auto l, auto* b) {
return getDataTempl(data, o, l, b);
};
FRUReader reader(getData);
-
- EXPECT_FALSE(findFRUHeader(reader, "error", blockData, offset));
+ auto sections = findFRUHeader(reader, "error", offset);
+ EXPECT_EQ(sections, std::nullopt);
}
TEST(FindFRUHeaderTest, TyanValidHeader)
@@ -384,14 +383,67 @@
copy(fruHeader.begin(), fruHeader.end(), back_inserter(data));
off_t offset = 0;
- std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> blockData{};
auto getData = [&data](auto o, auto l, auto* b) {
return getDataTempl(data, o, l, b);
};
FRUReader reader(getData);
- EXPECT_TRUE(findFRUHeader(reader, "error", blockData, offset));
- EXPECT_EQ(0x6000, offset);
+ auto sections = findFRUHeader(reader, "error", offset);
+
+ ASSERT_NE(sections, std::nullopt);
+ if (sections)
+ {
+ EXPECT_EQ(0x6000, sections->IpmiFruOffset);
+ }
+}
+
+TEST(FindFRUHeaderTest, GigaInvalidHeader)
+{
+ std::vector<uint8_t> data = {'G', 'I', 'G', 'A', 'B', 'Y', 'T', 'E'};
+ data.resize(0x6000 + I2C_SMBUS_BLOCK_MAX);
+ off_t offset = 0;
+ auto getData = [&data](auto o, auto l, auto* b) {
+ return getDataTempl(data, o, l, b);
+ };
+ FRUReader reader(getData);
+ auto sections = findFRUHeader(reader, "error", offset);
+ EXPECT_EQ(sections, std::nullopt);
+}
+
+TEST(FindFRUHeaderTest, GigaNoData)
+{
+ const std::vector<uint8_t> data = {'G', 'I', 'G', 'A', 'B', 'Y', 'T', 'E'};
+ off_t offset = 0;
+ auto getData = [&data](auto o, auto l, auto* b) {
+ return getDataTempl(data, o, l, b);
+ };
+ FRUReader reader(getData);
+ auto sections = findFRUHeader(reader, "error", offset);
+ EXPECT_EQ(sections, std::nullopt);
+}
+
+TEST(FindFRUHeaderTest, GigaValidHeader)
+{
+ std::vector<uint8_t> data = {'G', 'I', 'G', 'A', 'B', 'Y', 'T', 'E'};
+ data.resize(0x4000);
+ 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));
+
+ off_t offset = 0;
+ auto getData = [&data](auto o, auto l, auto* b) {
+ return getDataTempl(data, o, l, b);
+ };
+ FRUReader reader(getData);
+
+ auto sections = findFRUHeader(reader, "error", offset);
+
+ ASSERT_NE(sections, std::nullopt);
+ if (sections)
+ {
+ EXPECT_EQ(0x4000, sections->IpmiFruOffset);
+ EXPECT_EQ(512, sections->GigabyteXmlOffset);
+ }
}
TEST(formatIPMIFRU, FullDecode)
@@ -611,3 +663,32 @@
std::vector<uint8_t> fruData;
EXPECT_FALSE(assembleFruData(fruData, areas));
}
+
+constexpr auto gzip = std::to_array<uint8_t>(
+ {0x1f, 0x8b, 0x08, 0x08, 0x74, 0x47, 0xe4, 0x68, 0x00, 0x03, 0x66, 0x72,
+ 0x75, 0x2e, 0x62, 0x69, 0x6e, 0x00, 0x9d, 0x91, 0xdf, 0x0a, 0x82, 0x30,
+ 0x18, 0xc5, 0xef, 0x7d, 0x8a, 0xe1, 0x7d, 0x4d, 0xad, 0x0b, 0x1b, 0x73,
+ 0x52, 0x9a, 0x21, 0x51, 0x37, 0xcb, 0x07, 0x18, 0x6e, 0xea, 0xa0, 0x36,
+ 0x58, 0x12, 0x3d, 0x7e, 0x29, 0x5a, 0x5a, 0x08, 0xd1, 0xdd, 0x7e, 0xdf,
+ 0x9f, 0x9d, 0xc3, 0xf9, 0x70, 0x78, 0xbf, 0x9c, 0xc1, 0x4d, 0x98, 0xab,
+ 0xd4, 0x2a, 0xb0, 0xdd, 0xb9, 0x63, 0x03, 0xa1, 0x72, 0xcd, 0xa5, 0x2a,
+ 0x03, 0x3b, 0x3b, 0x25, 0x33, 0xdf, 0x0e, 0x89, 0x85, 0x77, 0x94, 0xee,
+ 0x33, 0x62, 0x01, 0x00, 0xf0, 0x46, 0x33, 0xc3, 0x53, 0x55, 0xe8, 0x16,
+ 0x9b, 0xca, 0x81, 0x49, 0xd5, 0x43, 0xc3, 0xc7, 0x34, 0x1a, 0x60, 0x53,
+ 0x49, 0x55, 0x2d, 0x4c, 0xc1, 0x72, 0xe1, 0x8e, 0x1b, 0xed, 0xb6, 0xe6,
+ 0x82, 0xc4, 0x82, 0xcb, 0x9c, 0xd5, 0x82, 0x63, 0xd8, 0xf2, 0xf7, 0x14,
+ 0xcb, 0xd7, 0x9c, 0x1b, 0x87, 0xb8, 0x0e, 0x4a, 0x12, 0xb4, 0x75, 0xd0,
+ 0x62, 0x85, 0xfc, 0x25, 0x8a, 0xa3, 0xe7, 0x46, 0xdf, 0x1b, 0x8b, 0xc2,
+ 0x29, 0xd5, 0xb7, 0x1d, 0x6f, 0xc2, 0x0e, 0xad, 0x98, 0xf9, 0xc3, 0x4b,
+ 0xfc, 0x83, 0x97, 0x0f, 0x49, 0x4c, 0x6b, 0x23, 0x54, 0x59, 0x57, 0xc4,
+ 0xc3, 0xf0, 0xf5, 0x1e, 0x84, 0x09, 0x07, 0x69, 0x36, 0xdf, 0x77, 0x51,
+ 0x63, 0x38, 0xb8, 0x03, 0x86, 0xdd, 0x7d, 0x1e, 0x15, 0xc1, 0xa2, 0x29,
+ 0xcf, 0x01, 0x00, 0x00});
+
+TEST(GzipUtils, parseMacFromGzipXmlHeader)
+{
+ FRUReader reader(std::bind_front(getDataTempl, gzip));
+
+ std::string mac = parseMacFromGzipXmlHeader(reader, 0);
+ EXPECT_EQ(mac, "10:FF:E0:39:84:DC");
+}
diff --git a/test/test_gzip-utils.cpp b/test/test_gzip-utils.cpp
new file mode 100644
index 0000000..58f51f1
--- /dev/null
+++ b/test/test_gzip-utils.cpp
@@ -0,0 +1,102 @@
+#include "fru_device/gzip_utils.hpp"
+
+#include <algorithm>
+#include <array>
+#include <iterator>
+#include <ranges>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+TEST(GzipUtils, EmptyCompressed)
+{
+ std::vector<uint8_t> compressed = {};
+ std::optional<std::string> uncompressed = gzipInflate(compressed);
+ EXPECT_EQ(uncompressed, std::nullopt);
+}
+
+TEST(GzipUtils, GoodCompressed)
+{
+ // Empty file created with
+ // touch foo && gzip -c foo | xxd -i
+ std::array<uint8_t, 24> emptyCompressed{
+ 0x1f, 0x8b, 0x08, 0x08, 0x16, 0x37, 0xdc, 0x68, 0x00, 0x03, 0x66, 0x6f,
+ 0x6f, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+ std::optional<std::string> uncompressed = gzipInflate(emptyCompressed);
+ ASSERT_NE(uncompressed, std::nullopt);
+ if (uncompressed)
+ {
+ EXPECT_EQ(uncompressed->size(), 0U);
+ }
+}
+
+TEST(GzipUtils, 100kcompressedZeros)
+{
+ // File created with
+ // dd if=/dev/zero of=10kfile bs=10k count=1 && gzip -c 10kfile | xxd -i
+ std::array<uint8_t, 53> emptyCompressed{
+ 0x1f, 0x8b, 0x08, 0x08, 0xcb, 0x37, 0xdc, 0x68, 0x00, 0x03, 0x31,
+ 0x30, 0x6b, 0x66, 0x69, 0x6c, 0x65, 0x00, 0xed, 0xc1, 0x01, 0x0d,
+ 0x00, 0x00, 0x00, 0xc2, 0xa0, 0xf7, 0x4f, 0x6d, 0x0e, 0x37, 0xa0,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x37,
+ 0x03, 0x9a, 0xde, 0x1d, 0x27, 0x00, 0x28, 0x00, 0x00};
+ std::optional<std::string> uncompressed = gzipInflate(emptyCompressed);
+ ASSERT_NE(uncompressed, std::nullopt);
+ if (uncompressed)
+ {
+ EXPECT_EQ(uncompressed->size(), 10240U);
+ EXPECT_TRUE(std::ranges::all_of(*uncompressed, [](uint8_t c) {
+ return c == 0U;
+ }));
+ }
+}
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+
+TEST(GzipUtils, getNodeFromXml)
+{
+ constexpr std::string_view xml = R"(<?xml version="1.0" encoding="UTF-8"?>
+ <bookstore>
+ <book category="cooking">
+ <title lang="en">Everyday Italian</title>
+ <author>Giada De Laurentiis</author>
+ <year>2005</year>
+ <price>30.00</price>
+ </book>
+ </bookstore>
+ )";
+
+ EXPECT_THAT(getNodeFromXml(xml, "/bookstore/book/title"),
+ ElementsAre("Everyday Italian"));
+ EXPECT_THAT(getNodeFromXml(xml, "/bookstore/book/author"),
+ ElementsAre("Giada De Laurentiis"));
+ EXPECT_THAT(getNodeFromXml(xml, "/bookstore/book/year"),
+ ElementsAre("2005"));
+ EXPECT_THAT(getNodeFromXml(xml, "/bookstore/book/price"),
+ ElementsAre("30.00"));
+ EXPECT_THAT(getNodeFromXml(xml, "//book/title"),
+ ElementsAre("Everyday Italian"));
+ EXPECT_THAT(getNodeFromXml(xml, "//book/author"),
+ ElementsAre("Giada De Laurentiis"));
+ EXPECT_THAT(getNodeFromXml(xml, "//book/year"), ElementsAre("2005"));
+ EXPECT_THAT(getNodeFromXml(xml, "//book/price"), ElementsAre("30.00"));
+ EXPECT_THAT(getNodeFromXml(xml, "///title"),
+ ElementsAre("Everyday Italian"));
+ EXPECT_THAT(getNodeFromXml(xml, "///author"),
+ ElementsAre("Giada De Laurentiis"));
+ EXPECT_THAT(getNodeFromXml(xml, "///year"), ElementsAre("2005"));
+ EXPECT_THAT(getNodeFromXml(xml, "///price"), ElementsAre("30.00"));
+
+ EXPECT_THAT(getNodeFromXml(xml, "/bookstore/book/noexist"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "/bookstore/noexist/title"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "/noexist/book/title"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "//book/noexist"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "//noexist/title"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "///noexist"), IsEmpty());
+
+ // invalid xpath
+ EXPECT_THAT(getNodeFromXml(xml, "foo"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "foo/bar"), IsEmpty());
+ EXPECT_THAT(getNodeFromXml(xml, "?"), IsEmpty());
+}