Expose formatted hexdump of raw EEPROM data in responses.

Google-Bug-Id:550525180
PiperOrigin-RevId: 974666708
Change-Id: Ia85d38b776354d60fd5ba8f13209968a64c9ebdc
diff --git a/redfish-core/include/utils/hex_utils.hpp b/redfish-core/include/utils/hex_utils.hpp
index b149e9e..70d138e 100644
--- a/redfish-core/include/utils/hex_utils.hpp
+++ b/redfish-core/include/utils/hex_utils.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <algorithm>
 #include <array>
 #include <cstddef>
 #include <cstdint>
@@ -60,3 +61,68 @@
   }
   return rc;
 }
+
+inline std::string bytesToHexDump(const std::vector<uint8_t>& bytes) {
+  std::string hex_dump;
+  if (bytes.empty()) {
+    return hex_dump;
+  }
+  const size_t rowLength = 16;
+  for (size_t i = 0; i < bytes.size(); i += rowLength) {
+    // Offset
+    hex_dump += intToHexString(i, 4);
+    hex_dump += ":  ";
+
+    // Hex bytes
+    size_t chunkLength = std::min(rowLength, bytes.size() - i);
+    for (size_t j = 0; j < rowLength; ++j) {
+      if (j < chunkLength) {
+        uint8_t byte = bytes[i + j];
+        // Bitwise AND 0xf0 extracts the top 4 bits.
+        // Shifting right by 4 puts those bits in the 0-15 range.
+        // digitsArray maps 0-15 to the corresponding hex char '0'-'F'.
+        hex_dump += digitsArray[(byte & 0xf0) >> 4];
+        // Bitwise AND 0x0f extracts the bottom 4 bits.
+        hex_dump += digitsArray[byte & 0x0f];
+      } else {
+        // If we don't have bytes remaining to pad to the end of the line,
+        // we print two spaces to match the two hex characters of a printed byte
+        // and preserve the ASCII representation alignment.
+        hex_dump += "  ";
+      }
+
+      // Formatting spaces.
+      if (j == 7) {
+        // For readability, add two spaces in the middle of each 16-byte row.
+        hex_dump += "  ";
+      } else if (j < rowLength - 1) {
+        // Space between bytes to separate them.
+        hex_dump += ' ';
+      }
+    }
+
+    // Separator between the hex and ASCII representation blocks.
+    hex_dump += "  | ";
+
+    // ASCII representation snippet mapping byte values to characters
+    for (size_t j = 0; j < chunkLength; ++j) {
+      uint8_t byte = bytes[i + j];
+      // Range 32 to 126 contains the standard printable ASCII characters.
+      // Control characters (0-31, 127) and extended ASCII (128+)
+      // are omitted since they will either not print correctly or will mess up
+      // the terminal output.
+      if (byte >= 32 && byte <= 126) {
+        // Cast the uint8_t byte to char. This is required so that the string's
+        // `operator+=` appends the character representation instead of possibly
+        // picking an overload that formats the integer.
+        hex_dump += static_cast<char>(byte);
+      } else {
+        // If the character is not printable, we append a safe placeholder '.'
+        // to maintain the alignment and clearly show unprintable data.
+        hex_dump += '.';
+      }
+    }
+    hex_dump += " |\n";
+  }
+  return hex_dump;
+}
diff --git a/redfish-core/lib/managers.hpp b/redfish-core/lib/managers.hpp
index 940bf7f..b1c6c68 100644
--- a/redfish-core/lib/managers.hpp
+++ b/redfish-core/lib/managers.hpp
@@ -52,6 +52,7 @@
 #include "chassis_utils.hpp"
 #include "collection.hpp"
 #include "dbus_utils.hpp"
+#include "hex_utils.hpp"
 #include "json_utils.hpp"
 #include "location_utils.hpp"
 #include "sw_utils.hpp"
@@ -2790,7 +2791,9 @@
                               return;
                             }
                             asyncResp->res.jsonValue["RawData"] = rawData;
-                          }),
+                            asyncResp->res.jsonValue["HexdumpFormattedRawData"] =
+                          bytesToHexDump(rawData);
+                    }),
                       service, "/xyz/openbmc_project/FruDevice",
                       "xyz.openbmc_project.FruDeviceManager", "GetRawFru",
                       static_cast<uint16_t>(bus),
