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");