diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index be06b9217a..cd8b9a787e 100644 --- a/include/crm/common/Makefile.am +++ b/include/crm/common/Makefile.am @@ -1,50 +1,52 @@ # # 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. # include $(top_srcdir)/mk/common.mk 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 \ results_compat.h \ roles.h \ rules.h \ scheduler.h \ scheduler_types.h \ schemas.h \ scores.h \ scores_compat.h \ strings.h \ util.h \ util_compat.h \ xml.h \ xml_compat.h \ + xml_element.h \ + xml_element_compat.h \ xml_io.h \ xml_names.h noinst_HEADERS = $(wildcard *internal.h) diff --git a/include/crm/common/history_internal.h b/include/crm/common/history_internal.h index c74ef296c8..bcf2015d60 100644 --- a/include/crm/common/history_internal.h +++ b/include/crm/common/history_internal.h @@ -1,52 +1,53 @@ /* * 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_HISTORY_INTERNAL__H #define PCMK__CRM_COMMON_HISTORY_INTERNAL__H #include // NULL #include // xmlNode #include // crm_element_value() #include // pcmk__str_empty() +#include // pcmk__xe_id() #include // PCMK__XA_OPERATION_KEY #ifdef __cplusplus extern "C" { #endif /*! * \internal * \brief Get the operation key from an action history entry * * \param[in] xml Action history entry * * \return Entry's operation key */ static inline const char * pcmk__xe_history_key(const xmlNode *xml) { if (xml == NULL) { return NULL; } else { /* @COMPAT Pacemaker <= 1.1.5 did not add the key, and used the ID * instead. Checking for that allows us to process old saved CIBs, * including some regression tests. */ const char *key = crm_element_value(xml, PCMK__XA_OPERATION_KEY); return pcmk__str_empty(key)? pcmk__xe_id(xml) : key; } } #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_HISTORY_INTERNAL__H diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h index ed5f5fc8f7..b8e4a2d45b 100644 --- a/include/crm/common/internal.h +++ b/include/crm/common/internal.h @@ -1,400 +1,357 @@ /* * Copyright 2015-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_INTERNAL__H #define PCMK__CRM_COMMON_INTERNAL__H #include // pid_t, getpid() #include // bool #include // uint8_t, uint64_t #include // guint, GList, GHashTable #include // xmlNode #include // do_crm_log_unlikely(), etc. #include // mainloop_io_t, struct ipc_client_callbacks #include // crm_strdup_printf() #include #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /* This says whether the current application is a Pacemaker daemon or not, * and is used to change default logging settings such as whether to log to * stderr, etc., as well as a few other details such as whether blackbox signal * handling is enabled. * * It is set when logging is initialized, and does not need to be set directly. */ extern bool pcmk__is_daemon; // Number of elements in a statically defined array #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) ) #if PCMK__ENABLE_CIBSECRETS /* internal CIB utilities (from cib_secrets.c) */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params); #endif /* internal main loop utilities (from mainloop.c) */ int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, const struct ipc_client_callbacks *callbacks, mainloop_io_t **source); guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer); /* internal node-related XML utilities (from nodes.c) */ /*! * \internal * \brief Add local node name and ID to an XML node * * \param[in,out] request XML node to modify * \param[in] node The local node's name * \param[in] nodeid The local node's ID (can be 0) */ void pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid); /* internal name/value utilities (from nvpair.c) */ int pcmk__scan_nvpair(const char *input, char **name, char **value); char *pcmk__format_nvpair(const char *name, const char *value, const char *units); -/*! - * \internal - * \brief Add a boolean attribute to an XML node. - * - * \param[in,out] node XML node to add attributes to - * \param[in] name XML attribute to create - * \param[in] value Value to give to the attribute - */ -void -pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value); - -/*! - * \internal - * \brief Extract a boolean attribute's value from an XML element - * - * \param[in] node XML node to get attribute from - * \param[in] name XML attribute to get - * - * \return True if the given \p name is an attribute on \p node and has - * the value \c PCMK_VALUE_TRUE, False in all other cases - */ -bool -pcmk__xe_attr_is_true(const xmlNode *node, const char *name); - -/*! - * \internal - * \brief Extract a boolean attribute's value from an XML element, with - * error checking - * - * \param[in] node XML node to get attribute from - * \param[in] name XML attribute to get - * \param[out] value Destination for the value of the attribute - * - * \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is - * NULL or the attribute does not exist, pcmk_rc_unknown_format - * if the attribute is not a boolean, and pcmk_rc_ok otherwise. - * - * \note \p value only has any meaning if the return value is pcmk_rc_ok. - */ -int -pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value); - - /* internal procfs utilities (from procfs.c) */ pid_t pcmk__procfs_pid_of(const char *name); unsigned int pcmk__procfs_num_cores(void); int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size); bool pcmk__procfs_has_pids(void); /* internal functions related to process IDs (from pid.c) */ /*! * \internal * \brief Check whether process exists (by PID and optionally executable path) * * \param[in] pid PID of process to check * \param[in] daemon If not NULL, path component to match with procfs entry * * \return Standard Pacemaker return code * \note Particular return codes of interest include pcmk_rc_ok for alive, * ESRCH for process is not alive (verified by kill and/or executable path * match), EACCES for caller unable or not allowed to check. A result of * "alive" is less reliable when \p daemon is not provided or procfs is * not available, since there is no guarantee that the PID has not been * recycled for another process. * \note This function cannot be used to verify \e authenticity of the process. */ int pcmk__pid_active(pid_t pid, const char *daemon); int pcmk__read_pidfile(const char *filename, pid_t *pid); int pcmk__pidfile_matches(const char *filename, pid_t expected_pid, const char *expected_name, pid_t *pid); int pcmk__lock_pidfile(const char *filename, const char *name); // bitwise arithmetic utilities /*! * \internal * \brief Set specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be set * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__set_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group | flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8llx (%s) for %s set by %s:%d", ((flag_type == NULL)? "Group of" : flag_type), (unsigned long long) flags, ((flags_str == NULL)? "flags" : flags_str), ((target == NULL)? "target" : target), function, line); } return result; } /*! * \internal * \brief Clear specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be cleared * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__clear_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group & ~flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8llx (%s) for %s cleared by %s:%d", ((flag_type == NULL)? "Group of" : flag_type), (unsigned long long) flags, ((flags_str == NULL)? "flags" : flags_str), ((target == NULL)? "target" : target), function, line); } return result; } /*! * \internal * \brief Get readable string for whether specified flags are set * * \param[in] flag_group Group of flags to check * \param[in] flags Which flags in \p flag_group should be checked * * \return "true" if all \p flags are set in \p flag_group, otherwise "false" */ static inline const char * pcmk__flag_text(uint64_t flag_group, uint64_t flags) { return pcmk__btoa(pcmk_all_flags_set(flag_group, flags)); } // miscellaneous utilities (from utils.c) void pcmk__daemonize(const char *name, const char *pidfile); void pcmk__panic(const char *reason); pid_t pcmk__locate_sbd(void); void pcmk__sleep_ms(unsigned int ms); extern int pcmk__score_red; extern int pcmk__score_green; extern int pcmk__score_yellow; /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] file File where \p function is located * \param[in] function Calling function * \param[in] line Line within \p file * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ static inline void * pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line, size_t nmemb, size_t size) { void *ptr = calloc(nmemb, size); if (ptr == NULL) { crm_abort(file, function, line, "Out of memory", FALSE, TRUE); crm_exit(CRM_EX_OSERR); } return ptr; } /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ #define pcmk__assert_alloc(nmemb, size) \ pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size) /*! * \internal * \brief Resize a dynamically allocated memory block * * \param[in] ptr Memory block to resize (or NULL to allocate new memory) * \param[in] size New size of memory block in bytes (must be > 0) * * \return Pointer to resized memory block * * \note This asserts on error, so the result is guaranteed to be non-NULL * (which is the main advantage of this over directly using realloc()). */ static inline void * pcmk__realloc(void *ptr, size_t size) { void *new_ptr; // realloc(p, 0) can replace free(p) but this wrapper can't pcmk__assert(size > 0); new_ptr = realloc(ptr, size); if (new_ptr == NULL) { free(ptr); abort(); } return new_ptr; } static inline char * pcmk__getpid_s(void) { return crm_strdup_printf("%lu", (unsigned long) getpid()); } // More efficient than g_list_length(list) == 1 static inline bool pcmk__list_of_1(GList *list) { return list && (list->next == NULL); } // More efficient than g_list_length(list) > 1 static inline bool pcmk__list_of_multiple(GList *list) { return list && (list->next != NULL); } /* convenience functions for failure-related node attributes */ #define PCMK__FAIL_COUNT_PREFIX "fail-count" #define PCMK__LAST_FAILURE_PREFIX "last-failure" /*! * \internal * \brief Generate a failure-related node attribute name for a resource * * \param[in] prefix Start of attribute name * \param[in] rsc_id Resource name * \param[in] op Operation name * \param[in] interval_ms Operation interval * * \return Newly allocated string with attribute name * * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example, * "fail-count-myrsc#monitor_30000"). The '#' is used because it is not * a valid character in a resource ID, to reliably distinguish where the * operation name begins. The '_' is used simply to be more comparable to * action labels like "myrsc_monitor_30000". */ static inline char * pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op, guint interval_ms) { CRM_CHECK(prefix && rsc_id && op, return NULL); return crm_strdup_printf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms); } static inline char * pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op, interval_ms); } static inline char * pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op, interval_ms); } // internal resource agent functions (from agents.c) int pcmk__effective_rc(int rc); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_INTERNAL__H diff --git a/include/crm/common/nvpair.h b/include/crm/common/nvpair.h index 46329d082a..29013c8080 100644 --- a/include/crm/common/nvpair.h +++ b/include/crm/common/nvpair.h @@ -1,90 +1,55 @@ /* * 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); xmlNode *crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, const char *value); 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 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #endif // PCMK__CRM_COMMON_NVPAIR__H diff --git a/include/crm/common/nvpair_internal.h b/include/crm/common/nvpair_internal.h index 4cfc1bcbe2..d73bbcf050 100644 --- a/include/crm/common/nvpair_internal.h +++ b/include/crm/common/nvpair_internal.h @@ -1,71 +1,67 @@ /* * 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_INTERNAL__H #define PCMK__CRM_COMMON_NVPAIR_INTERNAL__H #include // gboolean #include // xmlNode #include // pcmk_rule_input_t #include // crm_time_t #include // pcmk__str_eq(), etc. #ifdef __cplusplus extern "C" { #endif // Data needed to sort XML blocks of name/value pairs typedef struct unpack_data_s { GHashTable *values; // Where to put name/value pairs const char *first_id; // Block with this XML ID should sort first pcmk_rule_input_t rule_input; // Data used to evaluate rules /* Whether each block's values should overwrite any existing ones * * @COMPAT Only external call paths set this to true. Drop it when we drop * pe_eval_nvpairs() and pe_unpack_nvpairs() after replacing with a new * public API that doesn't overwrite. */ bool overwrite; // If not NULL, this will be set to when rule evaluations will change next crm_time_t *next_change; } pcmk__nvpair_unpack_t; /*! * \internal * \brief Insert a meta-attribute into a hash table * * \param[in] obj Resource (pcmk__resource_private_t) * or action (pcmk_action_t) to add to * \param[in] name Meta-attribute name * \param[in] value Value to add */ #define pcmk__insert_meta(obj, name, value) do { \ if (pcmk__str_eq((value), "#default", pcmk__str_casei)) { \ /* @COMPAT Deprecated since 2.1.8 */ \ pcmk__config_warn("Support for setting meta-attributes " \ "(such as %s) to the explicit value " \ "'#default' is deprecated and will be " \ "removed in a future release", (name)); \ } else if ((value) != NULL) { \ pcmk__insert_dup((obj)->meta, (name), (value)); \ } \ } while (0) -int pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t); -int pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest, - uint32_t default_value); - #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_NVPAIR_INTERNAL__H diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h index 3609ee2431..432c3bb4e7 100644 --- a/include/crm/common/xml_compat.h +++ b/include/crm/common/xml_compat.h @@ -1,88 +1,79 @@ /* * 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_XML_COMPAT__H #define PCMK__CRM_COMMON_XML_COMPAT__H #include // gboolean #include // xmlNode #include // crm_xml_add() #include // PCMK_XE_CLONE #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Deprecated Pacemaker XML API * \ingroup core * \deprecated Do not include this header directly. The XML APIs in this * header, and the header itself, will be removed in a future * release. */ // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Use name member directly static inline const char * crm_element_name(const xmlNode *xml) { return (xml == NULL)? NULL : (const char *) xml->name; } // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *copy_xml(xmlNode *src_node); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Call \c crm_log_init() or \c crm_log_cli_init() instead void crm_xml_init(void); //! \deprecated Exit with \c crm_exit() instead void crm_xml_cleanup(void); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void pcmk_free_xml_subtree(xmlNode *xml); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void free_xml(xmlNode *child); -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *expand_idref(xmlNode *input, xmlNode *top); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); - //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void crm_xml_sanitize_id(char *id); //! \deprecated Do not use char *calculate_on_disk_digest(xmlNode *input); //! \deprecated Do not use char *calculate_operation_digest(xmlNode *input, const char *version); //! \deprecated Do not use char *calculate_xml_versioned_digest(xmlNode *input, gboolean sort, gboolean do_filter, const char *version); -//! \deprecated Do not use -xmlNode *sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive); - #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_COMPAT__H diff --git a/include/crm/common/nvpair.h b/include/crm/common/xml_element.h similarity index 68% copy from include/crm/common/nvpair.h copy to include/crm/common/xml_element.h index 46329d082a..c49603b545 100644 --- a/include/crm/common/nvpair.h +++ b/include/crm/common/xml_element.h @@ -1,90 +1,71 @@ /* * 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 +#ifndef PCMK__CRM_COMMON_XML_ELEMENT__H +#define PCMK__CRM_COMMON_XML_ELEMENT__H #include // struct timeval -#include // gpointer, gboolean, guint, GHashTable -#include // xmlNode -#include +#include // guint +#include // xmlNode #ifdef __cplusplus extern "C" { #endif /** * \file - * \brief Functionality for manipulating name/value pairs + * \brief Wrappers for and extensions to libxml2 for XML elements * \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); - -xmlNode *crm_create_nvpair_xml(xmlNode *parent, const char *id, - const char *name, const char *value); -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 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) -#include +#include #endif -#endif // PCMK__CRM_COMMON_NVPAIR__H +#endif // PCMK__CRM_COMMON_XML_ELEMENT__H diff --git a/include/crm/common/xml_element_compat.h b/include/crm/common/xml_element_compat.h new file mode 100644 index 0000000000..7c6e1f1c84 --- /dev/null +++ b/include/crm/common/xml_element_compat.h @@ -0,0 +1,42 @@ +/* + * 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_XML_ELEMENT_COMPAT__H +#define PCMK__CRM_COMMON_XML_ELEMENT_COMPAT__H + +#include // gboolean +#include // xmlNode + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file + * \brief Deprecated Pacemaker XML element 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 Pacemaker for general-purpose XML manipulation +xmlNode *expand_idref(xmlNode *input, xmlNode *top); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); + +//! \deprecated Do not use +xmlNode *sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_XML_ELEMENT_COMPAT__H diff --git a/include/crm/common/xml_element_internal.h b/include/crm/common/xml_element_internal.h new file mode 100644 index 0000000000..260551ac0a --- /dev/null +++ b/include/crm/common/xml_element_internal.h @@ -0,0 +1,187 @@ +/* + * Copyright 2017-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_XML_ELEMENT_INTERNAL__H +#define PCMK__CRM_COMMON_XML_ELEMENT_INTERNAL__H + +/* + * Internal-only wrappers for and extensions to libxml2 for processing XML + * elements + */ + +#include // bool +#include // uint32_t +#include // NULL +#include // strcmp() + +#include // xmlNode, etc. + +#include // crm_time_t +#include // crm_element_value() +#include // PCMK_XA_ID + +#ifdef __cplusplus +extern "C" { +#endif + +const char *pcmk__xe_add_last_written(xmlNode *xe); + +xmlNode *pcmk__xe_first_child(const xmlNode *parent, const char *node_name, + const char *attr_n, const char *attr_v); + +void pcmk__xe_remove_attr(xmlNode *element, const char *name); +bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data); +void pcmk__xe_remove_matching_attrs(xmlNode *element, + bool (*match)(xmlAttrPtr, void *), + void *user_data); +int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search); +int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace); +int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags); + +/*! + * \internal + * \brief Retrieve the value of the \c PCMK_XA_ID XML attribute + * + * \param[in] xml XML element to check + * + * \return Value of the \c PCMK_XA_ID attribute (may be \c NULL) + */ +static inline const char * +pcmk__xe_id(const xmlNode *xml) +{ + return crm_element_value(xml, PCMK_XA_ID); +} + +/*! + * \internal + * \brief Check whether an XML element is of a particular type + * + * \param[in] xml XML element to compare + * \param[in] name XML element name to compare + * + * \return \c true if \p xml is of type \p name, otherwise \c false + */ +static inline bool +pcmk__xe_is(const xmlNode *xml, const char *name) +{ + return (xml != NULL) && (xml->name != NULL) && (name != NULL) + && (strcmp((const char *) xml->name, name) == 0); +} + +/*! + * \internal + * \brief Return next non-text sibling element of an XML element + * + * \param[in] child XML element to check + * + * \return Next sibling element of \p child (or NULL if none) + */ +static inline xmlNode * +pcmk__xe_next(const xmlNode *child) +{ + xmlNode *next = child? child->next : NULL; + + while (next && (next->type != XML_ELEMENT_NODE)) { + next = next->next; + } + return next; +} + +xmlNode *pcmk__xe_create(xmlNode *parent, const char *name); +xmlNode *pcmk__xe_next_same(const xmlNode *node); + +void pcmk__xe_set_content(xmlNode *node, const char *format, ...) + G_GNUC_PRINTF(2, 3); + +int pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score, + int default_score); + +int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags); +void pcmk__xe_sort_attrs(xmlNode *xml); + +void pcmk__xe_set_id(xmlNode *xml, const char *format, ...) + G_GNUC_PRINTF(2, 3); + +/*! + * \internal + * \brief Like pcmk__xe_set_props, but takes a va_list instead of + * arguments directly. + * + * \param[in,out] node XML to add attributes to + * \param[in] pairs NULL-terminated list of name/value pairs to add + */ +void +pcmk__xe_set_propv(xmlNodePtr node, va_list pairs); + +/*! + * \internal + * \brief Add a NULL-terminated list of name/value pairs to the given + * XML node as properties. + * + * \param[in,out] node XML node to add properties to + * \param[in] ... NULL-terminated list of name/value pairs + * + * \note A NULL name terminates the arguments; a NULL value will be skipped. + */ +void +pcmk__xe_set_props(xmlNodePtr node, ...) +G_GNUC_NULL_TERMINATED; + +/*! + * \internal + * \brief Get first attribute of an XML element + * + * \param[in] xe XML element to check + * + * \return First attribute of \p xe (or NULL if \p xe is NULL or has none) + */ +static inline xmlAttr * +pcmk__xe_first_attr(const xmlNode *xe) +{ + return (xe == NULL)? NULL : xe->properties; +} + +/*! + * \internal + * \brief Iterate over child elements of \p xml + * + * This function iterates over the children of \p xml, performing the + * callback function \p handler on each node. If the callback returns + * a value other than pcmk_rc_ok, the iteration stops and the value is + * returned. It is therefore possible that not all children will be + * visited. + * + * \param[in,out] xml The starting XML node. Can be NULL. + * \param[in] child_element_name The name that the node must match in order + * for \p handler to be run. If NULL, all + * child elements will match. + * \param[in] handler The callback function. + * \param[in,out] userdata User data to pass to the callback function. + * Can be NULL. + * + * \return Standard Pacemaker return code + */ +int +pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, + int (*handler)(xmlNode *xml, void *userdata), + void *userdata); + +int pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t); +int pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest, + uint32_t default_value); + +void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value); +int pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value); +bool pcmk__xe_attr_is_true(const xmlNode *node, const char *name); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_XML_ELEMENT_INTERNAL__H diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index 76c36dd148..72eae57ef5 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,592 +1,456 @@ /* * Copyright 2017-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_XML_INTERNAL__H #define PCMK__CRM_COMMON_XML_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 (libxslt) */ #include #include // uint32_t #include -#include #include /* transitively imports qblog.h */ #include +#include // PCMK_XA_ID, PCMK_XE_CLONE + +// This file is a wrapper for other xml_*_internal.h headers +#include +#include #include #include -#include // PCMK_XA_ID, PCMK_XE_CLONE +#include #include #ifdef __cplusplus extern "C" { #endif /*! * \brief Base for directing lib{xml2,xslt} log into standard libqb backend * * This macro implements the core of what can be needed for directing * libxml2 or libxslt error messaging into standard, preconfigured * libqb-backed log stream. * * It's a bit unfortunate that libxml2 (and more sparsely, also libxslt) * emits a single message by chunks (location is emitted separatedly from * the message itself), so we have to take the effort to combine these * chunks back to single message. Whether to do this or not is driven * with \p dechunk toggle. * * The form of a macro was chosen for implicit deriving of __FILE__, etc. * and also because static dechunking buffer should be differentiated per * library (here we assume different functions referring to this macro * will not ever be using both at once), preferably also per-library * context of use to avoid clashes altogether. * * Note that we cannot use qb_logt, because callsite data have to be known * at the moment of compilation, which it is not always the case -- xml_log * (and unfortunately there's no clear explanation of the fail to compile). * * Also note that there's no explicit guard against said libraries producing * never-newline-terminated chunks (which would just keep consuming memory), * as it's quite improbable. Termination of the program in between the * same-message chunks will raise a flag with valgrind and the likes, though. * * And lastly, regarding how dechunking combines with other non-message * parameters -- for \p priority, most important running specification * wins (possibly elevated to LOG_ERR in case of nonconformance with the * newline-termination "protocol"), \p dechunk is expected to always be * on once it was at the start, and the rest (\p postemit and \p prefix) * are picked directly from the last chunk entry finalizing the message * (also reasonable to always have it the same with all related entries). * * \param[in] priority Syslog priority for the message to be logged * \param[in] dechunk Whether to dechunk new-line terminated message * \param[in] postemit Code to be executed once message is sent out * \param[in] prefix How to prefix the message or NULL for raw passing * \param[in] fmt Format string as with printf-like functions * \param[in] ap Variable argument list to supplement \p fmt format string */ #define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \ do { \ if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \ qb_log_from_external_source_va(__func__, __FILE__, (fmt), \ (priority), __LINE__, 0, (ap)); \ (void) (postemit); \ } else { \ int CXLB_len = 0; \ char *CXLB_buf = NULL; \ static int CXLB_buffer_len = 0; \ static char *CXLB_buffer = NULL; \ static uint8_t CXLB_priority = 0; \ \ CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \ \ if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \ if (CXLB_len < 0) { \ CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\ CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \ } else if (CXLB_len > 0 /* && (dechunk) */ \ && CXLB_buf[CXLB_len - 1] == '\n') { \ CXLB_buf[CXLB_len - 1] = '\0'; \ } \ if (CXLB_buffer) { \ qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \ CXLB_priority, __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buffer, CXLB_buf); \ free(CXLB_buffer); \ } else { \ qb_log_from_external_source(__func__, __FILE__, "%s%s", \ (priority), __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buf); \ } \ if (CXLB_len < 0) { \ CXLB_buf = NULL; /* restore temporary override */ \ } \ CXLB_buffer = NULL; \ CXLB_buffer_len = 0; \ (void) (postemit); \ \ } else if (CXLB_buffer == NULL) { \ CXLB_buffer_len = CXLB_len; \ CXLB_buffer = CXLB_buf; \ CXLB_buf = NULL; \ CXLB_priority = (priority); /* remember as a running severest */ \ \ } else { \ CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \ memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \ CXLB_buffer_len += CXLB_len; \ CXLB_buffer[CXLB_buffer_len] = '\0'; \ CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \ } \ free(CXLB_buf); \ } \ } while (0) /* * \enum pcmk__xml_fmt_options * \brief Bit flags to control format in XML logs and dumps */ enum pcmk__xml_fmt_options { //! Exclude certain XML attributes (for calculating digests) pcmk__xml_fmt_filtered = (1 << 0), //! Include indentation and newlines pcmk__xml_fmt_pretty = (1 << 1), //! Include the opening tag of an XML element, and include XML comments pcmk__xml_fmt_open = (1 << 3), //! Include the children of an XML element pcmk__xml_fmt_children = (1 << 4), //! Include the closing tag of an XML element pcmk__xml_fmt_close = (1 << 5), // @COMPAT Can we start including text nodes unconditionally? //! Include XML text nodes pcmk__xml_fmt_text = (1 << 6), }; void pcmk__xml_init(void); void pcmk__xml_cleanup(void); int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options); int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml); /* XML search strings for guest, remote and pacemaker_remote nodes */ /* search string to find CIB resources entries for cluster nodes */ #define PCMK__XP_MEMBER_NODE_CONFIG \ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES \ "/" PCMK_XE_NODE \ "[not(@" PCMK_XA_TYPE ") or @" PCMK_XA_TYPE "='" PCMK_VALUE_MEMBER "']" /* search string to find CIB resources entries for guest nodes */ #define PCMK__XP_GUEST_NODE_CONFIG \ "//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \ "//" PCMK_XE_META_ATTRIBUTES "//" PCMK_XE_NVPAIR \ "[@" PCMK_XA_NAME "='" PCMK_META_REMOTE_NODE "']" /* search string to find CIB resources entries for remote nodes */ #define PCMK__XP_REMOTE_NODE_CONFIG \ "//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \ "[@" PCMK_XA_TYPE "='" PCMK_VALUE_REMOTE "']" \ "[@" PCMK_XA_PROVIDER "='pacemaker']" /* search string to find CIB node status entries for pacemaker_remote nodes */ #define PCMK__XP_REMOTE_NODE_STATUS \ "//" PCMK_XE_CIB "//" PCMK_XE_STATUS "//" PCMK__XE_NODE_STATE \ "[@" PCMK_XA_REMOTE_NODE "='" PCMK_VALUE_TRUE "']" enum pcmk__xml_artefact_ns { pcmk__xml_artefact_ns_legacy_rng = 1, pcmk__xml_artefact_ns_legacy_xslt, pcmk__xml_artefact_ns_base_rng, pcmk__xml_artefact_ns_base_xslt, }; void pcmk__strip_xml_text(xmlNode *xml); -const char *pcmk__xe_add_last_written(xmlNode *xe); - -xmlNode *pcmk__xe_first_child(const xmlNode *parent, const char *node_name, - const char *attr_n, const char *attr_v); - - -void pcmk__xe_remove_attr(xmlNode *element, const char *name); -bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data); -void pcmk__xe_remove_matching_attrs(xmlNode *element, - bool (*match)(xmlAttrPtr, void *), - void *user_data); -int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search); -int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace); -int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags); GString *pcmk__element_xpath(const xmlNode *xml); /*! * \internal * \enum pcmk__xml_escape_type * \brief Indicators of which XML characters to escape * * XML allows the escaping of special characters by replacing them with entity * references (for example, """) or character references (for * example, " "). * * The special characters '&' (except as the beginning of an entity * reference) and '<' are not allowed in their literal forms in XML * character data. Character data is non-markup text (for example, the content * of a text node). '>' is allowed under most circumstances; we escape * it for safety and symmetry. * * For more details, see the "Character Data and Markup" section of the XML * spec, currently section 2.4: * https://www.w3.org/TR/xml/#dt-markup * * Attribute values are handled specially. * * If an attribute value is delimited by single quotes, then single quotes * must be escaped within the value. * * Similarly, if an attribute value is delimited by double quotes, then double * quotes must be escaped within the value. * * A conformant XML processor replaces a literal whitespace character (tab, * newline, carriage return, space) in an attribute value with a space * (\c '#x20') character. However, a reference to a whitespace character (for * example, \c " " for \c '\n') does not get replaced. * * For more details, see the "Attribute-Value Normalization" section of the * XML spec, currently section 3.3.3. Note that the default attribute type * is CDATA; we don't deal with NMTOKENS, etc.: * https://www.w3.org/TR/xml/#AVNormalize * * Pacemaker always delimits attribute values with double quotes, so there's no * need to escape single quotes. * * Newlines and tabs should be escaped in attribute values when XML is * serialized to text, so that future parsing preserves them rather than * normalizing them to spaces. * * We always escape carriage returns, so that they're not converted to spaces * during attribute-value normalization and because displaying them as literals * is messy. */ enum pcmk__xml_escape_type { /*! * For text nodes. * * Escape \c '<', \c '>', and \c '&' using entity references. * * Do not escape \c '\n' and \c '\t'. * * Escape other non-printing characters using character references. */ pcmk__xml_escape_text, /*! * For attribute values. * * Escape \c '<', \c '>', \c '&', and \c '"' using entity references. * * Escape \c '\n', \c '\t', and other non-printing characters using * character references. */ pcmk__xml_escape_attr, /* @COMPAT Drop escaping of at least '\n' and '\t' for * pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip, * and openstack-virtual-ip resource agents no longer depend on it. * * At time of writing, openstack-info may set a multiline value for the * openstack_ports node attribute. The other two agents query the value and * require it to be on one line with no spaces. */ /*! * For attribute values displayed in text output delimited by double quotes. * * Escape \c '\n' as \c "\\n" * * Escape \c '\r' as \c "\\r" * * Escape \c '\t' as \c "\\t" * * Escape \c '"' as \c "\\"" */ pcmk__xml_escape_attr_pretty, }; bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type); char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type); /*! * \internal * \brief Get the root directory to scan XML artefacts of given kind for * * \param[in] ns governs the hierarchy nesting against the inherent root dir * * \return root directory to scan XML artefacts of given kind for */ char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns); /*! * \internal * \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT) * * \param[in] ns denotes path forming details (parent dir, suffix) * \param[in] filespec symbolic file specification to be combined with * #artefact_ns to form the final path * \return unwrapped path to particular XML artifact (RNG/XSLT) */ char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec); -/*! - * \internal - * \brief Retrieve the value of the \c PCMK_XA_ID XML attribute - * - * \param[in] xml XML element to check - * - * \return Value of the \c PCMK_XA_ID attribute (may be \c NULL) - */ -static inline const char * -pcmk__xe_id(const xmlNode *xml) -{ - return crm_element_value(xml, PCMK_XA_ID); -} - -/*! - * \internal - * \brief Check whether an XML element is of a particular type - * - * \param[in] xml XML element to compare - * \param[in] name XML element name to compare - * - * \return \c true if \p xml is of type \p name, otherwise \c false - */ -static inline bool -pcmk__xe_is(const xmlNode *xml, const char *name) -{ - return (xml != NULL) && (xml->name != NULL) && (name != NULL) - && (strcmp((const char *) xml->name, name) == 0); -} - /*! * \internal * \brief Return first non-text child node of an XML node * * \param[in] parent XML node to check * * \return First non-text child node of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xml_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type == XML_TEXT_NODE)) { child = child->next; } return child; } /*! * \internal * \brief Return next non-text sibling node of an XML node * * \param[in] child XML node to check * * \return Next non-text sibling of \p child (or NULL if none) */ static inline xmlNode * pcmk__xml_next(const xmlNode *child) { xmlNode *next = (child? child->next : NULL); while (next && (next->type == XML_TEXT_NODE)) { next = next->next; } return next; } -/*! - * \internal - * \brief Return next non-text sibling element of an XML element - * - * \param[in] child XML element to check - * - * \return Next sibling element of \p child (or NULL if none) - */ -static inline xmlNode * -pcmk__xe_next(const xmlNode *child) -{ - xmlNode *next = child? child->next : NULL; - - while (next && (next->type != XML_ELEMENT_NODE)) { - next = next->next; - } - return next; -} - -xmlNode *pcmk__xe_create(xmlNode *parent, const char *name); void pcmk__xml_free(xmlNode *xml); void pcmk__xml_free_doc(xmlDoc *doc); xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src); -xmlNode *pcmk__xe_next_same(const xmlNode *node); - -void pcmk__xe_set_content(xmlNode *node, const char *format, ...) - G_GNUC_PRINTF(2, 3); /*! * \internal * \enum pcmk__xa_flags * \brief Flags for operations affecting XML attributes */ enum pcmk__xa_flags { //! Flag has no effect pcmk__xaf_none = 0U, //! Don't overwrite existing values pcmk__xaf_no_overwrite = (1U << 0), /*! * Treat values as score updates where possible (see * \c pcmk__xe_set_score()) */ pcmk__xaf_score_update = (1U << 1), }; -int pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score, - int default_score); - -int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags); -void pcmk__xe_sort_attrs(xmlNode *xml); - void pcmk__xml_sanitize_id(char *id); -void pcmk__xe_set_id(xmlNode *xml, const char *format, ...) - G_GNUC_PRINTF(2, 3); - -/*! - * \internal - * \brief Like pcmk__xe_set_props, but takes a va_list instead of - * arguments directly. - * - * \param[in,out] node XML to add attributes to - * \param[in] pairs NULL-terminated list of name/value pairs to add - */ -void -pcmk__xe_set_propv(xmlNodePtr node, va_list pairs); - -/*! - * \internal - * \brief Add a NULL-terminated list of name/value pairs to the given - * XML node as properties. - * - * \param[in,out] node XML node to add properties to - * \param[in] ... NULL-terminated list of name/value pairs - * - * \note A NULL name terminates the arguments; a NULL value will be skipped. - */ -void -pcmk__xe_set_props(xmlNodePtr node, ...) -G_GNUC_NULL_TERMINATED; - -/*! - * \internal - * \brief Get first attribute of an XML element - * - * \param[in] xe XML element to check - * - * \return First attribute of \p xe (or NULL if \p xe is NULL or has none) - */ -static inline xmlAttr * -pcmk__xe_first_attr(const xmlNode *xe) -{ - return (xe == NULL)? NULL : xe->properties; -} /*! * \internal * \brief Extract the ID attribute from an XML element * * \param[in] xpath String to search * \param[in] node Node to get the ID for * * \return ID attribute of \p node in xpath string \p xpath */ char * pcmk__xpath_node_id(const char *xpath, const char *node); /*! * \internal * \brief Print an informational message if an xpath query returned multiple * items with the same ID. * * \param[in,out] out The output object * \param[in] search The xpath search result, most typically the result of * calling cib->cmds->query(). * \param[in] name The name searched for */ void pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search, const char *name); /* internal XML-related utilities */ enum xml_private_flags { pcmk__xf_none = 0x0000, pcmk__xf_dirty = 0x0001, pcmk__xf_deleted = 0x0002, pcmk__xf_created = 0x0004, pcmk__xf_modified = 0x0008, pcmk__xf_tracking = 0x0010, pcmk__xf_processed = 0x0020, pcmk__xf_skip = 0x0040, pcmk__xf_moved = 0x0080, pcmk__xf_acl_enabled = 0x0100, pcmk__xf_acl_read = 0x0200, pcmk__xf_acl_write = 0x0400, pcmk__xf_acl_deny = 0x0800, pcmk__xf_acl_create = 0x1000, pcmk__xf_acl_denied = 0x2000, pcmk__xf_lazy = 0x4000, }; void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag); -/*! - * \internal - * \brief Iterate over child elements of \p xml - * - * This function iterates over the children of \p xml, performing the - * callback function \p handler on each node. If the callback returns - * a value other than pcmk_rc_ok, the iteration stops and the value is - * returned. It is therefore possible that not all children will be - * visited. - * - * \param[in,out] xml The starting XML node. Can be NULL. - * \param[in] child_element_name The name that the node must match in order - * for \p handler to be run. If NULL, all - * child elements will match. - * \param[in] handler The callback function. - * \param[in,out] userdata User data to pass to the callback function. - * Can be NULL. - * - * \return Standard Pacemaker return code - */ -int -pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, - int (*handler)(xmlNode *xml, void *userdata), - void *userdata); - bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } /*! * \internal * \brief Check whether a given CIB element was modified in a CIB patchset * * \param[in] patchset CIB XML patchset * \param[in] element XML tag of CIB element to check (\c NULL is equivalent * to \c PCMK_XE_CIB). Supported values include any CIB * element supported by \c pcmk__cib_abs_xpath_for(). * * \return \c true if \p element was modified, or \c false otherwise */ bool pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_INTERNAL__H diff --git a/include/crm_internal.h b/include/crm_internal.h index 508ba478df..891f2e5b68 100644 --- a/include/crm_internal.h +++ b/include/crm_internal.h @@ -1,102 +1,99 @@ /* * Copyright 2006-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_INTERNAL__H #define PCMK__CRM_INTERNAL__H #ifndef PCMK__CONFIG_H #define PCMK__CONFIG_H #include #endif #include /* Our minimum glib dependency is 2.42. Define that as both the minimum and * maximum glib APIs that are allowed (i.e. APIs that were already deprecated * in 2.42, and APIs introduced after 2.42, cannot be used by Pacemaker code). */ #define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_42 #define GLIB_VERSION_MAX_ALLOWED GLIB_VERSION_2_42 #define G_LOG_DOMAIN "Pacemaker" #include #include #include /* Public API headers can guard including deprecated API headers with this * symbol, thus preventing internal code (which includes this header) from using * deprecated APIs, while still allowing external code to use them by default. */ #define PCMK_ALLOW_DEPRECATED 0 #include #include #include #include #include #include #include #include #include #include #include -#include #include -#include -#include #include #include #include #ifdef __cplusplus extern "C" { #endif #define N_(String) (String) #ifdef ENABLE_NLS #define _(String) gettext(String) #else #define _(String) (String) #endif /* * IPC service names that are only used internally */ #define PCMK__SERVER_BASED_RO "cib_ro" #define PCMK__SERVER_BASED_RW "cib_rw" #define PCMK__SERVER_BASED_SHM "cib_shm" /* * IPC commands that can be sent to Pacemaker daemons */ #define PCMK__ATTRD_CMD_PEER_REMOVE "peer-remove" #define PCMK__ATTRD_CMD_UPDATE "update" #define PCMK__ATTRD_CMD_UPDATE_BOTH "update-both" #define PCMK__ATTRD_CMD_UPDATE_DELAY "update-delay" #define PCMK__ATTRD_CMD_QUERY "query" #define PCMK__ATTRD_CMD_REFRESH "refresh" #define PCMK__ATTRD_CMD_SYNC_RESPONSE "sync-response" #define PCMK__ATTRD_CMD_CLEAR_FAILURE "clear-failure" #define PCMK__ATTRD_CMD_CONFIRM "confirm" #define PCMK__CONTROLD_CMD_NODES "list-nodes" #define ST__LEVEL_MIN 1 #define ST__LEVEL_MAX 9 #ifdef __cplusplus } #endif #endif // CRM_INTERNAL__H diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index 7be3629e4d..95518323a6 100644 --- a/lib/common/Makefile.am +++ b/lib/common/Makefile.am @@ -1,142 +1,143 @@ # # 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. # include $(top_srcdir)/mk/common.mk ## libraries lib_LTLIBRARIES = libcrmcommon.la check_LTLIBRARIES = libcrmcommon_test.la # Disable -Wcast-qual if used, because we do some hacky casting, # and because libxml2 has some signatures that should be const but aren't # for backward compatibility reasons. # s390 needs -fPIC # s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC CFLAGS = $(CFLAGS_COPY:-Wcast-qual=) -fPIC # Without "." here, check-recursive will run through the subdirectories first # and then run "make check" here. This will fail, because there's things in # the subdirectories that need check_LTLIBRARIES built first. Adding "." here # changes the order so the subdirectories are processed afterwards. SUBDIRS = . tests noinst_HEADERS = crmcommon_private.h \ mock_private.h libcrmcommon_la_LDFLAGS = -version-info 47:0:13 libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) libcrmcommon_la_LIBADD = @LIBADD_DL@ # If configured with --with-profiling or --with-coverage, BUILD_PROFILING will # be set and -fno-builtin will be added to the CFLAGS. However, libcrmcommon # uses the fabs() function which is normally supplied by gcc as one of its # builtins. Therefore we need to explicitly link against libm here or the # tests won't link. if BUILD_PROFILING libcrmcommon_la_LIBADD += -lm endif ## Library sources (*must* use += format for bumplibs) libcrmcommon_la_SOURCES = libcrmcommon_la_SOURCES += acl.c libcrmcommon_la_SOURCES += actions.c libcrmcommon_la_SOURCES += agents.c libcrmcommon_la_SOURCES += alerts.c libcrmcommon_la_SOURCES += attrs.c libcrmcommon_la_SOURCES += cib.c if BUILD_CIBSECRETS libcrmcommon_la_SOURCES += cib_secrets.c endif libcrmcommon_la_SOURCES += cmdline.c libcrmcommon_la_SOURCES += digest.c libcrmcommon_la_SOURCES += health.c libcrmcommon_la_SOURCES += io.c libcrmcommon_la_SOURCES += ipc_attrd.c libcrmcommon_la_SOURCES += ipc_client.c libcrmcommon_la_SOURCES += ipc_common.c libcrmcommon_la_SOURCES += ipc_controld.c libcrmcommon_la_SOURCES += ipc_pacemakerd.c libcrmcommon_la_SOURCES += ipc_schedulerd.c libcrmcommon_la_SOURCES += ipc_server.c libcrmcommon_la_SOURCES += iso8601.c libcrmcommon_la_SOURCES += lists.c libcrmcommon_la_SOURCES += logging.c libcrmcommon_la_SOURCES += mainloop.c libcrmcommon_la_SOURCES += messages.c libcrmcommon_la_SOURCES += nodes.c libcrmcommon_la_SOURCES += nvpair.c libcrmcommon_la_SOURCES += options.c libcrmcommon_la_SOURCES += options_display.c libcrmcommon_la_SOURCES += output.c libcrmcommon_la_SOURCES += output_html.c libcrmcommon_la_SOURCES += output_log.c libcrmcommon_la_SOURCES += output_none.c libcrmcommon_la_SOURCES += output_text.c libcrmcommon_la_SOURCES += output_xml.c libcrmcommon_la_SOURCES += patchset.c libcrmcommon_la_SOURCES += patchset_display.c libcrmcommon_la_SOURCES += pid.c libcrmcommon_la_SOURCES += probes.c libcrmcommon_la_SOURCES += procfs.c libcrmcommon_la_SOURCES += remote.c libcrmcommon_la_SOURCES += resources.c libcrmcommon_la_SOURCES += results.c libcrmcommon_la_SOURCES += roles.c libcrmcommon_la_SOURCES += rules.c libcrmcommon_la_SOURCES += scheduler.c libcrmcommon_la_SOURCES += schemas.c libcrmcommon_la_SOURCES += scores.c libcrmcommon_la_SOURCES += servers.c libcrmcommon_la_SOURCES += strings.c libcrmcommon_la_SOURCES += utils.c libcrmcommon_la_SOURCES += watchdog.c libcrmcommon_la_SOURCES += xml.c libcrmcommon_la_SOURCES += xml_attr.c libcrmcommon_la_SOURCES += xml_comment.c libcrmcommon_la_SOURCES += xml_display.c +libcrmcommon_la_SOURCES += xml_element.c libcrmcommon_la_SOURCES += xml_idref.c libcrmcommon_la_SOURCES += xml_io.c libcrmcommon_la_SOURCES += xpath.c # # libcrmcommon_test is used only with unit tests, so we can mock system calls. # See mock.c for details. # include $(top_srcdir)/mk/tap.mk libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES) libcrmcommon_test_la_SOURCES += mock.c libcrmcommon_test_la_SOURCES += unittest.c libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) \ -rpath $(libdir) \ $(LDFLAGS_WRAP) # If GCC emits a builtin function in place of something we've mocked up, that will # get used instead of the mocked version which leads to unexpected test results. So # disable all builtins. Older versions of GCC (at least, on RHEL7) will still emit # replacement code for strdup (and possibly other functions) unless -fno-inline is # also added. libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) \ -DPCMK__UNIT_TESTING \ -fno-builtin \ -fno-inline # If -fno-builtin is used, -lm also needs to be added. See the comment at # BUILD_PROFILING above. libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) if BUILD_COVERAGE libcrmcommon_test_la_LIBADD += -lgcov endif libcrmcommon_test_la_LIBADD += -lcmocka libcrmcommon_test_la_LIBADD += -lm nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES) diff --git a/lib/common/digest.c b/lib/common/digest.c index fbaffd69da..5d02d9cd3d 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,358 +1,359 @@ /* * Copyright 2015-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 // GString, etc. #include // gnutls_hash_fast(), gnutls_hash_get_len() #include // gnutls_strerror() #include #include #include "crmcommon_private.h" #define BEST_EFFORT_STATUS 0 /*! * \internal * \brief Dump XML in a format used with v1 digests * * \param[in] xml Root of XML to dump * * \return Newly allocated buffer containing dumped XML */ static GString * dump_xml_for_digest(xmlNodePtr xml) { GString *buffer = g_string_sized_new(1024); /* for compatibility with the old result which is used for v1 digests */ g_string_append_c(buffer, ' '); pcmk__xml_string(xml, 0, buffer, 0); g_string_append_c(buffer, '\n'); return buffer; } /*! * \internal * \brief Calculate and return v1 digest of XML tree * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest * * \note Example return value: "c048eae664dba840e1d2060f00299e9d" */ static char * calculate_xml_digest_v1(xmlNode *input) { GString *buffer = dump_xml_for_digest(input); char *digest = NULL; // buffer->len > 2 for initial space and trailing newline CRM_CHECK(buffer->len > 2, g_string_free(buffer, TRUE); return NULL); digest = crm_md5sum((const char *) buffer->str); crm_log_xml_trace(input, "digest:source"); g_string_free(buffer, TRUE); return digest; } /*! * \internal * \brief Calculate and return the digest of a CIB, suitable for storing on disk * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest */ char * pcmk__digest_on_disk_cib(xmlNode *input) { /* Always use the v1 format for on-disk digests. * * Switching to v2 affects even full-restart upgrades, so it would be a * compatibility nightmare. * * We only use this once at startup. All other invocations are in a * separate child process. */ return calculate_xml_digest_v1(input); } /*! * \internal * \brief Calculate and return digest of an operation XML element * * The digest is invariant to changes in the order of XML attributes, provided * that \p input has no children. * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest */ char * pcmk__digest_operation(xmlNode *input) { /* Switching to v2 digests would likely cause restarts during rolling * upgrades. * * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise. */ xmlNode *sorted = pcmk__xml_copy(NULL, input); char *digest = NULL; pcmk__xe_sort_attrs(sorted); digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } /*! * \internal * \brief Calculate and return the digest of an XML tree * * \param[in] xml XML tree to digest * \param[in] filter Whether to filter certain XML attributes * * \return Newly allocated string containing digest */ char * pcmk__digest_xml(xmlNode *xml, bool filter) { /* @TODO Filtering accounts for significant CPU usage. Consider removing if * possible. */ char *digest = NULL; GString *buf = g_string_sized_new(1024); pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); digest = crm_md5sum(buf->str); pcmk__if_tracing( { char *trace_file = crm_strdup_printf("%s/digest-%s", pcmk__get_tmpdir(), digest); crm_trace("Saving %s.%s.%s to %s", crm_element_value(xml, PCMK_XA_ADMIN_EPOCH), crm_element_value(xml, PCMK_XA_EPOCH), crm_element_value(xml, PCMK_XA_NUM_UPDATES), trace_file); save_xml_to_file(xml, "digest input", trace_file); free(trace_file); }, {} ); g_string_free(buf, TRUE); return digest; } /*! * \internal * \brief Check whether calculated digest of given XML matches expected digest * * \param[in] input Root of XML tree to digest * \param[in] expected Expected digest in on-disk format * * \return true if digests match, false on mismatch or error */ bool pcmk__verify_digest(xmlNode *input, const char *expected) { char *calculated = NULL; bool passed; if (input != NULL) { calculated = pcmk__digest_on_disk_cib(input); if (calculated == NULL) { crm_perror(LOG_ERR, "Could not calculate digest for comparison"); return false; } } passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); if (passed) { crm_trace("Digest comparison passed: %s", calculated); } else { crm_err("Digest comparison failed: expected %s, calculated %s", expected, calculated); } free(calculated); return passed; } /*! * \internal * \brief Check whether an XML attribute should be excluded from CIB digests * * \param[in] name XML attribute name * * \return true if XML attribute should be excluded from CIB digest calculation */ bool pcmk__xa_filterable(const char *name) { static const char *filter[] = { PCMK_XA_CRM_DEBUG_ORIGIN, PCMK_XA_CIB_LAST_WRITTEN, PCMK_XA_UPDATE_ORIGIN, PCMK_XA_UPDATE_CLIENT, PCMK_XA_UPDATE_USER, }; for (int i = 0; i < PCMK__NELEM(filter); i++) { if (strcmp(name, filter[i]) == 0) { return true; } } return false; } char * crm_md5sum(const char *buffer) { char *digest = NULL; gchar *raw_digest = NULL; if (buffer == NULL) { return NULL; } raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1); if (raw_digest == NULL) { crm_err("Failed to calculate hash"); return NULL; } digest = pcmk__str_copy(raw_digest); g_free(raw_digest); crm_trace("Digest %s.", digest); return digest; } // Return true if a is an attribute that should be filtered static bool should_filter_for_digest(xmlAttrPtr a, void *user_data) { if (strncmp((const char *) a->name, CRM_META "_", sizeof(CRM_META " ") - 1) == 0) { return true; } return pcmk__str_any_of((const char *) a->name, PCMK_XA_ID, PCMK_XA_CRM_FEATURE_SET, PCMK__XA_OP_DIGEST, PCMK__META_ON_NODE, PCMK__META_ON_NODE_UUID, "pcmk_external_ip", NULL); } /*! * \internal * \brief Remove XML attributes not needed for operation digest * * \param[in,out] param_set XML with operation parameters */ void pcmk__filter_op_for_digest(xmlNode *param_set) { char *key = NULL; char *timeout = NULL; guint interval_ms = 0; if (param_set == NULL) { return; } /* Timeout is useful for recurring operation digests, so grab it before * removing meta-attributes */ key = crm_meta_name(PCMK_META_INTERVAL); if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) { interval_ms = 0; } free(key); key = NULL; if (interval_ms != 0) { key = crm_meta_name(PCMK_META_TIMEOUT); timeout = crm_element_value_copy(param_set, key); } // Remove all CRM_meta_* attributes and certain other attributes pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL); // Add timeout back for recurring operation digests if (timeout != NULL) { crm_xml_add(param_set, key, timeout); } free(timeout); free(key); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include +#include char * calculate_on_disk_digest(xmlNode *input) { return calculate_xml_digest_v1(input); } char * calculate_operation_digest(xmlNode *input, const char *version) { xmlNode *sorted = sorted_xml(input, NULL, true); char *digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } char * calculate_xml_versioned_digest(xmlNode *input, gboolean sort, gboolean do_filter, const char *version) { if ((version == NULL) || (compare_version("3.0.5", version) > 0)) { xmlNode *sorted = NULL; char *digest = NULL; if (sort) { xmlNode *sorted = sorted_xml(input, NULL, true); input = sorted; } crm_trace("Using v1 digest algorithm for %s", pcmk__s(version, "unknown feature set")); digest = calculate_xml_digest_v1(input); pcmk__xml_free(sorted); return digest; } crm_trace("Using v2 digest algorithm for %s", version); return pcmk__digest_xml(input, do_filter); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c index 1637c78e21..cfe2a93618 100644 --- a/lib/common/nvpair.c +++ b/lib/common/nvpair.c @@ -1,1028 +1,517 @@ /* * 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_MAX #include // PRIu32 #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; pcmk__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); } // 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) -{ - // @TODO Replace with internal function that returns the new attribute - 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 the attribute already exists, this does nothing. Attribute values - * don't get private data. - */ - pcmk__xml_new_private_data((xmlNode *) attr); - - 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; - int rc = pcmk__scan_ll(value, &value_ll, 0LL); - - *dest = PCMK__PARSE_INT_DEFAULT; - if (rc != pcmk_rc_ok) { - crm_warn("Using default for %s " - "because '%s' is not a valid integer: %s", - name, value, pcmk_rc_str(rc)); - } else if ((value_ll < INT_MIN) || (value_ll > INT_MAX)) { - crm_warn("Using default for %s because '%s' is out of range", - name, value); - } else { - *dest = (int) value_ll; - return 0; - } - } - return -1; -} - -/*! - * \brief Retrieve a flag group from an XML attribute value - * - * This is like \c crm_element_value() except getting the value as a 32-bit - * unsigned integer. - * - * \param[in] xml XML node to check - * \param[in] name Attribute name to check (must not be NULL) - * \param[out] dest Where to store flags (may be NULL to just - * validate type) - * \param[in] default_value What to use for missing or invalid value - * - * \return Standard Pacemaker return code - */ -int -pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest, - uint32_t default_value) -{ - const char *value = NULL; - long long value_ll = 0LL; - int rc = pcmk_rc_ok; - - if (dest != NULL) { - *dest = default_value; - } - - if (name == NULL) { - return EINVAL; - } - if (xml == NULL) { - return pcmk_rc_ok; - } - value = crm_element_value(xml, name); - if (value == NULL) { - return pcmk_rc_ok; - } - - rc = pcmk__scan_ll(value, &value_ll, default_value); - if ((value_ll < 0) || (value_ll > UINT32_MAX)) { - value_ll = default_value; - if (rc == pcmk_rc_ok) { - rc = pcmk_rc_bad_input; - } - } - - if (dest != NULL) { - *dest = (uint32_t) value_ll; - } - return rc; -} - -/*! - * \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) { - int rc = pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT); - - if (rc == pcmk_rc_ok) { - return 0; - } - crm_warn("Using default for %s " - "because '%s' is not a valid integer: %s", - name, value, pcmk_rc_str(rc)); - } - 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; - int rc = pcmk_rc_ok; - - CRM_CHECK(dest != NULL, return -1); - *dest = 0; - value = crm_element_value(data, name); - rc = pcmk__scan_ll(value, &value_ll, 0LL); - if (rc != pcmk_rc_ok) { - crm_warn("Using default for %s " - "because '%s' is not valid milliseconds: %s", - name, value, pcmk_rc_str(rc)); - return -1; - } - if ((value_ll < 0) || (value_ll > G_MAXUINT)) { - crm_warn("Using default for %s because '%s' is out of range", - name, value); - 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 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; pcmk__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; pcmk__assert((pair_a != NULL) && (pair_a->name != NULL) && (pair_b != NULL) && (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); } 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; } 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); } void pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) { g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); } 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); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml.c b/lib/common/xml.c index 7df84fec25..c3597511ab 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,2623 +1,1597 @@ /* * 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 // xmlCleanupParser() +#include // xmlNode, etc. #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 (xml == NULL) { return true; } 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; } void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags) { for (; xml != NULL; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, flags); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { if (xml != NULL) { 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); pcmk__xml_set_parent_flags(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(). */ bool pcmk__xml_reset_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 */ static void mark_xml_tree_dirty_created(xmlNode *xml) { pcmk__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) { pcmk__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; } } } /*! * \internal * \brief Allocate and initialize private data for an XML node * * \param[in,out] node XML node whose private data to initialize * \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 new_private_data(xmlNode *node, void *user_data) { CRM_CHECK(node != NULL, return true); if (node->_private != NULL) { return true; } 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; node->_private = docpriv; pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); } 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; node->_private = nodepriv; pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; iter = iter->next) { new_private_data((xmlNode *) iter, user_data); } } break; case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: return true; default: CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); return true; } if (pcmk__tracking_xml_changes(node, false)) { pcmk__mark_xml_node_dirty(node); } return true; } /*! * \internal * \brief Free private data for an XML node * * \param[in,out] node XML node whose private data to free * \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 free_private_data(xmlNode *node, void *user_data) { CRM_CHECK(node != NULL, return true); if (node->_private == NULL) { return true; } if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data((xml_doc_private_t *) node->_private); } else { xml_node_private_t *nodepriv = node->_private; pcmk__assert(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC); for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; iter = iter->next) { free_private_data((xmlNode *) iter, user_data); } } free(node->_private); node->_private = NULL; return true; } /*! * \internal * \brief Allocate and initialize private data recursively for an XML tree * * \param[in,out] node XML node whose private data to initialize */ void pcmk__xml_new_private_data(xmlNode *xml) { pcmk__xml_tree_foreach(xml, new_private_data, NULL); } /*! * \internal * \brief Free private data recursively for an XML tree * * \param[in,out] node XML node whose private data to free */ void pcmk__xml_free_private_data(xmlNode *xml) { pcmk__xml_tree_foreach(xml, free_private_data, NULL); } 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) { pcmk__xml_reset_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 Parse an integer score from an XML attribute - * - * \param[in] xml XML element with attribute to parse - * \param[in] name Name of attribute to parse - * \param[out] score Where to store parsed score (can be NULL to - * just validate) - * \param[in] default_score What to return if the attribute value is not - * present or invalid - * - * \return Standard Pacemaker return code - */ -int -pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score, - int default_score) -{ - const char *value = NULL; - - CRM_CHECK((xml != NULL) && (name != NULL), return EINVAL); - value = crm_element_value(xml, name); - return pcmk_parse_score(value, score, default_score); -} - -/*! - * \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 pcmk_parse_score(). - * - * 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 (if NULL, initial value - * will be left unchanged) - * - * \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) { - // @TODO Maybe instead delete the attribute or set it to 0 - 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 == '='))) { - - int add = 1; - int old_value_i = 0; - int rc = pcmk_rc_ok; - - // If we're expanding ourselves, no previous value was set; use 0 - if (old_value != value) { - rc = pcmk_parse_score(old_value, &old_value_i, 0); - if (rc != pcmk_rc_ok) { - // @TODO This is inconsistent with old_value==NULL - crm_trace("Using 0 before incrementing %s because '%s' " - "is not a score", name, old_value); - } - } - - /* 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) - */ - if (*v != '+') { - rc = pcmk_parse_score(++v, &add, 0); - if (rc != pcmk_rc_ok) { - // @TODO We should probably skip expansion instead - crm_trace("Not incrementing %s because '%s' does not have " - "a valid increment", name, value); - } - } - - 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 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) { - pcmk__xa_remove(xmlHasProp(element, (pcmkXmlStr) name), false); - } -} - -/*! - * \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 (pcmk__xa_remove(a, false) != 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; - - pcmk__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_new_private_data(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) { xmlDoc *doc = xmlNewDoc(XML_VERSION); pcmk__mem_assert(doc); pcmk__xml_new_private_data((xmlNode *) 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) { if (doc != NULL) { pcmk__xml_free_private_data((xmlNode *) doc); 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; - - /* xmlNodeSetContent() frees node->children and replaces it with new - * text. If this function is called for a node that already has a non- - * text child, it's a bug. - */ - CRM_CHECK((node->children == NULL) - || (node->children->type == XML_TEXT_NODE), - return); - - 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 { - pcmk__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 pcmk__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; - - pcmk__assert(!pcmk__str_empty(format)); - - if (node == NULL) { - return; - } - - va_start(ap, format); - pcmk__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 */ void pcmk__xml_free_node(xmlNode *xml) { pcmk__xml_free_private_data(xml); 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); } } pcmk__xml_free_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 pcmk__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_new_private_data(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: pcmk__xml_free_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 pcmk__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 pcmk__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; /* Restore the old value (without setting dirty flag recursively upwards or * checking ACLs) */ pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) attr = xmlHasProp(new_xml, (pcmkXmlStr) attr_name); nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal pcmk__xa_remove(attr, false); 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) { xml_doc_private_t *docpriv = new_xml->doc->_private; 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 (without checking ACLs) pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // 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 pcmk__xa_remove(new_attr, true); } } } } /*! * \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, pcmk__xml_reset_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) { mark_xml_tree_dirty_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 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) -{ - // Pass old for its doc; it won't remain the parent of new - new = pcmk__xml_copy(old, new); - old = xmlReplaceNode(old, new); - - // old == NULL means memory allocation error - pcmk__assert(old != NULL); - - // May be unnecessary but avoids slight changes to some test outputs - pcmk__xml_tree_foreach(new, pcmk__xml_reset_node_flags, NULL); - - if (xml_tracking_changes(new)) { - // Replaced sections may have included relevant ACLs - pcmk__apply_acl(new); - } - xml_calculate_changes(old, new); - pcmk__xml_free_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); // 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); - - pcmk__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); pcmk__xml_new_private_data(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) { pcmk__xml_free_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); - pcmk__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 diff --git a/lib/common/xml_element.c b/lib/common/xml_element.c new file mode 100644 index 0000000000..f4aca591c7 --- /dev/null +++ b/lib/common/xml_element.c @@ -0,0 +1,1608 @@ +/* + * 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 // va_start(), etc. +#include // uint32_t +#include // NULL, etc. +#include // free(), etc. +#include // strchr(), etc. +#include // time_t, etc. + +#include // xmlNode, etc. +#include // xmlValidateNameValue() + +#include +#include // crm_xml_add(), etc. +#include // pcmk_rc_ok, etc. +#include +#include "crmcommon_private.h" + +/*! + * \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 Parse an integer score from an XML attribute + * + * \param[in] xml XML element with attribute to parse + * \param[in] name Name of attribute to parse + * \param[out] score Where to store parsed score (can be NULL to + * just validate) + * \param[in] default_score What to return if the attribute value is not + * present or invalid + * + * \return Standard Pacemaker return code + */ +int +pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score, + int default_score) +{ + const char *value = NULL; + + CRM_CHECK((xml != NULL) && (name != NULL), return EINVAL); + value = crm_element_value(xml, name); + return pcmk_parse_score(value, score, default_score); +} + +/*! + * \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 pcmk_parse_score(). + * + * 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 (if NULL, initial value + * will be left unchanged) + * + * \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) { + // @TODO Maybe instead delete the attribute or set it to 0 + 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 == '='))) { + + int add = 1; + int old_value_i = 0; + int rc = pcmk_rc_ok; + + // If we're expanding ourselves, no previous value was set; use 0 + if (old_value != value) { + rc = pcmk_parse_score(old_value, &old_value_i, 0); + if (rc != pcmk_rc_ok) { + // @TODO This is inconsistent with old_value==NULL + crm_trace("Using 0 before incrementing %s because '%s' " + "is not a score", name, old_value); + } + } + + /* 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) + */ + if (*v != '+') { + rc = pcmk_parse_score(++v, &add, 0); + if (rc != pcmk_rc_ok) { + // @TODO We should probably skip expansion instead + crm_trace("Not incrementing %s because '%s' does not have " + "a valid increment", name, value); + } + } + + 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 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) { + pcmk__xa_remove(xmlHasProp(element, (pcmkXmlStr) name), false); + } +} + +/*! + * \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 (pcmk__xa_remove(a, false) != 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; + + pcmk__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_new_private_data(node); + return node; +} + +/*! + * \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; + + /* xmlNodeSetContent() frees node->children and replaces it with new + * text. If this function is called for a node that already has a non- + * text child, it's a bug. + */ + CRM_CHECK((node->children == NULL) + || (node->children->type == XML_TEXT_NODE), + return); + + 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 { + pcmk__assert(vasprintf(&buf, format, ap) >= 0); + content = buf; + } + va_end(ap); + } + + xmlNodeSetContent(node, (pcmkXmlStr) content); + free(buf); + } +} + +/*! + * \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; + + pcmk__assert(!pcmk__str_empty(format)); + + if (node == NULL) { + return; + } + + va_start(ap, format); + pcmk__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 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 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) +{ + // Pass old for its doc; it won't remain the parent of new + new = pcmk__xml_copy(old, new); + old = xmlReplaceNode(old, new); + + // old == NULL means memory allocation error + pcmk__assert(old != NULL); + + // May be unnecessary but avoids slight changes to some test outputs + pcmk__xml_tree_foreach(new, pcmk__xml_reset_node_flags, NULL); + + if (xml_tracking_changes(new)) { + // Replaced sections may have included relevant ACLs + pcmk__apply_acl(new); + } + xml_calculate_changes(old, new); + pcmk__xml_free_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; +} + +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); + + pcmk__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; +} + +// 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) +{ + // @TODO Replace with internal function that returns the new attribute + 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 the attribute already exists, this does nothing. Attribute values + * don't get private data. + */ + pcmk__xml_new_private_data((xmlNode *) attr); + + 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; + int rc = pcmk__scan_ll(value, &value_ll, 0LL); + + *dest = PCMK__PARSE_INT_DEFAULT; + if (rc != pcmk_rc_ok) { + crm_warn("Using default for %s " + "because '%s' is not a valid integer: %s", + name, value, pcmk_rc_str(rc)); + } else if ((value_ll < INT_MIN) || (value_ll > INT_MAX)) { + crm_warn("Using default for %s because '%s' is out of range", + name, value); + } else { + *dest = (int) value_ll; + return 0; + } + } + return -1; +} + +/*! + * \internal + * \brief Retrieve a flag group from an XML attribute value + * + * This is like \c crm_element_value() except getting the value as a 32-bit + * unsigned integer. + * + * \param[in] xml XML node to check + * \param[in] name Attribute name to check (must not be NULL) + * \param[out] dest Where to store flags (may be NULL to just + * validate type) + * \param[in] default_value What to use for missing or invalid value + * + * \return Standard Pacemaker return code + */ +int +pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest, + uint32_t default_value) +{ + const char *value = NULL; + long long value_ll = 0LL; + int rc = pcmk_rc_ok; + + if (dest != NULL) { + *dest = default_value; + } + + if (name == NULL) { + return EINVAL; + } + if (xml == NULL) { + return pcmk_rc_ok; + } + value = crm_element_value(xml, name); + if (value == NULL) { + return pcmk_rc_ok; + } + + rc = pcmk__scan_ll(value, &value_ll, default_value); + if ((value_ll < 0) || (value_ll > UINT32_MAX)) { + value_ll = default_value; + if (rc == pcmk_rc_ok) { + rc = pcmk_rc_bad_input; + } + } + + if (dest != NULL) { + *dest = (uint32_t) value_ll; + } + return rc; +} + +/*! + * \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) { + int rc = pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT); + + if (rc == pcmk_rc_ok) { + return 0; + } + crm_warn("Using default for %s " + "because '%s' is not a valid integer: %s", + name, value, pcmk_rc_str(rc)); + } + 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; + int rc = pcmk_rc_ok; + + CRM_CHECK(dest != NULL, return -1); + *dest = 0; + value = crm_element_value(data, name); + rc = pcmk__scan_ll(value, &value_ll, 0LL); + if (rc != pcmk_rc_ok) { + crm_warn("Using default for %s " + "because '%s' is not valid milliseconds: %s", + name, value, pcmk_rc_str(rc)); + return -1; + } + if ((value_ll < 0) || (value_ll > G_MAXUINT)) { + crm_warn("Using default for %s because '%s' is out of range", + name, value); + 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)); +} + +/*! + * \internal + * \brief Add a boolean attribute to an XML node. + * + * \param[in,out] node XML node to add attributes to + * \param[in] name XML attribute to create + * \param[in] value Value to give to the attribute + */ +void +pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value) +{ + crm_xml_add(node, name, pcmk__btoa(value)); +} + +/*! + * \internal + * \brief Extract a boolean attribute's value from an XML element, with + * error checking + * + * \param[in] node XML node to get attribute from + * \param[in] name XML attribute to get + * \param[out] value Destination for the value of the attribute + * + * \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is + * NULL or the attribute does not exist, pcmk_rc_unknown_format + * if the attribute is not a boolean, and pcmk_rc_ok otherwise. + * + * \note \p value only has any meaning if the return value is pcmk_rc_ok. + */ +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; + } +} + +/*! + * \internal + * \brief Extract a boolean attribute's value from an XML element + * + * \param[in] node XML node to get attribute from + * \param[in] name XML attribute to get + * + * \return True if the given \p name is an attribute on \p node and has + * the value \c PCMK_VALUE_TRUE, False in all other cases + */ +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; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include // gboolean, GSList + +#include // pcmk_xml_attrs2nvpairs(), etc. +#include // crm_xml_sanitize_id() +#include + +xmlNode * +expand_idref(xmlNode *input, xmlNode *top) +{ + return pcmk__xe_resolve_idref(input, top); +} + +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); + pcmk__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