diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index fa8996ca63..f47503611a 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. # MAINTAINERCLEANFILES = Makefile.in headerdir=$(pkgincludedir)/crm/common header_HEADERS = acl.h \ actions.h \ agents.h \ agents_compat.h \ cib.h \ ipc.h \ ipc_controld.h \ ipc_pacemakerd.h \ ipc_schedulerd.h \ iso8601.h \ logging.h \ logging_compat.h \ mainloop.h \ mainloop_compat.h \ nodes.h \ nodes_internal.h \ nvpair.h \ options.h \ output.h \ resources.h \ results.h \ results_compat.h \ roles.h \ rules.h \ scheduler.h \ scheduler_types.h \ scores.h \ scores_compat.h \ tags.h \ tickets.h \ util.h \ util_compat.h \ xml.h \ xml_compat.h \ + xml_io.h \ + xml_io_compat.h \ xml_names.h noinst_HEADERS = $(wildcard *internal.h) diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h index f6830fa6e0..4d50542ad2 100644 --- a/include/crm/common/xml.h +++ b/include/crm/common/xml.h @@ -1,255 +1,241 @@ /* * 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__H # define PCMK__CRM_COMMON_XML__H # include # include # include # include # include # include # include # include # include # include +# include # include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Wrappers for and extensions to libxml2 * \ingroup core */ -/* Define compression parameters for IPC messages - * - * Compression costs a LOT, so we don't want to do it unless we're hitting - * message limits. Currently, we use 128KB as the threshold, because higher - * values don't play well with the heartbeat stack. With an earlier limit of - * 10KB, compressing 184 of 1071 messages accounted for 23% of the total CPU - * used by the cib. - */ -# define CRM_BZ2_BLOCKS 4 -# define CRM_BZ2_WORK 20 -# define CRM_BZ2_THRESHOLD 128 * 1024 - typedef const xmlChar *pcmkXmlStr; gboolean add_message_xml(xmlNode * msg, const char *field, xmlNode * xml); xmlNode *get_message_xml(const xmlNode *msg, const char *field); /* * \brief xmlCopyPropList ACLs-sensitive replacement expading i++ notation * * The gist is the same as with \c{xmlCopyPropList(target, src->properties)}. * The function exits prematurely when any attribute cannot be copied for * ACLs violation. Even without bailing out, the result can possibly be * incosistent with expectations in that case, hence the caller shall, * aposteriori, verify that no document-level-tracked denial was indicated * with \c{xml_acl_denied(target)} and drop whole such intermediate object. * * \param[in,out] target Element to receive attributes from #src element * \param[in] src Element carrying attributes to copy over to #target * * \note Original commit 1c632c506 sadly haven't stated which otherwise * assumed behaviours of xmlCopyPropList were missing beyond otherwise * custom extensions like said ACLs and "atomic increment" (that landed * later on, anyway). */ void copy_in_properties(xmlNode *target, const xmlNode *src); void expand_plus_plus(xmlNode * target, const char *name, const char *value); void fix_plus_plus_recursive(xmlNode * target); /* * Create a node named "name" as a child of "parent" * If parent is NULL, creates an unconnected node. * * Returns the created node * */ xmlNode *create_xml_node(xmlNode * parent, const char *name); /* * Create a node named "name" as a child of "parent", giving it the provided * text content. * If parent is NULL, creates an unconnected node. * * Returns the created node * */ xmlNode *pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content); /* * Create a new HTML node named "element_name" as a child of "parent", giving it the * provided text content. Optionally, apply a CSS #id and #class. * * Returns the created node. */ xmlNode *pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id, const char *class_name, const char *text); /* * Searching & Modifying */ xmlNode *find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find); void xml_remove_prop(xmlNode * obj, const char *name); gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only); gboolean update_xml_child(xmlNode * child, xmlNode * to_update); int find_xml_children(xmlNode ** children, xmlNode * root, const char *tag, const char *field, const char *value, gboolean search_matches); xmlNode *get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level); xmlNode *get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level); static inline const char * crm_map_element_name(const xmlNode *xml) { if (xml == NULL) { return NULL; } else if (strcmp((const char *) xml->name, "master") == 0) { // Can't use PCMK__XE_PROMOTABLE_LEGACY because it's internal return PCMK_XE_CLONE; } else { return (const char *) xml->name; } } char *calculate_on_disk_digest(xmlNode * local_cib); char *calculate_operation_digest(xmlNode * local_cib, const char *version); char *calculate_xml_versioned_digest(xmlNode * input, gboolean sort, gboolean do_filter, const char *version); /* schema-related functions (from schemas.c) */ gboolean validate_xml(xmlNode * xml_blob, const char *validation, gboolean to_logs); gboolean validate_xml_verbose(const xmlNode *xml_blob); /*! * \brief Update CIB XML to most recent schema version * * "Update" means either actively employ XSLT-based transformation(s) * (if intermediate product to transform valid per its declared schema version, * transformation available, proceeded successfully with a result valid per * expectated newer schema version), or just try to bump the marked validating * schema until all gradually rising schema versions attested or the first * such attempt subsequently fails to validate. Which of the two styles will * be used depends on \p transform parameter (positive/negative, respectively). * * \param[in,out] xml_blob XML tree representing CIB, may be swapped with * an "updated" one * \param[out] best The highest configuration version (per its index * in the global schemas table) it was possible to * reach during the update steps while ensuring * the validity of the result; if no validation * success was observed against possibly multiple * schemas, the value is less or equal the result * of \c get_schema_version applied on the input * \p xml_blob value (unless that function maps it * to -1, then 0 would be used instead) * \param[in] max When \p transform is positive, this allows to * set upper boundary schema (per its index in the * global schemas table) beyond which it's forbidden * to update by the means of XSLT transformation * \param[in] transform Whether to employ XSLT-based transformation so * as to allow overcoming possible incompatibilities * between major schema versions (see above) * \param[in] to_logs If true, output notable progress info to * internal log streams; if false, to stderr * * \return \c pcmk_ok if no non-recoverable error encountered (up to * caller to evaluate if the update satisfies the requirements * per returned \p best value), negative value carrying the reason * otherwise */ int update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, gboolean to_logs); int get_schema_version(const char *name); const char *get_schema_name(int version); const char *xml_latest_schema(void); gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs); /*! * \brief Initialize the CRM XML subsystem * * This method sets global XML settings and loads pacemaker schemas into the cache. */ void crm_xml_init(void); void crm_xml_cleanup(void); void pcmk_free_xml_subtree(xmlNode *xml); void free_xml(xmlNode * child); xmlNode *first_named_child(const xmlNode *parent, const char *name); xmlNode *crm_next_same_xml(const xmlNode *sibling); xmlNode *sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive); xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path); void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data); xmlNode *expand_idref(xmlNode * input, xmlNode * top); void freeXpathObject(xmlXPathObjectPtr xpathObj); xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index); void dedupXpathResults(xmlXPathObjectPtr xpathObj); static inline int numXpathResults(xmlXPathObjectPtr xpathObj) { if(xpathObj == NULL || xpathObj->nodesetval == NULL) { return 0; } return xpathObj->nodesetval->nodeNr; } bool xml_tracking_changes(xmlNode * xml); bool xml_document_dirty(xmlNode *xml); void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls); void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_accept_changes(xmlNode * xml); bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]); xmlNode *xml_create_patchset( int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version); int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version); void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest); -void save_xml_to_file(const xmlNode *xml, const char *desc, - const char *filename); - void crm_xml_sanitize_id(char *id); void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #ifdef __cplusplus } #endif #endif diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h index c8a757edaf..351e0bc4e9 100644 --- a/include/crm/common/xml_compat.h +++ b/include/crm/common/xml_compat.h @@ -1,126 +1,102 @@ /* * 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 // crm_xml_add() #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. */ //! \deprecated Do not use (will be removed in a future release) #define XML_PARANOIA_CHECKS 0 //! \deprecated This function will be removed in a future release xmlDoc *getDocPtr(xmlNode *node); //! \deprecated This function will be removed in a future release int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child); //! \deprecated This function will be removed in a future release xmlNode *find_entity(xmlNode *parent, const char *node_name, const char *id); //! \deprecated This function will be removed in a future release char *xml_get_path(const xmlNode *xml); //! \deprecated This function will be removed in a future release void xml_log_changes(uint8_t level, const char *function, const xmlNode *xml); //! \deprecated This function will be removed in a future release void xml_log_patchset(uint8_t level, const char *function, const xmlNode *xml); //! \deprecated Use xml_apply_patchset() instead gboolean apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml); //! \deprecated Do not use (will be removed in a future release) void crm_destroy_xml(gpointer data); //! \deprecated Check children member directly gboolean xml_has_children(const xmlNode *root); //! \deprecated Use crm_xml_add() with "true" or "false" instead static inline const char * crm_xml_add_boolean(xmlNode *node, const char *name, gboolean value) { return crm_xml_add(node, name, (value? "true" : "false")); } //! \deprecated Use name member directly static inline const char * crm_element_name(const xmlNode *xml) { return (xml == NULL)? NULL : (const char *) xml->name; } //! \deprecated Do not use char *crm_xml_escape(const char *text); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *copy_xml(xmlNode *src_node); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *add_node_copy(xmlNode *new_parent, xmlNode *xml_node); //! \deprecated Do not use void purge_diff_markers(xmlNode *a_node); //! \deprecated Do not use xmlNode *diff_xml_object(xmlNode *left, xmlNode *right, gboolean suppress); //! \deprecated Do not use xmlNode *subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, gboolean full, gboolean *changed, const char *marker); //! \deprecated Do not use gboolean can_prune_leaf(xmlNode *xml_node); -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *filename2xml(const char *filename); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *stdin2xml(void); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -xmlNode *string2xml(const char *input); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -int write_xml_fd(const xmlNode *xml, const char *filename, int fd, - gboolean compress); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -int write_xml_file(const xmlNode *xml, const char *filename, gboolean compress); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -char *dump_xml_formatted(const xmlNode *xml); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -char *dump_xml_formatted_with_text(const xmlNode *xml); - -//! \deprecated Do not use Pacemaker for general-purpose XML manipulation -char *dump_xml_unformatted(const xmlNode *xml); - #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_COMPAT__H diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index c0abd6051b..504d097fc9 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,494 +1,479 @@ /* * 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__XML_INTERNAL__H # define PCMK__XML_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 (libxslt) */ # include # include # include # include /* transitively imports qblog.h */ # include +# include # include /*! * \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), // @COMPAT Remove when v1 patchsets are removed //! Log a created XML subtree pcmk__xml_fmt_diff_plus = (1 << 7), // @COMPAT Remove when v1 patchsets are removed //! Log a removed XML subtree pcmk__xml_fmt_diff_minus = (1 << 8), // @COMPAT Remove when v1 patchsets are removed //! Log a minimal version of an XML diff (only showing the changes) pcmk__xml_fmt_diff_short = (1 << 9), }; 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(@type) or @type='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 \ "[@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 \ "[@type='remote'][@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 "='true']" /*! * \internal * \brief Serialize XML (using libxml) into provided descriptor * * \param[in] fd File descriptor to (piece-wise) write to * \param[in] cur XML subtree to proceed * * \return a standard Pacemaker return code */ int pcmk__xml2fd(int fd, xmlNode *cur); 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_match(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v); void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data); GString *pcmk__element_xpath(const xmlNode *xml); bool pcmk__xml_needs_escape(const char *text, bool escape_quote); char *pcmk__xml_escape(const char *text, bool escape_quote); /*! * \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 first non-text child element of an XML node * * \param[in] parent XML node to check * * \return First child element of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xe_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type != XML_ELEMENT_NODE)) { child = child->next; } return child; } /*! * \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__xml_copy(xmlNode *parent, xmlNode *src); void pcmk__xe_set_content(xmlNode *node, const char *content); /*! * \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 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); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context); void pcmk__log_known_schemas(void); const char *pcmk__remote_schema_dir(void); void pcmk__sort_schemas(void); - -/* - * I/O - */ - -xmlNode *pcmk__xml_read(const char *filename); -xmlNode *pcmk__xml_parse(const char *input); - -int pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd, - bool compress, unsigned int *nbytes); -int pcmk__xml_write_file(const xmlNode *xml, const char *filename, - bool compress, unsigned int *nbytes); - -gchar *pcmk__xml_dump(const xmlNode *xml, uint32_t flags); - - // @COMPAT Remove when v1 patchsets are removed xmlNode *pcmk__diff_v1_xml_object(xmlNode *left, xmlNode *right, bool suppress); #endif // PCMK__XML_INTERNAL__H diff --git a/include/crm/common/xml_io.h b/include/crm/common/xml_io.h new file mode 100644 index 0000000000..a5e454cb8c --- /dev/null +++ b/include/crm/common/xml_io.h @@ -0,0 +1,48 @@ +/* + * Copyright 2004-2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK__CRM_COMMON_XML_IO__H +#define PCMK__CRM_COMMON_XML_IO__H + +#include // xmlNode + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file + * \brief Wrappers for and extensions to XML input/output functions + * \ingroup core + */ + +/* Define compression parameters for IPC messages + * + * Compression costs a LOT, so we don't want to do it unless we're hitting + * message limits. Currently, we use 128KB as the threshold, because higher + * values don't play well with the heartbeat stack. With an earlier limit of + * 10KB, compressing 184 of 1071 messages accounted for 23% of the total CPU + * used by the cib. + */ +#define CRM_BZ2_BLOCKS 4 +#define CRM_BZ2_WORK 20 +#define CRM_BZ2_THRESHOLD (128 * 1024) + +void save_xml_to_file(const xmlNode *xml, const char *desc, + const char *filename); + +#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) +#include +#endif + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_XML_IO__H diff --git a/include/crm/common/xml_io_compat.h b/include/crm/common/xml_io_compat.h new file mode 100644 index 0000000000..74e5f1da81 --- /dev/null +++ b/include/crm/common/xml_io_compat.h @@ -0,0 +1,58 @@ +/* + * 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_IO_COMPAT__H +#define PCMK__CRM_COMMON_XML_IO_COMPAT__H + +#include // gboolean +#include // xmlNode + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file + * \brief Deprecated Pacemaker XML I/O 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. + */ + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +xmlNode *filename2xml(const char *filename); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +xmlNode *stdin2xml(void); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +xmlNode *string2xml(const char *input); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +int write_xml_fd(const xmlNode *xml, const char *filename, int fd, + gboolean compress); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +int write_xml_file(const xmlNode *xml, const char *filename, gboolean compress); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +char *dump_xml_formatted(const xmlNode *xml); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +char *dump_xml_formatted_with_text(const xmlNode *xml); + +//! \deprecated Do not use Pacemaker for general-purpose XML manipulation +char *dump_xml_unformatted(const xmlNode *xml); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_XML_IO_COMPAT__H diff --git a/include/crm/common/xml_io_internal.h b/include/crm/common/xml_io_internal.h new file mode 100644 index 0000000000..52035669da --- /dev/null +++ b/include/crm/common/xml_io_internal.h @@ -0,0 +1,32 @@ +/* + * 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__XML_IO_INTERNAL__H +#define PCMK__XML_IO_INTERNAL__H + +/* + * Internal-only wrappers for and extensions to libxml2 I/O + */ + +#include // bool + +#include // xmlNode + +xmlNode *pcmk__xml_read(const char *filename); +xmlNode *pcmk__xml_parse(const char *input); + +int pcmk__xml2fd(int fd, xmlNode *cur); +int pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd, + bool compress, unsigned int *nbytes); +int pcmk__xml_write_file(const xmlNode *xml, const char *filename, + bool compress, unsigned int *nbytes); + +gchar *pcmk__xml_dump(const xmlNode *xml, uint32_t flags); + +#endif // PCMK__XML_IO_INTERNAL__H diff --git a/include/crm_internal.h b/include/crm_internal.h index 3dc90fc7cf..424b8e8f25 100644 --- a/include/crm_internal.h +++ b/include/crm_internal.h @@ -1,84 +1,85 @@ /* * 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 CRM_INTERNAL__H # define 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 # 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 #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_FLUSH "flush" #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" #endif /* CRM_INTERNAL__H */ diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index 8d37d99245..317801b9f5 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 AM_CPPFLAGS += -I$(top_builddir)/lib/gnu \ -I$(top_srcdir)/lib/gnu ## 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 46:0:12 libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) libcrmcommon_la_LIBADD = @LIBADD_DL@ \ $(top_builddir)/lib/gnu/libgnu.la # 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 += 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_display.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/xml.c b/lib/common/xml.c index 5aadf416a0..6d3f09b552 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,3091 +1,2269 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include +#include +#include #include -#include -#include -#include -#include -#include #include -#include -#include +#include +#include // stat(), S_ISREG, etc. +#include #include #include -#include /* xmlAllocOutputBuffer */ #include #include -#include // PCMK__XML_LOG_BASE, etc. +#include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" // Define this as 1 in development to get insanely verbose trace messages #ifndef XML_PARSER_DEBUG #define XML_PARSER_DEBUG 0 #endif -/* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around - * by libxml2, which is potentially ambiguous and dangerous. We should drop it - * when we can break backward compatibility with configurations that might be - * relying on it (i.e. pacemaker 3.0.0). - * - * It might be a good idea to have a transitional period where we first try - * parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with - * it, logging a warning if it succeeds. - */ -#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) -#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER) - bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_tracking)) { return FALSE; } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_lazy)) { return FALSE; } return TRUE; } static inline void set_parent_flag(xmlNode *xml, long flag) { for(; xml; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv == NULL) { /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ } else { pcmk__set_xml_flags(nodepriv, flag); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { if(xml && xml->doc && xml->doc->_private){ /* During calls to xmlDocCopyNode(), xml->doc may be unset */ xml_doc_private_t *docpriv = xml->doc->_private; pcmk__set_xml_flags(docpriv, flag); } } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); set_parent_flag(xml, pcmk__xf_dirty); } // Clear flags on XML node and its children static void reset_xml_node_flags(xmlNode *xml) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = xml->_private; if (nodepriv) { nodepriv->flags = 0; } for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { reset_xml_node_flags(cIter); } } // Set xpf_created flag on XML node and any children void pcmk__mark_xml_created(xmlNode *xml) { xmlNode *cIter = NULL; xml_node_private_t *nodepriv = NULL; CRM_ASSERT(xml != NULL); nodepriv = xml->_private; if (nodepriv && pcmk__tracking_xml_changes(xml, FALSE)) { if (!pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { pcmk__set_xml_flags(nodepriv, pcmk__xf_created); pcmk__mark_xml_node_dirty(xml); } for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { pcmk__mark_xml_created(cIter); } } } #define XML_DOC_PRIVATE_MAGIC 0x81726354UL #define XML_NODE_PRIVATE_MAGIC 0x54637281UL // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC); free(docpriv->user); docpriv->user = NULL; if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } // Free all private data associated with an XML node static void free_private_data(xmlNode *node) { /* Note: This function frees private data assosciated with an XML node, unless the function is being called as a result of internal XSLT cleanup. That could happen through, for example, the following chain of function calls: xsltApplyStylesheetInternal -> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc And in that case, the node would fulfill three conditions: 1. It would be a standalone document (i.e. it wouldn't be part of a document) 2. It would have a space-prefixed name (for reference, please see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) 3. It would carry its own payload in the _private field. We do not free data in this circumstance to avoid a failed assertion on the XML_*_PRIVATE_MAGIC later. */ if (node->name == NULL || node->name[0] != ' ') { if (node->_private) { if (node->type == XML_DOCUMENT_NODE) { reset_xml_private_data(node->_private); } else { CRM_ASSERT(((xml_node_private_t *) node->_private)->check == XML_NODE_PRIVATE_MAGIC); /* nothing dynamically allocated nested */ } free(node->_private); node->_private = NULL; } } } // Allocate and initialize private data for an XML node static void new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { xml_doc_private_t *docpriv = NULL; docpriv = calloc(1, sizeof(xml_doc_private_t)); CRM_ASSERT(docpriv != NULL); docpriv->check = XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = docpriv; break; } case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { xml_node_private_t *nodepriv = NULL; nodepriv = calloc(1, sizeof(xml_node_private_t)); CRM_ASSERT(nodepriv != NULL); nodepriv->check = XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); node->_private = nodepriv; if (pcmk__tracking_xml_changes(node, FALSE)) { /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is * not hooked up at the point we are called */ pcmk__mark_xml_node_dirty(node); } break; } case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: break; default: /* Ignore */ crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); break; } } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_dirty); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } // Remove all attributes marked as deleted from an XML node static void accept_attr_deletions(xmlNode *xml) { // Clear XML node's flags ((xml_node_private_t *) xml->_private)->flags = pcmk__xf_none; // Remove this XML node's attributes that were marked as deleted pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); // Recursively do the same for this XML node's children for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { accept_attr_deletions(cIter); } } /*! * \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_match(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; accept_attr_deletions(top); } xmlNode * find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) { xmlNode *a_child = NULL; const char *name = (root == NULL)? "" : (const char *) root->name; if (search_path == NULL) { crm_warn("Will never find "); return NULL; } for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (strcmp((const char *)a_child->name, search_path) == 0) { return a_child; } } if (must_find) { crm_warn("Could not find %s in %s.", search_path, name); } else if (root != NULL) { crm_trace("Could not find %s in %s.", search_path, name); } else { crm_trace("Could not find %s in .", search_path); } return NULL; } #define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \ (v), pcmk__str_none) /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search * \param[in] node_name If not NULL, only match children of this type * \param[in] attr_n If not 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 NULL if none found */ xmlNode * pcmk__xe_match(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { CRM_CHECK(parent != NULL, return NULL); CRM_CHECK(attr_v == NULL || attr_n != NULL, return NULL); for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL; child = pcmk__xml_next(child)) { if (((node_name == NULL) || pcmk__xe_is(child, node_name)) && ((attr_n == NULL) || (attr_v == NULL && xmlHasProp(child, (pcmkXmlStr) attr_n)) || (attr_v != NULL && attr_matches(child, attr_n, attr_v)))) { return child; } } crm_trace("XML child node <%s%s%s%s%s> not found in %s", (node_name? node_name : "(any)"), (attr_n? " " : ""), (attr_n? attr_n : ""), (attr_n? "=" : ""), (attr_n? attr_v : ""), (const char *) parent->name); return NULL; } void copy_in_properties(xmlNode *target, const xmlNode *src) { if (src == NULL) { crm_warn("No node to copy properties from"); } else if (target == NULL) { crm_err("No node to copy properties into"); } else { for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); expand_plus_plus(target, p_name, p_value); if (xml_acl_denied(target)) { crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); return; } } } return; } /*! * \brief Parse integer assignment statements on this node and all its child * nodes * * \param[in,out] target Root XML node to be processed * * \note This function is recursive */ void fix_plus_plus_recursive(xmlNode *target) { /* TODO: Remove recursion and use xpath searches for value++ */ xmlNode *child = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); expand_plus_plus(target, p_name, p_value); } for (child = pcmk__xml_first_child(target); child != NULL; child = pcmk__xml_next(child)) { fix_plus_plus_recursive(child); } } /*! * \brief Update current XML attribute value per parsed integer assignment statement * * \param[in,out] target an XML node, containing a XML attribute that is * initialized to some numeric value, to be processed * \param[in] name name of the XML attribute, e.g. X, whose value * should be updated * \param[in] value assignment statement, e.g. "X++" or * "X+=5", to be applied to the initialized value. * * \note The original XML attribute value is treated as 0 if non-numeric and * truncated to be an integer if decimal-point-containing. * \note The final XML attribute value is truncated to not exceed 1000000. * \note Undefined behavior if unexpected input. */ void expand_plus_plus(xmlNode * target, const char *name, const char *value) { int offset = 1; int name_len = 0; int int_value = 0; int value_len = 0; const char *old_value = NULL; if (target == NULL || value == NULL || name == NULL) { return; } old_value = crm_element_value(target, name); if (old_value == NULL) { /* if no previous value, set unexpanded */ goto set_unexpanded; } else if (strstr(value, name) != value) { goto set_unexpanded; } name_len = strlen(name); value_len = strlen(value); if (value_len < (name_len + 2) || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) { goto set_unexpanded; } /* if we are expanding ourselves, * then no previous value was set and leave int_value as 0 */ if (old_value != value) { int_value = char2score(old_value); } if (value[name_len + 1] != '+') { const char *offset_s = value + (name_len + 2); offset = char2score(offset_s); } int_value += offset; if (int_value > PCMK_SCORE_INFINITY) { int_value = PCMK_SCORE_INFINITY; } crm_xml_add_int(target, name, int_value); return; set_unexpanded: if (old_value == value) { /* the old value is already set, nothing to do */ return; } crm_xml_add(target, name, value); return; } /*! * \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__check_acl(element, NULL, pcmk__xf_acl_write)) { crm_trace("ACLs prevent removal of attributes (%s and " "possibly others) from %s element", (const char *) a->name, (const char *) element->name); return; // ACLs apply to element, not particular attributes } if (pcmk__tracking_xml_changes(element, false)) { // Leave (marked for removal) until after diff is calculated set_parent_flag(element, pcmk__xf_dirty); pcmk__set_xml_flags((xml_node_private_t *) a->_private, pcmk__xf_deleted); } else { xmlRemoveProp(a); } } } } xmlNode * create_xml_node(xmlNode * parent, const char *name) { xmlDoc *doc = NULL; xmlNode *node = NULL; if (pcmk__str_empty(name)) { CRM_CHECK(name != NULL && name[0] == 0, return NULL); return NULL; } if (parent == NULL) { doc = xmlNewDoc(PCMK__XML_VERSION); if (doc == NULL) { return NULL; } node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); if (node == NULL) { xmlFreeDoc(doc); return NULL; } xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); if (node == NULL) { return NULL; } } pcmk__mark_xml_created(node); return node; } /*! * \internal * \brief Set a given string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] content String to set as the content * * \note \c xmlNodeSetContent() does not escape special characters. */ void pcmk__xe_set_content(xmlNode *node, const char *content) { if (node != NULL) { char *escaped = pcmk__xml_escape(content, false); xmlNodeSetContent(node, (pcmkXmlStr) escaped); free(escaped); } } xmlNode * pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content) { xmlNode *node = create_xml_node(parent, name); pcmk__xe_set_content(node, content); return node; } xmlNode * pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id, const char *class_name, const char *text) { xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text); if (class_name != NULL) { crm_xml_add(node, PCMK_XA_CLASS, class_name); } if (id != NULL) { crm_xml_add(node, PCMK_XA_ID, id); } return node; } /*! * Free an XML element and all of its children, removing it from its parent * * \param[in,out] xml XML element to free */ void pcmk_free_xml_subtree(xmlNode *xml) { xmlUnlinkNode(xml); // Detaches from parent and siblings xmlFreeNode(xml); // Frees } static void free_xml_with_position(xmlNode * child, int position) { if (child != NULL) { xmlNode *top = NULL; xmlDoc *doc = child->doc; xml_node_private_t *nodepriv = child->_private; xml_doc_private_t *docpriv = NULL; if (doc != NULL) { top = xmlDocGetRootElement(doc); } if (doc != NULL && top == child) { /* Free everything */ xmlFreeDoc(doc); } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(child); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, (const char *) xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } else { if (doc && pcmk__tracking_xml_changes(child, FALSE) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { GString *xpath = pcmk__element_xpath(child); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", (const char *) xpath->str, child, doc); deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = strdup((const char *) xpath->str); CRM_ASSERT(deleted_obj->path != NULL); g_string_free(xpath, TRUE); deleted_obj->position = -1; /* Record the "position" only for XML comments for now */ if (child->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(child, pcmk__xf_skip); } } docpriv = doc->_private; docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); } } pcmk_free_xml_subtree(child); } } } void free_xml(xmlNode * child) { free_xml_with_position(child, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document CRM_ASSERT(src->type == XML_ELEMENT_NODE); doc = xmlNewDoc(PCMK__XML_VERSION); CRM_ASSERT(doc != NULL); copy = xmlDocCopyNode(src, doc, 1); CRM_ASSERT(copy != NULL); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); CRM_ASSERT(copy != NULL); xmlAddChild(parent, copy); } pcmk__mark_xml_created(copy); return copy; } -/*! - * \internal - * \brief Read from \c stdin until EOF or error - * - * \return Newly allocated string containing the bytes read from \c stdin, or - * \c NULL on error - * - * \note The caller is responsible for freeing the return value using \c free(). - */ -static char * -read_stdin(void) -{ - char *buf = NULL; - size_t length = 0; - - do { - buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1); - length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin); - } while ((feof(stdin) == 0) && (ferror(stdin) == 0)); - - if (ferror(stdin) != 0) { - crm_err("Error reading input from stdin"); - free(buf); - buf = NULL; - } else { - buf[length] = '\0'; - } - clearerr(stdin); - return buf; -} - -/*! - * \internal - * \brief Decompress a bzip2-compressed file into a string buffer - * - * \param[in] filename Name of file to decompress - * - * \return Newly allocated string with the decompressed contents of \p filename, - * or \c NULL on error. - * - * \note The caller is responsible for freeing the return value using \c free(). - */ -static char * -decompress_file(const char *filename) -{ - char *buffer = NULL; - int rc = pcmk_rc_ok; - size_t length = 0; - BZFILE *bz_file = NULL; - FILE *input = fopen(filename, "r"); - - if (input == NULL) { - crm_perror(LOG_ERR, "Could not open %s for reading", filename); - return NULL; - } - - bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); - rc = pcmk__bzlib2rc(rc); - if (rc != pcmk_rc_ok) { - crm_err("Could not prepare to read compressed %s: %s " - CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); - goto done; - } - - // cppcheck seems not to understand the abort-logic in pcmk__realloc - // cppcheck-suppress memleak - do { - int read_len = 0; - - buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1); - read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); - - if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) { - crm_trace("Read %ld bytes from file: %d", (long) read_len, rc); - length += read_len; - } - } while (rc == BZ_OK); - - rc = pcmk__bzlib2rc(rc); - if (rc != pcmk_rc_ok) { - rc = pcmk__bzlib2rc(rc); - crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", - filename, pcmk_rc_str(rc), rc); - free(buffer); - buffer = NULL; - } else { - buffer[length] = '\0'; - } - -done: - BZ2_bzReadClose(&rc, bz_file); - fclose(input); - return buffer; -} - /*! * \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: /* Remove it */ pcmk_free_xml_subtree(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } -// @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER -/*! - * \internal - * \brief Try to parse XML first without and then with recovery enabled - * - * \param[out] result Where to store the resulting XML doc (xmlDoc **) - * \param[in] fn XML parser function - * \param[in] ... All arguments for \p fn except the final one (an - * \c xmlParserOption group) - */ -#define parse_xml_recover(result, fn, ...) do { \ - *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); \ - if (*result == NULL) { \ - *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER); \ - \ - if (*result != NULL) { \ - crm_warn("Successfully recovered from XML errors " \ - "(note: a future release will treat this as a " \ - "fatal failure)"); \ - } \ - } \ - } while (0); - -/*! - * \internal - * \brief Parse XML from a file - * - * \param[in] filename Name of file containing XML (\c NULL or \c "-" for - * \c stdin); if \p filename ends in \c ".bz2", the file - * will be decompressed using \c bzip2 - * - * \return XML tree parsed from the given file; may be \c NULL or only partial - * on error - */ -xmlNode * -pcmk__xml_read(const char *filename) -{ - bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches); - xmlNode *xml = NULL; - xmlDoc *output = NULL; - xmlParserCtxt *ctxt = NULL; - const xmlError *last_error = NULL; - - // Create a parser context - ctxt = xmlNewParserCtxt(); - CRM_CHECK(ctxt != NULL, return NULL); - - xmlCtxtResetLastError(ctxt); - xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - - if (use_stdin) { - /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing - * stdin into a buffer and instead call - * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS); - * - * For now we have to save the input so that we can use it twice. - */ - char *input = read_stdin(); - - if (input != NULL) { - parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, - NULL, NULL); - free(input); - } - - } else if (pcmk__ends_with_ext(filename, ".bz2")) { - char *input = decompress_file(filename); - - if (input != NULL) { - parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, - NULL, NULL); - free(input); - } - - } else { - parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL); - } - - if (output != NULL) { - xml = xmlDocGetRootElement(output); - if (xml != NULL) { - /* @TODO Should we really be stripping out text? This seems like an - * overly broad way to get rid of whitespace, if that's the goal. - * Text nodes may be invalid in most or all Pacemaker inputs, but - * stripping them in a generic "parse XML from file" function may - * not be the best way to ignore them. - */ - pcmk__strip_xml_text(xml); - } - } - - // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error - last_error = xmlCtxtGetLastError(ctxt); - if (last_error != NULL) { - crm_err("Couldn't parse XML from %s", (use_stdin? "stdin": filename)); - - if (xml != NULL) { - crm_log_xml_info(xml, "Partial"); - } - } - - xmlFreeParserCtxt(ctxt); - return xml; -} - -/*! - * \internal - * \brief Parse XML from a string - * - * \param[in] input String to parse - * - * \return XML tree parsed from the given string; may be \c NULL or only partial - * on error - */ -xmlNode * -pcmk__xml_parse(const char *input) -{ - xmlNode *xml = NULL; - xmlDoc *output = NULL; - xmlParserCtxt *ctxt = NULL; - const xmlError *last_error = NULL; - - if (input == NULL) { - return NULL; - } - - ctxt = xmlNewParserCtxt(); - if (ctxt == NULL) { - return NULL; - } - - xmlCtxtResetLastError(ctxt); - xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - - parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, - NULL); - - if (output != NULL) { - xml = xmlDocGetRootElement(output); - } - - // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen - last_error = xmlCtxtGetLastError(ctxt); - if (last_error != NULL) { - crm_err("Couldn't parse XML from string: %s", input); - - if (xml != NULL) { - crm_log_xml_info(xml, "Partial"); - } - } - - xmlFreeParserCtxt(ctxt); - return xml; -} - /*! * \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; } /*! * \brief Sanitize a string so it is usable as an XML ID * * \param[in,out] id String to sanitize */ void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { /* @TODO Sanitize more comprehensively */ switch (*c) { case ':': case '#': *c = '.'; } } } /*! * \brief Set the ID of an XML element using a format * * \param[in,out] xml XML element * \param[in] fmt printf-style format * \param[in] ... any arguments required by format */ void crm_xml_set_id(xmlNode *xml, const char *format, ...) { va_list ap; int len = 0; char *id = NULL; /* equivalent to crm_strdup_printf() */ va_start(ap, format); len = vasprintf(&id, format, ap); va_end(ap); CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); crm_xml_add(xml, PCMK_XA_ID, id); free(id); } -/*! - * \internal - * \brief Write a string to a file stream, compressed using \c bzip2 - * - * \param[in] text String to write - * \param[in] filename Name of file being written (for logging only) - * \param[in,out] stream Open file stream to write to - * \param[out] bytes_out Number of bytes written (valid only on success) - * - * \return Standard Pacemaker return code - */ -static int -write_compressed_stream(char *text, const char *filename, FILE *stream, - unsigned int *bytes_out) -{ - unsigned int bytes_in = 0; - int bzerror = BZ_OK; - int rc = pcmk_rc_ok; - - // (5, 0, 0): (intermediate block size, silent, default workFactor) - BZFILE *bz_file = BZ2_bzWriteOpen(&bzerror, stream, 5, 0, 0); - - if (bzerror != BZ_OK) { - rc = pcmk__bzlib2rc(bzerror); - crm_warn("Not compressing %s: could not prepare file stream: %s " - CRM_XS " rc=%d", - filename, pcmk_rc_str(rc), rc); - goto done; - } - - BZ2_bzWrite(&bzerror, bz_file, text, strlen(text)); - if (bzerror != BZ_OK) { - rc = pcmk__bzlib2rc(bzerror); - crm_warn("Not compressing %s: could not compress data: %s " - CRM_XS " rc=%d errno=%d", - filename, pcmk_rc_str(rc), rc, errno); - goto done; - } - - BZ2_bzWriteClose(&bzerror, bz_file, 0, &bytes_in, bytes_out); - bz_file = NULL; - if (bzerror != BZ_OK) { - rc = pcmk__bzlib2rc(bzerror); - crm_warn("Not compressing %s: could not write compressed data: %s " - CRM_XS " rc=%d errno=%d", - filename, pcmk_rc_str(rc), rc, errno); - goto done; - } - - crm_trace("Compressed XML for %s from %u bytes to %u", - filename, bytes_in, *bytes_out); - -done: - if (bz_file != NULL) { - BZ2_bzWriteClose(&bzerror, bz_file, 0, NULL, NULL); - } - return rc; -} - -/*! - * \internal - * \brief Write XML to a file stream - * - * \param[in] xml XML to write - * \param[in] filename Name of file being written (for logging only) - * \param[in,out] stream Open file stream corresponding to filename (closed - * when this function returns) - * \param[in] compress Whether to compress XML before writing - * \param[out] nbytes Number of bytes written - * - * \return Standard Pacemaker return code - */ -static int -write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, - bool compress, unsigned int *nbytes) -{ - // @COMPAT Drop nbytes as arg when we drop write_xml_fd()/write_xml_file() - gchar *buffer = NULL; - unsigned int bytes_out = 0; - int rc = pcmk_rc_ok; - - buffer = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty); - CRM_CHECK(!pcmk__str_empty(buffer), - crm_log_xml_info(xml, "dump-failed"); - rc = pcmk_rc_error; - goto done); - - crm_log_xml_trace(xml, "writing"); - - if (compress - && (write_compressed_stream(buffer, filename, stream, - &bytes_out) == pcmk_rc_ok)) { - goto done; - } - - rc = fprintf(stream, "%s", buffer); - if (rc < 0) { - rc = EIO; - crm_perror(LOG_ERR, "writing %s", filename); - goto done; - } - bytes_out = (unsigned int) rc; - rc = pcmk_rc_ok; - -done: - if (fflush(stream) != 0) { - rc = errno; - crm_perror(LOG_ERR, "flushing %s", filename); - } - - // Don't report error if the file does not support synchronization - if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) { - rc = errno; - crm_perror(LOG_ERR, "synchronizing %s", filename); - } - - fclose(stream); - crm_trace("Saved %u bytes to %s as XML", bytes_out, filename); - - if (nbytes != NULL) { - *nbytes = bytes_out; - } - g_free(buffer); - return rc; -} - -/*! - * \internal - * \brief Write XML to a file descriptor - * - * \param[in] xml XML to write - * \param[in] filename Name of file being written (for logging only) - * \param[in] fd Open file descriptor corresponding to \p filename - * \param[in] compress If \c true, compress XML before writing - * \param[out] nbytes Number of bytes written (can be \c NULL) - * - * \return Standard Pacemaker return code - */ -int -pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd, - bool compress, unsigned int *nbytes) -{ - // @COMPAT Drop compress and nbytes arguments when we drop write_xml_fd() - FILE *stream = NULL; - - CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL); - stream = fdopen(fd, "w"); - if (stream == NULL) { - return errno; - } - - return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream, - compress, nbytes); -} - -/*! - * \internal - * \brief Write XML to a file - * - * \param[in] xml XML to write - * \param[in] filename Name of file to write - * \param[in] compress If \c true, compress XML before writing - * \param[out] nbytes Number of bytes written (can be \c NULL) - * - * \return Standard Pacemaker return code - */ -int -pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress, - unsigned int *nbytes) -{ - // @COMPAT Drop nbytes argument when we drop write_xml_fd() - FILE *stream = NULL; - - CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL); - stream = fopen(filename, "w"); - if (stream == NULL) { - return errno; - } - - return write_xml_stream(xml, filename, stream, compress, nbytes); -} - /*! * \internal * \brief Get consecutive bytes encoding non-ASCII UTF-8 characters * * \param[in] text String to check * * \return Number of non-ASCII UTF-8 bytes at the beginning of \p text */ static size_t utf8_bytes(const char *text) { // Total number of consecutive bytes containing UTF-8 characters size_t c_bytes = 0; if (text == NULL) { return 0; } /* UTF-8 uses one to four 8-bit bytes per character. The first byte * indicates the width of the character. A byte beginning with a '0' bit is * a one-byte ASCII character. * * A C byte is 8 bits on most systems, but this is not guaranteed. * * Count until we find an ASCII character or an invalid byte. Check bytes * aligned with the C byte boundary. */ for (const uint8_t *utf8_byte = (const uint8_t *) text; (*utf8_byte & 0x80) != 0; utf8_byte = (const uint8_t *) (text + c_bytes)) { size_t utf8_bits = 0; if ((*utf8_byte & 0xf0) == 0xf0) { // Four-byte character (first byte: 11110xxx) utf8_bits = 32; } else if ((*utf8_byte & 0xe0) == 0xe0) { // Three-byte character (first byte: 1110xxxx) utf8_bits = 24; } else if ((*utf8_byte & 0xc0) == 0xc0) { // Two-byte character (first byte: 110xxxxx) utf8_bits = 16; } else { crm_warn("Found invalid UTF-8 character %.2x", (unsigned char) *utf8_byte); return c_bytes; } c_bytes += utf8_bits / CHAR_BIT; #if (CHAR_BIT != 8) // Coverity complains about dead code without this CPP guard if ((utf8_bits % CHAR_BIT) > 0) { c_bytes++; } #endif // CHAR_BIT != 8 } return c_bytes; } /*! * \internal * \brief Replace a character in a dynamically allocated string, reallocating * memory * * \param[in,out] text String to replace a character in * \param[in,out] index Index of character to replace with new string; on * return, reset to index of end of replacement string * \param[in,out] length Length of \p text * \param[in] replace String to replace character at \p index with (must * not be empty) * * \return \p text, with the character at \p index replaced by \p replace */ static char * replace_text(char *text, size_t *index, size_t *length, const char *replace) { /* @TODO Replace with GString? Or at least copy char-by-char, escaping * characters as needed, instead of shifting characters on every replacement */ // We have space for 1 char already size_t offset = strlen(replace) - 1; if (offset > 0) { *length += offset; text = pcmk__realloc(text, *length + 1); // Shift characters to the right to make room for the replacement string for (size_t i = *length; i > (*index + offset); i--) { text[i] = text[i - offset]; } } // Replace the character at index by the replacement string memcpy(text + *index, replace, offset + 1); // Reset index to the end of replacement string *index += offset; return text; } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() for more details. * * \param[in] text String to check * \param[in] escape_quote If \c true, double quotes must be escaped * * \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, bool escape_quote) { size_t length = 0; if (text == NULL) { return false; } length = strlen(text); for (size_t index = 0; index < length; index++) { // Don't escape any non-ASCII characters index += utf8_bytes(&(text[index])); switch (text[index]) { case '\0': // Reached end of string by skipping UTF-8 bytes return false; case '<': return true; case '>': // Not necessary, but for symmetry with '<' return true; case '&': return true; case '"': if (escape_quote) { return true; } break; case '\n': case '\t': // Don't escape newline or tab break; default: if ((text[index] < 0x20) || (text[index] >= 0x7f)) { // Escape non-printing characters return true; } break; } } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * XML allows the escaping of special characters by replacing them with entity * references (for example, """) or character references (for * example, " "). * * The special characters '<' 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). * * Additionally, 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. * * 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 * * Pacemaker always delimits attribute values with double quotes, so this * function doesn't escape single quotes. * * \param[in] text Text to escape * \param[in] escape_quote If \c true, escape double quotes (should be enabled * for attribute values) * * \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 */ char * pcmk__xml_escape(const char *text, bool escape_quote) { size_t length = 0; char *copy = NULL; char buf[32] = { '\0', }; if (text == NULL) { return NULL; } length = strlen(text); pcmk__str_update(©, text); for (size_t index = 0; index < length; index++) { // Don't escape any non-ASCII characters index += utf8_bytes(&(copy[index])); switch (copy[index]) { case '\0': // Reached end of string by skipping UTF-8 bytes break; case '<': copy = replace_text(copy, &index, &length, "<"); break; case '>': // Not necessary, but for symmetry with '<' copy = replace_text(copy, &index, &length, ">"); break; case '&': copy = replace_text(copy, &index, &length, "&"); break; case '"': if (escape_quote) { copy = replace_text(copy, &index, &length, """); } break; case '\n': case '\t': // Don't escape newlines and tabs break; default: if ((copy[index] < 0x20) || (copy[index] >= 0x7f)) { // Escape non-printing characters snprintf(buf, sizeof(buf), "&#%.2x;", copy[index]); copy = replace_text(copy, &index, &length, buf); } break; } } return copy; } -/*! - * \internal - * \brief Append a string representation of an XML element to a buffer - * - * \param[in] data XML whose representation to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<", data->name, NULL); - - for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; - attr = attr->next) { - - if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { - pcmk__dump_xml_attr(attr, buffer); - } - } - - if (data->children == NULL) { - g_string_append(buffer, "/>"); - - } else { - g_string_append_c(buffer, '>'); - } - - if (pretty) { - g_string_append_c(buffer, '\n'); - } - - if (data->children) { - for (const xmlNode *child = data->children; child != NULL; - child = child->next) { - pcmk__xml2text(child, options, buffer, depth + 1); - } - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "name, ">", NULL); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } - } -} - -/*! - * \internal - * \brief Append XML text content to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p xml_log_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - const char *content = (const char *) data->content; - char *content_esc = NULL; - - if (pcmk__xml_needs_escape(content, false)) { - content_esc = pcmk__xml_escape(content, false); - content = content_esc; - } - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - g_string_append(buffer, content); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } - free(content_esc); -} - -/*! - * \internal - * \brief Append XML CDATA content to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "content, "]]>", - NULL); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } -} - -/*! - * \internal - * \brief Append an XML comment to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "", NULL); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } -} - -/*! - * \internal - * \brief Get a string representation of an XML element type - * - * \param[in] type XML element type - * - * \return String representation of \p type - */ -static const char * -xml_element_type2str(xmlElementType type) -{ - static const char *const element_type_names[] = { - [XML_ELEMENT_NODE] = "element", - [XML_ATTRIBUTE_NODE] = "attribute", - [XML_TEXT_NODE] = "text", - [XML_CDATA_SECTION_NODE] = "CDATA section", - [XML_ENTITY_REF_NODE] = "entity reference", - [XML_ENTITY_NODE] = "entity", - [XML_PI_NODE] = "PI", - [XML_COMMENT_NODE] = "comment", - [XML_DOCUMENT_NODE] = "document", - [XML_DOCUMENT_TYPE_NODE] = "document type", - [XML_DOCUMENT_FRAG_NODE] = "document fragment", - [XML_NOTATION_NODE] = "notation", - [XML_HTML_DOCUMENT_NODE] = "HTML document", - [XML_DTD_NODE] = "DTD", - [XML_ELEMENT_DECL] = "element declaration", - [XML_ATTRIBUTE_DECL] = "attribute declaration", - [XML_ENTITY_DECL] = "entity declaration", - [XML_NAMESPACE_DECL] = "namespace declaration", - [XML_XINCLUDE_START] = "XInclude start", - [XML_XINCLUDE_END] = "XInclude end", - }; - - if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { - return "unrecognized type"; - } - return element_type_names[type]; -} - -/*! - * \internal - * \brief Create a text representation of an XML object - * - * \param[in] data XML to convert - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to store the text (must not be \p NULL) - * \param[in] depth Current indentation level - */ -void -pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - if (data == NULL) { - crm_trace("Nothing to dump"); - return; - } - - CRM_ASSERT(buffer != NULL); - CRM_CHECK(depth >= 0, depth = 0); - - switch(data->type) { - case XML_ELEMENT_NODE: - /* Handle below */ - dump_xml_element(data, options, buffer, depth); - break; - case XML_TEXT_NODE: - if (pcmk_is_set(options, pcmk__xml_fmt_text)) { - dump_xml_text(data, options, buffer, depth); - } - break; - case XML_COMMENT_NODE: - dump_xml_comment(data, options, buffer, depth); - break; - case XML_CDATA_SECTION_NODE: - dump_xml_cdata(data, options, buffer, depth); - break; - default: - crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", - xml_element_type2str(data->type), data->type); - break; - } -} - -/*! - * \internal - * \brief Dump an XML tree to a string - * - * \param[in] xml XML tree to dump - * \param[in] flags Group of enum pcmk__xml_fmt_options flags - * - * \return Newly allocated string representation of \p xml - * - * \note The caller is responsible for freeing the return value using - * \c g_free(). - */ -gchar * -pcmk__xml_dump(const xmlNode *xml, uint32_t flags) -{ - /* libxml2's xmlNodeDumpOutput() doesn't allow filtering, doesn't escape - * special characters thoroughly, and doesn't allow a const argument. - * - * @COMPAT Can we start including text nodes unconditionally? - */ - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, flags, g_buffer, 0); - return g_string_free(g_buffer, FALSE); -} - -int -pcmk__xml2fd(int fd, xmlNode *cur) -{ - bool success; - - xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); - CRM_ASSERT(fd_out != NULL); - xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); - - success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; - - success = xmlOutputBufferClose(fd_out) != -1 && success; - - if (!success) { - return EIO; - } - - fsync(fd); - return pcmk_rc_ok; -} - void xml_remove_prop(xmlNode * obj, const char *name) { if (crm_element_value(obj, name) == NULL) { return; } if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) { crm_trace("Cannot remove %s from %s", name, obj->name); } else if (pcmk__tracking_xml_changes(obj, FALSE)) { /* Leave in place (marked for removal) until after the diff is calculated */ xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name); xml_node_private_t *nodepriv = attr->_private; set_parent_flag(obj, pcmk__xf_dirty); pcmk__set_xml_flags(nodepriv, pcmk__xf_deleted); } else { xmlUnsetProp(obj, (pcmkXmlStr) name); } } -void -save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) -{ - char *f = NULL; - - if (filename == NULL) { - char *uuid = crm_generate_uuid(); - - f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); - filename = f; - free(uuid); - } - - crm_info("Saving %s to %s", desc, filename); - pcmk__xml_write_file(xml, filename, false, NULL); - free(f); -} - /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; // Prevent the dirty flag being set recursively upwards pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); // Restore the old value (and the tracking flag) attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal xml_remove_prop(new_xml, attr_name); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); // Restore the original value xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute xmlUnsetProp(new_xml, new_attr->name); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children reset_xml_node_flags(candidate); // 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 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 *cIter = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { pcmk__mark_xml_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 (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) { xmlNode *old_child = cIter; xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true); cIter = pcmk__xml_next(cIter); if(new_child) { mark_xml_changes(old_child, new_child, TRUE); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) { xmlNode *new_child = cIter; xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true); cIter = pcmk__xml_next(cIter); if(old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); 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); } } } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { xml_track_changes(new_xml, NULL, NULL, FALSE); } mark_xml_changes(old_xml, new_xml, FALSE); } /*! * \internal * \brief Find a comment with matching content in specified XML * * \param[in] root XML to search * \param[in] search_comment Comment whose content should be searched for * \param[in] exact If true, comment must also be at same position */ xmlNode * pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) { xmlNode *a_child = NULL; int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); for (a_child = pcmk__xml_first_child(root); a_child != NULL; a_child = pcmk__xml_next(a_child)) { if (exact) { int offset = pcmk__xml_position(a_child, pcmk__xf_skip); xml_node_private_t *nodepriv = a_child->_private; if (offset < search_offset) { continue; } else if (offset > search_offset) { return NULL; } if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { continue; } } if (a_child->type == XML_COMMENT_NODE && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { return a_child; } else if (exact) { return NULL; } } return NULL; } /*! * \internal * \brief Make one XML comment match another (in content) * * \param[in,out] parent If \p target is NULL and this is not, add or update * comment child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML comment node * \param[in] update Make comment content match this (must not be NULL) * * \note At least one of \parent and \target must be non-NULL */ void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) { CRM_CHECK(update != NULL, return); CRM_CHECK(update->type == XML_COMMENT_NODE, return); if (target == NULL) { target = pcmk__xc_match(parent, update, false); } if (target == NULL) { pcmk__xml_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); target->content = xmlStrdup(update->content); } } /*! * \internal * \brief Make one XML tree match another (in children and attributes) * * \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 NULL) * \param[in] as_diff If false, expand "++" when making attributes match * * \note At least one of \p parent and \p target must be non-NULL */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, bool as_diff) { xmlNode *a_child = NULL; const char *object_name = NULL, *object_href = NULL, *object_href_val = NULL; #if XML_PARSER_DEBUG crm_log_xml_trace(update, "update:"); crm_log_xml_trace(target, "target:"); #endif CRM_CHECK(update != NULL, return); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); return; } object_name = (const char *) update->name; object_href_val = pcmk__xe_id(update); if (object_href_val != NULL) { object_href = PCMK_XA_ID; } else { object_href_val = crm_element_value(update, PCMK_XA_ID_REF); object_href = (object_href_val == NULL)? NULL : PCMK_XA_ID_REF; } CRM_CHECK(object_name != NULL, return); CRM_CHECK(target != NULL || parent != NULL, return); if (target == NULL) { target = pcmk__xe_match(parent, object_name, object_href, object_href_val); } if (target == NULL) { target = create_xml_node(parent, object_name); CRM_CHECK(target != NULL, return); #if XML_PARSER_DEBUG crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); } else { crm_trace("Found node <%s%s%s%s%s/> to update", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); if (as_diff == FALSE) { /* So that expand_plus_plus() gets called */ copy_in_properties(target, update); } else { /* No need for expand_plus_plus(), just raw speed */ for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); /* Remove it first so the ordering of the update is preserved */ xmlUnsetProp(target, a->name); xmlSetProp(target, a->name, (pcmkXmlStr) p_value); } } for (a_child = pcmk__xml_first_child(update); a_child != NULL; a_child = pcmk__xml_next(a_child)) { #if XML_PARSER_DEBUG crm_trace("Updating child <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif pcmk__xml_update(target, NULL, a_child, as_diff); } #if XML_PARSER_DEBUG crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, ""), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } gboolean update_xml_child(xmlNode * child, xmlNode * to_update) { gboolean can_update = TRUE; xmlNode *child_of_child = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(to_update != NULL, return FALSE); if (!pcmk__xe_is(to_update, (const char *) child->name)) { can_update = FALSE; } else if (!pcmk__str_eq(pcmk__xe_id(to_update), pcmk__xe_id(child), pcmk__str_none)) { can_update = FALSE; } else if (can_update) { #if XML_PARSER_DEBUG crm_log_xml_trace(child, "Update match found..."); #endif pcmk__xml_update(NULL, child, to_update, false); } for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL; child_of_child = pcmk__xml_next(child_of_child)) { /* only update the first one */ if (can_update) { break; } can_update = update_xml_child(child_of_child, to_update); } return can_update; } int find_xml_children(xmlNode ** children, xmlNode * root, const char *tag, const char *field, const char *value, gboolean search_matches) { int match_found = 0; CRM_CHECK(root != NULL, return FALSE); CRM_CHECK(children != NULL, return FALSE); if ((tag != NULL) && !pcmk__xe_is(root, tag)) { } else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) { } else { if (*children == NULL) { *children = create_xml_node(NULL, __func__); } pcmk__xml_copy(*children, root); match_found = 1; } if (search_matches || match_found == 0) { xmlNode *child = NULL; for (child = pcmk__xml_first_child(root); child != NULL; child = pcmk__xml_next(child)) { match_found += find_xml_children(children, child, tag, field, value, search_matches); } } return match_found; } gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) { gboolean can_delete = FALSE; xmlNode *child_of_child = NULL; const char *up_id = NULL; const char *child_id = NULL; const char *right_val = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(update != NULL, return FALSE); up_id = pcmk__xe_id(update); child_id = pcmk__xe_id(child); if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { can_delete = TRUE; } if (!pcmk__xe_is(update, (const char *) child->name)) { can_delete = FALSE; } if (can_delete && delete_only) { for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_name = (const char *) a->name; const char *p_value = pcmk__xml_attr_value(a); right_val = crm_element_value(child, p_name); if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) { can_delete = FALSE; } } } if (can_delete && parent != NULL) { crm_log_xml_trace(child, "Delete match found..."); if (delete_only || update == NULL) { free_xml(child); } else { xmlNode *old = child; xmlNode *new = xmlCopyNode(update, 1); CRM_ASSERT(new != NULL); // May be unnecessary but avoids slight changes to some test outputs reset_xml_node_flags(new); old = xmlReplaceNode(old, new); if (xml_tracking_changes(new)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); xmlFreeNode(old); } return TRUE; } else if (can_delete) { crm_log_xml_debug(child, "Cannot delete the search root"); can_delete = FALSE; } child_of_child = pcmk__xml_first_child(child); while (child_of_child) { xmlNode *next = pcmk__xml_next(child_of_child); can_delete = replace_xml_child(child, child_of_child, update, delete_only); /* only delete the first one */ if (can_delete) { child_of_child = NULL; } else { child_of_child = next; } } return can_delete; } 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 = create_xml_node(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__xml_first_child(input); child != NULL; child = pcmk__xml_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } xmlNode * first_named_child(const xmlNode *parent, const char *name) { xmlNode *match = NULL; for (match = pcmk__xe_first_child(parent); match != NULL; match = pcmk__xe_next(match)) { /* * name == NULL gives first child regardless of name; this is * semantically incorrect in this function, but may be necessary * due to prior use of xml_child_iter_filter */ if ((name == NULL) || pcmk__xe_is(match, name)) { return match; } } return NULL; } /*! * \brief Get next instance of same XML tag * * \param[in] sibling XML tag to start from * * \return Next sibling XML tag with same name */ xmlNode * crm_next_same_xml(const xmlNode *sibling) { xmlNode *match = pcmk__xe_next(sibling); while (match != NULL) { if (pcmk__xe_is(match, (const char *) sibling->name)) { return match; } match = pcmk__xe_next(match); } return NULL; } void crm_xml_init(void) { static bool init = true; if(init) { init = false; /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many * pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds) * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in * less than 1 second. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); /* Populate and free the _private field when nodes are created and destroyed */ xmlDeregisterNodeDefault(free_private_data); xmlRegisterNodeDefault(new_private_data); crm_schema_init(); } } void crm_xml_cleanup(void) { crm_schema_cleanup(); xmlCleanupParser(); } #define XPATH_MAX 512 xmlNode * expand_idref(xmlNode * input, xmlNode * top) { char *xpath = NULL; const char *ref = NULL; xmlNode *result = NULL; if (input == NULL) { return NULL; } ref = crm_element_value(input, PCMK_XA_ID_REF); if (ref == NULL) { return input; } if (top == NULL) { top = input; } xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref); result = get_xpath_object(xpath, top, LOG_DEBUG); if (result == NULL) { // Not possible with schema validation enabled pcmk__config_err("Ignoring invalid %s configuration: " PCMK_XA_ID_REF " '%s' does not reference " "a valid object " CRM_XS " xpath=%s", input->name, ref, xpath); } free(xpath); return result; } 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 = CRM_SCHEMA_DIRECTORY; } 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(); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) { while (true) { const char *name, *value; name = va_arg(pairs, const char *); if (name == NULL) { return; } value = va_arg(pairs, const char *); if (value != NULL) { crm_xml_add(node, name, value); } } } void pcmk__xe_set_props(xmlNodePtr node, ...) { va_list pairs; va_start(pairs, node); pcmk__xe_set_propv(node, pairs); va_end(pairs); } int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata) { xmlNode *children = (xml? xml->children : NULL); CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && ((child_element_name == NULL) || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include xmlNode * find_entity(xmlNode *parent, const char *node_name, const char *id) { return pcmk__xe_match(parent, node_name, ((id == NULL)? id : PCMK_XA_ID), id); } void crm_destroy_xml(gpointer data) { free_xml(data); } xmlDoc * getDocPtr(xmlNode *node) { xmlDoc *doc = NULL; CRM_CHECK(node != NULL, return NULL); doc = node->doc; if (doc == NULL) { doc = xmlNewDoc(PCMK__XML_VERSION); xmlDocSetRootElement(doc, node); } return doc; } xmlNode * add_node_copy(xmlNode *parent, xmlNode *src_node) { xmlNode *child = NULL; CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); child = xmlDocCopyNode(src_node, parent->doc, 1); if (child == NULL) { return NULL; } xmlAddChild(parent, child); pcmk__mark_xml_created(child); return child; } int add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) { add_node_copy(parent, child); free_xml(child); return 1; } gboolean xml_has_children(const xmlNode * xml_root) { if (xml_root != NULL && xml_root->children != NULL) { return TRUE; } return FALSE; } char * crm_xml_escape(const char *text) { size_t length = 0; char *copy = NULL; if (text == NULL) { return NULL; } length = strlen(text); copy = strdup(text); CRM_ASSERT(copy != NULL); for (size_t index = 0; index <= length; index++) { if(copy[index] & 0x80 && copy[index+1] & 0x80){ index++; continue; } switch (copy[index]) { case 0: // Sanity only; loop should stop at the last non-null byte break; case '<': copy = replace_text(copy, &index, &length, "<"); break; case '>': copy = replace_text(copy, &index, &length, ">"); break; case '"': copy = replace_text(copy, &index, &length, """); break; case '\'': copy = replace_text(copy, &index, &length, "'"); break; case '&': copy = replace_text(copy, &index, &length, "&"); break; case '\t': /* Might as well just expand to a few spaces... */ copy = replace_text(copy, &index, &length, " "); break; case '\n': copy = replace_text(copy, &index, &length, "\\n"); break; case '\r': copy = replace_text(copy, &index, &length, "\\r"); break; default: /* Check for and replace non-printing characters with their octal equivalent */ if(copy[index] < ' ' || copy[index] > '~') { char *replace = crm_strdup_printf("\\%.3o", copy[index]); copy = replace_text(copy, &index, &length, replace); free(replace); } } } return copy; } xmlNode * copy_xml(xmlNode *src) { xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); xmlNode *copy = xmlDocCopyNode(src, doc, 1); CRM_ASSERT(copy != NULL); xmlDocSetRootElement(doc, copy); return copy; } -xmlNode * -filename2xml(const char *filename) -{ - return pcmk__xml_read(filename); -} - -xmlNode * -stdin2xml(void) -{ - return pcmk__xml_read(NULL); -} - -xmlNode * -string2xml(const char *input) -{ - return pcmk__xml_parse(input); -} - -int -write_xml_fd(const xmlNode *xml, const char *filename, int fd, - gboolean compress) -{ - unsigned int nbytes = 0; - int rc = pcmk__xml_write_fd(xml, filename, fd, compress, &nbytes); - - if (rc != pcmk_rc_ok) { - return pcmk_rc2legacy(rc); - } - return (int) nbytes; -} - -int -write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) -{ - unsigned int nbytes = 0; - int rc = pcmk__xml_write_file(xml, filename, compress, &nbytes); - - if (rc != pcmk_rc_ok) { - return pcmk_rc2legacy(rc); - } - return (int) nbytes; -} - -char * -dump_xml_formatted(const xmlNode *xml) -{ - char *str = NULL; - gchar *g_str = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty); - - pcmk__str_update(&str, g_str); - g_free(g_str); - return str; -} - -char * -dump_xml_formatted_with_text(const xmlNode *xml) -{ - char *str = NULL; - gchar *g_str = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text); - - pcmk__str_update(&str, g_str); - g_free(g_str); - return str; -} - -char * -dump_xml_unformatted(const xmlNode *xml) -{ - char *str = NULL; - gchar *g_str = pcmk__xml_dump(xml, 0); - - pcmk__str_update(&str, g_str); - g_free(g_str); - return str; -} - // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c new file mode 100644 index 0000000000..075064407e --- /dev/null +++ b/lib/common/xml_io.c @@ -0,0 +1,859 @@ +/* + * Copyright 2004-2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include // xmlOutputBuffer* + +#include +#include +#include +#include "crmcommon_private.h" + +/* @COMPAT XML_PARSE_RECOVER allows some XML errors to be silently worked around + * by libxml2, which is potentially ambiguous and dangerous. We should drop it + * when we can break backward compatibility with configurations that might be + * relying on it (i.e. pacemaker 3.0.0). + */ +#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) +#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS \ + |XML_PARSE_RECOVER) + +/*! + * \internal + * \brief Read from \c stdin until EOF or error + * + * \return Newly allocated string containing the bytes read from \c stdin, or + * \c NULL on error + * + * \note The caller is responsible for freeing the return value using \c free(). + */ +static char * +read_stdin(void) +{ + char *buf = NULL; + size_t length = 0; + + do { + buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1); + length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin); + } while ((feof(stdin) == 0) && (ferror(stdin) == 0)); + + if (ferror(stdin) != 0) { + crm_err("Error reading input from stdin"); + free(buf); + buf = NULL; + } else { + buf[length] = '\0'; + } + clearerr(stdin); + return buf; +} + +/*! + * \internal + * \brief Decompress a bzip2-compressed file into a string buffer + * + * \param[in] filename Name of file to decompress + * + * \return Newly allocated string with the decompressed contents of \p filename, + * or \c NULL on error. + * + * \note The caller is responsible for freeing the return value using \c free(). + */ +static char * +decompress_file(const char *filename) +{ + char *buffer = NULL; + int rc = pcmk_rc_ok; + size_t length = 0; + BZFILE *bz_file = NULL; + FILE *input = fopen(filename, "r"); + + if (input == NULL) { + crm_perror(LOG_ERR, "Could not open %s for reading", filename); + return NULL; + } + + bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + crm_err("Could not prepare to read compressed %s: %s " + CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); + goto done; + } + + // cppcheck seems not to understand the abort-logic in pcmk__realloc + // cppcheck-suppress memleak + do { + int read_len = 0; + + buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1); + read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); + + if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) { + crm_trace("Read %ld bytes from file: %d", (long) read_len, rc); + length += read_len; + } + } while (rc == BZ_OK); + + rc = pcmk__bzlib2rc(rc); + if (rc != pcmk_rc_ok) { + rc = pcmk__bzlib2rc(rc); + crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", + filename, pcmk_rc_str(rc), rc); + free(buffer); + buffer = NULL; + } else { + buffer[length] = '\0'; + } + +done: + BZ2_bzReadClose(&rc, bz_file); + fclose(input); + return buffer; +} + +// @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER +/*! + * \internal + * \brief Try to parse XML first without and then with recovery enabled + * + * \param[out] result Where to store the resulting XML doc (xmlDoc **) + * \param[in] fn XML parser function + * \param[in] ... All arguments for \p fn except the final one (an + * \c xmlParserOption group) + */ +#define parse_xml_recover(result, fn, ...) do { \ + *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); \ + if (*result == NULL) { \ + *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER); \ + \ + if (*result != NULL) { \ + crm_warn("Successfully recovered from XML errors " \ + "(note: a future release will treat this as a " \ + "fatal failure)"); \ + } \ + } \ + } while (0); + +/*! + * \internal + * \brief Parse XML from a file + * + * \param[in] filename Name of file containing XML (\c NULL or \c "-" for + * \c stdin); if \p filename ends in \c ".bz2", the file + * will be decompressed using \c bzip2 + * + * \return XML tree parsed from the given file; may be \c NULL or only partial + * on error + */ +xmlNode * +pcmk__xml_read(const char *filename) +{ + bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches); + xmlNode *xml = NULL; + xmlDoc *output = NULL; + xmlParserCtxt *ctxt = NULL; + const xmlError *last_error = NULL; + + // Create a parser context + ctxt = xmlNewParserCtxt(); + CRM_CHECK(ctxt != NULL, return NULL); + + xmlCtxtResetLastError(ctxt); + xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); + + if (use_stdin) { + /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing + * stdin into a buffer and instead call + * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS); + * + * For now we have to save the input so that we can use it twice. + */ + char *input = read_stdin(); + + if (input != NULL) { + parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, + NULL, NULL); + free(input); + } + + } else if (pcmk__ends_with_ext(filename, ".bz2")) { + char *input = decompress_file(filename); + + if (input != NULL) { + parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, + NULL, NULL); + free(input); + } + + } else { + parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL); + } + + if (output != NULL) { + xml = xmlDocGetRootElement(output); + if (xml != NULL) { + /* @TODO Should we really be stripping out text? This seems like an + * overly broad way to get rid of whitespace, if that's the goal. + * Text nodes may be invalid in most or all Pacemaker inputs, but + * stripping them in a generic "parse XML from file" function may + * not be the best way to ignore them. + */ + pcmk__strip_xml_text(xml); + } + } + + // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error + last_error = xmlCtxtGetLastError(ctxt); + if (last_error != NULL) { + crm_err("Couldn't parse XML from %s", (use_stdin? "stdin": filename)); + + if (xml != NULL) { + crm_log_xml_info(xml, "Partial"); + } + } + + xmlFreeParserCtxt(ctxt); + return xml; +} + +/*! + * \internal + * \brief Parse XML from a string + * + * \param[in] input String to parse + * + * \return XML tree parsed from the given string; may be \c NULL or only partial + * on error + */ +xmlNode * +pcmk__xml_parse(const char *input) +{ + xmlNode *xml = NULL; + xmlDoc *output = NULL; + xmlParserCtxt *ctxt = NULL; + const xmlError *last_error = NULL; + + if (input == NULL) { + return NULL; + } + + ctxt = xmlNewParserCtxt(); + if (ctxt == NULL) { + return NULL; + } + + xmlCtxtResetLastError(ctxt); + xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); + + parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, + NULL); + + if (output != NULL) { + xml = xmlDocGetRootElement(output); + } + + // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen + last_error = xmlCtxtGetLastError(ctxt); + if (last_error != NULL) { + crm_err("Couldn't parse XML from string: %s", input); + + if (xml != NULL) { + crm_log_xml_info(xml, "Partial"); + } + } + + xmlFreeParserCtxt(ctxt); + return xml; +} + +/*! + * \internal + * \brief Append a string representation of an XML element to a buffer + * + * \param[in] data XML whose representation to append + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); + int spaces = pretty? (2 * depth) : 0; + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "<", data->name, NULL); + + for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; + attr = attr->next) { + + if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { + pcmk__dump_xml_attr(attr, buffer); + } + } + + if (data->children == NULL) { + g_string_append(buffer, "/>"); + + } else { + g_string_append_c(buffer, '>'); + } + + if (pretty) { + g_string_append_c(buffer, '\n'); + } + + if (data->children) { + for (const xmlNode *child = data->children; child != NULL; + child = child->next) { + pcmk__xml2text(child, options, buffer, depth + 1); + } + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "name, ">", NULL); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } + } +} + +/*! + * \internal + * \brief Append XML text content to a buffer + * + * \param[in] data XML whose content to append + * \param[in] options Group of \p xml_log_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + const char *content = (const char *) data->content; + char *content_esc = NULL; + + if (pcmk__xml_needs_escape(content, false)) { + content_esc = pcmk__xml_escape(content, false); + content = content_esc; + } + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + g_string_append(buffer, content); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } + free(content_esc); +} + +/*! + * \internal + * \brief Append XML CDATA content to a buffer + * + * \param[in] data XML whose content to append + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "content, "]]>", + NULL); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } +} + +/*! + * \internal + * \brief Append an XML comment to a buffer + * + * \param[in] data XML whose content to append + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to append the content (must not be \p NULL) + * \param[in] depth Current indentation level + */ +static void +dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); + int spaces = pretty? (2 * depth) : 0; + + for (int lpc = 0; lpc < spaces; lpc++) { + g_string_append_c(buffer, ' '); + } + + pcmk__g_strcat(buffer, "", NULL); + + if (pretty) { + g_string_append_c(buffer, '\n'); + } +} + +/*! + * \internal + * \brief Get a string representation of an XML element type + * + * \param[in] type XML element type + * + * \return String representation of \p type + */ +static const char * +xml_element_type2str(xmlElementType type) +{ + static const char *const element_type_names[] = { + [XML_ELEMENT_NODE] = "element", + [XML_ATTRIBUTE_NODE] = "attribute", + [XML_TEXT_NODE] = "text", + [XML_CDATA_SECTION_NODE] = "CDATA section", + [XML_ENTITY_REF_NODE] = "entity reference", + [XML_ENTITY_NODE] = "entity", + [XML_PI_NODE] = "PI", + [XML_COMMENT_NODE] = "comment", + [XML_DOCUMENT_NODE] = "document", + [XML_DOCUMENT_TYPE_NODE] = "document type", + [XML_DOCUMENT_FRAG_NODE] = "document fragment", + [XML_NOTATION_NODE] = "notation", + [XML_HTML_DOCUMENT_NODE] = "HTML document", + [XML_DTD_NODE] = "DTD", + [XML_ELEMENT_DECL] = "element declaration", + [XML_ATTRIBUTE_DECL] = "attribute declaration", + [XML_ENTITY_DECL] = "entity declaration", + [XML_NAMESPACE_DECL] = "namespace declaration", + [XML_XINCLUDE_START] = "XInclude start", + [XML_XINCLUDE_END] = "XInclude end", + }; + + if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { + return "unrecognized type"; + } + return element_type_names[type]; +} + +/*! + * \internal + * \brief Create a text representation of an XML object + * + * \param[in] data XML to convert + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * \param[in,out] buffer Where to store the text (must not be \p NULL) + * \param[in] depth Current indentation level + */ +void +pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, + int depth) +{ + if (data == NULL) { + crm_trace("Nothing to dump"); + return; + } + + CRM_ASSERT(buffer != NULL); + CRM_CHECK(depth >= 0, depth = 0); + + switch(data->type) { + case XML_ELEMENT_NODE: + /* Handle below */ + dump_xml_element(data, options, buffer, depth); + break; + case XML_TEXT_NODE: + if (pcmk_is_set(options, pcmk__xml_fmt_text)) { + dump_xml_text(data, options, buffer, depth); + } + break; + case XML_COMMENT_NODE: + dump_xml_comment(data, options, buffer, depth); + break; + case XML_CDATA_SECTION_NODE: + dump_xml_cdata(data, options, buffer, depth); + break; + default: + crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", + xml_element_type2str(data->type), data->type); + break; + } +} + +/*! + * \internal + * \brief Dump an XML tree to a string + * + * \param[in] xml XML tree to dump + * \param[in] flags Group of enum pcmk__xml_fmt_options flags + * + * \return Newly allocated string representation of \p xml + * + * \note The caller is responsible for freeing the return value using + * \c g_free(). + */ +gchar * +pcmk__xml_dump(const xmlNode *xml, uint32_t flags) +{ + /* libxml2's xmlNodeDumpOutput() doesn't allow filtering, doesn't escape + * special characters thoroughly, and doesn't allow a const argument. + * + * @COMPAT Can we start including text nodes unconditionally? + */ + GString *g_buffer = g_string_sized_new(1024); + + pcmk__xml2text(xml, flags, g_buffer, 0); + return g_string_free(g_buffer, FALSE); +} + +/*! + * \internal + * \brief Write a string to a file stream, compressed using \c bzip2 + * + * \param[in] text String to write + * \param[in] filename Name of file being written (for logging only) + * \param[in,out] stream Open file stream to write to + * \param[out] bytes_out Number of bytes written (valid only on success) + * + * \return Standard Pacemaker return code + */ +static int +write_compressed_stream(char *text, const char *filename, FILE *stream, + unsigned int *bytes_out) +{ + unsigned int bytes_in = 0; + int bzerror = BZ_OK; + int rc = pcmk_rc_ok; + + // (5, 0, 0): (intermediate block size, silent, default workFactor) + BZFILE *bz_file = BZ2_bzWriteOpen(&bzerror, stream, 5, 0, 0); + + if (bzerror != BZ_OK) { + rc = pcmk__bzlib2rc(bzerror); + crm_warn("Not compressing %s: could not prepare file stream: %s " + CRM_XS " rc=%d", + filename, pcmk_rc_str(rc), rc); + goto done; + } + + BZ2_bzWrite(&bzerror, bz_file, text, strlen(text)); + if (bzerror != BZ_OK) { + rc = pcmk__bzlib2rc(bzerror); + crm_warn("Not compressing %s: could not compress data: %s " + CRM_XS " rc=%d errno=%d", + filename, pcmk_rc_str(rc), rc, errno); + goto done; + } + + BZ2_bzWriteClose(&bzerror, bz_file, 0, &bytes_in, bytes_out); + bz_file = NULL; + if (bzerror != BZ_OK) { + rc = pcmk__bzlib2rc(bzerror); + crm_warn("Not compressing %s: could not write compressed data: %s " + CRM_XS " rc=%d errno=%d", + filename, pcmk_rc_str(rc), rc, errno); + goto done; + } + + crm_trace("Compressed XML for %s from %u bytes to %u", + filename, bytes_in, *bytes_out); + +done: + if (bz_file != NULL) { + BZ2_bzWriteClose(&bzerror, bz_file, 0, NULL, NULL); + } + return rc; +} + +/*! + * \internal + * \brief Write XML to a file stream + * + * \param[in] xml XML to write + * \param[in] filename Name of file being written (for logging only) + * \param[in,out] stream Open file stream corresponding to filename (closed + * when this function returns) + * \param[in] compress Whether to compress XML before writing + * \param[out] nbytes Number of bytes written + * + * \return Standard Pacemaker return code + */ +static int +write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, + bool compress, unsigned int *nbytes) +{ + // @COMPAT Drop nbytes as arg when we drop write_xml_fd()/write_xml_file() + gchar *buffer = NULL; + unsigned int bytes_out = 0; + int rc = pcmk_rc_ok; + + buffer = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty); + CRM_CHECK(!pcmk__str_empty(buffer), + crm_log_xml_info(xml, "dump-failed"); + rc = pcmk_rc_error; + goto done); + + crm_log_xml_trace(xml, "writing"); + + if (compress + && (write_compressed_stream(buffer, filename, stream, + &bytes_out) == pcmk_rc_ok)) { + goto done; + } + + rc = fprintf(stream, "%s", buffer); + if (rc < 0) { + rc = EIO; + crm_perror(LOG_ERR, "writing %s", filename); + goto done; + } + bytes_out = (unsigned int) rc; + rc = pcmk_rc_ok; + +done: + if (fflush(stream) != 0) { + rc = errno; + crm_perror(LOG_ERR, "flushing %s", filename); + } + + // Don't report error if the file does not support synchronization + if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) { + rc = errno; + crm_perror(LOG_ERR, "synchronizing %s", filename); + } + + fclose(stream); + crm_trace("Saved %u bytes to %s as XML", bytes_out, filename); + + if (nbytes != NULL) { + *nbytes = bytes_out; + } + g_free(buffer); + return rc; +} + +/*! + * \internal + * \brief Write XML to a file descriptor + * + * \param[in] xml XML to write + * \param[in] filename Name of file being written (for logging only) + * \param[in] fd Open file descriptor corresponding to \p filename + * \param[in] compress If \c true, compress XML before writing + * \param[out] nbytes Number of bytes written (can be \c NULL) + * + * \return Standard Pacemaker return code + */ +int +pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd, + bool compress, unsigned int *nbytes) +{ + // @COMPAT Drop compress and nbytes arguments when we drop write_xml_fd() + FILE *stream = NULL; + + CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL); + stream = fdopen(fd, "w"); + if (stream == NULL) { + return errno; + } + + return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream, + compress, nbytes); +} + +/*! + * \internal + * \brief Write XML to a file + * + * \param[in] xml XML to write + * \param[in] filename Name of file to write + * \param[in] compress If \c true, compress XML before writing + * \param[out] nbytes Number of bytes written (can be \c NULL) + * + * \return Standard Pacemaker return code + */ +int +pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress, + unsigned int *nbytes) +{ + // @COMPAT Drop nbytes argument when we drop write_xml_fd() + FILE *stream = NULL; + + CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL); + stream = fopen(filename, "w"); + if (stream == NULL) { + return errno; + } + + return write_xml_stream(xml, filename, stream, compress, nbytes); +} + +/*! + * \internal + * \brief Serialize XML (using libxml) into provided descriptor + * + * \param[in] fd File descriptor to (piece-wise) write to + * \param[in] cur XML subtree to proceed + * + * \return a standard Pacemaker return code + */ +int +pcmk__xml2fd(int fd, xmlNode *cur) +{ + bool success; + + xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); + CRM_ASSERT(fd_out != NULL); + xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); + + success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; + + success = xmlOutputBufferClose(fd_out) != -1 && success; + + if (!success) { + return EIO; + } + + fsync(fd); + return pcmk_rc_ok; +} + +void +save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) +{ + char *f = NULL; + + if (filename == NULL) { + char *uuid = crm_generate_uuid(); + + f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); + filename = f; + free(uuid); + } + + crm_info("Saving %s to %s", desc, filename); + pcmk__xml_write_file(xml, filename, false, NULL); + free(f); +} + + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +xmlNode * +filename2xml(const char *filename) +{ + return pcmk__xml_read(filename); +} + +xmlNode * +stdin2xml(void) +{ + return pcmk__xml_read(NULL); +} + +xmlNode * +string2xml(const char *input) +{ + return pcmk__xml_parse(input); +} + +char * +dump_xml_formatted(const xmlNode *xml) +{ + char *str = NULL; + gchar *g_str = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty); + + pcmk__str_update(&str, g_str); + g_free(g_str); + return str; +} + +char * +dump_xml_formatted_with_text(const xmlNode *xml) +{ + char *str = NULL; + gchar *g_str = pcmk__xml_dump(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text); + + pcmk__str_update(&str, g_str); + g_free(g_str); + return str; +} + +char * +dump_xml_unformatted(const xmlNode *xml) +{ + char *str = NULL; + gchar *g_str = pcmk__xml_dump(xml, 0); + + pcmk__str_update(&str, g_str); + g_free(g_str); + return str; +} + +int +write_xml_fd(const xmlNode *xml, const char *filename, int fd, + gboolean compress) +{ + unsigned int nbytes = 0; + int rc = pcmk__xml_write_fd(xml, filename, fd, compress, &nbytes); + + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + return (int) nbytes; +} + +int +write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) +{ + unsigned int nbytes = 0; + int rc = pcmk__xml_write_file(xml, filename, compress, &nbytes); + + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + return (int) nbytes; +} + +// LCOV_EXCL_STOP +// End deprecated API