Allow multipart update when no targets are specified

If no targets are provided, bmcweb will automatically select the sole
software object that implements both the StartUpdate and
xyz.openbmc_project.Software.MultipartUpdate interfaces [1], provided
there is only one such object; otherwise, the behavior remains unchanged
(an error requiring explicit target is returned).

This enables support for PLDM [2], which uses a single StartUpdate API
and handles multi-component updates internally [3]. If multiple
MultipartUpdate instances are present and no targets are specified,
an error is returned.

Behavior:
- Single target specified: Unchanged; updates proceed as before.
- No targets specified: If exactly one entity implements both
MultipartUpdate interface and StartUpdate, invoke StartUpdate on it.
Otherwise, return an error requiring explicit target.
- Multiple targets: Not supported, return an error (unchanged).

[1] https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/78905
[2] https://gerrit.openbmc.org/c/openbmc/pldm/+/79192
[3] https://gerrit.openbmc.org/c/openbmc/docs/+/76645

Tests:
- FW Update using Multipart URI
```
curl -X POST -k https://{ip}/redfish/v1/UpdateService/update-multipart --form 'UpdateParameters={"Targets":[]};type=application/json' --form "UpdateFile=@vbios.fwpkg;type=application/octet-stream"
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task"
  ...
  "PercentComplete": 0,
  "StartTime": "2025-08-05T13:33:23+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Running",
  "TaskStatus": "OK"
}
```

Signed-off-by: Rajeev Ranjan <ranjan.rajeev1609@gmail.com>
Change-Id: I294b1447b6faf2055419d3659f9c963aeb6d5d0d
diff --git a/redfish-core/lib/update_service.hpp b/redfish-core/lib/update_service.hpp
index 72ade3c..e76dc0c 100644
--- a/redfish-core/lib/update_service.hpp
+++ b/redfish-core/lib/update_service.hpp
@@ -83,6 +83,9 @@
 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 static std::unique_ptr<boost::asio::steady_timer> fwAvailableTimer;
 
+/* @brief String that indicates the Software Update D-Bus interface */
+constexpr const char* updateInterface = "xyz.openbmc_project.Software.Update";
+
 struct MemoryFileDescriptor
 {
     int fd = -1;
@@ -690,7 +693,7 @@
     struct UpdateParameters
     {
         std::optional<std::string> applyTime;
-        std::vector<std::string> targets;
+        std::optional<std::vector<std::string>> targets;
     } params;
 };
 
@@ -760,36 +763,39 @@
         return std::nullopt;
     }
 
-    std::vector<std::string> tempTargets;
     if (!json_util::readJsonObject(                            //
             *obj, asyncResp->res,                              //
             "@Redfish.OperationApplyTime", multiRet.applyTime, //
-            "Targets", tempTargets                             //
+            "Targets", multiRet.targets                        //
             ))
     {
         return std::nullopt;
     }
 
-    for (size_t urlIndex = 0; urlIndex < tempTargets.size(); urlIndex++)
+    if (multiRet.targets)
     {
-        const std::string& target = tempTargets[urlIndex];
-        boost::system::result<boost::urls::url_view> url =
-            boost::urls::parse_origin_form(target);
-        auto res = processUrl(url);
-        if (!res.has_value())
+        if (multiRet.targets->size() > 1)
         {
-            messages::propertyValueFormatError(
-                asyncResp->res, target, std::format("Targets/{}", urlIndex));
+            messages::propertyValueFormatError(asyncResp->res,
+                                               *multiRet.targets, "Targets");
             return std::nullopt;
         }
-        multiRet.targets.emplace_back(res.value());
+
+        for (auto& target : *multiRet.targets)
+        {
+            boost::system::result<boost::urls::url_view> url =
+                boost::urls::parse_origin_form(target);
+            auto res = processUrl(url);
+            if (!res.has_value())
+            {
+                messages::propertyValueFormatError(asyncResp->res, target,
+                                                   "Targets");
+                return std::nullopt;
+            }
+            target = res.value();
+        }
     }
-    if (multiRet.targets.size() != 1)
-    {
-        messages::propertyValueFormatError(asyncResp->res, multiRet.targets,
-                                           "Targets");
-        return std::nullopt;
-    }
+
     return multiRet;
 }
 
