nvme: support bridged MCTP endpoints

Updated nvmed to support bridged MCTP endpoints by parsing a list of
BusInfo interfaces from Entity-Manager. Moved the matching logic into
MctpReactorDevice::setup() to keep the device self-resolving and
encapsulate discovery logic. Refactored MctpReactorBusInfo to
maintain root-to-leaf order for consistency, and added an operator==
overload for subset matching.

Example EM config for a bridged device:
```
{
    "Protocol": "mi_mctp",
    "Type": "NVME1000",
    "xyz.openbmc_project.Configuration.NVME1000.BusInfo0": {
        "BusType": "USB",
        "Port": "1.5.1",
        "RootHubPath": "/sys/devices/platform/ahb/f0806000.usb"
    },
    "xyz.openbmc_project.Configuration.NVME1000.BusInfo1": {
        "BusType": "MCTP Bridge",
        "EIDOffset": 2
    }
}
```

Tested: Ran unit tests in Docker container (test_mctp_util, test_nvme).
Google-Bug-Id: 509971428
Change-Id: I8633ed947d1c58d6fb948f5e6bd43e8521fa042e
Signed-off-by: Hao Jiang <jianghao@google.com>
diff --git a/src/MctpReactorDevice.cpp b/src/MctpReactorDevice.cpp
index f3386b3..fb369af 100644
--- a/src/MctpReactorDevice.cpp
+++ b/src/MctpReactorDevice.cpp
@@ -122,7 +122,7 @@
 
 MctpReactorDevice::MctpReactorDevice(
     const std::shared_ptr<sdbusplus::asio::connection>& connection,
-    const std::string& deviceObjectPath, const BusInfo& busInfo,
+    const std::string& deviceObjectPath, const std::vector<BusInfo>& busInfo,
     std::optional<int> busNumber, std::optional<int> address) :
     connection(connection), deviceObjectPath(deviceObjectPath),
     prettyObjectName(extractSuffixFromObjectPath(deviceObjectPath)),
@@ -173,9 +173,13 @@
 
         for (const auto& [epPath, epConfigChain] : mctpEndpointConfigMap)
         {
-            MctpReactorBusInfo epBusInfo(epConfigChain);
-            if (epBusInfo.isValid() &&
-                epBusInfo.getProperties() == targetBusInfo)
+            MctpReactorBusInfo epBusInfoObj(epConfigChain);
+            if (!epBusInfoObj.isValid())
+            {
+                continue;
+            }
+
+            if (epBusInfoObj == targetBusInfo)
             {
                 finaliseEndpoint(epPath, std::move(cb));
                 return;
diff --git a/src/MctpReactorDevice.hpp b/src/MctpReactorDevice.hpp
index 212a355..19f24cb 100644
--- a/src/MctpReactorDevice.hpp
+++ b/src/MctpReactorDevice.hpp
@@ -28,8 +28,9 @@
         std::optional<int> address);
     MctpReactorDevice(
         const std::shared_ptr<sdbusplus::asio::connection>& connection,
-        const std::string& deviceObjectPath, const BusInfo& busInfo,
-        std::optional<int> busNumber, std::optional<int> address);
+        const std::string& deviceObjectPath,
+        const std::vector<BusInfo>& busInfo, std::optional<int> busNumber,
+        std::optional<int> address);
     MctpReactorDevice(const MctpDevice& other) = delete;
     MctpReactorDevice(MctpDevice&& other) = delete;
     ~MctpReactorDevice() override;
@@ -62,7 +63,7 @@
     std::shared_ptr<MctpdEndpoint> endpoint;
     std::unique_ptr<sdbusplus::bus::match_t> removeMatch;
 
-    BusInfo targetBusInfo;
+    std::vector<BusInfo> targetBusInfo;
     std::optional<MctpCallbackToken> callbackToken;
     std::function<void(const std::error_code&,
                        const std::shared_ptr<MctpEndpoint>&)>
diff --git a/src/MctpUtil.cpp b/src/MctpUtil.cpp
index 85deb4c..c1a3f48 100644
--- a/src/MctpUtil.cpp
+++ b/src/MctpUtil.cpp
@@ -11,6 +11,7 @@
 #include <chrono>
 #include <cstdint>
 #include <filesystem>
