diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index b53810f3f3..005bfd62f7 100644 --- a/include/crm/common/Makefile.am +++ b/include/crm/common/Makefile.am @@ -1,47 +1,48 @@ # # Copyright 2004-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # MAINTAINERCLEANFILES = Makefile.in headerdir=$(pkgincludedir)/crm/common header_HEADERS = acl.h \ actions.h \ agents.h \ cib.h \ ipc.h \ ipc_controld.h \ ipc_pacemakerd.h \ ipc_schedulerd.h \ iso8601.h \ logging.h \ logging_compat.h \ mainloop.h \ nodes.h \ nvpair.h \ + nvpair_compat.h \ options.h \ output.h \ probes.h \ resources.h \ results.h \ roles.h \ rules.h \ scheduler.h \ scheduler_types.h \ schemas.h \ scores.h \ strings.h \ util.h \ util_compat.h \ xml.h \ xml_compat.h \ xml_io.h \ xml_names.h noinst_HEADERS = $(wildcard *internal.h) diff --git a/include/crm/common/nvpair.h b/include/crm/common/nvpair.h index b68ba70d41..c6d0b22ded 100644 --- a/include/crm/common/nvpair.h +++ b/include/crm/common/nvpair.h @@ -1,90 +1,89 @@ /* * Copyright 2004-2024 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_NVPAIR__H #define PCMK__CRM_COMMON_NVPAIR__H #include // struct timeval #include // gpointer, gboolean, guint, GHashTable #include // xmlNode #include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Functionality for manipulating name/value pairs * \ingroup core */ typedef struct pcmk_nvpair_s { char *name; char *value; } pcmk_nvpair_t; GSList *pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value); void pcmk_free_nvpairs(GSList *nvpairs); -GSList *pcmk_sort_nvpairs(GSList *list); GSList *pcmk_xml_attrs2nvpairs(const xmlNode *xml); void pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml); xmlNode *crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, const char *value); void hash2nvpair(gpointer key, gpointer value, gpointer user_data); void hash2field(gpointer key, gpointer value, gpointer user_data); void hash2metafield(gpointer key, gpointer value, gpointer user_data); void hash2smartfield(gpointer key, gpointer value, gpointer user_data); GHashTable *xml2list(const xmlNode *parent); const char *crm_xml_add(xmlNode *node, const char *name, const char *value); const char *crm_xml_add_int(xmlNode *node, const char *name, int value); const char *crm_xml_add_ll(xmlNode *node, const char *name, long long value); const char *crm_xml_add_ms(xmlNode *node, const char *name, guint ms); const char *crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, const struct timeval *value); const char *crm_element_value(const xmlNode *data, const char *name); int crm_element_value_int(const xmlNode *data, const char *name, int *dest); int crm_element_value_ll(const xmlNode *data, const char *name, long long *dest); int crm_element_value_ms(const xmlNode *data, const char *name, guint *dest); int crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest); int crm_element_value_timeval(const xmlNode *data, const char *name_sec, const char *name_usec, struct timeval *dest); char *crm_element_value_copy(const xmlNode *data, const char *name); char *crm_meta_name(const char *field); const char *crm_meta_value(GHashTable *hash, const char *field); /*! * \brief Copy an element from one XML object to another * * \param[in] obj1 Source XML * \param[in,out] obj2 Destination XML * \param[in] element Name of element to copy * * \return Pointer to copied value (from source) */ static inline const char * crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, const char *element) { const char *value = crm_element_value(obj1, element); crm_xml_add(obj2, element, value); return value; } #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_NVPAIR__H diff --git a/include/crm/common/nvpair_compat.h b/include/crm/common/nvpair_compat.h new file mode 100644 index 0000000000..de0ca19869 --- /dev/null +++ b/include/crm/common/nvpair_compat.h @@ -0,0 +1,35 @@ +/* + * Copyright 2004-2024 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_NVPAIR_COMPAT__H +#define PCMK__CRM_COMMON_NVPAIR_COMPAT__H + +#include // GSList + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file + * \brief Deprecated Pacemaker name-value pair API + * \ingroup core + * \deprecated Do not include this header directly. The nvpair APIs in this + * header, and the header itself, will be removed in a future + * release. + */ + +//! \deprecated Do not use +GSList *pcmk_sort_nvpairs(GSList *list); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_NVPAIR_COMPAT__H diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c index 1116cdf330..414d4d774f 100644 --- a/lib/common/nvpair.c +++ b/lib/common/nvpair.c @@ -1,994 +1,986 @@ /* * Copyright 2004-2024 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 #include #include #include #include #include "crmcommon_private.h" /* * This file isolates handling of various kinds of name/value pairs: * * - pcmk_nvpair_t data type * - XML attributes () * - XML nvpair elements () * - Meta-attributes (for resources and actions) */ // pcmk_nvpair_t handling /*! * \internal * \brief Allocate a new name/value pair * * \param[in] name New name (required) * \param[in] value New value * * \return Newly allocated name/value pair * \note The caller is responsible for freeing the result with * \c pcmk__free_nvpair(). */ static pcmk_nvpair_t * pcmk__new_nvpair(const char *name, const char *value) { pcmk_nvpair_t *nvpair = NULL; CRM_ASSERT(name); nvpair = pcmk__assert_alloc(1, sizeof(pcmk_nvpair_t)); nvpair->name = pcmk__str_copy(name); nvpair->value = pcmk__str_copy(value); return nvpair; } /*! * \internal * \brief Free a name/value pair * * \param[in,out] nvpair Name/value pair to free */ static void pcmk__free_nvpair(gpointer data) { if (data) { pcmk_nvpair_t *nvpair = data; free(nvpair->name); free(nvpair->value); free(nvpair); } } /*! * \brief Prepend a name/value pair to a list * * \param[in,out] nvpairs List to modify * \param[in] name New entry's name * \param[in] value New entry's value * * \return New head of list * \note The caller is responsible for freeing the list with * \c pcmk_free_nvpairs(). */ GSList * pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value) { return g_slist_prepend(nvpairs, pcmk__new_nvpair(name, value)); } /*! * \brief Free a list of name/value pairs * * \param[in,out] list List to free */ void pcmk_free_nvpairs(GSList *nvpairs) { g_slist_free_full(nvpairs, pcmk__free_nvpair); } -/*! - * \internal - * \brief Compare two name/value pairs - * - * \param[in] a First name/value pair to compare - * \param[in] b Second name/value pair to compare - * - * \return 0 if a == b, 1 if a > b, -1 if a < b - */ -static gint -pcmk__compare_nvpair(gconstpointer a, gconstpointer b) -{ - int rc = 0; - const pcmk_nvpair_t *pair_a = a; - const pcmk_nvpair_t *pair_b = b; - - CRM_ASSERT(a != NULL); - CRM_ASSERT(pair_a->name != NULL); - - CRM_ASSERT(b != NULL); - CRM_ASSERT(pair_b->name != NULL); - - rc = strcmp(pair_a->name, pair_b->name); - if (rc < 0) { - return -1; - } else if (rc > 0) { - return 1; - } - return 0; -} - -/*! - * \brief Sort a list of name/value pairs - * - * \param[in,out] list List to sort - * - * \return New head of list - */ -GSList * -pcmk_sort_nvpairs(GSList *list) -{ - return g_slist_sort(list, pcmk__compare_nvpair); -} - /*! * \brief Create a list of name/value pairs from an XML node's attributes * * \param[in] XML to parse * * \return New list of name/value pairs * \note It is the caller's responsibility to free the list with * \c pcmk_free_nvpairs(). */ GSList * pcmk_xml_attrs2nvpairs(const xmlNode *xml) { GSList *result = NULL; for (xmlAttrPtr iter = pcmk__xe_first_attr(xml); iter != NULL; iter = iter->next) { result = pcmk_prepend_nvpair(result, (const char *) iter->name, (const char *) pcmk__xml_attr_value(iter)); } return result; } /*! * \internal * \brief Add an XML attribute corresponding to a name/value pair * * Suitable for glib list iterators, this function adds a NAME=VALUE * XML attribute based on a given name/value pair. * * \param[in] data Name/value pair * \param[out] user_data XML node to add attributes to */ static void pcmk__nvpair_add_xml_attr(gpointer data, gpointer user_data) { pcmk_nvpair_t *pair = data; xmlNode *parent = user_data; crm_xml_add(parent, pair->name, pair->value); } /*! * \brief Add XML attributes based on a list of name/value pairs * * \param[in,out] list List of name/value pairs * \param[in,out] xml XML node to add attributes to */ void pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) { g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); } // convenience function for name=value strings /*! * \internal * \brief Extract the name and value from an input string formatted as "name=value". * If unable to extract them, they are returned as NULL. * * \param[in] input The input string, likely from the command line * \param[out] name Everything before the first '=' in the input string * \param[out] value Everything after the first '=' in the input string * * \return 2 if both name and value could be extracted, 1 if only one could, and * and error code otherwise */ int pcmk__scan_nvpair(const char *input, char **name, char **value) { #ifdef HAVE_SSCANF_M *name = NULL; *value = NULL; if (sscanf(input, "%m[^=]=%m[^\n]", name, value) <= 0) { return -pcmk_err_bad_nvpair; } #else char *sep = NULL; *name = NULL; *value = NULL; sep = strstr(optarg, "="); if (sep == NULL) { return -pcmk_err_bad_nvpair; } *name = strndup(input, sep-input); if (*name == NULL) { return -ENOMEM; } /* If the last char in optarg is =, the user gave no * value for the option. Leave it as NULL. */ if (*(sep+1) != '\0') { *value = strdup(sep+1); if (*value == NULL) { return -ENOMEM; } } #endif if (*name != NULL && *value != NULL) { return 2; } else if (*name != NULL || *value != NULL) { return 1; } else { return -pcmk_err_bad_nvpair; } } /*! * \internal * \brief Format a name/value pair. * * Units can optionally be provided for the value. Note that unlike most * formatting functions, this one returns the formatted string. It is * assumed that the most common use of this function will be to build up * a string to be output as part of other functions. * * \note The caller is responsible for freeing the return value after use. * * \param[in] name The name of the nvpair. * \param[in] value The value of the nvpair. * \param[in] units Optional units for the value, or NULL. * * \return Newly allocated string with name/value pair */ char * pcmk__format_nvpair(const char *name, const char *value, const char *units) { return crm_strdup_printf("%s=\"%s%s\"", name, value, units ? units : ""); } // XML attribute handling /*! * \brief Create an XML attribute with specified name and value * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value on success, \c NULL otherwise * \note This does nothing if node, name, or value are \c NULL or empty. */ const char * crm_xml_add(xmlNode *node, const char *name, const char *value) { bool dirty = FALSE; xmlAttr *attr = NULL; CRM_CHECK(node != NULL, return NULL); CRM_CHECK(name != NULL, return NULL); if (value == NULL) { return NULL; } if (pcmk__tracking_xml_changes(node, FALSE)) { const char *old = crm_element_value(node, name); if (old == NULL || value == NULL || strcmp(old, value) != 0) { dirty = TRUE; } } if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) { crm_trace("Cannot add %s=%s to %s", name, value, node->name); return NULL; } attr = xmlSetProp(node, (pcmkXmlStr) name, (pcmkXmlStr) value); if (dirty) { pcmk__mark_xml_attr_dirty(attr); } CRM_CHECK(attr && attr->children && attr->children->content, return NULL); return (char *)attr->children->content; } /*! * \brief Create an XML attribute with specified name and integer value * * This is like \c crm_xml_add() but taking an integer value. * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if node or name are \c NULL or empty. */ const char * crm_xml_add_int(xmlNode *node, const char *name, int value) { char *number = pcmk__itoa(value); const char *added = crm_xml_add(node, name, number); free(number); return added; } /*! * \brief Create an XML attribute with specified name and unsigned value * * This is like \c crm_xml_add() but taking a guint value. * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] ms Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if node or name are \c NULL or empty. */ const char * crm_xml_add_ms(xmlNode *node, const char *name, guint ms) { char *number = crm_strdup_printf("%u", ms); const char *added = crm_xml_add(node, name, number); free(number); return added; } // Maximum size of null-terminated string representation of 64-bit integer // -9223372036854775808 #define LLSTRSIZE 21 /*! * \brief Create an XML attribute with specified name and long long int value * * This is like \c crm_xml_add() but taking a long long int value. It is a * useful equivalent for defined types like time_t, etc. * * \param[in,out] xml XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if xml or name are \c NULL or empty. * This does not support greater than 64-bit values. */ const char * crm_xml_add_ll(xmlNode *xml, const char *name, long long value) { char s[LLSTRSIZE] = { '\0', }; if (snprintf(s, LLSTRSIZE, "%lld", (long long) value) == LLSTRSIZE) { return NULL; } return crm_xml_add(xml, name, s); } /*! * \brief Create XML attributes for seconds and microseconds * * This is like \c crm_xml_add() but taking a struct timeval. * * \param[in,out] xml XML node to modify * \param[in] name_sec Name of XML attribute for seconds * \param[in] name_usec Name of XML attribute for microseconds (or NULL) * \param[in] value Time value to set * * \return New seconds value as string on success, \c NULL otherwise * \note This does nothing if xml, name_sec, or value is \c NULL. */ const char * crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, const struct timeval *value) { const char *added = NULL; if (xml && name_sec && value) { added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec); if (added && name_usec) { // Any error is ignored (we successfully added seconds) crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec); } } return added; } /*! * \brief Retrieve the value of an XML attribute * * \param[in] data XML node to check * \param[in] name Attribute name to check * * \return Value of specified attribute (may be \c NULL) */ const char * crm_element_value(const xmlNode *data, const char *name) { xmlAttr *attr = NULL; if (data == NULL) { crm_err("Couldn't find %s in NULL", name ? name : ""); CRM_LOG_ASSERT(data != NULL); return NULL; } else if (name == NULL) { crm_err("Couldn't find NULL in %s", data->name); return NULL; } attr = xmlHasProp(data, (pcmkXmlStr) name); if (!attr || !attr->children) { return NULL; } return (const char *) attr->children->content; } /*! * \brief Retrieve the integer value of an XML attribute * * This is like \c crm_element_value() but getting the value as an integer. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store element value * * \return 0 on success, -1 otherwise */ int crm_element_value_int(const xmlNode *data, const char *name, int *dest) { const char *value = NULL; CRM_CHECK(dest != NULL, return -1); value = crm_element_value(data, name); if (value) { long long value_ll; if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) || (value_ll < INT_MIN) || (value_ll > INT_MAX)) { *dest = PCMK__PARSE_INT_DEFAULT; } else { *dest = (int) value_ll; return 0; } } return -1; } /*! * \brief Retrieve the long long integer value of an XML attribute * * This is like \c crm_element_value() but getting the value as a long long int. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store element value * * \return 0 on success, -1 otherwise */ int crm_element_value_ll(const xmlNode *data, const char *name, long long *dest) { const char *value = NULL; CRM_CHECK(dest != NULL, return -1); value = crm_element_value(data, name); if ((value != NULL) && (pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT) == pcmk_rc_ok)) { return 0; } return -1; } /*! * \brief Retrieve the millisecond value of an XML attribute * * This is like \c crm_element_value() but returning the value as a guint. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store attribute value * * \return \c pcmk_ok on success, -1 otherwise */ int crm_element_value_ms(const xmlNode *data, const char *name, guint *dest) { const char *value = NULL; long long value_ll; CRM_CHECK(dest != NULL, return -1); *dest = 0; value = crm_element_value(data, name); if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) || (value_ll < 0) || (value_ll > G_MAXUINT)) { return -1; } *dest = (guint) value_ll; return pcmk_ok; } /*! * \brief Retrieve the seconds-since-epoch value of an XML attribute * * This is like \c crm_element_value() but returning the value as a time_t. * * \param[in] xml XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store attribute value * * \return \c pcmk_ok on success, -1 otherwise */ int crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest) { long long value_ll = 0; if (crm_element_value_ll(xml, name, &value_ll) < 0) { return -1; } /* Unfortunately, we can't do any bounds checking, since time_t has neither * standardized bounds nor constants defined for them. */ *dest = (time_t) value_ll; return pcmk_ok; } /*! * \brief Retrieve the value of XML second/microsecond attributes as time * * This is like \c crm_element_value() but returning value as a struct timeval. * * \param[in] xml XML to parse * \param[in] name_sec Name of XML attribute for seconds * \param[in] name_usec Name of XML attribute for microseconds * \param[out] dest Where to store result * * \return \c pcmk_ok on success, -errno on error * \note Values default to 0 if XML or XML attribute does not exist */ int crm_element_value_timeval(const xmlNode *xml, const char *name_sec, const char *name_usec, struct timeval *dest) { long long value_i = 0; CRM_CHECK(dest != NULL, return -EINVAL); dest->tv_sec = 0; dest->tv_usec = 0; if (xml == NULL) { return pcmk_ok; } /* Unfortunately, we can't do any bounds checking, since there are no * constants provided for the bounds of time_t and suseconds_t, and * calculating them isn't worth the effort. If there are XML values * beyond the native sizes, there will probably be worse problems anyway. */ // Parse seconds errno = 0; if (crm_element_value_ll(xml, name_sec, &value_i) < 0) { return -errno; } dest->tv_sec = (time_t) value_i; // Parse microseconds if (crm_element_value_ll(xml, name_usec, &value_i) < 0) { return -errno; } dest->tv_usec = (suseconds_t) value_i; return pcmk_ok; } /*! * \internal * \brief Get a date/time object from an XML attribute value * * \param[in] xml XML with attribute to parse (from CIB) * \param[in] attr Name of attribute to parse * \param[out] t Where to create date/time object * (\p *t must be NULL initially) * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p *t using crm_time_free(). */ int pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t) { const char *value = NULL; if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) { return EINVAL; } value = crm_element_value(xml, attr); if (value != NULL) { *t = crm_time_new(value); if (*t == NULL) { return pcmk_rc_unpack_error; } } return pcmk_rc_ok; } /*! * \brief Retrieve a copy of the value of an XML attribute * * This is like \c crm_element_value() but allocating new memory for the result. * * \param[in] data XML node to check * \param[in] name Attribute name to check * * \return Value of specified attribute (may be \c NULL) * \note The caller is responsible for freeing the result. */ char * crm_element_value_copy(const xmlNode *data, const char *name) { return pcmk__str_copy(crm_element_value(data, name)); } /*! * \brief Safely add hash table entry to XML as attribute or name-value pair * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the specified name and value if it does not already exist. If the key * name starts with a digit, then it's not a valid XML attribute name. In that * case, this will instead add a child * to the XML. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2smartfield(gpointer key, gpointer value, gpointer user_data) { /* @TODO Generate PCMK__XE_PARAM nodes for all keys that aren't valid XML * attribute names (not just those that start with digits), or possibly for * all keys to simplify parsing. * * Consider either deprecating as public API or exposing PCMK__XE_PARAM. * PCMK__XE_PARAM is currently private because it doesn't appear in any * output that Pacemaker generates. */ const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; if (isdigit(name[0])) { xmlNode *tmp = pcmk__xe_create(xml_node, PCMK__XE_PARAM); crm_xml_add(tmp, PCMK_XA_NAME, name); crm_xml_add(tmp, PCMK_XA_VALUE, s_value); } else if (crm_element_value(xml_node, name) == NULL) { crm_xml_add(xml_node, name, s_value); crm_trace("dumped: %s=%s", name, s_value); } else { crm_trace("duplicate: %s=%s", name, s_value); } } /*! * \brief Set XML attribute based on hash table entry * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the specified name and value if it does not already exist. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2field(gpointer key, gpointer value, gpointer user_data) { const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; if (crm_element_value(xml_node, name) == NULL) { crm_xml_add(xml_node, name, s_value); } else { crm_trace("duplicate: %s=%s", name, s_value); } } /*! * \brief Set XML attribute based on hash table entry, as meta-attribute name * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the meta-attribute version of the specified name and value if it does * not already exist and if the name does not appear to be cluster-internal. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2metafield(gpointer key, gpointer value, gpointer user_data) { char *crm_name = NULL; if (key == NULL || value == NULL) { return; } /* Filter out cluster-generated attributes that contain a '#' or ':' * (like fail-count and last-failure). */ for (crm_name = key; *crm_name; ++crm_name) { if ((*crm_name == '#') || (*crm_name == ':')) { return; } } crm_name = crm_meta_name(key); hash2field(crm_name, value, user_data); free(crm_name); } // nvpair handling /*! * \brief Create an XML name/value pair * * \param[in,out] parent If not \c NULL, make new XML node a child of this one * \param[in] id Set this as XML ID (or NULL to auto-generate) * \param[in] name Name to use * \param[in] value Value to use * * \return New XML object on success, \c NULL otherwise */ xmlNode * crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, const char *value) { xmlNode *nvp; /* id can be NULL so we auto-generate one, and name can be NULL if this * will be used to delete a name/value pair by ID, but both can't be NULL */ CRM_CHECK(id || name, return NULL); nvp = pcmk__xe_create(parent, PCMK_XE_NVPAIR); if (id) { crm_xml_add(nvp, PCMK_XA_ID, id); } else { pcmk__xe_set_id(nvp, "%s-%s", pcmk__s(pcmk__xe_id(parent), PCMK_XE_NVPAIR), name); } crm_xml_add(nvp, PCMK_XA_NAME, name); crm_xml_add(nvp, PCMK_XA_VALUE, value); return nvp; } /*! * \brief Add XML nvpair element based on hash table entry * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as the user data, and adds an \c nvpair * XML element with the specified name and value. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2nvpair(gpointer key, gpointer value, gpointer user_data) { const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; crm_create_nvpair_xml(xml_node, name, name, s_value); crm_trace("dumped: name=%s value=%s", name, s_value); } /*! * \brief Retrieve XML attributes as a hash table * * Given an XML element, this will look for any \ element child, * creating a hash table of (newly allocated string) name/value pairs taken * first from the attributes element's NAME=VALUE XML attributes, and then * from any \ children of attributes. * * \param[in] XML node to parse * * \return Hash table with name/value pairs * \note It is the caller's responsibility to free the result using * \c g_hash_table_destroy(). */ GHashTable * xml2list(const xmlNode *parent) { xmlNode *child = NULL; xmlAttrPtr pIter = NULL; xmlNode *nvpair_list = NULL; GHashTable *nvpair_hash = pcmk__strkey_table(free, free); CRM_CHECK(parent != NULL, return nvpair_hash); nvpair_list = pcmk__xe_first_child(parent, PCMK__XE_ATTRIBUTES, NULL, NULL); if (nvpair_list == NULL) { crm_trace("No attributes in %s", parent->name); crm_log_xml_trace(parent, "No attributes for resource op"); } crm_log_xml_trace(nvpair_list, "Unpacking"); for (pIter = pcmk__xe_first_attr(nvpair_list); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); crm_trace("Added %s=%s", p_name, p_value); pcmk__insert_dup(nvpair_hash, p_name, p_value); } for (child = pcmk__xe_first_child(nvpair_list, PCMK__XE_PARAM, NULL, NULL); child != NULL; child = pcmk__xe_next_same(child)) { const char *key = crm_element_value(child, PCMK_XA_NAME); const char *value = crm_element_value(child, PCMK_XA_VALUE); crm_trace("Added %s=%s", key, value); if (key != NULL && value != NULL) { pcmk__insert_dup(nvpair_hash, key, value); } } return nvpair_hash; } void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value) { crm_xml_add(node, name, pcmk__btoa(value)); } int pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value) { const char *xml_value = NULL; int ret, rc; if (node == NULL) { return ENODATA; } else if (name == NULL || value == NULL) { return EINVAL; } xml_value = crm_element_value(node, name); if (xml_value == NULL) { return ENODATA; } rc = crm_str_to_boolean(xml_value, &ret); if (rc == 1) { *value = ret; return pcmk_rc_ok; } else { return pcmk_rc_bad_input; } } bool pcmk__xe_attr_is_true(const xmlNode *node, const char *name) { bool value = false; int rc; rc = pcmk__xe_get_bool_attr(node, name, &value); return rc == pcmk_rc_ok && value == true; } // Meta-attribute handling /*! * \brief Get the environment variable equivalent of a meta-attribute name * * \param[in] attr_name Name of meta-attribute * * \return Newly allocated string for \p attr_name with "CRM_meta_" prefix and * underbars instead of dashes * \note This asserts on an invalid argument or memory allocation error, so * callers can assume the result is non-NULL. The caller is responsible * for freeing the result using free(). */ char * crm_meta_name(const char *attr_name) { char *env_name = NULL; CRM_ASSERT(!pcmk__str_empty(attr_name)); env_name = crm_strdup_printf(CRM_META "_%s", attr_name); for (char *c = env_name; *c != '\0'; ++c) { if (*c == '-') { *c = '_'; } } return env_name; } /*! * \brief Get the value of a meta-attribute * * Get the value of a meta-attribute from a hash table whose keys are * meta-attribute environment variable names (as crm_meta_name() would * create, like pcmk__graph_action_t:params, not pcmk_resource_t:meta). * * \param[in] meta Hash table of meta-attributes * \param[in] attr_name Name of meta-attribute to get * * \return Value of given meta-attribute */ const char * crm_meta_value(GHashTable *meta, const char *attr_name) { if ((meta != NULL) && (attr_name != NULL)) { char *key = crm_meta_name(attr_name); const char *value = g_hash_table_lookup(meta, key); free(key); return value; } return NULL; } + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +static gint +pcmk__compare_nvpair(gconstpointer a, gconstpointer b) +{ + int rc = 0; + const pcmk_nvpair_t *pair_a = a; + const pcmk_nvpair_t *pair_b = b; + + CRM_ASSERT(a != NULL); + CRM_ASSERT(pair_a->name != NULL); + + CRM_ASSERT(b != NULL); + CRM_ASSERT(pair_b->name != NULL); + + rc = strcmp(pair_a->name, pair_b->name); + if (rc < 0) { + return -1; + } else if (rc > 0) { + return 1; + } + return 0; +} + +GSList * +pcmk_sort_nvpairs(GSList *list) +{ + return g_slist_sort(list, pcmk__compare_nvpair); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/xml.c b/lib/common/xml.c index 1191250cb1..060f23d73c 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,2645 +1,2646 @@ /* * Copyright 2004-2024 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 // uint32_t #include #include #include #include // stat(), S_ISREG, etc. #include #include // gboolean, GString #include #include #include // xmlGetUTF8Char() #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" //! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 #define XML_VERSION ((pcmkXmlStr) "1.0") /*! * \internal * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * * \param[in,out] xml XML tree to traverse * \param[in,out] fn Function to call for each node (returns \c true to * continue traversing the tree or \c false to stop) * \param[in,out] user_data Argument to \p fn * * \return \c false if any \p fn call returned \c false, or \c true otherwise * * \note This function is recursive. */ bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data) { if (!fn(xml, user_data)) { return false; } for (xml = pcmk__xml_first_child(xml); xml != NULL; xml = pcmk__xml_next(xml)) { if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { return false; } } return true; } bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_tracking)) { return FALSE; } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_lazy)) { return FALSE; } return TRUE; } static inline void set_parent_flag(xmlNode *xml, long flag) { for(; xml; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv == NULL) { /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ } else { pcmk__set_xml_flags(nodepriv, flag); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { if(xml && xml->doc && xml->doc->_private){ /* During calls to xmlDocCopyNode(), xml->doc may be unset */ xml_doc_private_t *docpriv = xml->doc->_private; pcmk__set_xml_flags(docpriv, flag); } } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); set_parent_flag(xml, pcmk__xf_dirty); } /*! * \internal * \brief Clear flags on an XML node * * \param[in,out] xml XML node whose flags to reset * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool reset_xml_node_flags(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { nodepriv->flags = pcmk__xf_none; } return true; } /*! * \internal * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node * * \param[in,out] xml Node whose flags to set * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool mark_xml_dirty_created(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } return true; } /*! * \internal * \brief Mark an XML tree as dirty and created, and mark its parents dirty * * Also mark the document dirty. * * \param[in,out] xml Tree to mark as dirty and created */ void pcmk__xml_mark_created(xmlNode *xml) { CRM_ASSERT(xml != NULL); if (!pcmk__tracking_xml_changes(xml, false)) { // Tracking is disabled for entire document return; } // Mark all parents and document dirty pcmk__mark_xml_node_dirty(xml); pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; g_free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { CRM_ASSERT(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC); free(docpriv->user); docpriv->user = NULL; if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } // Free all private data associated with an XML node static void free_private_data(xmlNode *node) { /* Note: This function frees private data assosciated with an XML node, unless the function is being called as a result of internal XSLT cleanup. That could happen through, for example, the following chain of function calls: xsltApplyStylesheetInternal -> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc And in that case, the node would fulfill three conditions: 1. It would be a standalone document (i.e. it wouldn't be part of a document) 2. It would have a space-prefixed name (for reference, please see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) 3. It would carry its own payload in the _private field. We do not free data in this circumstance to avoid a failed assertion on the PCMK__XML_*_PRIVATE_MAGIC later. */ if (node->name == NULL || node->name[0] != ' ') { if (node->_private) { if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data(node->_private); } else { CRM_ASSERT(((xml_node_private_t *) node->_private)->check == PCMK__XML_NODE_PRIVATE_MAGIC); /* nothing dynamically allocated nested */ } free(node->_private); node->_private = NULL; } } } // Allocate and initialize private data for an XML node static void new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = docpriv; break; } case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = pcmk__assert_alloc(1, sizeof(xml_node_private_t)); nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = nodepriv; if (pcmk__tracking_xml_changes(node, FALSE)) { /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is * not hooked up at the point we are called */ pcmk__mark_xml_node_dirty(node); } break; } case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: break; default: /* Ignore */ crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); break; } } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_dirty); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } /*! * \internal * \brief Remove all attributes marked as deleted from an XML node * * \param[in,out] xml XML node whose deleted attributes to remove * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool accept_attr_deletions(xmlNode *xml, void *user_data) { reset_xml_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); return true; } /*! * \internal * \brief Find first child XML node matching another given XML node * * \param[in] haystack XML whose children should be checked * \param[in] needle XML to match (comment content or element name and ID) * \param[in] exact If true and needle is a comment, position must match */ xmlNode * pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) { CRM_CHECK(needle != NULL, return NULL); if (needle->type == XML_COMMENT_NODE) { return pcmk__xc_match(haystack, needle, exact); } else { const char *id = pcmk__xe_id(needle); const char *attr = (id == NULL)? NULL : PCMK_XA_ID; return pcmk__xe_first_child(haystack, (const char *) needle->name, attr, id); } } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_doc_private_t *docpriv = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); docpriv = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); reset_xml_private_data(xml->doc->_private); if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { docpriv->flags = pcmk__xf_none; return; } docpriv->flags = pcmk__xf_none; pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search (can be \c NULL) * \param[in] node_name If not \c NULL, only match children of this type * \param[in] attr_n If not \c NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * * \return Matching XML child element, or \c NULL if none found */ xmlNode * pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { xmlNode *child = NULL; const char *parent_name = ""; CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); if (parent != NULL) { child = parent->children; while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { child = child->next; } parent_name = (const char *) parent->name; } for (; child != NULL; child = pcmk__xe_next(child)) { const char *value = NULL; if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { // Node name mismatch continue; } if (attr_n == NULL) { // No attribute match needed return child; } value = crm_element_value(child, attr_n); if ((attr_v == NULL) && (value != NULL)) { // attr_v == NULL: Attribute attr_n must be set (to any value) return child; } if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { // attr_v != NULL: Attribute attr_n must be set to value attr_v return child; } } if (node_name == NULL) { node_name = "(any)"; // For logging } if (attr_n != NULL) { crm_trace("XML child node <%s %s=%s> not found in %s", node_name, attr_n, attr_v, parent_name); } else { crm_trace("XML child node <%s> not found in %s", node_name, parent_name); } return NULL; } /*! * \internal * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate * * If \p target already has an attribute named \p name set to an integer value * and \p value is an addition assignment expression on \p name, then expand * \p value to an integer and set attribute \p name to the expanded value in * \p target. * * Otherwise, set attribute \p name on \p target using the literal \p value. * * The original attribute value in \p target and the number in an assignment * expression in \p value are parsed and added as scores (that is, their values * are capped at \c INFINITY and \c -INFINITY). For more details, refer to * \c char2score(). * * For example, suppose \p target has an attribute named \c "X" with value * \c "5", and that \p name is \c "X". * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * * \param[in,out] target XML node whose attribute to set * \param[in] name Name of the attribute to set * \param[in] value New value of attribute to set * * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid * argument, or \c pcmk_rc_ok otherwise) */ int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { const char *old_value = NULL; CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); if (value == NULL) { return pcmk_rc_ok; } old_value = crm_element_value(target, name); // If no previous value, skip to default case and set the value unexpanded. if (old_value != NULL) { const char *n = name; const char *v = value; // Stop at first character that differs between name and value for (; (*n == *v) && (*n != '\0'); n++, v++); // If value begins with name followed by a "++" or "+=" if ((*n == '\0') && (*v++ == '+') && ((*v == '+') || (*v == '='))) { // If we're expanding ourselves, no previous value was set; use 0 int old_value_i = (old_value != value)? char2score(old_value) : 0; /* value="X++": new value of X is old_value + 1 * value="X+=Y": new value of X is old_value + Y (for some number Y) */ int add = (*v == '+')? 1 : char2score(++v); crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); return pcmk_rc_ok; } } // Default case: set the attribute unexpanded (with value treated literally) if (old_value != value) { crm_xml_add(target, name, value); } return pcmk_rc_ok; } /*! * \internal * \brief Copy XML attributes from a source element to a target element * * This is similar to \c xmlCopyPropList() except that attributes are marked * as dirty for change tracking purposes. * * \param[in,out] target XML element to receive copied attributes from \p src * \param[in] src XML element whose attributes to copy to \p target * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code */ int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; const char *value = pcmk__xml_attr_value(attr); if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) && (crm_element_value(target, name) != NULL)) { continue; } if (pcmk_is_set(flags, pcmk__xaf_score_update)) { pcmk__xe_set_score(target, name, value); } else { crm_xml_add(target, name, value); } } return pcmk_rc_ok; } /*! * \internal * \brief Compare two XML attributes by name * * \param[in] a First XML attribute to compare * \param[in] b Second XML attribute to compare * * \retval negative \c a->name is \c NULL or comes before \c b->name * lexicographically * \retval 0 \c a->name and \c b->name are equal * \retval positive \c b->name is \c NULL or comes before \c a->name * lexicographically */ static gint compare_xml_attr(gconstpointer a, gconstpointer b) { const xmlAttr *attr_a = a; const xmlAttr *attr_b = b; return pcmk__strcmp((const char *) attr_a->name, (const char *) attr_b->name, pcmk__str_none); } /*! * \internal * \brief Sort an XML element's attributes by name * * This does not consider ACLs and does not mark the attributes as deleted or * dirty. Upon return, all attributes still exist and are set to the same values * as before the call. The only thing that may change is the order of the * attribute list. * * \param[in,out] xml XML element whose attributes to sort */ void pcmk__xe_sort_attrs(xmlNode *xml) { GSList *attr_list = NULL; for (xmlAttr *iter = pcmk__xe_first_attr(xml); iter != NULL; iter = iter->next) { attr_list = g_slist_prepend(attr_list, iter); } attr_list = g_slist_sort(attr_list, compare_xml_attr); for (GSList *iter = attr_list; iter != NULL; iter = iter->next) { xmlNode *attr = iter->data; xmlUnlinkNode(attr); xmlAddChild(xml, attr); } g_slist_free(attr_list); } /*! * \internal * \brief Remove an XML attribute from an element * * \param[in,out] element XML element that owns \p attr * \param[in,out] attr XML attribute to remove from \p element * * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of * attributes from \p element, or \c pcmk_rc_ok otherwise) */ static int remove_xe_attr(xmlNode *element, xmlAttr *attr) { if (attr == NULL) { return pcmk_rc_ok; } if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { // ACLs apply to element, not to particular attributes crm_trace("ACLs prevent removal of attributes from %s element", (const char *) element->name); return EPERM; } if (pcmk__tracking_xml_changes(element, false)) { // Leave in place (marked for removal) until after diff is calculated set_parent_flag(element, pcmk__xf_dirty); pcmk__set_xml_flags((xml_node_private_t *) attr->_private, pcmk__xf_deleted); } else { xmlRemoveProp(attr); } return pcmk_rc_ok; } /*! * \internal * \brief Remove a named attribute from an XML element * * \param[in,out] element XML element to remove an attribute from * \param[in] name Name of attribute to remove */ void pcmk__xe_remove_attr(xmlNode *element, const char *name) { if (name != NULL) { remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name)); } } /*! * \internal * \brief Remove a named attribute from an XML element * * This is a wrapper for \c pcmk__xe_remove_attr() for use with * \c pcmk__xml_tree_foreach(). * * \param[in,out] xml XML element to remove an attribute from * \param[in] user_data Name of attribute to remove * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) { const char *name = user_data; pcmk__xe_remove_attr(xml, name); return true; } /*! * \internal * \brief Remove an XML element's attributes that match some criteria * * \param[in,out] element XML element to modify * \param[in] match If not NULL, only remove attributes for which * this function returns true * \param[in,out] user_data Data to pass to \p match */ void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data) { xmlAttrPtr next = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { if (remove_xe_attr(element, a) != pcmk_rc_ok) { return; } } } } /*! * \internal * \brief Create a new XML element under a given parent * * \param[in,out] parent XML element that will be the new element's parent * (\c NULL to create a new XML document with the new * node as root) * \param[in] name Name of new element * * \return Newly created XML element (guaranteed not to be \c NULL) */ xmlNode * pcmk__xe_create(xmlNode *parent, const char *name) { xmlNode *node = NULL; CRM_ASSERT(!pcmk__str_empty(name)); if (parent == NULL) { xmlDoc *doc = pcmk__xml_new_doc(); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); } pcmk__xml_mark_created(node); return node; } /*! * \internal * \brief Create a new XML document * * \return Newly allocated XML document (guaranteed not to be \c NULL) * * \note The caller is responsible for freeing the return value using * \c pcmk__xml_free_doc(). */ xmlDoc * pcmk__xml_new_doc(void) { /* @TODO Allocate document private data here when we drop * new_private_data()/free_private_data() */ xmlDoc *doc = xmlNewDoc(XML_VERSION); pcmk__mem_assert(doc); return doc; } /*! * \internal * \brief Free a new XML document * * \param[in,out] doc XML document to free */ void pcmk__xml_free_doc(xmlDoc *doc) { /* @TODO Free document private data here when we drop * new_private_data()/free_private_data() */ if (doc != NULL) { xmlFreeDoc(doc); } } /*! * \internal * \brief Set a formatted string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format * * \note This function escapes special characters. \c xmlNodeSetContent() does * not. */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_content(xmlNode *node, const char *format, ...) { if (node != NULL) { const char *content = NULL; char *buf = NULL; if (strchr(format, '%') == NULL) { // Nothing to format content = format; } else { va_list ap; va_start(ap, format); if (pcmk__str_eq(format, "%s", pcmk__str_none)) { // No need to make a copy content = va_arg(ap, const char *); } else { CRM_ASSERT(vasprintf(&buf, format, ap) >= 0); content = buf; } va_end(ap); } xmlNodeSetContent(node, (pcmkXmlStr) content); free(buf); } } /*! * \internal * \brief Check whether the first character of a string is an XML NameStartChar * * See https://www.w3.org/TR/xml/#NT-NameStartChar. * * This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they * don't expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false * otherwise */ bool pcmk__xml_is_name_start_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } /* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to * "the minimum number of bytes present in the sequence... to assure the * next character is completely contained within the sequence." It's similar * to the "n" in the strn*() functions. However, this doesn't make any sense * for null-terminated strings, and there's no value that indicates "keep * going until '\0'." So we set it to 4, the max number of bytes in a UTF-8 * character. * * At return, it's set to the actual number of bytes in the char, or 0 on * error. */ *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((pcmkXmlStr) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return (c == '_') || (c == ':') || ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Check whether the first character of a string is an XML NameChar * * See https://www.w3.org/TR/xml/#NT-NameChar. * * This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't * expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameChar, or \c false * otherwise */ bool pcmk__xml_is_name_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } // See comment regarding len in pcmk__xml_is_name_start_char() *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((pcmkXmlStr) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_') || (c == ':') || (c == '-') || (c == '.') || (c == 0xB7) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x300) && (c <= 0x36F)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x203F) && (c <= 0x2040)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Sanitize a string so it is usable as an XML ID * * An ID must match the Name production as defined here: * https://www.w3.org/TR/xml/#NT-Name. * * Convert an invalid start character to \c '_'. Convert an invalid character * after the start character to \c '.'. * * \param[in,out] id String to sanitize */ void pcmk__xml_sanitize_id(char *id) { bool valid = true; int len = 0; // If id is empty or NULL, there's no way to make it a valid XML ID CRM_ASSERT(!pcmk__str_empty(id)); /* @TODO Suppose there are two strings and each has an invalid ID character * in the same position. The strings are otherwise identical. Both strings * will be sanitized to the same valid ID, which is incorrect. * * The caller is responsible for ensuring the sanitized ID does not already * exist in a given XML document before using it, if uniqueness is desired. */ valid = pcmk__xml_is_name_start_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { *id = '_'; for (int i = 1; i < len; i++) { id[i] = '.'; } } for (id += len; *id != '\0'; id += len) { valid = pcmk__xml_is_name_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { for (int i = 0; i < len; i++) { id[i] = '.'; } } } } /*! * \internal * \brief Set a formatted string as an XML element's ID * * If the formatted string would not be a valid ID, it's first sanitized by * \c pcmk__xml_sanitize_id(). * * \param[in,out] node Node whose ID to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_id(xmlNode *node, const char *format, ...) { char *id = NULL; va_list ap; CRM_ASSERT(!pcmk__str_empty(format)); if (node == NULL) { return; } va_start(ap, format); CRM_ASSERT(vasprintf(&id, format, ap) >= 0); va_end(ap); if (!xmlValidateNameValue((pcmkXmlStr) id)) { pcmk__xml_sanitize_id(id); } crm_xml_add(node, PCMK_XA_ID, id); free(id); } /*! * \internal * \brief Free an XML tree without ACL checks or change tracking * * \param[in,out] xml XML node to free */ static void free_xml_node(xmlNode *xml) { /* @TODO Free tree private data here when we drop * new_private_data()/free_private_data() */ xmlUnlinkNode(xml); xmlFreeNode(xml); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p node is the root of its document, free the entire document. * * \param[in,out] node XML node to free * \param[in] position Position of \p node among its siblings for change * tracking (negative to calculate automatically if * needed) */ static void free_xml_with_position(xmlNode *node, int position) { xmlDoc *doc = NULL; xml_node_private_t *nodepriv = NULL; if (node == NULL) { return; } doc = node->doc; nodepriv = node->_private; if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) { /* @TODO Should we check ACLs first? Otherwise it seems like we could * free the root element without write permission. */ pcmk__xml_free_doc(doc); return; } if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(node); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } if ((doc != NULL) && pcmk__tracking_xml_changes(node, false) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { xml_doc_private_t *docpriv = doc->_private; GString *xpath = pcmk__element_xpath(node); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", xpath->str, node, doc); deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = g_string_free(xpath, FALSE); deleted_obj->position = -1; // Record the position only for XML comments for now if (node->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip); } } docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(node, pcmk__xf_dirty); } } free_xml_node(node); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p xml is the root of its document, free the entire document. * * \param[in,out] xml XML node to free */ void pcmk__xml_free(xmlNode *xml) { free_xml_with_position(xml, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document CRM_ASSERT(src->type == XML_ELEMENT_NODE); doc = pcmk__xml_new_doc(); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); pcmk__mem_assert(copy); xmlAddChild(parent, copy); } pcmk__xml_mark_created(copy); return copy; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: free_xml_node(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time * * \param[in,out] xe XML element to add attribute to * * \return Value that was set, or NULL on error */ const char * pcmk__xe_add_last_written(xmlNode *xe) { char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * * \param[in] text String to check * \param[in] type Type of escaping * * \return \c true if \p text has special characters that need to be escaped, or * \c false otherwise */ bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { if (text == NULL) { return false; } while (*text != '\0') { switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': case '>': case '&': return true; case '\n': case '\t': break; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': case '>': case '&': case '"': return true; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '\n': case '\r': case '\t': case '"': return true; default: break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or \c NULL if \p text * is \c NULL). If \p text is not \c NULL, the return value is * guaranteed not to be \c NULL. * * \note There are libxml functions that purport to do this: * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). * However, their escaping is incomplete. See: * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 * \note The caller is responsible for freeing the return value using * \c g_free(). */ gchar * pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { GString *copy = NULL; if (text == NULL) { return NULL; } copy = g_string_sized_new(strlen(text)); while (*text != '\0') { // Don't escape any non-ASCII characters if ((*text & 0x80) != 0) { size_t bytes = g_utf8_next_char(text) - text; g_string_append_len(copy, text, bytes); text += bytes; continue; } switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '\n': case '\t': g_string_append_c(copy, *text); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '"': g_string_append(copy, PCMK__XML_ENTITY_QUOT); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '"': g_string_append(copy, "\\\""); break; case '\n': g_string_append(copy, "\\n"); break; case '\r': g_string_append(copy, "\\r"); break; case '\t': g_string_append(copy, "\\t"); break; default: g_string_append_c(copy, *text); break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return g_string_free(copy, FALSE); } /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; // Prevent the dirty flag being set recursively upwards pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); // Restore the old value (and the tracking flag) attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal remove_xe_attr(new_xml, attr); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); // Restore the original value xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute xmlUnsetProp(new_xml, new_attr->name); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); // Remove the child again (which will track it in document's deleted_objs) free_xml_with_position(candidate, pcmk__xml_position(old_child, pcmk__xf_skip)); if (pcmk__xml_match(new_parent, old_child, true) == NULL) { pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), pcmk__xf_skip); } } static void mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, int p_old, int p_new) { xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with " PCMK_XA_ID "='%s' moved from position %d to %d under %s", new_child->name, pcmk__s(pcmk__xe_id(new_child), ""), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); if (p_old > p_new) { nodepriv = old_child->_private; } else { nodepriv = new_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } // Given original and new XML, mark new XML portions that have changed static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *old_child = NULL; xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { pcmk__xml_mark_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } nodepriv = new_xml->_private; CRM_CHECK(nodepriv != NULL, return); if(nodepriv->flags & pcmk__xf_processed) { /* Avoid re-comparing nodes */ return; } pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { new_child = pcmk__xml_match(new_xml, old_child, true); if (new_child != NULL) { mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children new_child = pcmk__xml_first_child(new_xml); while (new_child != NULL) { xmlNode *next = pcmk__xml_next(new_child); old_child = pcmk__xml_match(old_xml, new_child, true); if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); // May free new_child mark_xml_changes(old_child, new_child, true); } else { /* Check for movement, we already checked for differences */ int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); if(p_old != p_new) { mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } new_child = next; } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { xml_track_changes(new_xml, NULL, NULL, FALSE); } mark_xml_changes(old_xml, new_xml, FALSE); } /*! * \internal * \brief Find a comment with matching content in specified XML * * \param[in] root XML to search * \param[in] search_comment Comment whose content should be searched for * \param[in] exact If true, comment must also be at same position */ xmlNode * pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) { xmlNode *a_child = NULL; int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (exact) { int offset = pcmk__xml_position(a_child, pcmk__xf_skip); xml_node_private_t *nodepriv = a_child->_private; if (offset < search_offset) { continue; } else if (offset > search_offset) { return NULL; } if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { continue; } } if (a_child->type == XML_COMMENT_NODE && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { return a_child; } else if (exact) { return NULL; } } return NULL; } /*! * \internal * \brief Make one XML comment match another (in content) * * \param[in,out] parent If \p target is NULL and this is not, add or update * comment child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML comment node * \param[in] update Make comment content match this (must not be NULL) * * \note At least one of \parent and \target must be non-NULL */ void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) { CRM_CHECK(update != NULL, return); CRM_CHECK(update->type == XML_COMMENT_NODE, return); if (target == NULL) { target = pcmk__xc_match(parent, update, false); } if (target == NULL) { pcmk__xml_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); target->content = xmlStrdup(update->content); } } /*! * \internal * \brief Merge one XML tree into another * * Here, "merge" means: * 1. Copy attribute values from \p update to the target, overwriting in case of * conflict. * 2. Descend through \p update and the target in parallel. At each level, for * each child of \p update, look for a matching child of the target. * a. For each child, if a match is found, go to step 1, recursively merging * the child of \p update into the child of the target. * b. Otherwise, copy the child of \p update as a child of the target. * * A match is defined as the first child of the same type within the target, * with: * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update * * This function does not delete any elements or attributes from the target. It * may add elements or overwrite attributes, as described above. * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML * \param[in] update Make the desired XML match this (must not be \c NULL) * \param[in] flags Group of enum pcmk__xa_flags * * \note At least one of \p parent and \p target must be non-NULL. * \note This function is recursive. For the top-level call, \p parent is * \c NULL and \p target is not \c NULL. For recursive calls, \p target is * \c NULL and \p parent is not \c NULL. */ static void update_xe(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags) { // @TODO Try to refactor further, possibly using pcmk__xml_tree_foreach() const char *update_name = NULL; const char *update_id_attr = NULL; const char *update_id_val = NULL; char *trace_s = NULL; crm_log_xml_trace(update, "update"); crm_log_xml_trace(target, "target"); CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); goto done; } update_name = (const char *) update->name; CRM_CHECK(update_name != NULL, goto done); CRM_CHECK((target != NULL) || (parent != NULL), goto done); update_id_val = pcmk__xe_id(update); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID; } else { update_id_val = crm_element_value(update, PCMK_XA_ID_REF); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID_REF; } } pcmk__if_tracing( { if (update_id_attr != NULL) { trace_s = crm_strdup_printf("<%s %s=%s/>", update_name, update_id_attr, update_id_val); } else { trace_s = crm_strdup_printf("<%s/>", update_name); } }, {} ); if (target == NULL) { // Recursive call target = pcmk__xe_first_child(parent, update_name, update_id_attr, update_id_val); } if (target == NULL) { // Recursive call with no existing matching child target = pcmk__xe_create(parent, update_name); crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { // Either recursive call with match, or top-level call crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); pcmk__xe_copy_attrs(target, update, flags); for (xmlNode *child = pcmk__xml_first_child(update); child != NULL; child = pcmk__xml_next(child)) { crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); update_xe(target, NULL, child, flags); } crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); done: free(trace_s); } /*! * \internal * \brief Delete an XML subtree if it matches a search element * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has attributes set, \p xml has those attributes set to the * same values. (\p xml may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to delete upon match * \param[in] user_data Search element * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was deleted) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool delete_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *search = user_data; if (!pcmk__xe_is(search, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; attr = attr->next) { const char *search_val = pcmk__xml_attr_value(attr); const char *xml_val = crm_element_value(xml, (const char *) attr->name); if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { // No match: an attr in xml doesn't match the attr in search return true; } } crm_log_xml_trace(xml, "delete-match"); crm_log_xml_trace(search, "delete-search"); pcmk__xml_free(xml); // Found a match and deleted it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and delete the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p search are both element nodes of the same type. * * If \p search has attributes set, \c node has those attributes set to the * same values. (\c node may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to search * \param[in] search Element to match against * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful deletion and an error code otherwise) */ int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { // See @COMPAT comment in pcmk__xe_replace_match() CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { // Found and deleted an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Replace one XML node with a copy of another XML node * * This function handles change tracking and applies ACLs. * * \param[in,out] old XML node to replace * \param[in] new XML node to copy as replacement for \p old * * \note This frees \p old. */ static void replace_node(xmlNode *old, xmlNode *new) { new = xmlCopyNode(new, 1); pcmk__mem_assert(new); // May be unnecessary but avoids slight changes to some test outputs pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL); old = xmlReplaceNode(old, new); if (xml_tracking_changes(new)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); free_xml_node(old); } /*! * \internal * \brief Replace one XML subtree with a copy of another if the two match * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML subtree to replace with \p user_data upon match * \param[in] user_data XML to replace \p xml with a copy of upon match * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was replaced by \p user_data) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool replace_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *replace = user_data; const char *xml_id = NULL; const char *replace_id = NULL; xml_id = pcmk__xe_id(xml); replace_id = pcmk__xe_id(replace); if (!pcmk__xe_is(replace, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if ((replace_id != NULL) && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { // No match: ID was provided in replace and doesn't match xml's ID return true; } crm_log_xml_trace(xml, "replace-match"); crm_log_xml_trace(replace, "replace-with"); replace_node(xml, replace); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and replace the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p replace are both element nodes of the same type. * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML tree to search * \param[in] replace XML to replace a matching element with a copy of * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful replacement and an error code otherwise) */ int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) { /* @COMPAT Some of this behavior (like not matching the tree root, which is * allowed by pcmk__xe_update_match()) is questionable for general use but * required for backward compatibility by cib_process_replace() and * cib_process_delete(). Behavior can change at a major version release if * desired. */ CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { // Found and replaced an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } //! User data for \c update_xe_if_matching() struct update_data { xmlNode *update; //!< Update source uint32_t flags; //!< Group of enum pcmk__xa_flags }; /*! * \internal * \brief Update one XML subtree with another if the two match * * "Update" means to merge a source subtree into a target subtree (see * \c update_xe()). * * A match is defined as follows: * * \p xml and \p user_data->update are both element nodes of the same type. * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute * value, or \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML subtree to update with \p user_data->update * upon match * \param[in] user_data struct update_data object * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was updated by \p user_data->update) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool update_xe_if_matching(xmlNode *xml, void *user_data) { struct update_data *data = user_data; xmlNode *update = data->update; if (!pcmk__xe_is(update, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { // No match: ID mismatch return true; } crm_log_xml_trace(xml, "update-match"); crm_log_xml_trace(update, "update-with"); update_xe(NULL, xml, update, data->flags); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and update the first matching element * * "Update" means to merge a source subtree into a target subtree (see * \c update_xe()). * * A match with a node \c node is defined as follows: * * \c node and \p update are both element nodes of the same type. * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or * \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML tree to search * \param[in] update XML to update a matching element with * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful update and an error code otherwise) */ int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) { /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we * compare IDs only if the equivalent of the update argument has an ID. * Here, we're stricter: we consider it a mismatch if only one element has * an ID attribute, or if both elements have IDs but they don't match. * * Perhaps we should align the behavior at a major version release. */ struct update_data data = { .update = update, .flags = flags, }; CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { // Found and updated an element return pcmk_rc_ok; } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Get next sibling XML element with the same name as a given element * * \param[in] node XML element to start from * * \return Next sibling XML element with same name */ xmlNode * pcmk__xe_next_same(const xmlNode *node) { for (xmlNode *match = pcmk__xe_next(node); match != NULL; match = pcmk__xe_next(match)) { if (pcmk__xe_is(match, (const char *) node->name)) { return match; } } return NULL; } /*! * \internal * \brief Initialize the Pacemaker XML environment * * Set an XML buffer allocation scheme, set XML node create and destroy * callbacks, and load schemas into the cache. */ void pcmk__xml_init(void) { // @TODO Try to find a better caller than crm_log_preinit() static bool initialized = false; if (!initialized) { initialized = true; /* Double the buffer size when the buffer needs to grow. The default * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance * due to the number of reallocs. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); // Initialize private data at node creation xmlRegisterNodeDefault(new_private_data); // Free private data at node destruction xmlDeregisterNodeDefault(free_private_data); // Load schemas into the cache pcmk__schema_init(); } } /*! * \internal * \brief Tear down the Pacemaker XML environment * * Destroy schema cache and clean up memory allocated by libxml2. */ void pcmk__xml_cleanup(void) { pcmk__schema_cleanup(); xmlCleanupParser(); } char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) { static const char *base = NULL; char *ret = NULL; if (base == NULL) { base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY); } if (pcmk__str_empty(base)) { base = PCMK_SCHEMA_DIR; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); free(ret); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) { while (true) { const char *name, *value; name = va_arg(pairs, const char *); if (name == NULL) { return; } value = va_arg(pairs, const char *); if (value != NULL) { crm_xml_add(node, name, value); } } } void pcmk__xe_set_props(xmlNodePtr node, ...) { va_list pairs; va_start(pairs, node); pcmk__xe_set_propv(node, pairs); va_end(pairs); } int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata) { xmlNode *children = (xml? xml->children : NULL); CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && ((child_element_name == NULL) || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START +#include #include xmlNode * copy_xml(xmlNode *src) { xmlDoc *doc = pcmk__xml_new_doc(); xmlNode *copy = NULL; copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); return copy; } void crm_xml_init(void) { pcmk__xml_init(); } void crm_xml_cleanup(void) { pcmk__xml_cleanup(); } void pcmk_free_xml_subtree(xmlNode *xml) { free_xml_node(xml); } void free_xml(xmlNode *child) { pcmk__xml_free(child); } xmlNode * expand_idref(xmlNode *input, xmlNode *top) { return pcmk__xe_resolve_idref(input, top); } void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { switch (*c) { case ':': case '#': *c = '.'; } } } void crm_xml_set_id(xmlNode *xml, const char *format, ...) { va_list ap; int len = 0; char *id = NULL; /* equivalent to crm_strdup_printf() */ va_start(ap, format); len = vasprintf(&id, format, ap); va_end(ap); CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); crm_xml_add(xml, PCMK_XA_ID, id); free(id); } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; CRM_CHECK(input != NULL, return NULL); result = pcmk__xe_create(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } // LCOV_EXCL_STOP // End deprecated API