root/lib/common/xpath.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. pcmk__xpath_result
  2. pcmk__xpath_match_element
  3. pcmk__xpath_search
  4. pcmk__xpath_foreach_result
  5. pcmk__xpath_find_one
  6. pcmk__element_xpath
  7. pcmk__xpath_node_id
  8. output_attr_child
  9. pcmk__warn_multiple_name_matches
  10. xpath_search
  11. getXpathResult
  12. freeXpathObject
  13. dedupXpathResults
  14. crm_foreach_xpath_result
  15. get_xpath_object

   1 /*
   2  * Copyright 2004-2025 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU Lesser General Public License
   7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 #include <crm_internal.h>
  11 #include <stdint.h>                     // uint8_t
  12 #include <stdio.h>
  13 #include <string.h>
  14 
  15 #include <libxml/tree.h>                // xmlNode
  16 #include <libxml/xmlstring.h>           // xmlChar
  17 #include <libxml/xpath.h>               // xmlXPathObject, etc.
  18 
  19 #include <crm/common/xml.h>
  20 #include <crm/common/xml_internal.h>
  21 #include "crmcommon_private.h"
  22 
  23 /*!
  24  * \internal
  25  * \brief Get a node from the result set of evaluating an XPath expression
  26  *
  27  * Evaluating an XPath expression stores the list of matching nodes in an
  28  * \c xmlXPathObject. This function gets the node at a particular index within
  29  * that list.
  30  *
  31  * \param[in,out] xpath_obj  XPath object containing result nodes
  32  * \param[in]     index      Index of result node to get
  33  *
  34  * \return Result node at the given index if possible, or \c NULL otherwise
  35  *
  36  * \note This has a side effect: it sets the result node at \p index to NULL
  37  *       within \p xpath_obj, so the result at a given index can be retrieved
  38  *       only once. This is a workaround to prevent a use-after-free error.
  39  *
  40  *       All elements returned by an XPath query are pointers to elements from
  41  *       the tree, except namespace nodes (which are allocated separately for
  42  *       the XPath object's node set). Accordingly, only namespace nodes and the
  43  *       node set itself are freed when libxml2 frees a node set.
  44  *
  45  *       This logic requires checking the type of every node in the node set.
  46  *       However, a node may have been freed already while processing an XPath
  47  *       object -- either directly (for example, with \c xmlFreeNode()) or
  48  *       indirectly (for example, with \c xmlNodeSetContent()). In that case,
  49  *       checking the freed node's type while freeing the XPath object is a
  50  *       use-after-free error.
  51  *
  52  *       To reduce the likelihood of this, when we access a node in the XPath
  53  *       object, we remove it from the XPath object's node set by setting it to
  54  *       \c NULL. This approach is adapted from \c xpath2.c in libxml2's
  55  *       examples. That file also describes a way to reproduce the
  56  *       use-after-free error.
  57  *
  58  *       However, there are still ways that a use-after-free can occur. For
  59  *       example, freeing the entire XML tree before freeing an XPath object
  60  *       that contains pointers to it would be an error. It's dangerous to mix
  61  *       processing XPath search results with modifications to a tree, and it
  62  *       must be done with care.
  63  */
  64 xmlNode *
  65 pcmk__xpath_result(xmlXPathObject *xpath_obj, int index)
     /* [previous][next][first][last][top][bottom][index][help] */
  66 {
  67     xmlNode *match = NULL;
  68 
  69     CRM_CHECK((xpath_obj != NULL) && (index >= 0), return NULL);
  70 
  71     match = xmlXPathNodeSetItem(xpath_obj->nodesetval, index);
  72     if (match == NULL) {
  73         // Previously requested or out of range
  74         return NULL;
  75     }
  76 
  77     if (match->type != XML_NAMESPACE_DECL) {
  78         xpath_obj->nodesetval->nodeTab[index] = NULL;
  79     }
  80 
  81     return match;
  82 }
  83 
  84 /*!
  85  * \internal
  86  * \brief Get an element node corresponding to an XPath match node
  87  *
  88  * Each node in an XPath object's result node set may be of an arbitrary type.
  89  * This function is guaranteed to return an element node (or \c NULL).
  90  *
  91  * \param[in] match  XML node that matched some XPath expression
  92  *
  93  * \retval \p match if \p match is an element
  94  * \retval Root element of \p match if \p match is a document
  95  * \retval <tt>match->parent</tt> if \p match is not an element but its parent
  96  *         is an element
  97  * \retval \c NULL otherwise
  98  *
  99  * \todo Phase this out. Code that relies on this behavior is likely buggy.
 100  */
 101 xmlNode *
 102 pcmk__xpath_match_element(xmlNode *match)
     /* [previous][next][first][last][top][bottom][index][help] */
 103 {
 104     pcmk__assert(match != NULL);
 105 
 106     switch (match->type) {
 107         case XML_ELEMENT_NODE:
 108             return match;
 109 
 110         case XML_DOCUMENT_NODE:
 111             // Happens if XPath expression is "/"; return root element instead
 112             return xmlDocGetRootElement((xmlDoc *) match);
 113 
 114         default:
 115             if ((match->parent != NULL)
 116                 && (match->parent->type == XML_ELEMENT_NODE)) {
 117 
 118                 // Probably an attribute; return parent element instead
 119                 return match->parent;
 120             }
 121             crm_err("Cannot get element from XPath expression match of type %s",
 122                     pcmk__xml_element_type_text(match->type));
 123             return NULL;
 124     }
 125 }
 126 
 127 /*!
 128  * \internal
 129  * \brief Search an XML document using an XPath expression
 130  *
 131  * \param[in] doc   XML document to search
 132  * \param[in] path  XPath expression to evaluate in the context of \p doc
 133  *
 134  * \return XPath object containing result of evaluating \p path against \p doc
 135  */
 136 xmlXPathObject *
 137 pcmk__xpath_search(xmlDoc *doc, const char *path)
     /* [previous][next][first][last][top][bottom][index][help] */
 138 {
 139     const xmlChar *xpath_expr = (const xmlChar *) path;
 140     xmlXPathContext *xpath_context = NULL;
 141     xmlXPathObject *xpath_obj = NULL;
 142 
 143     CRM_CHECK((doc != NULL) && !pcmk__str_empty(path), return NULL);
 144 
 145     xpath_context = xmlXPathNewContext(doc);
 146     pcmk__mem_assert(xpath_context);
 147 
 148     xpath_obj = xmlXPathEval(xpath_expr, xpath_context);
 149 
 150     xmlXPathFreeContext(xpath_context);
 151     return xpath_obj;
 152 }
 153 
 154 /*!
 155  * \internal
 156  * \brief Run a supplied function for each result of an XPath search
 157  *
 158  * \param[in,out] doc        XML document to search
 159  * \param[in]     path       XPath expression to evaluate in the context of
 160  *                           \p doc
 161  * \param[in]     fn         Function to call for each result XML element
 162  * \param[in,out] user_data  Data to pass to \p fn
 163  *
 164  * \note This function processes the result node set in forward order. If \p fn
 165  *       may free any part of any result node, then it is safer to process the
 166  *       result node set in reverse order. (The node set is in document order.)
 167  *       See comments in libxml's <tt>examples/xpath2.c</tt> file.
 168  */
 169 void
 170 pcmk__xpath_foreach_result(xmlDoc *doc, const char *path,
     /* [previous][next][first][last][top][bottom][index][help] */
 171                            void (*fn)(xmlNode *, void *), void *user_data)
 172 {
 173     xmlXPathObject *xpath_obj = NULL;
 174     int num_results = 0;
 175 
 176     CRM_CHECK((doc != NULL) && !pcmk__str_empty(path) && (fn != NULL), return);
 177 
 178     xpath_obj = pcmk__xpath_search(doc, path);
 179     num_results = pcmk__xpath_num_results(xpath_obj);
 180 
 181     for (int i = 0; i < num_results; i++) {
 182         xmlNode *result = pcmk__xpath_result(xpath_obj, i);
 183 
 184         if (result != NULL) {
 185             (*fn)(result, user_data);
 186         }
 187     }
 188     xmlXPathFreeObject(xpath_obj);
 189 }
 190 
 191 /*!
 192  * \internal
 193  * \brief Search an XML document using an XPath expression and get result node
 194  *
 195  * This function requires a unique result node from evaluating the XPath
 196  * expression. If there are multiple result nodes or no result nodes, it returns
 197  * \c NULL.
 198  *
 199  * \param[in] doc    XML document to search
 200  * \param[in] path   XPath expression to evaluate in the context of \p doc
 201  * \param[in] level  Log level for errors
 202  *
 203  * \return Result node from evaluating \p path if unique, or \c NULL otherwise
 204  */
 205 xmlNode *
 206 pcmk__xpath_find_one(xmlDoc *doc, const char *path, uint8_t level)
     /* [previous][next][first][last][top][bottom][index][help] */
 207 {
 208     int num_results = 0;
 209     xmlNode *result = NULL;
 210     xmlXPathObject *xpath_obj = NULL;
 211     const xmlNode *root = NULL;
 212     const char *root_name = "(unknown)";
 213 
 214     CRM_CHECK((doc != NULL) && (path != NULL), goto done);
 215 
 216     xpath_obj = pcmk__xpath_search(doc, path);
 217     num_results = pcmk__xpath_num_results(xpath_obj);
 218 
 219     if (num_results == 1) {
 220         result = pcmk__xpath_result(xpath_obj, 0);
 221         goto done;
 222     }
 223 
 224     if (level >= LOG_NEVER) {
 225         // For no matches or multiple matches, the rest is just logging
 226         goto done;
 227     }
 228 
 229     root = xmlDocGetRootElement(doc);
 230     if (root != NULL) {
 231         root_name = (const char *) root->name;
 232     }
 233 
 234     if (num_results < 1) {
 235         do_crm_log(level, "No match for %s in <%s>", path, root_name);
 236 
 237         if (root != NULL) {
 238             crm_log_xml_explicit(root, "no-match");
 239         }
 240         goto done;
 241     }
 242 
 243     do_crm_log(level, "Multiple matches for %s in <%s>", path, root_name);
 244 
 245     for (int i = 0; i < num_results; i++) {
 246         xmlNode *match = pcmk__xpath_result(xpath_obj, i);
 247         xmlChar *match_path = NULL;
 248 
 249         if (match == NULL) {
 250             CRM_LOG_ASSERT(match != NULL);
 251             continue;
 252         }
 253 
 254         match_path = xmlGetNodePath(match);
 255         do_crm_log(level, "%s[%d] = %s",
 256                    path, i, pcmk__s((const char *) match_path, "(unknown)"));
 257         free(match_path);
 258     }
 259 
 260     if (root != NULL) {
 261         crm_log_xml_explicit(root, "multiple-matches");
 262     }
 263 
 264 done:
 265     xmlXPathFreeObject(xpath_obj);
 266     return result;
 267 }
 268 
 269 /*!
 270  * \internal
 271  * \brief Get an XPath string that matches an XML element as closely as possible
 272  *
 273  * \param[in] xml  The XML element for which to build an XPath string
 274  *
 275  * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL.
 276  *
 277  * \note The caller is responsible for freeing the string using
 278  *       \p g_string_free().
 279  */
 280 GString *
 281 pcmk__element_xpath(const xmlNode *xml)
     /* [previous][next][first][last][top][bottom][index][help] */
 282 {
 283     const xmlNode *parent = NULL;
 284     GString *xpath = NULL;
 285     const char *id = NULL;
 286 
 287     if (xml == NULL) {
 288         return NULL;
 289     }
 290 
 291     parent = xml->parent;
 292     xpath = pcmk__element_xpath(parent);
 293     if (xpath == NULL) {
 294         xpath = g_string_sized_new(256);
 295     }
 296 
 297     // Build xpath like "/" -> "/cib" -> "/cib/configuration"
 298     if (parent == NULL) {
 299         g_string_append_c(xpath, '/');
 300     } else if (parent->parent == NULL) {
 301         g_string_append(xpath, (const gchar *) xml->name);
 302     } else {
 303         pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL);
 304     }
 305 
 306     id = pcmk__xe_id(xml);
 307     if (id != NULL) {
 308         pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL);
 309     }
 310 
 311     return xpath;
 312 }
 313 
 314 /*!
 315  * \internal
 316  * \brief Extract the ID attribute from an XML element
 317  *
 318  * \param[in] xpath String to search
 319  * \param[in] node  Node to get the ID for
 320  *
 321  * \return ID attribute of \p node in xpath string \p xpath
 322  */
 323 char *
 324 pcmk__xpath_node_id(const char *xpath, const char *node)
     /* [previous][next][first][last][top][bottom][index][help] */
 325 {
 326     char *retval = NULL;
 327     char *patt = NULL;
 328     char *start = NULL;
 329     char *end = NULL;
 330 
 331     if (node == NULL || xpath == NULL) {
 332         return retval;
 333     }
 334 
 335     patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node);
 336     start = strstr(xpath, patt);
 337 
 338     if (!start) {
 339         free(patt);
 340         return retval;
 341     }
 342 
 343     start += strlen(patt);
 344     start++;
 345 
 346     end = strstr(start, "\'");
 347     pcmk__assert(end != NULL);
 348     retval = strndup(start, end-start);
 349 
 350     free(patt);
 351     return retval;
 352 }
 353 
 354 static int
 355 output_attr_child(xmlNode *child, void *userdata)
     /* [previous][next][first][last][top][bottom][index][help] */
 356 {
 357     pcmk__output_t *out = userdata;
 358 
 359     out->info(out, "  Value: %s \t(id=%s)",
 360               crm_element_value(child, PCMK_XA_VALUE),
 361               pcmk__s(pcmk__xe_id(child), "<none>"));
 362     return pcmk_rc_ok;
 363 }
 364 
 365 /*!
 366  * \internal
 367  * \brief Warn if an XPath query returned multiple nodes with the same ID
 368  *
 369  * \param[in,out] out     Output object
 370  * \param[in]     search  XPath search result, most typically the result of
 371  *                        calling <tt>cib->cmds->query()</tt>.
 372  * \param[in]     name    Name searched for
 373  */
 374 void
 375 pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
     /* [previous][next][first][last][top][bottom][index][help] */
 376                                  const char *name)
 377 {
 378     if (out == NULL || name == NULL || search == NULL ||
 379         search->children == NULL) {
 380         return;
 381     }
 382 
 383     out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name);
 384     pcmk__xe_foreach_child(search, NULL, output_attr_child, out);
 385 }
 386 
 387 // Deprecated functions kept only for backward API compatibility
 388 // LCOV_EXCL_START
 389 
 390 #include <crm/common/xml_compat.h>
 391 
 392 xmlXPathObjectPtr
 393 xpath_search(const xmlNode *xml_top, const char *path)
     /* [previous][next][first][last][top][bottom][index][help] */
 394 {
 395     CRM_CHECK(xml_top != NULL, return NULL);
 396 
 397     return pcmk__xpath_search(xml_top->doc, path);
 398 }
 399 
 400 xmlNode *
 401 getXpathResult(xmlXPathObjectPtr xpathObj, int index)
     /* [previous][next][first][last][top][bottom][index][help] */
 402 {
 403     xmlNode *match = NULL;
 404     int max = pcmk__xpath_num_results(xpathObj);
 405 
 406     CRM_CHECK(index >= 0, return NULL);
 407     CRM_CHECK(xpathObj != NULL, return NULL);
 408 
 409     if (index >= max) {
 410         crm_err("Requested index %d of only %d items", index, max);
 411         return NULL;
 412 
 413     } else if(xpathObj->nodesetval->nodeTab[index] == NULL) {
 414         /* Previously requested */
 415         return NULL;
 416     }
 417 
 418     match = xpathObj->nodesetval->nodeTab[index];
 419     CRM_CHECK(match != NULL, return NULL);
 420 
 421     if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) {
 422         // See the comment for pcmk__xpath_result()
 423         xpathObj->nodesetval->nodeTab[index] = NULL;
 424     }
 425 
 426     switch (match->type) {
 427         case XML_ELEMENT_NODE:
 428             return match;
 429 
 430         case XML_DOCUMENT_NODE: // Searched for '/'
 431             return match->children;
 432 
 433         default:
 434            if ((match->parent != NULL)
 435                && (match->parent->type == XML_ELEMENT_NODE)) {
 436                 return match->parent;
 437            }
 438            crm_warn("Unsupported XPath match type %d (bug?)", match->type);
 439            return NULL;
 440     }
 441 }
 442 
 443 void
 444 freeXpathObject(xmlXPathObjectPtr xpathObj)
     /* [previous][next][first][last][top][bottom][index][help] */
 445 {
 446     int max = pcmk__xpath_num_results(xpathObj);
 447 
 448     if (xpathObj == NULL) {
 449         return;
 450     }
 451 
 452     for (int lpc = 0; lpc < max; lpc++) {
 453         if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) {
 454             xpathObj->nodesetval->nodeTab[lpc] = NULL;
 455         }
 456     }
 457 
 458     /* _Now_ it's safe to free it */
 459     xmlXPathFreeObject(xpathObj);
 460 }
 461 
 462 void
 463 dedupXpathResults(xmlXPathObjectPtr xpathObj)
     /* [previous][next][first][last][top][bottom][index][help] */
 464 {
 465     int max = pcmk__xpath_num_results(xpathObj);
 466 
 467     if (xpathObj == NULL) {
 468         return;
 469     }
 470 
 471     for (int lpc = 0; lpc < max; lpc++) {
 472         xmlNode *xml = NULL;
 473         gboolean dedup = FALSE;
 474 
 475         if (xpathObj->nodesetval->nodeTab[lpc] == NULL) {
 476             continue;
 477         }
 478 
 479         xml = xpathObj->nodesetval->nodeTab[lpc]->parent;
 480 
 481         for (; xml; xml = xml->parent) {
 482             int lpc2 = 0;
 483 
 484             for (lpc2 = 0; lpc2 < max; lpc2++) {
 485                 if (xpathObj->nodesetval->nodeTab[lpc2] == xml) {
 486                     xpathObj->nodesetval->nodeTab[lpc] = NULL;
 487                     dedup = TRUE;
 488                     break;
 489                 }
 490             }
 491 
 492             if (dedup) {
 493                 break;
 494             }
 495         }
 496     }
 497 }
 498 
 499 void
 500 crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
     /* [previous][next][first][last][top][bottom][index][help] */
 501                          void (*helper)(xmlNode*, void*), void *user_data)
 502 {
 503     xmlXPathObject *xpathObj = NULL;
 504     int nresults = 0;
 505 
 506     CRM_CHECK(xml != NULL, return);
 507 
 508     xpathObj = pcmk__xpath_search(xml->doc, xpath);
 509     nresults = pcmk__xpath_num_results(xpathObj);
 510 
 511     for (int i = 0; i < nresults; i++) {
 512         xmlNode *result = pcmk__xpath_result(xpathObj, i);
 513 
 514         CRM_LOG_ASSERT(result != NULL);
 515 
 516         if (result != NULL) {
 517             result = pcmk__xpath_match_element(result);
 518 
 519             CRM_LOG_ASSERT(result != NULL);
 520 
 521             if (result != NULL) {
 522                 (*helper)(result, user_data);
 523             }
 524         }
 525     }
 526     xmlXPathFreeObject(xpathObj);
 527 }
 528 
 529 xmlNode *
 530 get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
     /* [previous][next][first][last][top][bottom][index][help] */
 531 {
 532     int max;
 533     xmlNode *result = NULL;
 534     xmlXPathObject *xpathObj = NULL;
 535     char *nodePath = NULL;
 536     char *matchNodePath = NULL;
 537 
 538     if (xpath == NULL) {
 539         return xml_obj;         /* or return NULL? */
 540     }
 541 
 542     xpathObj = pcmk__xpath_search(xml_obj->doc, xpath);
 543     nodePath = (char *)xmlGetNodePath(xml_obj);
 544     max = pcmk__xpath_num_results(xpathObj);
 545 
 546     if (max == 0) {
 547         if (error_level < LOG_NEVER) {
 548             do_crm_log(error_level, "No match for %s in %s",
 549                        xpath, pcmk__s(nodePath, "unknown path"));
 550             crm_log_xml_explicit(xml_obj, "Unexpected Input");
 551         }
 552 
 553     } else if (max > 1) {
 554         if (error_level < LOG_NEVER) {
 555             int lpc = 0;
 556 
 557             do_crm_log(error_level, "Too many matches for %s in %s",
 558                        xpath, pcmk__s(nodePath, "unknown path"));
 559 
 560             for (lpc = 0; lpc < max; lpc++) {
 561                 xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
 562 
 563                 CRM_LOG_ASSERT(match != NULL);
 564                 if (match != NULL) {
 565                     match = pcmk__xpath_match_element(match);
 566 
 567                     CRM_LOG_ASSERT(match != NULL);
 568                     if (match != NULL) {
 569                         matchNodePath = (char *) xmlGetNodePath(match);
 570                         do_crm_log(error_level, "%s[%d] = %s",
 571                                    xpath, lpc,
 572                                    pcmk__s(matchNodePath,
 573                                            "unrecognizable match"));
 574                         free(matchNodePath);
 575                     }
 576                 }
 577             }
 578             crm_log_xml_explicit(xml_obj, "Bad Input");
 579         }
 580 
 581     } else {
 582         result = pcmk__xpath_result(xpathObj, 0);
 583         if (result != NULL) {
 584             result = pcmk__xpath_match_element(result);
 585         }
 586     }
 587 
 588     xmlXPathFreeObject(xpathObj);
 589     free(nodePath);
 590 
 591     return result;
 592 }
 593 
 594 // LCOV_EXCL_STOP
 595 // End deprecated API

/* [previous][next][first][last][top][bottom][index][help] */