+#include <ranges>
 
 std::map<std::string, std::vector<SensorData>> mctpEndpointConfigMap;
 
@@ -617,20 +618,27 @@
     };
 
     // For a MCTP device behind (multiple layers of) MCTP bridge,
-    // configDataChain include all config/busInfo including its bridge routing
-    // info (a.k.a BridgeDownsteamMCTPDDevice) and the top most port info (a.k.a
-    // MCTPUSBDevice/etc).
-    // And MctpReactorBusInfo extracts the top most port info for difference bus
-    // types.
-    for (const auto& configData : configDataChain)
+    // configDataChain include all config/busInfo from its bottom-most bridge
+    // routing info (a.k.a BridgeDownsteamMCTPDDevice) to the top-most port info
+    // (a.k.a MCTPUSBDevice/etc).
+
+    // And MctpReactorBusInfo extracts the config for
+    // both routing and the top most port info and put them into a reverse order
+    // (from top most port to the bottom most routing) to accomodate the
+    // NVMe1000 bus config
+    for (const auto& configData : std::views::reverse(configDataChain))
     {
+        std::string type;
+        std::map<std::string, std::string> properties;
+        bool levelValid = false;
+
         for (const auto& [intf, props] : configData)
         {
             if (absl::StrContains(
                     intf, "xyz.openbmc_project.Configuration.MCTPUSBDevice"))
             {
                 type = "USB";
-                valid = true;
+                levelValid = true;
                 properties["BusType"] = "USB";
 
                 extractProp(props, properties, "RootHubPath");
@@ -643,7 +651,7 @@
                     intf, "xyz.openbmc_project.Configuration.MCTPI2CTarget"))
             {
                 type = "I2C";
-                valid = true;
+                levelValid = true;
                 properties["BusType"] = "I2C";
 
                 extractProp(props, properties, "Bus");
@@ -654,17 +662,60 @@
                     intf, "xyz.openbmc_project.Configuration.MCTPI3CTarget"))
             {
                 type = "I3C";
-                valid = true;
+                levelValid = true;
                 properties["BusType"] = "I3C";
 
                 extractProp(props, properties, "Bus");
                 extractProp(props, properties, "Address");
                 break;
             }
+            if (absl::StrContains(
+                    intf,
+                    "xyz.openbmc_project.Configuration.MCTPBridgeDownstreamDevice"))
+            {
+                type = "MCTP Bridge";
+                levelValid = true;
+                properties["BusType"] = "MCTP Bridge";
+
+                extractProp(props, properties, "EIDOffset");
+                break;
+            }
+        }
+
+        if (levelValid)
+        {
+            types.push_back(type);
+            propertiesList.push_back(properties);
+            valid = true;
         }
     }
 }
 