diff --git a/test/redfish-core/include/utils/hex_utils_test.cpp b/test/redfish-core/include/utils/hex_utils_test.cpp
index 6971001..2004d67 100644
--- a/test/redfish-core/include/utils/hex_utils_test.cpp
+++ b/test/redfish-core/include/utils/hex_utils_test.cpp
@@ -1,8 +1,10 @@
 #include "hex_utils.hpp"
 
+#include <algorithm>
 #include <cctype>
 #include <cstdint>
 #include <limits>
+#include <string>
 #include <vector>
 
 #include <gmock/gmock.h>  // IWYU pragma: keep
@@ -42,6 +44,35 @@
   EXPECT_EQ(bytesToHexString({0x1a, 0x2b}), "1A2B");
 }
 
+TEST(BytesToHexDump, ZeroLength) { EXPECT_EQ(bytesToHexDump({}), ""); }
+
+TEST(BytesToHexDump, MultipleLines) {
+  std::vector<uint8_t> bytes;
+  for (int i = 0; i < 20; ++i) {
+    bytes.push_back(static_cast<uint8_t>('A' + i));
+  }
+  std::string expected =
+      "0000:  41 42 43 44 45 46 47 48  49 4A 4B 4C 4D 4E 4F 50  | "
+      "ABCDEFGHIJKLMNOP |\n"
+      "0010:  51 52 53 54                                       | QRST |\n";
+  EXPECT_EQ(bytesToHexDump(bytes), expected);
+}
+
+TEST(BytesToHexDump, LargeLength) {
+  std::vector<uint8_t> bytes(512, 'A');
+  std::string result = bytesToHexDump(bytes);
+  EXPECT_EQ(result.length(), 32 * 78);  // 32 lines, 78 chars each
+  EXPECT_EQ(std::count(result.begin(), result.end(), '\n'), 32);
+}
+
+TEST(BytesToHexDump, ActualHexValues) {
+  std::vector<uint8_t> bytes = {0x00, 0x01, 0x02, 0xAB, 0xFF, 'A', 'B', 'C'};
+  std::string expected =
+      "0000:  00 01 02 AB FF 41 42 43                           | .....ABC "
+      "|\n";
+  EXPECT_EQ(bytesToHexDump(bytes), expected);
+}
+
 TEST(HexCharToNibble, ReturnsCorrectNibbleForEveryHexChar) {
   for (char c = 0; c < std::numeric_limits<char>::max(); ++c) {
     uint8_t expected = 16;
diff --git a/test/redfish-core/lib/manager_test.cpp b/test/redfish-core/lib/manager_test.cpp
index 33f6a29..115221d 100644
--- a/test/redfish-core/lib/manager_test.cpp
+++ b/test/redfish-core/lib/manager_test.cpp
@@ -649,11 +649,117 @@
     "Name": "Google Raw EEPROM",
     "Bus": 2,
     "Address": 81,
-    "RawData": [1, 2, 3]
+    "RawData": [1, 2, 3],
+    "HexdumpFormattedRawData": "0000:  01 02 03                                          | ... |\n"
   })"));
   EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
 }
 
