diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h index d6a3cf3183..5863e9bfad 100644 --- a/include/crm/common/xml.h +++ b/include/crm/common/xml.h @@ -1,73 +1,70 @@ /* * 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); - 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 12ffb61f6c..4ff9cd973c 100644 --- a/include/crm/common/xml_compat.h +++ b/include/crm/common/xml_compat.h @@ -1,105 +1,109 @@ /* * 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); //! \deprecated Do not use void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data); +// NOTE: sbd (as of at least 1.5.2) uses this +//! \deprecated Do not use +xmlNode *get_xpath_object(const char *xpath, xmlNode *xml_obj, int error_level); + #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_COMPAT__H diff --git a/lib/common/xpath.c b/lib/common/xpath.c index d5dfd75494..3263f5a18e 100644 --- a/lib/common/xpath.c +++ b/lib/common/xpath.c @@ -1,593 +1,593 @@ /* * 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 // uint8_t #include #include #include // xmlXPathObject, etc. #include #include #include "crmcommon_private.h" /*! * \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; } } /*! * \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; } /*! * \internal * \brief Run a supplied function for each result of an XPath search * * \param[in,out] doc XML document to search * \param[in] path XPath expression to evaluate in the context of * \p doc * \param[in] fn Function to call for each result XML element * \param[in,out] user_data Data to pass to \p fn * * \note This function processes the result node set in forward order. If \p fn * may free any part of any result node, then it is safer to process the * result node set in reverse order. (The node set is in document order.) * See comments in libxml's examples/xpath2.c file. */ void pcmk__xpath_foreach_result(xmlDoc *doc, const char *path, void (*fn)(xmlNode *, void *), void *user_data) { xmlXPathObject *xpath_obj = NULL; int num_results = 0; CRM_CHECK((doc != NULL) && !pcmk__str_empty(path) && (fn != NULL), return); xpath_obj = pcmk__xpath_search(doc, path); num_results = pcmk__xpath_num_results(xpath_obj); for (int i = 0; i < num_results; i++) { xmlNode *result = pcmk__xpath_result(xpath_obj, i); if (result != NULL) { (*fn)(result, user_data); } } xmlXPathFreeObject(xpath_obj); } /*! * \internal * \brief Search an XML document using an XPath expression and get result node * * This function requires a unique result node from evaluating the XPath * expression. If there are multiple result nodes or no result nodes, it returns * \c NULL. * * \param[in] doc XML document to search * \param[in] path XPath expression to evaluate in the context of \p doc * \param[in] level Log level for errors * * \return Result node from evaluating \p path if unique, or \c NULL otherwise */ xmlNode * pcmk__xpath_find_one(xmlDoc *doc, const char *path, uint8_t level) { int num_results = 0; xmlNode *result = NULL; xmlXPathObject *xpath_obj = NULL; const xmlNode *root = NULL; const char *root_name = "(unknown)"; CRM_CHECK((doc != NULL) && (path != NULL), goto done); xpath_obj = pcmk__xpath_search(doc, path); num_results = pcmk__xpath_num_results(xpath_obj); if (num_results == 1) { result = pcmk__xpath_result(xpath_obj, 0); goto done; } if (level >= LOG_NEVER) { // For no matches or multiple matches, the rest is just logging goto done; } root = xmlDocGetRootElement(doc); if (root != NULL) { root_name = (const char *) root->name; } if (num_results < 1) { do_crm_log(level, "No match for %s in <%s>", path, root_name); if (root != NULL) { crm_log_xml_explicit(root, "no-match"); } goto done; } do_crm_log(level, "Multiple matches for %s in <%s>", path, root_name); for (int i = 0; i < num_results; i++) { xmlNode *match = pcmk__xpath_result(xpath_obj, i); xmlChar *match_path = NULL; if (match == NULL) { CRM_LOG_ASSERT(match != NULL); continue; } match_path = xmlGetNodePath(match); do_crm_log(level, "%s[%d] = %s", path, i, pcmk__s((const char *) match_path, "(unknown)")); free(match_path); } if (root != NULL) { crm_log_xml_explicit(root, "multiple-matches"); } done: xmlXPathFreeObject(xpath_obj); return result; } -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 == 0) { - 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; } } } } 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 == 0) { + 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; +} + // LCOV_EXCL_STOP // End deprecated API