+bool MctpReactorBusInfo::operator==(
+    const std::vector<BusInfo>& targetBusInfo) const
+{
+    if (propertiesList.size() != targetBusInfo.size())
+    {
+        return false;
+    }
+
+    for (size_t i = 0; i < targetBusInfo.size(); ++i)
+    {
+        const auto& configBusInfo = targetBusInfo[i];
+        const auto& epProps = propertiesList[i];
+
+        for (const auto& [key, val] : configBusInfo)
+        {
+            auto it = epProps.find(key);
+            if (it == epProps.end() || it->second != val)
+            {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
 void cleanupMctpEndpointListener()
 {
     associationMatch.reset();
diff --git a/src/MctpUtil.hpp b/src/MctpUtil.hpp
index 7a049fb..3372235 100644
--- a/src/MctpUtil.hpp
+++ b/src/MctpUtil.hpp
@@ -27,23 +27,25 @@
 {
   public:
     explicit MctpReactorBusInfo(const std::vector<SensorData>& configData);
-    std::string getType() const
+    bool operator==(const std::vector<BusInfo>& targetBusInfo) const;
+    std::vector<std::string> getType() const
     {
-        return type;
+        return types;
     }
     bool isValid() const
     {
         return valid;
     }
-    const std::map<std::string, std::string>& getProperties() const
+    const std::vector<std::map<std::string, std::string>> &
+        getProperties() const
     {
-        return properties;
+        return propertiesList;
     }
 
   private:
-    std::string type;
+    std::vector<std::string> types;
     bool valid = false;
-    std::map<std::string, std::string> properties;
+    std::vector<std::map<std::string, std::string>> propertiesList;
 };
 
 extern std::map<std::string, std::vector<SensorData>> mctpEndpointConfigMap;
diff --git a/src/NVMeSensorMain.cpp b/src/NVMeSensorMain.cpp
index c64b106..3a71371 100644
--- a/src/NVMeSensorMain.cpp
+++ b/src/NVMeSensorMain.cpp
@@ -200,9 +200,13 @@
     return false;
 }
 
-static BusInfo extractNVMeBusInfo(const SensorData& configData)
+static std::vector<BusInfo> extractNVMeBusInfo(const SensorData& configData)
 {
-    BusInfo busInfo;
+    std::vector<BusInfo> busInfoList;
+    std::map<int, BusInfo> indexedBusInfo;
+    BusInfo legacyBusInfo;
+    bool hasLegacy = false;
+
     for (const auto& pair : configData)
     {
         const std::string& intf = pair.first;
@@ -210,6 +214,7 @@
         if (absl::StrContains(
                 intf, "xyz.openbmc_project.Configuration.NVME1000.BusInfo"))
         {
+            BusInfo busInfo;
             std::string busType;
             auto it = props.find("BusType");
             if (it != props.end())
@@ -249,14 +254,55 @@
                 extractProp("Bus");
                 extractProp("Address");
             }
+            else if (busType == "MCTP Bridge")
+            {
+                extractProp("EIDOffset");
+            }
             else if (!busType.empty())
             {
                 lg2::warning("Unsupported BusType {TYPE} in NVME1000 config",
                              "TYPE", busType);
             }
+
+            std::string suffix = intf.substr(
+                std::string(
+                    "xyz.openbmc_project.Configuration.NVME1000.BusInfo")
+                    .length());
+            if (suffix.empty())
+            {
+                legacyBusInfo = busInfo;
+                hasLegacy = true;
+            }
+            else
+            {
+                try
+                {
+                    int index = std::stoi(suffix);
+                    indexedBusInfo[index] = busInfo;
+                }
+                catch (const std::exception& e)
+                {
+                    lg2::error(
+                        "Failed to parse index from interface {INTF}: {ERROR}",
+                        "INTF", intf, "ERROR", e.what());
+                }
+            }
         }
     }
-    return busInfo;
+
+    if (hasLegacy)
+    {
+        busInfoList.push_back(legacyBusInfo);
+    }
+    else
+    {
+        for (const auto& [index, busInfo] : indexedBusInfo)
+        {
+            busInfoList.push_back(busInfo);
+        }
+    }
+
+    return busInfoList;
 }
 
 static void handleConfigurations(
@@ -287,7 +333,8 @@
 
         std::optional<int> busNumber;
         std::optional<int> address;
-        BusInfo busInfo;
+        std::vector<BusInfo> busInfoList;
+        std::string endpointPath;
 
         if (*nvmeProtocol == "mi_basic" || *nvmeProtocol == "mi_i2c")
         {
@@ -300,33 +347,32 @@
         }
         else if (*nvmeProtocol == "mi_mctp")
         {
-            busInfo = extractNVMeBusInfo(configData);
-            if (busInfo.empty())
+            busInfoList = extractNVMeBusInfo(configData);
+            if (busInfoList.empty())
             {
                 lg2::error("Missing BusInfo for mi_mctp device {PATH}", "PATH",
                            nvmeObjectPath.str);
                 continue;
             }
 
-            auto itType = busInfo.find("BusType");
-            if (itType != busInfo.end() && itType->second == "I2C")
-            {
-                auto itBus = busInfo.find("Bus");
-                if (itBus != busInfo.end())
-                {
-                    busNumber = std::stoi(itBus->second);
-                }
-                auto itAddr = busInfo.find("Address");
-                if (itAddr != busInfo.end())
-                {
-                    address = std::stoi(itAddr->second, nullptr, 0);
-                }
+            // Matching is now handled in MctpReactorDevice::setup()
 
-                if (!busNumber)
+            if (!busInfoList.empty())
+            {
+                const auto& rootBusInfo = busInfoList.front();
+                auto itType = rootBusInfo.find("BusType");
+                if (itType != rootBusInfo.end() && itType->second == "I2C")
                 {
-                    lg2::error("Missing Bus in BusInfo for {PATH}", "PATH",
-                               nvmeObjectPath.str);
-                    continue;
+                    auto itBus = rootBusInfo.find("Bus");
+                    if (itBus != rootBusInfo.end())
+                    {
+                        busNumber = std::stoi(itBus->second);
+                    }
+                    auto itAddr = rootBusInfo.find("Address");
+                    if (itAddr != rootBusInfo.end())
+                    {
+                        address = std::stoi(itAddr->second, nullptr, 0);
+                    }
                 }
             }
         }
@@ -432,8 +478,8 @@
                 if (*nvmeProtocol == "mi_mctp")
                 {
                     mctpDev = std::make_shared<MctpReactorDevice>(
-                        dbusConnection, nvmeObjectPath.str, busInfo, busNumber,
-                        address);
+                        dbusConnection, nvmeObjectPath.str, busInfoList,
+                        busNumber, address);
                 }
                 else if (!isMctpReactorDevice)
                 {
diff --git a/tests/test_MctpUtil.cpp b/tests/test_MctpUtil.cpp
index ee110d2..cf2bfe3 100644
--- a/tests/test_MctpUtil.cpp
+++ b/tests/test_MctpUtil.cpp
@@ -1050,7 +1050,8 @@
 
     // Create MctpReactorDevice
     auto mctpDev = std::make_shared<MctpReactorDevice>(
-        conn, emConfigPath, targetBusInfo, std::nullopt, std::nullopt);
+        conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
+        std::nullopt);
     mctpDev->setup([&](const std::error_code& ec,
                        const std::shared_ptr<MctpEndpoint>& ep) {
         setupCompleted = true;
@@ -1126,7 +1127,8 @@
 
     // Create MctpReactorDevice
     auto mctpDev = std::make_shared<MctpReactorDevice>(
-        conn, emConfigPath, targetBusInfo, std::nullopt, std::nullopt);
+        conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
+        std::nullopt);
     mctpDev->setup([&](const std::error_code& ec,
                        const std::shared_ptr<MctpEndpoint>& ep) {
         setupCompleted = true;
@@ -1185,7 +1187,8 @@
 
     // Create MctpReactorDevice
     auto mctpDev = std::make_shared<MctpReactorDevice>(
-        conn, emConfigPath, targetBusInfo, std::nullopt, std::nullopt);
+        conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
+        std::nullopt);
     mctpDev->setup([&](const std::error_code& ec,
                        const std::shared_ptr<MctpEndpoint>& ep) {
         setupCompleted = true;
@@ -1243,7 +1246,8 @@
 
     // Create MctpReactorDevice
     auto mctpDev = std::make_shared<MctpReactorDevice>(
-        conn, emConfigPath, targetBusInfo, std::nullopt, std::nullopt);
+        conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
+        std::nullopt);
     mctpDev->setup([&](const std::error_code& ec,
                        const std::shared_ptr<MctpEndpoint>& ep) {
         setupCompleted = true;
@@ -1312,7 +1316,8 @@
 
         // Create MctpReactorDevice
         auto mctpDev = std::make_shared<MctpReactorDevice>(
-            conn, emConfigPath, targetBusInfo, std::nullopt, std::nullopt);
+            conn, emConfigPath, std::vector<BusInfo>{targetBusInfo},
+            std::nullopt, std::nullopt);
 
         mctpDev->setup([&](const std::error_code&,
                            const std::shared_ptr<MctpEndpoint>& ep) {
@@ -1425,8 +1430,8 @@
 
     MctpReactorBusInfo busInfo(it->second);
     EXPECT_TRUE(busInfo.isValid());
-    EXPECT_EQ(busInfo.getType(), "USB");
-    EXPECT_EQ(busInfo.getProperties().at("Port"), "1.5.1");
+    EXPECT_EQ(busInfo.getType().front(), "USB");
+    EXPECT_EQ(busInfo.getProperties().front().at("Port"), "1.5.1");
 }
 
 // 17. Broken Bridge Chain (Non-existent bridge)
@@ -1561,8 +1566,8 @@
 
     MctpReactorBusInfo busInfo(it->second);
     EXPECT_TRUE(busInfo.isValid());
-    EXPECT_EQ(busInfo.getType(), "USB");
-    EXPECT_EQ(busInfo.getProperties().at("Port"), "1.5.1");
+    EXPECT_EQ(busInfo.getType().front(), "USB");
+    EXPECT_EQ(busInfo.getProperties().front().at("Port"), "1.5.1");
 }
 
 // 19. Out-of-Order Triggers (Endpoint before Bridge) - Expect Success after
@@ -1776,6 +1781,73 @@
     EXPECT_NE(it, mctpEndpointConfigMap.end());
 }
 
+// 22. MctpReactorBusInfo chain parsing and operator== matching
+TEST_F(MctpUtilTest, MctpReactorBusInfo_ChainParsingAndMatching)
+{
+    std::vector<SensorData> chain;
+
+    // Leaf (endpoint)
+    SensorBaseConfigMap epProps;
+    epProps["EIDOffset"] = uint64_t(2);
+    SensorData epData;
+    epData["xyz.openbmc_project.Configuration.MCTPBridgeDownstreamDevice"] =
+        epProps;
+    chain.push_back(epData);
+
+    // Bridge
+    SensorBaseConfigMap bridgeProps;
+    bridgeProps["BusType"] = std::string("USB");
+    bridgeProps["Port"] = std::string("1.5.1");
+    bridgeProps["Configuration"] =
+        std::string("1"); // Extra property not in target
+    SensorData bridgeData;
+    bridgeData["xyz.openbmc_project.Configuration.MCTPUSBDevice"] = bridgeProps;
+    chain.push_back(bridgeData);
+
+    MctpReactorBusInfo busInfo(chain);
+
+    EXPECT_TRUE(busInfo.isValid());
+    auto propsList = busInfo.getProperties();
+    ASSERT_EQ(propsList.size(), 2);
+
+    // Verify order is root-to-leaf (from Test 22)
+    EXPECT_EQ(propsList[0].at("BusType"), "USB");
+    EXPECT_EQ(propsList[0].at("Port"), "1.5.1");
+
+    EXPECT_EQ(propsList[1].at("BusType"), "MCTP Bridge");
+    EXPECT_EQ(propsList[1].at("EIDOffset"), "2");
+
+    // Verify operator== subset matching (from Test 23)
+    std::vector<BusInfo> targetConfig;
+
+    // Root level
+    BusInfo targetBridge;
+    targetBridge["BusType"] = "USB";
+    targetBridge["Port"] = "1.5.1";
+    targetConfig.push_back(targetBridge);
+
+    // Leaf level
+    BusInfo targetEp;
+    targetEp["EIDOffset"] = "2";
+    targetConfig.push_back(targetEp);
+
+    // Should match because target is a subset of extracted properties
+    EXPECT_TRUE(busInfo == targetConfig);
+
+    // Test mismatch in value
+    targetConfig[1]["EIDOffset"] = "3";
+    EXPECT_FALSE(busInfo == targetConfig);
+
+    // Test missing property in extracted props
+    targetConfig[1]["EIDOffset"] = "2";
+    targetConfig[1]["ExtraProp"] = "value";
+    EXPECT_FALSE(busInfo == targetConfig);
+
+    // Test size mismatch
+    targetConfig.pop_back();
+    EXPECT_FALSE(busInfo == targetConfig);
+}
+
 int main(int argc, char** argv)
 {
     ::testing::InitGoogleTest(&argc, argv);