+TEST_F(SnapshotFixture, handleRawEepromMemberGetZeroLength) {
+  std::string rawId = "Eeprom1";
+  KeyType key(ManagedType::kManagedPropertyMap, "xyz.openbmc_project.FruDevice",
+              "/xyz/openbmc_project/FruDevice/" + rawId,
+              "xyz.openbmc_project.Inventory.Item.I2CDevice");
+
+  DBusPropertiesMap props = {
+      {std::make_pair("Bus", DbusVariantType(static_cast<uint32_t>(2)))},
+      {std::make_pair("Address", DbusVariantType(static_cast<uint32_t>(81)))},
+  };
+
+  ASSERT_TRUE(
+      dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
+          managedStore::GetManagedObjectStore())
+          ->upsertMockObjectIntoManagedStore(
+              key, managedStore::MockManagedStoreTest::CreateValueType(props))
+          .ok());
+
+  EXPECT_CALL(
+      *dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
+          managedStore::GetManagedObjectStore()),
+      PostDbusCallToIoContextThreadSafe(
+          testing::_,
+          testing::An<
+              absl::AnyInvocable<void(const boost::system::error_code&,
+                                      const std::vector<uint8_t>&)>&&>(),
+          "xyz.openbmc_project.FruDevice", "/xyz/openbmc_project/FruDevice",
+          "xyz.openbmc_project.FruDeviceManager", "GetRawFru",
+          testing::Eq(static_cast<uint16_t>(2)),
+          testing::Eq(static_cast<uint8_t>(81))))
+      .WillOnce(
+          managedStore::
+              SimulateSuccessfulAsyncPostDbusCallThreadSafeWithValueAction<
+                  std::vector<uint8_t>>::
+                  SimulateSuccessfulAsyncPostDbusCallWithValue(
+                      std::make_shared<std::vector<uint8_t>>(
+                          std::vector<uint8_t>{})));
+
+  handleRawEepromMemberGet(app_, CreateRequest(), share_async_resp_, rawId);
+
+  RunIoUntilDone();
+
+  nlohmann::json& json = share_async_resp_->res.jsonValue;
+  EXPECT_EQ(json["RawData"], std::vector<uint8_t>{});
+  EXPECT_EQ(json["HexdumpFormattedRawData"], "");
+  EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
+}
+
+TEST_F(SnapshotFixture, handleRawEepromMemberGetMultipleLines) {
+  std::string rawId = "Eeprom1";
+  KeyType key(ManagedType::kManagedPropertyMap, "xyz.openbmc_project.FruDevice",
+              "/xyz/openbmc_project/FruDevice/" + rawId,
+              "xyz.openbmc_project.Inventory.Item.I2CDevice");
+
+  DBusPropertiesMap props = {
+      {std::make_pair("Bus", DbusVariantType(static_cast<uint32_t>(2)))},
+      {std::make_pair("Address", DbusVariantType(static_cast<uint32_t>(81)))},
+  };
+
+  ASSERT_TRUE(
+      dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
+          managedStore::GetManagedObjectStore())
+          ->upsertMockObjectIntoManagedStore(
+              key, managedStore::MockManagedStoreTest::CreateValueType(props))
+          .ok());
+
+  std::vector<uint8_t> rawData;
+  for (int i = 0; i < 20; ++i) {
+    rawData.push_back(static_cast<uint8_t>('A' + i));
+  }
+
+  EXPECT_CALL(
+      *dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
+          managedStore::GetManagedObjectStore()),
+      PostDbusCallToIoContextThreadSafe(
+          testing::_,
+          testing::An<
+              absl::AnyInvocable<void(const boost::system::error_code&,
+                                      const std::vector<uint8_t>&)>&&>(),
+          "xyz.openbmc_project.FruDevice", "/xyz/openbmc_project/FruDevice",
+          "xyz.openbmc_project.FruDeviceManager", "GetRawFru",
+          testing::Eq(static_cast<uint16_t>(2)),
+          testing::Eq(static_cast<uint8_t>(81))))
+      .WillOnce(
+          managedStore::
+              SimulateSuccessfulAsyncPostDbusCallThreadSafeWithValueAction<
+                  std::vector<uint8_t>>::
+                  SimulateSuccessfulAsyncPostDbusCallWithValue(
+                      std::make_shared<std::vector<uint8_t>>(rawData)));
+
+  handleRawEepromMemberGet(app_, CreateRequest(), share_async_resp_, rawId);
+
+  RunIoUntilDone();
+
+  nlohmann::json& json = share_async_resp_->res.jsonValue;
+  EXPECT_EQ(json["RawData"], rawData);
+
+  std::string expectedHexdump =
+      "0000:  41 42 43 44 45 46 47 48  49 4A 4B 4C 4D 4E 4F 50  | "
+      "ABCDEFGHIJKLMNOP |\n"
+      "0010:  51 52 53 54                                       | QRST |\n";
+  EXPECT_EQ(json["HexdumpFormattedRawData"], expectedHexdump);
+  EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
+}
+
 TEST_F(SnapshotFixture, handleRawEepromMemberGetNotFound) {
   handleRawEepromMemberGet(app_, CreateRequest(), share_async_resp_,
                            "NonExistentEeprom");