diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h index cf83ebde79..68c414493b 100644 --- a/include/crm/common/xml.h +++ b/include/crm/common/xml.h @@ -1,78 +1,76 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_XML__H #define PCMK__CRM_COMMON_XML__H #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Wrappers for and extensions to libxml2 * \ingroup core */ typedef const xmlChar *pcmkXmlStr; /* * Searching & Modifying */ // NOTE: sbd (as of at least 1.5.2) uses this xmlNode *get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level); void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data); -void dedupXpathResults(xmlXPathObjectPtr xpathObj); - bool xml_tracking_changes(xmlNode * xml); bool xml_document_dirty(xmlNode *xml); void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls); void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_accept_changes(xmlNode * xml); bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]); xmlNode *xml_create_patchset( int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version); int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version); void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest); #ifdef __cplusplus } #endif #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #endif diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h index 555db4389f..038179ecc1 100644 --- a/include/crm/common/xml_compat.h +++ b/include/crm/common/xml_compat.h @@ -1,98 +1,101 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_XML_COMPAT__H #define PCMK__CRM_COMMON_XML_COMPAT__H #include // gboolean #include // xmlNode #include // xmlXPathObject #include // crm_xml_add() #include // PCMK_XE_CLONE #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Deprecated Pacemaker XML API * \ingroup core * \deprecated Do not include this header directly. The XML APIs in this * header, and the header itself, will be removed in a future * release. */ // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Use name member directly static inline const char * crm_element_name(const xmlNode *xml) { return (xml == NULL)? NULL : (const char *) xml->name; } // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *copy_xml(xmlNode *src_node); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Call \c crm_log_init() or \c crm_log_cli_init() instead void crm_xml_init(void); //! \deprecated Exit with \c crm_exit() instead void crm_xml_cleanup(void); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void pcmk_free_xml_subtree(xmlNode *xml); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void free_xml(xmlNode *child); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void crm_xml_sanitize_id(char *id); //! \deprecated Do not use char *calculate_on_disk_digest(xmlNode *input); //! \deprecated Do not use char *calculate_operation_digest(xmlNode *input, const char *version); //! \deprecated Do not use char *calculate_xml_versioned_digest(xmlNode *input, gboolean sort, gboolean do_filter, const char *version); //! \deprecated Do not use xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path); //! \deprecated Do not use static inline int numXpathResults(xmlXPathObjectPtr xpathObj) { if ((xpathObj == NULL) || (xpathObj->nodesetval == NULL)) { return 0; } return xpathObj->nodesetval->nodeNr; } //! \deprecated Do not use xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index); //! \deprecated Do not use void freeXpathObject(xmlXPathObjectPtr xpathObj); +//! \deprecated Do not use +void dedupXpathResults(xmlXPathObjectPtr xpathObj); + #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_COMPAT__H diff --git a/lib/common/xpath.c b/lib/common/xpath.c index 25aa259d28..d3654cfbf9 100644 --- a/lib/common/xpath.c +++ b/lib/common/xpath.c @@ -1,488 +1,488 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include "crmcommon_private.h" #include // xmlXPathObject, etc. /*! * \internal * \brief Get a node from the result set of evaluating an XPath expression * * Evaluating an XPath expression stores the list of matching nodes in an * \c xmlXPathObject. This function gets the node at a particular index within * that list. * * \param[in,out] xpath_obj XPath object containing result nodes * \param[in] index Index of result node to get * * \return Result node at the given index if possible, or \c NULL otherwise * * \note This has a side effect: it sets the result node at \p index to NULL * within \p xpath_obj, so the result at a given index can be retrieved * only once. This is a workaround to prevent a use-after-free error. * * All elements returned by an XPath query are pointers to elements from * the tree, except namespace nodes (which are allocated separately for * the XPath object's node set). Accordingly, only namespace nodes and the * node set itself are freed when libxml2 frees a node set. * * This logic requires checking the type of every node in the node set. * However, a node may have been freed already while processing an XPath * object -- either directly (for example, with \c xmlFreeNode()) or * indirectly (for example, with \c xmlNodeSetContent()). In that case, * checking the freed node's type while freeing the XPath object is a * use-after-free error. * * To reduce the likelihood of this, when we access a node in the XPath * object, we remove it from the XPath object's node set by setting it to * \c NULL. This approach is adapted from \c xpath2.c in libxml2's * examples. That file also describes a way to reproduce the * use-after-free error. * * However, there are still ways that a use-after-free can occur. For * example, freeing the entire XML tree before freeing an XPath object * that contains pointers to it would be an error. It's dangerous to mix * processing XPath search results with modifications to a tree, and it * must be done with care. */ xmlNode * pcmk__xpath_result(xmlXPathObject *xpath_obj, int index) { xmlNode *match = NULL; CRM_CHECK((xpath_obj != NULL) && (index >= 0), return NULL); match = xmlXPathNodeSetItem(xpath_obj->nodesetval, index); if (match == NULL) { // Previously requested or out of range return NULL; } if (match->type != XML_NAMESPACE_DECL) { xpath_obj->nodesetval->nodeTab[index] = NULL; } return match; } /*! * \internal * \brief Get an element node corresponding to an XPath match node * * Each node in an XPath object's result node set may be of an arbitrary type. * This function is guaranteed to return an element node (or \c NULL). * * \param[in] match XML node that matched some XPath expression * * \retval \p match if \p match is an element * \retval Root element of \p match if \p match is a document * \retval match->parent if \p match is not an element but its parent * is an element * \retval \c NULL otherwise * * \todo Phase this out. Code that relies on this behavior is likely buggy. */ xmlNode * pcmk__xpath_match_element(xmlNode *match) { pcmk__assert(match != NULL); switch (match->type) { case XML_ELEMENT_NODE: return match; case XML_DOCUMENT_NODE: // Happens if XPath expression is "/"; return root element instead return xmlDocGetRootElement((xmlDoc *) match); default: if ((match->parent != NULL) && (match->parent->type == XML_ELEMENT_NODE)) { // Probably an attribute; return parent element instead return match->parent; } crm_err("Cannot get element from XPath expression match of type %s", pcmk__xml_element_type_text(match->type)); return NULL; } } -void -dedupXpathResults(xmlXPathObjectPtr xpathObj) -{ - int max = pcmk__xpath_num_results(xpathObj); - - if (xpathObj == NULL) { - return; - } - - for (int lpc = 0; lpc < max; lpc++) { - xmlNode *xml = NULL; - gboolean dedup = FALSE; - - if (xpathObj->nodesetval->nodeTab[lpc] == NULL) { - continue; - } - - xml = xpathObj->nodesetval->nodeTab[lpc]->parent; - - for (; xml; xml = xml->parent) { - int lpc2 = 0; - - for (lpc2 = 0; lpc2 < max; lpc2++) { - if (xpathObj->nodesetval->nodeTab[lpc2] == xml) { - xpathObj->nodesetval->nodeTab[lpc] = NULL; - dedup = TRUE; - break; - } - } - - if (dedup) { - break; - } - } - } -} - /*! * \internal * \brief Search an XML document using an XPath expression * * \param[in] doc XML document to search * \param[in] path XPath expression to evaluate in the context of \p doc * * \return XPath object containing result of evaluating \p path against \p doc */ xmlXPathObject * pcmk__xpath_search(xmlDoc *doc, const char *path) { pcmkXmlStr xpath_expr = (pcmkXmlStr) path; xmlXPathContext *xpath_context = NULL; xmlXPathObject *xpath_obj = NULL; CRM_CHECK((doc != NULL) && !pcmk__str_empty(path), return NULL); xpath_context = xmlXPathNewContext(doc); pcmk__mem_assert(xpath_context); xpath_obj = xmlXPathEval(xpath_expr, xpath_context); xmlXPathFreeContext(xpath_context); return xpath_obj; } /*! * \brief Run a supplied function for each result of an xpath search * * \param[in,out] xml XML to search * \param[in] xpath XPath search string * \param[in] helper Function to call for each result * \param[in,out] user_data Data to pass to supplied function * * \note The helper function will be passed the XML node of the result, * and the supplied user_data. This function does not otherwise * use user_data. */ void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data) { xmlXPathObject *xpathObj = NULL; int nresults = 0; CRM_CHECK(xml != NULL, return); xpathObj = pcmk__xpath_search(xml->doc, xpath); nresults = pcmk__xpath_num_results(xpathObj); for (int i = 0; i < nresults; i++) { xmlNode *result = pcmk__xpath_result(xpathObj, i); CRM_LOG_ASSERT(result != NULL); if (result != NULL) { result = pcmk__xpath_match_element(result); CRM_LOG_ASSERT(result != NULL); if (result != NULL) { (*helper)(result, user_data); } } } xmlXPathFreeObject(xpathObj); } xmlNode * get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level) { int max; xmlNode *result = NULL; xmlXPathObject *xpathObj = NULL; char *nodePath = NULL; char *matchNodePath = NULL; if (xpath == NULL) { return xml_obj; /* or return NULL? */ } xpathObj = pcmk__xpath_search(xml_obj->doc, xpath); nodePath = (char *)xmlGetNodePath(xml_obj); max = pcmk__xpath_num_results(xpathObj); if (max < 1) { if (error_level < LOG_NEVER) { do_crm_log(error_level, "No match for %s in %s", xpath, pcmk__s(nodePath, "unknown path")); crm_log_xml_explicit(xml_obj, "Unexpected Input"); } } else if (max > 1) { if (error_level < LOG_NEVER) { int lpc = 0; do_crm_log(error_level, "Too many matches for %s in %s", xpath, pcmk__s(nodePath, "unknown path")); for (lpc = 0; lpc < max; lpc++) { xmlNode *match = pcmk__xpath_result(xpathObj, lpc); CRM_LOG_ASSERT(match != NULL); if (match != NULL) { match = pcmk__xpath_match_element(match); CRM_LOG_ASSERT(match != NULL); if (match != NULL) { matchNodePath = (char *) xmlGetNodePath(match); do_crm_log(error_level, "%s[%d] = %s", xpath, lpc, pcmk__s(matchNodePath, "unrecognizable match")); free(matchNodePath); } } } crm_log_xml_explicit(xml_obj, "Bad Input"); } } else { result = pcmk__xpath_result(xpathObj, 0); if (result != NULL) { result = pcmk__xpath_match_element(result); } } xmlXPathFreeObject(xpathObj); free(nodePath); return result; } /*! * \internal * \brief Get an XPath string that matches an XML element as closely as possible * * \param[in] xml The XML element for which to build an XPath string * * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL. * * \note The caller is responsible for freeing the string using * \p g_string_free(). */ GString * pcmk__element_xpath(const xmlNode *xml) { const xmlNode *parent = NULL; GString *xpath = NULL; const char *id = NULL; if (xml == NULL) { return NULL; } parent = xml->parent; xpath = pcmk__element_xpath(parent); if (xpath == NULL) { xpath = g_string_sized_new(256); } // Build xpath like "/" -> "/cib" -> "/cib/configuration" if (parent == NULL) { g_string_append_c(xpath, '/'); } else if (parent->parent == NULL) { g_string_append(xpath, (const gchar *) xml->name); } else { pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL); } id = pcmk__xe_id(xml); if (id != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL); } return xpath; } /*! * \internal * \brief Extract the ID attribute from an XML element * * \param[in] xpath String to search * \param[in] node Node to get the ID for * * \return ID attribute of \p node in xpath string \p xpath */ char * pcmk__xpath_node_id(const char *xpath, const char *node) { char *retval = NULL; char *patt = NULL; char *start = NULL; char *end = NULL; if (node == NULL || xpath == NULL) { return retval; } patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node); start = strstr(xpath, patt); if (!start) { free(patt); return retval; } start += strlen(patt); start++; end = strstr(start, "\'"); pcmk__assert(end != NULL); retval = strndup(start, end-start); free(patt); return retval; } static int output_attr_child(xmlNode *child, void *userdata) { pcmk__output_t *out = userdata; out->info(out, " Value: %s \t(id=%s)", crm_element_value(child, PCMK_XA_VALUE), pcmk__s(pcmk__xe_id(child), "")); return pcmk_rc_ok; } /*! * \internal * \brief Warn if an XPath query returned multiple nodes with the same ID * * \param[in,out] out Output object * \param[in] search XPath search result, most typically the result of * calling cib->cmds->query(). * \param[in] name Name searched for */ void pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search, const char *name) { if (out == NULL || name == NULL || search == NULL || search->children == NULL) { return; } out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name); pcmk__xe_foreach_child(search, NULL, output_attr_child, out); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path) { CRM_CHECK(xml_top != NULL, return NULL); return pcmk__xpath_search(xml_top->doc, path); } xmlNode * getXpathResult(xmlXPathObjectPtr xpathObj, int index) { xmlNode *match = NULL; int max = pcmk__xpath_num_results(xpathObj); CRM_CHECK(index >= 0, return NULL); CRM_CHECK(xpathObj != NULL, return NULL); if (index >= max) { crm_err("Requested index %d of only %d items", index, max); return NULL; } else if(xpathObj->nodesetval->nodeTab[index] == NULL) { /* Previously requested */ return NULL; } match = xpathObj->nodesetval->nodeTab[index]; CRM_CHECK(match != NULL, return NULL); if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) { // See the comment for pcmk__xpath_result() xpathObj->nodesetval->nodeTab[index] = NULL; } switch (match->type) { case XML_ELEMENT_NODE: return match; case XML_DOCUMENT_NODE: // Searched for '/' return match->children; default: if ((match->parent != NULL) && (match->parent->type == XML_ELEMENT_NODE)) { return match->parent; } crm_warn("Unsupported XPath match type %d (bug?)", match->type); return NULL; } } void freeXpathObject(xmlXPathObjectPtr xpathObj) { int max = pcmk__xpath_num_results(xpathObj); if (xpathObj == NULL) { return; } for (int lpc = 0; lpc < max; lpc++) { if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) { xpathObj->nodesetval->nodeTab[lpc] = NULL; } } /* _Now_ it's safe to free it */ xmlXPathFreeObject(xpathObj); } +void +dedupXpathResults(xmlXPathObjectPtr xpathObj) +{ + int max = pcmk__xpath_num_results(xpathObj); + + if (xpathObj == NULL) { + return; + } + + for (int lpc = 0; lpc < max; lpc++) { + xmlNode *xml = NULL; + gboolean dedup = FALSE; + + if (xpathObj->nodesetval->nodeTab[lpc] == NULL) { + continue; + } + + xml = xpathObj->nodesetval->nodeTab[lpc]->parent; + + for (; xml; xml = xml->parent) { + int lpc2 = 0; + + for (lpc2 = 0; lpc2 < max; lpc2++) { + if (xpathObj->nodesetval->nodeTab[lpc2] == xml) { + xpathObj->nodesetval->nodeTab[lpc] = NULL; + dedup = TRUE; + break; + } + } + + if (dedup) { + break; + } + } + } +} + // LCOV_EXCL_STOP // End deprecated API