From cbd031ca7f0e3b7061ad8240dc8740d57666c672 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 4 Jan 2024 16:44:12 +0100 Subject: [PATCH] lib/upnp/Action: common UpnpSendAction() wrapper for pupnp and npupnp Merge a lot of duplicate code. --- .../plugins/upnp/ContentDirectoryService.cxx | 205 ++++-------------- src/lib/upnp/Action.cxx | 81 +++++++ src/lib/upnp/Action.hxx | 88 +++++--- src/lib/upnp/ContentDirectoryService.cxx | 50 +---- src/lib/upnp/UniqueIxml.hxx | 21 -- src/lib/upnp/ixmlwrap.cxx | 31 --- src/lib/upnp/ixmlwrap.hxx | 17 -- src/lib/upnp/meson.build | 2 +- 8 files changed, 187 insertions(+), 308 deletions(-) create mode 100644 src/lib/upnp/Action.cxx delete mode 100644 src/lib/upnp/UniqueIxml.hxx delete mode 100644 src/lib/upnp/ixmlwrap.cxx delete mode 100644 src/lib/upnp/ixmlwrap.hxx diff --git a/src/db/plugins/upnp/ContentDirectoryService.cxx b/src/db/plugins/upnp/ContentDirectoryService.cxx index 696e2a523..1760ae9d7 100644 --- a/src/db/plugins/upnp/ContentDirectoryService.cxx +++ b/src/db/plugins/upnp/ContentDirectoryService.cxx @@ -2,33 +2,21 @@ // Copyright The Music Player Daemon Project #include "lib/upnp/ContentDirectoryService.hxx" -#include "lib/upnp/Error.hxx" +#include "lib/upnp/Action.hxx" #include "Directory.hxx" #include "util/NumberParser.hxx" -#include "util/ScopeExit.hxx" -#include "config.h" - -#ifdef USING_PUPNP -#include "lib/upnp/ixmlwrap.hxx" -#include "lib/upnp/Action.hxx" -#include "lib/upnp/UniqueIxml.hxx" -#endif #include -#include - -#ifdef USING_PUPNP static void -ReadResultTag(UPnPDirContent &dirbuf, IXML_Document *response) +ReadResultTag(UPnPDirContent &dirbuf, const UpnpActionResponse &response) { - const char *p = ixmlwrap::getFirstElementValue(response, "Result"); + const char *p = response.GetValue("Result"); if (p == nullptr) p = ""; dirbuf.Parse(p); } -#endif inline void ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl, @@ -37,71 +25,28 @@ ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl, unsigned &didreadp, unsigned &totalp) const { -#ifdef USING_PUPNP // Some devices require an empty SortCriteria, else bad params - IXML_Document *request = - MakeActionHelper("Browse", m_serviceType.c_str(), - "ObjectID", objectId, - "BrowseFlag", "BrowseDirectChildren", - "Filter", "*", - "SortCriteria", "", - "StartingIndex", - fmt::format_int{offset}.c_str(), - "RequestedCount", - fmt::format_int{count}.c_str()); - if (request == nullptr) - throw std::runtime_error("UpnpMakeAction() failed"); + const auto response = UpnpSendAction(hdl, m_actionURL.c_str(), + "Browse", m_serviceType.c_str(), + { + {"ObjectID", objectId}, + {"BrowseFlag", "BrowseDirectChildren"}, + {"Filter", "*"}, + {"SortCriteria", ""}, + {"StartingIndex", fmt::format_int{offset}.c_str()}, + {"RequestedCount", fmt::format_int{count}.c_str()}, + }); - AtScopeExit(request) { ixmlDocument_free(request); }; - - IXML_Document *response; - int code = UpnpSendAction(hdl, m_actionURL.c_str(), m_serviceType.c_str(), - nullptr /*devUDN*/, request, &response); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - AtScopeExit(response) { ixmlDocument_free(response); }; - - const char *value = ixmlwrap::getFirstElementValue(response, "NumberReturned"); + const char *value = response.GetValue("NumberReturned"); didreadp = value != nullptr ? ParseUnsigned(value) : 0; - value = ixmlwrap::getFirstElementValue(response, "TotalMatches"); + value = response.GetValue("TotalMatches"); if (value != nullptr) totalp = ParseUnsigned(value); ReadResultTag(dirbuf, response); -#else - std::vector> actionParams{ - {"ObjectID", objectId}, - {"BrowseFlag", "BrowseDirectChildren"}, - {"Filter", "*"}, - {"SortCriteria", ""}, - {"StartingIndex", fmt::format_int{offset}.c_str()}, - {"RequestedCount", fmt::format_int{count}.c_str()}, - }; - std::vector> responseData; - int errcode; - std::string errdesc; - int code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, "Browse", - actionParams, responseData, &errcode, errdesc); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - const char *p = ""; - didreadp = 0; - for (const auto &entry : responseData) { - if (entry.first == "Result") { - p = entry.second.c_str(); - } else if (entry.first == "TotalMatches") { - totalp = ParseUnsigned(entry.second.c_str()); - } else if (entry.first == "NumberReturned") { - didreadp = ParseUnsigned(entry.second.c_str()); - } - } - dirbuf.Parse(p); -#endif } UPnPDirContent @@ -130,73 +75,29 @@ ContentDirectoryService::search(UpnpClient_Handle hdl, unsigned offset = 0, total = -1, count; do { -#ifdef USING_PUPNP - UniqueIxmlDocument request(MakeActionHelper("Search", m_serviceType.c_str(), - "ContainerID", objectId, - "SearchCriteria", ss, - "Filter", "*", - "SortCriteria", "", - "StartingIndex", - fmt::format_int{offset}.c_str(), - "RequestedCount", "0")); // Setting a value here gets twonky into fits - if (!request) - throw std::runtime_error("UpnpMakeAction() failed"); + const auto response = UpnpSendAction(hdl, m_actionURL.c_str(), + "Search", m_serviceType.c_str(), + { + {"ContainerID", objectId}, + {"SearchCriteria", ss}, + {"Filter", "*"}, + {"SortCriteria", ""}, + {"StartingIndex", fmt::format_int{offset}.c_str()}, + {"RequestedCount", "0"}, // Setting a value here gets twonky into fits + }); - IXML_Document *_response; - auto code = UpnpSendAction(hdl, m_actionURL.c_str(), - m_serviceType.c_str(), - nullptr /*devUDN*/, - request.get(), &_response); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - UniqueIxmlDocument response(_response); - - const char *value = - ixmlwrap::getFirstElementValue(response.get(), - "NumberReturned"); + const char *value = response.GetValue("NumberReturned"); count = value != nullptr ? ParseUnsigned(value) : 0; offset += count; - value = ixmlwrap::getFirstElementValue(response.get(), - "TotalMatches"); + value = response.GetValue("TotalMatches"); if (value != nullptr) total = ParseUnsigned(value); - ReadResultTag(dirbuf, response.get()); -#else - std::vector> actionParams{ - {"ContainerID", objectId}, - {"SearchCriteria", ss}, - {"Filter", "*"}, - {"SortCriteria", ""}, - {"StartingIndex", fmt::format_int{offset}.c_str()}, - {"RequestedCount", "0"}}; - std::vector> responseData; - int errcode; - std::string errdesc; - int code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, "Search", - actionParams, responseData, &errcode, errdesc); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - const char *p = ""; - count = 0; - for (const auto &entry : responseData) { - if (entry.first == "Result") { - p = entry.second.c_str(); - } else if (entry.first == "TotalMatches") { - total = ParseUnsigned(entry.second.c_str()); - } else if (entry.first == "NumberReturned") { - count = ParseUnsigned(entry.second.c_str()); - offset += count; - } - } - dirbuf.Parse(p); -#endif + ReadResultTag(dirbuf, response); } while (count > 0 && offset < total); return dirbuf; @@ -206,46 +107,18 @@ UPnPDirContent ContentDirectoryService::getMetadata(UpnpClient_Handle hdl, const char *objectId) const { -#ifdef USING_PUPNP - // Create request - UniqueIxmlDocument request(MakeActionHelper("Browse", m_serviceType.c_str(), - "ObjectID", objectId, - "BrowseFlag", "BrowseMetadata", - "Filter", "*", - "SortCriteria", "", - "StartingIndex", "0", - "RequestedCount", "1")); - if (request == nullptr) - throw std::runtime_error("UpnpMakeAction() failed"); + const auto response = UpnpSendAction(hdl, m_actionURL.c_str(), + "Browse", m_serviceType.c_str(), + { + {"ObjectID", objectId}, + {"BrowseFlag", "BrowseMetadata"}, + {"Filter", "*"}, + {"SortCriteria", ""}, + {"StartingIndex", "0"}, + {"RequestedCount", "1"}, + }); - IXML_Document *_response; - auto code = UpnpSendAction(hdl, m_actionURL.c_str(), - m_serviceType.c_str(), - nullptr /*devUDN*/, request.get(), &_response); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - UniqueIxmlDocument response(_response); UPnPDirContent dirbuf; - ReadResultTag(dirbuf, response.get()); + ReadResultTag(dirbuf, response); return dirbuf; -#else - std::vector> actionParams{ - {"ObjectID", objectId}, {"BrowseFlag", "BrowseMetadata"}, - {"Filter", "*"}, {"SortCriteria", ""}, - {"StartingIndex", "0"}, {"RequestedCount", "1"}}; - std::vector> responseData; - int errcode; - std::string errdesc; - int code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, "Browse", - actionParams, responseData, &errcode, errdesc); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - auto it = std::find_if(responseData.begin(), responseData.end(), [](auto&& entry){ return entry.first == "Result"; }); - const char *p = it != responseData.end() ? it->second.c_str() : ""; - UPnPDirContent dirbuf; - dirbuf.Parse(p); - return dirbuf; -#endif } diff --git a/src/lib/upnp/Action.cxx b/src/lib/upnp/Action.cxx new file mode 100644 index 000000000..ae6124e1f --- /dev/null +++ b/src/lib/upnp/Action.cxx @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "Action.hxx" +#include "Error.hxx" + +#ifdef USING_PUPNP +#include "util/ScopeExit.hxx" + +#include + +static IXML_Document * +UpnpMakeAction(const char *action_name, const char *service_type, + std::initializer_list> args) +{ + IXML_Document *doc = UpnpMakeAction(action_name, service_type, 0, nullptr, nullptr); + + for (const auto &[name, value] : args) + UpnpAddToAction(&doc, action_name, service_type, name, value); + + return doc; +} + +const char * +UpnpActionResponse::GetValue(const char *name) const noexcept +{ + IXML_NodeList *nodes = ixmlDocument_getElementsByTagName(document, name); + if (!nodes) + return nullptr; + + AtScopeExit(nodes) { ixmlNodeList_free(nodes); }; + + IXML_Node *first = ixmlNodeList_item(nodes, 0); + if (!first) + return nullptr; + + IXML_Node *dnode = ixmlNode_getFirstChild(first); + if (!dnode) + return nullptr; + + return ixmlNode_getNodeValue(dnode); +} + +UpnpActionResponse +UpnpSendAction(UpnpClient_Handle handle, const char *url, + const char *action_name, const char *service_type, + std::initializer_list> args) +{ + IXML_Document *request = UpnpMakeAction(action_name, service_type, args); + AtScopeExit(request) { ixmlDocument_free(request); }; + + IXML_Document *response; + int code = UpnpSendAction(handle, url, service_type, nullptr, + request, &response); + if (code != UPNP_E_SUCCESS) + throw Upnp::MakeError(code, "UpnpSendAction() failed"); + + return UpnpActionResponse{response}; +} + +#else // USING_PUPNP + +UpnpActionResponse +UpnpSendAction(UpnpClient_Handle handle, const char *url, + const char *action_name, const char *service_type, + const std::vector> &args) +{ + std::vector> params{args}; + std::vector> response; + + int errcode; + std::string errdesc; + int code = UpnpSendAction(handle, "", url, service_type, action_name, + params, response, &errcode, errdesc); + if (code != UPNP_E_SUCCESS) + throw Upnp::MakeError(code, "UpnpSendAction() failed"); + + return UpnpActionResponse{std::move(response)}; +} + +#endif // !USING_PUPNP diff --git a/src/lib/upnp/Action.hxx b/src/lib/upnp/Action.hxx index 14d710d06..e90555f0e 100644 --- a/src/lib/upnp/Action.hxx +++ b/src/lib/upnp/Action.hxx @@ -3,33 +3,67 @@ #pragma once -#include +#include "config.h" // for USING_PUPNP -static constexpr unsigned -CountNameValuePairs() noexcept -{ - return 0; -} +#include // for UpnpClient_Handle -template -static constexpr unsigned -CountNameValuePairs([[maybe_unused]] const char *name, [[maybe_unused]] const char *value, - Args... args) noexcept -{ - return 1 + CountNameValuePairs(args...); -} +#include // for std::pair -/** - * A wrapper for UpnpMakeAction() that counts the number of name/value - * pairs and adds the nullptr sentinel. - */ -template -static inline IXML_Document * -MakeActionHelper(const char *action_name, const char *service_type, - Args... args) noexcept -{ - const unsigned n = CountNameValuePairs(args...); - return UpnpMakeAction(action_name, service_type, n, - args..., - nullptr, nullptr); -} +#ifdef USING_PUPNP + +#include + +class UpnpActionResponse { + IXML_Document *document; + +public: + explicit UpnpActionResponse(IXML_Document *_document) noexcept + :document(_document) {} + + ~UpnpActionResponse() noexcept { + ixmlDocument_free(document); + } + + UpnpActionResponse(const UpnpActionResponse &) = delete; + UpnpActionResponse &operator=(const UpnpActionResponse &) = delete; + + [[gnu::pure]] + const char *GetValue(const char *name) const noexcept; +}; + +UpnpActionResponse +UpnpSendAction(UpnpClient_Handle handle, const char *url, + const char *action_name, const char *service_type, + std::initializer_list> args); + +#else // USING_PUPNP + +#include +#include + +class UpnpActionResponse { + std::vector> data; + +public: + explicit UpnpActionResponse(std::vector> &&_data) noexcept + :data(_data) {} + + UpnpActionResponse(const UpnpActionResponse &) = delete; + UpnpActionResponse &operator=(const UpnpActionResponse &) = delete; + + [[gnu::pure]] + const char *GetValue(std::string_view name) const noexcept { + for (const auto &i : data) + if (i.first == name) + return i.second.c_str(); + + return nullptr; + } +}; + +UpnpActionResponse +UpnpSendAction(UpnpClient_Handle handle, const char *url, + const char *action_name, const char *service_type, + const std::vector> &args); + +#endif // !USING_PUPNP diff --git a/src/lib/upnp/ContentDirectoryService.cxx b/src/lib/upnp/ContentDirectoryService.cxx index d6d09aebd..6fb83adfc 100644 --- a/src/lib/upnp/ContentDirectoryService.cxx +++ b/src/lib/upnp/ContentDirectoryService.cxx @@ -2,23 +2,13 @@ // Copyright The Music Player Daemon Project #include "ContentDirectoryService.hxx" +#include "Action.hxx" #include "Device.hxx" -#include "Error.hxx" #include "util/IterableSplitString.hxx" #include "util/UriRelative.hxx" #include "util/UriUtil.hxx" #include "config.h" -#ifdef USING_PUPNP -#include "ixmlwrap.hxx" -#include "Action.hxx" -#include "UniqueIxml.hxx" -#endif - -#include - -#include - using std::string_view_literals::operator""sv; ContentDirectoryService::ContentDirectoryService(const UPnPDevice &device, @@ -42,41 +32,11 @@ ContentDirectoryService::~ContentDirectoryService() noexcept = default; std::forward_list ContentDirectoryService::getSearchCapabilities(UpnpClient_Handle hdl) const { -#ifdef USING_PUPNP - UniqueIxmlDocument request(UpnpMakeAction("GetSearchCapabilities", m_serviceType.c_str(), - 0, - nullptr, nullptr)); - if (!request) - throw std::runtime_error("UpnpMakeAction() failed"); + const auto response = UpnpSendAction(hdl, m_actionURL.c_str(), + "GetSearchCapabilities", m_serviceType.c_str(), + {}); - IXML_Document *_response; - auto code = UpnpSendAction(hdl, m_actionURL.c_str(), - m_serviceType.c_str(), - nullptr /*devUDN*/, request.get(), &_response); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - UniqueIxmlDocument response(_response); - - const char *s = ixmlwrap::getFirstElementValue(response.get(), - "SearchCaps"); -#else - std::vector> responseData; - int errcode; - std::string errdesc; - auto code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, - "GetSearchCapabilities", {}, responseData, &errcode, - errdesc); - if (code != UPNP_E_SUCCESS) - throw Upnp::MakeError(code, "UpnpSendAction() failed"); - - const char *s{nullptr}; - for (auto &entry : responseData) { - if (entry.first == "SearchCaps") { - s = entry.second.c_str(); - } - } -#endif + const char *s = response.GetValue("SearchCaps"); if (s == nullptr || *s == 0) return {}; diff --git a/src/lib/upnp/UniqueIxml.hxx b/src/lib/upnp/UniqueIxml.hxx deleted file mode 100644 index 62e852c8a..000000000 --- a/src/lib/upnp/UniqueIxml.hxx +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -#include - -#include - -struct UpnpIxmlDeleter { - void operator()(IXML_Document *doc) noexcept { - ixmlDocument_free(doc); - } - - void operator()(IXML_NodeList *nl) noexcept { - ixmlNodeList_free(nl); - } -}; - -typedef std::unique_ptr UniqueIxmlDocument; -typedef std::unique_ptr UniqueIxmlNodeList; diff --git a/src/lib/upnp/ixmlwrap.cxx b/src/lib/upnp/ixmlwrap.cxx deleted file mode 100644 index 1b76a0bab..000000000 --- a/src/lib/upnp/ixmlwrap.cxx +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright J.F.Dockes - -#include "config.h" - -#ifdef USING_PUPNP -# include "ixmlwrap.hxx" -# include "UniqueIxml.hxx" - -namespace ixmlwrap { - -const char * -getFirstElementValue(IXML_Document *doc, const char *name) noexcept -{ - UniqueIxmlNodeList nodes(ixmlDocument_getElementsByTagName(doc, name)); - if (!nodes) - return nullptr; - - IXML_Node *first = ixmlNodeList_item(nodes.get(), 0); - if (!first) - return nullptr; - - IXML_Node *dnode = ixmlNode_getFirstChild(first); - if (!dnode) - return nullptr; - - return ixmlNode_getNodeValue(dnode); -} - -} // namespace ixmlwrap -#endif diff --git a/src/lib/upnp/ixmlwrap.hxx b/src/lib/upnp/ixmlwrap.hxx deleted file mode 100644 index 077884552..000000000 --- a/src/lib/upnp/ixmlwrap.hxx +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright J.F.Dockes - -#pragma once - -#include - -namespace ixmlwrap { - /** - * Retrieve the text content for the first element of given - * name. Returns nullptr if the element does not - * contain a text node - */ - const char *getFirstElementValue(IXML_Document *doc, - const char *name) noexcept; - -} diff --git a/src/lib/upnp/meson.build b/src/lib/upnp/meson.build index 85ac8b648..5af5326d4 100644 --- a/src/lib/upnp/meson.build +++ b/src/lib/upnp/meson.build @@ -47,8 +47,8 @@ upnp = static_library( 'Device.cxx', 'ContentDirectoryService.cxx', 'Discovery.cxx', - 'ixmlwrap.cxx', 'Util.cxx', + 'Action.cxx', include_directories: inc, dependencies: [ log_dep,