@@ -838,11 +844,7 @@
         messages::propertyMissing(asyncResp->res, "UpdateFile");
         return std::nullopt;
     }
-    if (multiRet.params.targets.empty())
-    {
-        messages::propertyMissing(asyncResp->res, "Targets");
-        return std::nullopt;
-    }
+
     return multiRet;
 }
 
@@ -877,8 +879,8 @@
             handleStartUpdate(asyncResp, std::move(payload), objectPath, ec1,
                               retPath);
         },
-        serviceName, objectPath, "xyz.openbmc_project.Software.Update",
-        "StartUpdate", sdbusplus::message::unix_fd(memfd.fd), applyTime);
+        serviceName, objectPath, updateInterface, "StartUpdate",
+        sdbusplus::message::unix_fd(memfd.fd), applyTime);
 }
 
 inline void getSwInfo(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
@@ -947,10 +949,48 @@
                 functionalSoftware[0], "xyz.openbmc_project.Software.Manager");
 }
 
+inline void handleMultipartManagerUpdate(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, task::Payload payload,
+    const MemoryFileDescriptor& memfd, const std::string& applyTime,
+    const boost::system::error_code& ec,
+    const dbus::utility::MapperGetSubTreeResponse& subtree)
+{
+    if (ec)
+    {
+        BMCWEB_LOG_ERROR("error_code = {}", ec);
+        BMCWEB_LOG_ERROR("error msg = {}", ec.message());
+        messages::internalError(asyncResp->res);
+        return;
+    }
+    if (subtree.size() != 1)
+    {
+        BMCWEB_LOG_ERROR("Found {} MultipartUpdate objects, expected exactly 1",
+                         subtree.size());
+        messages::internalError(asyncResp->res);
+        return;
+    }
+
+    const auto& [objectPath, services] = subtree[0];
+    for (const auto& [serviceName, ifaces] : services)
+    {
+        if (std::find(ifaces.begin(), ifaces.end(), updateInterface) !=
+            ifaces.end())
+        {
+            startUpdate(asyncResp, std::move(payload), memfd, applyTime,
+                        objectPath, serviceName);
+            return;
+        }
+    }
+    BMCWEB_LOG_ERROR(
+        "MultipartUpdate object at {} does not implement {} interface",
+        objectPath, updateInterface);
+    messages::internalError(asyncResp->res);
+}
+
 inline void processUpdateRequest(
     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
     task::Payload&& payload, std::string_view body,
-    const std::string& applyTime, std::vector<std::string>& targets)
+    const std::string& applyTime, const std::vector<std::string>& targets)
 {
     MemoryFileDescriptor memfd("update-image");
     if (memfd.fd == -1)
@@ -972,7 +1012,23 @@
         return;
     }
 
-    if (!targets.empty() && targets[0] == BMCWEB_REDFISH_MANAGER_URI_NAME)
+    if (targets.empty())
+    {
+        constexpr std::array<std::string_view, 1> interfaces = {
+            "xyz.openbmc_project.Software.MultipartUpdate"};
+        dbus::utility::getSubTree(
+            "/xyz/openbmc_project/software", 1, interfaces,
+            [asyncResp, payload = std::move(payload), memfd = std::move(memfd),
+             applyTime](const boost::system::error_code& ec,
+                        const dbus::utility::MapperGetSubTreeResponse&
+                            subtree) mutable {
+                handleMultipartManagerUpdate(asyncResp, std::move(payload),
+                                             memfd, applyTime, ec, subtree);
+            });
+        return;
+    }
+
+    if (targets[0] == BMCWEB_REDFISH_MANAGER_URI_NAME)
     {
         dbus::utility::getAssociationEndPoints(
             "/xyz/openbmc_project/software/bmc/updateable",
@@ -1025,9 +1081,10 @@
         }
         task::Payload payload(req);
 
-        processUpdateRequest(asyncResp, std::move(payload),
-                             multipart->uploadData, applyTimeNewVal,
-                             multipart->params.targets);
+        processUpdateRequest(
+            asyncResp, std::move(payload), multipart->uploadData,
+            applyTimeNewVal,
+            multipart->params.targets.value_or(std::vector<std::string>{}));
     }
     else
     {